"""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). The encoder does not yet reserve a black entry (docs/STATUS.md, encoder gaps), so the letterbox gets the closest thing to black the palette has. """ pal = 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)