Pressure-Cooker Fixed-Point Evaluator

Algorithms & Data

A big straight-line fixed-point expression, evaluated per pixel, whose comparison flag is decided early but not used until after a storm of 32-bit multiplies and divides — each one a call that trashes the flags. This is compiler stress-test #109: it forces a compare result to live across the call-clobber under a jammed-full register file, the exact pressure a real llvm-mos register-scavenger fix handles.

loading core…

Self-running — an implicit surface with a level-set boundary; it computes for a beat first.

Click the screen, then play. (Tab away and it pauses.)

What it is

An implicit surface is a shape defined by an expression: colour each point by the value of a formula, and a boundary appears where the formula crosses a threshold. Here the formula is a deliberately gnarly fixed-point chain, and the threshold decision is made early but consumed late — on purpose.

int cond = (r2 - 4000 > 0);          // a comparison — decided HERE

int32_t d = (a*7)  / (b+13);          // ...then a divide
int32_t e = (c*5)  / (a+17);          // ...another divide
int32_t f = (r2*3) / (t*t+101);       // ...and another
int32_t g = (d+e) * (f+1);            // ...and multiplies
int32_t h = g / (c*c+29);             // ...and one more divide

return cond ? base+1000000 : base-1000000;   // the comparison, USED at last

The comparison sits at the top; the multiplies and divides come after; the comparison is used only at the bottom. That gap is the test. The gate evaluates the field and folds it into a CRC — a corrupted flag would flip the boundary and change the CRC.

Compiler stress-test #109 — Round 6: hardening the fixes

ItemWhat it exercises
a flag held across the stormOn this chip, comparisons leave their result in the processor’s flag bits — a single, precious register. This demo makes a comparison, then does a whole run of 32-bit multiplies and divides (each a subroutine call that trashes the flags and every scratch register), and only at the very end uses the comparison. The compiler has to stash that flag somewhere safe and bring it back — under a dozen live 32-bit values all competing for space.
the bug this guards (patch 0011)Under exactly this pressure — a live flag that must survive a spill — the register scavenger once had no safe place to route it. The fix threads it out through a spare index register into a reserved slot. This demo recreates the worst case: a comparison forced to live across six multiplies and four divides, with the register file jammed full, and confirms the flag comes back intact.
a dozen live valuesThe expression is deliberately one long straight line with no reuse — a, b, c, r², and half a dozen intermediates are all needed at the end, so nothing can be discarded early. That is what pins the register allocator against the wall and makes the flag-spill actually happen.
exact integers, exact answerEverything is signed 32-bit integer arithmetic, which is bit-for-bit identical on the host and on the SNES. If the held comparison flag were corrupted, the final choice would flip and the whole field would change. The gate folds the field into a CRC and checks it matches.
result: five ways greenhost == default == +mos-a16 == +mos-xy16 on both MAME and bsnes-jg, -verify-machineinstrs clean. The 16-bit-accumulator modes are the ones that matter here, and they stay green.
the visualThe expression is evaluated for every pixel, and its value chooses a colour band — so the screen shows an implicit surface: a smooth field with a level-set boundary (the circle) drawn wherever the held comparison flips. The palette drifts and the surface re-evaluates over time. It computes for a beat before appearing — that beat is thousands of multiplies and divides.

Compiler bug this demo guards against

Patch 0011 — a +mos-a16/+mos-xy16 backend crash: a 16-bit compare kept the N/Z flags live across a frame-carry spill, forcing the carry-flag pseudo-register $p to be preserved across an unbalanced range — but $p (register class Cc) has no general-purpose-register home, so the register scavenger emitted an illegal spill of $p / read an undefined $p. The scavenger logic predates the 16-bit-accumulator feature; this was the first code path to hit its $p-has-no-home gap.

Fix: Route $p hard-stack-neutrally through a dead index register into a spare register class, and drop a now-stale liveness assumption in the scavenger.

This demo's heavy fixed-point expression evaluator — a dozen live temporaries whose compare result is consumed after several multiply/divide calls — reproduces the exact register pressure that triggered the crash, as a standing regression guard.

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 0xEE6D — a fold of the evaluated field; give it a few seconds, the per-pixel evaluation is heavy) live in this tab. No far pointers — host == default == +mos-a16 == +mos-xy16, -verify clean.