The 68000 decoder draws pixel-exact frames, and does not fit
src/player/decode.s parses DLX1 and decodes straight into GVRAM. Verified pixel-exact over a 120-frame sequential run of the worst sustained window on the disc -- all four block modes, full temporal recursion, so the last frame is only right if all 120 were. In check.sh. It costs a mean of 81.7% of a 12fps frame budget, and 31% of frames exceed 100% (42% at scsi). CPU is now the binding constraint. FINDINGS 28. Three things that were believed and are not true: - The dual-display-path plan of FINDINGS 24.5/25.6 is incoherent. The compose path needs a RAM copy of the previous reconstruction; the direct path's selling point is that it keeps none. Mixing them shows stale pixels on 70 of 120 frames, worst frame 18.8% of the screen. Every coherent repair is dearer than not mixing, and 24.5's two figures were both copies with no decode in either, so there was never a crossover to find. One path ships, and the 96KB reference frame is gone. tools/analysis/10_pathmix_drift.py keeps the counterexample runnable; check.sh asserts it still reproduces. - The four block modes do not cost the same. V1 300, V4 448, RAW 400 cycles against the old model's flat 207.8. V4 is 25% of blocks and 50% of the cycles, and the mode decision charges it bytes it does not charge cycles for. tools/analysis/11_cpu_budget.py reproduces all four frames timed on the 68000 to within 1 point. Hand-derived timings agree to 0.5% on V1. - The container is big-endian but not aligned. Variable-length records laid end to end put frame 1's length field at an odd address, and move.l (a0)+ there is an address error: frame 0 decoded perfectly and then vectored into the IPL for 59 emulated seconds looking like a hang. Found by dumping PC, not by reading the source. Also: an all-V1 frame, the cheapest possible full redraw, is 110.5% of budget. No mode assignment fits a scene cut at 12fps. That one needs a decision, not a measurement. Next: charge cycles in the mode decision and bisect against 833,333 per frame, the way session 6 bisects lam against bytes -- but with no bucket, because a late frame cannot be banked. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -862,6 +862,12 @@ leaving them uninitialised. This is why 96KB, not 48KB, is the irreducible
|
||||
write traffic.
|
||||
|
||||
### 24.5 The architecture question, and where it turns over
|
||||
> **Superseded by FINDINGS 28.1/28.2 (session 7).** The two-path plan below is
|
||||
> incoherent — the compose path needs a RAM reference the direct path never
|
||||
> writes — and its two costs are both *copies*, so they were never comparable to
|
||||
> a decode. The "76.6% x non-SKIP fraction" model is also 2.03x optimistic:
|
||||
> the four block modes cost 300/448/400 cycles, not one figure. One path ships.
|
||||
|
||||
V4 prices the access pattern a decoder that writes codewords **straight into
|
||||
GVRAM** actually has: 4 rows of 8 bytes at a 1024-byte stride per 4x4 block. The
|
||||
same 96KB of writes costs **76.6%** in block order versus 53.6% row-linear — the
|
||||
@@ -980,6 +986,11 @@ rate-control problem, not a codec-structure problem: the RD decision is behaving
|
||||
correctly for the lam it was given.
|
||||
|
||||
### 25.6 The decoder needs BOTH display paths, chosen per frame
|
||||
> **Superseded by FINDINGS 28.1 (session 7).** Mixing the paths displays stale
|
||||
> pixels on 70 of these 120 frames. The "median 37.0%, capped at 53.6%" below is
|
||||
> the cost of an incorrect player; every coherent version is dearer, and plain
|
||||
> direct-to-GVRAM is the cheapest of them.
|
||||
|
||||
Applying FINDINGS 24.5's crossover to the real per-frame distribution:
|
||||
|
||||
| | median non-SKIP | p90 | frames over the 70% crossover |
|
||||
@@ -1216,3 +1227,147 @@ bisection instead of a 5-rung ladder — which was the actual defect in 26.3.
|
||||
The cache holds **one frame**. At ~133 KB of intermediates per frame, caching
|
||||
the sequence would cost 900 MB on a 9.4-minute stream to save nothing: every
|
||||
caller works a frame at a time.
|
||||
|
||||
---
|
||||
|
||||
## 28. The 68000 decoder exists, is pixel-exact, and does not fit (session 7)
|
||||
|
||||
`src/player/decode.s` parses DLX1 and draws frames on the emulated X68000. It is
|
||||
**pixel-exact across a 120-frame sequential run** of the worst sustained window
|
||||
on the disc (`tools/bench/verify_decode.py`), exercising all four block modes
|
||||
and the full temporal recursion — the last frame is only right if every frame
|
||||
before it was.
|
||||
|
||||
It is also **too slow**. On that window, at the shipping `sasi` profile:
|
||||
|
||||
| | non-SKIP blocks | measured cost |
|
||||
|---|---:|---:|
|
||||
| cheapest frame | 15.4% | 31.5% of a 12fps frame |
|
||||
| median frame | 47.8% | 73.8% |
|
||||
| p90 frame | 82.5% | **116.4%** |
|
||||
| worst frame | 100.0% | **135.8%** |
|
||||
| mean over the window | 47.8% | **81.7%** |
|
||||
|
||||
**31% of frames miss the 833,333-cycle budget**, and like every figure since
|
||||
FINDINGS 24 these are instruction cycles against zero-wait-state memory, so
|
||||
they are a floor. This is the first time CPU, not disk, is the binding
|
||||
constraint — FINDINGS 21 retired the bandwidth worry, and this replaces it.
|
||||
|
||||
### 28.1 The dual-path plan of 24.5/25.6 was incoherent, and is withdrawn
|
||||
FINDINGS 24.5 specified two display paths chosen per frame on the non-SKIP
|
||||
count, and 25.6 costed the mix at "median 37.0%, capped at 53.6%". Two of its
|
||||
premises cannot both hold:
|
||||
|
||||
- compose-in-RAM-then-blit exists to make the blit **row-linear**, so it must
|
||||
assemble a **full** frame in RAM. The pixels it does not decode this frame —
|
||||
the SKIP blocks — can only come from a RAM copy of the previous
|
||||
reconstruction.
|
||||
- decode-direct-to-GVRAM's stated advantage is that **"no RAM reference frame
|
||||
is needed"**, because the previous frame is already in GVRAM.
|
||||
|
||||
So every direct frame silently invalidates the reference the next compose frame
|
||||
reads. Simulated on the Singe window at the crossover the plan specifies
|
||||
(`tools/analysis/10_pathmix_drift.py`): **70 of 120 frames display pixels no
|
||||
correct player would display**, first at frame 2, worst frame 18.8% of the
|
||||
screen. This is FINDINGS 26 in different clothing — two code paths disagreeing
|
||||
about what "the previous frame" means — and it is the **sixth** false premise
|
||||
this project has caught before it shipped.
|
||||
|
||||
Every coherent repair is worse than not mixing at all:
|
||||
|
||||
| strategy | median | p90 | max | correct |
|
||||
|---|---:|---:|---:|---|
|
||||
| mix per frame, as specified | 36.6% | 53.6% | 53.6% | **no** |
|
||||
| mix, direct also writes the RAM reference | 53.6% | 68.4% | 81.4% | yes |
|
||||
| mix, re-read GVRAM into RAM on each switch | 36.6% | 107.2% | 107.2% | yes, 13 frames miss |
|
||||
| compose only | 53.6% | 53.6% | 53.6% | yes |
|
||||
| **direct only** | **36.6%** | 62.5% | 76.6% | yes |
|
||||
|
||||
(Costs in that table are 24.5's own model, for like-for-like comparison; 28.2
|
||||
replaces the model itself.)
|
||||
|
||||
**24.5 also compared the wrong two things.** Its 53.6% and 76.6% are both
|
||||
*copies* measured in `blit.s` — neither includes decoding. A real compose path
|
||||
costs decode-into-RAM **plus** the 53.6% blit, so it is strictly dearer than
|
||||
decoding straight into GVRAM, whatever the block mix. There was never a
|
||||
crossover to find.
|
||||
|
||||
**The decoder therefore implements one path, direct-to-GVRAM**, and drops the
|
||||
96 KB RAM reference frame entirely.
|
||||
|
||||
### 28.2 The four block modes do not cost the same, and V4 is the expensive one
|
||||
24.5's model — "76.6% of a frame x the non-SKIP fraction" — prices every
|
||||
non-SKIP block as one `movem.l` burst. Measured separately, with synthetic
|
||||
single-mode frames (`tools/bench/prep_dlx.py`):
|
||||
|
||||
| mode | cycles/block | vs the 24.5 model (207.8) |
|
||||
|---|---:|---:|
|
||||
| SKIP, in an all-SKIP header byte | 13.3 | model says 0 |
|
||||
| SKIP, inside a mixed byte | ~45 | model says 0 |
|
||||
| V1 (one 4x4 codeword) | **299.9** | 1.44x |
|
||||
| V4 (four 2x2 codewords) | **448.2** | 2.16x |
|
||||
| RAW (16 literal indices) | **400.4** | 1.93x |
|
||||
|
||||
Applied to the real per-frame histograms (`tools/analysis/11_cpu_budget.py`),
|
||||
the model reproduces all four frames timed on the 68000 to within **1
|
||||
percentage point**, and shows 24.5 to be **2.03x optimistic at the median**.
|
||||
|
||||
Where the cycles actually go over the window:
|
||||
|
||||
| mode | % of blocks | % of cycles |
|
||||
|---|---:|---:|
|
||||
| SKIP | 46.4% | 9.2% |
|
||||
| V1 | 19.8% | 26.1% |
|
||||
| V4 | **25.2%** | **49.7%** |
|
||||
| RAW | 8.5% | 15.0% |
|
||||
|
||||
**V4 is a quarter of the blocks and half the cycles.** It costs 1.49x a V1 block
|
||||
while the mode decision in `vq_hybrid.py` charges it only its 4x payload bytes.
|
||||
The lagrangian trades distortion against *bytes*; on this machine it now has to
|
||||
trade distortion against *cycles* as well.
|
||||
|
||||
### 28.3 The container is big-endian but not aligned, and that is an address error
|
||||
The DLX1 header docstring says every multi-byte field is big-endian "so the
|
||||
68000 reads them with a plain `move`". Alignment is the other half of that
|
||||
sentence and the container does not have it: frame records are
|
||||
`[u32 length][768-byte mode header][payload]` laid end to end with arbitrary
|
||||
payload lengths, so record boundaries land on odd addresses.
|
||||
|
||||
`move.l (a0)+,d0` at an odd address is an **address error** on a 68000 — not a
|
||||
slow read. The first run decoded frame 0 perfectly, consumed exactly its 8,715
|
||||
payload bytes, then read frame 1's length at `$03220F` and vectored into the IPL
|
||||
at `$FF059A`, where it sat for 59 emulated seconds looking like an infinite
|
||||
loop. It was found by dumping PC and the address registers, not by reading the
|
||||
source: the code was correct, the data layout was not.
|
||||
|
||||
The decoder now rounds each record start up to 4. **The container should carry
|
||||
the padding itself** so a streaming player can DMA records into place: measured
|
||||
cost on this window is **199 bytes over 120 frames — 1.66 B/frame, 20 B/s**
|
||||
against a 110 KB/s budget. Until `encode.py` does it, `prep_dlx.py` realigns at
|
||||
load time.
|
||||
|
||||
### 28.4 The measurements agree with hand-derived MC68000 timings
|
||||
As in FINDINGS 24, each figure was derived from the instruction timing tables
|
||||
before being believed. A V1 block, summing dispatch, index decode, the indexed
|
||||
`movem.l` load and four `movem.l` stores, plus its quarter share of the header
|
||||
loop: **298.5 cycles derived against 299.9 measured — 0.5%.** RAW derives to
|
||||
396 against 400.4 measured (1%). V4 derives to 415 against 448 (7%, the gap
|
||||
being in the indexed two-register `movem.l`, the mode this decoder uses most
|
||||
heavily). So these are 68000 cycles, not a MAME artefact.
|
||||
|
||||
### 28.5 A full frame does not fit at 12fps in ANY mode
|
||||
An all-V1 frame — the cheapest possible way to redraw all 3,072 blocks — costs
|
||||
**921,187 cycles, 110.5% of the budget**. All-V4 is 165.2% and all-RAW 147.6%.
|
||||
|
||||
So the ceiling is structural, not a tuning problem: **at 12fps on a 10MHz 68000
|
||||
no more than ~88% of the screen can change in one frame**, however cheaply it is
|
||||
coded. Scene cuts change 100% of it. Either a cut gets one late frame (the
|
||||
outgoing content is unrelated, so this may be free to the eye), or cuts have to
|
||||
be spread across two frame times, or the framerate has to come down — at 10fps
|
||||
the budget is 1,000,000 cycles and an all-V1 frame fits.
|
||||
|
||||
### 28.6 What this does not measure
|
||||
One 10 s window of one stream at one profile, and MAME still models no GVRAM
|
||||
wait states. The `scsi` profile will be worse: FINDINGS 25.5 has it collapsing
|
||||
to RAW under stress, and RAW is 1.93x the old model's block. Nothing here has
|
||||
been run on `00020` or on quiet content, where the median frame is far cheaper.
|
||||
|
||||
Reference in New Issue
Block a user