Skip to content

Sprite Multiplexing

Draw sixteen sprites in two fixed bands, with a complete raster IRQ example verified on PAL and NTSC emulators.

Code status: assembled and emulator-executed. PAL and NTSC C64, two fixed sprite bands, in Emu198x and VICE. The recorded checks identify the tested sources and observations. They establish the stated cases on that configuration; original-hardware checks are separate.

Sixteen sprites from eight

Run this example and you get eight solid sprites near the top of the screen and eight outlines further down. The same eight hardware sprites draw both bands. Once the VIC-II finishes the upper band, a raster interrupt changes their positions, pointers and colours before the lower band starts.

This is a complete two-band multiplexer for a PAL or NTSC C64. Each band holds zero to eight unexpanded, single-colour sprites. The fixed spacing makes the timing easy to inspect before attempting a scheduler for freely moving sprites.

Run it

Save these two files together, assemble demo.asm, load the PRG and type RUN:

Shell
asm198x --dialect acme --prg demo.asm -o demo.prg
demo.asm
; RUN after loading: sixteen sprites in two bands on a PAL or NTSC C64.
* = $0801
    !byte $0b, $08, $0a, $00, $9e
    !text "2064"
    !byte 0, 0, 0
* = $0810
start:
    sei
    lda #$7f
    sta $dc0d            ; no CIA timer IRQ or NMI during the raster chain
    sta $dd0d
    lda $dc0d
    lda $dd0d
    lda #$35             ; RAM at BASIC/KERNAL, I/O visible
    sta $01
    lda #<nmi_return
    sta $fffa
    lda #>nmi_return
    sta $fffb
    lda $dd02
    ora #3
    sta $dd02
    lda $dd00
    ora #3
    sta $dd00            ; VIC-II bank 0 ($0000-$3fff)
    lda #$14
    sta $d018            ; screen $0400, character ROM $1000
    lda #$08
    sta $d016            ; 40 columns, no multicolour text
    lda #0
    sta $d020
    sta $d021
    ldx #0
    lda #32
clear_screen:
    sta $0400, x
    sta $0500, x
    sta $0600, x
    sta $06e8, x
    inx
    bne clear_screen
    jsr mux_init
    cli
idle:
    jmp idle
nmi_return:
    rti

!source "multiplexer.inc"

* = $2000                ; sprite pointers $80 and $81, 64-byte alignment
solid:
    !fill 63, $ff
    !byte 0
outline:
    !byte $ff, $ff, $ff
    !for row, 1, 19 {
        !byte $80, 0, 1
    }
    !byte $ff, $ff, $ff, 0

The upper band starts at raster Y=64; the lower band starts at Y=176. Each sprite is 24 pixels wide and 21 lines high. The two rightmost positions exceed 255, so both bands exercise the ninth X bit in $D010.

The IRQ and its tables

multiplexer.inc
; Two bands of 0..8 unexpanded, single-colour sprites, at Y=64 and Y=176.
; Owns the raster IRQ and VIC-II sprite registers. Call mux_init with IRQs off,
; RAM vectors visible ($01=$35), bank 0 and screen $0400 selected.
; The caller enables IRQs. Counts are atomic bytes; do not edit tables live.
mux_init:
    lda #0
    sta $d01a
    sta $d015
    sta $d017
    sta $d01b
    sta $d01c
    sta $d01d
    sta mux_phase
    sta mux_frames
    sta mux_errors
    sta mux_errors + 1
    lda #<mux_irq
    sta $fffe
    lda #>mux_irq
    sta $ffff
    lda #$1b             ; display on, 25 rows; raster compare high bit clear
    sta $d011
    lda #32
    sta $d012
    lda #$0f
    sta $d019            ; clear pending VIC-II sources before enabling raster
    lda #1
    sta $d01a
    rts

mux_irq:
    pha
    txa
    pha
    tya
    pha
    lda #1
    sta $d019
    lda #0
    sta $d015            ; the preceding band has finished
    sta mux_msb
    sta mux_slot
    sta mux_offset
    ldx mux_phase
    sta mux_errors, x
    lda mux_counts, x
    cmp #9
    bcc mux_valid
    lda #1               ; invalid count: reject this band, never index its data
    sta mux_errors, x
    lda #0
mux_valid:
    sta mux_count
    sta mux_remaining
    lda mux_starts, x
    sta mux_index
    lda mux_y, x
    sta mux_current_y
    lda mux_remaining
    beq mux_commit       ; zero entries means no table reads
mux_sprite:
    ldy mux_index
    ldx mux_offset       ; position registers have a two-byte stride
    lda mux_xlo, y
    sta $d000, x
    lda mux_current_y
    sta $d001, x
    ldx mux_slot         ; pointers and colours have a one-byte stride
    lda mux_pointers, y
    sta $07f8, x
    lda mux_colours, y
    sta $d027, x
    lda mux_xhi, y
    beq mux_low_x
    lda mux_msb
    ora mux_bits, x
    sta mux_msb
