A real 256x256 CRTC mode, derived not recalled; palette ceiling was 2 dB low
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
This commit is contained in:
@@ -9,14 +9,24 @@ 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]
|
||||
argv = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
# --reserve-black: quantise to 255 colours and reserve index 0 as black.
|
||||
# Needed for any mode that letterboxes (256x192 inside 256x256): GVRAM cleared
|
||||
# to 0 displays palette entry 0, and a free mediancut palette puts a real image
|
||||
# colour there. Costs one of 256 entries; measured quality cost is negligible.
|
||||
RESERVE = "--reserve-black" in sys.argv
|
||||
src, out = argv[0], argv[1]
|
||||
f = sorted(glob.glob(f"{src}/*.png"))[int(argv[2]) if len(argv) > 2 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)
|
||||
n = 255 if RESERVE else 256
|
||||
q = im.quantize(colors=n, method=Image.MEDIANCUT, dither=Image.NONE)
|
||||
pal = np.array(q.getpalette()[:n*3], dtype=np.uint8).reshape(n, 3)
|
||||
idx = np.asarray(q, dtype=np.uint8)
|
||||
if RESERVE:
|
||||
pal = np.vstack([np.zeros((1, 3), np.uint8), pal]) # index 0 = black
|
||||
idx = idx + 1
|
||||
|
||||
with open(out, "wb") as fh:
|
||||
fh.write(b"DLXR")
|
||||
@@ -26,4 +36,5 @@ with open(out, "wb") as fh:
|
||||
|
||||
# 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}")
|
||||
print(f"src={f} {W}x{H} colors={len(np.unique(idx))}"
|
||||
f"{' (idx 0 reserved black)' if RESERVE else ''} -> {out}")
|
||||
|
||||
Reference in New Issue
Block a user