Pressure-Cooker Fixed-Point Evaluator
Algorithms & DataA 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.
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
| Item | What it exercises |
|---|---|
| a flag held across the storm | On 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 values | The 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 answer | Everything 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 green | host == 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 visual | The 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.