Boids Flock
Physics & Simulation Reynolds boids flocking, computed live on a
Super Nintendo — and built entirely on a vec2 { int16_t x, y } struct that
the steering kernel passes and returns by value. On the 16-bit 65816 a 4-byte aggregate
return forces the compiler's aggregate-return ABI — a small-struct register pair versus
an sret hidden pointer — and the three
rules (separation, alignment, cohesion) chain those returns
O(N²) times a frame. A correct ABI paints a coherent flock: dots clump, align
into streams (each boid coloured by its heading, so aligned birds share a hue) and dodge
each other. Get the returned struct wrong and the flock scatters into noise. The picture is the
proof — and it is bit-for-bit identical to host x86. Runs automatically; no joypad
needed. Written in C and compiled with the
llvm-mos-based 65816 toolchain
(+mos-a16).
Self-running demo — the flock swirls and the heading colours flow. (A title card shows first; the flock fades in after it.)
Click the screen to focus, then watch. Tab away and it pauses.
What it is
Each dot is a boid — a simple agent steered by three rules over its neighbours:
separation (don't crowd), alignment (match the flock's heading) and
cohesion (drift toward the flock's centre). Run them on a few dozen agents and a
lifelike flock emerges. The twist here is purely about the compiler: the velocity and position
of every boid is a vec2 struct, and every steering helper takes and returns
that struct by value:
typedef struct { int16_t x, y; } vec2; // a 4-byte aggregate
vec2 v2_add(vec2 a, vec2 b); // by-value in, by-value out
vec2 separation(const Boid *f, ...); // each rule RETURNS a vec2
vec2 acc = v2_add(v2_add(sep, ali), coh); // chained aggregate returns
On a register-rich CPU that is free. On the 65816 a returned vec2 must travel by the
ABI's aggregate-return path — a register pair if it fits, otherwise an sret
hidden pointer the caller allocates — and the steering composition hits that path thousands of
times per frame. The flock lives in bank-0 WRAM (no far pointers), so the same kernel compiles
and runs five ways, all asserted equal.
Compiler stress-test #26 — struct by value
A Round 2 demo — each Round-2 entry targets a codegen corner the first twenty never execute. Here it is the aggregate / by-value struct ABI: structs exist in earlier demos (a heap, arrays) but are always reached through pointers. This is the first to pass and return a struct by value, on a hot path, so the register-pair-vs-sret return convention is genuinely exercised.
| Item | What it exercises |
|---|---|
| Struct passed/returned by value | The whole kernel is a vec2 {int16_t x,y} VALUE type. v2_add/v2_sub/v2_scale and the three Reynolds rules (separation/alignment/cohesion) each TAKE and RETURN a vec2 by value, and the composition chains those returns: v2_add(v2_add(sep, ali), coh). No other demo on the site passes an aggregate by value |
| The aggregate-return ABI | A 4-byte struct return forces the compiler to pick its aggregate-return convention — a small-struct register pair vs. an sret hidden pointer — and the steering composition exercises it O(N²) times per frame. That ABI path is otherwise untested by the battery |
| noinline so the ABI survives -Os | The vec functions are __attribute__((noinline)) on purpose: at -Os the optimiser would inline the whole kernel and the ABI would never run. The disasm gate asserts the by-value calls are still there (497 call sites in the slice) |
| Bit-exact differential | All math is integer fixed-point (Q12.4; int16_t components, products widened to int32_t → __mulsi3, neighbour-count divides → __divsi3). Exact integer ops ⇒ host x86 == console bit-for-bit. Verified host == default-8bit == +mos-a16 == +mos-xy16 == gate CRC 0xA8AB — any returned-struct miscompile diverges it |
| The picture IS the proof | A correct ABI yields a coherent flock — dots clump, align into streams (each boid coloured by its heading octant, so aligned birds share a hue) and avoid collisions. A returned-struct miscompile would scatter the flock into noise or freeze the gate CRC |
The flocking kernel is a portable C header (examples/65816/boids.h) linked into the
SNES ROM, a host oracle, and the corpus differential slice alike. The gate runs a small seeded
flock a fixed number of steps through the by-value kernel and folds every boid's position and
velocity into corpus_result. Because the math is exact integer fixed-point, that
16-bit hash must be identical on host x86 and on the console — and it is, across every
target codegen mode (default 8-bit, +mos-a16, +mos-xy16).
Written in C with the llvm-mos 65816 toolchain. Hit Verify fidelity to
reproduce the build gate's WRAM assert (gate CRC 0xA8AB) live in this tab — the same
hash the host oracle and the cycle-accurate bsnes-jg core agree on, bit for bit. The disasm gate
proves the shape: by-value calls = 497, __mulsi3 = 6,
__divsi3 = 4, rep/sep = 103 — the struct-return ABI in native-16 mode,
the aggregate-return path surviving -Os exactly as intended.