Skip to content
RetrogamingDeep Dive Published Updated 6 min readViews unavailable

C64 VIC-II Sprite Pointers: Bank-Relative Data and Vertical Reuse

Follow VIC-II sprite-pointer fetches, 64-byte data slots, and raster-timed vertical reuse without treating multiplexing as unlimited sprites per line.

The Commodore 64 has eight VIC-II hardware sprites, but a game may show more than eight distinct objects in one frame. The trick is vertical reuse: after a sprite has finished its first appearance, software can change its position and image pointer so the same hardware channel appears again farther down the raster. This is not extra sprite hardware, nor does it remove the eight-sprite-per-line limit. To understand why it works - and why timing errors make sprites flicker - start with where the VIC fetches each sprite’s data.

A pointer is a block number in the current VIC bank

A standard sprite image is 24 pixels wide and 21 rows tall. Three bytes encode each row, so visible shape data occupies 63 bytes. The VIC allocates it in a 64-byte-aligned slot. A one-byte sprite pointer selects a slot inside the VIC’s current 16 KiB memory bank: the bank-relative data offset is pointer × 64. Christian Bauer’s measured VIC-II description explains that the pointer supplies the upper address bits while a six-bit sprite data counter supplies the lower bits. The unused 64th byte is not a 22nd visible row.

The eight pointers live at the last eight bytes of the selected 1 KiB video matrix: screen_base + $03F8 through screen_base + $03FF. With the familiar screen matrix at $0400, the bytes are $07F8–$07FF; this is a default-layout example, not a fixed set of sprite registers. Move the screen matrix with the VIC configuration and the pointer bytes move with it. A program that updates $07F8 after changing the matrix base may write a perfectly real RAM byte that the VIC no longer uses for sprite 0.

Work through the address arithmetic before writing any raster code. In a VIC bank starting at $0000, pointer $20 selects offset $0800; pointer $21 selects $0840. In a bank starting at $4000, the same pointer $20 selects $4800. The value is not an absolute CPU address. Confirm the active bank, screen matrix base, and 64-byte alignment together. The VIC’s view of memory can also differ from the CPU’s mapped view, so “the CPU can read these bytes” is not by itself proof that the VIC fetches them from the expected source.

Pointer fetch and sprite-data fetch are separate events

Bauer distinguishes a pointer access (p-access) from sprite-data accesses (s-accesses). The pointer is fetched for each sprite on every raster line, even when that sprite’s DMA is off. When DMA is on, the VIC then reads three data bytes for the relevant sprite row. VICE’s cycle-oriented implementation makes this distinction visible: its sprite-pointer fetch reads video-matrix offset 0x3f8 + sprite_index, while its data fetch forms an address from pointer << 6 plus the sprite’s data counter.

This is why changing a pointer during a sprite’s appearance can affect a later row without retroactively changing rows already shifted onto the display. It is also why a whole-frame snapshot of pointer RAM is insufficient for a timing-sensitive emulator. The program may write a new pointer between line fetches. A correct trace needs the write’s raster position and the subsequent pointer fetch, not just the byte’s value at the end of the frame.

The VIC has independent coordinate and control registers. $D015 enables sprite channels; pairs of registers beginning at $D000 hold low X and Y coordinates, while $D010 supplies the ninth X-coordinate bits. Color registers begin at $D027. Those controls select whether and where an image appears; the pointer bytes select the image source. Changing only the pointer does not start a disabled sprite, and changing only Y does not upload new pixel data.

Multiplex vertically, with a real timing budget

Bauer’s tested model describes a Y-coordinate match near the end of a raster line that starts sprite DMA, followed by pointer and data fetches. For a normal, unexpanded sprite, the image consumes 21 displayed rows. After it has completed, code can assign a later Y coordinate and optionally a new X, pointer, and color so the same channel is seen again lower on the screen. That is vertical multiplexing. The next appearance must be scheduled early enough for the hardware’s Y comparison and data fetch; writing registers after the relevant fetch cannot repair the missed appearance.

A conceptual schedule might use channels 0–7 for eight objects near the top, then reuse some of those channels for lower objects after the first group has finished. The scheduler must account for each object’s start and end line, Y expansion, and any overlap with other objects. Two objects that require the same channel on the same raster line cannot both be shown by reprogramming it. Bauer explicitly notes that horizontal reuse within one line does not work after the sprite shift register has emptied: the practical cap remains eight sprites on a line.

Raster interrupt work also consumes CPU cycles. The VIC may take bus time for sprite data and, on bad lines, for the video matrix. A handler that updates several Y registers, pointers, colors, and X bits may miss its deadline even though the equivalent writes work when run slowly in a monitor. Plan a worst-case line rather than a frame-average budget. Keep timing numbers tied to a particular VIC model - Bauer’s cycle-numbered sprite rules are specifically stated for the 6569 PAL chip, and NTSC has a different line structure.

Validate the memory map before the effect

First make one static sprite work. Put a known 63-byte pattern in an aligned slot, set its pointer in the selected video matrix, enable its channel, and verify its coordinates and color. Then move only the sprite data to another 64-byte slot and update only the pointer; the image should change without changing its position. Next change the screen matrix base and move the pointer table accordingly. These controlled steps distinguish bad sprite bytes from a bad pointer address.

For a multiplexing test, give each logical object a visibly different pattern and color. Schedule two non-overlapping vertical appearances on one hardware channel. Record the raster line of each register write and compare it with the first line of each expected appearance. Add objects until the deadline is tight, then test PAL and NTSC separately. If lower objects disappear, inspect the last pointer fetch and Y-match opportunity before blaming the sprite bitmap. If they show the wrong image, inspect which pointer byte the VIC fetched under the current bank and matrix base.

An emulator should test mid-sprite pointer changes, channel disable/enable transitions, expanded-height sprites, and overlapping object schedules. A game that only changes all sprite registers during vertical blanking will not reveal those edge cases. The useful model is a sequence of bank-relative pointer fetches, data fetches, and raster-timed control changes - not a list of eight immutable objects for an entire frame.

Related:

Sources:

Comments