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
+63 -2
View File
@@ -1,4 +1,4 @@
# Status & next-session handoff — end of session 2 (2026-08-23)
# Status & next-session handoff — end of session 3 (2026-08-23)
## Decisions locked
@@ -51,6 +51,27 @@ rate-distortion curve, not two codecs.
---
## What session 3 settled
1. **The display path works and is verified end to end.** First real frame on an
emulated X68000 screen: `docs/images/x68k_first_frame_compare.png`. Full
write-up in **FINDINGS 22**. Everything before this session was Python-side
or a headless `-video none` run, which cannot snapshot at all.
2. **The render is pixel-exact, not merely close.** With monitor contrast at 15,
all 256 palette entries render exactly as `GGGGGRRRRRBBBBBI` + `pal6bit`
predicts. That exactness is the regression test — see
`tools/bench/verify_frame.py`, which exits non-zero if it ever drifts.
3. **Three hardware facts that were previously assumed are now confirmed from
MAME 0.277 source**, not folklore: the palette word format, the 1024-byte
GVRAM line stride, and the 256-colour page aliasing in `HARDWARE.md`. All
three were already written down correctly; they are now cited.
4. **A new quality ceiling was measured** — the 15-bit+I palette alone costs
38.88 dB, the same order as the `scsi` profile's own codec error. FINDINGS
22.4. This bounds how much further `scsi` is worth raising.
5. **Two shell traps that wedged session 2's background jobs** are documented in
the working-setup section below. They cost ~1.5 h of wall clock and a wedged
CPU core, and one of them was hit again this session.
## What session 2 settled
1. **The critical-path question is answered.** "Does VQ soften Bluth's linework
@@ -205,6 +226,15 @@ SDL_VIDEODRIVER=dummy mame x68000 -bios ipl10 -video soft -window \
68000's output against. Validates the 38% full-frame blit estimate that the
whole CPU budget rests on. Still needs a real CRTC mode table for 256x256;
the harness deliberately borrows the IPL's timing and invents nothing.
2a. **CRTC mode table for 256x192-in-256x256.** Prerequisite for (2) and the
smallest well-defined unit of work available right now. Needs real R00-R08
timing values. **Do not write these from memory** — session 3 lost time to
exactly that failure mode on the video registers. Derive them from the CRTC
dividers in `x68k_crtc.cpp` (`m_reg[20] & 0x1f` selects the dot-clock
divisor; the IPL's `0x16` gives /2 off the 69MHz clock), or lift a known-good
set from a real X68000 title and verify by snapshot. The harness makes this
cheap to iterate: change values, snapshot, look.
3. **Wire rate control into `encode.py`.** No longer a blocker (FINDINGS 21), but
it is what gives a deterministic ceiling over content not yet measured, which
was the original reason for choosing VQ. Insurance, not a fix. Pairs with (1).
@@ -229,7 +259,38 @@ SDL_VIDEODRIVER=dummy mame x68000 -bios ipl10 -video soft -window \
- ~~Flat 4x4 VQ.~~ Rejected by eye (FINDINGS 9).
## Not yet started
- Any 68000 player code
- **Any 68000 player code.** `src/player/` is still empty. The display path is
proven, but proven *from Lua* — no 68000 instruction has yet drawn a pixel.
- **A real CRTC mode table.** The harness deliberately borrows the IPL's 768x512
text timing and invents no CRTC values, which is why the frame repeats at
x=512 (FINDINGS 22.5). A 256x256 mode needs real R00-R08 values, and those
must be derived or measured, NOT recalled from memory — see the note below.
- ADPCM audio extraction/encoding
- Disk image packaging
- Game logic (scene branching, input windows, death clips)
## Reproducing the display result
```
python3 tools/encoder/extract.py 00020 tmp/fr_00020 12 crop
python3 tools/bench/prep_frame.py tmp/fr_00020 tmp/frame.bin 0
mkdir -p tmp/snap_verify && cd tmp && SDL_VIDEODRIVER=dummy mame x68000 -bios ipl10 \
-video soft -window -sound none -nothrottle -plugins \
-autoboot_script ../tools/bench/show_frame.lua \
-snapshot_directory ./snap_verify -snapview native -seconds_to_run 6
cd .. && python3 tools/bench/verify_frame.py
```
Verified cold from the Blu-ray at end of session 3: exact match, 38.88 dB.
`tmp/` is gitignored scratch. The frames are NOT in the repo — regenerate them
with `extract.py`; the earlier ones lived in `/tmp` and do not survive a reboot.
## Reference material on this box (not in the repo)
- **MAME 0.277 source: `~/src/mame-mame0277/`** (tarball `~/src/mame0277.tar.gz`).
Downloaded this session to settle the graphics-layer question. The files that
matter are `src/mame/sharp/x68k_v.cpp`, `x68k_crtc.cpp`, `x68k_crtc.h`,
`x68k.cpp`. **Read these before theorising about X68000 video behaviour**
six register-poking attempts failed against a gate that one grep found.
- Blu-ray mounted at `/media/reala-misaki/BDROM` via
`udisksctl loop-setup -r -f DRAGONS_LAIR.iso`.
+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)