Funnel-Shift Kaleidoscope
Rendering & Graphics
An 8-fold-symmetric kaleidoscope on a Super Nintendo driven by two-source funnel shifts:
__builtin_elementwise_fshl(A,B,k) and fshr(B,A,k) with A ≠ B.
Because the two sources differ, the
llvm-mos backend cannot fold them into a simple
rotate — it must expand each call into a full double-source shift+OR sequence, the "terrible" default
lowering the backend source itself warns about. Compiler stress-test #73 of the battery,
exercising a code path none of the first 72 demos ever triggered.
Self-running demo — the mandala animates as the funnel-shift count sweeps.
Click the screen to focus, then watch. Tab away and it pauses.
What it is
A funnel shift takes two operands and a count: place hi in the top half and lo in the bottom half of a double-width integer, shift the whole thing, and take the relevant half. When hi and lo are the same value the result is a plain rotation; with different values it's a genuine two-source mix. This demo forces the latter by building A and B from independent linear combinations, then XOR-ing the left and right funnel results to colour each cell of a 16×16 tile mandala with 8-fold symmetry.
// Two-source funnel shift: A != B, so the compiler // cannot simplify to a rotate. A = ox*17 + oy*7 + t*5; // source 1 B = ox*11 + oy*13 + t*3 + 0xA5A5; // source 2 k = (ox + oy*3 + t) & 15; // shift count [0..15] L = fshl(A, B, k); // (A<<k) | (B>>(16-k)) R = fshr(B, A, k); // (B<<(16-k)) | (A>>k) colour = (L ^ R) >> 12; // top nibble → palette [0..3]
ox/oy are the octant-folded tile coordinates (absolute value then sorted so ox ≤ oy), giving the 8-fold symmetry. k is a runtime variable — the compiler can't constant-fold the shift count, so the full funnel expansion must be emitted. The animation tick t changes each frame, morphing the pattern.
Compiler stress-test
| Item | What it exercises |
|---|---|
| G_FSHL / G_FSHR | Two-source funnel shift: concatenate hi:lo into 32 bits, shift, take the relevant 16-bit slice. When hi==lo the backend can combine it into a single G_ROTL/ROTR. With hi≠lo the combiner refuses and the full double-source shift+OR expansion fires. |
| the "terrible" lowering | LLVM's LegalizerHelper at line 317 contains a comment saying the default funnel lowering is "terrible" because it expands to three operations (shift A left, shift B right, OR) each of which may itself expand. On the 65816, variable shifts already invoke a shift loop or libcall — so the funnel produces a sequence the backend has never had to emit before. |
| why A ≠ B matters | matchFunnelShiftToRotate scans for the pattern (x << k) | (x >> (w−k)) with the same operand x on both sides. With independent A and B the scan fails, the rescue path is skipped, and the raw G_FSHL/G_FSHR nodes reach the target legalizer unchanged. No prior demo among the 72 ever triggered this path. |
| 8-fold symmetry | Each tile's (x,y) coordinates are folded into the first octant (abs then sort) before computing A and B, so the mandala is symmetric under reflection and 90° rotation. The same computation runs for all eight octant reflections — the mandala is a genuine 8-fold kaleidoscope. |
| integer-exact gate | Funnel shift on a fixed-width uint16 is a pure bitwise operation: no rounding, no UB, bit-identical host vs target. The differential gate folds all 256 tiles over one pass; a wrong lowering (e.g. treating A=B when they're not) diverges the CRC immediately. |
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 (gate CRC 0xEED4 —
a rotating-XOR fold of fk_cell() over all 256 tiles) live in this tab. No far
pointers — host == default == +mos-a16 == +mos-xy16, -verify clean.