mux_low_x:
    inc mux_index
    inc mux_slot
    inc mux_offset
    inc mux_offset
    dec mux_remaining
    bne mux_sprite
mux_commit:
    lda mux_msb          ; rebuild all eight bits, including bits to clear
    sta $d010
    ldx mux_count
    lda mux_enables, x
    sta $d015
    ldx mux_phase
    lda $d012            ; completion raster, retained for the timing check
    sta mux_last_write, x
    txa
    eor #1
    sta mux_phase
    tax
    lda mux_irq_lines, x
    sta $d012
    cpx #0
    bne mux_return
    inc mux_frames
mux_return:
    pla
    tay
    pla
    tax
    pla
    rti

mux_counts:     !byte 8, 8
mux_xlo:        !byte 24, 64, 104, 144, 184, 224, 8, 48
                !byte 48, 8, 224, 184, 144, 104, 64, 24
mux_xhi:        !byte 0, 0, 0, 0, 0, 0, 1, 1
                !byte 1, 1, 0, 0, 0, 0, 0, 0
mux_pointers:   !fill 8, $80
                !fill 8, $81
mux_colours:    !byte 1, 2, 3, 4, 5, 6, 7, 8
                !byte 8, 7, 6, 5, 4, 3, 2, 1
mux_starts:     !byte 0, 8
mux_y:          !byte 64, 176
mux_irq_lines:  !byte 32, 144
mux_bits:       !byte 1, 2, 4, 8, 16, 32, 64, 128
mux_enables:    !byte 0, 1, 3, 7, 15, 31, 63, 127, 255
mux_phase:      !byte 0
mux_frames:     !byte 0
mux_errors:     !byte 0, 0
mux_last_write: !byte 0, 0
mux_msb:        !byte 0
mux_slot:       !byte 0
mux_offset:     !byte 0
mux_count:      !byte 0
mux_remaining:  !byte 0
mux_index:      !byte 0
mux_current_y:  !byte 0

mux_counts contains one count for each band. Try changing 8, 8 to 4, 6. The first eight entries of each sprite table belong to the upper band, and the next eight belong to the lower band. mux_xhi holds zero or one for each X coordinate. Each pointer selects a 64-byte sprite block in VIC-II bank 0.

The handler uses two indices: twice the slot number for the interleaved X/Y registers, and the slot number for colour and pointer writes. It rebuilds the whole $D010 mask each time, clearing high bits that belonged to the preceding band. It also rebuilds $D015, so reducing a count disables unused sprites.

Zero skips the table loop. A count above eight disables that band and sets its mux_errors byte; it cannot read beyond the eight entries. The next valid count clears the error. Each IRQ saves and restores A, X and Y; RTI restores the interrupted flags.

Why the timing fits

Band Setup IRQ Sprite lines Observed final write
Upper 32 64–84 45
Lower 144 176–196 158

Those final-write lines were measured with all eight sprites enabled in Emu198x and VICE, on both PAL and NTSC configurations with display DMA active. The setup finishes 19 or 18 lines before drawing starts. The earlier band has also finished before its registers are reused. The IRQ does not need to land on one exact CPU cycle for this deliberately generous schedule.

The maintained source and verification recipe compares Asm198x output with ACME, checks every visible sprite pixel, and tests counts 0, 1, 7, 8, 9 and 255, separate empty bands, and an 8→0→8 transition. Removing the X-high-bit write makes the picture check fail. These are emulator observations, not measurements from an original C64.

What the example owns

The demo takes over the machine. It selects VIC-II bank 0, screen $0400, ordinary 40-column text and RAM interrupt vectors, disables CIA interrupts, and owns the raster IRQ and all sprite registers. Sprite data occupies $2000 and $2040. It does not return to BASIC or share interrupt ownership with a cartridge or another raster handler.

A foreground routine may change a count with a single-byte write; the next IRQ for that band picks it up. Keep the descriptor tables unchanged while the multiplexer runs. Updating a whole moving-sprite list would require a separate publication scheme so the IRQ cannot see half an update.

Keep the fixed Y positions, unexpanded sprites and IRQ lines together. This example does not handle arbitrary Y positions, more than eight overlapping sprites, expansion, or sprites that cross the bottom of the frame. A general multiplexer must schedule around those cases; sorting by Y alone does not make an impossible overlap fit.

Patterns: Hardware Sprites, Raster Splits

Vault: VIC-II | Commodore 64

Sources

Commodore, Commodore 64 Programmer’s Reference Guide (1983), chapter 3, pp. 131–140: eight 24×21 sprites, raster-interrupt reuse, pointers, positions and the ninth X bit. Its VIC-II register map describes the raster and IRQ registers. Timing and execution claims above come from the sample’s retained Emu198x and VICE checks.

Explore the cross-platform sprite family for software drawing, hardware limits, restoration and overload policies.