Session 3 left the harness on the IPL's 768x512 text timing because no CRTC
values had been derived and guessing them was the failure mode to avoid. This
derives them from MAME 0.277's divisor ladder instead, and the derivation is
self-checking: the 256-wide mode runs at div 6 against the 768 mode's div 2, so
htotal is exactly 1104/3 = 368 dots and every horizontal register divides by
three with no remainder. Only the blanking split rounds. Verified by snapshot:
native 256x512, active area pixel-exact, x=512 wrap gone.
Two things fell out that change numbers elsewhere:
- The palette's shared LSB I must be chosen per entry, not hardcoded to 1.
Doing so lifts the display ceiling from 38.85 to 40.81 dB and is the only way
to reach true black at all, since pal6bit(1) = 4. 102 of 256 entries want
I = 0, so this is not a corner case. Supersedes FINDINGS 22.4; scsi has ~2 dB
more headroom than that section claimed. The encoder does not do this yet.
- Letterboxing costs a palette entry: GVRAM cleared to zero shows entry 0, and
a free mediancut palette puts a real image colour there. 255 colours plus a
reserved black, via prep_frame.py --reserve-black.
MAME's graphics double-scan is phase-shifted one raster line (it halves the
absolute scanline and vbegin is odd), which produced a false failure before it
was understood; the regression test now asserts the shifted pairing explicitly.
Still Lua-side. No 68000 instruction has drawn a pixel; the 38% blit estimate
remains unvalidated. What this buys is a defined geometry for the decoder to
write into: 256 words per row, 1024-byte stride, rows 32..223.
Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
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
First pixels on an actual X68000 screen. Everything up to now was Python-side
or a headless -video none run, which cannot snapshot at all.
The blocker was not the video controller. The IPL leaves CRTC R20 = 0x0B16,
and bit 11 is "G-VRAM set to buffer", which makes MAME's draw_gfx() return
early. GVRAM writes still land and read back correctly while the layer is
invisible, so six attempts at $E82400/$E82500/$E82600 all rendered black with
every register holding the value I intended.
Two more facts, both confirmed against MAME 0.277 source rather than assumed:
- $E8E001 monitor contrast is left at 14 by the IPL, scaling all output to
93.3%. The player must set it to 15. Contrast 0 blanks the screen, which is
a free fade-to-black for scene transitions.
- The palette word is GGGGGRRRRRBBBBBI with a shared LSB, expanded as
pal6bit((field<<1)|I). With contrast at 15 the render is pixel-exact, not
merely close, which also confirms the 1024-byte GVRAM line stride.
That exactness gives a new quality ceiling: the 15-bit+I palette alone costs
38.88 dB against the 24-bit palettised source, the same order as the scsi
profile's own codec error. scsi is close to display-transparent on hardware,
which bounds how much further it is worth raising.
Unblocks next step 2, the 68000 decoder skeleton, which now has a known-good
reference image to diff against.
Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
Verified GVRAM is one word-access per pixel in ALL color modes; chose 256-color
256x192 with movem.l bursts (page 1 sacrificed as double-buffer).
Measured 8 scenes from the Blu-ray source: blit costs under 8% of the 12fps
cycle budget, so I/O is the bottleneck, not CPU. Naive delta+RLE reaches only
3.2:1 (365 KB/s, 470MB) -> decision to use 4x4 vector quantization (~30 KB/s).
"Shot on twos" assumption failed: the transfer has zero duplicate frames, so
12fps requires explicit decimation.
Documents three false measurement results and their root causes (per-frame
Floyd-Steinberg dithering, temporal denoise, exact-match dedupe on noisy source).
MAME Lua injection harness works and is reusable for cycle-cost measurement;
the IOCS _B_READ disk benchmark is blocked returning -1.
Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6