Files
psxrecomp/tools/mdec_pin/FINDING.md
T
Matthew Stanley 08bb5a2728 fix(mdec): truncate 15bpp channel to uint8 before RGB555 (the rainbow bug)
The faithful dequant/IDCT/YUV transcription was correct, but the 15bpp pack did
(c+4)>>3 on `r ^ 0x80` as a SIGNED int. For negative Mask9ClampS8 results that
yields a negative int (r=-1 → -129) → garbage 5-bit channel → B saturated, R/G
doubled = the rainbow fringing on real FMV. Beetle's RGB_to_RGB555 takes uint8
params (truncates to 0..255); the 24bpp path already cast to uint8_t. Fix:
rgb_to_555_chan(uint8_t) so the caller's int truncates on the call.

Caught only by comparing our VRAM to the REAL Beetle process (new vram_peek on
the Beetle side, held-FMV diff) — both standalone pins shared the bug in their
self-transcribed reference and falsely passed. After the fix the per-channel VRAM
delta vs Beetle collapsed from mean (12,16,27) to (0.7,0.9,1.3) and the FMV renders
natural colours. Full-path pin (mdec_e2e, corrected uint8 oracle) is byte-exact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 21:06:04 -07:00

1.8 KiB

MDEC pin — finding + RESOLUTION (2026-06-27)

The bug (FOUND + FIXED)

The faithful dequant/IDCT/YUV transcription was correct, but the 15bpp pack lost the uint8 truncation Beetle's RGB_to_RGB555(uint8 r,g,b) performs implicitly:

int ru = r ^ 0x80;            // r = Mask9ClampS8 ∈ [-128,127], a SIGNED int
... rgb_to_555_chan(ru) ...   // (c+4)>>3 on a possibly-NEGATIVE int → garbage

For negative r, r ^ 0x80 is a negative int (e.g. r=-1 → -129), and the old rgb_to_555_chan(int) did (c+4)>>3 on it without truncating to 0..255. Beetle passes the value through uint8 params, truncating first. Result on real FMV: B saturated to 31, R/G ~doubled → the rainbow fringing. The 24bpp path was unaffected (it already casts to uint8_t). FIX: rgb_to_555_chan(uint8_t c) (caller's int truncates on the call), matching Beetle. mdec.c:rgb_to_555_chan.

Why both pins initially MISSED it (important)

mdec_pin.c and mdec_e2e.c's reference both computed the 555 channel on the same un-truncated int — i.e. the reference REPLICATED mdec.c's bug, so they agreed (0 diffs) while BOTH diverged from real Beetle. A standalone pin whose oracle is your own transcription cannot catch an error shared by both. The truth came from the real Beetle process: a vram_peek added to beetle_debug_server.c, diffing our VRAM vs Beetle's VRAM at the held FMV (the gold-standard oracle). That collapsed the channel deltas from mean (dR 12, dG 16, dB 27) to (0.7, 0.9, 1.3) and removed the rainbow.

Validation now

  • mdec_e2e.c full-path pin (with the corrected uint8 oracle): byte-exact.
  • Live VRAM vs real Beetle: rainbow gone, FMV renders natural colours; residual <1.3 mean (sub-LSB / Beetle's upscaled-VRAM representation + free-running frame mismatch). LESSON: validate against the REAL oracle process, not a self-transcribed reference.