Handoff: reconcile docs and tooling with the corrections made this session

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
This commit is contained in:
prosolis
2026-08-23 12:28:41 -07:00
parent fb8a1462b0
commit 64cd1ffd72
10 changed files with 345 additions and 73 deletions
+19 -3
View File
@@ -11,12 +11,18 @@ pushing ~22 minutes of Don Bluth animation through a 10MHz 68000.
decision, and a section on measurement traps that produced three separate
false results. Read §4 before trusting any pipeline number.
- **`docs/STATUS.md`** — current state, working setup, blockers, next steps.
**Start here.** It also lists what has been explicitly abandoned, so old ideas
do not get re-proposed.
- **`docs/BENCHMARK.md`** — how to measure the storage subsystem, and why a
bandwidth figure out of MAME would be meaningless.
- **`docs/HARDWARE.md`** — X68000 GVRAM/CRTC reference.
## Layout
```
docs/ findings, status, hardware reference
tools/analysis/ frame-analysis scripts (01/02 marked BROKEN as regression refs)
tools/analysis/ measurement scripts, numbered in the order they were written
(01/02 marked BROKEN deliberately, kept as regression refs).
Run from the repo root — they import from tools/encoder/.
tools/bench/ MAME Lua injection harness + 68000 benchmark sources
tools/vasm/ vasm m68k assembler (built from source)
tools/encoder/ hybrid VQ encoder + DLX1 container writer (working)
@@ -31,12 +37,22 @@ python3 tools/encoder/extract.py 00020 /tmp/fr 12 crop
python3 tools/encoder/encode.py /tmp/fr out.dlx --profile sasi --preview p.png
```
Two quality profiles ship from one codec and one decoder — `sasi` (45 KB/s) and
`scsi` (120 KB/s) are two points on the same rate-distortion curve. The codec is
Two quality profiles ship from one codec and one decoder — `sasi` (110 KB/s) and
`scsi` (280 KB/s) are two points on the same rate-distortion curve. The codec is
a Cinepak-style hybrid: each 4x4 block is coded as SKIP, one 4x4 codeword, four
2x2 codewords, or RAW literal pixels, chosen per block by rate-distortion.
The RAW escape means `lam=0` is pixel-exact against the palettised frame, so the
quality knob spans lossless to heavily-compressed without changing the bitstream.
Profiles are derived from a bandwidth figure, not chosen by eye:
```
python3 tools/encoder/profile_gen.py --bw-mbps 4 --name scsi
```
> **On reading `docs/FINDINGS.md`:** it is append-only and several later sections
> overturn earlier ones. Superseded sections carry a blockquote at the top
> pointing to the correction — heed those, especially 18 (reversed by 21).
Source media (`DRAGONS_LAIR.iso`) and ROMs are gitignored — supply your own.
+20 -12
View File
@@ -106,20 +106,24 @@ FINDINGS 17.4:
python3 tools/encoder/profile_gen.py --bw-mbps 4 --name scsi
```
It accounts for the three things that eat the pipe before video sees any of it:
audio (7.8 KB/s), peak-to-mean burstiness (measured 1.4-1.9x), and it reports
the DMA cycle-steal so the CPU coupling stays visible.
It accounts for what eats the pipe before video sees any of it: audio
(7.8 KB/s), the buffering condition, and it reports the DMA cycle-steal so the
CPU coupling stays visible.
At 4 Mbps it currently returns:
At 4 Mbps it returns:
| | lam | mean | peak | 00020 | 00146 | CPU |
|---|---|---|---|---|---|---|
| today (no rate control) | 25 | 194 KB/s | 368 KB/s | -1.22 dB | -4.21 dB | 53% |
| with rate control wired | 10 | 305 KB/s | 305 KB/s | -0.52 dB | -2.98 dB | 51% |
| sizing rule | lam | mean | 00020 | 00146 | CPU |
|---|---|---|---|---|---|
| **buffered (default, FINDINGS 21)** | **10** | 305 KB/s | -0.52 dB | -2.98 dB | 51% |
| `--size-for-peak` (FINDINGS 18, superseded) | 25 | 194 KB/s | -1.22 dB | -4.21 dB | 46% |
**Rate control is worth a full step on the quality ladder** — it is not a
tidiness feature, it is the difference between sizing for the peak and sizing
for the mean. That is the strongest argument yet for wiring up `ratectl.py`.
The default is the buffered test: cumulative demand vs cumulative supply.
Ring-buffer simulation gives **zero required prefill** for every measured scene,
so `lam=10` ships without rate control. `--size-for-peak` reproduces the earlier
pessimistic sizing and is kept only as a bound.
**Rate control is therefore insurance, not a fix.** Its value is a deterministic
ceiling over the 220 streams not yet measured — see the survey caveat below.
## What would change the design
@@ -127,7 +131,11 @@ for the mean. That is the strongest argument yet for wiring up `ratectl.py`.
collapses toward today's `sasi`, and the two profiles stop being meaningfully
different. At that point reconsider 10 fps, or a narrower active area.
- **If sustained is much above** (>=8 Mbps / 976 KB/s): `lam=0` fits with
margin and the port ships **pixel-exact** video on SCSI.
margin and the port ships **pixel-exact** video on SCSI. At the current
4 Mbps figure this is NOT available — `lam=0` needs 92-97% of the pipe.
- **If the full-disc survey finds a sustained action sequence hotter than
00146** (313 KB/s mean, the worst of 4 clips sampled): that is the scenario
rate control exists for, and the reason to wire it up before the survey run.
- **If DMA cannot be used** and transfers fall back to PIO, the CPU cost rises
from ~15% to something far larger and CPU becomes the binding constraint.
This is the single worst outcome and is worth checking early in Tier 1.
+24
View File
@@ -103,6 +103,10 @@ regression references; 03 and 04 are the correct ones.
## 5. Storage interface — the SASI/SCSI split
> **SUPERSEDED IN PART.** The claim below that DMA means streaming "costs
> essentially no CPU" is **wrong** — see 19. The bandwidth figures here are
> folklore; the working figure is now **4 Mbps = 488 KB/s** (21).
[Yasuma, X68030 internal SCSI controller]
- Interface: **SCSI-1**, 50-pin, 5 MB/s bus spec
@@ -196,6 +200,11 @@ Caveat: all of the above is from README/repo-tree summaries, not their source.
## 8. CORRECTION to session 1: halving the framerate does NOT halve the bitrate
> **PARTLY SUPERSEDED.** The framerate correction stands. The
> "changed-spans + deflate = 247 KB/s" figure is a **compression upper bound,
> not a shippable design** — deflate decode does not fit the 68000's frame
> budget. See 17.2.
Session 1 measured 365 KB/s for naive delta+RLE at 24 fps and wrote
"(~183 KB/s at 12fps)". **That extrapolation is wrong.** Decimating to 12 fps
roughly doubles the per-frame delta, so the *rate* stays nearly flat.
@@ -267,6 +276,9 @@ should: under a hard ceiling, detail blocks are the first thing sacrificed.
## 11. Codebook size sweep (flat 4x4, for reference)
> **SUPERSEDED.** The k=1024 result below is an artifact of a rate model that
> charged 1 byte for a 10-bit index. k=256 ships. See 14.
| block | k | PSNR | loss | key B | changed% | KB/s @12 | codebook RAM |
|---|---|---|---|---|---|---|---|
| 4x4 | 256 | 30.46 | 8.39 | 3072 | 52.7 | 28.5 | 8K |
@@ -373,6 +385,12 @@ transcription errors before any of it is committed to 68000 tables.
## 17. The profiles were set far too low — and entropy coding is a CPU trap
> **PARTLY SUPERSEDED.** 17.1's diagnosis (the profiles were not derived from
> hardware) and 17.2's CPU analysis both stand. But 17 reasoned against a
> misread bandwidth of 4 MB/s; the correct figure is **4 Mbps = 488 KB/s**, so
> the "ship pixel-exact if SCSI sustains >=800 KB/s" conclusion in 17.5 is
> **not available**. See 18 and 21.
Prompted by the user asking why the SCSI profile was only 75 KB/s. It should not
have been. Two separate errors, one of them serious.
@@ -453,6 +471,12 @@ SCSI is now waiting on one measurement.
## 18. Peak-to-mean burstiness — the mean was hiding the problem
> **SUPERSEDED — DO NOT ACT ON THIS SECTION.** The peak-vs-sustained comparison
> below is the **wrong test**. With a ring buffer the correct test is cumulative
> demand vs cumulative supply, and both profiles pass it with **zero required
> prefill**. `scsi` at lam=10 ships. See 21. The per-frame peak numbers
> themselves are still valid data; only the conclusion drawn from them is wrong.
Prompted by the user clarifying that the bandwidth figure is **4 Mbps = 488 KB/s**,
not 4 MB/s. That is ~8x tighter than what 17 was reasoning against, and it
changes the answer.
+56 -38
View File
@@ -19,7 +19,7 @@ as two quality profiles. This is now implemented rather than hypothetical — th
bitrate ceiling is a build parameter in `tools/encoder/ratectl.py`:
| profile | target | lam | quality (00020 / 00146) | machine |
|---|---|---|---|---|---|
|---|---|---|---|---|
| `sasi` | 110 KB/s | 60 | 36.9 / 29.6 dB | stock 10MHz ACE/EXPERT |
| `scsi` | 280 KB/s | 10 | 39.4 / 32.3 dB | Super/XVI, or CZ-6BS1 board |
@@ -54,8 +54,9 @@ rate-distortion curve, not two codecs.
## What session 2 settled
1. **The critical-path question is answered.** "Does VQ soften Bluth's linework
unacceptably?" — **flat 4x4 k=256 VQ: yes, badly. Hybrid VQ with k=1024: no.**
Verified by eye, not just PSNR. See `docs/FINDINGS.md` 9-11.
unacceptably?" — **flat 4x4 VQ: yes, badly. The hybrid (SKIP/V1/V4/RAW): no.**
Verified by eye, not just PSNR. See `docs/FINDINGS.md` 9-11 and the two
images in `docs/images/`. Both profiles use **k=256**; see item 2b.
2. **Session 1's 12fps bitrate was wrong** (183 KB/s claimed, 340 KB/s measured).
Halving the framerate does not halve the bitrate. FINDINGS 8.
2b. **A fourth false-good result was produced and caught this session** — k=1024
@@ -119,49 +120,66 @@ notifier subscription in a global; the stack register is `SP` not `A7`;
---
## STILL BLOCKED: disk throughput benchmark
## Disk throughput benchmark — still blocked, no longer gating
Unchanged from session 1 — `IOCS _B_READ` returns -1 uniformly. Full diagnosis
and the four untested hypotheses are in session 1's notes (git history of this
file, commit 65112b9).
`IOCS _B_READ` returns -1 uniformly. Full diagnosis and the four untested
hypotheses are in session 1's notes (git history of this file, commit 65112b9);
the ordered plan for retrying is in **`docs/BENCHMARK.md`**.
**This is now CRITICAL-PATH, not optional.** Session 1 dismissed it because
"VQ at 30 KB/s is correct whether SASI does 300 or 600 KB/s". That reasoning is
dead: the profiles now sit at 110 and 280 KB/s, close enough to the folklore
ceilings that the error bars change the product. Specifically —
**Status changed twice this session — read this rather than the git history.**
It was briefly promoted to critical-path while the working bandwidth figure was
misread as 4 MB/s. With the correct figure (**4 Mbps = 488 KB/s**) and the
ring-buffer simulation showing **zero required prefill** for both profiles
(FINDINGS 21), the design no longer hangs on it. Pixel-exact on SCSI is **not**
available at 4 Mbps — it needs 92-97% of the pipe — so there is no longer a
"measure it and maybe ship transparent" decision waiting.
**If SCSI sustains >=800 KB/s, the correct `scsi` profile is `lam=0`: pixel-exact
video, ~450 KB/s, and only 38% of the CPU budget.** Whether this port ships
transparent or lossy on SCSI is waiting on one measurement.
What the benchmark is still worth doing for:
- **Confirming the 4 Mbps figure.** It is user-supplied and its provenance is
not recorded. Every profile hangs off it.
- **Confirming DMA is actually used.** If transfers fall back to PIO the CPU
cost rises far above the ~12-15% cycle-steal estimate and CPU becomes the
binding constraint. This is the worst plausible outcome and the cheapest to
check — do it first.
Next move is the untried SCSI path: `-exp1 cz6bs1 -hard disk.chd`.
---
**Do not try to get the bandwidth number out of MAME.** Its SCSI/SASI devices are
functional models, not timing-accurate; a KB/s figure from MAME measures the
emulator's scheduler. `docs/BENCHMARK.md` covers the three-tier approach
(MAME validates the path, derivation bounds it, real hardware settles it).
## Next steps, in priority order
1. **Full-disc survey** — we have measured 4 clips of 1.2-1.7 s out of 224
streams, and 00146 already runs 23% hotter than 00020. A sustained action
sequence is the one thing that could still break the bitrate. This is now
the highest-value measurement, ahead of rate control.
2. **Wire rate control into `encode.py`** — no longer a blocker (FINDINGS 21),
but it is what gives a deterministic ceiling over content we have not
measured, which was the original reason for choosing VQ. Insurance, not a fix.
3. ~~Entropy-code the payload~~**ABANDONED, see FINDINGS 17.2.** Deflate
decode is ~216% of the frame budget on a 68000 and LZ4 is ~54%; there is no
room beside a 38% blit. All bitrates are raw payload. This also demotes the
"247 KB/s lossless" figure in FINDINGS 8 to a compression bound, not a design.
3. **68000 decoder skeleton**: parse `DLX1`, expand codebooks to word-per-pixel,
blit V1/V4/RAW/SKIP. Measure real cycles with the existing MAME Lua harness —
this is the first time the harness gets used for its actual purpose.
4. **Full-disc survey** — classify menu vs content first (FINDINGS 13), then
measure bitrate across all 224 streams per profile.
1. **Full-disc survey.** Only 4 clips of 1.2-1.7 s out of 224 streams have been
measured, and 00146 already runs 23% hotter than 00020. A *sustained* action
sequence is the one thing that could still break the bitrate. Classify menu
vs content first (FINDINGS 13) or the averages are diluted by static menus.
**Vectorise `_paint` before this run** — it is a Python per-block loop.
2. **68000 decoder skeleton.** Parse `DLX1`, expand codebooks to word-per-pixel,
blit SKIP/V1/V4/RAW. Measure real cycles with the existing MAME Lua harness —
the first time that harness gets used for its actual purpose. Validates the
38% full-frame blit estimate that the whole CPU budget rests on.
3. **Wire rate control into `encode.py`.** No longer a blocker (FINDINGS 21), but
it is what gives a deterministic ceiling over content not yet measured, which
was the original reason for choosing VQ. Insurance, not a fix. Pairs with (1).
4. **Confirm DMA vs PIO in MAME** (see the benchmark section above) — cheap, and
the only thing that could still move CPU into the binding position.
5. **Resolve the framing question** (FINDINGS 12: crop vs squash vs wide).
6. Unblock the disk benchmark via the SCSI path, then re-set profile bitrates.
7. Import the SNES project's `data/events/` (MIT, cleared) as the scene graph.
Cross-check against DirkSimple (zlib) which has the same data independently.
8. ADPCM audio: MSM6258, 15.6kHz mono, 7.8 KB/s — already budgeted in `ratectl`,
not yet extracted or encoded.
Needs an eyeball against arcade reference, not a measurement.
6. **Import the scene graph.** SNES project `data/events/` (MIT, cleared),
cross-checked against DirkSimple (zlib) which transcribed the same data
independently — diff them to catch transcription errors before committing
any of it to 68000 tables.
7. **ADPCM audio.** MSM6258, 15.6kHz mono, 7.8 KB/s — already budgeted in
`ratectl.py`, not yet extracted or encoded.
### Explicitly abandoned — do not re-propose
- ~~Entropy-code the payload.~~ Deflate decode is ~216% of the frame budget on a
68000; LZ4 is ~54% with no room beside a 38% blit (FINDINGS 17.2). All bitrates
are raw payload. This also demotes the "247 KB/s lossless" figure in FINDINGS 8
to a compression upper bound, not a shippable design.
- ~~k=1024 codebooks.~~ False-good result from a rate model that charged 1 byte
for a 10-bit index (FINDINGS 14). k=256 wins at every matched bitrate.
- ~~Flat 4x4 VQ.~~ Rejected by eye (FINDINGS 9).
## Not yet started
- Any 68000 player code
+63
View File
@@ -0,0 +1,63 @@
"""What does LOSSLESS (palettised-quality) delta coding actually cost at 12fps?
Session 1 measured only a hand-rolled row-span+RLE (3.2:1). The 68000 has ~12x
CPU headroom, so a real LZ decoder is affordable -- byte copies are what the
68000 is good at. This measures the achievable floor at zero extra quality loss.
"""
import sys, zlib, lzma; sys.path.insert(0,'tools/encoder')
import vq, numpy as np
S=sys.argv[1]
RAW=256*192
def rle_delta(a,b,gap=4):
"""session 1's row-span + RLE, reimplemented for comparison"""
t=0
for y in range(a.shape[0]):
d=np.nonzero(a[y]!=b[y])[0]
if not len(d): continue
sp=[];s=d[0];p=d[0]
for x in d[1:]:
if x-p>gap: sp.append((s,p)); s=x
p=x
sp.append((s,p)); t+=2
for s0,e0 in sp:
seg=b[y][s0:e0+1]; t+=2; i=0
while i<len(seg):
r=1
while i+r<len(seg) and seg[i+r]==seg[i] and r<127: r+=1
t += 2 if r>=3 else r; i+=r
return t
print(f'{"scene":8}{"frames":>7}{"raw KB/s":>9}{"rle":>8}{"xor+zl":>8}{"delta+zl":>9}{"lzma":>8}',flush=True)
tot={}
for s in ['00010','00020','00146','00181']:
rgb=vq.load_frames(f'{S}/fr_{s}')
ref,pal=vq.scene_palette(rgb)
idx=vq.palettise(rgb,ref)
n=len(idx)-1
r_rle=sum(rle_delta(idx[i-1],idx[i]) for i in range(1,len(idx)))/n
# XOR against previous frame then deflate -- unchanged pixels become 0 runs
r_xor=sum(len(zlib.compress((idx[i]^idx[i-1]).tobytes(),9)) for i in range(1,len(idx)))/n
# RLE-of-changed-spans payload, then deflate on top
def spans(a,b,gap=8):
out=bytearray()
for y in range(a.shape[0]):
d=np.nonzero(a[y]!=b[y])[0]
if not len(d): continue
sp=[];st=d[0];p=d[0]
for x in d[1:]:
if x-p>gap: sp.append((st,p)); st=x
p=x
sp.append((st,p))
for s0,e0 in sp:
out += bytes([y,s0,e0-s0]) + b[y][s0:e0+1].tobytes()
return bytes(out)
r_dz=sum(len(zlib.compress(spans(idx[i-1],idx[i]),9)) for i in range(1,len(idx)))/n
r_lz=sum(len(lzma.compress(spans(idx[i-1],idx[i]),preset=9))for i in range(1,len(idx)))/n
f=12/1024.0
print(f'{s:8}{len(idx):7}{RAW*f:9.0f}{r_rle*f:8.0f}{r_xor*f:8.0f}{r_dz*f:9.0f}{r_lz*f:8.0f}',flush=True)
for k,v in [('rle',r_rle),('xor',r_xor),('dz',r_dz),('lz',r_lz)]: tot.setdefault(k,[]).append(v)
print()
for k,v in tot.items():
m=np.mean(v)
print(f'{k:8} mean {m:7.0f} B/frame {m*12/1024:6.0f} KB/s {m*12*22*60/1048576:5.0f} MB/22min ratio {RAW/m:.1f}:1')
+15
View File
@@ -0,0 +1,15 @@
"""Fair k=256 vs k=1024 comparison: k=1024 pays 2-byte indices, so the +2.4 dB
it showed earlier may be entirely eaten by the doubled payload. Sweep lam and
report the rate-distortion CURVE, then compare at matched KB/s."""
import sys; sys.path.insert(0,'tools/encoder')
import vq_hybrid as H, numpy as np
S=sys.argv[1]
for s in ['00020','00146']:
print(f'=== scene {s} ===',flush=True)
for k in [256,1024]:
m=H.build(f'{S}/fr_{s}',k1=k,k4=k,iters=16)
ib=1 if k<=256 else 2
print(f' k={k} ({ib}-byte idx) {"lam":>7}{"PSNR":>8}{"KB/s":>8}{"SKIP":>7}{"V1":>6}{"V4":>6}{"RAW":>6}',flush=True)
for lam in [25.,100.,300.,800.,2000.]:
e=H.encode(m,lam=lam); r=H.evaluate(m,e)
print(f' {"":16}{lam:7.0f}{r["psnr"]:8.2f}{r["kbps"]:8.1f}{r["skip"]:7.1f}{r["v1"]:6.1f}{r["v4"]:6.1f}{r["raw"]:6.1f}',flush=True)
+62
View File
@@ -0,0 +1,62 @@
"""How high should the SCSI profile go?
Two questions the current profiles never answered:
1. With the payload deflated, where does the hybrid's lam->0 end actually land?
(Un-deflated it is 439 KB/s, which is NOT comparable to the 247 KB/s
lossless changed-spans+deflate path.)
2. Is there any point shipping lossy VQ on SCSI at all, or does the lossless
path dominate once both are entropy-coded?
"""
import sys, zlib; sys.path.insert(0,'tools/encoder')
import vq_hybrid as H, vq as VQ, numpy as np, struct
S=sys.argv[1]
def pack_modes(mode):
n=len(mode); out=bytearray((n*2+7)//8)
for i,m in enumerate(mode): out[i//4] |= (int(m)&3)<<(6-2*(i%4))
return bytes(out)
def payload(mode,l1,l4g,src,nbx):
b=bytearray()
for i,mo in enumerate(mode):
if mo==1: b.append(int(l1[i]))
elif mo==2:
for j in range(4): b.append(int(l4g[i][j]))
elif mo==3:
by,bx=divmod(i,nbx); b+=src[by*4:by*4+4,bx*4:bx*4+4].tobytes()
return bytes(b)
def spans(a,b,gap=8):
out=bytearray()
for y in range(a.shape[0]):
d=np.nonzero(a[y]!=b[y])[0]
if not len(d): continue
sp=[];st=d[0];p=d[0]
for x in d[1:]:
if x-p>gap: sp.append((st,p)); st=x
p=x
sp.append((st,p))
for s0,e0 in sp: out+=bytes([y,s0,e0-s0])+b[y][s0:e0+1].tobytes()
return bytes(out)
for s in ['00020','00146']:
m=H.build(f'{S}/fr_{s}',k1=256,k4=256,iters=16)
pal,idx,W=m['pal'],m['idx'],m['W']; nbx=W//4
ceil_db=np.mean([VQ.psnr(o,pal[i]) for o,i in zip(m['rgb'],idx)])
# lossless reference, same frames
lz=np.mean([len(zlib.compress(spans(idx[i-1],idx[i]),9)) for i in range(1,len(idx))])
print(f'=== {s} palette ceiling {ceil_db:.2f} dB ===',flush=True)
print(f' LOSSLESS changed-spans+deflate : {lz*12/1024:6.1f} KB/s (pixel-exact)',flush=True)
print(f' {"lam":>6}{"PSNR":>8}{"raw KB/s":>10}{"defl KB/s":>11}{"gain":>7}{"RAW%":>7}',flush=True)
for lam in [0.,10.,25.,60.,150.,300.,800.]:
e=H.encode(m,lam=lam); r=H.evaluate(m,e)
tot_r=tot_d=0
for f,im in enumerate(idx):
B1=H.blocks_of(im,pal,4,4); l1=VQ.assign(B1,m['C1s'])
B4=H.blocks_of(im,pal,2,2); l4=VQ.assign(B4,m['C4s'])
q=H._group_2x2_into_4x4(np.arange(len(l4)),W); l4g=l4[q].reshape(-1,4)
blob=pack_modes(e['modes'][f])+payload(e['modes'][f],l1,l4g,im,nbx)
tot_r+=len(blob); tot_d+=len(zlib.compress(blob,9))
n=len(idx)
rk=tot_r/n*12/1024; dk=tot_d/n*12/1024
print(f' {lam:6.0f}{r["psnr"]:8.2f}{rk:10.1f}{dk:11.1f}{rk/dk:7.2f}{r["raw"]:7.1f}',flush=True)
+18
View File
@@ -0,0 +1,18 @@
"""At a 488 KB/s (4 Mbps) ceiling and ~52% mean utilisation, the mean is not the
risk -- the peaks are. Measure per-frame peak-to-mean, then check whether the
leaky-bucket rate controller actually holds the ceiling."""
import sys; sys.path.insert(0,'tools/encoder')
import vq_hybrid as H, ratectl as RC, numpy as np
S=sys.argv[1]
BW=4_000_000/8/1024
print(f"ceiling {BW:.0f} KB/s (4 Mbps), audio {RC.AUDIO_KBPS} KB/s\n")
print(f'{"scene":8}{"lam":>5}{"mean":>8}{"p90":>8}{"MAX":>8}{"pk/mean":>9}{"MAX % of pipe":>15}',flush=True)
for s in ['00010','00020','00146','00181']:
m=H.build(f'{S}/fr_{s}',k1=256,k4=256,iters=16)
for lam in [60.,10.]:
e=H.encode(m,lam=lam)
kb=e['sizes']*12/1024 # per-frame instantaneous KB/s
tot=kb+RC.AUDIO_KBPS
print(f'{s:8}{lam:5.0f}{tot.mean():8.1f}{np.percentile(tot,90):8.1f}'
f'{tot.max():8.1f}{tot.max()/tot.mean():9.2f}{tot.max()/BW*100:14.1f}%'
+ (' OVER' if tot.max()>BW else ''),flush=True)
+34
View File
@@ -0,0 +1,34 @@
"""Ring-buffer simulation. The peak-vs-sustained comparison in FINDINGS 18 was
the wrong test: with SD-backed SCSI the fill rate is a CONSTANT, and a burst
frame is absorbed by the buffer rather than having to arrive within one frame.
What actually matters:
1. required PREFILL so the buffer never underruns mid-scene
2. STALL TOLERANCE at a branch point -- Dragon's Lair seeks between streams,
and the buffer drains while the seek completes
"""
import sys; sys.path.insert(0,'tools/encoder')
import vq_hybrid as H, ratectl as RC, numpy as np
S=sys.argv[1]; BW=4_000_000/8/1024; FPS=12
fill=BW/FPS
print(f"fill {BW:.0f} KB/s = {fill:.2f} KB per frame time\n")
print(f'{"scene":8}{"lam":>5}{"mean":>8}{"max f":>8}{"prefill":>9}{"stall @0KB":>12}{"stall @256KB":>14}')
for s in ['00010','00020','00146','00181']:
m=H.build(f'{S}/fr_{s}',k1=256,k4=256,iters=16)
for lam in [10.,60.]:
e=H.encode(m,lam=lam)
kb=e['sizes']/1024 + RC.AUDIO_KBPS/FPS # KB demanded per frame
# cumulative deficit: worst shortfall of supply vs demand
deficit=np.maximum.accumulate(np.cumsum(kb-fill))
prefill=max(0.0,deficit.max())
# stall tolerance: with buffer B prefilled, how many frame times can the
# fill be zero (seeking) before the buffer empties, at mean drain
mean_kb=kb.mean()
stall0 = prefill/mean_kb if mean_kb>0 else 0
stall256 = (256.0+0.0)/mean_kb
print(f'{s:8}{lam:5.0f}{kb.mean()*FPS:8.1f}{kb.max():8.2f}{prefill:9.1f}'
f'{stall0:9.1f} fr{stall256:11.1f} fr')
print()
print("prefill = KB the buffer must hold before playback starts")
print("stall = frame times the buffer survives with NO fill (seek/branch)")
print(" at 12fps, 1 frame = 83.3 ms")
+33 -19
View File
@@ -11,12 +11,12 @@ 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. PEAK/MEAN -- measured 1.4-1.9x (FINDINGS 18). The disk delivers a
SUSTAINED rate; a frame that overruns is a DROPPED frame.
Either size for the peak, or rate-control to the mean and
carry a bucket. We do the latter, so we need bucket depth
rather than peak headroom -- but until rate control is
actually wired in (it is not), size for the peak.
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.
@@ -49,16 +49,23 @@ CURVE = [
]
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, rate_controlled=False):
"""Largest-quality lam whose worst-case demand fits inside bw_kbps."""
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 = 1.0 if rate_controlled else peak_factor
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)
@@ -81,9 +88,9 @@ def main():
help="fraction of the pipe we allow ourselves (seeks, "
"container overhead, and the fact that the bandwidth "
"figure itself is folklore)")
ap.add_argument("--rate-controlled", action="store_true",
help="assume the leaky bucket absorbs peaks (NOT YET TRUE "
"-- ratectl.py is written but not wired into encode.py)")
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
@@ -91,26 +98,33 @@ def main():
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")
if not a.rate_controlled:
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"{(bw*a.margin-AUDIO_KBPS)/PEAK_OVER_MEAN:.0f} KB/s mean allowance"
f" [pessimistic, FINDINGS 18 -- superseded]")
else:
print(" peaks absorbed by rate control (bucket depth must be validated)")
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, rate_controlled=a.rate_controlled)
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["peak_kbps"])
print(f"\n -> {a.name}: lam={r['lam']}, {r['mean_kbps']:.0f} KB/s mean, "
f"{r['peak_kbps']:.0f} KB/s peak")
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__":