Truncation Staircase
Algorithms & Numeric
Three horizontal bands compare truncate, floor, and
round applied to a scrolling linear ramp. All three are built
from (int16_t)x / (float)q — the backend's
G_FPTOSI/G_SITOFP (__fixsfsi/
__floatsisf) — because floorf/ceilf/
truncf are .unsupported() in the SNES SDK. Watch
the three staircases diverge at the negative zero-crossing. Compiler
stress-test #83.
Self-running — top=trunc · middle=floor · bottom=round, scrolling through zero.
Click the screen to focus, then watch. Tab away and it pauses.
What it is
A linear ramp scrolls horizontally; each of the three bands quantizes it with a different rounding rule and plots the result as a staircase. On positive inputs the three agree; as the ramp crosses zero they split — truncation rounds toward zero, floor rounds down, and round-half goes to the nearest — so the negative half shows three distinct step patterns.
// "truncf(x)" without a libcall — floorf/ceilf/truncf are .unsupported() in the SDK. int16_t q = (int16_t)x_f; // G_FPTOSI → __fixsfsi (truncate toward zero) float qf = (float)q; // G_SITOFP → __floatsisf (back to float) float diff = x_f - qf; // G_FSUB → __subsf3 (fractional remainder) // floor = trunc, then correct downward for negative non-integers: int16_t f = (x_f - (float)(int16_t)x_f < 0.0f) ? (int16_t)x_f - 1 : (int16_t)x_f; // round = trunc(x ± 0.5): int16_t r = (x_f >= 0.0f) ? (int16_t)(x_f + 0.5f) : (int16_t)(x_f - 0.5f);
None of this uses a math-library rounding function: the SNES C toolchain has no
truncf/floorf/ceilf, so the demo synthesizes them
from integer casts (the soft-float __fixsfsi/__floatsisf calls)
plus a sign correction — the same idiom real 65816 code must use.
Compiler stress-test
| Item | What it exercises |
|---|---|
| G_FPTOSI / G_SITOFP | The fundamental float↔integer conversion nodes. On the 65816 they lower to __fixsfsi (float→int, truncation toward zero) and __floatsisf (int→float) — soft-float library calls, since the CPU has no floating-point unit. |
| the floorf/ceilf/truncf gap | The MOS SDK marks truncf, floorf, and ceilf as .unsupported() in the legalizer — calling them directly fails to link (no symbol exists). This demo shows the idiomatic workaround: (float)((int16_t)x) performs truncf inline via G_FPTOSI + G_SITOFP with no libcall, and floor/round are built from trunc plus a sign correction. |
| trunc vs floor vs round | The three rounding modes agree on positive values but diverge on negatives: trunc(−0.75)=0, floor(−0.75)=−1, round(−0.75)=−1; trunc(−1.5)=−1, floor(−1.5)=−2, round(−1.5)=−2. The three bands make the divergence visible as the ramp scrolls through zero. |
| what the differential caught | This demo's cross-emulator differential gate caught a real bug — a demo-side out-of-bounds write in the renderer (the round band drew one tile row past the 16-row canvas, overflowing the tile buffer and corrupting the result register). It surfaced only on bsnes-jg (whose power-on RAM is non-zero) and was masked on the zero-init emulator — the classic "reads uninitialized memory" signature. The compiler codegen was correct throughout; the fix was a bounds guard in the demo. |
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
(rounding CRC 0x02CA — a fold of 48 trunc/sitofp cycles) live in this tab.
No far pointers — host == default == +mos-a16 == +mos-xy16,
-verify clean.