Skip to content

Raster Splits

Fire an interrupt when the beam reaches a chosen scanline and change a VIC-II register mid-screen — the basis of colour bars, split panels, and beating the chip's limits.

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

Shell
git clone https://github.com/code198x/code-samples.git
cd code-samples/commodore-64/patterns/assembly/rendering/raster-splits
make

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

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

split.inc
; 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:

  1. 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.
  2. Exclusive interrupt ownership. Writing $7F to CIA1’s $DC0D disables its interrupt sources; reading the register acknowledges pending ones. The caller also disables and acknowledges CIA2 sources. Do not enable another IRQ source or execute BRK while this handler owns the vector.
  3. Raster comparison. Writing $D012 sets the low eight compare bits; reading it returns the current line. Bit 7 of $D011 has the same distinction for bit 8. Both scheduled lines here are below 256.
  4. Acknowledgement. Writing 1 to bit 0 of $D019 clears the raster interrupt latch. Writing $01 to $D01A enables 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.

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.