Align the container to the disc, and find the decoder-free packed player fits
Two sessions, unrecorded until now, committed together because their edits share files and cannot be split cleanly after the fact. Session 28 (FINDINGS 60): the container is DLX5 -- every record sector-aligned, 120/120 starting on a boundary where 3/120 did, +0.48% on the wire and zero clocks -- and the ring's release rounds to RECALN so no pad is stranded. Two encoder levers measured and refused: `--spans all` buys +0.19 dB for +67% of the wire, and joint span/lam selection emits byte-identical containers because `lam` never leaves its floor on any of 120 frames. Session 29 (FINDINGS 61): the packed full-frame blit is 27.3% of a 12 fps frame, a channel fills GVRAM in buffer mode off the disc with the CPU halted, and it walks the 1,024 B line stride itself through array chaining. At the 9 clk/B dual-address floor the codec is 110.4% of a frame and a decoder-free packed literal player is 55.2%, at +4.89 dB -- 2.75 dB past a ceiling the codec's scene-wide palette cannot cross. Encoder work is parked; the codec is kept and not built on. check.sh is ALL GREEN before and after, plus one new stage that gates the ORDER of the measured paint costs rather than their values. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -5803,3 +5803,510 @@ cross-check at the top of the tool is what licenses every number under it.
|
||||
**Re-deriving 59.7 against a lighter container needs that harness re-run first**,
|
||||
and until it is, "34% too big" is a statement about the heaviest container and
|
||||
not about the project.
|
||||
|
||||
---
|
||||
|
||||
## 60. The re-encode bundle: the container agrees with the medium, and two encoder levers turn out not to be levers (session 28)
|
||||
|
||||
**Emulated** — MAME 0.277, `x68000 -exp1 cz6bs1 -ramsize 2M/6M`, plus px68k's
|
||||
C68K core in `tools/bench/c68k`. **No real hardware ran.** `./tools/bench/
|
||||
check.sh` was ALL GREEN before this session and is ALL GREEN after it, including
|
||||
a full re-encode of the gate container.
|
||||
|
||||
ROADMAP's re-encode bundle was four items collected under P2 because they shared
|
||||
one re-measurement. All four are now closed, and **two of them close as
|
||||
negatives** — which is the more useful half of the session.
|
||||
|
||||
### 60.1 DLX5: the container is laid out in sectors, and the disc now moves exactly the records
|
||||
|
||||
`tools/encoder/encode.py` pads every record up to **512 bytes** instead of 4,
|
||||
and pads the scene header so the frame stream itself starts on a sector
|
||||
boundary. `tools/encoder/dlx.py` reads it as `DLX5`; `rec_align` is a property
|
||||
of the container version and `record_lengths()` is the one place the rule is
|
||||
applied, replacing four hand-copied `4 + n + (-(4+n) % 4)` expressions in the
|
||||
analysis tools.
|
||||
|
||||
**Why 4 was not enough** is 58.3 option C and 59.4: a SCSI target answers in
|
||||
512 B blocks, 117 of 120 DLX4 records started part way into one, and
|
||||
`sc_in_data` **refuses** a windowed read when the data phase belongs to the
|
||||
DMAC, because a channel writes a contiguous run and cannot drop the 300 bytes in
|
||||
front of a record. Windowed PIO absorbed that for free and does not survive the
|
||||
move to the channel.
|
||||
|
||||
| on the gate container | DLX4 | **DLX5** |
|
||||
|---|---:|---:|
|
||||
| records starting on a sector boundary | 3/120 | **120/120** |
|
||||
| bytes off the disc, for 4,488,577 B of record | 4,548,608 (+1.34%) | **4,510,208 (+0.48%)** |
|
||||
| bytes into the ring | 4,488,588 | **4,510,208** |
|
||||
| clocks of window or bounce copy | 0 (PIO only) / +5 clk/B (DMA) | **0, and a channel can run it** |
|
||||
| largest record | 40,984 B | 41,472 B — a 256 KB ring still holds it 6 times |
|
||||
|
||||
**The disc figure and the ring figure are now the same number, and check.sh
|
||||
gates on that identity** rather than on two constants. Both are read out of the
|
||||
container at check time: the old gate had 4,488,588 and 4,548,608 written into
|
||||
it as literals, and the re-encode went red on both — correctly, because the
|
||||
container had changed and the expectation had not. **A gate whose expected value
|
||||
is a literal tests the literal.**
|
||||
|
||||
### 60.2 The consumer had to be told, and the failure was a byte-exact wrong answer
|
||||
|
||||
`stream.s` released the ring up to the last byte it *read*, rounded to 4. Under
|
||||
DLX5 that strands up to 511 bytes of pad per record, and the ring's own audit
|
||||
caught it on frame 0: `RD_PTR MISMATCH: decoder released 00040F08, record ends
|
||||
00041000`. The release now rounds to `RECALN` (geom.i), which is the record
|
||||
alignment the container guarantees, and is correct only because the ring base is
|
||||
`RECALN`-aligned too — stated where the constant is defined rather than assumed.
|
||||
|
||||
**This is the shape of the whole item.** Sector alignment is not a tidier
|
||||
version of 4-byte alignment; it is a contract with *two* sides, and the producer
|
||||
side alone would have drifted the free-space arithmetic by up to 511 B a frame
|
||||
with every frame still decoding pixel-exact.
|
||||
|
||||
### 60.3 Reserved black at index 0 — the letterbox is true black, and it costs 0.04 dB
|
||||
|
||||
23.4, open since session 5, was the other half of P2. `VQ.scene_palette` now
|
||||
quantises the picture into **255 entries and reserves index 0 as (0,0,0)**;
|
||||
`dlxload.pack_palette` gives it `I = 0` by its own minimum-squared-error rule
|
||||
with no special case, so 23.3's "the bars sit at RGB (4,4,4)" goes away for
|
||||
free. GVRAM cleared to zero displays entry 0, and a free mediancut palette put a
|
||||
real image colour there — on 00020 f0001, `(206,192,176)`, used by 210 image
|
||||
pixels.
|
||||
|
||||
Black is **reserved, not withheld**: the mapper may still spend index 0 on
|
||||
genuinely black pixels. What the reservation buys is that index 0 is black
|
||||
whatever the scene contains, which is what the letterbox needs and what a free
|
||||
palette cannot promise. Measured cost on the Singe window: **28.92 dB → 28.88 dB
|
||||
at `--spans need`**, and the palette ceiling is unmoved at 31.32 dB.
|
||||
|
||||
### 60.4 `--spans all` as the default: MEASURED, and the recommendation is REFUSED
|
||||
|
||||
E2 has been "a recommendation, not a measurement" since 43.6.1. It is now a
|
||||
measurement, on the DLX5 container, 120 frames, the gate recipe
|
||||
(`--kbps 280 --span-kbps 488`):
|
||||
|
||||
| | KB/s incl. audio | PSNR | frames that miss the 12 fps deadline |
|
||||
|---|---:|---:|---:|
|
||||
| `--spans need` (shipped default) | **267.9** | 28.88 | 2/120 |
|
||||
| `--spans all` | 448.2 | 29.07 | 1/120 |
|
||||
|
||||
**+67% of the wire, for +0.19 dB and one frame of 120.** Session 27 established
|
||||
that the frame affords 6.69 clocks a byte and that a dual-address byte costs 9,
|
||||
so the container's problem is that it is too big; spending 180 KB/s to buy a
|
||||
fifth of a decibel is the wrong direction, and `need` stays the default.
|
||||
|
||||
**The item is closed, and the answer is no.** The gate container keeps
|
||||
`--spans all` — it is a fixture chosen to exercise the newest path in the
|
||||
decoder, not a shipping recipe, and check.sh says so.
|
||||
|
||||
### 60.5 Joint span/lam selection: implemented, measured, and it is a NO-OP for a reason
|
||||
|
||||
E3 (39.3 item 5) asked for span selection to be re-derived jointly with `lam`
|
||||
instead of greedily after it. The argument is sound on paper: the span pass
|
||||
*removes* the block payload of every block it covers, so the frame lands under
|
||||
its byte allowance and the blocks that were **not** spanned were priced at a
|
||||
`lam` chosen as if those bytes were still needed.
|
||||
|
||||
`ratectl._refit_joint` (`--joint-spans`) hands the freed bytes back to the lam
|
||||
search and re-spans, to a fixed point or two rounds. A round is kept only if the
|
||||
frame still fits both ceilings it was already fitting, so `lam` can only fall
|
||||
and the un-spanned blocks can only improve.
|
||||
|
||||
**It emits byte-identical containers.** All four cells of `{need, all} x {greedy,
|
||||
joint}` produce two md5s, not four — and so does `--rc-floor open`:
|
||||
|
||||
| | lam, 120 frames | container |
|
||||
|---|---|---|
|
||||
| `need` greedy / joint | 10.0 min, median, p90, max | **identical** |
|
||||
| `all` greedy / joint | 10.0 min, median, p90, max | **identical** |
|
||||
| `all --rc-floor open` greedy / joint | 1.0 min, median, p90, max | **identical** |
|
||||
|
||||
**The reason is 44.3, and it is structural rather than incidental: `lam` never
|
||||
leaves its floor on any frame, at either floor the encoder offers.** The block
|
||||
coder at the profile floor already lands under the per-frame byte budget, so
|
||||
there is nothing for a joint re-search to spend the freed bytes on. E3 is not a
|
||||
lever, and it is not a lever for the same reason `--kbps` and the bucket are
|
||||
not: **the byte side of this encoder is governed by the span pass and by `mu`.**
|
||||
|
||||
The code stays, defaulted off, because the measurement is the finding and a
|
||||
future container that moved `lam` off its floor would make the question live
|
||||
again. That is 44's pattern kept deliberately: ask whether the lever is loaded
|
||||
before pulling it.
|
||||
|
||||
### 60.6 The apparatus lied, silently, and only a byte comparison caught it
|
||||
|
||||
**MAME 0.277 served a compressed CHD's own file bytes as sector data.** On the
|
||||
DLX5 volume, every `READ(10)` reported success and returned the wrong bytes: the
|
||||
destination buffer after a 4,096 B read at LBA 0 was **byte-for-byte the first
|
||||
4,096 bytes of `dlxdisk.chd`, starting `MComprHD`** — the CHD file header — while
|
||||
`chdman verify` reported both SHA1s correct.
|
||||
|
||||
Isolated by experiment, and the trigger is the image's **content**:
|
||||
|
||||
| volume | compression | result |
|
||||
|---|---|---|
|
||||
| DLX4 gate, 8,768 sectors | default (lzma/zlib/huff/flac) | **byte-exact** |
|
||||
| DLX5 gate, 8,809 sectors | default | CHD header served as LBA 0 |
|
||||
| DLX5 gate, padded to 9,072 sectors, 16x63 geometry | default | same failure |
|
||||
| DLX5 gate, **truncated to 8,768 sectors** — the working length | default | same failure |
|
||||
| DLX5 gate | `-c zlib` alone | same failure |
|
||||
| DLX5 gate | **`-c none`** | **byte-exact** |
|
||||
|
||||
So it is not size, not geometry and not the codec; compression decides it and
|
||||
uncompressed is sound. **The MAME-side cause is not diagnosed** and is not
|
||||
claimed here. `tools/bench/mkvol.sh` now builds the volume `-c none`, at 4.5 MB
|
||||
in `tmp/` against 1.6 MB, with the measurement written next to the flag.
|
||||
|
||||
**What is worth keeping is not the workaround.** The failure was *silent at the
|
||||
transport layer* — the SPC handshake completed, the phase sequence was correct,
|
||||
the byte count was right, and `scsi_read` returned 0. The only thing in the tree
|
||||
that could see it was `tools/bench/scsi.lua` comparing every delivered byte
|
||||
against the host's copy of the same image. A transport gate that checked status
|
||||
and length would have been green all session, and FINDINGS 58's "byte-exact
|
||||
against the host's copy" would have been the load-bearing phrase nobody noticed.
|
||||
|
||||
### 60.7 What the bundle did to the budget: almost nothing, which is the result
|
||||
|
||||
The re-measurement the four items shared, on the rebuilt gate container, with
|
||||
`tmp/c68k_frames.csv` regenerated by the same `check.sh` run:
|
||||
|
||||
| | before (DLX4, session 27) | **after (DLX5, session 28)** |
|
||||
|---|---:|---:|
|
||||
| decoder, MEASURED on C68K | 68.5% of the frame | **68.6%** |
|
||||
| delivered bytes a frame | 37,403 | **37,585** (the pad is delivered, so it is charged) |
|
||||
| headroom after decode + best-case audio | 6.74 clk/B | **6.69 clk/B** |
|
||||
| single address held, `W`=5, total | 92.2% | **92.4%** |
|
||||
| dual address held, `W`=9, total | 110.2% | **110.4%** |
|
||||
| E7's byte target at the 9 clk/B floor | 328 KB/s, 34% too big | **327 KB/s, 35% too big** |
|
||||
| B1's zero-prefill delivery requirement | 451.4 KB/s | **453.6 KB/s** |
|
||||
| scene header (P1/53.5, 22_scene_load) | 6,164 B | **6,656 B** |
|
||||
|
||||
**Every conclusion in 59.7 survives unchanged**, which is what a precondition is
|
||||
supposed to do: it removes an obstacle without moving the arithmetic. The two
|
||||
numbers that did move are both the alignment pad being paid honestly —
|
||||
`15_bus_occupancy.py` now charges the **padded** record rather than the payload,
|
||||
because the disc carries the pad whether or not a frame contains it.
|
||||
|
||||
### 60.8 Two caches that would have tested the wrong artefact
|
||||
|
||||
`H.build` acquired an option this session (`reserve_black`), and two tools cache
|
||||
its output in a pickle **keyed on the frames directory alone** —
|
||||
`16_span_roundtrip.py`, which is the span container's round-trip gate, and
|
||||
`13_cpu_ratectl.py`. A stale pickle would have let the gate round-trip a
|
||||
container built from the old palette while the shipping encoder emitted the new
|
||||
one: green, and testing an artefact that no longer exists. Both now store the
|
||||
build parameters with the model and rebuild on a mismatch.
|
||||
|
||||
### 60.9 What is now true, and what P4a still owes
|
||||
|
||||
**M2's re-encode bundle is done and the container is what the DMA path needs.**
|
||||
`dma_run.sh` still gates on the *refusal* — `WINDOWED DMA READ REFUSED, as it
|
||||
must be` — which is now a negative control rather than a description of the
|
||||
container, because the container no longer asks for a windowed read.
|
||||
|
||||
What is left of P4a is the wiring: putting the channel behind `ring.i`'s `XF_*`
|
||||
mailbox in place of the PIO loop in `xfer.i`, which 59.4 blocked on exactly this
|
||||
precondition. `xfer.i`'s sector arithmetic already degenerates correctly —
|
||||
`SC_WSKIP` is 0 and `SC_WKEEP` is the whole record on every record — so what
|
||||
changes is which loop moves the bytes, not what is asked for.
|
||||
|
||||
## 61. The decoder-free packed player fits the clocks the codec misses, and the codec cannot have the packing (session 29)
|
||||
|
||||
**Emulated.** MAME 0.277, `x68000 -bios ipl10`, and for the transport runs
|
||||
`-exp1 cz6bs1` with a zero-filled `scsiexrom.bin`. No real hardware ran, and
|
||||
47.4 — does buffer mode blank the layer — still wants a board.
|
||||
|
||||
FINDINGS 44.7 asked what a player with no codec at all would cost and answered
|
||||
"it fits the clocks and dies on the medium". 46.5/47.1 found the off switch for
|
||||
the 2:1 GVRAM tax, 47.2 built the packed layout and rendered it pixel-exactly on
|
||||
both emulators, and **47.5 re-derived the budget on a cost model that has since
|
||||
been replaced in full** — the transport was an unmeasured `c` when it was
|
||||
written, and sessions 25b–28 measured it (58.2), bounded it (59.2) and priced
|
||||
what a frame can afford against it (59.7/60.7). 47.6.1 filed the packed paint's
|
||||
`movem` shape as an *assumption*; 47.6.2 said the DMAC "has not been near this";
|
||||
47.6.4 left the codec's own survival under the layout untouched.
|
||||
|
||||
All four are answered here. `tools/analysis/29_packed_player.py` is the
|
||||
arithmetic, `tools/bench/blit.s` V8/V9/V10 are the paint measurements, and
|
||||
`src/player/dmagate.s` runs 4–6 are the transport ones.
|
||||
|
||||
### 61.1 The packed paint, MEASURED: it costs what the unpacked path pays to WRITE
|
||||
|
||||
`blit.s` V8 is V1 with one thing different — a row is 128 words instead of 256,
|
||||
because R20 bit 11 lets a word carry two picture bytes — and `blit.lua` times it
|
||||
in the same run as V1, V2 and V3, so it is quoted against numbers that have not
|
||||
moved since session 9.
|
||||
|
||||
| variant | clk/frame | % of a 12 fps frame | what it moves |
|
||||
|---|---:|---:|---|
|
||||
| V1 unpacked `movem` blit | 446,286 | 53.6% | 96 KB read + 96 KB write |
|
||||
| V2 byte-source expansion | 1,284,174 | 154.1% | 48 KB read + 96 KB write |
|
||||
| V3 unpacked WRITE-ONLY floor | 225,789 | 27.1% | no source read at all |
|
||||
| **V8 PACKED `movem` blit** | **227,553** | **27.3%** | **48 KB read + 48 KB write** |
|
||||
|
||||
All four put the same 49,152 pixels on screen. **V8 is 51.0% of V1 and 100.8% of
|
||||
V3: the packed blit costs what the unpacked one pays to write alone, with its
|
||||
source read thrown in free.** It is not exactly half of V1 because the 192-row
|
||||
loop does not halve with the words — per word V1 is 9.080 clocks and V8 is
|
||||
9.259, and the difference is the row loop amortised over half as many bursts.
|
||||
|
||||
V2 is worth keeping in view: it is the "send 1 byte a pixel and let the CPU
|
||||
expand it" trade, and at 154.1% it is not a trade, which is why the unpacked
|
||||
path has no cheap wire.
|
||||
|
||||
V8 is a **timing** variant and does not set bit 11. MAME's `gvram_w` carries no
|
||||
timing in either arm, so the bit cannot move a cycle; what it moves is the
|
||||
picture, and the picture is what `show_frame256_packed.lua` and `gvpack` already
|
||||
verify pixel-exactly (47.2). The source is pre-interleaved host-side, which is
|
||||
the honest half of the claim — the packing is an encoder-side transform, the
|
||||
same argument 46.3 made for the text plane.
|
||||
|
||||
### 61.2 The channel writes GVRAM in buffer mode, and it walks the line stride itself
|
||||
|
||||
47.6.2 was right that nothing in this tree had ever pointed a channel at
|
||||
`$C00000`. `dmagate.s` now does, three times, and `dma_run.sh` gates all three:
|
||||
|
||||
| run | R20 | result |
|
||||
|---|---|---|
|
||||
| **4. DMA → GVRAM, bus held, bit 11 SET** | `$0916` | **2,048 B byte-exact against the disc**; 1,024 landed in page 1 (high half), 1,024 in page 0 |
|
||||
| **5. the SAME, bit 11 CLEAR** | `$0116` | **457 bytes lost, every one of them at an EVEN offset; not one ODD byte harmed** |
|
||||
| **6. ARRAY CHAINED, 8 rows at the 1024 B stride** | `$0916` | **2,048 B byte-exact at eight separate row bases from ONE start**; `MAR` ended at `+7,424` |
|
||||
|
||||
In all three the discriminator reads the same as 59.1's: **`MTC` sampled by the
|
||||
instruction after `START` is 0 of 2,048 and the CPU went round its wait loop
|
||||
once** — the whole transfer happened between two instructions.
|
||||
|
||||
**Run 5 exists because the first cut of run 4 was a test that could not fail.**
|
||||
It OR-ed bit 11 on top of whatever the IPL left, and the IPL leaves `$0B16`
|
||||
(22.1) — bit 11 **already set**, and `COL = %11`, the 65,536-colour setup, which
|
||||
writes whole words with or without it. It passed and proved nothing. Both runs
|
||||
now write R20 outright and differ in exactly that bit.
|
||||
|
||||
**And the control's signature is a PLACE, not a count.** The obvious assertion —
|
||||
"half the bytes must differ" — is wrong, and asserting it turned the gate red on
|
||||
a correct run. In masked 256-colour mode `gvram_w` takes `data & 0x00ff` and
|
||||
ignores `mem_mask`, so a byte written to an even address is never stored and the
|
||||
high half keeps what it held; where the record is pad, the stale half matches the
|
||||
disc by coincidence, and 567 of 1,024 did. What the mechanism says is that
|
||||
**every odd byte survives and only even ones can be lost**, and that is what the
|
||||
gate asserts.
|
||||
|
||||
Run 6 is the one that changes an architecture. A picture row is 256 B of a
|
||||
1,024 B line stride, so a frame is **192 destinations, not one**, and a channel
|
||||
writes a contiguous run — the same limitation that makes it refuse a windowed
|
||||
read (58.3). 46.6 said "no stride for a DMAC to skip" about the bytes *within* a
|
||||
row and never examined the rows. The MC68450 answers with sequential array
|
||||
chaining, `src/player/dma.i` now programs it behind a `DM_BARV` mailbox that is
|
||||
**zero unless a caller asks**, and the channel walked an 8-entry array on its
|
||||
own. **The CPU does not restart the channel per row.**
|
||||
|
||||
### 61.3 The codec cannot have the packing — 47.6.4, closed
|
||||
|
||||
A 4×4 block owns four bytes at **stride 2** under the packed layout, because the
|
||||
high bytes of its four words belong to the block 128 columns away. There are
|
||||
exactly two ways a block decoder could live with that, and `blit.s` V9 and V10
|
||||
are them:
|
||||
|
||||
| variant | clk/frame | % of frame | vs V4 |
|
||||
|---|---:|---:|---:|
|
||||
| V4 block order, UNPACKED (the shipping shape) | 637,971 | 76.6% | — |
|
||||
| **V9 block order, PACKED, 16 `move.b` at stride 2** | **815,839** | **97.9%** | **+28%** |
|
||||
| **V10 block order, PACKED, blocks PAIRED** | **320,456** | **38.5%** | **−50%** |
|
||||
|
||||
**V9 is 28% DEARER than V4, and buys nothing on the wire** — a codeword is
|
||||
already one byte a pixel, so the packed layout has no bytes left to save a block
|
||||
decoder. That route costs 177,868 clocks a frame for nothing.
|
||||
|
||||
**V10 halves the paint and pays for it in the mode map.** Pairing the block at
|
||||
x with the block at x+128 gives V4's `movem` shape back, and a pair skips only
|
||||
if *both* its blocks skip. On the gate container:
|
||||
|
||||
| | now | paired |
|
||||
|---|---:|---:|
|
||||
| SKIP | **66.3%** of blocks | **46.1%** of pairs |
|
||||
| painted | **33.7%** | **53.9%** |
|
||||
|
||||
So pairing paints **1.60×** as many blocks for **0.50×** the paint per block:
|
||||
**−20% on the clock, and about +60% on the BYTES**, because a coded block is
|
||||
bytes in the container whether its half of the pair changed or not. E7 needs the
|
||||
bytes **down 35%**.
|
||||
|
||||
**The packed layout is not an upgrade the codec can take. It is the thing you
|
||||
get instead of the codec.**
|
||||
|
||||
### 61.4 The budget, re-derived on the measured model
|
||||
|
||||
Every cell below is CPU work plus transport plus best-case audio (10,417 clk,
|
||||
1.25%, from the IPL ROM's own channel-3 setup — 21_iplrom_dmac.py). None of them
|
||||
overlap: `buscost.DMA_OVERLAPS = False`.
|
||||
|
||||
| architecture | B/frame | W=5 | **W=9** | W=12 | W=16 | W=19 |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| CODEC, CPU-decoded (the shipping design) | 37,585 | 92.4% | **110.4%** | 124.0% | 142.0% | 155.5% |
|
||||
| free / DMAC device→GVRAM / unpacked | 98,304 | 61.1% | 108.2% | 143.6% | 190.8% | 226.2% |
|
||||
| **free / DMAC device→GVRAM / PACKED** | **49,152** | 31.6% | **55.2%** | 72.9% | 96.5% | 114.1% |
|
||||
| free / CPU-painted / unpacked, 2 B/px wire | 98,304 | 113.8% | 161.0% | 196.4% | 243.5% | 278.9% |
|
||||
| free / CPU-painted / unpacked, 1 B/px wire | 49,152 | 184.8% | 208.4% | 226.1% | 249.7% | 267.4% |
|
||||
| **free / CPU-painted / PACKED** | **49,152** | 58.0% | **81.6%** | 99.3% | 122.9% | 140.6% |
|
||||
|
||||
**W=9 is the column that matters**, because 59.2 found the only configurations
|
||||
this machine can be shown to run are dual-address, and a dual-address byte is a
|
||||
4-clock read of the device plus a 5-clock write to memory. Everything left of it
|
||||
is a hardware fact nobody here has (ROADMAP B3).
|
||||
|
||||
**At the floor the CODEC misses by 10.4% and the DECODER-FREE PACKED PLAYER FITS
|
||||
WITH 45% TO SPARE.** Decoding 37,585 bytes costs 109% of a frame; not decoding
|
||||
49,152 costs 54%.
|
||||
|
||||
That is not a small correction to 47.5. **44.7 said it in advance, on a cost
|
||||
model that has since been thrown away: "the codec is not there to save CPU — it
|
||||
is there to save the wire."** The measured model agrees and goes further: the
|
||||
CPU side is not merely affordable without the codec, it is *strictly cheaper*.
|
||||
|
||||
### 61.5 So it is entirely a medium question, and the medium is the unmeasured one
|
||||
|
||||
| architecture | B/frame | KB/s | GB for 22.8 min |
|
||||
|---|---:|---:|---:|
|
||||
| CODEC, gate container | 37,585 | 440.4 | 0.62 |
|
||||
| CODEC, `need` default | — | 267.9 | ~0.38 |
|
||||
| CODEC, E7's target at the 9 clk/B floor | 27,924 | 327 | 0.46 |
|
||||
| **decoder-free PACKED, either architecture** | **49,152** | **576.0** | **0.81** |
|
||||
| decoder-free unpacked | 98,304 | 1,152.0 | 1.61 |
|
||||
|
||||
**576 KB/s, sustained, with no lever to pull.** A codec's bitrate is a lever; a
|
||||
literal frame's is geometry, and no scene in the picture costs less than another.
|
||||
ROADMAP B1 is unmeasured, and the 0.7–1.7 MB/s usually quoted for BlueSCSI on an
|
||||
X68000 is **folklore with no published benchmark behind it**. 576 KB/s sits
|
||||
inside that range, which is exactly the reason the range has to be measured
|
||||
rather than cited: **a codec at 327 KB/s survives a slower answer and a literal
|
||||
frame does not degrade, it drops.**
|
||||
|
||||
**B1 has therefore changed character.** It used to set how much headroom the
|
||||
player has. It now decides which player exists.
|
||||
|
||||
### 61.6 And under MAME's blanking reading, the cheap architecture is the dark one
|
||||
|
||||
47.4's question is untouched by any of this and it still needs a board. What the
|
||||
measured paint does is turn 48.3's range into numbers, and it exposes an
|
||||
asymmetry nobody had stated: **R20 bit 11 only has to be set across the GVRAM
|
||||
writes, and where those writes come from decides how long that is.**
|
||||
|
||||
| architecture | black interval | bit 11 set for |
|
||||
|---|---:|---|
|
||||
| free / DMAC device→GVRAM / unpacked | 60%..225% of a frame | the whole DMA |
|
||||
| free / DMAC device→GVRAM / PACKED | 30%..113% of a frame | the whole DMA |
|
||||
| free / CPU-painted / unpacked | 53.6% | the blit only |
|
||||
| **free / CPU-painted / PACKED** | **27.3%** | **the blit only** |
|
||||
|
||||
The CPU-painted packed path has the smallest black window of any of them,
|
||||
because its transport lands in RAM where bit 11 is irrelevant and only the
|
||||
227,553-clock blit needs the bit. **The DMAC-direct path, which is cheaper in
|
||||
clocks at every rung of the ladder, is the one that must hold the bit across its
|
||||
whole transfer.** If MAME is right, the cheap architecture is the dark one and
|
||||
the expensive one is merely dim.
|
||||
|
||||
Both are a strobe at the frame rate over the whole picture, and the packed layout
|
||||
has no page to flip to: both 256-colour pages carry picture, which is the entire
|
||||
point of it (48.3). If px68k is right, every number in 61.4 and 61.5 stands as
|
||||
written. 48.1's prior — an assertion against a silence — has not moved.
|
||||
|
||||
### 61.7 What this does NOT establish
|
||||
|
||||
1. **No clock here is a transport clock.** MAME's DMAC runs on wall-clock
|
||||
attotimes and models a held bus by halting the CPU (42.5), so runs 4–6 settle
|
||||
which configurations *work* and not what one costs. Every `W` in 61.4 is
|
||||
datasheet arithmetic except PIO, and the array-chain entry at 36 clocks is
|
||||
Fig 4-25 sheet 1, not a measurement.
|
||||
2. **One frame's worth of transport, not a stream.** Run 6 chained eight rows,
|
||||
not 192, and nothing here ran a chained transfer back to back at 12 fps or
|
||||
through `ring.i`. A decoder-free player has no ring at all in the DMAC-direct
|
||||
form, which is a simplification this tree has not tested.
|
||||
3. **The paint is a lower bound.** Every `blit.s` number is instruction cycles
|
||||
against zero-wait-state memory; real GVRAM stalls the CPU and neither
|
||||
emulator models it (`blit.lua`'s header, and 45's note on C68K).
|
||||
4. **61.3's pairing cost is one container's.** 66.3% / 46.1% is the gate
|
||||
container, deliberately the heaviest thing the encoder emits. A lighter
|
||||
container has a different SKIP map and would pair differently — though not,
|
||||
at that margin, differently enough to change the sign.
|
||||
5. **No audio, no branching, no seek.** 61.4 charges the audio DMA and nothing
|
||||
else. A decoder-free player still has to make 56's branch decisions and 51's
|
||||
seeks, and at 576 KB/s it has less slack to make them in, not more.
|
||||
|
||||
### 61.8 Encoder work is parked (session 29, USER DECISION)
|
||||
|
||||
61.4 first closed with "nothing here is a reason to stop work on the codec — it
|
||||
is the only branch that survives a slow answer to B1." **That does not survive
|
||||
its own arithmetic and is withdrawn.**
|
||||
|
||||
It rested on 576 KB/s against **E7's 327 KB/s target, which does not exist.** The
|
||||
codec that exists is **440 KB/s and 110.4% of a frame**, so the gap is **1.31x,
|
||||
not 1.76x** — and reaching 327 needs a 35% byte reduction after 60.4 and 60.5
|
||||
measured two of the encoder's three byte levers and found neither is a lever. The
|
||||
reward on success is a design at ~100% of the frame with no margin, which is where
|
||||
55.2% already is.
|
||||
|
||||
The two branches are not symmetric, and counting what each NEEDS is the argument:
|
||||
|
||||
| | facts it needs |
|
||||
|---|---|
|
||||
| **decoder-free packed** | buffer mode does not blank (B2); medium clears 576 KB/s (B1) |
|
||||
| **codec** | E7 succeeds (unproven, two levers dead); **and** medium clears 327; **and** it ships at ~100% of a frame |
|
||||
|
||||
**So E7 and E4 are parked, and C1 with them.** E4 is included deliberately: it is
|
||||
`H.build`'s k-means, it builds **VQ codebooks**, and a literal player has no VQ.
|
||||
|
||||
**What is NOT parked is the codec itself.** It stays on disk, gated by check.sh,
|
||||
and nothing is built on it. 48.1's prior leans against packing — MAME asserts the
|
||||
blanking semantic twice and deliberately, px68k's display path never reads the
|
||||
bit at all, and an assertion against a silence is not a tie — and 48.3 stands:
|
||||
if buffer mode blanks there is no version of the packed player that is merely
|
||||
expensive. In that branch the codec is the only path left. **Keeping a working
|
||||
decoder is inventory; building on it is work, and the work waits on B2.**
|
||||
|
||||
### 61.9 And the picture is BETTER — the codec is capped below the thing replacing it
|
||||
|
||||
Asked while scoping what a packed player would take, and it is the finding that
|
||||
makes the branch worth building rather than merely affordable. The numbers are
|
||||
`18_text_plane_16col.py`'s, over the same 120-frame window everything else is
|
||||
measured on; 46.3 computed them to give the *text plane* something to be scored
|
||||
against and never turned them on the 256-colour path itself.
|
||||
|
||||
| | PSNR vs the 24-bit source |
|
||||
|---|---:|
|
||||
| shipping container (the codec, as it ships) | **29.19 dB** |
|
||||
| 256 colours, SCENE palette — **the codec's CEILING** | **31.33 dB** |
|
||||
| 256 colours, **PER-FRAME** palette | **34.08 dB** |
|
||||
|
||||
**The middle row is a ceiling, not a rival.** Every codeword the codec emits is
|
||||
an index *into* `vq.scene_palette`, so no bitrate takes it past 31.33 dB; it
|
||||
spends 440 KB/s getting within 2.14 dB of it.
|
||||
|
||||
**A literal frame has no codebooks, so nothing forces the scene palette on it,
|
||||
and per-frame palettes become legal.** 46.3 stated the constraint in as many
|
||||
words while arguing the *other* side — "the 256-colour path cannot do this: its
|
||||
palette is shared scene-wide because the codec's codebooks are indices INTO it."
|
||||
Remove the codec and the constraint goes with it.
|
||||
|
||||
**So the decoder-free packed player is +4.89 dB on the shipping container and
|
||||
+2.75 dB past a ceiling the codec cannot cross** — while costing 55.2% of a frame
|
||||
against 110.4%. It is not a quality compromise bought with clocks. It is better
|
||||
on both, and the whole of its cost is on the wire.
|
||||
|
||||
What the per-frame palette costs:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| wire | 512 B/frame → 49,664 B, **582.0 KB/s (+1.0%)** |
|
||||
| clocks | ~2,370, **0.28% of a frame** if the CPU writes it — DERIVED from V8's measured 9.259 clk/word in the same `movem` shape |
|
||||
| colours | **254, not 256** — the packed layout spends index 0 on the transparency key and puts black at 255 (47.2, `prep_frame.py --pack-transparent`), where `--reserve-black` spends one. A reserved entry measured 0.04 dB in 60.3, so this is noise against +4.89 |
|
||||
|
||||
**Two things this does NOT settle.**
|
||||
|
||||
1. **Whether a channel can write `$E82000`.** If the palette registers take a
|
||||
byte-wide DMA the way GVRAM does in buffer mode, the palette is a 193rd array
|
||||
entry and costs the CPU nothing at all — one channel start still paints a
|
||||
whole frame. 61.2 only ever pointed a channel at GVRAM. **This is the next
|
||||
probe, and it is the same shape as the ones that worked.**
|
||||
2. **The quantiser is PIL's MEDIANCUT, not this project's.** `vq.scene_palette`
|
||||
and `H.build` are what would actually ship the palette. The direction is
|
||||
measured and the magnitude is about right; re-derive the per-frame figure
|
||||
against the real builder before quoting it as the player's number.
|
||||
|
||||
Reference in New Issue
Block a user