Files
prosolis 1be428c270 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
2026-08-25 06:54:27 -07:00

126 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""Lay a DLX3 container out as a DISK for the ring-buffer rig (FINDINGS 49).
python3 tools/bench/prep_stream.py <in.dlx> [--out tmp/stream]
prep_dlx.py's output is one blob that tools/bench/decode.lua pushes into
emulated RAM in its entirety. That is what makes its rig RAM-bound -- a `scsi`
window is 5,261,814 B of stream and needs a 6 MB machine to hold it (FINDINGS
45) -- and, much more importantly, it is nothing like the shipping player, which
never holds a window at once.
This writes three files instead:
<out>_cb.bin codebooks + palette. ~10 KB, loaded into RAM once, exactly as
before: these are LOAD-TIME costs and not per-frame ones.
<out>_disk.bin the frame records, `[u32 len][body]` each padded up to 4, laid
end to end. tools/bench/stream.lua reads this from the HOST
filesystem and feeds it into a bounded ring, so the emulated
machine's RAM stops bounding how much of a window can be
tested. A stock 2 MB machine can now run all 120 frames.
<out>_idx.bin the DLX4 RECORD INDEX, nframes u16 big-endian, straight out
of the container's scene header. This is what the 68000
producer reads (src/player/ring.i); it is not derived here,
because deriving it is precisely what a player cannot do.
<out>_meta.lua geometry, and the record index.
THE RECORD INDEX IS NOT A CONVENIENCE. src/player/stream.s takes each frame's
base address from a descriptor the producer wrote, rather than deriving it from
where the last frame ended, because under the `aligned` wrap policy the next
record may be at the ring's base instead of just after its predecessor. The
producer therefore has to know record boundaries before it places them -- which
is what an index is. A branching laserdisc game needs one anyway to seek to a
branch point, so the policy that costs no clocks (tools/analysis/19_ring_stream.py)
reuses a structure the player cannot avoid.
The 4-byte record padding is the same one decode.s needs and DLX3 already
carries: `move.l (a0)+,d0` on an odd address is an ADDRESS ERROR on a 68000, not
a slow read. FINDINGS 28.3.
NO SYNTHETIC TIMING FRAMES. prep_dlx.py appends ten of them to price the block
modes separately; this rig measures delivery, not decode, and its per-frame cost
anchors are prep_dlx.py's job. Mixing them in would put frames on the wire that
no encoder emits and no rate controller sized.
"""
import sys, os, argparse
sys.path.insert(0, "tools/encoder")
sys.path.insert(0, "tools/bench")
import numpy as np
from dlx import DLX
import dlxload as DL
ap = argparse.ArgumentParser()
ap.add_argument("container")
ap.add_argument("--out", default="tmp/stream")
a = ap.parse_args()
d = DLX(a.container)
if d.idx_bytes != 1:
sys.exit("2-byte codebook indices: src/player/ assumes 1 (k<=256)")
if not d.has_spans:
sys.exit(f"{a.container} is DLX{d.version}: src/player/stream.s expects the "
f"DLX3 span section (see prep_dlx.py for why a DLX2 container "
f"decodes as garbage rather than merely losing its spans).")
cb1, cb4 = DL.expand_codebooks(d)
palb, dark, rendered = DL.pack_palette(d)
open(a.out + "_cb.bin", "wb").write(cb1.tobytes() + cb4.tobytes() + palb.tobytes())
disk, index = bytearray(), []
for (o, n) in d.frames:
start = len(disk)
disk += n.to_bytes(4, "big") + d.raw[o:o + n]
# The container's own alignment rule, not this script's copy of it: DLX5
# pads to 512 so a DMA channel can read whole sectors into the ring, DLX4
# to 4 so `move.l (a0)+` does not take an address error (28.3).
while len(disk) % d.rec_align:
disk += b"\0"
index.append((start, len(disk) - start))
open(a.out + "_disk.bin", "wb").write(bytes(disk))
# THE CONTAINER'S OWN INDEX, and it is checked against this layout rather than
# regenerated from it. src/player/ring.i walks the disk with a running sum of
# these lengths and never reads a record's length word before fetching it, so a
# container index that disagreed with the disk image by one byte would place
# every later record at the wrong address -- and the block loop reads without a
# bounds check (49.2), so the symptom would be wrong pixels, not a fault.
if d.has_index:
want = [ln // 4 for _, ln in index]
if d.index != want:
bad = next(i for i in range(len(want)) if d.index[i] != want[i])
sys.exit(f"{a.container}: the DLX4 index disagrees with this disk "
f"layout at record {bad}: {d.index[bad]} vs {want[bad]} "
f"longwords")
open(a.out + "_idx.bin", "wb").write(
b"".join(q.to_bytes(2, "big") for q in d.index))
else:
if os.path.exists(a.out + "_idx.bin"):
os.remove(a.out + "_idx.bin") # a stale index is worse than none
rec = np.array([n for _, n in index])
with open(a.out + "_meta.lua", "w") as fh:
fh.write("-- generated by tools/bench/prep_stream.py -- do not edit\nreturn {\n")
fh.write(f" W={d.W}, H={d.H}, fps={d.fps}, nframes={d.nframes}, dark={dark},\n")
fh.write(f" cb1_len={cb1.nbytes}, cb4_len={cb4.nbytes}, pal_len={palb.nbytes},\n")
fh.write(f" disk_len={len(disk)}, maxrec={int(rec.max())},\n")
fh.write(f" dlx_version={d.version}, "
f"has_index={'true' if d.has_index else 'false'},\n")
fh.write(f" padrec={{{','.join(str(ln) for _, ln in index)}}},\n")
fh.write(" index={\n")
for off, ln in index:
fh.write(f" {{off={off}, len={ln}}},\n")
fh.write(" },\n}\n")
print(f"{a.container}: {d.nframes} frames, {d.W}x{d.H}")
print(f" codebooks+palette {cb1.nbytes + cb4.nbytes + palb.nbytes:,} B -> "
f"{a.out}_cb.bin")
print(f" disk image {len(disk):,} B -> {a.out}_disk.bin "
f"(records: min {rec.min():,} median {int(np.median(rec)):,} "
f"max {rec.max():,})")
print(f" wire rate {rec.mean()*d.fps/1024:.1f} KB/s video at {d.fps} fps")
if d.has_index:
print(f" DLX4 record index: {2*d.nframes:,} B of scene header, and it "
f"agrees with the disk image on all {d.nframes} records")
print(f" A ring must hold one whole record contiguously: >= {rec.max():,} B "
f"({rec.max()/1024:.1f} KB) before any policy or prefill.")