Skip to content

Sprite Multiplexing

More sprites than hardware allows

A sprite multiplexer shows more moving objects than a machine has hardware sprites, by moving each sprite further down the screen once the beam has finished drawing it. On the Commodore 64 it meant sorting objects by height and a chain of raster interrupts every frame; the Amiga's sprite hardware could reuse sprites by itself; the NES rotated its sprite list and accepted flicker.

The Commodore 64’s video chip has eight sprites. Commodore’s own Programmer’s Reference Guide admitted in 1982 that this was not the real limit: “Up to 8 sprites at a time can be maintained by the VIC-II chip automatically. More sprites can be displayed using RASTER INTERRUPT techniques.” It added a warning that held for the rest of the decade: “Unfortunately BASIC isn’t fast enough to do this very well.”

The trick works because a sprite is not an object the chip keeps track of. It is a set of registers the chip reads as the picture is drawn. Once the beam has drawn a sprite, the program can give it a new position lower down, with a new shape and colour, and the chip will draw it again there. Do that for all eight, several times a frame, and eight sprites become twenty or thirty. By 1991 ZZAP!64 could promise that Chris Butler’s Turbo Charge had, “for the technically minded”, “22 multiplexed hardware sprites on screen”. Making it work took a sort, a chain of raster interrupts, careful timing and a fair share of every frame. Martin Walker, planning his next game in his ZZAP!64 diary after Hunter’s Moon, noted the other catch: “simply having 32 sprites whizzing about does not a game make.”

The rules on the C64

The VIC-II sets the terms:

  • No more than eight sprites on one line. Each sprite has one shift register, filled from memory once per line. A ninth object on the same line needs a ninth sprite, and there is none.
  • A sprite can be reused only below itself. A sprite is 21 lines tall, so the next object that shares its hardware must start at least 21 lines further down.
  • The new Y position must be written before the beam reaches it. Lasse Öörni, who writes as Cadaver, calls this “the most important hardware feature to take into account”: if the Y coordinate is late, “the re-used sprite won’t display at all!” It is not as tight as it sounds. The Y register can be rewritten as soon as the previous use has started drawing, since changing it in the middle “won’t ‘cut’ it or anything”.
  • Shape and colour are the harder part. “It will take at least one rasterline to rewrite the frame & color registers,” Öörni wrote, so a badly timed write shows the next object’s graphics on the last line of the previous one.

Reusing sprites in rows

The simplest multiplexer does not sort anything. It shows the eight sprites in one row, and when the beam has passed the row, a raster interrupt moves all eight down to the next. Öörni calls this the “zone split” and dissects an example from Turrican, where the first boss, a giant fist, is “5 sprites per row and 4 rows (20 sprites total)”. An interrupt before each row rewrites the Y positions, shapes and colours of five sprites; the X positions stay the same, so they are set once.

This program does the same with all eight sprites, four rows of them, 32 boxes in four colours. One handler serves every row, taking the next row’s Y position, colour and interrupt line from small tables.

Assembly
; eight hardware sprites shown four times each: 32 objects (ACME syntax)
; start it from a BASIC SYS line
start   sei
        ldx #62             ; sprite shape: a hollow box in block 13 ($0340)
-       lda shape,x
        sta $0340,x
        dex
        bpl -
        ldx #7
-       lda #13
        sta $07f8,x         ; all eight sprites use block 13
        lda xpos,x
        pha
        txa
        asl
        tay                 ; Y = 2 * sprite number
        pla
        sta $d000,y         ; X position
        lda #60
        sta $d001,y         ; first row at Y = 60
        lda #1
        sta $d027,x         ; white
        dex
        bpl -
        lda #$ff
        sta $d015           ; all eight sprites on
        lda #$7f
        sta $dc0d           ; switch off CIA 1's interrupts
        lda $dc0d
        lda #<irq
        sta $0314
        lda #>irq
        sta $0315
        lda #84
        sta $d012           ; first interrupt: row 0 has been drawn
        lda $d011
        and #$7f
        sta $d011
        lda #$01
        sta $d01a
        sta $d019
        cli
        rts

irq     lda #$01
        sta $d019           ; acknowledge
        ldx zone
        lda newy,x          ; move all eight sprites down to the next row
        sta $d001
        sta $d003
        sta $d005
        sta $d007
        sta $d009
        sta $d00b
        sta $d00d
        sta $d00f
        lda newcol,x        ; and give that row its own colour
        sta $d027
        sta $d028
        sta $d029
        sta $d02a
        sta $d02b
        sta $d02c
        sta $d02d
        sta $d02e
        lda nextline,x
        sta $d012           ; where the next interrupt happens
        inx
        cpx #4
        bne +
        ldx #0
        stx zone
        jmp $ea31           ; once a frame, let the KERNAL run
