Align the container to the disc, and find the decoder-free packed player fits

Two sessions, unrecorded until now, committed together because their edits
share files and cannot be split cleanly after the fact.

Session 28 (FINDINGS 60): the container is DLX5 -- every record sector-aligned,
120/120 starting on a boundary where 3/120 did, +0.48% on the wire and zero
clocks -- and the ring's release rounds to RECALN so no pad is stranded.  Two
encoder levers measured and refused: `--spans all` buys +0.19 dB for +67% of
the wire, and joint span/lam selection emits byte-identical containers because
`lam` never leaves its floor on any of 120 frames.

Session 29 (FINDINGS 61): the packed full-frame blit is 27.3% of a 12 fps
frame, a channel fills GVRAM in buffer mode off the disc with the CPU halted,
and it walks the 1,024 B line stride itself through array chaining.  At the
9 clk/B dual-address floor the codec is 110.4% of a frame and a decoder-free
packed literal player is 55.2%, at +4.89 dB -- 2.75 dB past a ceiling the
codec's scene-wide palette cannot cross.  Encoder work is parked; the codec is
kept and not built on.

check.sh is ALL GREEN before and after, plus one new stage that gates the ORDER
of the measured paint costs rather than their values.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 06:54:27 -07:00
parent 8800d8f8c0
commit 1be428c270
28 changed files with 2203 additions and 144 deletions
+44 -10
View File
@@ -29,6 +29,13 @@ import spans as SP
MODE_SKIP, MODE_V1, MODE_V4, MODE_RAW = 0, 1, 2, 3
# A SCSI target answers in 512-byte blocks and a record is not a sector: on the
# DLX4 gate container 117 of 120 records start part way into one. DLX5 makes
# the container agree with the medium instead of making the transport reconcile
# them (tools/analysis/26_sector_align.py prices all three ways).
SECTOR = 512
class DLX:
def __init__(self, path):
self.raw = open(path, "rb").read()
@@ -38,12 +45,21 @@ class DLX:
# (FINDINGS 28.3), so the padding is part of the format, not a loader
# convenience -- but DLX1 containers stay readable, because every
# measurement in FINDINGS 28-31 was taken on one.
if b[:4] not in (b"DLX1", b"DLX2", b"DLX3", b"DLX4"):
if b[:4] not in (b"DLX1", b"DLX2", b"DLX3", b"DLX4", b"DLX5"):
raise ValueError(f"{path}: not a DLX container")
self.version = int(b[3:4])
self.aligned = self.version >= 2
self.has_spans = self.version >= 3
self.has_index = self.version >= 4
# DLX5: every record starts on a 512-BYTE SECTOR boundary, and so does
# the frame stream itself. That is not a tidier version of DLX4's
# 4-byte rule -- it is what lets a DMA channel read a record as whole
# sectors straight into the ring, with no window and no bounce copy
# (FINDINGS 58.3 option C, and 59.4 made it a precondition: sc_in_data
# REFUSES a windowed read when the data phase is the channel's).
self.sector_aligned = self.version >= 5
self.rec_align = SECTOR if self.sector_aligned else (4 if self.aligned
else 1)
(self.W, self.H, self.fps, self.nframes,
self.k1, self.k4) = struct.unpack(">HHHHHH", b[4:16])
off_pal, off_cb1, off_cb4, off_frm = struct.unpack(">IIII", b[16:32])
@@ -61,21 +77,24 @@ class DLX:
self.mode_bytes = (self.nb * 2 + 7) // 8
# frame directory: (offset of the mode header, payload length)
if self.aligned and off_frm % 4:
raise ValueError(f"{path}: DLX2 frame stream starts at {off_frm}, "
f"which is not 4-byte aligned")
if off_frm % self.rec_align:
raise ValueError(f"{path}: DLX{self.version} frame stream starts at "
f"{off_frm}, which is not {self.rec_align}-byte "
f"aligned")
self.frames = []
p = off_frm
for _ in range(self.nframes):
(n,) = struct.unpack(">I", b[p:p + 4])
self.frames.append((p + 4, n))
p += 4 + n
if self.aligned:
p += -p % 4 # skip the pad to the next record
# The writer does not pad after the LAST record -- nothing follows it --
# so `p` may have advanced past the end by up to 3 bytes there.
p += -p % self.rec_align # skip the pad to the next record
# DLX2/DLX3 do not pad after the LAST record -- nothing follows it --
# so `p` may have advanced past the end by up to 3 bytes there. DLX4
# and DLX5 DO pad it, because a producer that trusts the index fetches
# a whole padded record for the last frame like any other.
slack = len(b) - p
if not (slack == 0 or (self.aligned and -3 <= slack < 0)):
if not (slack == 0 or (self.aligned and not self.has_index
and -(self.rec_align - 1) <= slack < 0)):
raise ValueError(f"{path}: {slack} trailing bytes after "
f"{self.nframes} frames")
@@ -92,7 +111,7 @@ class DLX:
if self.has_index:
self.index = list(struct.unpack(
f">{self.nframes}H", b[off_idx:off_idx + 2 * self.nframes]))
walked = [(-(4 + n) % 4 + 4 + n) // 4 for _, n in self.frames]
walked = [self._padded(n) // 4 for _, n in self.frames]
if self.index != walked:
bad = next(i for i in range(self.nframes)
if self.index[i] != walked[i])
@@ -106,6 +125,21 @@ class DLX:
f"{off_frm + 4 * sum(self.index)} bytes and the file is "
f"{len(b)} -- a producer trusting it would run off the end")
def _padded(self, n):
"""Bytes one record of `n` payload bytes occupies, pad included."""
ln = 4 + n
return ln + (-ln % self.rec_align)
def record_lengths(self):
"""Padded record lengths in BYTES, in stream order.
The one place the container's alignment rule is applied. Every caller
that used to write `4 + n + (-(4+n) % 4)` was carrying its own copy of
that rule, which is exactly the kind of duplication that made DLX5 a
multi-file change instead of a one-line one.
"""
return [self._padded(n) for _, n in self.frames]
def modes(self, f):
o, _ = self.frames[f]
h = np.frombuffer(self.raw, np.uint8, self.mode_bytes, o)
+41 -7
View File
@@ -66,6 +66,7 @@ import argparse, struct, sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import numpy as np
import vq as VQ, vq_hybrid as H, ratectl as RC, spans as SP
from dlx import SECTOR
# Measured on the emulated 68000, FINDINGS 24. Instruction cycles against
# zero-wait-state memory, so these are floors, not hardware predictions.
@@ -134,8 +135,12 @@ def build_records(m, enc, span_mode):
return out
def write_container(path, m, frames, fps, k1, k4, span_mode):
"""Write the whole container. Returns (total bytes, video bytes, pad)."""
def write_container(path, m, frames, fps, k1, k4, span_mode, sector=True):
"""Write the whole container. Returns (total bytes, video bytes, pad).
`sector` selects DLX5's 512-byte record alignment over DLX4's 4-byte one.
"""
align = SECTOR if sector else 4
palette = m["pal"][:256]
if len(palette) < 256:
palette = np.vstack([palette, np.zeros((256 - len(palette), 3), np.uint8)])
@@ -144,6 +149,9 @@ def write_container(path, m, frames, fps, k1, k4, span_mode):
cb4_b = m["cb4"].astype(np.uint8).tobytes()
off_pal = 36 if span_mode else 32
if not span_mode:
sector = False # DLX2 has no index and no sector rule
align = 4
off_cb1 = off_pal + len(pal_b)
off_cb4 = off_cb1 + len(cb1_b)
# DLX4: the record index sits with the palette and the codebooks, ahead of
@@ -154,7 +162,7 @@ def write_container(path, m, frames, fps, k1, k4, span_mode):
qlens = []
for i, rec in enumerate(frames):
n = 4 + len(rec)
n += -n % 4
n += -n % align
q = n // 4
assert q <= 0xFFFF, (f"frame {i} is {n} B: a u16 longword count "
f"caps a record at 262,140 B")
@@ -169,9 +177,15 @@ def write_container(path, m, frames, fps, k1, k4, span_mode):
# a 68000, not a slow read. It vectors into the IPL and looks exactly like
# an infinite loop (FINDINGS 28.3). tools/bench/prep_dlx.py has been
# realigning at load time; the container now carries it.
tbl_pad = -off_frm % 4
# DLX5 aligns the frame stream itself as well as the records inside it, so
# the whole container can be laid on a volume at a sector boundary and every
# record lands on one. Aligning the records to each other and not the run
# they sit in would leave 117 of 120 of them off-sector again the moment the
# scene header changed length by a byte.
tbl_pad = -off_frm % align
off_frm += tbl_pad
hdr = ((b"DLX4" if span_mode else b"DLX2")
magic = (b"DLX5" if sector else b"DLX4") if span_mode else b"DLX2"
hdr = (magic
+ struct.pack(">HHHHHH", m["W"], m["H"], fps, len(frames), k1, k4)
+ struct.pack(">IIII", off_pal, off_cb1, off_cb4, off_frm))
if span_mode:
@@ -190,7 +204,7 @@ def write_container(path, m, frames, fps, k1, k4, span_mode):
# record is padded too, and the file ends where the index says it
# does rather than up to 3 bytes short of it.
if span_mode or i + 1 < len(frames):
n = -(4 + len(rec)) % 4
n = -(4 + len(rec)) % align
fh.write(b"\0" * n); frm_pad += n
total = os.path.getsize(path)
return total, sum(len(r) + 4 for r in frames) + frm_pad, frm_pad
@@ -252,6 +266,13 @@ def main():
"Default OFF: measured, it is worth one frame of 120 "
"at --spans need and a 2%% regression at --spans all "
"(FINDINGS 44)")
ap.add_argument("--joint-spans", action="store_true",
help="re-run the lam search with the bytes the span pass "
"freed, then re-span (E3, FINDINGS 39.3 item 5). The "
"span pass removes the block payload of every block "
"it covers, so without this the frame lands under its "
"allowance and the blocks that were NOT spanned were "
"priced as if those bytes were still needed.")
ap.add_argument("--no-cpu-fit", action="store_true",
help="drop the per-frame 68000 decode ceiling (session 7 "
"behaviour: 31%% of frames on hard content do not fit)")
@@ -259,6 +280,13 @@ def main():
help="how full the player's buffer is assumed to be at "
"scene start, as a fraction of the bucket (0 = cold "
"buffer after a seek, the conservative assumption)")
ap.add_argument("--no-reserve-black", action="store_true",
help="let the scene palette spend all 256 entries on the "
"picture. The default RESERVES index 0 as true black "
"(FINDINGS 23.4), because GVRAM cleared to zero shows "
"entry 0 and the 256x192 picture sits in a 256x256 "
"mode -- so a free palette letterboxes the frame in "
"whatever colour mediancut happened to put first.")
ap.add_argument("--preview")
a = ap.parse_args()
@@ -279,6 +307,7 @@ def main():
if a.disk_clk_byte is not None:
RC.DISK_CLK_BYTE = a.disk_clk_byte
RC.JOINT_DECIDE = a.joint_decide
RC.JOINT_SPANS = a.joint_spans
RC.JOINT_BUCKET = a.joint_bucket
cyc_budget = None if a.no_cpu_fit else RC.FRAME_CYCLES
span_mode = None if (a.spans == "off" or not rc) else a.spans
@@ -301,8 +330,13 @@ def main():
else:
print(f" target {prof['kbps']} KB/s, FIXED lam={lam} (no rate control)")
print(f" k1={k1} k4={k4}, {_IDX_BYTES}-byte indices")
print(f" palette: " + ("255 picture colours, index 0 RESERVED as true "
"black for the letterbox (23.4)" if not a.no_reserve_black
else "all 256 entries to the picture (--no-reserve-black); index 0 "
"is whatever mediancut put there, and the letterbox with it"))
m = H.build(a.frames_dir, k1=k1, k4=k4, iters=a.iters)
m = H.build(a.frames_dir, k1=k1, k4=k4, iters=a.iters,
reserve_black=not a.no_reserve_black)
if rc:
enc = RC.encode_rate_controlled(m, prof["kbps"], fps=a.fps,
bucket_frames=a.bucket_frames,
+59
View File
@@ -157,6 +157,23 @@ JOINT_DECIDE = False
# `--joint-bucket` turns it on.
JOINT_BUCKET = False
# E3 / FINDINGS 39.3 item 5: SPAN SELECTION IS GREEDY AFTER `lam`, and this is
# the switch that makes the two joint. The lam bisection picks a mode map
# against a byte allowance, and the span pass then REMOVES the block payload of
# every block it covers -- so the frame lands under the allowance by exactly
# the bytes the spans freed, and the blocks that were NOT spanned were priced
# at a lam chosen as if those bytes were still needed. Joint mode hands the
# freed bytes back to the lam search and re-spans the result, to a fixed point
# or two rounds, whichever comes first.
#
# It is a REFINEMENT, not a different objective: lam can only fall (the
# allowance only grows), so the un-spanned blocks can only improve, and a round
# is kept only if the frame still fits both ceilings it was already fitting.
# Default OFF until measured, which is 44.3's lesson -- ask whether the lever
# is loaded before pulling it.
JOINT_SPANS = False
JOINT_SPAN_ROUNDS = 2
def _byte_clk():
"""The debit the mode decision is allowed to see (0 = the old decision)."""
@@ -287,6 +304,39 @@ def _fit_spans(m, ctx, mode, sz, room, cyc_budget, span_mode, ib):
return nmode, nsz, H.cycles(nmode) + sel["clocks"], sel
def _refit_joint(m, ctx, allow, span_allow, lam_lo, lam_hi, cyc_budget,
span_mode, ib, mode_pre, mode, sz, cyc, sel, mu=0.0):
"""Give the lam search back the bytes the span pass freed, then re-span.
`mode_pre` is the mode map BEFORE spanning and `mode` the one after, so the
difference in frame_bytes is exactly what the spans made unnecessary. The
ceiling the result is judged against is the one _fit_spans was already
working to, so a kept round is never a frame that grew past a budget it was
inside.
"""
ceiling = span_allow if span_allow is not None else allow
for _ in range(JOINT_SPAN_ROUNDS):
if sel is None:
break
freed = (H.frame_bytes(mode_pre, ctx["nb"], ib)
- H.frame_bytes(mode, ctx["nb"], ib))
if freed <= 0:
break
lam2, mode2, sz2, _ = _search_lam(ctx, allow + freed, lam_lo, lam_hi, mu=mu)
if sz2 <= H.frame_bytes(mode_pre, ctx["nb"], ib):
break # lam did not move: already at the floor
n_pre, n_mode, n_sz, n_cyc, n_sel = (
mode2, *_fit_spans(m, ctx, mode2, sz2, span_allow if span_allow
is not None else allow, cyc_budget, span_mode, ib))
if n_sel is None or n_sz > ceiling:
break
if cyc_budget is not None and n_cyc + DISK_CLK_BYTE * n_sz > cyc_budget \
and cyc + DISK_CLK_BYTE * sz <= cyc_budget:
break # round 1 made the deadline and this does not
mode_pre, mode, sz, cyc, sel = n_pre, n_mode, n_sz, n_cyc, n_sel
return mode_pre, mode, sz, cyc, sel
def encode_rate_controlled(m, target_kbps, fps=12, bucket_frames=8,
lam_lo=1.0, lam_hi=LAM_CLIFF, prefill=0.0,
steps=None, verbose=False, cycle_budget=None,
@@ -364,6 +414,10 @@ def encode_rate_controlled(m, target_kbps, fps=12, bucket_frames=8,
mode_pre = mode
mode, sz, cyc, sel = _fit_spans(m, ctx, mode, sz, span_allow,
cycle_budget, span_mode, ib)
if JOINT_SPANS:
mode_pre, mode, sz, cyc, sel = _refit_joint(
m, ctx, allow, span_budget and span_allow, lam_lo, lam_hi,
cycle_budget, span_mode, ib, mode_pre, mode, sz, cyc, sel)
if cycle_budget is not None and cyc + DISK_CLK_BYTE * sz > cycle_budget:
# The byte allowance could not buy the frame's deadline, so fall
# back to the controller that pays in picture -- and then offer
@@ -374,6 +428,11 @@ def encode_rate_controlled(m, target_kbps, fps=12, bucket_frames=8,
mode_pre = mode
mode, sz, cyc, sel = _fit_spans(m, ctx, mode, sz, span_allow,
cycle_budget, span_mode, ib)
if JOINT_SPANS:
mode_pre, mode, sz, cyc, sel = _refit_joint(
m, ctx, allow, span_budget and span_allow, lam_lo,
lam_hi, cycle_budget, span_mode, ib, mode_pre, mode,
sz, cyc, sel, mu=mu)
late = cyc + DISK_CLK_BYTE * sz > cycle_budget
# Paint from the mode map as it was BEFORE spanning. A spanned run's
# blocks read SKIP in the emitted header, but SKIP means "hold the
+33 -5
View File
@@ -28,12 +28,40 @@ def load_frames(d):
return [np.asarray(Image.open(f).convert("RGB")) for f in fs]
def scene_palette(rgb, colors=256, stride=3):
"""One shared palette for the whole scene, no dithering (cel art is flat)."""
def scene_palette(rgb, colors=256, stride=3, reserve_black=True):
"""One shared palette for the whole scene, no dithering (cel art is flat).
`reserve_black` puts TRUE BLACK at index 0 and quantises the picture into
the other 255 entries. It is not a cosmetic default (FINDINGS 23.4): the
picture is 256x192 inside a 256x256 mode, GVRAM cleared to zero displays
palette entry 0, and a free mediancut palette puts a real image colour
there -- on 00020 f0001 it was (206,192,176), used by 210 image pixels, so
the 64 blank rows of letterbox came out beige. Entry 0 also needs `I = 0`
in the X68000's GRB555 word or the bars sit at RGB (4,4,4) (23.3); that
half is `dlxload.pack_palette`'s and it needs no special case, because a
(0,0,0) entry picks I=0 by its own minimum-squared-error rule.
Black is RESERVED, not withheld: the mapper may still spend index 0 on
genuinely black pixels, which is the entry it would have wanted anyway.
What the reservation buys is that index 0 is black REGARDLESS of what the
scene contains, which is what the letterbox needs and what a free palette
cannot promise.
"""
samp = np.concatenate([r.reshape(-1, 3) for r in rgb[::stride]])
ref = Image.fromarray(samp.reshape(-1, 1, 3)).quantize(
colors=colors, method=Image.MEDIANCUT, dither=Image.NONE)
pal = np.array(ref.getpalette()[:colors * 3], dtype=np.uint8).reshape(-1, 3)
n = colors - 1 if reserve_black else colors
q = Image.fromarray(samp.reshape(-1, 1, 3)).quantize(
colors=n, method=Image.MEDIANCUT, dither=Image.NONE)
pal = np.array(q.getpalette()[:n * 3], dtype=np.uint8).reshape(-1, 3)
if not reserve_black:
return q, pal
pal = np.vstack([np.zeros((1, 3), np.uint8), pal])
# The quantiser above cannot be reused as the mapping reference: its
# palette is the 255 it chose, at the wrong indices. A P-mode image
# carrying the FINAL palette is what every frame is then mapped against,
# so the indices in the container and the entries in the container's
# palette section are the same table by construction.
ref = Image.new("P", (1, 1))
ref.putpalette(pal.tobytes().ljust(768, b"\0"))
return ref, pal
+2 -2
View File
@@ -113,10 +113,10 @@ def blocks_of(idx, pal, bw, bh):
return VQ.blockify(idx, pal, bw, bh)
def build(frames_dir, k1=256, k4=256, iters=16, lam=0.0):
def build(frames_dir, k1=256, k4=256, iters=16, lam=0.0, reserve_black=True):
rgb = VQ.load_frames(frames_dir)
H, W = rgb[0].shape[:2]
ref, pal = VQ.scene_palette(rgb)
ref, pal = VQ.scene_palette(rgb, reserve_black=reserve_black)
idx = VQ.palettise(rgb, ref)
# --- two codebooks, trained on the whole scene ---