Step-Sequencer VM

Signals & Audio

A tiny register machine runs a looping bytecode "song" on a Super Nintendo, its eight registers driving an 8-bar equalizer. The interesting part is the opcode dispatch: a sparse switch — 14 non-contiguous byte cases (0x00, 0x10, 0x23, … 0xF0) too sparse for a jump table, so the compiler must lower it to a binary-search comparison tree — a different code shape than a threaded computed-goto or a dense jump table. Runs automatically; no joypad needed. Written in C and compiled with the llvm-mos-based 65816 toolchain (+mos-a16, 16-bit accumulator mode).

loading core…

Self-running demo — the VM plays its bytecode song on its own.

Click the screen to focus, then watch. Tab away and it pauses.

What it is

Every interpreter has to answer "what does this opcode do?" — and how the compiler translates that switch depends on the opcode values. Pack them densely (0, 1, 2, 3…) and you get a jump table: one indexed jump, constant time. Spread them out, like real instruction sets do, and a table would be mostly empty holes — so the compiler emits a tree of comparisons instead:

switch (op) {           // 14 NON-CONTIGUOUS cases: 0x00, 0x10, 0x23 ... 0xF0
    case 0x10: r[a] = b;              break;   // SET
    case 0x23: r[a] += r[b];          break;   // ADD
    case 0x47: r[a] ^= r[b];          break;   // XOR
    case 0x88: r[a] = rol(r[a],r[b]); break;   // ROL
    case 0xC1: r[a] ^= tick + b;      break;   // MIX (time)
    case 0xD3: if (r[a]) pc = b;      break;   // JNZ (loop)
    ...                                        // 14 total
}

This VM uses 14 opcodes scattered from 0x00 to 0xF0, forcing the comparison-tree path. The bytecode stirs eight registers with add / xor / rotate / shift and a time-injecting "mix" op so the equalizer never settles. It stays bit-exact across every codegen mode — the tree always picks the right case.

Compiler stress-test

ItemWhat it exercises
sparse switchThe case values (0x00, 0x10, 0x23, 0x47, 0x88, 0xC1, 0xF0 …) are widely spaced. Only ~14 of 241 possible bytes are used, so a jump table would be 94% empty — the compiler builds a binary-search comparison tree instead.
comparison treeA cascade of cmp/branch that narrows to the matching case in ~log₂(14) steps. The disasm gate asserts ≥ 8 immediate compares and ZERO indexed-indirect jumps — proving it is a tree, not a table.
vs #38 / #29aDistinct from the computed-goto threaded VM (#38, indexed-indirect jmp) and a dense jump table (#29a). Three different ways C dispatch lowers on the 65816 — this is the sparse one.
control-flow onlyDeliberately no multiply or divide: the point is the branch structure. A miscompiled comparison tree would pick the wrong case and diverge the differential CRC — which it does not.

Written in C with the llvm-mos 65816 toolchain and verified against bsnes-jg (and MAME where the SPC700 IPL is present). Hit Verify fidelity to reproduce the build gate's WRAM assert (gate CRC 0xE8C5 — a rotate-XOR fold of the VM registers over 400 steps) live in this tab. No far pointers — the same source passes every way: host == default == +mos-a16 == +mos-xy16 == bsnes-jg, -verify clean.