The GPU
There is a second processor in your ACE, and it is inside the video card.
It is a TMS9900 — a 16-bit chip from 1976, the one in the TI-99/4A — with its own RAM, direct access to VRAM and the palette and every VDP register, a pixel-plotting instruction the original never had, and no interrupts to worry about. You give it an address and it runs.
You have already used it. The detection probe in Turning it on is a GPU program: six bytes that erase themselves, which is how you find out the GPU is there.
Why you would
The 6502 in an ACE runs at a few million cycles a second and has to do everything: game logic, input, sound, and every byte that goes into the video card, two writes at a time through one port.
The GPU is on the other side of that port. It reads and writes VRAM directly, as memory, with 16-bit instructions. Anything that is moving bytes around inside the video card is work it can take off you entirely:
- Clearing or filling a name table between levels.
- Plotting into the bitmap layer.
- Updating a sprite table from a list you handed it.
- Animating a palette.
- Anything you would have wanted on every scan line, which the 6502 cannot afford and this can.
And it can be told to do it once per frame or once per scan line, automatically, without the 6502 being involved at all.
Starting it
Two registers hold the address, and writing the low byte starts it running:
lda #$40
ldx #54
jsr SetVdpReg ; high byte
lda #$00
ldx #55
jsr SetVdpReg ; low byte — and it goes- Writing register 55 starts it once, at that address.
- Register 50's GPU_HTRIG starts it at the top of every scan line.
- Register 50's GPU_VTRIG starts it once per frame.
Status register 2 says whether it is still running, in bit 7. The other seven bits of that register are yours: the GPU can put anything it likes there and the 6502 can read it, which is the channel for I finished, here is what I found, or a frame counter.
What it can see
The GPU's address space is not the 6502's, and it is not quite VRAM either:
| Range | Size | What is there |
|---|---|---|
$0000-$3FFF | 16 KB | VRAM — the same 16 KB the 6502 reaches through the data port |
$4000-$47FF | 2 KB | GRAM — the GPU's own RAM, where its code normally lives |
$5000-$507F | 128 B | Palette RAM, 64 entries of two bytes, word access only |
$6000-$603F | 64 B | The VDP registers, readable and writable |
$7000 | 1 B | Current scan line |
$7001 | 1 B | Blanking status |
$B000-$B00F | 16 B | The status registers |
The first 16 KB is the same VRAM your 6502 code writes through the data port — same bytes, reached directly. Above that is the interesting part: the palette as memory, the VDP registers as memory, and the current scan line as a byte you can read.
That last one is what makes a scan-line program possible. Wait for the beam to reach a line, change a scroll register, wait again — inside the card, with no interrupt and no 6502 cycles spent.
A Pico9918 has far more room
A real F18A leaves everything outside its defined regions unimplemented. The Pico9918 backs the whole 64 KB with RAM, so roughly 40 KB of scratch space exists that F18A code cannot assume.
| Range | Size | What is there |
|---|---|---|
$6040-$6FFF | ~4 KB | More GPU RAM |
$7002-$AFFF | ~16 KB | More GPU RAM |
$B010-$FFFF | ~20 KB | More GPU RAM |
Handy, and not portable. A program that assumes it will not run on a real F18A.
The instruction set
It is a 9900, so any 9900 assembler will assemble for it. Five instructions are new:
| Instruction | Opcode | What it does |
|---|---|---|
| CALL | $0C80 | Call a subroutine, pushing the return address |
| RET | $0C00 | Return, popping it back |
| PUSH | $0D00 | Push a word |
| POP | $0F00 | Pop a word |
| SLC | $0E00 | Shift left circular |
Your assembler will not know their names. Emit them as data words inline — DATA >0C00 is RET — which is exactly as unpleasant as it sounds and exactly what everyone does.
CALL, RET, PUSH and POP need a stack, and the GPU uses R15 as the stack pointer. Set it up before the first one:
LI R15,>47FE * top of the GPU's own RAMThe stack grows down and is always 16-bit words on even addresses.
Several existing instructions mean something different here:
| Was | Is now | What it does |
|---|---|---|
XOP | PIX | Plot, read, test or address a pixel in one instruction |
IDLE | IDLE | Stop until the next trigger. Safe here, unlike on a real 9900 in a computer |
RTWP | RTWP | R14 to the program counter, R15 to the flags. R13 is not used |
CKON | SPI enable | F18A only — chip select low on its flash chip |
CKOF | SPI disable | F18A only — chip select high |
LDCR | SPI out | F18A only — write a byte to flash |
STCR | SPI in | F18A only — read a byte from flash |
And a set of them is simply gone: SBO, SBZ, TB, BLWP, STWP, LWPI, LIMI, RSET, LREX.
There is no workspace pointer, no interrupt system and no CRU, so the instructions that use them are gone. An unknown opcode does nothing at all rather than derailing the GPU.
IDLE is safe here
IDLE on a real 9900 in a real computer stops the processor until an interrupt arrives, which on a machine with no interrupt to send is a way to hang it.
The GPU has no interrupts, and IDLE means stop and wait for the next trigger. It is how a GPU program ends. Every one of them finishes with it.
PIX
The interesting instruction. It reads, writes, conditionally writes, or merely locates a pixel — given an X and a Y, in one instruction.
It uses the XOP opcode, which the GPU had no use for, so any 9900 assembler will emit it. The source operand is the coordinate, X in the high byte; the destination register is a command:
MAxxRWCE xxOOxxPP| Field | What it means |
|---|---|
M | 1 = work out a Graphics II address instead of a bitmap-layer one |
A | 1 = return the address rather than touching the pixel |
R | 1 = read the current pixel back into PP, after any write |
W | 1 = do not write |
C | 1 = write only if the comparison in E holds |
E | 1 = write only where the pixel equals OO; 0 = only where it does not |
OO | The pixel value to compare against |
PP | The pixel value to write, and where a read comes back |
Which gives you, in one instruction each:
LI R0,>2020 * x=32, y=32
LI R1,>0001 * plot color 1
XOP R0,R1
LI R1,>0801 * plot 1, and hand back what was there
XOP @XY,R1
LI R1,>0302 * plot 2, but only where the pixel is 0
XOP @XY,R1
LI R1,>0213 * plot 3, but only where the pixel is not 1
XOP @XY,R1
LI R1,>8000 * don't plot — just tell me the address, Graphics II layout
XOP @XY,R1The conditional forms are the ones worth staring at. Write only where the pixel is background is a sprite mask. Write only where it is not is a flood-fill boundary test. Both are one instruction where a 6502 needs a read, a mask, a compare, a branch and a write.
The address arithmetic it replaces is eight instructions of bit-twiddling in the TI Editor/Assembler manual. In hardware it is an adder and some wiring.
Block copying
The Pico9918 adds something the F18A does not have: a block copier, driven by a command block at $8000 and triggered by touching that address.
| Offset | Bytes | Field | What it is |
|---|---|---|---|
+$00 | 2 | Source | Source address, high byte first |
+$02 | 2 | Destination | Destination address, high byte first |
+$04 | 1 | Width | Bytes per row |
+$05 | 1 | Height | Rows |
+$06 | 1 | Stride | Row width in VRAM |
+$07 | 1 | Params | D0 = hold the source address, D1 = write the destination backwards |
+$08 | 1 | Status | 0 when it is done |
Rectangular copies with a stride, so it moves a region of a name table rather than a run of bytes — which is the shape almost every VRAM copy actually has. The status byte goes to zero when it is done.
On a real F18A, $8000 is a free-running 32-bit counter instead. This is the sharpest difference between the two cards, and code that uses either will not run on the other.
What a real F18A has that yours does not
- A serial flash chip the GPU can read and write, holding the FPGA bit stream, the default palettes and about 22 character sets. Writing the wrong part of it bricks the card.
- A small catalog of preloaded routines in that flash: block copy, font load, and three for reading the catalog itself.
- A hardware random number generator and a free-running 32-bit counter in the GPU's address space.
The flash chip is the one to know about, because a large part of the F18A's own documentation is about reading and writing it, and none of that applies here. The Pico9918 updates its firmware through a different mechanism entirely, and there is no serial flash for a GPU program to reach.
Getting a program in
There is no toolchain for this in the ACE's world, and that is the honest state of it. What you have:
- Any 9900 assembler, which will handle everything except the five new instructions and
PIX's operand format. - The instructions above as
DATAwords for the rest. - A blob in your 6502 program, written into VRAM the same way the six-byte detection probe is written, and started with two register writes.
For a program of any size, assemble the 9900 code separately, include the bytes in your source, and copy them in at startup. It is not comfortable. It is also the only processor in this machine that can run a routine on every scan line without costing you anything, so for the right effect it is worth the discomfort.

