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
This commit is contained in:
prosolis
2026-08-23 13:12:27 -07:00
parent 22c67f1cb8
commit b322e84cd4
7 changed files with 198 additions and 3 deletions
+80
View File
@@ -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 <framedir> 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.