Skip to content

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:

RangeSizeWhat is there
$0000-$3FFF16 KBVRAM — the same 16 KB the 6502 reaches through the data port
$4000-$47FF2 KBGRAM — the GPU's own RAM, where its code normally lives
$5000-$507F128 BPalette RAM, 64 entries of two bytes, word access only
$6000-$603F64 BThe VDP registers, readable and writable
$70001 BCurrent scan line
$70011 BBlanking status
$B000-$B00F16 BThe 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.

RangeSizeWhat is there
$6040-$6FFF~4 KBMore GPU RAM
$7002-$AFFF~16 KBMore GPU RAM
$B010-$FFFF~20 KBMore 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:

InstructionOpcodeWhat it does
CALL$0C80Call a subroutine, pushing the return address
RET$0C00Return, popping it back
PUSH$0D00Push a word
POP$0F00Pop a word
SLC$0E00Shift 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 RAM

The stack grows down and is always 16-bit words on even addresses.

Several existing instructions mean something different here:

WasIs nowWhat it does
XOPPIXPlot, read, test or address a pixel in one instruction
IDLEIDLEStop until the next trigger. Safe here, unlike on a real 9900 in a computer
RTWPRTWPR14 to the program counter, R15 to the flags. R13 is not used
CKONSPI enableF18A only — chip select low on its flash chip
CKOFSPI disableF18A only — chip select high
LDCRSPI outF18A only — write a byte to flash
STCRSPI inF18A 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
FieldWhat it means
M1 = work out a Graphics II address instead of a bitmap-layer one
A1 = return the address rather than touching the pixel
R1 = read the current pixel back into PP, after any write
W1 = do not write
C1 = write only if the comparison in E holds
E1 = write only where the pixel equals OO; 0 = only where it does not
OOThe pixel value to compare against
PPThe 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,R1

The 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.

OffsetBytesFieldWhat it is
+$002SourceSource address, high byte first
+$022DestinationDestination address, high byte first
+$041WidthBytes per row
+$051HeightRows
+$061StrideRow width in VRAM
+$071ParamsD0 = hold the source address, D1 = write the destination backwards
+$081Status0 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 DATA words 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.

Written for BIOS v1.5. Released under the MIT License.