The last few days I've been working on a second version of my TMS9900 arcade game library, called SPECTRA2.
Compared to the original SPECTRA1 library I wrote in 2009, I'm taking a different approach this time. I see the library as a miniature operating system for running games. It will help the processing of input (joystick, keyboard) and output (video, sound and speech). A core concept is the use of timers for running multiple tasks. Ofcourse it will also include some utility functions that help in the communication with the VDP, setting up timers etc.
Therefore I have decided that I won't be including many super duper high-level VDP tricks in the library. Why? Because I think such high-level routines put a lot of restrictions on the programmer. What I like about assembly language is that -as a programmer- you have full control. So I don't want my library to put too many constraints on the programmer. Instead it should offer a basic structure, help master basic functions and leave the game programmer the freedom to do what he needs to do; concentrate on the game.
The goal is to implement a runtime that doesn't take more than 2 kilobytes of ROM space and that is able to handle sound, speech, keyboard & joystick pulling, VDP communication, timers and sprite movement. Also RAM requirements are very moderate. The target platform is an unexpanded TI-99/4A which means we only have 256 bytes of scratch-pad memory to our disposal.
If you want to learn more about the original SPECTRA1 manual, then please check here.
Below are the key features I have in mind for SPECTRA2:
- Stack usage
- No need to save all registers. A lot of pushes/pops can be avoided by defining certain registers as "temporary" registers. This saves CPU cycles, memory and ROM space.
- Scratch-pad memory layout
- Taking a byte oriented approach where possible for saving on scratch pad memory. A small memory area is reserved for self modifying code that is used to optimize loops and writes to/from VDP, bank-switch trampoline code, etc.
- Task scheduler
- Reduced memory usage for timers. Now use 4 bytes instead of 8 bytes for a timer slot. It's also possible to have multiple timer tables and the scheduler itself offers some more possibilities. The task scheduler is at the core of the library and plays an important role as a lot of functionality will depend on it.
- Support for bank-switching
- Support for using "Monitor" OS routines and disk+file I/O support
- Consistent function labels, parameter passing, etc.
Scratch-pad memory layout
The main focus of the library is to support games that run from the cartridge space on a bare TI-99/4A console. In that case we only have 256 bytes of CPU RAM (scratch-pad memory) to our disposal. If you think about it as a mini-os, then it's clear it also needs some memory for its own. I've set the limit (including register workspace) to 64 bytes. That is 25% of the available memory. We're currently using 50 bytes. Is that a good value? I don't know, we'll find out soon enough.
Nonetheless, I guess that the memory layout pretty much has stabilized now. At least for a basic installation. By that I mean that each game or program using spectra2 must use the range >8300 - >8333 as described. I call this range "memory area 1".
There might be a small "memory area 2" in the future. This setting will be controlled by one of the flags in (D). Reason for that is that some of the routines may require some additional pointers for doing some stuff. However this area will be optional and depends on the features you use.
That's obvious right? On the TMS9900 CPU we need a workspace in memory to hold the 16 registers.
Address of timer table
We can now have multiple timer tables and the timer table can also be relocated. This offers more flexibility compared to the original SPECTRA1 implementation where we only had a fixed address.
Address of sound table
The built-in sound player will be compatible with the ISR sound format. The tune can either be in VDP memory or ROM/RAM. This is controlled by one of the bits in the mini-os status field
Cursor YX position
This is new in spectra[sup]2[/sup]. We'll have some functions you can use to put tiles & text at specific coordinates without you having to calculate each individual position.
Virtual game keyboard flags or keycodes
Already had the concept of a virtual game keyboard in SPECTRA[sup]1[/sup]. The idea is that we have a keyboard (including mapping to joysticks) and each bit represents a game key. Most likely there will be an additional keyboard scan routine that can hold the typed key and a status flag.
Loop code + return from stack
This is new in SPECTRA2. It contains some machine code that is copied from ROM on library startup. We can easily modify the machine code during runtime by overwriting some of the opcodes.
Below is the code that is copied on startup:
* ; Tight loop DATA >0000 ; \ M3LOOP: opcode+operands set by caller DATA >0606 ; | DEC R6 (TMP2) DATA >16FD ; / JNE M3LOOP DATA >045B ; / B *R11 * ; Return from stack DATA >C2FD ; \ M3POPR: MOV *RSTACK+,R11 DATA >045B ; / B *R11
I'll be going into detail once I covered register usage. What I can say is that -among others- the tight loop is used for reading/writing to the VDP. The "return from stack" code is there for speed reasons and because I want to implement some kind of routine for calling subroutines accross 8K banks. By fiddling with the "return from stack" code, subroutines could become bank-aware.
Scratch-pad memory layout for GPL usage
In order to use the provided GPLLNK routine, you'll have to setup memory in a certain way. The below table shows what memory locations have a GPL specific usage.
So let's talk registers now. Below you find the overview on how the registers will be used in SPECTRA2.
!important! There is a comprehensive set of equates available for addressing all of the below registers. It is strongly advised you use these equates where possible. It'll allow register reorganization without you having to modify your program big time. There are also equates available for accessing the high/low bytes of any of the mentioned registers.
General purpose registers (R0-R3)
You can use these registers in your subroutines, but if you change any of these registers you'll have to push them on the stack first. Ofcourse upon subroutine exit you'll have to pop the old values of the stack. There are subroutines to help you with that. If your subroutine is called as a timer, then R0-R2 will contain information that identifies the slot, target tick count, etc. Hence the requirement for pushing/popping if you change R0-R2. Also R0-R3 are the registers to use if you are going to implement nested subroutines.
Temporary registers (R4-R7)
These registers can be used for storing temporary values. You have to consider that they likely will be overwritten if you call any of the spectra2 routines. So, it's a good idea to store register values on the stack or in R0-R3 before calling a routine. Please use the TMP0..TMP3 equates instead of R4-R7. The registers may be reorganized at a later time. But you are safe if you use TMP0..TMP3
Stack pointer (R8)
Pointer to both subroutine return stack and data stack. The stack pointer is set by the SPECTRA2 initialisation routine. For debugging purposes you could set the data stack equate to another register, e.g. R7
Address of return routine (R9)
The address is is set by the spectra2 initialisation routine. It points to the M3POPR machine code in scratch-pad memory. For exiting a nested subroutine a "B *R9" is to be used. Benefit: opcode size is only 2 bytes instead of 6 bytes when using "B @>xxxx". Will also be used when a return accross ROM memory banks is required.
Highest slot in use & internal counter for timers (R10)
The high byte of R10 keeps track of the highest slot used in the timer table. The low byte of R10 is the timer tick counter and is updated every 1/50th or 1/60th of a second.
Subroutine return address (R11)
Same use as in Editor/Assembler. Contains the return address when doing "BL xxxx"
Configuration register (R12)
Flags for controlling and checking various features of the SPECTRA2 library and your TI-99/4A console.
Copy of VDP status register and internal counter for sound list (R13)
The high byte of R13 contains a copy of the VDP status register. This byte is automatically updated by the timer manager. The low byte of R13 contains an internal counter used by the sound player.
Copy of VDP register #0 and VDP register #1 (R14)
This is new in spectra2. In SPECTRA1 a copy of the values in VDP write-only registers 0-7 was present in scratch-pad RAM. To save memory, this feature was rejected in SPECTRA2. It's now returning but only for VDP write-only registers 0-1 as these control most of the features of the VDP.
VDP write address (R15)
Contains the address of the VDP data window. The address is set by the SPECTRA2 initialisation routine. This register is heavily used by the built-in functions responsible for VDP communication.