Double-Precision Mandelbrot
Fractals
The Mandelbrot set computed live on a
Super Nintendo in 64-bit IEEE-754 double.
The 65816 has no FPU, so every z² + c multiply and add is a
soft-float libcall — __muldf3, __adddf3, __gtdf2 —
over a register pair-of-pairs. The top half is rendered in double; the
bottom half in the 32-bit float of demo #21.
At whole-set scale the two precisions agree pixel-for-pixel — the set is symmetric about
the real axis, so the seam is invisible, and that is exactly the point: the escape buffer
is bit-for-bit identical to host x86. Runs automatically; no joypad needed. Written in C
and compiled with the llvm-mos-based 65816
toolchain (+mos-a16).
Self-running demo — the set grinds in top-to-bottom (the double top half is visibly slower than the float bottom), then spins while it re-computes. (Boot takes a moment to grind the first 64-bit floats.)
Click the screen to focus, then watch. Tab away and it pauses.
What it is
Escape-time Mandelbrot: for each point
c, iterate z ← z² + c from z = 0 and colour by how fast
|z| escapes. Demo #21 does it in 32-bit
float; this one does the top half in 64-bit double, which on a CPU with
no floating-point hardware means software routines over four 16-bit words:
mandel_cell(cr, ci): // one op per statement — no FMA to fuse
zr = zi = 0
repeat:
zr2 = zr*zr // __muldf3
zi2 = zi*zi // __muldf3
if zr2 + zi2 > 4.0: break // __adddf3, __gtdf2
zr,zi = (zr2 - zi2) + cr, // __subdf3, __adddf3
(zr*zi + zr*zi) + ci // __muldf3, __adddf3
The 64×56 escape buffer lives in high
WRAM (the 24-bit far store/load path, which is why the ROM is +mos-a16-only) and is
blitted into Mode 7 character VRAM, framed 4× to fill the screen; the hardware affine matrix spins
it while the palette cycles. Each pass re-grinds the set, so every frame is a fresh batch of 64-bit
multiplies and adds.
Compiler stress-test #33 — 64-bit double soft-float
A Round 3 demo — each Round-3 entry targets a codegen corner none of the first 32 execute.
Here it is the double-precision soft-float library: __muldf3,
__adddf3/__subdf3, __gtdf2, __floatsidf — plus
__truncdfsf2/__extendsfdf2 for the float twin. That 64-bit-float
family is disjoint from #22's 64-bit-integer family
(__muldi3/__udivdi3) and is otherwise untested by the battery.
| Item | What it exercises |
|---|---|
| No FPU — every op a 64-bit libcall | The 65816 has no floating-point unit, so each IEEE-754 double operation is a soft-float library call over a register pair-of-pairs: __muldf3 (z·z, zr·zi), __adddf3/__subdf3 (the z²+c recurrence), __gtdf2 (the |z|²>4 escape test), __floatsidf (pixel index → double). The 64-bit-float family no other demo touches |
| Bit-exact differential | IEEE-754 double +,−,× and the comparisons are correctly rounded (round-to-nearest-even), so a conforming soft-float on the 65816 must equal host x86 double bit-for-bit. FMA contraction is forbidden by construction (one op per statement, no a·b±c), the oracle built -ffp-contract=off. Verified host == default == +mos-a16 == +mos-xy16 == gate CRC 0x0EDF |
| Double beside a float twin | The top half is rendered in 64-bit double, the bottom half in the 32-bit float of demo #21 — the set is symmetric about the real axis, so the two halves join seamlessly. At whole-set scale the precisions agree pixel-for-pixel (the seam is invisible — that IS the result); casting the shared double window to float coords also exercises __truncdfsf2 |
| The precision cliff | Float and double only diverge at EXTREME zoom (re_span ~1e-6), where the per-pixel step drops below float’s ~6e-8 epsilon and the float side collapses into chunky blocks while double stays crisp. Resolving that depth needs hundreds of iterations per pixel — tens of thousands of 64-bit libcalls each — so it is far too slow to render live; the gate folds both a double and a float buffer to catch any divergence numerically |
| A bit-exact orbit witness | Beyond the escape buffers, the gate iterates the recurrence at an interior point and folds the RAW 64-bit bits of |z|² each step (a union type-pun) into an FNV accumulator — the sharp “double is fully specified” claim: a single-ULP slip in any __muldf3/__adddf3 would change the hash |
The escape kernel is a portable C header (examples/65816/mandel-double.h) linked into
the SNES ROM, a host oracle, and the corpus differential slice alike. The gate folds a small
double escape buffer, a float twin buffer, a bit-exact 64-bit orbit
witness and a double↔float conversion witness into corpus_result. Because double
arithmetic is correctly rounded, that 16-bit hash must be identical on host x86 (native
double) and on the console — and it is, across every target codegen mode
(default 8-bit, +mos-a16, +mos-xy16), bit for bit.
Written in C with the llvm-mos 65816 toolchain. Hit Verify fidelity to
reproduce the build gate's WRAM assert (gate CRC 0x0EDF) live in this tab — the same
hash the host oracle and the cycle-accurate bsnes-jg core agree on, bit for bit. The disasm gate
proves the shape: __muldf3 = 8, __adddf3/__subdf3 = 12,
rep/sep = 31 — 64-bit doubles in native-16 mode.