Skip to content

Hardware Sprites

Show a complete 24×21 sprite, set its ninth X bit, and preserve the other sprites’ shared registers. Verified on PAL and NTSC emulators.

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:

Shell
asm198x --dialect acme --prg demo.asm -o demo.prg

Reset 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.

demo.asm
; 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, 0

Try 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.inc
; 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
    rts

The 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.

shape.inc
; 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 0

The 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:

Code
pointer = (shape address − VIC bank base) / 64

Here 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.

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.