Harmonograph
Motion & Curves
A harmonograph — the Victorian
pendulum-driven drawing machine — running live on a Super Nintendo. Four damped pendulums (two
per axis, slightly detuned) each trace a decaying sinusoid; their sum is a
Lissajous figure that slowly precesses and spirals inward as the exponential
damping bleeds off energy. When a figure settles, the next of four presets blooms. Written in C
and compiled with the llvm-mos-based 65816
toolchain (+mos-a16, 16-bit accumulator). Runs automatically.
Self-running demo — the curve draws, decays, and the next figure blooms.
Click the screen to focus, then watch. Tab away and it pauses.
What it is
A real harmonograph hangs a pen and the paper from independent pendulums; as they swing and slowly lose energy to friction, the pen traces a Lissajous curve whose amplitude shrinks — a figure that spirals gracefully into its own centre. This is the digital version: four oscillators, two driving the x-coordinate and two the y, each a damped sine
x(t) = A0·sin(f0·t + p0)·e^(−d0·t) + A1·sin(f1·t + p1)·e^(−d1·t) y(t) = A2·sin(f2·t + p2)·e^(−d2·t) + A3·sin(f3·t + p3)·e^(−d3·t)
The two oscillators on each axis sit at a near-integer frequency ratio with a tiny detune
(f1 ≈ f0·(1+ε)), so the figure does not close — it precesses, drawing the
characteristic nested, slowly-rotating loops. The exponential envelope
e^(−d·t) is computed incrementally as a multiply by a constant just under 1 each
sample, so the whole curve decays into a point and the next preset takes over.
Why it stresses the compiler
This is the damped sibling of the spirograph. Where the
spirograph plots a constant-amplitude curve, every term here carries a running envelope
that is itself state, decayed each sample. The hot loop issues eight 16/32-bit
multiplies per point — four amplitude products sin·env and four
envelope-decay products env·decay — a sustained fixed-point __mulsi3 +
accumulation profile, under +mos-a16 rep/sep brackets,
and multiply-only (the damping needs no divide).
Compiler stress-test
| Item | What it exercises |
|---|---|
| Sine LUT | Four phase accumulators index a 256-entry signed Q8.8 sine table each sample — the same sin/cos-LUT inner loop the spirograph uses, here driving four independent oscillators |
| Amplitude product | Each oscillator’s sample is sin·env — a 16×16→32 __mulsi3 scaling the sine by the pendulum’s current amplitude; four per sample, then summed per axis |
| Exponential damping | The new stress vs the spirograph: each amplitude is a running envelope decayed every sample by env = (env·decay) >> 15 — a sustained fixed-point multiply + accumulation, four more __mulsi3 |
| Multiply-only | Eight __mulsi3 per sample and NO divide — the damping is a multiply by a Q15 constant < 1, so the disasm gate asserts zero __udivsi3/__udivmodsi4 |
The gate folds 256 samples of preset 0 into a rotating-XOR hash and asserts
corpus_result (0x0EBB) is identical on host, default 8-bit SNES,
+mos-a16, and +mos-xy16. The Verify fidelity button above
reproduces that gate live in this tab via the same cycle-accurate bsnes-jg core used in CI.
Written in C with the llvm-mos 65816 toolchain. Hit Verify fidelity
to reproduce the build gate's WRAM assert (corpus_result = 0x0EBB) live in
this tab. No far pointers — the same logic checked four ways: host == default ==
+mos-a16 == +mos-xy16, with bsnes-jg as the in-browser
confirmation.