Code status: assembled and emulator-executed. PAL and NTSC C64, normal-size single-colour sprite 0, in Emu198x and VICE. The recorded checks identify the tested sources and observations. They establish these cases on those configurations; original-hardware checks are separate.
A sprite on screen
Run this example and a white 24×21-pixel outline with a central cross appears at VIC-II coordinates X=160, Y=120 on a black screen. The VIC-II draws it from its shape data while the CPU runs an observation loop.
The C64 has eight hardware sprites. Each has a position, colour and shape pointer. The CPU does not paint their pixels into the background, but display is not free: sprite DMA takes bus time from the CPU. Moving or recolouring a sprite also requires CPU writes.
Run it
Save these three files together: demo.asm, sprite.inc and shape.inc. Assemble the demonstration, load the resulting PRG in a PAL or NTSC C64 emulator, and type RUN:
asm198x --dialect acme --prg demo.asm -o demo.prgReset to leave. This standalone demonstration takes over the screen, VIC bank, sprites and interrupt setup. It maps out BASIC and KERNAL and does not return to BASIC.
; Load the PRG, then RUN. Reset to leave this standalone demonstration.
; It owns the screen, VIC bank, CIA interrupt masks and sprite registers.
DEMO_X = 160
DEMO_Y = 120
SHARED_SEED = 0 ; verification seeds bits belonging to other sprites
* = $0801
!byte $0b, $08, $0a, $00, $9e
!text "2064"
!byte 0, 0, 0
* = $0810
start:
sei
lda #$7f
sta $dc0d
sta $dd0d
lda $dc0d
lda $dd0d
lda #$35
sta $01 ; RAM at BASIC/KERNAL, I/O visible
lda #<nmi_return
sta $fffa
lda #>nmi_return
sta $fffb
lda $dd02
ora #3
sta $dd02
lda $dd00
ora #3
sta $dd00 ; VIC bank 0
lda #$14
sta $d018 ; screen $0400, character ROM $1000
lda #$1b
sta $d011 ; display on, normal text
lda #$08
sta $d016 ; 40 columns, single-colour text
lda #0
sta $d015
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
; Blank shapes keep the other sprites invisible during ownership checks.
ldx #7
lda #$81
blank_pointers:
sta $07f8, x
dex
bpl blank_pointers
lda #SHARED_SEED
sta $d015
ora #1 ; sprite 0 starts with wrong high/mode bits
sta $d010
sta $d017
sta $d01b
sta $d01c
sta $d01d
lda #<DEMO_X
ldx #>DEMO_X
ldy #DEMO_Y
jsr sprite0_show
; Guest snapshots make shared-bit preservation observable.
ldx #0
copy_registers:
lda $d000, x
sta observed, x
inx
cpx #$2f
bne copy_registers
lda $07f8
sta observed_pointer
wait_top:
bit $d011
bmi wait_top
wait_bottom:
bit $d011
bpl wait_bottom
inc frames
jmp wait_top
nmi_return:
rti
frames:
!byte 0
observed:
!fill $2f, 0
observed_pointer:
!byte 0
!source "sprite.inc"
!source "shape.inc"
blank_shape:
!fill 64, 0Try changing DEMO_X to 296 and rebuilding. That position needs X bit 8 as well as its low byte. DEMO_Y is the raster coordinate, not a row counted from the top of the text area. Leave SHARED_SEED at zero for the demonstration; the verifier uses it to check ownership of the shared registers.
The routine’s contract
sprite0_show takes the X position’s low byte in A, its high bit (0 or 1) in X, and raster Y (0–255) in Y. It changes A and flags and preserves X and Y. It expects I/O visible, VIC bank 0 ($0000–$3FFF), screen RAM at $0400, and the complete shape at $2000.
The routine owns sprite 0. It sets white, normal size, single-colour mode and foreground priority, then enables the sprite. It preserves bits 1–7 of the shared sprite registers, so it does not disable or change the modes of sprites 1–7.
; Sprite 0: normal size, single-colour white, in front of the background.
; Input: A = X low byte, X = X high bit (0 or 1), Y = raster Y (0..255).
; Requires: VIC bank 0, screen $0400, complete shape at $2000, I/O visible.
; Owns sprite 0; preserves bits 1..7 of shared sprite registers.
; Clobbers A and flags; preserves X and Y. Caller excludes competing writers.
sprite0_show:
sta $d000
sty $d001
lda $d010
and #$fe
cpx #0
beq x_ready
ora #1
x_ready:
sta $d010
lda #$80 ; ($2000 - bank base $0000) / 64
sta $07f8 ; screen $0400 + $3f8
lda #1
sta $d027
lda $d017
and #$fe
sta $d017 ; no vertical expansion
lda $d01d
and #$fe
sta $d01d ; no horizontal expansion
lda $d01c
and #$fe
sta $d01c ; single-colour
lda $d01b
and #$fe
sta $d01b ; in front of background
lda $d015
ora #1
sta $d015
rtsThe caller must exclude competing writes while these read/modify/write sequences run. Preserving bits does not make an interrupted update atomic. The demonstration initialises once; calling midway through display can expose a partly updated position or mode. Raster-safe movement is a separate timing problem.
All 21 rows
Each row uses three bytes: 24 bits, most significant bit first. A set bit draws the sprite’s colour; a clear bit leaves the background visible. This shape has a border and a central cross. The loops supply repeated rows, so all 63 bytes are present.
; 21 rows, three bytes per row: a 24x21 outline, with a central cross.
; The 64th byte is padding, not another row.
* = $2000
sprite_shape:
!byte $ff, $ff, $ff
!for row, 1, 9 {
!byte $80, $18, $01
}
!byte $ff, $ff, $ff
!for row, 1, 9 {
!byte $80, $18, $01
}
!byte $ff, $ff, $ff
!byte 0The final byte pads the shape to a 64-byte block. It is not a twenty-second row. In the caller, the following blank block supplies transparent shapes for the other sprites during verification.
Trade-offs
| Aspect | Cost |
|---|---|
| CPU | Setup and movement writes; the VIC-II composites pixels, but sprite DMA takes CPU bus time |
| Memory | 63 shape bytes in a 64-byte-aligned block, plus one screen-relative pointer per sprite |
| Limitation | Eight hardware sprites, each 24×21 pixels before expansion; more visible objects need reuse or software drawing |
When to use: Players, enemies, bullets and cursors that move independently of the background.
When to avoid: Large or numerous moving areas. Reusing sprites further down the screen needs a schedule such as the two-band multiplexer; it cannot create unlimited overlapping sprites on one raster line.
Sprite registers
All live in the VIC-II at $D000. For sprite n, replace the index:
| Register | Purpose |
|---|---|
$D000 + 2n / $D001 + 2n |
X / Y position |
$D010 |
X bit 8 (one bit per sprite — X positions 256–511) |
$D015 |
Enable (one bit per sprite) |
$D027 + n |
Colour |
$D01D / $D017 |
Expand X / Expand Y (double width / height) |
$D01B |
Priority — set bit n to draw the sprite behind the background |
$D01C |
Multicolour select (one bit per sprite) |
$D025 / $D026 |
The two shared multicolour sprite colours |
The pointer is a block number
A sprite pointer is a 64-byte block number relative to the selected 16 KB VIC bank:
pointer = (shape address − VIC bank base) / 64Here the bank starts at $0000 and the shape at $2000, so the pointer is $80 (128). Shape addresses must be aligned to 64 bytes and accessible to the VIC-II in that bank. The CPU’s memory map alone does not tell you what the VIC-II will fetch.
The eight pointers occupy screen base + $3F8 through $3FF: $07F8–$07FF with the screen at $0400. Moving the screen moves its pointer table; changing the VIC bank changes where the same pointer value fetches its shape.
Multicolour sprites
Setting a sprite’s bit in $D01C switches it to multicolour mode: pixels are read in pairs, giving four colours (transparent, the two shared colours in $D025/$D026, and the sprite’s own colour) at half the horizontal resolution — 12 fat pixels instead of 24. The trade is colour for detail, and it’s how most C64 game characters were drawn.
What has been checked
The source and verification recipe compares Asm198x and ACME output, then checks five positions in Emu198x and VICE on PAL and NTSC: 20 captures. It checks every lit pixel and guest register snapshots, including the X=255/256 boundary, colour, shape and preservation of the other sprites’ shared bits. Removing the ninth X-bit update must fail the picture check. These observations do not measure DMA cost or establish multicolour, expansion or original-hardware behaviour.
Related
Patterns: Sprite Movement & Bounds, Game Loop (Basic)
Vault: VIC-II | Commodore 64
Sources
Commodore, Commodore 64 Programmer’s Reference Guide (1983), chapter 3, pp. 101–104 (VIC banks and screen memory), 131–144 (sprite data, pointers, modes, positioning and priority). MOS Technology, 6567 Video Interface Chip (VIC-II), Preliminary, “System Interface / DMA Operation” (sprite fetches and CPU bus access).
Explore the cross-platform sprite family for software drawing, hardware limits, restoration and overload policies.