Skip to content

Interrupt Management

Alekmaul edited this page Jul 23, 2026 · 1 revision

PVSnesLib provides a built-in Vertical Blank Interrupt Service Routine (VBlank ISR / NMI handler). In most games you never need to write your own interrupt entry point; instead, you register a callback that is executed every frame during VBlank.

This design keeps the library responsible for critical hardware operations while allowing your game to execute code safely during VBlank.

How the VBlank interrupt works

When VBlank interrupts are enabled, the library ISR executes the following operations every frame:

  • Upload OAM (sprites) when the frame is not a lag frame.
  • Call the user VBlank callback (nmi_handler).
  • Increment the internal VBlank counter.
  • Read joypads automatically.
  • Update the lag-frame counter when necessary.

This execution order guarantees that your callback runs after sprite upload and before controller data becomes available for the next frame.

This is why calling nmiSet() changes the user callback executed by VBlank, not the interrupt vector itself.

The default interrupt handler

Normally an application only needs:

int main(void)
{
    // initialization...

    while(1)
    {
        WaitForVBlank();

        // game logic
    }
}

WaitForVBlank() synchronizes the game loop with the next VBlank interrupt.

Registering a custom VBlank routine

To execute code automatically every frame during VBlank, register a callback with nmiSet().

void myVBlank(void)
{
    // VRAM updates
    // CGRAM updates
    // DMA transfers
}

int main(void)
{
    nmiSet(myVBlank);

    while(1)
    {
        WaitForVBlank();

        // main game logic
    }
}

nmiSet() safely replaces the current callback, enables VBlank interrupts, enables automatic joypad reading, and disables active IRQ interrupts before installing the new handler.

The example graphics/Backgrounds/Mode1ContinuosScroll demonstrates one of the most common uses of nmiSet().
The game scrolls across a map larger than VRAM can hold at once. During gameplay, the main loop determines which background pages must be refreshed.

Instead of writing directly to VRAM, it fills a small structure describing the pending transfers.

bgInfo.refreshBG1 = 1;
bgInfo.refreshBG2 = 1;

The registered VBlank callback myconsoleVBlank checks these flags during VBlank and performs the required VRAM transfers using DMA.

Sometimes the game requires a completely different VBlank routine.

For example:

  • loading a level
  • streaming graphics
  • performing large tilemap updates
  • enabling special rendering effects

Instead of testing many conditions inside a single interrupt handler, PVSnesLib allows replacing the callback at runtime.

void normalVBlank(void)
{
    // regular frame updates
}

void loadingVBlank(void)
{
    // transfer level graphics
}

...

nmiSet(normalVBlank);

...

// During level loading
nmiSet(loadingVBlank);

...

// Return to the normal handler
nmiSet(normalVBlank);

Because nmiSet() safely updates the callback, it should always be preferred over writing directly to nmi_handler. Writing to nmi_handler directly is not atomic and can crash if a VBlank occurs while the function pointer is being modified.

What should go inside the VBlank callback?

Only operations that must occur during VBlank should be placed here.

Typical examples include:

  • DMA transfers
  • VRAM uploads
  • Tilemap updates
  • Palette updates
  • Register updates that require VBlank

Game logic, AI, physics and collision detection should remain inside the main loop whenever possible.

A short interrupt routine leaves more VBlank time available for graphics transfers.

Important

Keep the VBlank callback as short as possible.
Only perform operations that require VBlank.
Do not place gameplay logic inside the interrupt unless it must execute there.
Exchange data between the main loop and the VBlank callback through shared variables or flags. Always use WaitForVBlank() in the main loop to keep gameplay synchronized with the display refresh.

Native and Emulation Interrupt Vector Tables (hdr.asm)

Every PVSnesLib project contains a hdr.asm file that defines the SNES cartridge header and the CPU interrupt vector tables.

Near the end of the file you will find two sections similar to the following:

.SNESNATIVEVECTOR               ; Native Mode interrupt vector table
  COP    EmptyHandler
  BRK    EmptyHandler
  ABORT  EmptyHandler
  NMI    VBlank
  IRQ    EmptyHandler
.ENDNATIVEVECTOR

.SNESEMUVECTOR                  ; Emulation Mode interrupt vector table
  COP     EmptyHandler
  ABORT   EmptyHandler
  NMI     EmptyHandler
  RESET   tcc__start
  IRQBRK  EmptyHandler
.ENDEMUVECTOR

These tables are required by the SNES hardware. They tell the 65C816 CPU which function to execute when an interrupt or exception occurs.

Native Mode vs Emulation Mode

The 65C816 processor has two operating modes:

  • Emulation Mode, which behaves like a 6502-compatible CPU.
  • Native Mode, which exposes the full 16-bit capabilities of the 65C816.

PVSnesLib switches to Native Mode immediately after reset. As a result:

  • the Emulation Mode vectors are only used during startup, before the C runtime is initialized;
  • once the game begins, only the Native Mode vectors are normally used.

Native Mode vector table

.SNESNATIVEVECTOR
  COP    EmptyHandler
  BRK    EmptyHandler
  ABORT  EmptyHandler
  NMI    VBlank
  IRQ    EmptyHandler
.ENDNATIVEVECTOR

Each entry corresponds to one CPU exception.

Vector Description Default handler
COP Software coprocessor instruction EmptyHandler
BRK Software breakpoint EmptyHandler
ABORT Memory abort (rarely used on SNES) EmptyHandler
NMI Vertical Blank interrupt VBlank
IRQ Maskable interrupt (HBlank, timers, peripherals...) EmptyHandler

Clone this wiki locally