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.

loading core…

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

ItemWhat it exercises
G_FPTOSI / G_SITOFPThe 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 gapThe 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 roundThe 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 caughtThis 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.