Put sound in the packed container, and find the padding is a rate error

ROADMAP P6b, FINDINGS 67.  DLXP2: a 64-byte header and then groups -- one
audio lump of A sectors, then F records -- so record i is at
off_frm + i*rec + (i//F)*A*512 and lump k at off_aud + k*(F*rec + A*512).
Still no index and still none needed, which is the packed branch's whole
claim surviving the one change that could have ended it.  The player carries
the third term in six instructions once a frame and zero parsing, and 120 of
120 records are still pixel-exact off a real MB89352 volume with the
interleave in, against a silent control that says no picture byte moved.

The finding is what 65.3 called padding.  A lump is 7,168 B of SPACE; eleven
frames of audio is 7,161.4583... B, so the payload alternates 7,161 and
7,162 and the rest is zero.  A player that fed the chip the whole lump --
which is what "14 sectors every 11 frames" invites -- runs 0.09% fast, and
that is not waste, it is drift: 0.84 ms a group, 1.25 s of lip-sync over the
game's 22.8 minutes.  What a player carries is one accumulator,
acc += 11*15625; n = acc//24; acc %= 24, which is clock.i's shape for
clock.i's reason and the third time this tree has met the pattern.

The four ADPCM axes ride in the header as fields rather than a version
number, and the gate flips each one to prove they earn it: nibble order
-31.99 dB, delta formula -24.86, clamp 0.00, accumulator -0.49.  Nothing
parses a packed container, so the gate partitions the whole file -- 131
spans, no overlap, no gap -- and asserts what a cadence-blind player would
read: exactly records 11..119 wrong, and frames 0..10 identical either way,
which is how an off-by-one like that survives a rig that checks frame 0.

Wire 582.0 + 7.64 = 589.6 KB/s, 65.3's prediction to the tenth.

Green light ALL GREEN before (tmp/check_s35_start.log) and after
(tmp/check_s35_end.log), with the new stage in it.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 11:22:38 -07:00
parent 6dd3fb3597
commit e3778f62b0
13 changed files with 1001 additions and 51 deletions
+56 -11
View File
@@ -395,6 +395,37 @@ the obvious one-lump-per-record cadence wastes **57.3%** of every audio sector.
The wire goes **582.0 → 589.6 KB/s**. The codec container, which already has the
index the packed one deleted, pays **zero**.
**And the packed container has sound in it now — its padding turned out to be
drift.** DLXP2 is a 64-byte header and then *groups*: one audio lump of 14
sectors, then 11 records, so `record i = off_frm + i*rec + (i//11)*7,168`. **Still
no index and still none needed** — the packed branch's whole claim survives the
one change that could have ended it — and on the 68000 the third term is **six
instructions once a frame** and zero parsing, because the cadence is two numbers
in the header rather than a table in the stream. `src/player/packed.s` fetches
records out of the interleaved container off a real MB89352 volume and **120 of
120 are still pixel-exact**, against a **silent control** built from the same
frames that says no picture byte moved.
The finding is what 65.3 called padding. A lump is **7,168 bytes of space** and
eleven frames of audio is **7,161.4583… bytes**, so the *payload* alternates
7,161 and 7,162 and the rest is zero. A player that fed the chip the whole lump
— which is what "14 sectors every 11 frames" invites — runs **0.09% fast**, and
that is not waste, it is **drift: 1.25 seconds of lip-sync over the game's 22.8
minutes**. What a player carries is one accumulator, `acc += 11*15625;
n = acc//24; acc %= 24`, which is the frame clock's shape for the frame clock's
reason. **Third time this tree has met a ratio with a remainder**, and the rule
it keeps writing is that rounding one *once* is an error and rounding it *every
period* is a rate.
The four ADPCM axes ride in the header as fields rather than a version number,
and the gate flips each one to show they earn it: **nibble order 31.99 dB,
delta formula 24.86, clamp 0.00, accumulator 0.49**. Nothing parses a packed
container, so the gate **partitions the whole file** — 131 spans, no overlap, no
gap — and asserts what a cadence-blind player would read: exactly records
11..119 wrong, **and frames 0..10 identical either way**, which is how an
off-by-one like that survives a rig that checks frame 0. The wire is **582.0 +
7.64 = 589.6 KB/s**, which is 65.3's prediction to the tenth (FINDINGS 67).
**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,
516 sequences, 906 input windows — and 5.4% of the game's 612 branch transitions
@@ -425,11 +456,13 @@ mounted) re-runs both display regression tests, the rate-control drift gate, the
display-path coherency counterexample, a 120-frame 68000 decode on two CPU
cores, the ring and paced-ring passes, the DMAC configuration gate and the
load-time transforms on both cores, then imports and gates the scene graph
when a DirkSimple checkout is present, then builds the packed container and
renders it through px68k's own GVRAM model, then **runs the packed player for
120 frames off a real volume and compares every one of them**, then encodes the
same window's audio and gates it against ffmpeg's decoder, then prints
`ALL GREEN`.
when a DirkSimple checkout is present, then builds the packed container --
with audio in it, and a silent control beside it -- and renders it through
px68k's own GVRAM model, then **runs the packed player for 120 frames off a real
volume and compares every one of them**, then encodes the same window's audio
and gates it against ffmpeg's decoder, then asks the emulated MSM6258 which of
sixteen decoders it is, then gates the DLXP2 container and every one of its
axes, then prints `ALL GREEN`.
## Reproducing this
@@ -631,6 +664,11 @@ tools/import/ the ONLY code in this tree coupled to somebody else's source.
project's own DLXSCENE1 schema, with the sources' licences and
attribution inside it. Nothing is vendored and the output is
gitignored derived data.
34 is DLXP2's gate: the file partitioned into spans, the
records checked against a SILENT control, the lumps checked
byte-exact against the encoder, and one negative control per
ADPCM axis. Nothing parses a packed container, so a lump one
sector out does not fail -- it paints.
30 is the PACKED container's gate: the format's invariants
(round-trip, sectors, the transparency key, the palette word),
and the quality re-derivation 61.9 asked for, in both the RGB888
@@ -649,12 +687,19 @@ tools/encoder/ hybrid VQ encoder and DLX3 container writer.
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.
dlxp.py and pack.py are the OTHER container -- DLXP1, the
decoder-free packed one (FINDINGS 63). Nothing is shared with
the codec's writer on purpose: a packed record is a palette and
a picture, both geometry, and dlxp.py is the one place the
interleave and the 97-sector record are stated. There is no
rate control in pack.py because there is no rate lever.
dlxp.py and pack.py are the OTHER container -- DLXP2, the
decoder-free packed one (FINDINGS 63, 67). Nothing is shared
with the codec's writer on purpose: a packed record is a
palette and a picture, both geometry, and dlxp.py is the one
place the interleave, the 97-sector record, the audio cadence
and the lump PAYLOAD are stated. There is no rate control in
pack.py because there is no rate lever.
adpcm.py is the MSM6258 codec and it carries TWO decoders on
purpose: the module defaults are ffmpeg's, so that
tools/bench/verify_adpcm.py stays a check against an
independent implementation, and adpcm.CHIP is the set measured
out of the machine's own chip (66). Anything that encodes FOR
the machine passes CHIP explicitly.
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