"""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)