A Simple Technique for Overlapping IRQ Handlers

Written by Trident .:•:.

A Simple Technique for Overlapping IRQ Handlers

Overlapping IRQ handlers is a a neat trick that allow us to write long-running subroutines without worrying that they will interfere with our other raster interrupts.

This article introduces a simple macro that allow us to do overlapping IRQ handlers so easily that they can be used for pretty much every long-running subroutine we call from our raster interrupts.

There is a longer version of this in my talk at Fjälldata 2026:

Rastertime on the C64

On the C64, the CPU and the graphics chip (the VIC-II) are in perfect synch. Each cycle, the CPU runs one cycle and the VIC chip runs one cycle. Sometimes the VIC chip needs to access memory and then it steals a few cycles from the CPU.

This means that everything the CPU does can be measured in the time it takes to paint pixels on the screen. Each CPU cycle matches 8 pixels and every line of pixels corresponds to 63 cycles (unless there are cycled stolen by the VIC chip) – this is typically called one raster line.

This is typically what we mean with rastertime: the time between two raster lines on the screen. Rastertime is typically measured in rasterlines. So if we say that a subroutine has a rastertime of 10 lines, that means that, roughly speaking, that the subroutine consumes some 630 cycles of CPU time.

Rastertime can be visualized easily. By setting the border color to a different color before the subroutine is called and resetting it after the subroutine has returned, we get an immediate visual repressentation of that subroutine’s rastertime.

Raster time

The code for this, using anonymous IRQ handlers, looks something like this:

irq:
{
    irq_wait_rasterline($80)
    dec $d020
    jsr subroutine
    inc $d020

    jmp irq
}

Rastertime Between Interrupts

Almost everything we do on the screen in a C64 demo has to be updated at 50 frames per second for it to look smooth. With 63 cycles per rasterline, and 312 rasterlines per frame, that gives us a total cycle budget of 19656 cycles per frame. On top to of that, we may lose a few thousand cycles because of the VIC chip stealing cycles (depending on how we tweak it at runtime).

But, in addition to the total cycle budget, we often have to face another challenge: the rastertime between two interrupts. By default on the C64, when one interrupt is handled, other interrupts are blocked. Once we return from the interrupt handler, new interrupts can occur.

In the pictures below, the red arrows indicate where a raster interrupt happens. In the left picture, the first raster interrupt is handled in good time before the second raster interrupt happens. This is fine: the first interrupt handlers will have returned, and re-enabled interrupts, before the second one occurs.

But things are not as good in the right picture. Here, the second raster interrupt is scheduled to occur before the first one completed. This will result in a frame-skip: the second raster interrupt will happen on in the next frame.

Frame-skips are the bane of every C64 demo programmer. They will appear as highly visble glitches on the screen and, because the music routine has to be called at exactly 50 Hz, there will be an audiable glitch as well. Horrible!

We can solve this situation in several different ways:

  1. Split the subroutine in the first interrupt handler into two chunks, and call the second chunk in the second interrupt handler.
  2. Call the long-running subroutine from the main loop instead of from the interrupt handler.
  3. Implement a multi-tasking mechanism.
  4. Implement overlapping IRQ handlers.

The talk in the video above goes into detail on how to do 1-3 in the list above. Here we will focus on number 4, overlapping IRQ handlers.

Overlapping IRQ Handlers

Overlapping IRQ handlers is a technique often used by C64 demo coders to avoid rastertime problems between interrupt handlers.

This article presents a very simple implementation of overlapping IRQ handlers that adds no additional complexity to the code that is using it. Is so easy to use that it can be used widely, even in situations where there is plenty of rastertime available. This makes the resulting code much more flexible than it otherwise would have been: the called subroutines can now use as much rastertime as needed, without having to cater for them being called from an interrupt context.

The overhead is very low: 12 bytes of memory and 19 cycles per usage.

How Overlapping IRQ Handlers Work

The idea is simple: inside the interrupt handler, set up the next raster interrupt, acknowledge the existing raster interrupt, then explicitly enable interrupts, before calling the subroutine you wish to call. The next raster interrupt will now take place on top of the subroutine, in case it has not yet completed.

In case the subroutine had already completed, the effect will be exactly the same as when running with no overlapping IRQ handlers: the next raster interrupt handler will execute when its interrupt happens.

The prereuisite for this technique is that we store and restore processor registers to the stack in our interrupt handlers. This ensures that they will be able to execute on top of each other.

Handling Reentrancy With One Flag

But there is one big problem that we need to solve before we can run overlapping IRQ handlers as our default mechanism: reentrancy. That is, what happens if our subroutine does not finish until its interrupt handler gets called again, on the next frame?

If our subroutine gets called again on top of each other, two problems will happen:

  • We may run out of stack, if this happens over and over again.
  • The subroutine may fail completely, because its code was never intended to be called on top of itself.

Both of these problems are difficult to debug. They will most likely result in a seemingly random crash because the stack overwrites itself or because of a seemingly random failure of the called subroutine.

Fortunately, there is an easy fix for this problem. For every overlapping IRQ handler, we keep a flag that signals if the subroutine is currently being called. If it is already called, we avoid calling it on top of itself.

We can even save memory by encoding this flag as self-modifying code inside the calling code.

The Code

All of this can now be nearly implemented as a new macro, in addition to our anonymous IRQ handlers:

