Skip to content

Stable Raster

Taking the wobble out of raster interrupts

A raster interrupt on the Commodore 64 starts its work a few cycles late, by a different amount each time, because the processor must finish the instruction it is running. Stable raster routines, most often built from two interrupts in a row, remove that last jitter so a program can change the screen on an exact cycle.

“Too many graphical effects, also called raster effects, have been coded in a very sloppy way,” Marko Mäkelä wrote in the online magazine C=Hacking in 1995. “For instance, if there are any color bars on the screen in a game or demo, the colors often jitter a bit.” A program that changes the border colour from a raster interrupt at line 100 gets its colour change on line 100, but not at the same point along the line each frame, so the edge of the colour shivers from side to side. A stable raster routine removes that shiver. It finds out exactly where the beam is, to the cycle, and waits just long enough to line up, so that everything after it runs at the same point on the screen in every frame.

On the C64 one processor cycle is 8 pixels wide: the VIC-II’s pixel clock runs at eight times the processor clock. A few cycles of jitter is therefore a ragged edge tens of pixels wide, and some effects, such as opening the side borders, fail completely unless a register write lands on one particular cycle.

Where the jitter comes from

The VIC-II checks for the interrupt line at the start of each raster line, in cycle 0, and pulls the processor’s IRQ line low. Bauer’s description of the chip gives the exact point, and adds that for line 0 the check happens in cycle 1. The processor does not stop at once.

  • The current instruction finishes first. 6510 instructions take 2 to 7 cycles, so the delay depends on what the main program happened to be doing. MOS Technology’s own programming manual works through the worst case: an interrupt “might be occurring in the middle of a seven cycle, read/modify/write instruction”. Mäkelä counted the result: “the beginning position of the interrupt can jump between 7 different positions.”
  • The interrupt sequence itself takes 7 cycles, to push the program counter and status register and fetch the vector. That part is fixed, and so is any register saving the handler does next, but it pushes the useful work further along the line.
  • Badlines and sprites. Bauer notes that the 6510 recognises an interrupt only while its RDY line is high. On a badline the VIC-II holds RDY low for 40 to 43 cycles while it reads character codes, and each sprite on the line takes another 2 cycles with a 3-cycle warning. An interrupt that arrives during a stall waits for it to end. The VIC-II entry has the figures.

A handler that saves A, X and Y on the stack in the usual way (13 cycles) therefore starts its real work some 20 to 27 cycles after the start of the line, before any stolen cycles are counted.

The double interrupt

The method that became standard on the C64 uses a second interrupt to measure the first. Mäkelä, who took the idea from a routine by his friend Andreas Boose, described it like this:

  1. The first raster interrupt arrives, with the usual jitter. Its handler sets up a second raster interrupt for the next line, points the interrupt vector at a second handler, acknowledges the first interrupt and re-enables interrupts with CLI.
  2. It then runs a row of NOP instructions. Every NOP takes exactly 2 cycles, so when the second interrupt arrives during the row, the processor is at most one cycle away from the end of an instruction. The jitter falls from up to seven cycles to one.
  3. The second handler reads the raster register at the moment it is about to change. Either it has just changed or it will change on the next cycle, so comparing two reads shows which. “You just need to compare if the register changed one cycle too early or not,” Mäkelä wrote, “and delay a cycle when needed. This is easily accomplished with a branch to the next address.”

The second handler never returns to the first. It throws away the stack frame the second interrupt pushed, finishes the effect and returns straight to the main program.

A worked routine

This version follows the one TWW of the group Creators published on the Codebase64 wiki. It assumes the KERNAL is switched out ($35 in location 1), so the processor fetches the interrupt vector straight from $FFFE, and it is timed for a PAL machine with 63-cycle lines. The first line and the next interrupt’s line must not be badlines, and no sprites may be shown on them.

Assembly
irq1:   pha                 ; save A, X and Y (13 cycles)
        txa
        pha
        tya
        pha

        lda #<wedge         ; the next interrupt goes to "wedge"
        sta $fffe
        lda #>wedge
        sta $ffff
        inc $d012           ; ...on the next raster line
        lda #$01
        sta $d019           ; acknowledge this interrupt
        tsx                 ; remember the stack pointer
        cli                 ; let the next interrupt in
        nop                 ; 2 cycles each: the next interrupt
        nop                 ; arrives somewhere in this row, so
        nop                 ; it can be late by at most one cycle
        nop
        nop
        nop
        nop
        nop

wedge:  txs                 ; drop the second interrupt's stack frame
        ldx #$08            ; wait until the end of the line
-       dex
        bne -
        bit $00             ; 3 cycles
        lda $d012
        cmp $d012           ; has the line changed between the two reads?
        beq +               ; not yet: 3 cycles; already: 2 cycles
