Code status: assembled and emulator-executed. PAL and NTSC C64 with the KERNAL IRQ entry, in Emu198x and VICE. The recorded checks identify the tested sources and observations. These are configuration-specific emulator checks; original-hardware verification remains separate.
Split the background
The example shows a black upper background and a blue lower background. It uses two raster interrupts per frame: one near the top to restore black, and one to select blue farther down.
The VIC-II requests an interrupt when its raster counter matches the programmed value. The CPU still has to enter the interrupt, run the KERNAL prologue and reach our colour write. The compare line and the visible colour change are different events. In this caller, compare line 130 gives a first blue displayed line of 131.
Run it
git clone https://github.com/code198x/code-samples.git
cd code-samples/commodore-64/patterns/assembly/rendering/raster-splits
makemake uses Asm198x to produce build/demo.prg. Load it in a PAL or NTSC C64 emulator and type RUN. Reset to leave.
The caller selects a normal 40-column, 25-row text display, clears it to spaces and disables sprites. It owns the screen, VIC bank, colours, IRQ vector and CIA interrupt masks. KERNAL ROM stays mapped, but its normal keyboard scan and software clock service are no longer called. There is no BASIC return or RESTORE-key interface.
; Load and RUN. Reset to leave. Owns display and interrupt configuration.
; KERNAL remains mapped: its IRQ prologue saves A/X/Y for our handler.
* = $0801
!byte $0b, $08, $0a, $00, $9e
!text "2064"
!byte 0, 0, 0
* = $0810
start:
sei
lda #$37
sta $01
lda #$7f
sta $dd0d
lda $dd0d
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 ; 25 rows, Y scroll 3
lda #$08
sta $d016 ; 40 columns, single-colour text
lda #0
sta $d015
sta $d020
sta $d021
sta $d01a
lda #$0f
sta $d019
ldx #0
lda #32
clear_screen:
sta $0400, x
sta $0500, x
sta $0600, x
sta $06e8, x
inx
bne clear_screen
jsr split_init
lda #$5a
ldx #$a5
ldy #$3c
cli
wait_top:
cmp #$5a ; IRQ must preserve the foreground registers
bne failed
cpx #$a5
bne failed
cpy #$3c
bne failed
bit $d011
bmi wait_top
wait_bottom:
bit $d011
bpl wait_bottom
inc frames
picture_ready:
jmp wait_top
failed:
inc errors
halt:
jmp halt
frames: !byte 0
errors: !byte 0
!source "split.inc"The foreground loop checks that interrupts preserve A, X and Y, and counts completed frames. Those checks make the demonstration observable; a game would put its own foreground work here.
The interrupt routine
Call split_init with IRQs masked, KERNAL and I/O visible, CIA2 sources disabled and the display already configured. It changes A and flags, preserves X and Y, and leaves IRQs masked. The caller executes CLI when ready.
; Call with IRQs masked, KERNAL and I/O visible, and CIA2 sources disabled.
; Owns the KERNAL IRQ vector, CIA1 masks and VIC raster IRQ. Caller enables IRQs.
; Normal text display; foreground must service IRQs promptly.
VIC_RASTER = $D012 ; Read current raster; write compare (low 8 bits)
VIC_CTRL1 = $D011 ; Read current raster bit 8; write compare bit 8
VIC_IRQ = $D019 ; Interrupt status — write a 1 to a bit to acknowledge
VIC_IRQEN = $D01A ; Interrupt enable — bit 0 = raster
BGCOLOR = $D021 ; Background colour
CIA1_ICR = $DC0D ; CIA #1 interrupt control
IRQVEC = $0314 ; KERNAL IRQ vector (low byte / high byte)
SPLIT_LINE = 130 ; Compare value, not the time of the colour write
split_init:
sei ; no interrupts while we wire this up
lda #<raster_irq ; point the IRQ vector at our handler
sta IRQVEC
lda #>raster_irq
sta IRQVEC+1
lda #$7f ; switch off CIA #1 timer interrupts...
sta CIA1_ICR
lda CIA1_ICR ; ...and acknowledge any that were pending
lda #$01 ; enable the VIC-II raster interrupt
sta VIC_IRQEN
lda #0 ; first interrupt at the top of the screen
sta VIC_RASTER
lda VIC_CTRL1 ; clear compare-line bit 8 (our lines are < 256)
and #$7f
sta VIC_CTRL1
lda #$01 ; acknowledge any pending raster interrupt
sta VIC_IRQ
rts
; The handler fires twice a frame: once at the top, once at SPLIT_LINE.
; Read the raster line to tell which, paint that zone, and aim at the other.
raster_irq:
lda #$01
sta VIC_IRQ ; acknowledge the raster interrupt
lda VIC_RASTER
cmp #SPLIT_LINE
bcs at_split
at_top:
lda #$00 ; black above the split
sta BGCOLOR
lda #SPLIT_LINE ; next interrupt: the split line
sta VIC_RASTER
jmp $EA81 ; KERNAL exit — restore registers and RTI
at_split:
lda #$06 ; blue below the split
sta BGCOLOR
lda #0 ; next interrupt: top of the next frame
sta VIC_RASTER
jmp $EA81
split_end:Four connections make this work:
- KERNAL entry. The ROM IRQ prologue saves A, X and Y before jumping through the RAM vector at
$0314/$0315. Our handler exits through$EA81, which restores those saved registers and returns from the interrupt. This pairing requires the expected C64 KERNAL ROM; it is not a direct hardware-vector handler. - Exclusive interrupt ownership. Writing
$7Fto CIA1’s$DC0Ddisables its interrupt sources; reading the register acknowledges pending ones. The caller also disables and acknowledges CIA2 sources. Do not enable another IRQ source or executeBRKwhile this handler owns the vector. - Raster comparison. Writing
$D012sets the low eight compare bits; reading it returns the current line. Bit 7 of$D011has the same distinction for bit 8. Both scheduled lines here are below 256. - Acknowledgement. Writing 1 to bit 0 of
$D019clears the raster interrupt latch. Writing$01to$D01Aenables only that VIC interrupt source.
The handler reads the current line to choose the region, changes $D021, then schedules the other interrupt. This assumes prompt service: masking IRQs across a split or adding a long handler can miss the intended schedule. $D021 changes the background behind the spaces; it does not change the border colour in $D020.
Where the change appears
The verification runs the same caller with four compare values. These are observations of this program, not general latency guarantees:
| Compare line | First visible blue pixels | First fully blue display line |
|---|---|---|
| 64 | 65 | 65 |
| 130 | 131 | 131 |
| 131 | Part-way across 132 | 133 |
| 200 | 201 | 201 |
Both emulators and both video standards produce these row ranges. The numbers are hardware raster lines, not screenshot rows or sprite Y coordinates.
With this display’s vertical scroll setting, line 131 is a badline: the VIC-II fetches a row of screen pointers and stalls the CPU. The extra delay moves the colour write farther across the following line. The remaining pixels on that line become blue; pixels already drawn stay black.
Foreground instruction timing can also vary the horizontal position of a write. These sampled frames do not measure the full jitter range under arbitrary workloads. Enabling sprites, changing the display mode or moving code can alter timing too.
Trade-offs and alternatives
| Aspect | Cost or limit |
|---|---|
| Code | 81 assembled bytes for setup and handler, excluding the caller and KERNAL routines |
| State | No private handler state; two diagnostic bytes in the caller |
| CPU | Two interrupts per frame, including CPU entry, KERNAL work and possible DMA stalls; no universal cycle bound is claimed |
| Timing | A delayed, unstabilised register write; no guarantee of a fixed horizontal edge |
| Ownership | Exclusive raster IRQ use; normal KERNAL keyboard and clock servicing is suspended |
Use this approach to learn mid-frame register changes or create regions where the exact horizontal transition is unimportant. A fixed score panel above a scrolling playfield also needs separate screen/scroll configuration and careful scheduling; changing the background alone does not build that panel.
For a static display, polling in the basic game loop is simpler. For several colour bands, a handler can advance through line/colour pairs, provided each next compare is installed before its deadline. The verified example here has only two regions.
For an edge that must land at a fixed horizontal position, use a stable raster technique with a measured, model-specific schedule. Adding a few NOPs alone does not remove unknown interrupt-entry delay.
What has been checked
The verification recipe compares Asm198x and ACME binaries and checks 48 captures of the maintained example, plus 12 captures of the original listing. It checks frame progress, A/X/Y preservation and every coloured pixel, including the partial-line transition. Fixed capture origins account for each emulator’s border crop. Emu198x runs normally after debugger observations to emit a fresh frame; a live colour-change control verifies that refresh and reproduces the stale debugger-only image.
Twelve deliberately broken captures replace the blue write with black; the same picture observer rejects all of them. The observations establish the displayed result under the stated conditions, not the exact CPU bus cycle of the write or behaviour on every C64 revision.
Related
Patterns: Game Loop (Basic), Hardware Sprites, Sprite Multiplexing
Vault: VIC-II | Commodore 64
Sources
Commodore Semiconductor Group, 6567 Video Interface Chip Specification Sheet, drawing 318014, sheets 11 (raster and IRQ registers) and 15 (system interface and DMA). Commodore, Commodore 64 Programmer’s Reference Guide (1983), pp. 150–152 (raster/IRQ registers). The transition table reports the linked execution record.