Sprite Multiplexing
More sprites than hardware allows
By repositioning hardware sprites mid-frame as the raster beam passes, programmers displayed far more objects than the hardware officially supported.
Overview
The VIC-II has 8 sprites. The NES has 64 sprites but only 8 per scanline. The Amiga has 8 sprites. Yet games routinely displayed dozens of moving objects. The trick: reposition sprites after the raster beam has drawn them, allowing the same hardware sprite to appear multiple times per frame.
The principle
Frame start:
Sprite 0 at Y=50
Raster draws sprite at line 50
Raster reaches line 71 (sprite done):
Move sprite 0 to Y=150
Raster draws "second" sprite at line 150
Result: One hardware sprite appears twice
Commodore 64 implementation
Simple approach
; Wait for raster to pass sprite's bottom
lda #71 ; line after first sprite
.wait:
cmp $d012
bcs .wait
; Reposition sprite for second appearance
lda #150
sta $d001 ; sprite 0 Y position
lda new_x
sta $d000 ; sprite 0 X position
Full multiplexer
A proper multiplexer:
- Sorts all virtual sprites by Y position
- Assigns hardware sprites to the nearest virtual sprites
- Uses raster interrupts to reposition at exact moments
; Simplified multiplexer structure
virtual_sprites: ; array of Y,X,pointer for each object
.res MAX_SPRITES * 4
sorted_indices: ; Y-sorted order
.res MAX_SPRITES
; IRQ chain repositions sprites as raster descends
Sorting requirement
Sprites must be Y-sorted because:
- The raster moves top to bottom
- A sprite can only be reused after it's been drawn
- Out-of-order sprites cause flicker or missing objects
NES sprite considerations
The NES has different constraints:
- 64 sprites in OAM (Object Attribute Memory)
- Only 8 sprites per scanline (hardware limit)
- Excess sprites on a line become invisible
- Sprite 0 is always evaluated first — never flickered, and used for sprite-0-hit timing
NES solution: cycling priority
The PPU evaluates OAM in order, keeping the first 8 sprites it finds on each scanline. Rotating the OAM start offset each frame distributes the flicker:
; Rotate which sprites get priority each frame
; Spreads flicker across all objects instead of
; always hiding the same ones
inc frame_count
lda frame_count
and #$1C ; 0, 4, 8, 12, 16, 20, 24, 28
tay ; OAM start index
; Build OAM buffer starting at offset Y, wrapping at 256
The mask #$1C gives 8 different start offsets cycling every 32 frames, so each sprite spends roughly 1/8 of its time at low priority. This is the standard Super Mario Bros. / Contra idiom.
Amiga sprite multiplexing
The Copper makes Amiga multiplexing elegant:
copper_list:
; Sprite 0 first appearance
dc.w $0120, sprite0_hi
dc.w $0122, sprite0_lo
dc.w $0140, $5050 ; position 1
; Wait for sprite to pass
dc.w $6007, $fffe
; Sprite 0 second appearance
dc.w $0140, $a070 ; position 2
dc.w $0144, new_data
No CPU interrupts needed — the Copper handles repositioning. The Copper can also reposition sprites mid-scanline by writing to SPRxPOS at the right horizontal position; the constraint is the Copper DMA budget, not "after the sprite has finished drawing" as on the C64.
Limitations
| Constraint | Cause | Mitigation |
|---|---|---|
| Vertical only | Can't reuse until raster passes | Sort by Y, design levels accordingly |
| Flicker | Too many sprites at same Y | Spread objects vertically |
| CPU cost | Sorting, repositioning | Pre-sort static objects |
| Per-line limits | Hardware constraint | Design around it |
Games that used multiplexing
| Game | Platform | Virtual sprites |
|---|---|---|
| Commando | C64 | 20+ enemies |
| Armalyte | C64 | Massive bullet counts |
| Katakis | C64 | Horizontal shooter |
| R-Type | Various | Dozens of bullets |
Flicker management
When multiplexing can't avoid overlap:
- Alternate visibility: swap which sprites are hidden each frame
- Priority rotation: give different sprites priority each frame
- Design: place enemies at staggered Y positions