+
        ; From here on, every instruction runs on the same cycle of
        ; the line in every frame.
        lda #1
        sta $d020           ; for example, change the border colour

        lda #<irq1          ; put the first handler back
        sta $fffe
        lda #>irq1
        sta $ffff
        lda #100            ; choose the line for the next interrupt
        sta $d012
        lda #$01
        sta $d019           ; acknowledge the second interrupt
        pla                 ; restore Y, X and A
        tay
        pla
        tax
        pla
        rti                 ; back to the main program

The branch beq + jumps to the very next instruction, so it changes nothing except time. A taken branch costs 3 cycles and an untaken one 2. If the two reads of $D012 match, the routine is running one cycle early, and the taken branch spends the extra cycle; if they differ, it is already one cycle late and the branch falls through. Either way the next instruction starts on the same cycle.

The delay before the comparison is what makes it work, and it depends on the line length. TWW’s routine gives the settings for all three kinds of VIC-II:

Chip Cycles per line Delay loop Padding Extra NOP in the row
6569 (PAL) 63 ldx #$08 bit $00 no
6567R56A (early NTSC) 64 ldx #$08 nop nop no
6567R8 (later NTSC) 65 ldx #$09 none yes

The comparison only works if it falls exactly across the moment the line number changes, so a routine tuned for one line length does not stabilise on a machine with another.

Other methods

The double interrupt was not the only way. Mäkelä’s article surveys the alternatives he knew:

  • Polling the raster register over several lines, spending a cycle more whenever it changed too early. He counted busy-waiting and polling among “the ugliest ways” to synchronise, though he used a polling loop once, at start-up, to line up his timers.
  • The light pen latch. The VIC-II records the beam position when its light pen input changes, and that input is wired to the keyboard matrix, so a program can trigger it itself. Bauer points out that this gives the beam’s horizontal position, which the chip offers no other way to read, and that it “can e.g. be used to synchronize raster interrupt routines on exact cycles”. Mäkelä’s objection was practical: it “will fail if the user presses the fire button on Joystick port 1”.
  • An auxiliary timer. A CIA timer running in step with the screen tells the interrupt routine how late it is, and the routine waits that many extra cycles. Mäkelä used this on the VIC-20, whose video chip has no raster interrupt, and noted that on the C64 he had “never seen the auxiliary timer scheme being used”.

What can still go wrong

A stable routine is stable only while nothing else steals cycles. TWW’s routine lists its own enemies: other interrupts, a raster line of $FF, sprites, and a badline on the line or the one before it.

The RESTORE key is one of those other interrupts. It pulls the processor’s NMI line, which SEI cannot block. Mäkelä’s example programs show the usual defence: make CIA 2’s timer raise an NMI and never acknowledge it, so the NMI line stays low and later presses of RESTORE cause no new interrupt. Because the CIA’s timer interrupt arrives one cycle later on the older 6526 than on later chips, cycle-exact code that uses the CIA timers can also depend on which CIA the machine has; see CIA 6526.

What it costs and what it buys

Each stabilised split costs the main program most of two raster lines, since the first handler waits out a whole line in its NOP row and the second waits almost another before it compares. In return, register writes land on known cycles, and several well-known VIC-II tricks depend on that. Opening the side borders needs a write to $D016 in exactly cycle 56 of the line (open borders). FLI writes $D011 and $D018 on every line. Colour bars with straight edges need the same precision.

Sources

  • Marko Mäkelä, “Making stable raster routines (C64 and VIC-20)”, C=Hacking issue 10 (June 1995): the jitter, seven positions, the auxiliary timer and double-interrupt methods, Andreas Boose, polling and the light pen, the NMI lock for the RESTORE key, 8 pixels per C64 cycle.
  • Christian Bauer, The MOS 6567/6569 video controller (VIC-II) and its application in the Commodore 64 (1996): section 2.2 (IRQ recognised only while RDY is high); section 3.5 (badlines stun the processor for 40–43 cycles); section 3.11 (light pen position used to synchronise raster routines); section 3.12 (the raster compare in cycle 0, cycle 1 for line 0).
  • MOS Technology, MCS6500 Microcomputer Family Programming Manual (January 1976), p. 138: an interrupt arriving during a seven-cycle read-modify-write instruction, “7 cycles for the interrupt”.
  • TWW/Creators, “Stable Raster Routine”, Codebase64 wiki (web, secondary): the routine this entry’s listing follows, its cycle counts, the PAL and NTSC delay settings and its list of what breaks it. https://codebase.c64.org/doku.php?id=base:stable_raster_routine

See also

Checked. Verified against primary sources and signed off by a person.