Times-table Cardioid
Motion & Curves
A times-table trick
on the Super Nintendo: 200 equally-spaced points circle a canvas; a chord runs from each
point i to point (k·i) mod N. At k=2 the envelope of
all 200 chords traces a cardioid; k=3 gives a nephroid; higher k gives higher epicycloid
envelopes. The multiplier k animates continuously from 2 to 30. Written in C and compiled
with the llvm-mos-based 65816 toolchain
(+mos-a16).
Self-running demo — chords bloom automatically, k advances every ~0.7 s. (Boot holds a moment while the gate CRC computes.)
Click the screen to focus, then watch. Tab away and it pauses.
What it is
Each figure is drawn by connecting every pair of points (i, j) where j = (k·i) mod 200. The modular arithmetic is the only computation; no trigonometry runs during animation (circle points are precomputed once at startup). Ten chords bloom per frame, so each figure takes about 20 frames to complete; then it holds briefly before k advances and the canvas clears for the next figure.
// hot inner loop — forces __mulsi3 + __umodsi3:
for (uint16_t i = 0; i < CARD_N; i++) {
uint32_t j = (uint32_t)k * ((uint32_t)i + 65536u) % (uint32_t)CARD_N;
canvas_line(&canvas, pts_x[i], pts_y[i], pts_x[j], pts_y[j], 2);
} Compiler stress-test #27 — modulo-heavy inner loop
A Round 2 demo targeting the codegen corner that earlier demos skipped: the
% operator as the dominant hot operation. Prior demos that used division also
consumed the quotient, so the compiler emitted __udivmodsi4 (combined
divide+remainder). Here only the remainder matters, so the compiler takes the
__umodsi3 path (32-bit unsigned remainder only).
| Corner | What it exercises |
|---|---|
| Times-table trick | Place N=200 equally-spaced points on a circle. For multiplier k, draw a chord from point i to point (k·i) mod N. At k=2 the envelope of all 200 chords traces a cardioid; at k=3 a nephroid; higher k produces higher-order epicycloid envelopes. The demo animates k from 2 to 30, clearing and redrawing the full chord set for each value. |
| __mulsi3 + __umodsi3 | The inner loop computes j = k × (i + 65536) % N. The +65536 offset makes the second factor exceed UINT16_MAX so LLVM cannot narrow the multiply to 16-bit — the product is genuinely 32-bit, forcing __mulsi3. The 32-bit remainder then goes through __umodsi3 (not the 16-bit __umodhi3 shortcut). Confirmed by the disasm gate: __mulsi3=1, __umodsi3=1, rep/sep=6. |
| Bit-exact across all modes | Integer-only arithmetic means host C (32-bit int) and the 65816 target (16-bit int) agree identically — the explicit uint32_t casts in the hot loop prevent any width-dependent divergence. Gate CRC 0x523B confirmed host == default == +mos-a16 == +mos-xy16 on bsnes-jg. |
| Circle via SPIRO_SIN_LUT | The 200 circle points are precomputed at startup using a 256-entry Q8.8 sine LUT (the same table the spirograph demo uses). The trig is NOT in the gate — only the modular arithmetic is folded into the CRC. Each chord is drawn by Bresenham line into a 128×128 BitmapCanvas; ten chords render per frame during the bloom phase. |
Written in C with the llvm-mos 65816 toolchain. Hit Verify fidelity to
reproduce the build gate's WRAM assert (gate CRC 0x523B) live in this tab. The
disasm gate shape: __mulsi3=1, __umodsi3=1,
rep/sep=6 — 32-bit modulo-heavy loop in native-16 mode.
No compiler bug found — modulo codegen correct across all modes.