#!/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()