Critters
Games & Classics
A swarm of little critters on a Super Nintendo, each running its own scripted patrol as a
protothread — a tiny
coroutine that yields every frame and resumes exactly where it stopped, via a saved
continuation index. Dozens of independent behaviours, cooperatively scheduled. Runs
automatically; no joypad needed. Written in C and compiled with the
llvm-mos-based 65816 toolchain
(+mos-a16, 16-bit accumulator mode).
Self-running demo — each critter runs its own scripted patrol.
Click the screen to focus, then watch. Tab away and it pauses.
What it is
A coroutine is a function that can pause itself and be resumed later, picking up mid-execution. A protothread implements that in a handful of bytes: it remembers a continuation index and, on the next call, switches on it to jump back to where it yielded:
switch (c->lc) { // resume where we left off
case 0:
for (;;) {
walk_right(); c->lc = 1; return; case 1: ; // yield inside loop
walk_down(); c->lc = 2; return; case 2: ;
reverse();
}
}
The case labels land inside the loops, so resuming jumps into the middle of a loop —
the same tangled control flow as Duff's device. Any state the critter needs across frames lives
in its struct, not in local variables. Each of the two-dozen critters is one of these. It stays
bit-exact across every codegen mode.
Compiler stress-test
| Item | What it exercises |
|---|---|
| protothread | A protothread is a coroutine that fits in a few bytes: a function that runs a bit, yields, and later resumes exactly where it stopped. Each critter is one, driving its own scripted patrol. |
| saved case-index | Resumption is a saved continuation index. On re-entry the function switches on it and jumps back to the yield point — with case labels sitting inside the loops, the same irreducible control flow Duff's device uses. |
| state preservation | True local variables would be lost between calls, so the coroutine's state — position, velocity, the loop counter — lives in a struct that persists across re-entry. That cross-call state is the compiler test. |
| bit-exact | Pure branch and pointer arithmetic — no multiply, no divide on the hot path. 24 critters stepped for 120 frames; host == default == +mos-a16 == +mos-xy16 == bsnes-jg, byte for byte. |
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 hash 0xAD9F — a fold of 24 critters stepped 120 frames)
live in this tab. No far pointers — the same source passes every way: host == default ==
+mos-a16 == +mos-xy16 == bsnes-jg, -verify clean.