Branch: feat/crashbash-fw-fixes — commit c4474b9
Fixes: Crash Bash (SCUS-94570) menu-character 30 Hz strobe + in-game
character flattening. Root-caused and verified 2026-07-09.
The runtime GTE hardcoded lm = 1 (clamp IR results to [0, 0x7FFF]) on
every IR write-back in the shared lighting/depth-cue helpers
(light_transform, light_color, color_output, depth_cue_from_ir).
Real hardware takes lm from bit 10 of the instruction word — lm = 0
clamps to [-0x8000, 0x7FFF] and preserves negative results.
depth_cue_from_ir also ignored sf (bit 19) and skipped the hardware's
intermediate step clamp.
Affected ops: INTPL, DPCS, DPCT, DPCL, NCDS, NCDT, NCS, NCT,
NCCS, NCCT, CDP, CC. (RTPS/RTPT/MVMVA/OP/SQR/GPF/GPL
already honored the instruction word and are untouched.)
Crash Bash tweens its 10 fps keyframe vertex animation to the 30 fps render
rate through INTPL with lm = 0 — an off-label, signed-vector use of the
color-interpolation op:
- pose-A vertex → IR1–3
- pose-B vertex → far-color regs (FC) via
ctc2 - blend factor → IR0
INTPL(0x0980011) → lerped vertex read back from IR1–3 (mfc2)
With lm forced on, every negative component of a tweened vertex clamped
to 0, collapsing the character mesh onto its anchor point on tween frames.
Keyframe frames (no INTPL) stayed healthy, so with alternating draw buffers
exactly one display buffer ever held a healthy pose → the character strobed
at 30 Hz on menus and rendered flattened/half-visible in-game. Beetle and
real hardware honor per-instruction lm, so they never showed it.
light_transform/light_color/color_output:lmread from the instruction word and passed to everysaturate_irwrite-back.depth_cue_from_ir: rewritten to the faithful hardware formula —base = IR << 12;step = lim±0x8000(((FC << 12) − base) >> sf·12)(intermediate clamp withlmforced OFF, per hardware);MAC = (base + IR0·step) >> sf·12; finalIR = lim(MAC, lm)withlmfrom the instruction. MAC overflow flags checked.- Always-on GTE observability rings (build-dbg debug-server commands):
gte_ring_dump(per-RTPS/RTPT inputs+outputs),gte_intpl_dump(per-INTPL inputs+outputs, frame filter + offset paging),gte_frame_stats(per-framenproj/nsat/nflat/nintpl/nintpl_tiny),gte_latch_dump(retained saturated projections).
Low, and skewed toward correctness:
- Standard libgte lighting calls encode
lm = 1, so games using the ops as designed see identical clamping to before. - The two real behavior changes are (a)
lm = 0callers now keep negative results — the bug being fixed — and (b) the depth-cue step is now clamped to ±0x8000 before the IR0 multiply, which only matters when|FC − IR| > 0x7FFF(far color very distant from the lit color). Both changes move us toward hardware/Beetle behavior, so the only regression class is a game that accidentally depended on our old inaccuracy. sfhandling: all common libgte encodings of these ops usesf = 1, which reproduces the old shift exactly.
This branch does not merge to master until every supported title has been validated against this change. No subset, no sampling.
Validated:
Crash Bash (SCUS-94570) — the fix target. Menu strobe gone;
nintpl_tiny 335–352/473 → 0–3; character present in both VRAM
double-buffers and in all frames of a jittered screenshot burst;
menus/animation healthy.
Ape Escape (SCUS-94423) — 2026-07-10. Full branch-flavor rebuild
(BIOS + game regen, build-crashbash-fw/). Booted to title; lit title
models healthy (Ape was the highest-risk title — heavy GTE user, most
likely to exercise the depth-cue step clamp). User-validated in play.
Tomba 2 (SCUS-94454) — 2026-07-10. Full branch-flavor rebuild. Attract
gameplay healthy; gte_frame_stats over the attract scene: nintpl = 0
(no INTPL use), steady nsat/nflat, nintpl_tiny = 0. User-validated.
Mega Man X6 (SLUS-01395) — 2026-07-10. Full branch-flavor rebuild.
Attract gameplay demo healthy (standard lm = 1 lighting path).
All four titles validated → merged to master e4145d7 on 2026-07-10.
Post-merge gate: Tomba rebuilt clean against merged master; Crash Bash
junction re-pointed to master, regenerated, rebuilt — SELECT GAME TYPE
character present, nintpl_tiny = 0 across the sampled frames.
Per-title procedure (same for all):
- Boot to gameplay through the title's existing verify flow on this branch.
- Capture
gte_frame_statsover a representative scene and diffnsat/nflatagainst a pre-fix baseline built from master + this branch's parent commit — any shift is a stop-and-investigate, not a note. - Eyeball lit and fogged/depth-cued scenes side-by-side with the Beetle oracle; the oracle is ground truth, not the pre-fix recomp.