Files
Dragon-s-Lair-X68k/tools/analysis/11_cpu_budget.py
T
prosolis 06b98d4b47 Price cycles in the mode decision: 37 misses become 1, for 0.26 dB
The decoder has been CPU-bound since FINDINGS 28 while the mode decision
minimised D + lam*R -- distortion against BYTES. decide() now minimises
D + lam*bytes + mu*cycles, and ratectl bisects mu per frame against the
833,333-cycle budget with the lam bisection nested inside it. On the worst
sustained window:

  sasi  27.22 -> 26.95 dB, 109.5 -> 109.4 KB/s, 37/120 misses -> 1
  scsi  29.90 -> 29.27 dB, 280.0 -> 278.6 KB/s, 51/120 misses -> 1

Bitrate does not move: the byte controller still binds, and mu changes WHICH
modes are bought. V4 is what it stops buying -- 25.2 -> 20.3% of blocks at sasi
and 15.0 -> 5.3% at scsi, where RAW takes it. That is 28.8's inversion in
practice: RAW is dearer in bytes and cheaper in cycles, so only the byte-rich
profile can buy its way out of V4.

Three things worth knowing beyond the headline:

  - The one frame that still misses, at both profiles, is FRAME 0 -- no previous
    reconstruction, so 100% changed by definition, which is also what a scene
    cut is. It comes out at the all-V1 floor of 110.6% and is emitted late on
    purpose. Freezing a cut to make a deadline is the worse failure.
  - 28.7's "11 frames are impossible" was too pessimistic. That floor held the
    SKIP set fixed and asked how cheaply the drawn blocks could be drawn; the
    real decision can also MOVE a block to SKIP, which above ~90% non-SKIP is
    the only lever left.
  - SKIP's price depends on its neighbours (13.25 cycles clustered, 45 mixed),
    which a per-block lagrangian cannot see. The way out is that the two uses
    need not share a cost function: a ranking constant inside decide(), the
    exact clustered rule for the frame-level bisection. vq_hybrid.cycles() is
    now the one definition of that rule and 11_cpu_budget.py imports it.

Gated: 09_ratectl_drift.py runs both controllers, both 0/120 drifting frames.
The cost-aware container decodes pixel-exact on the 68000 (120 frames). ON by
default in encode.py; --no-cpu-fit restores session 7. check.sh ALL GREEN.

Still a model, not a measurement, for THIS container: FINDINGS 31's cycle
figures come from vq_hybrid.cycles (within 1 point of the 68000 on four frames
of the session-7 container). Timing this one on the machine is step 1 of the
next session -- it was started and killed for time, and it is slow.

FINDINGS 31. tools/analysis/13_cpu_ratectl.py.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-23 16:24:22 -07:00

