src/player/load.i expands both codebooks to word-per-pixel form and packs the palette to GGGGGRRRRRBBBBBI out of the RAW container header, byte-exact against tools/bench/dlxload.py on both CPU cores. The palette half is gated on words read back out of the palette registers at $E82000, so "the words reached the hardware" is part of what passes. ROADMAP P1 is done; P2's encoder half (a reserved black entry, 23.4) is not, and is a re-encode rather than an edit. A scene change costs 18.96 ms of 68000 time, 22.8% of one 12 fps frame; boot costs 24.70 ms. The scratch tables describe the CRTC, not the scene, so pal_tables is a separate entry point built once at boot -- 5.29 ms off every scene change. The one that moves something: the scene header is 5,920 B that no rate table in this tree included, because it belongs to no frame record. In FINDINGS 51.3's currency it is divided by the surplus pipe - wire, so it is hypersensitive: 138 ms of extra refill climb at 488 KB/s and 1.099 s at 451.4 KB/s, for the same bytes. tools/analysis/22_scene_load.py prices it across explicit rates. Recorded as open: the two CPU cores agree to <3% on every stage but the table build, where they differ by 16.4%. px68k's C68K charges a flat 50 clocks for MULU/MULS (c68kmacro.h:1869) where the 68000 charges 38+2n, which explains 4,608 of the 8,703 clock gap. 4,095 clocks are unexplained. Nothing else in src/player/ multiplies, so no figure in FINDINGS 24-52 is affected. decode.s and stream.s are untouched; decode.bin is still 1,296 B at the same MD5. check.sh gains a stage that gates byte-exactness on both cores and deliberately does not gate the cycle counts -- MAME's clock is 1/55.46 s and a wall timing would make the green light host-sensitive. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
107 lines
5.0 KiB
Python
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([4 + n + (-(4 + n) % 4) for _, n in d.frames], 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.""")
|