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:
asm198x --dialect acme --prg demo.asm -o demo.prg; 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, 0The 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
; 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 0mux_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.
Related
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.