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
61 lines
2.9 KiB
Python
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)
|