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
147 lines
6.7 KiB
Python
147 lines
6.7 KiB
Python
"""Seek slack: how long a branch point can stop delivery (STATUS 4, FINDINGS 51).
|
|
|
|
19_ring_stream.py asks whether a container ARRIVES in time, and prints one
|
|
"seek slack" column derived statically as (usable ring - maxrec)/mean record.
|
|
That is a capacity estimate and it quietly assumes the ring is full when the
|
|
seek happens. It is not, and the difference is the whole finding:
|
|
|
|
A ring's slack is ACCUMULATED, not owned. It is built out of the surplus
|
|
between the pipe and the wire demand, at (pipe - wire) bytes per second, and
|
|
a seek spends all of it. How long a branch point can stall is a property of
|
|
the ring; how soon the NEXT branch point can be afforded is a property of the
|
|
surplus, and a bigger ring makes that one WORSE.
|
|
|
|
This is the paced-rig model (tools/bench/stream.lua with DLX_PACE=1) written
|
|
independently, and it exists to be compared against it, not to replace it. The
|
|
rig drives a real 68000 through a real ring and is the measurement; this is the
|
|
cheap sweep that says where to point it. Where they disagree, the rig wins.
|
|
|
|
python3 tools/analysis/20_seek_slack.py [container ...] --kbps R [R ...]
|
|
[--ring KB [KB ...]]
|
|
|
|
`--kbps` is REQUIRED and takes no default, for the reason FINDINGS 50 gives.
|
|
"""
|
|
import sys, os, argparse
|
|
sys.path.insert(0, "tools/encoder")
|
|
import numpy as np
|
|
from dlx import DLX
|
|
import ratectl as RC
|
|
|
|
SECTOR = 512
|
|
|
|
|
|
def records(path):
|
|
d = DLX(path)
|
|
rec = np.array(d.record_lengths(), np.int64)
|
|
return d, rec
|
|
|
|
|
|
def paced_sim(rec, ring, fill_per_frame, ticks_per_frame=8):
|
|
"""Paced-decoder ring sim. Returns TWO per-tick lookahead series.
|
|
|
|
THE ANSWER IS BRACKETED TO ONE RECORD AND IS NOT SHARPER THAN THAT. At
|
|
these rates the pipe delivers almost exactly one record per frame slot, so
|
|
"how many records are resident at slot i" depends on whether you look before
|
|
or after that slot's delivery -- and the two answers differ by one, every
|
|
time. Sampled after, this agreed with the rig's ceiling in 33 of 35 cells;
|
|
sampled before, it was exactly one record lower in 33 of 35. Neither is
|
|
wrong. Picking the one that matched would have been fitting the model to
|
|
the measurement and then reporting the agreement as a cross-check, so both
|
|
are returned and the caller prints the range. The rig sits at the top of it.
|
|
|
|
The producer is `aligned` (19_ring_stream.py): it will not start a record it
|
|
cannot finish before the end of the ring, and it will not place one over
|
|
bytes the decoder still owns. The decoder consumes exactly one record per
|
|
frame time and releases it whole.
|
|
|
|
Sub-stepping matters. Delivery and consumption interleave inside a frame
|
|
time on the rig -- the producer runs on MAME's machine-frame notifier, ~5x
|
|
per 12fps slot -- and a model that delivers a whole frame's bytes at once
|
|
can place a record into space the decoder has not released yet, or refuse
|
|
one it has. Eight sub-steps is well past the point the answer stops moving.
|
|
"""
|
|
n = len(rec)
|
|
live = [] # [idx, off, len] still owned by the decoder
|
|
wcur, nsent, credit = 0, 0, 0.0
|
|
lo, hi, ring_ref, rate_ref = [], [], 0, 0
|
|
|
|
def overlaps(off, ln):
|
|
return any(off < r[1] + r[2] and r[1] < off + ln for r in live)
|
|
|
|
for i in range(n):
|
|
if nsent < n:
|
|
lo.append(sum(1 for r in live if r[0] >= i))
|
|
for _ in range(ticks_per_frame):
|
|
credit += fill_per_frame / ticks_per_frame
|
|
while nsent < n:
|
|
r = int(rec[nsent])
|
|
if credit < r:
|
|
rate_ref += 1
|
|
break
|
|
w, hole = wcur, 0
|
|
if w + r > ring:
|
|
w, hole = 0, ring - wcur
|
|
if overlaps(w, r):
|
|
ring_ref += 1
|
|
break
|
|
# sector quantisation: a partial sector is not resident
|
|
credit -= r
|
|
live.append([nsent, w, r])
|
|
wcur, nsent = w + r, nsent + 1
|
|
if nsent < n:
|
|
hi.append(sum(1 for r in live if r[0] >= i))
|
|
# the decoder consumed record i during the slot and releases it whole
|
|
live = [r for r in live if r[0] > i]
|
|
return np.array(lo), np.array(hi), ring_ref, rate_ref
|
|
|
|
|
|
def main():
|
|
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): this project has never measured the "
|
|
"delivery pipe and a default is how the last unmeasured "
|
|
"one stayed load-bearing for five sessions.")
|
|
ap.add_argument("--ring", type=float, nargs="+",
|
|
default=[64, 96, 128, 192, 256, 384, 512])
|
|
a = ap.parse_args()
|
|
FPS = 12
|
|
|
|
for path in a.containers:
|
|
if not os.path.exists(path):
|
|
print(f"{path}: MISSING -- skipped\n"); continue
|
|
d, rec = records(path)
|
|
wire = rec.mean() * FPS / 1024 + RC.AUDIO_KBPS
|
|
print(f"=== {path}: {d.nframes} frames @ {d.fps}fps, mean record "
|
|
f"{rec.mean()/1024:.1f} KB, wire {wire:.1f} KB/s")
|
|
print(f"{'ring KB':>8} {'pipe':>8} {'ceiling':>9} {'build s':>8} "
|
|
f"{'mean':>11} bound")
|
|
for ring_kb in a.ring:
|
|
ring = int(ring_kb * 1024)
|
|
if rec.max() > ring:
|
|
print(f"{ring_kb:>8.0f} maxrec {rec.max():,} does not fit")
|
|
continue
|
|
for kbps in a.kbps:
|
|
fill = ((kbps - RC.AUDIO_KBPS) * 1024 / FPS) if kbps > 0 else 1e12
|
|
lo, hi, ring_ref, rate_ref = paced_sim(rec, ring, fill)
|
|
c_lo, c_hi = int(lo.max()), int(hi.max())
|
|
build = int(np.argmax(hi >= c_hi)) if len(hi) else -1
|
|
print(f"{ring_kb:>8.0f} {kbps:>8.0f} "
|
|
f"{f'{c_lo}-{c_hi}':>9} {build/FPS:>8.2f} "
|
|
f"{f'{lo.mean():.1f}-{hi.mean():.1f}':>11} "
|
|
f"{'ring' if ring_ref else 'rate'}")
|
|
# The surplus model, stated so it can be checked against the sweep
|
|
# above rather than believed: slack accrues at (pipe - wire) and a
|
|
# full ring holds `ceiling` records, so a branch point costs about
|
|
# ceiling*mean_record/(pipe - wire) seconds of play to earn back.
|
|
print()
|
|
print("Slack is accumulated, not owned. A bigger ring raises the ceiling AND")
|
|
print("lengthens the climb to it: the surplus (pipe - wire) is what fills it,")
|
|
print("and that is set by the encoder and the medium, not by the buffer.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|