+       stx zone
        jmp $ea81

zone     !byte 0
newy     !byte 100, 140, 180, 60   ; row each interrupt sets up
newcol   !byte 7, 5, 3, 1          ; yellow, green, cyan, white
nextline !byte 124, 164, 212, 84   ; each just below the row now showing
xpos     !byte 30, 62, 94, 126, 158, 190, 222, 254
shape    !byte $ff,$ff,$ff         ; top edge
         !for r, 1, 19 {
         !byte $80,$00,$01         ; left and right edges
         }
         !byte $ff,$ff,$ff         ; bottom edge

Each interrupt comes a few lines below a row that has finished drawing and well above the next, so the timing has plenty of slack. Move the interrupt for the second row to line 105, after that row should have started, and the whole second row vanishes: the Y positions arrive too late, exactly as Öörni warned. The last interrupt of the frame, at line 212, puts the sprites back at the top ready for the next frame.

Rows are easy because the program decides where everything goes. In a game, objects move where they like.

A general multiplexer

Games such as Green Beret and Ghosts’n Goblins, in Öörni’s words, “allow free usage of over 8 sprites anywhere on the screen, any colors and frames”. To do that, a multiplexer treats the game’s objects as virtual sprites and does four jobs every frame:

  1. Sort the virtual sprites by Y position, top of the screen first.
  2. Map them onto the eight real sprites in turn: the first eight sorted objects go to sprites 0 to 7, the ninth to sprite 0 again, the tenth to sprite 1, and so on round.
  3. Reject any object that would be a ninth sprite on a line. With round-robin mapping, object n shares a sprite with object n − 8, so the test is whether they are less than 21 lines apart. Öörni gives it as if (spry[next_sprite] - sortspry[sorted_sprites - 8] < 21) then reject(). The first eight never need the test.
  4. Display them with a chain of raster interrupts down the screen, each writing the registers for the next few objects, Y positions first because they are the ones that must not be late.

The sort is where the time goes. Öörni named the one he preferred “Ocean” sorting, “because it can be found from many Ocean/Imagine games, like Green Beret or Midnight Resistance”. It keeps the sorted order from the previous frame and repairs it, swapping neighbours that have changed places. Objects in a game rarely overtake each other vertically, so most frames need almost no work, but “on some frame it’ll eat a lot of time, when there’s a lot of changes in the sprite order”. Falco Paul later tested the candidates in Java over random, extreme and game-like data. On the game-like pattern the Ocean sort “performs very fast”; on random data it “performs lousy (bubbesort like)”. A bucket sort, grouping objects by Y, was steadier, and in his tests “only 18% slower than the Ocean sort” on game-like data.

What it cost the programmer

Programmers learned this from each other and by taking games apart. Andrew Braybrook’s diary of Morpheus in ZZAP!64 shows the process from the inside. In January 1987 he saw Gary Liddon’s multiplexer and could not “understand why his runs faster”, so he borrowed Liddon’s method of “building a table in the system stack so I can use faster instructions to read the table”. The multiplexer then set its own traps. In April the sprites vanished between the title screen and the game, because “the sprite multiplexor clears out the sprite table ready for the next position”, and no new positions arrived while the program was busy setting up the screen; he asked readers what to do about the pause mode. In August, arranging the enemy line-up on the title screen, he found “the sprite positioning had to be pixel perfect to actually work properly, there’s no room for any play in the vertical positioning at all”.

Öörni, writing years later, had found no better written guide. Full multiplexing was “a subject seldom discussed in magazines”, and the Braybrook diary was “referring to some of the issues involved quite cryptically”, so he reverse-engineered “the sprite sorting/multiplexing routines of games like Green Beret, Midnight Resistance etc.” himself.

Game design had to live with the eight-per-line rule. Walker put it plainly in his Citadel diary in November 1988: “the main problem with any multiplexor is making sure that it is never possible for more than eight sprites to attempt to occupy the same horizontal ‘strip’”. Fixing some of Citadel’s sprites in position on the landscape would help. “If all the sprites may be moving, the alternatives are a lot of effort to ensure that the wrong combination can never occur (Armalyte) or nasty flickers and lurchers (Zynaps, for one).”

Flicker and the late interrupt

Some of the flicker in C64 games came not from too many sprites on a line but from interrupts that arrived late. A raster interrupt fires only when the beam reaches its line exactly. If a handler runs long and then sets the next interrupt for a line the beam has already passed, that interrupt does not come until the next frame, and the sprites it should have moved are missing or wrong. Öörni describes the result as “flicker & slowdown (caused by the new raster interrupt being higher or at same position than the current $d012 value)”.

