Files
Dragon-s-Lair-X68k/tools/bench/dlxload.py
T
prosolis f1007a0dbc Put the frame in a container with no decoder, and find the palette is not free
ROADMAP K2. DLXP1: a 49,664 B record that is 97 sectors exactly, no index and
no length word, because a packed record's length is geometry rather than
content. 582.0 KB/s, which is what FINDINGS 61.9 predicted to the tenth, and it
encodes in 3.3 s because there is no k-means in it.

px68k's own x68k/gvram.c renders the container's bytes index-exact with the
harness computing no interleave -- the only test that can catch an encoder whose
byte order is wrong, since a container round-trips against its own inverse
either way. Both negative controls fail as they must.

The picture is re-derived against this project's builder rather than PIL's
(34.05 dB against 61.9's 34.08) and the GGGGGRRRRRBBBBBI word is charged for the
first time in this tree: 0.53 dB, on every row, so it moves no comparison.

What the control found is the finding. A packed container on a SCENE palette
lands exactly on the codec's ceiling, so the whole +2.31 dB is the per-frame
palette and nothing else -- and 231 of 256 entries change every frame, which
makes a mismatched paint 12.8 dB worse than the correct pairing, on screen for
roughly half of every frame slot if buffer mode does not blank. So B2 now
decides which packed CONTAINER ships, not only which player. The fallback is
already a flag: --scene-palette --no-palette is 30.79 dB, zero churn, 576.0 KB/s
and still +2.07 dB on the shipping codec.

62.5 is priced and is a wash: palette first 20.32 dB, palette last 20.33.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-25 07:31:05 -07:00

61 lines
2.9 KiB
Python

"""Load-time transforms every src/player/ front-end's loader has to do.
Split out of prep_dlx.py in session 18 so that prep_dlx.py (the preloaded-stream
rig) and prep_stream.py (the ring-buffer streaming rig, FINDINGS 49) share ONE
copy of them. Two copies would drift, and the drift would be silent: both rigs
would still decode, and only the colours or the codebook scaling would be
subtly wrong in one of them.
The split is a no-op by construction -- tools/bench/check.sh asserts prep_dlx.py
still emits a byte-identical blob for the gate container.
Neither transform is part of the per-frame cost being measured. The 68000 would
do both once at load time; charging them to the inner loop would flatter or damn
it for no reason.
"""
import numpy as np
def expand_codebooks(d):
"""CB1/CB4 to one WORD per pixel, so the inner loop movems them straight out.
The high byte of every GVRAM word write is discarded by the hardware, so it
is left zero and never has to be cleared. Word-per-pixel form is also what
makes index scaling a shift rather than a multiply: lsl.w #5 and lsl.w #3.
"""
cb1 = np.zeros((d.k1, 16, 2), np.uint8); cb1[:, :, 1] = d.cb1.reshape(d.k1, 16)
cb4 = np.zeros((d.k4, 4, 2), np.uint8); cb4[:, :, 1] = d.cb4.reshape(d.k4, 4)
return cb1, cb4
def pack_palette(d):
"""24-bit palette -> GGGGGRRRRRBBBBBI, shared LSB chosen PER ENTRY.
Choosing I per entry by minimum squared error rather than fixing it is worth
1.96 dB (FINDINGS 23.3). Identical maths to tools/bench/verify_frame256.py,
which is the point: the verifier and the loader must agree or a colour bug
reads as a decoder bug.
Returns (palette bytes 256x2 big-endian, index of the darkest entry, and the
RGB888 the hardware actually RENDERS from those words). The encoder does not
reserve a black entry in the CODEC container (docs/STATUS.md, encoder gaps),
so the letterbox gets the closest thing to black the palette has; the PACKED
container does reserve one (tools/encoder/dlxp.py, index 255).
`d` is a DLX container OR a bare (256,3) uint8 palette. The packed path has
no codebooks and so no DLX object to carry a palette on, and this had to stay
the ONE copy of the GRB555+I maths -- the verifier, the loader and now the
packed encoder all have to agree or a colour bug reads as a decoder bug.
"""
pal = (d if isinstance(d, np.ndarray) else d.pal).astype(int)
p6 = lambda v: ((v << 2) | (v >> 4)) & 0xFF
f = pal >> 3
render = lambda I: p6((f << 1) | I[:, None])
I = (((render(np.ones(256, int)) - pal) ** 2).sum(1)
< ((render(np.zeros(256, int)) - pal) ** 2).sum(1)).astype(int)
words = (f[:, 1] << 11) | (f[:, 0] << 6) | (f[:, 2] << 1) | I
palb = np.zeros((256, 2), np.uint8)
palb[:, 0], palb[:, 1] = words >> 8, words & 0xFF
dark = int(((render(I).astype(int)) ** 2).sum(1).argmin())
return palb, dark, render(I)