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
126 lines
6.2 KiB
Python
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.")
|