Handoff: reconcile docs with the verified display path

Session 3 summary in STATUS.md, plus the things a cold start needs.

- Reproduce section for the display result, verified cold from the Blu-ray at
  end of session: extract -> prep -> MAME -> verify, exact match, 38.88 dB.
  The frames are not in the repo and the old ones lived in /tmp, so the chain
  starts from extract.py rather than assuming a scratch directory survives.
- tools/bench/verify_frame.py turns FINDINGS 22 into a regression check. It is
  deliberately an exact test rather than a PSNR threshold, since the whole
  point of that section is that the render is bit-for-bit predictable. It
  prints the three registers to check when it fails.
- Recorded where the MAME source now lives, and why to read it first: six
  register-poking attempts failed against a gate that one grep found.
- Split the CRTC mode table out as its own next step. It is the prerequisite
  for the decoder skeleton and the smallest well-defined task available, with
  an explicit warning not to write the timing values from memory.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-23 13:16:04 -07:00
parent b322e84cd4
commit 3265bf2740
2 changed files with 101 additions and 2 deletions
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Regression check for the display path (FINDINGS 22).
Compares a MAME snapshot of GVRAM against what the X68000's 15-bit+I palette
MUST produce for the same source frame. This is an exact test, not a threshold:
if the palette packing, the line stride, or the monitor-contrast setting drifts,
`exact` goes False. Do not soften it into a PSNR threshold -- the whole point of
FINDINGS 22 is that the render is bit-for-bit predictable.
python3 tools/bench/verify_frame.py tmp/frame_ref.png tmp/snap_verify/x68000/0000.png
"""
import sys, numpy as np
from PIL import Image
ref_path = sys.argv[1] if len(sys.argv) > 1 else "tmp/frame_ref.png"
snap_path = sys.argv[2] if len(sys.argv) > 2 else "tmp/snap_verify/x68000/0000.png"
ref = np.asarray(Image.open(ref_path).convert("RGB")).astype(int)
snap = np.asarray(Image.open(snap_path).convert("RGB")).astype(int)
H, W = ref.shape[:2]
got = snap[0:H, 0:W]
# MAME x68k_v.cpp: GGGGGRRRRRBBBBBI, expanded via pal6bit((field << 1) | I)
pal6bit = lambda v: (v << 2) | (v >> 4)
pred = pal6bit(((ref >> 3) << 1) | 1)
exact = bool((pred == got).all())
mse = ((ref - got) ** 2).mean()
psnr = 99.0 if mse == 0 else 10 * np.log10(255 * 255 / mse)
print(f"exact match vs 15-bit+I prediction : {exact}")
print(f"display quantisation cost : {psnr:.2f} dB (expected 38.88 on 00020 f0001)")
if not exact:
bad = np.argwhere((pred != got).any(axis=2))
print(f" {len(bad)} differing pixels; first at {tuple(bad[0])}"
f" pred={tuple(pred[tuple(bad[0])])} got={tuple(got[tuple(bad[0])])}")
print(" check: CRTC R20 bit 11 clear, R20 bits 9-8 = 0x0100, $E8E001 contrast = 15")
sys.exit(0 if exact else 1)