Saturating Comet Trails

Motion & Curves

Six comets streak across a Super Nintendo canvas, leaving glowing trails that additively saturate to white where they cross, then fade cleanly to black. The demo uses __builtin_elementwise_add_sat and sub_sat on both uint8_t (glow/decay) and int16_t (velocity), exercising all four saturating-arithmetic nodes. Compiler stress-test #75.

loading core…

Self-running — comets streak and cross, their trails saturating to white.

Click the screen to focus, then watch. Tab away and it pauses.

What it is

Saturating arithmetic clamps at the range boundary instead of wrapping: adding 200 + 100 in uint8_t with saturation gives 255, not 44. This makes it correct for things like colour accumulation (brightness never wraps back to 0) and velocity limiting (speed never jumps to the wrong sign). Each glow cell decays by 8 per frame (sub_sat, floors at 0), and each comet adds 100 brightness (add_sat, caps at 255). Where two comets meet, the brightness hits the ceiling and the tile turns white.

// Unsigned uint8 saturating ops (G_UADDSAT / G_USUBSAT)
glow[y][x] = __builtin_elementwise_sub_sat(glow[y][x], 8);  // decay: floors at 0
glow[cy][cx] = __builtin_elementwise_add_sat(glow[cy][cx], 100); // brighten: caps at 255

// Signed int16 saturating ops (G_SADDSAT / G_SSUBSAT)
vx = __builtin_elementwise_add_sat(vx, kick_x);  // velocity kick
vy = __builtin_elementwise_sub_sat(vy, kick_y);  // clamped at ±INT16_MAX

// All four → lowerAddSubSatToMinMax at MOSLegalizerInfo.cpp:246 (branchless min/max)

The velocity kick uses signed 16-bit saturating ops to show that the same lowering handles both signed and unsigned variants — all four nodes route through the same lowerAddSubSatToMinMax expansion in the LLVM legalizer.

Compiler stress-test

ItemWhat it exercises
G_UADDSAT / G_USUBSATUnsigned 8-bit saturating add and subtract: glow accumulates up to 255 (never wraps to 0) and decays down to 0 (never wraps to 255). Distinct from demo #44 hdr-bloom which uses __builtin_add_overflow → G_UADDO (flag-test+branch, not a saturating clamp) and demo #70 dither (hand-written ternary → G_ICMP+G_SELECT, never the *SAT intrinsic node).
G_SADDSAT / G_SSUBSATSigned 16-bit saturating add and subtract on the comet velocity, clamping at ±INT16_MAX. The velocity is also cast to int8_t for storage, so the signed sat limits are never hit in practice — but the node is still emitted and lowered, exercising the signed clamp path that unsigned operators never reach.
lowerAddSubSatToMinMaxAll four saturating nodes lower via the same function at MOSLegalizerInfo.cpp:246: compute the raw result, then select between the clamped bound and the result using a comparison. On the 65816 this becomes a compare + conditional branch, verified by the cmp/cpx/cpy count in the disasm gate.
white crossing = visible saturationWhere two comet trails overlap, the brightness from the second add_sat can reach 200 (100+100) or higher if more comets cross. The colour palette maps 201..255 to white — so crossing comets visually blow out to white, proving the unsigned add really saturates rather than wrapping. The demo would look different (incorrect) if add_sat were silently replaced by wrapping addition.
demo bug caught by differentialThe initial comet positions used py = i*3+1, giving py=16 for the 6th comet — out of bounds for the 16-row glow array. The differential found it: on the host the out-of-bounds write hit padding or stack (no net effect on the CRC); on the 65816 target it hit different WRAM and changed the trajectory. Fixed by masking with &15. The differential gate is correct — it catches demo code bugs too, not only compiler bugs.

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 0xC2AF — a fold of 256 glow values after 100 simulation steps) live in this tab. No far pointers — host == default == +mos-a16 == +mos-xy16, -verify clean.