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:
@@ -12,3 +12,4 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.dlx
|
*.dlx
|
||||||
a.out
|
a.out
|
||||||
|
tmp/
|
||||||
|
|||||||
@@ -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.
|
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`.
|
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.
|
||||||
|
|||||||
+41
-3
@@ -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
|
emulator's scheduler. `docs/BENCHMARK.md` covers the three-tier approach
|
||||||
(MAME validates the path, derivation bounds it, real hardware settles it).
|
(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 <script>.lua \
|
||||||
|
-snapshot_directory ./snap -snapview native -seconds_to_run 6
|
||||||
|
```
|
||||||
|
`-snapview native` drops MAME's LED artwork and gives a clean 768x512 screen.
|
||||||
|
|
||||||
## Next steps, in priority order
|
## Next steps, in priority order
|
||||||
|
|
||||||
1. **Full-disc survey.** Only 4 clips of 1.2-1.7 s out of 224 streams have been
|
1. **Full-disc survey.** Only 4 clips of 1.2-1.7 s out of 224 streams have been
|
||||||
@@ -164,9 +199,12 @@ emulator's scheduler. `docs/BENCHMARK.md` covers the three-tier approach
|
|||||||
vs content first (FINDINGS 13) or the averages are diluted by static menus.
|
vs content first (FINDINGS 13) or the averages are diluted by static menus.
|
||||||
**Vectorise `_paint` before this run** — it is a Python per-block loop.
|
**Vectorise `_paint` before this run** — it is a Python per-block loop.
|
||||||
2. **68000 decoder skeleton.** Parse `DLX1`, expand codebooks to word-per-pixel,
|
2. **68000 decoder skeleton.** Parse `DLX1`, expand codebooks to word-per-pixel,
|
||||||
blit SKIP/V1/V4/RAW. Measure real cycles with the existing MAME Lua harness —
|
blit SKIP/V1/V4/RAW. Measure real cycles with the existing MAME Lua harness.
|
||||||
the first time that harness gets used for its actual purpose. Validates the
|
**Now unblocked** — the display path is verified (FINDINGS 22) and
|
||||||
38% full-frame blit estimate that the whole CPU budget rests on.
|
`tools/bench/show_frame.lua` gives a known-good reference image to diff the
|
||||||
|
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.
|
||||||
3. **Wire rate control into `encode.py`.** No longer a blocker (FINDINGS 21), but
|
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
|
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).
|
was the original reason for choosing VQ. Insurance, not a fix. Pairs with (1).
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Frame -> flat (RGB888 palette + index plane) blob for the MAME Lua loader.
|
||||||
|
|
||||||
|
Packing into the X68000 palette word is done Lua-side on purpose: the exact
|
||||||
|
channel order is a hardware fact we intend to CONFIRM BY EYE, not assume, so it
|
||||||
|
has to be cheap to change without regenerating the blob.
|
||||||
|
"""
|
||||||
|
import sys, struct, glob
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
src, out = sys.argv[1], sys.argv[2]
|
||||||
|
f = sorted(glob.glob(f"{src}/*.png"))[int(sys.argv[3]) if len(sys.argv) > 3 else 0]
|
||||||
|
im = Image.open(f).convert("RGB")
|
||||||
|
W, H = im.size
|
||||||
|
|
||||||
|
q = im.quantize(colors=256, method=Image.MEDIANCUT, dither=Image.NONE)
|
||||||
|
pal = np.array(q.getpalette()[:256*3], dtype=np.uint8).reshape(256, 3)
|
||||||
|
idx = np.asarray(q, dtype=np.uint8)
|
||||||
|
|
||||||
|
with open(out, "wb") as fh:
|
||||||
|
fh.write(b"DLXR")
|
||||||
|
fh.write(struct.pack(">HH", W, H))
|
||||||
|
fh.write(pal.tobytes())
|
||||||
|
fh.write(idx.tobytes())
|
||||||
|
|
||||||
|
# reference PNG of exactly what the X68000 should display
|
||||||
|
Image.fromarray(pal[idx]).save(out.replace(".bin", "_ref.png"))
|
||||||
|
print(f"src={f} {W}x{H} colors={len(np.unique(idx))} -> {out}")
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
-- Real frame, 256-colour, with the ACTUAL display gate from MAME's source:
|
||||||
|
-- CRTC R20 bit 11 = "G-VRAM set to buffer" -> graphics layer invisible.
|
||||||
|
-- CRTC R20 bits 9-8 = colour setup: 0x0100 = 256-colour, low byte per word.
|
||||||
|
-- The IPL leaves R20 = 0x0B16 (buffered + 65536c), which is why every earlier
|
||||||
|
-- attempt rendered black no matter what the video controller said.
|
||||||
|
M=manager.machine; SP=M.devices[":maincpu"].spaces["program"]; SUB=nil
|
||||||
|
|
||||||
|
local CRTC20 = 0xE80000 + 20*2
|
||||||
|
local GVRAM, GPAL = 0xC00000, 0xE82000
|
||||||
|
|
||||||
|
local f=io.open("frame.bin","rb"); local d=f:read("a"); f:close()
|
||||||
|
local function B(i) return string.byte(d,i) end
|
||||||
|
local W,H = B(5)*256+B(6), B(7)*256+B(8)
|
||||||
|
local PAL0, PIX0 = 9, 9+256*3
|
||||||
|
|
||||||
|
-- GGGGGRRRRRBBBBBI, confirmed from x68k_v.cpp
|
||||||
|
local function pack(r,g,b) return ((g>>3)<<11)|((r>>3)<<6)|((b>>3)<<1)|1 end
|
||||||
|
|
||||||
|
local function T() local t=M.time; return t.seconds+t.attoseconds/1e18 end
|
||||||
|
local st,tp="wait",nil
|
||||||
|
|
||||||
|
SUB = emu.add_machine_frame_notifier(function()
|
||||||
|
local t=T()
|
||||||
|
if st=="wait" then
|
||||||
|
if t<3.0 then return end
|
||||||
|
local old = SP:read_u16(CRTC20)
|
||||||
|
local new = (old & ~0x0B00) | 0x0100 -- clear buffer bit, select 256c
|
||||||
|
SP:write_u16(CRTC20, new)
|
||||||
|
SP:write_u16(0xE82400, 0x0001) -- video ctrl: 256 colours
|
||||||
|
SP:write_u16(0xE82600, 0x001F) -- graphics + pages on, text off
|
||||||
|
SP:write_u8(0xE8E001, 15) -- monitor contrast: IPL leaves it at 14
|
||||||
|
for c=0,255 do
|
||||||
|
local o=PAL0+c*3
|
||||||
|
SP:write_u16(GPAL+c*2, pack(B(o),B(o+1),B(o+2)))
|
||||||
|
end
|
||||||
|
for y=0,H-1 do
|
||||||
|
local row,base = PIX0+y*W, GVRAM+y*1024
|
||||||
|
for x=0,W-1 do SP:write_u16(base+x*2, B(row+x)) end
|
||||||
|
end
|
||||||
|
print(string.format("[SHOW3] R20 %04X->%04X E82400=%04X E82600=%04X gv=%04X t=%.3f",
|
||||||
|
old, SP:read_u16(CRTC20), SP:read_u16(0xE82400), SP:read_u16(0xE82600),
|
||||||
|
SP:read_u16(GVRAM), t))
|
||||||
|
st,tp="painted",t
|
||||||
|
elseif st=="painted" and t>tp+0.30 then
|
||||||
|
M.video:snapshot(); print("[SHOW3] snapshot"); st="done"; M:exit()
|
||||||
|
end
|
||||||
|
end)
|
||||||
Reference in New Issue
Block a user