The Default Flips

Rusty Nail ran a conventional Lua runtime - doubles for numbers, a preprocessor for PICO-8's dialect. Three days ago I started replacing it with z8lua, PICO-8's own fixed-point Lua, behind a feature flag, with a rule attached: the default flips only when every gate passes on evidence. Tonight the default flipped. This post is the three days of earning it, and the theme throughout is that the instruments kept correcting me.

What the player saw

One chart carries the whole arc. Dank Tomb - the heaviest cart I own, a torchlit dungeon that renders its lighting per-pixel in Lua - as presented on the actual HDMI screen, stage by stage:

Dank Tomb presented fps: 15.4 on the shared executor, 44 with its own executor, 46 with GC staging, 52 with the draw levers, 56 at 480MHz with ping-pong buffers, 59 at the 7.8MHz wire

The first bar is the discovery that mattered most - and the short version is that the renderer was never slow; the display pipeline was. The game was RENDERING 46 frames a second while the screen showed 15. The link task shared a cooperative executor with the cart, and under a saturating game its DMA-complete wakeup sat queued behind whole frames - 64 ms "transfer" windows of which 18 ms was actual wire time. The fix is the same one the audio path already used: the link got its own interrupt executor, so DMA completion is serviced at interrupt latency no matter what Lua is doing.

That split had a subtlety worth writing down. The old design was tear-free BY ACCIDENT.

The link only ever read the framebuffer while the cart happened to be parked - a property of cooperative scheduling, not of the design. Make the link preemptive and that assumption silently dies: it could now read a frame while the cart was halfway through drawing it.

So the conversion moved to the publisher's side. The frame converts into the DMA buffer at the moment it is published, while the cart is provably parked, and the link task never touches the framebuffer at all. The no-tear property went from an accident to a structural guarantee.

Then a field report improved it again: Solais presented 39fps while rendering 57. One buffer means a publish landing mid-transfer gets dropped, and the wire IDLES until the next publish - a beat frequency between the publish cadence and the transfer time. Two buffers, ping-pong: every publish converts into the slot the wire is not reading, and the wire starts the next frame the instant the last one ends.

Folklore, measured

With presentation fixed, the interpreter itself was the frontier. The optimisation catalogue had three candidates; I ran all three through the same A/B harness in one evening:

The interpreter lever ledger: ITCM hot text +0.9% (noise), lvm-only -Ofast -1.8%, the 480MHz clock +24%

Moving the VM's dispatch loop into zero-wait ITCM RAM - a classic embedded win, and the internet swears by it - measured at NOTHING. The M7's instruction cache was already covering the 10 KB hot loop; flash-resident text was never being fetched twice. Reverted, and the negative result recorded so it never gets re-tried on vibes. -Ofast on the interpreter measured WORSE than -O3. Reverted, recorded.

The one that paid was physics: this chip's silicon revision runs 480 MHz at its top voltage scale, a 20% clock bump - and game-shaped workloads gained MORE than 20%, because the memory busses rose with the core. Everything that must not speed up (the display link, the audio sample rate, USB) hangs off separate PLLs by design, so the audio pitch survived to the cycle. Two folklore levers at zero, one physics lever at +24%.

The one-byte regression

The compatibility side had its own lesson queued. A menu-text bug (glyph escape codes showing as literal \144 on screen) traced to the cart converter escaping PICO-8's glyph bytes - correct inside quoted strings, WRONG inside [[long strings]] where Lua never decodes escapes. The principled fix: the fixed-point core reads PICO-8's own dialect natively, so feed it the cart's bytes VERBATIM.

Then the 900-cart load sweep - the widest net I have, and the score both runtimes compete on - dropped from 872 to 853, and the failures said unexpected symbol at lines that looked innocent. The specimen that cracked it had a raw NUL byte embedded mid-line: decompressed cart code carries them as padding, the old sanitiser had been quietly converting them to spaces for months, and my "fully verbatim" purity deleted that along with the escaping. One byte type, nineteen carts:

The 900-cart sweep: doubles 867, fix32 at the swap 872, fully raw 853, with the NUL fix 875 - the record

NUL-to-space went back in (it is the ONE byte that cannot travel); the glyphs stay verbatim. The sweep landed at 875 of 900 - the best load rate either core has ever recorded, three carts ABOVE where the verbatim change started, because the glyph fix was right all along. It just had a single casualty that only a specimen, not an assumption, could identify.

The same discipline got teeth in the benchmark harness this week. An early benchmark row for Dank Tomb turned out to have measured a CRASHED cart - it was out of memory, error-spinning at 55 "frames" a second, and the numbers looked plausible enough to publish. The parser now refuses any measurement window containing cart errors:

LIVENESS: 7590 cart-error lines inside the measurement window -
this capture measures a CRASHED cart, refusing to parse

That is it pointed at the original bogus capture. The replacement row, certified through the gate: 57.2 frames a second against a declared 60, two runs agreeing to 0.13%.

The decision

Stage 9's gates, as finally passed: the on-device compatibility corpus at 27 of 30 categories zero-fail against the old core's 23; the sweep record above; every reference game equal or faster with the pathological frames collapsed (Celeste's worst frame fell from 308 ms to 48); heap parity under equal collector policy; and days of just playing the thing. The default flipped in one commit - the host tools now build the fixed-point core by default (and no longer need a system Lua at all; the vendored core carries itself), CI's production firmware build is the fixed-point one, and the old doubles core is demoted to a legacy flag awaiting deletion.

One frame short

The night should have ended there, but the screen was sitting at 56 and the wire clock ladder had an itch in it. The link's SPI clock came from a PLL shared with the audio subsystem, which pinned it to integer divisions: 7.38 MHz was clean, 8.0 MHz corrupted a line in EVERY frame - measured three separate times, the last one tonight, just to be sure. Nothing exists between those integer rungs... unless the audio moves to a different PLL computing the identical frequency, which frees the link's PLL to be tuned alone:

SAI audio:  HSI/4 x 12 / 17 = 11.294 MHz   (PLL3 now - same maths, same pitch)
DVI link:   HSI/4 x 39 / 40 / 2 = 7.8 MHz  (PLL2, its own PLL at last)

7.8 MHz: fifty-nine frames a second, zero shimmer, the receiver's per-line checksum indicator solid green. The corruption cliff lives somewhere in the final 0.2 MHz before 8 - and I am not going looking for it on jumper wires. One frame short of sixty, on a bird's nest of flying leads, from a runtime that three days ago presented fifteen.

The last frame belongs to the printed circuit board.