Drop SASI on capacity, then find the budget never had the disk in it
USER DECISION: drop the `sasi` profile. Not on bandwidth -- on capacity. A SASI volume is 40 MB, and the 22.8 min of unique scene footage on the source Blu-ray (streams 00000-00201, measured, not recalled) is 146 MiB at the LOWEST rate this codec makes -- more than the machine's whole 4-unit SASI space. `scsi` is the only profile now. FINDINGS 32. Then the user asked whether we were drawing the wrong conclusions about PIO vs DMA, and we were, more broadly than the question implied. Every CPU figure in FINDINGS 24-34 is scored against the full 833,333 cycles/frame with nothing subtracted for moving the bitstream off disk. Debiting the HD63450 cycle-steal at the long-standing 8 clk/word ESTIMATE, "1 frame of 120 misses" becomes 84 of 120, median 112.4%. PIO at the span rate is 99.8% of the machine. Spans buy cycles by spending bandwidth and the bandwidth returns as steal, so 31.6's "fits completely" becomes a worst frame of 114.3%. 10 fps absorbs it: median 93.7%, 1/120. FINDINGS 35. `11_cpu_budget.py` takes --io dma|pio|none, defaults to dma, and warns if asked for none. Also landed: - item 1 done: the cost model checked against the 68000 on a cost-aware container, -3.07% to +0.01%, whole-window mean -1.22%. FINDINGS 34. - item 4 done: the container carries its own 4-byte record alignment (DLX2). 94/120 record starts were on odd addresses -- an address error, not a slow read -- now 0/120 for 16 B/s. Re-encoding reproduces 31.1 exactly. FINDINGS 33. - a `scsi` window does not fit the 2 MB machine the rig emulates (2.84 MB of stream past a 0x200000 ceiling). The gate now verifies 80 of 120 frames and SAYS so, and fails loudly when the pass does not complete, instead of reporting a phantom 49,005-pixel diff. FINDINGS 36. Three near-misses this session had one shape: an unobservable run nearly produced a false finding. stdbuf -oL on any MAME job that prints progress -- a file is block-buffered too, and a run that is merely finishing looks exactly like one that is wedged. check.sh ALL GREEN. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
+106
-75
@@ -1,93 +1,112 @@
|
||||
# Status & next-session handoff — end of session 8 (2026-08-23)
|
||||
# Status & next-session handoff — end of session 9 (2026-08-23)
|
||||
|
||||
## Where this stands
|
||||
|
||||
Session 7 left the decoder pixel-exact and **31% of frames over the CPU budget**
|
||||
at `sasi`, 42% at `scsi` (FINDINGS 28), with two levers proposed and neither
|
||||
measured. Session 8 did both.
|
||||
Session 9 did three things: dropped a profile on the user's instruction, closed
|
||||
the last encoder gap, validated the cost model against the machine — and then
|
||||
found that **the CPU budget every one of those numbers was scored against has
|
||||
never had the disk in it**.
|
||||
|
||||
**Lever A, spans: measured.** A row-linear span of word-expanded literals costs
|
||||
**43.7 cycles per span + 9.152 per pixel** — but only in an encoder-assisted
|
||||
format, `{u32 absolute GVRAM address, u16 jump displacement}` into an unrolled
|
||||
copy chain. The obvious decoder, handed `(x, npix)`, is 97.9 + 10.46.
|
||||
FINDINGS 30, `tools/bench/span.sh` (~25 s).
|
||||
**FINDINGS 35 is the headline, and it is bad news.** Raised by the user ("PIO is
|
||||
such a CPU killer. DMA is not. I'm concerned about us drawing the wrong
|
||||
conclusions"). Every CPU figure in FINDINGS 24-34 is measured against the full
|
||||
833,333 cycles/frame, with nothing subtracted for moving the bitstream off SCSI.
|
||||
Debiting the HD63450 cycle-steal at the long-standing 8 clocks/word estimate:
|
||||
|
||||
**Lever B, the cost-aware mode decision: implemented, measured, and ON by
|
||||
default.** `decide()` minimises `D + lam*bytes + mu*cycles`; `mu` is bisected
|
||||
per frame against 833,333 cycles with the `lam` bisection nested inside it.
|
||||
FINDINGS 31, `tools/analysis/13_cpu_ratectl.py`.
|
||||
| `scsi` container | budget left | median frame | worst | frames missing |
|
||||
|---|---:|---:|---:|---:|
|
||||
| no I/O — the pre-session-9 premise | 833,333 | 99.6% | 110.6% | **1/120** |
|
||||
| **DMA** (8 clk/word, ESTIMATED) | 738,234 | **112.4%** | 124.8% | **84/120** |
|
||||
| PIO (12 clk/B, hand-derived floor) | 548,036 | 151.4% | 168.1% | **120/120** |
|
||||
|
||||
| `sasi`, worst sustained window | PSNR | KB/s | CPU median | CPU max | missing |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| bytes only (session 7) | 27.22 dB | 109.5 | 74.4% | 136.2% | **37/120** |
|
||||
| + cycle ceiling (now the default) | 26.95 dB | 109.4 | 81.5% | 110.6% | **1/120** |
|
||||
| + ceiling + spans (MODEL, nothing implements it) | — | 449.3 | 56.8% | 91.5% | **0/120** |
|
||||
So "1 frame of 120 misses" was against the wrong budget. `11_cpu_budget.py` now
|
||||
takes `--io dma|pio|none`, defaults to `dma`, and warns if asked for `none`.
|
||||
|
||||
`scsi`: 51/120 -> 1/120 for 0.62 dB. The one remaining miss at either profile is
|
||||
**frame 0**, which has no previous reconstruction and so is 100% changed by
|
||||
definition — the same case as a scene cut. It is emitted late on purpose.
|
||||
**The way out is 10 fps, and it works on paper**: same container, DMA debited,
|
||||
10 fps -> median 93.7%, worst 104.0%, **1/120** — and that is conservative,
|
||||
because it holds the 12 fps byte rate. FINDINGS 35.5.
|
||||
|
||||
The cost-aware container is verified pixel-exact on the 68000 (120 frames,
|
||||
`tools/bench/verify_decode.py`).
|
||||
**SASI is dropped (USER DECISION).** A SASI volume is 40 MB and the game's 22.8
|
||||
minutes of unique footage is 146 MiB at the *lowest* rate this codec makes.
|
||||
`scsi` is the only profile. Delivery is SD-backed SCSI, as locked in session 2 —
|
||||
capacity does not bind there, and it is what rules CD-ROM out. FINDINGS 32.
|
||||
|
||||
**The encoder gap is closed.** `encode.py` emits **DLX2**, 4-byte-aligned frame
|
||||
records: 94/120 record starts were on odd addresses, now 0/120, for 16 B/s.
|
||||
Re-encoding reproduces FINDINGS 31.1 exactly. FINDINGS 33.
|
||||
|
||||
**The cost model is validated on a cost-aware container** (STATUS item 1, done):
|
||||
errors -3.07% to +0.01% against the 68000, whole-window mean -1.22%. It
|
||||
under-predicts light frames and is exact on heavy ones. FINDINGS 34.
|
||||
|
||||
**A `scsi` window does not fit the 2 MB machine the rig emulates** — 2.84 MB of
|
||||
stream against a 0x200000 ceiling. The decoder gate now verifies 80 of 120
|
||||
frames and says so, rather than failing as a phantom pixel diff. FINDINGS 36.
|
||||
|
||||
## NEXT SESSION, in order
|
||||
|
||||
0. **Green light first.** `./tools/bench/check.sh` (~3 min, Blu-ray mounted).
|
||||
The drift stage now runs BOTH controllers; both must report 0/120.
|
||||
0. **Green light first.** `./tools/bench/check.sh` (~5 min, Blu-ray mounted).
|
||||
Verified green at end of session 9. The gate container is now
|
||||
`tmp/rc_fr_singe_scsi_cpufit.dlx`, and it decodes **80 of 120 frames**, not
|
||||
120: the rig loads the whole stream into a 2 MB machine and a `scsi` window
|
||||
is 2.84 MB, which overran the top of RAM by 940 KB and produced a failure
|
||||
that looked exactly like a decoder bug. `prep_dlx.py` now truncates to what
|
||||
fits and says so; `verify_decode.py --nframes` replays the same prefix; and
|
||||
the stage fails loudly if the sequential pass did not complete. FINDINGS 36.
|
||||
|
||||
1. **Time the cost-aware container on the 68000.** Everything in FINDINGS 31 is
|
||||
the validated cost MODEL (`vq_hybrid.cycles`, within 1 point of the machine
|
||||
on four frames of the session-7 container), not a measurement of this one.
|
||||
The full timing pass was started and killed for time:
|
||||
```
|
||||
python3 tools/bench/prep_dlx.py tmp/rc_fr_singe_sasi_cpufit.dlx
|
||||
tools/vasm/vasmm68k_mot -Fbin -o tmp/decode.bin src/player/decode.s
|
||||
( cd tmp && SDL_VIDEODRIVER=dummy mame x68000 -bios ipl10 -ramsize 2M \
|
||||
-video soft -window -sound none -nothrottle -plugins \
|
||||
-autoboot_script ../tools/bench/decode.lua -snapshot_directory ./snap_decode \
|
||||
-snapview native -seconds_to_run 300 > decode_cpufit.log 2>&1 )
|
||||
```
|
||||
**Budget real time for it: the run was still going at 12 minutes of CPU.**
|
||||
MAME's stdout is block-buffered to a file, so there is no progress to watch
|
||||
— wait on the PID, never on a `pgrep -f` match (see the shell traps below).
|
||||
Confirm the four anchors against `11_cpu_budget.py` on the same container,
|
||||
and update FINDINGS 31 with measured-vs-model errors.
|
||||
At 278.6 KB/s a 2 MB machine holds ~6.7 s of stream, so **the strongest test
|
||||
in the tree can only ever audit a prefix of a window.** Getting the coverage
|
||||
back means gating on more than one window, not one longer pass.
|
||||
|
||||
2. **Put spans in the bitstream.** This is the big one and it is now fully
|
||||
specified by measurement: format in FINDINGS 30.2, costs in 30.5, and the
|
||||
scene-cut arithmetic in 30.6. It touches `encode.py` (a fifth mode and a
|
||||
run-aware decision), `dlx.py` (the reference decoder), and `decode.s`. The
|
||||
24-pixel quantisation and the free row overrun are part of the format, not
|
||||
optimisations to add later. Order it AFTER item 1 so the model that prices it
|
||||
has been checked against the machine once more.
|
||||
1. **Measure the DMA cycle-steal, or derive it from primary sources.** This is
|
||||
now the most load-bearing unmeasured number in the project: at 8 clocks/word
|
||||
the port is marginal, at 12 it is dead, at 4 it is comfortable. It has been
|
||||
an estimate from a session-2 datasheet reading since FINDINGS 5. MAME cannot
|
||||
answer it (functional models, not transfer-timing accurate) — this needs the
|
||||
HD63450 and MB89352 datasheets or real hardware. **Everything below is
|
||||
contingent on it.**
|
||||
|
||||
3. **The three open items of FINDINGS 29.5/30.7**, now load-bearing because a
|
||||
span design runs at ~449 KB/s of a 488 KB/s pipe: re-run the ring-buffer
|
||||
simulation at that rate (FINDINGS 21 was established at 110 and 280), confirm
|
||||
the provenance of the user's 4 Mbps figure, and **confirm DMA rather than
|
||||
PIO** — a PIO fallback puts a 449 KB/s transfer back on the CPU the whole
|
||||
lever exists to relieve. The DMA check is the cheapest of the three and the
|
||||
most consequential.
|
||||
2. **Make sure the player actually gets DMA.** DMA-vs-PIO is a property of our
|
||||
code, not the board: the CZ-6BS1's DMA path is real and modelled
|
||||
(FINDINGS 32.4), but if the player reads through IOCS and IOCS does PIO we
|
||||
get the 120/120 row. `docs/BENCHMARK.md` item 4 (drive the MB89352 directly)
|
||||
is no longer an optimisation. Benchmark `x68000 -exp1 cz6bs1`, **never
|
||||
`x68ksupr`** — MAME's internal SCSI has no DMA glue at all (`// TODO:
|
||||
duplicate DMA glue from CZ-6BS1`) and would measure a PIO fallback the real
|
||||
machine does not have.
|
||||
|
||||
4. **Encoder gap, still open from session 7:** `encode.py` should pad frame
|
||||
records to 4 bytes. Frame boundaries land on odd addresses and a 68000 takes
|
||||
an address error, not a slow read (FINDINGS 28.3). `prep_dlx.py` pads at load
|
||||
time, which is why the decoder works; the container itself does not.
|
||||
Measured cost of fixing it: 1.5 B/frame = 18 B/s.
|
||||
3. **Re-decide the framerate, now that it is the lever that pays for the disk.**
|
||||
10 fps absorbs the DMA steal on current estimates. This was item 5 and a
|
||||
quality question; it is now arithmetic. It is still the user's call.
|
||||
|
||||
5. **A quality-vs-framerate question that is the user's, not the encoder's.**
|
||||
Every miss is now one frame per cut. The options remain: one late frame at
|
||||
each cut (the outgoing content is unrelated, so it may be invisible), a cut
|
||||
spread over two frame times, or 10fps. Spans (item 2) make the question go
|
||||
away if they land as modelled.
|
||||
4. **Then spans.** Fully specified by measurement (format FINDINGS 30.2, costs
|
||||
30.5, scene-cut arithmetic 30.6). Note what 35.2 does to the case for them:
|
||||
spans buy cycles by spending bandwidth, and the bandwidth comes back as
|
||||
steal, so 31.6's "fits completely" becomes a worst frame of 114.3%. They are
|
||||
still worth 14 points at the median — they are just no longer sufficient
|
||||
alone.
|
||||
|
||||
5. **Re-run the ring-buffer simulation at the surviving rate** and confirm the
|
||||
488 KB/s figure's provenance (FINDINGS 29.5/30.7, still open).
|
||||
|
||||
**Do not start by hand-optimising `decode.s`.** The hand-derived timings agree
|
||||
with the measurements to 0.5% on V1 and 1% on RAW (FINDINGS 28.4), so the inner
|
||||
loop is close to what the instruction set allows.
|
||||
with the measurements to 0.5% on V1 and 1% on RAW (FINDINGS 28.4), and FINDINGS
|
||||
34 confirms the model on a second container. The inner loop is close to what the
|
||||
instruction set allows; the cycles to be won are in the budget, not the loop.
|
||||
|
||||
**Three of this session's near-misses had the same shape: an unobservable run
|
||||
almost produced a false finding.** The 23-minute "hang" (FINDINGS 34.1), the
|
||||
RAM overrun that looked like a 4x-slow decoder (FINDINGS 36), and the truncated
|
||||
gate that reported 49,005 differing pixels. In each case the instrument was
|
||||
broken, not the thing being measured. **Always `stdbuf -oL` a MAME job that
|
||||
prints progress.** A file is
|
||||
block-buffered too, so a long run is unobservable until it exits — and an
|
||||
unobservable run that is merely finishing looks exactly like one that is wedged.
|
||||
Session 8 lost this measurement to that; session 9 lost 23 minutes to it before
|
||||
spending 25 seconds getting the answer with `stdbuf`. FINDINGS 34.1.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## What session 8 settled
|
||||
|
||||
0. **The mode decision can see cycles, it is on by default, and it costs
|
||||
@@ -245,22 +264,34 @@ The two session-7 stages are worth knowing the shape of before they fail on you:
|
||||
| Display mode | 256 colors, 256x192 in 256x256 CRTC mode | every mode is 1 word-access/pixel, so 256c is free vs 16c |
|
||||
| Double buffer | **none** — page 1 sacrificed | enables `movem.l` 24px bursts; delta coding needs a RAM reference frame anyway |
|
||||
| **Codec** | **hybrid VQ: SKIP / V1 4x4 / V4 four-2x2 / RAW, per-block rate-distortion** | flat 4x4 VQ was measured and rejected — see FINDINGS 9-10 |
|
||||
| **Quality modes** | **two: `sasi` and `scsi`** (USER DECISION, session 2) | one codec, one decoder, one bitstream; only `lam` differs |
|
||||
| **Quality modes** | **one: `scsi`** (USER DECISION, session 9 — `sasi` dropped) | a SASI volume is 40 MB and the game is 146 MiB at the LOWEST rate this codec makes. FINDINGS 32 |
|
||||
| Delivery medium | **SD-backed SCSI** (BlueSCSI / SCSI2SD), as locked in session 2 | capacity does not bind on SD at any rate this codec makes; it is what killed SASI, and it is what rules CD-ROM OUT (650 MiB with spans, and 1x CD is ~150 KB/s). FINDINGS 32.3 |
|
||||
| Profile axis | **I/O bandwidth only** | the profiles say nothing about CPU; **both target the same stock 10MHz 68000**, and the Super has SCSI at 10MHz. FINDINGS 28.7 |
|
||||
| Framerate | 12 fps, **explicit decimation** | source has zero duplicate frames; no free "twos" win |
|
||||
| Emulator | MAME 0.277 x68000 | accurate enough that measured cycles mean something |
|
||||
| SNES project reuse | **MIT — cleared** | `data/events/` scene graph is reusable with attribution |
|
||||
|
||||
### The SASI/SCSI question is RESOLVED
|
||||
Session 1 left "which machine do we target" open. The user's answer: **ship both**,
|
||||
as two quality profiles. This is now implemented rather than hypothetical — the
|
||||
bitrate ceiling is a build parameter in `tools/encoder/ratectl.py`:
|
||||
### The SASI/SCSI question is RESOLVED — and in session 9 SASI was DROPPED
|
||||
Session 1 left "which machine do we target" open. Session 2's answer was **ship
|
||||
both**, as two quality profiles. **Session 9 retired `sasi`** (USER DECISION) on
|
||||
CAPACITY, not bandwidth: a SASI volume is limited to 40 MB, and the 22.8 minutes
|
||||
of unique scene footage on the source Blu-ray is **146 MiB even at 110 KB/s** —
|
||||
more than the machine's whole 4-unit SASI address space. FINDINGS 32.
|
||||
|
||||
Everything below this line about *two* profiles is the session-2..8 record, kept
|
||||
because every measurement in FINDINGS 27-31 was taken against it. One profile
|
||||
ships:
|
||||
|
||||
| profile | target | lam | quality (00020 / 00146) | machine |
|
||||
|---|---|---|---|---|
|
||||
| `sasi` | 110 KB/s | 60 (floor) | 36.9 / 29.6 dB | stock 10MHz ACE/EXPERT |
|
||||
| ~~`sasi`~~ | ~~110 KB/s~~ | ~~60~~ | ~~36.9 / 29.6 dB~~ | **RETIRED session 9** |
|
||||
| `scsi` | 280 KB/s | 10 (floor) | 39.4 / 32.3 dB | Super/XVI, or CZ-6BS1 board |
|
||||
|
||||
The 110 KB/s **rate point** is not necessarily gone with the interface: a 1x
|
||||
CD-ROM sustains ~150 KB/s and CD-ROM is the only period medium with room for the
|
||||
whole game (374 MiB at `scsi`, 600 MiB with spans). The user's call was to ship
|
||||
one profile now and settle the medium when the pipe is measured. FINDINGS 32.3.
|
||||
|
||||
**That "machine" column is about the BUS, not the CPU.** The profiles are an
|
||||
I/O-bandwidth axis and say nothing about clock speed: the X68000 Super has
|
||||
built-in SCSI at 10 MHz (`x68k.cpp:1194`, `40_MHz_XTAL/4`, same as the base
|
||||
|
||||
Reference in New Issue
Block a user