Session 2 reversed several of its own conclusions. The docs are append-only, so a reader could land on a superseded section and act on it. This pass makes the repo internally consistent. Defects found and fixed in STATUS.md: - claimed "Hybrid VQ with k=1024: no" as the answer to the linework question, directly contradicting FINDINGS 14, which rejected k=1024. Both profiles are k=256. - malformed profile table (six column separators, five columns). - next-steps list had two items numbered 3 and listed the full-disc survey twice. - the disk-benchmark section still read CRITICAL-PATH with "if SCSI sustains >=800 KB/s, ship pixel-exact". That was written while the bandwidth figure was misread as 4 MB/s. At 4 Mbps pixel-exact needs 92-97% of the pipe and is not available, and the ring-buffer result means the design no longer hangs on the benchmark at all. Rewritten with what it IS still worth doing: confirming the 4 Mbps provenance, and confirming DMA is used rather than PIO. FINDINGS now carries supersession blockquotes on 5, 8, 11, 17 and 18 pointing at the sections that correct them. 18 is the dangerous one -- its peak-vs- sustained test is reversed by 21 -- so it is marked DO NOT ACT ON THIS SECTION while noting the per-frame data itself remains valid. profile_gen.py had the same problem in code: it defaulted to the superseded peak sizing and returned lam=25 where the docs say lam=10. The buffered test is now the default and peak sizing is behind --size-for-peak as a bound only. A tool that contradicts the findings is worse than no tool. Also preserves the five measurement scripts that produced this session's numbers as tools/analysis/05-09, following the session 1 precedent, and adds an "explicitly abandoned -- do not re-propose" list to STATUS covering entropy coding, k=1024 codebooks and flat 4x4 VQ. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
132 lines
6.4 KiB
Python
132 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Derive quality profiles FROM a measured bandwidth, instead of guessing lam.
|
|
|
|
python3 tools/encoder/profile_gen.py --bw-kbps 488 --name scsi
|
|
python3 tools/encoder/profile_gen.py --bw-mbps 4 # same thing
|
|
|
|
Session 2 set the profile bitrates by eye off the rate-distortion knee, which
|
|
was wrong twice over (FINDINGS 17.1). This inverts the dependency: give it a
|
|
bandwidth and it returns the lam that fits, with the headroom accounted for.
|
|
|
|
Three things eat the pipe before video gets any:
|
|
|
|
1. AUDIO -- 7.8 KB/s of MSM6258 ADPCM, constant.
|
|
2. BUFFERING -- NOT peak/mean. FINDINGS 18 sized against the per-frame
|
|
peak; 21 showed that is the wrong test. The disk keeps
|
|
filling DURING a frame, so the condition is cumulative
|
|
demand vs cumulative supply, which every measured scene
|
|
passes with ZERO required prefill. Peak sizing is kept
|
|
behind --size-for-peak only as a pessimistic bound.
|
|
3. DMA CYCLE-STEAL -- the HD63450 steals ~8 clocks per 16-bit word from the
|
|
68000. At 488 KB/s that is 20% of the CPU, on top of the
|
|
blit. Bandwidth and CPU are NOT independent budgets.
|
|
FINDINGS 5 said streaming "costs essentially no CPU";
|
|
that is wrong -- cycle-stealing DMA is not free DMA.
|
|
|
|
The rate-distortion points are MEASURED (FINDINGS 17.4), not modelled, so this
|
|
interpolates real data rather than fitting a curve to a guess.
|
|
"""
|
|
import argparse
|
|
|
|
AUDIO_KBPS = 7.8
|
|
CLK = 10_000_000
|
|
FPS = 12
|
|
BLIT_FULL_FRAME_PCT = 38.3 # FINDINGS 17.2
|
|
DMA_CLOCKS_PER_WORD = 8 # FINDINGS 5 (ESTIMATE, from HD63450 timing)
|
|
|
|
# (lam, KB/s, PSNR) measured on the two probe scenes -- FINDINGS 17.4.
|
|
# 00146 is the harder scene; we size against it so profiles are not tuned to
|
|
# the easy case. Rates are RAW payload: entropy coding is ruled out (17.2).
|
|
CURVE = [
|
|
# lam 00020 KB/s 00020 dB 00146 KB/s 00146 dB
|
|
( 0, 442.1, 39.90, 467.6, 35.25),
|
|
( 10, 248.1, 39.38, 305.2, 32.27),
|
|
( 25, 182.2, 38.68, 193.5, 31.04),
|
|
( 60, 108.0, 36.94, 103.1, 29.61),
|
|
( 150, 55.6, 35.31, 56.1, 28.63),
|
|
( 300, 44.1, 34.80, 44.4, 28.28),
|
|
( 800, 32.5, 33.87, 36.1, 27.77),
|
|
]
|
|
CEILING = {"00020": 39.90, "00146": 35.25}
|
|
PEAK_OVER_MEAN = 1.9 # measured worst case, FINDINGS 18
|
|
# Per-frame fill at 12fps must cover the worst single frame, else the buffer
|
|
# has to carry the difference. Measured worst frame is 42.10 KB (00146 lam=10).
|
|
WORST_FRAME_KB = 42.10
|
|
|
|
|
|
def dma_steal_pct(kbps):
|
|
return (kbps * 1024 / 2) * DMA_CLOCKS_PER_WORD / CLK * 100
|
|
|
|
|
|
def pick(bw_kbps, peak_factor=PEAK_OVER_MEAN, margin=0.85, size_for_peak=False):
|
|
"""Largest-quality lam whose sustained demand fits inside bw_kbps.
|
|
|
|
Default is the BUFFERED test (FINDINGS 21): compare mean demand against the
|
|
sustained fill. size_for_peak=True restores the pessimistic FINDINGS 18
|
|
sizing, which is retained only as a bound -- it is not the shipping rule."""
|
|
usable = bw_kbps * margin - AUDIO_KBPS
|
|
factor = peak_factor if size_for_peak else 1.0
|
|
allow_mean = usable / factor
|
|
for lam, k20, d20, k146, d146 in CURVE:
|
|
worst = max(k20, k146)
|
|
if worst <= allow_mean:
|
|
return dict(lam=lam, mean_kbps=worst, peak_kbps=worst * factor,
|
|
psnr20=d20, psnr146=d146,
|
|
loss20=CEILING["00020"] - d20,
|
|
loss146=CEILING["00146"] - d146,
|
|
allow_mean=allow_mean, usable=usable)
|
|
return None
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
g = ap.add_mutually_exclusive_group(required=True)
|
|
g.add_argument("--bw-kbps", type=float)
|
|
g.add_argument("--bw-mbps", type=float, help="megaBITS/sec")
|
|
ap.add_argument("--name", default="profile")
|
|
ap.add_argument("--margin", type=float, default=0.85,
|
|
help="fraction of the pipe we allow ourselves (seeks, "
|
|
"container overhead, and the fact that the bandwidth "
|
|
"figure itself is folklore)")
|
|
ap.add_argument("--size-for-peak", action="store_true",
|
|
help="pessimistic FINDINGS 18 sizing (mean * 1.9). Superseded "
|
|
"by 21 -- kept only as a bound, not the shipping rule.")
|
|
a = ap.parse_args()
|
|
|
|
bw = a.bw_kbps if a.bw_kbps else a.bw_mbps * 1_000_000 / 8 / 1024
|
|
src = f"{a.bw_mbps} Mbps" if a.bw_mbps else f"{a.bw_kbps} KB/s"
|
|
print(f"bandwidth {src} = {bw:.0f} KB/s sustained")
|
|
print(f" usable at {a.margin:.0%} margin : {bw*a.margin:.0f} KB/s")
|
|
print(f" less audio ({AUDIO_KBPS}) : {bw*a.margin-AUDIO_KBPS:.0f} KB/s for video")
|
|
fill_per_frame = bw / FPS
|
|
if a.size_for_peak:
|
|
print(f" less peak/mean {PEAK_OVER_MEAN}x : "
|
|
f"{(bw*a.margin-AUDIO_KBPS)/PEAK_OVER_MEAN:.0f} KB/s mean allowance"
|
|
f" [pessimistic, FINDINGS 18 -- superseded]")
|
|
else:
|
|
print(f" fill per frame time : {fill_per_frame:.2f} KB "
|
|
f"(worst measured frame {WORST_FRAME_KB:.2f} KB"
|
|
f"{' -- COVERED' if fill_per_frame >= WORST_FRAME_KB else ' -- needs buffer'})")
|
|
|
|
r = pick(bw, margin=a.margin, size_for_peak=a.size_for_peak)
|
|
if r is None:
|
|
print("\n NO PROFILE FITS -- even lam=800 overruns. Lower the framerate,")
|
|
print(" the resolution, or get more bandwidth.")
|
|
return
|
|
steal = dma_steal_pct(r["mean_kbps"])
|
|
print(f"\n -> {a.name}: lam={r['lam']}, {r['mean_kbps']:.0f} KB/s mean")
|
|
print(f" quality 00020 {r['psnr20']:.2f} dB (-{r['loss20']:.2f} from ceiling)")
|
|
print(f" 00146 {r['psnr146']:.2f} dB (-{r['loss146']:.2f} from ceiling)")
|
|
print(f" CPU blit {BLIT_FULL_FRAME_PCT:.0f}% + DMA steal {steal:.1f}% "
|
|
f"= {BLIT_FULL_FRAME_PCT+steal:.0f}% of the frame budget")
|
|
if BLIT_FULL_FRAME_PCT + steal > 85:
|
|
print(" WARNING: CPU is now the binding constraint, not the bus.")
|
|
if not a.size_for_peak and fill_per_frame < WORST_FRAME_KB:
|
|
short = WORST_FRAME_KB - fill_per_frame
|
|
print(f" note: worst frame exceeds one frame-time of fill by "
|
|
f"{short:.2f} KB -- buffer must carry it (2MB RAM, non-issue).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|