From b322e84cd4791582f4ad06d60496513de6adb901 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:12:27 -0700 Subject: [PATCH] Get a real Dragon's Lair frame onto the emulated X68000 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 --- .gitignore | 1 + docs/FINDINGS.md | 80 +++++++++++++++++++++++ docs/STATUS.md | 44 ++++++++++++- docs/images/x68k_first_frame.png | Bin 0 -> 58952 bytes docs/images/x68k_first_frame_compare.png | Bin 0 -> 187412 bytes tools/bench/prep_frame.py | 29 ++++++++ tools/bench/show_frame.lua | 47 +++++++++++++ 7 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 docs/images/x68k_first_frame.png create mode 100644 docs/images/x68k_first_frame_compare.png create mode 100644 tools/bench/prep_frame.py create mode 100644 tools/bench/show_frame.lua diff --git a/.gitignore b/.gitignore index b50d4a3..ed83152 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ __pycache__/ *.pyc *.dlx a.out +tmp/ diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md index a5091d5..b7d4bd2 100644 --- a/docs/FINDINGS.md +++ b/docs/FINDINGS.md @@ -590,3 +590,83 @@ plausibly exceed the pipe where a 1.7s clip does not. Rate control gives a original reason for choosing VQ over a lossless delta in the first place. Still worth wiring in. No longer a blocker for shipping `scsi` at `lam=10`. + +## 22. The display path, measured — first real frame on the X68000 + +Everything before this section was Python-side or a headless `-video none` run. +This is the first time pixels reached an emulated X68000 screen, and it produced +four hardware facts and one blocker that no amount of reasoning would have found. + +Reproduce: +``` +python3 tools/bench/prep_frame.py tmp/frame.bin 0 +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 -snapview native -seconds_to_run 6 +``` + +### 22.1 The blocker: CRTC R20 bit 11 hides the graphics layer +The IPL leaves **CRTC R20 (`$E80028`) = `0x0B16`**. Bit 11 is *"G-VRAM set to +buffer"*, and MAME's `x68k_v.cpp` bails out of `draw_gfx()` on it outright: + +```c +if (m_crtc->gfx_layer_buffer()) // if graphic layers are set to buffer, they aren't visible + return false; +// x68k_crtc.h: bool gfx_layer_buffer() const { return BIT(m_reg[20], 11); } +``` + +While that bit is set, GVRAM writes still land and read back correctly — which +is exactly what makes it so misleading. Six separate attempts at the video +controller (`$E82400/$E82500/$E82600`) rendered black with every register +reading back the intended value. **The video controller was never the problem.** + +`R20` bits 9-8 select the colour setup, and this determines how `$C00000` is +decoded: `0x0300` = 65536c (16 bits/word), `0x0100` = 256c (low byte), +`0x0000` = 16c (4 bits). Set `R20 = 0x0116` for our mode. + +### 22.2 Monitor contrast: the IPL leaves it at 14, not 15 +`$E8E001` bits 3-0 are monitor contrast; MAME does +`m_screen->set_brightness(contrast * 0x11)`. The IPL leaves it at **14**, which +scales all output to 14/15 = 93.3%. Every rendered colour came out ~7% dark +until this was set to 15. **The player must write `$E8E001 = 15` at startup.** + +Contrast `0` blanks the screen entirely (`x68k_v.cpp:661`) — that is the cheap +fade-to-black for scene transitions, no palette animation required. + +### 22.3 Palette format CONFIRMED (was previously an assumption) +`PALETTE(config, m_gfxpalette).set_format(2, &x68k_state::GGGGGRRRRRBBBBBI, 256)` + +``` +bit 15..11 10..6 5..1 0 + GGGGG RRRRR BBBBB I <- I is a shared LSB for all three channels +``` +Expansion is `pal6bit((field << 1) | I)`, i.e. `(v << 2) | (v >> 4)`. +With contrast at 15, **all 256 entries render exactly as this predicts** — the +frame is pixel-identical, not merely close. GVRAM line stride is confirmed as +512 words = 1024 bytes, matching `HARDWARE.md`. + +### 22.4 A new quality ceiling: the 15-bit palette costs 38.88 dB +Section 3 called the 256-colour palettised frame "the real quality ceiling". +That was measured in 24-bit RGB. The hardware palette only stores 5 bits per +channel plus a shared LSB, so there is a **second** quantisation below it: + +| stage | PSNR | +|---|---| +| 24-bit palettised source -> X68000 15-bit+I display | **38.88 dB** | +| `scsi` profile codec error (00020, FINDINGS 15) | 39.4 dB | + +The codec's error at `scsi` is **the same order as the display's own error**. +On real hardware `scsi` is therefore close to display-transparent, and pushing +`lam` below 10 buys quality the monitor cannot show. This bounds how much the +`scsi` profile is worth raising — it does not change the profiles themselves. + +Caveat: measured on one frame (00020 f0001). It is a property of the palette, +not the content, so it should generalise, but it has not been checked across +scenes. + +### 22.5 Why the first frame appears twice +GVRAM is a 512-pixel-wide page while the IPL's CRTC is still in its 768-wide +text timing, so the layer repeats at exactly x=512. This is correct hardware +behaviour, not a bug. The player sets its own CRTC mode and the wrap disappears. +No CRTC timing table has been written yet — the harness deliberately keeps the +IPL's timing so that no invented CRTC values are in play. diff --git a/docs/STATUS.md b/docs/STATUS.md index a52eefa..0f5181f 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -156,6 +156,41 @@ functional models, not timing-accurate; a KB/s figure from MAME measures the emulator's scheduler. `docs/BENCHMARK.md` covers the three-tier approach (MAME validates the path, derivation bounds it, real hardware settles it). +## Display path — WORKING, verified end to end (session 3) + +The first real frame is on screen: `docs/images/x68k_first_frame_compare.png`. +Full write-up in **FINDINGS 22**. Harness: `tools/bench/show_frame.lua` + +`tools/bench/prep_frame.py`. + +Three facts the player MUST honour, none of which were guessable: + +| what | where | value | +|---|---|---| +| **Un-hide the graphics layer** | CRTC R20 `$E80028` | clear bit 11 ("G-VRAM set to buffer"); IPL leaves `0x0B16` | +| Colour setup (256c) | CRTC R20 bits 9-8 | `0x0100` -> `R20 = 0x0116` | +| **Monitor contrast** | `$E8E001` bits 3-0 | IPL leaves **14**; write **15** or everything renders 7% dark | + +Bit 11 is the one that cost the most time: GVRAM writes land and read back +correctly while the layer is invisible, so the video controller looks guilty and +is not. Contrast `0` blanks the screen — free fade-to-black for transitions. + +Palette format is now **confirmed from MAME source**, not assumed: +`GGGGGRRRRRBBBBBI` (G 15:11, R 10:6, B 5:1, shared LSB I), expanded as +`pal6bit((field<<1)|I)`. With contrast at 15 the render is **pixel-exact**. + +New 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 +(39.4 dB). `scsi` is close to display-transparent on real hardware. See +FINDINGS 22.4 before considering raising quality further. + +Snapshot recipe that works (`-video none` CANNOT snapshot): +``` +SDL_VIDEODRIVER=dummy mame x68000 -bios ipl10 -video soft -window \ + -sound none -nothrottle -plugins -autoboot_script