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:
prosolis
2026-08-25 06:54:27 -07:00
parent 8800d8f8c0
commit 1be428c270
28 changed files with 2203 additions and 144 deletions
+52 -11
View File
@@ -189,19 +189,56 @@ payload to **328** — which is an encoder target, entirely inside this project,
and measured against the heaviest container the encoder emits rather than
against a shipping one (FINDINGS 59.7).
**A record is not a sector, and the cheapest fix is a re-encode.** 117 of 120
records start part way into a 512 B block, and reading whole blocks into the
ring corrupts the neighbouring records rather than merely wasting bytes — the
block loop reads with no bounds check. PIO absorbs this for free by simply not
storing the bytes outside the window, which is a property that disappears the
**A record was not a sector, and the fix was a re-encode — it is done.** 117 of
120 records used to start part way into a 512 B block, and reading whole blocks
into the ring corrupts the neighbouring records rather than merely wasting bytes
— the block loop reads with no bounds check. PIO absorbed this for free by
simply not storing the bytes outside the window, a property that disappears the
moment a DMA channel takes over. Priced three ways: windowed PIO is +1.34% on
the wire and cannot be done by a channel at all; a bounce buffer is +1.34% and
**+5 clocks on every delivered byte**, 22.4% of a frame; sector-aligning records
in the container is **+0.43% and zero clocks**. The last wins on both axes and
joins the re-encode bundle (FINDINGS 58.3). **Session 27 made it a
precondition rather than a preference**: the transport now *refuses* a windowed
read when the data phase is the channel's, so the container has to meet it
before the DMAC can sit behind the ring (FINDINGS 59.4).
in the container is **+0.43% and zero clocks** (FINDINGS 58.3). Session 27 made
it a precondition rather than a preference — the transport *refuses* a windowed
read when the data phase is the channel's (59.4) — and **session 28 met it: the
container is DLX5, every record is padded to 512 B and the frame stream starts
on a sector boundary. 120 of 120 records are aligned, the realised wire cost is
+0.48%, and the disc now moves exactly the records** — the bytes off the disc
and the bytes into the ring are the same number, which is what check.sh gates on
(FINDINGS 60.1).
**And the player that has no decoder at all fits the budget the codec misses.**
256-colour GVRAM throws away the high byte of every word a CPU writes, so a
picture byte normally costs two disc bytes — but CRTC R20 bit 11 turns the
masking off, and with the two 256-colour pages scrolled apart one word carries
two pixels (FINDINGS 46/47). Session 29 measured what that is worth. The packed
full-frame blit is **227,553 clocks, 27.3% of a 12 fps frame** — 51% of the
unpacked one, and the *same* as the unpacked path's write-only floor, so packing
buys back the whole of the source read. A DMA channel fills GVRAM in buffer mode
straight off the disc with the CPU halted, and **walks the 1,024-byte line stride
itself** through array chaining, so a frame is one channel start and not 192. At
the 9 clk/B dual-address floor — the only configuration this machine can be shown
to run — **the shipping codec is 110.4% of a frame and a decoder-free packed
player is 55.2%.** Decoding 37,585 bytes costs more than not decoding 49,152.
What it costs is the wire: **576 KB/s, fixed, with no lever** — a codec's bitrate
is adjustable and a literal frame's is geometry — against 327 KB/s for the codec
at the same floor. So the two open hardware facts changed character: **whether
the medium sustains 576 KB/s, and whether buffer mode blanks the layer while it
is being written, now decide which player exists** rather than how much headroom
one has. The codec cannot take the packing either way: writing 4×4 blocks a byte
at a time is **28% dearer** than the shipping shape, and pairing the blocks 128
columns apart to get the burst back drops SKIP from 66.3% of blocks to 46.1% of
pairs — about **+60% on the bytes**, against a target that needs them 35% lower
(FINDINGS 61).
**So encoder work is PARKED (USER DECISION, session 29).** Not because the codec
is wrong, but because its remaining path is a conjunction and the packed one is
not. The codec that exists is 440 KB/s and 110.4% of a frame; reaching E7's
327 KB/s needs a 35% byte reduction after two of its three levers were measured
and found inert (60.4, 60.5), and the reward on success is a design at ~100% of
the frame. The packed player is at 55.2% today. **The codec is kept on disk and
not built on**, because B2 is unanswered and 48.1's prior leans against packing —
if buffer mode blanks, it is the only thing left (48.3).
**The scene graph is in, and the worst gap between two decision points is
zero.** `tools/import/scenegraph.py` imports the arcade scene graph — 40 scenes,
@@ -434,7 +471,11 @@ tools/encoder/ hybrid VQ encoder and DLX3 container writer.
side. It must match blit.s and decode.s: 11 coarse units of
24 px, 11 fine of 2.
DLX2 4-byte-aligns every frame record, because an odd move.l
is an ADDRESS ERROR on a 68000, not a slow read.
is an ADDRESS ERROR on a 68000, not a slow read. DLX5 aligns
them to 512 B sectors instead, so a DMA channel can read a
record as whole sectors straight into the ring with no window
and no bounce copy; dlx.record_lengths() is the one place that
rule is applied.
dlx.py is the reference DECODER, ground truth for the 68000.
24 models the ring with the 68000 owning it: the request
queue, the poll-only-when-not-decoding rule and 54.4's frame