Raster Interrupts
Precision timing with the video beam
Raster interrupts trigger code at exact screen positions, enabling split-screen effects, colour bars, and multiplexed sprites that defined 8-bit graphics.
The video display is drawn line by line, top to bottom, 50 or 60 times per second. Raster interrupts let your code execute when the electron beam reaches a specific line. This enables effects impossible otherwise: different colours in different screen regions, multiplexed sprites, and split-screen displays.
How it works
Display generation:
Line 0 → top of screen
Line 1 →
... →
Line 100 → [INTERRUPT FIRES HERE]
... → your code runs, changes colours/sprites
Line 200 →
... →
Line 311 → bottom of frame (PAL)
VBlank → vertical retrace
Line 0 → next frame starts
Commodore 64 implementation
Setting up a raster interrupt
setup_raster:
sei ; disable interrupts
; Select raster as interrupt source
lda #$01
sta $d01a ; enable raster interrupt
; Set raster line (low 8 bits)
lda #100
sta $d012
; Clear bit 7 of $d011 for lines 0-255
; (or set for lines 256-311)
lda $d011
and #$7f
sta $d011
; Point IRQ vector to our handler
lda #<raster_handler
sta $0314
lda #>raster_handler
sta $0315
; Acknowledge any pending interrupt
lda #$01
sta $d019
cli ; enable interrupts
rts
raster_handler:
; Change something (colour, sprite, etc.)
lda #2 ; red
sta $d020 ; border colour
; Acknowledge interrupt
lda #$01
sta $d019
; Set up next interrupt at different line
lda #150
sta $d012
; Point to second handler
lda #<raster_handler2
sta $0314
lda #>raster_handler2
sta $0315
; Return from interrupt
jmp $ea31 ; KERNAL IRQ return (PAL — NTSC uses $ea81)
For NTSC C64s the equivalent KERNAL exit point is at $ea81. PAL games typically hard-code $ea31; cross-region games either use a region check or skip the KERNAL entirely (see “Skipping KERNAL” below).
Chained interrupts
Multiple raster interrupts per frame:
raster_handler1: ; line 50 - red border
lda #2
sta $d020
lda #100
sta $d012
; point to handler2
...
jmp $ea31
raster_handler2: ; line 100 - blue border
lda #6
sta $d020
lda #150
sta $d012
; point to handler3
...
jmp $ea31
raster_handler3: ; line 150 - black border
lda #0
sta $d020
lda #50
sta $d012
; point back to handler1
...
jmp $ea31
NES implementation
The NES uses NMI for VBlank and mapper IRQs for mid-screen:
VBlank NMI
nmi_handler:
; VBlank started - safe to update VRAM
pha
txa
pha
tya
pha
; Update sprites
lda #$02
sta $4014 ; OAM DMA
; Update palettes, nametables, etc.
...
pla
tay
pla
tax
pla
rti
MMC3 scanline counter
; Set IRQ to fire at line 100
lda #100
sta $C000 ; set counter
sta $C001 ; reload counter
sta $E001 ; enable IRQ
irq_handler:
; Change scroll, palette, etc.
...
sta $E000 ; acknowledge IRQ
rti
Amiga implementation
The Amiga uses the Copper for beam-synced effects, but can also use interrupts:
Copper-triggered interrupt
copperlist:
dc.w $0180,$0000 ; COLOR00 = black
dc.w $8007,$fffe ; wait line 128
dc.w $009c,$8010 ; trigger copper interrupt
dc.w $ffff,$fffe ; end
; Level 3 interrupt handler
copper_interrupt:
btst #4,$dff01f ; copper interrupt?
beq .not_copper
move.w #$0010,$dff09c ; acknowledge
; Do stuff
...
.not_copper:
rte
Common uses
| Effect | How it works |
|---|---|
| Colour bars | Change background colour at each scanline |
| Split screen | Different scroll values above/below split |
| Sprite multiplex | Reposition sprites after they’re drawn |
| Status bar | Fixed area with different graphics mode |
| Parallax | Different scroll speeds per region |
Timing precision
Jitter
Interrupts don’t fire at exactly the same cycle each time:
Instruction being executed varies
Interrupt acknowledged after instruction completes
Handler entry timing varies by 0-7 cycles
Stable raster
For cycle-exact effects, synchronise first:
stable_raster:
; Known entry point timing
lda #$00 ; 2 cycles
sta $d012 ; 4 cycles
...
; Double interrupt technique or
; NOPs to align to specific cycle
Performance considerations
| Factor | Impact |
|---|---|
| Interrupt latency | ~40 cycles to enter handler (varies 0-7 cycles based on current instruction — see Stable Raster) |
| Handler length | Must complete before next interrupt |
| Nested interrupts | C64 doesn’t auto-nest IRQs — the I flag is set on entry. Re-enable interrupts mid-handler (CLI) only if you’re sure the handler can be re-entered safely |
| KERNAL return | Uses ~30 cycles; skip for speed |
Skipping KERNAL
For maximum speed:
fast_handler:
; Save registers
pha
txa
pha
tya
pha
; Your code here
...
; Acknowledge
lda #$01
sta $d019
; Restore registers
pla
tay
pla
tax
pla
rti ; direct return, skip KERNAL