Matrix Cascade

Algorithms & Numeric

A spinning wireframe lattice driven by chained 2×2 matrix multiplies. The stress is the sret (hidden-pointer) struct-return ABI: a mat2 is 8 bytes — too big to return in registers — so the compiler passes a hidden pointer to caller-allocated result storage. mat_mul takes and returns mat2 by value, and the cascade feeds each result into the next. Compiler stress-test #91.

loading core…

Self-running — a wireframe lattice tumbles under chained matrix transforms.

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

\1 A 5×5 grid of points is transformed each frame by a matrix built from a short cascade of rotation matrices, then drawn as a wireframe. As the phase advances the whole lattice rotates and shears. Every one of those matrix products returns an 8-byte struct by value. \2 class="rp-code">// mat2 is 8 bytes — over the register-return threshold — so it comes back via a HIDDEN pointer. typedef struct { int16_t a, b, c, d; } mat2; // 8 bytes -> sret mat2 mat_mul(mat2 x, mat2 y) { // caller passes &result (sret); we store through it mat2 r; r.a = (x.a*y.a + x.b*y.c) >> 8; // __mulsi3 16x16->32 multiply-accumulate, Q8 descale r.b = (x.a*y.b + x.b*y.d) >> 8; r.c = (x.c*y.a + x.d*y.c) >> 8; r.d = (x.c*y.b + x.d*y.d) >> 8; return r; // written through the hidden result pointer } // cascade: m = mat_mul(m, rot(k)); // chains sret returns

Cleanup handlers are how C code does RAII-style resource release (closing files, freeing locks) without a destructor. Correctly duplicating them onto every exit edge — and only those edges, in the right order — is a real obligation of the front-end + backend, and this demo checks it end to end on the 65816.

Compiler stress-test

ItemWhat it exercises
sret hidden pointerA struct return that is too big for the register-return convention (here 8 bytes, over the getNaturalAlignIndirect threshold at MOS.cpp:88) is passed back via a caller-allocated buffer whose address the caller hands in as an invisible extra argument. The callee writes the result through that pointer instead of returning it in registers.
chained by-value returnsThe cascade does m = mat_mul(m, rot(k)) repeatedly, so each call both takes structs by value and returns one by value — the sret buffer of one call feeds straight into the next. Getting the buffer lifetimes and stores right across the chain is the obligation being tested.
distinct from #26 boids#26 returns a vec2 (4 bytes, 32-bit) which fits the register-pair return convention — no hidden pointer. mat2 crosses the threshold, so this is specifically the sret path, which #26 never exercises.
32-bit MACsEach matrix element is (a*c + b*d) >> 8 — two 16x16->32 products (via the software __mulsi3) summed in a 32-bit accumulator, then descaled. The wireframe lattice is the transform applied to a grid of points.

Written in C with the llvm-mos 65816 toolchain and verified against bsnes-jg and MAME. Hit Verify fidelity to reproduce the build gate's WRAM assert (cascade CRC 0x8064 — a fold of every matrix element across the chain) live in this tab. No far pointers — host == default == +mos-a16 == +mos-xy16, -verify clean.