.macro irq_call_wait_rasterline(subroutine, rasterline) {
   irq_setup(next, rasterline)

    // This is the flag that indicates if the subroutine has been called or not:
running: lda #0
    // If we have already called this subroutine, we will not call it again now
    bne dont_call

    // Set the flag so that any subsequent invokations will avoid calling the subroutine
    inc running + 1

    // Acknowledge the raster interrupt with the VIC
    inc $d019

    // Enable IRQs for the CPU
    cli

    // Call the subroutine - this call may take a very long time
    jsr subroutine

    // Once the subroutine is done, we reset the flag again
    lda #0
    sta running + 1
dont_call:
    // We exit the interrupt handler here, 
    // both if we called the subroutine, or if we skipped it
    irq_leave()
next:
    // We will arrive here because of the raster intterupt on the line given by rasterline
    irq_enter()
}

That’s it! Not quite as neat as the anonymous IRQ handlers, but reasonably self-explanatory.

Now let’s take a look at how to use this. Let’s say we want to call three subroutines, which we might call red, green, and blue, on given places on the screen. We simply implement our IRQ code like this:

irq:
{
    irq_wait_rasterline($18)

    // We are now at rasterline $18, call red and wait until rasterline $64
    irq_call_wait_rasterline(red, $64)

    // We are now at rasterline $64, call green and wait until rasterline $b0
    irq_call_wait_rasterline(green, $b0)

    // We are now at rasterline $64, call blue and wait until rasterline $ff
    irq_call_wait_rasterline(blue, $ff)

    // We are now at rasterline $ff

    jmp irq
}

This will result in the following screen:

Overlapping IRQ handlers

In this case, the subroutines did not actually end up overlap each other at all. But if we slightly change the raster lines, we get a different picture:

Overlapping IRQ handlers

Here we see the green subroutine interrupting the red one. Then green is itself interrupted by the blue subroutine. When the blue subroutine finishes, we see green continue until it is done. Then red will finally get the CPU back and can continue until it is finished.

In this example we also see another sideeffect of overlapping IRQ handlers: we have an ordering of priority. The subroutine that gets called first has to wait for the later ones to finish.

Whe to use Overlapping IRQ Handlers

Because of the easy-to-use macro, overlapping IRQ handlers can be used pretty much every time you need to call a potentially long running subroutine from an interrupt handler.

Myself, I use them all the time, making me worry way less about rastertime than I used to do. See the talk for a few examples.

The Full Source Code

For reference, here is the full source code for this (in Kickasm format):

.macro irq_setup(irqhandler, rasterline) {
    lda #rasterline
    sta $d012 // Set the low 8 bits of the rasterline in $d012
    lda $d011 // Set the 9th bit of the rasterline in the 8th bit of $d011
    .if (rasterline < $100) {
        and #$7f
    } else {
        ora #$80
    }
    sta $d011

    lda #<irqhandler
    sta $fffe
    lda #>irqhandler
    sta $ffff // Set the interrupt handler address
}
.macro irq_enter() {
    pha
    txa
    pha
    tya
    pha  // Store registers on the stack
}
.macro irq_leave() {
    inc $d019 // Acknowledge the raster interrupt

    pla
    tay
    pla
    tax
    pla  // Restore processor registers from the stack
    rti  // Return from interrupt
}

.macro irq_wait_rasterline(rasterline) {
    irq_setup(anonymous, rasterline)
    irq_leave()
anonymous:
    irq_enter()
}


.macro irq_call_wait_rasterline(subroutine, rasterline) {
   irq_setup(next, rasterline)

running: lda #0
    bne dont_call
    inc running + 1
    inc $d019
    cli
    jsr subroutine
    lda #0
    sta running + 1
dont_call:
    irq_leave()
next:
    irq_enter()
}

    * = $0801
    BasicUpstart(start)

    * = $0810
start:
{
    lda #$7f
    sta $dc0d // Turn off timer interrupts
    lda $dc0d // Acknowledge any lingering timer interrupt

    sei       // Disable interrupts while we work on the interrupt settings

    lda #$35
    sta $01   // Turn off ROM

    lda #$01
    sta $d01a // Enable raster interrupts

    irq_setup(trampoline, 0) // Set up our trampoline interrupt at the top of the screen

    cli       // Enable interrupts

              // Here we could load the next part, but for now, just do an
    jmp *     // infinite loop - the interrupt handlers drive everything now

trampoline:
{
    irq_enter()
    jmp irq
}
}

irq:
{
    irq_wait_rasterline($18)

    lda #0
    sta $d011

    irq_call_wait_rasterline(red, $64)

    irq_call_wait_rasterline(green, $b0)

    irq_call_wait_rasterline(blue, $ff)

    jmp irq
}

red:
{
    lda $d020
    pha
    lda #2
    sta $d020
    ldx #0
!:
    {
        ldy #14
    !:
        dey
        bpl !-
    }
    inx
    cpx #40
    bne !-
    pla
    sta $d020
    rts
}


green:
{
    lda $d020
    pha
    lda #5
    sta $d020
    ldx #0
!:
    {
        ldy #14
    !:
        dey
        bpl !-
    }
    inx
    cpx #40
    bne !-
    pla
    sta $d020
    rts
}


blue:
{
    lda $d020
    pha
    lda #6
    sta $d020
    ldx #0
!:
    {
        ldy #14
    !:
        dey
        bpl !-
    }
    inx
    cpx #40
    bne !-
    pla
    sta $d020
    rts
}