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

107 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""What a scene change costs, now that the loader runs on the 68000 (FINDINGS 53).
python3 tools/analysis/22_scene_load.py [container ...] --kbps R [R ...]
ROADMAP P1 asked for the codebook expansion to be priced "against the refill
climb, not treated as free setup", and that is the whole job of this file. A
scene change is the one moment where every cost in this project lands at once:
the ring is empty because of the seek, the header has to arrive before a single
frame can be drawn, and the 68000 cannot decode anything until it has expanded
the codebooks out of that header.
THREE COSTS, IN THREE DIFFERENT UNITS, and they are not interchangeable:
* BYTES. The container's header region -- palette, CB1, CB4 -- must be
delivered before frame 0 can be decoded. It is not part of any frame
record, so no rate table in this tree has ever counted it.
* CLOCKS. What src/player/load.i costs to turn that header into what the
block loop reads, MEASURED on the emulated 68000 by tools/bench/load.lua
and parsed out of its log rather than copied in here as a constant.
* ACCUMULATED SLACK. The bytes above are bytes the pipe did not spend
filling the ring, so they cost play-time at the surplus rate (pipe - wire),
which is the currency FINDINGS 51.3 established a branch point spends.
This is the one that compounds: it is charged on top of the seek itself.
`--kbps` is REQUIRED and takes no default, for the reason FINDINGS 50 gives.
Rates are decimal-KB per the rest of the tree's tooling; sizes are KiB.
"""
import sys, os, re, argparse
sys.path.insert(0, "tools/encoder")
import numpy as np
from dlx import DLX
import ratectl as RC
FPS = 12
CPUHZ = 10_000_000
def rig_cycles(path):
"""The measured per-stage cost, out of tools/bench/load.lua's own log.
Parsed rather than pasted: a constant copied in here would go stale the
first time load.i changed, and it would go stale SILENTLY -- the arithmetic
below would keep working and keep being wrong.
"""
if not os.path.exists(path):
sys.exit(f"no rig log at {path} -- run tools/bench/load_run.sh first")
out = {}
for line in open(path, "rb").read().decode("utf-8", "replace").splitlines():
m = re.search(r"^\[LOD\]\s+(\S.*?)\s{2,}(\d+) cyc", line)
if m:
out[m.group(1).strip()] = int(m.group(2))
need = ("SCENE CHANGE: codebooks + palette", "scratch tables only (boot, once)")
for k in need:
if k not in out:
sys.exit(f"{path} has no '{k}' line -- is it a load.lua summary?")
return out
ap = argparse.ArgumentParser()
ap.add_argument("containers", nargs="*", default=["tmp/rc_fr_singe_scsi_span.dlx"])
ap.add_argument("--kbps", type=float, nargs="+", required=True,
help="delivered pipe rates, KB/s. REQUIRED, no default (FINDINGS 50)")
ap.add_argument("--log", default="tmp/load_check.log",
help="tools/bench/load.lua's log, for the measured cycle counts")
a = ap.parse_args()
cyc = rig_cycles(a.log)
scene_cyc = cyc["SCENE CHANGE: codebooks + palette"]
boot_cyc = cyc["scratch tables only (boot, once)"]
frame_cyc = CPUHZ / FPS
print(f"measured on the emulated 68000 ({a.log}):")
print(f" per scene change {scene_cyc:>8,} clocks = {1000*scene_cyc/CPUHZ:6.2f} ms "
f"= {100*scene_cyc/frame_cyc:.1f}% of one {FPS}fps frame")
print(f" once at boot {boot_cyc:>8,} clocks = {1000*boot_cyc/CPUHZ:6.2f} ms "
f" (the three scratch tables: scene-independent)")
for path in a.containers:
d = DLX(path)
hdr = int.from_bytes(d.raw[28:32], "big")
rec = np.array(d.record_lengths(), np.int64)
wire = rec.mean() * FPS / 1024 + RC.AUDIO_KBPS
print(f"\n=== {path}: header region {hdr:,} B "
f"(pal 768 + cb1 {d.k1*16:,} + cb4 {d.k4*4:,} + 32), wire {wire:.1f} KB/s")
print(f"{'pipe':>6} {'header ms':>10} {'+load ms':>9} {'total':>7} "
f"{'frames':>7} {'surplus':>9} {'slack s':>9}")
for kbps in a.kbps:
hdr_ms = 1000 * hdr / (kbps * 1024)
load_ms = 1000 * scene_cyc / CPUHZ
total = hdr_ms + load_ms
surplus = kbps - wire
# What the header costs in the currency of 51.3: play-time at the
# surplus rate. A negative surplus means the container does not fit the
# pipe at all and no amount of play buys the bytes back.
slack = f"{hdr/(surplus*1024):8.3f}" if surplus > 0 else " NEVER"
print(f"{kbps:>6.0f} {hdr_ms:>10.2f} {load_ms:>9.2f} {total:>7.2f} "
f"{total/(1000/FPS):>7.2f} {surplus:>9.1f} {slack:>9}")
print("""
Reading it. The 'frames' column is the scene change's FIXED cost in 12fps
frame slots, before the ring has been given a single frame of lookahead -- so it
is a floor under the black gap at a branch point, not the gap itself. The
'slack s' column is the one that compounds with FINDINGS 51.3: the header's
bytes are bytes that did not go into the ring, so they lengthen the climb back
to the seek-slack ceiling by that much play-time, every time.""")