VBlank Interrupt Tally

Signals & Audio

A real C VBlank interrupt handler colliding with native 16-bit mainline code. This is compiler stress-test #123 — the ROM that exposed an asynchronous M/X-width bug in the llvm-mos 65816 backend, and now runs green with the fix.

loading core…

Self-running — the interrupt tally fills to N=078 (120 decimal), then freezes at verified CRC DA3B.

Click the screen, then play. (Tab away and it pauses.)

The bug this ROM found

The 65816 has two width flags: M controls whether A is 8 or 16 bits, and X controls whether X/Y are 8 or 16 bits. A hardware interrupt stacks the processor status for RTI, but the live handler initially inherits whatever widths the interrupted instruction was using.

The old compiler emitted an ISR beginning cld; pha while assuming ordinary M8/X8 C-function entry. If NMI landed inside a native-width region, that first pha pushed two bytes instead of one. The rest of the generated save/restore sequence no longer balanced, so the result depended on the exact landing point.

Before: default M8/X8 returned 0xDA3B, but +mos-a16 returned 0xF4F4 or 0x0000, and +mos-xy16 returned 0x0000.

Fix: the holistic #321 native-width patch now wraps every native-mode 65816 C ISR in a fixed-width outer save: rep #$30; pha; phx; phy; sep #$30. On exit it restores under M16/X16, then RTI restores the hardware-stacked P and the exact interrupted widths.

After: host, default, a16, and xy16 all return 0xDA3B; three repeated a16 runs are deterministic.

What the gate does

The ISR is armed for exactly 120 VBlanks. Each NMI updates volatile 16- and 32-bit tallies while the main loop continuously performs unrelated native-width arithmetic. On interrupt 120 the ISR disarms itself and publishes a done byte, making the snapshot deterministic without pretending interrupt timing is deterministic.

void nmi(void) __attribute__((interrupt));
void nmi(void) {
    if (armed) {
        uint16_t next = tally + 1;
        tally = next;
        wide += 0x10001u + next;
        if (next == 120) { armed = 0; done = 1; }
    }
}

The browser's Verify fidelity button reads the resulting CRC directly from emulated WRAM. It is the same assertion used by the compiler gate.

Compiler stress-test #123, Round 7. Source and full investigation live in llvm-mos-65816; the fix is folded into the holistic 0002-321-accum16.patch.