121 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Per-frame CPU cost of the real decoder, from MEASURED per-mode block costs.
python3 tools/analysis/11_cpu_budget.py [container.dlx]
FINDINGS 24.5 priced the display path as "76.6% of a 12fps frame x the non-SKIP
block fraction", i.e. every non-SKIP block costs the same. It does not: the four
block modes were measured separately on the 68000 (synthetic single-mode frames,
tools/bench/prep_dlx.py) and V4 costs 1.5x V1. Since V4 is roughly half of all
non-SKIP blocks on hard content, the old model runs ~1.8x optimistic exactly
where it matters.
This applies the measured costs to a real container's mode histograms and
reports what fraction of frames actually fit 833,333 cycles.
Costs are MEASURED (tools/bench/decode.lua), cross-checked against hand-derived
MC68000 timings in FINDINGS 28.4. They are instruction cycles against
zero-wait-state memory, so like every figure in this project since FINDINGS 24
they are a LOWER BOUND -- real GVRAM stalls the CPU.
"""
import sys, os, argparse
sys.path.insert(0, "tools/encoder")
import numpy as np
from dlx import DLX
import vq_hybrid as H
# Machine clocks, confirmed from MAME 0.277 src/mame/sharp/x68k.cpp:1133/1194/
# 1200 -- not recalled. x68000 and x68ksupr are BOTH 40_MHz_XTAL/4 = 10 MHz;
# only the XVI is faster, at 33.33_MHz_XTAL/2. So "has SCSI" and "has a faster
# CPU" are different sets of machines: the Super has SCSI at 10 MHz.
CLOCKS = {"stock": 10.0, "super": 10.0, "xvi": 33.33 / 2, "x68030": 25.0}
FPS = 12
# Cycles per block, measured on the emulated 68000 (synthetic single-mode
# frames). Defined in tools/encoder/vq_hybrid.py, which is where the mode
# decision needs them too -- one copy, not two, so a re-measurement cannot
# leave the encoder and the scorer disagreeing.
C_V1, C_V4, C_RAW = H.C_V1, H.C_V4, H.C_RAW
C_SKIP_FAST, C_SKIP_MIXED = H.C_SKIP_CLUSTERED, H.C_SKIP_MIXED
cycles = H.cycles
ap = argparse.ArgumentParser()
ap.add_argument("container", nargs="?",
default="tmp/rc_fr_singe_sasi_rcprofile.dlx")
ap.add_argument("--machine", default="stock", choices=list(CLOCKS),
help="which X68000's clock to budget against (default stock)")
ap.add_argument("--fps", type=float, default=FPS)
a = ap.parse_args()
CPUHZ = CLOCKS[a.machine] * 1e6
FPS = a.fps
FRAME = CPUHZ / FPS
if not os.path.exists(a.container):
sys.exit(f"missing {a.container}")
d = DLX(a.container)
modes = [d.modes(f) for f in range(d.nframes)]
cyc = np.array([cycles(m) for m in modes])
pct = 100 * cyc / FRAME
ns = np.array([100 * (m != 0).mean() for m in modes])
print(f"{a.container}: {d.nframes} frames, {d.nb} blocks/frame")
print(f"budget: {a.machine} @ {CLOCKS[a.machine]:.2f} MHz, {FPS:g} fps "
f"-> {FRAME:,.0f} cycles/frame")
if a.machine != "stock":
print(" (derived: scaled by clock from cycles measured on the 10 MHz core.\n"
" MAME 0.277 marks x68ksupr/x68kxvi/x68030 MACHINE_NOT_WORKING, so\n"
" this is not measured on those machines and ignores any difference\n"
" in memory timing.)")
print(f"measured block costs: SKIP {C_SKIP_FAST*4:.0f}/4 (clustered) "
f"{C_SKIP_MIXED:.0f} (mixed) V1 {C_V1:.0f} V4 {C_V4:.0f} RAW {C_RAW:.0f} cycles\n")
# --- validation against the four real frames timed on the 68000. These
# timings belong to ONE container; quoting them against any other would be
# comparing a model of this stream to a measurement of a different one.
TIMED = "tmp/rc_fr_singe_sasi_rcprofile.dlx"
TIMED_FRAMES = (("min non-SKIP", 15.4, 31.5), ("median", 48.1, 73.8),
("p90", 82.5, 116.4), ("max non-SKIP", 100.0, 135.8))
if (os.path.abspath(a.container) == os.path.abspath(TIMED)
and a.machine == "stock" and a.fps == 12):
print("model vs the frames actually timed on the 68000:")
for label, frac, meas in TIMED_FRAMES:
i = int(np.argmin(abs(ns - frac)))
print(f" {label:<14} non-SKIP {ns[i]:5.1f}% model {pct[i]:6.1f}% "
f"measured {meas:5.1f}% error {pct[i]-meas:+.1f} pt")
else:
print(f"(no 68000 timings for this container/machine -- the model was "
f"validated to\n within 1 pt on {TIMED} at stock/12fps;\n"
f" run tools/bench/decode.lua to time another container)")
print(f"\nper-frame cost, % of a {FPS:g}fps frame budget:")
print(f" measured-cost model: median {np.median(pct):5.1f} "
f"p90 {np.percentile(pct,90):5.1f} max {pct.max():5.1f}")
if a.machine == "stock" and a.fps == 12:
# 24.5's 76.6% is a 10 MHz / 12 fps figure; quoting it at another clock or
# framerate would be comparing against a model that was never stated there.
old = 76.6 * ns / 100
print(f" FINDINGS 24.5 model: median {np.median(old):5.1f} "
f"p90 {np.percentile(old,90):5.1f} max {old.max():5.1f} "
f"(optimistic by {np.median(pct)/np.median(old):.2f}x at the median)")
miss = pct > 100
print(f"\nframes that do NOT fit {FRAME:,.0f} cycles: {miss.sum()}/{d.nframes} "
f"({100*miss.mean():.0f}%)")
print(f" sustainable framerate if EVERY frame must fit: "
f"{CPUHZ/cyc.max():.1f} fps; at the mean frame {CPUHZ/cyc.mean():.1f} fps")
if miss.any():
print(f" worst {pct.max():.1f}% -- {(pct.max()-100)/100*1000/FPS:.0f} ms late "
f"on an {1000/FPS:.0f} ms frame")
print(f" the budget is first missed at {ns[miss].min():.1f}% non-SKIP blocks")
# Where do the cycles go? This is what a cost-aware mode decision would act on.
tot = np.array([[(m == k).sum() for k in range(4)] for m in modes]).sum(0)
spend = tot * np.array([C_SKIP_MIXED, C_V1, C_V4, C_RAW])
print(f"\nwhere the cycles go, over the whole window:")
for k, n in enumerate(("SKIP", "V1", "V4", "RAW")):
print(f" {n:<5} {100*tot[k]/tot.sum():5.1f}% of blocks "
f"{100*spend[k]/spend.sum():5.1f}% of the cycles")
print(f"\nV4 is {C_V4/C_V1:.2f}x a V1 block for {4}x the payload bytes -- the mode "
f"decision\nin vq_hybrid.py charges it the bytes but not the cycles.")