His fix checks before returning. If the next interrupt’s line is already close, the handler jumps straight to the next handler’s code instead:

Assembly
        sta $d012           ; A = line of the next interrupt
        sec
        sbc #$03
        cmp $d012           ; late from next IRQ?
        bcc go_to_irq_directly

The margin of three lines is from experience: “3 lines feel like paranoidically much ‘safety’ for the interrupt but in fact I got flicker in some rare cases if I was subtracting only 2.” He also advises keeping objects that are off the visible screen out of the list, and keeping sprites a couple of lines clear of the top of a score panel, so that the panel’s own interrupt is not pushed late.

On the Amiga

The Amiga’s Denise chip also has eight sprites, but Amiga sprites are fetched by DMA from a list in memory, and the list can describe several uses of the same sprite down the screen. Commodore’s Hardware Reference Manual says so plainly: “Sprite DMA channels can be reused several times within the same display field. Thus, you are not limited to having only eight sprites on the screen at the same time.” The only restriction is a gap: “the bottom line of one usage of a sprite must be separated from the top line of the next usage by at least one horizontal scan line”, because the channel uses that line to fetch the next position. No interrupt is needed. The one thing the list cannot change is colour; for that the manual has the copper wait for the last line of one use and rewrite the sprite’s colour registers before the next.

Amiga games still needed sorting, because the list must run top to bottom. Braybrook, writing his Uridium 2 diary for The One in 1992, explained the term for its readers: “Multi-plexor’s a fancy name for a routine which sorts the objects to be displayed into vertical position sequence and re-uses hardware sprites to show them”.

On the NES

The NES works the other way round. Its PPU holds 64 sprites, but draws only the first eight it finds on each line, in the order they sit in sprite memory. The rest on that line are not drawn. Reusing sprites down the screen is not the problem; too many on one line is. The usual answer was to change the order of sprite memory from frame to frame, so that a different sprite drops out each time and every object is visible some of the time. That trades missing objects for sprite flicker, the shimmer familiar from busy NES games.

Sources

  • Commodore, Commodore 64 Programmer’s Reference Guide (1982): “More sprites can be displayed using RASTER INTERRUPT techniques” (p. 131); displaying more than 8 sprites with interrupts, and “BASIC isn’t fast enough” (pp. 151–152).
  • ZZAP!64 issue 24, April 1987, p. 90: Andrew Braybrook’s Morpheus diary, on Gary Liddon’s multiplexer and the table in the stack. Issue 26, June 1987, p. 70: the multiplexer clearing the sprite table. Issue 30, October 1987, p. 51: the title screen and “pixel perfect” positioning.
  • ZZAP!64 issue 36, April 1988, p. 51: Martin Walker’s diary, “32 sprites whizzing about does not a game make”. Issue 43, November 1988, p. 114: Walker on the eight-sprite problem, Armalyte and Zynaps.
  • ZZAP!64 issue 75, July 1991, p. 39: preview of Chris Butler’s Turbo Charge, “22 multiplexed hardware sprites”.
  • The One issue 51, December 1992: Andrew Braybrook’s Uridium 2 diary, the definition of a multiplexor.
  • Lasse Öörni (Cadaver), “Sprite multiplexing”, mirrored on the Codebase64 wiki (web, secondary): the zone split and Turrican, the Y-coordinate rule, frames and colours, round-robin mapping, the 21-line rejection test, “Ocean” sorting, late interrupts and the three-line margin, and his search for published information. https://codebase.c64.org/doku.php?id=base:sprite_multiplexing
  • Falco Paul, “On sprite sorting methods”, Codebase64 wiki (web, secondary): the Java test framework and its conclusions. https://codebase.c64.org/doku.php?id=base:sprite_multiplexer_sorting
  • Amiga Hardware Reference Manual, 3rd edition (Commodore-Amiga, Addison-Wesley, 1991): changing sprite colours between uses with the copper (pp. 26–27); sprites reusable within a field (p. 97); the data list for reuse and the one-line gap (pp. 116–117).
  • NESdev Wiki, “PPU sprite evaluation” (community reference, web, secondary): the first eight sprites in memory order are drawn on each line. https://www.nesdev.org/wiki/PPU_sprite_evaluation

The row-reuse listing was assembled with ACME and run in the VICE emulator (x64sc) as PAL and NTSC machines, and again with the second row’s interrupt moved to line 105.

See also

Checked. Verified against primary sources and signed off by a person.