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.

loading core…

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

ItemWhat it exercises
G_FSHL / G_FSHRTwo-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" loweringLLVM'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 mattersmatchFunnelShiftToRotate 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 symmetryEach 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 gateFunnel 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.