diff --git a/docs/STATUS.md b/docs/STATUS.md index 0f5181f..8e35703 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -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`. diff --git a/tools/bench/verify_frame.py b/tools/bench/verify_frame.py new file mode 100644 index 0000000..95a2801 --- /dev/null +++ b/tools/bench/verify_frame.py @@ -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)