Put the container on the chip, and find the held bus costs 463 times the seam

ROADMAP P6c, FINDINGS 68. 78,125 B of a DLXP2's audio out of channel 3,
sample-exact, while the video channel fetches records off the same disc.

The two pieces 67.6 said were missing: the lump buffer (pg_afill/pg_afetch,
three slots and the minimum is unmeasured) and 67.2's remainder accumulator
(pg_apay). The capture prices what the accumulator avoided at 1.26 s of
lip-sync over the game, against 67.2's predicted 1.25.

The finding is the third piece, which nothing had named: the MSM6258 has no
starvation state, so the gap between a channel counting out and the next arm
is a held nibble pair driving the predictor. Stealing, the seam is 0.51 ms
over ten seconds because dma.i's new DM_HOOK services the chip from inside
the transfer wait -- 250,000 of 250,240 looks. Held, the 68000 is halted and
gets 369: every one of the ten lump boundaries has a seam, worst 72.8 ms,
2.31% of the audio. Identical bytes, different sound. 64.3 reaching the audio.

Two bugs, and no counter in the player could see either. Clearing DM_BARV does
not unchain a channel -- OCR bits 3-2 are what it obeys -- and the symptom is
POLL TIMEOUT on the lump and every record after it. And the refill ran one lump
ahead of its ring and overwrote the buffer the channel was reading: 11 of 11
armed, 11 fetched, no starve, and the sound wrong from 0.2 s in. Which is why
the gate is a WAV: verify_packed_audio.py walks the stream one delivered byte
at a time, because MAME's okim6258 resets the nibble select on every write and
a byte is two nibbles only 99.994% of the time.

check.sh ALL GREEN before (tmp/check_s36_start.log) and after
(tmp/check_s36_end.log), with the new stage.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 13:11:37 -07:00
parent e3778f62b0
commit 191f2b47bb
13 changed files with 1413 additions and 20 deletions
+149
View File
@@ -7219,3 +7219,152 @@ whole sectors and the wire pays for the zero ones.
disc is still unmeasured.
* **The lump buffer is not allocated anywhere.** 65.3 charges 14,336 B for
double-buffering the cadence and no player holds it.
## 68. The container is played — and holding the bus costs the audio 463 times the seam (session 36)
**ROADMAP P6c. Session 36.** `src/player/packed.s` (the audio path),
`src/player/dma.i` (`DM_HOOK`), `src/player/scsi.i`, `tools/encoder/adpcm.py`
(`decode_state`), `tools/bench/verify_packed_audio.py`,
`tools/bench/prep_packed.py`, `tools/bench/packed.lua`,
`tools/bench/packed_run.sh`, `tools/bench/check.sh`.
**NAME THE LAYER.** 68000 code fetched an interleaved DLXP2 off a real MB89352
volume, ran the video path against it, and fed the audio lumps to the machine's
**MSM6258 as MAME models it** through the IPL ROM's own HD63450 channel 3. The
capture is MAME's `-wavwrite`. **No board ran**, and every number below is the
emulated machine's. What is new is that a byte of this container has now been
**heard** on some layer, which 67.6 recorded that none had.
### 68.1 It plays, and the gate is the speaker rather than the counters
All **78,125 B** of the container's audio reached the chip, in order, and every
sample the chip produced from them is exact against `adpcm.py` run with the four
axes **out of the container's own header****156,257 samples**, `signal =
sample/8` recovering the chip's stream with a worst residual of 1 count.
`packed.s` also reports 11 of 11 lumps armed, 11 fetched, 78,125 B of payload,
**0 starves and 0 re-arms with bytes still outstanding**. Those numbers are not
the gate and must not be: see 68.4.
### 68.2 THE HEADLINE. Held, the bus costs the audio 236 ms of replayed byte
The MSM6258 has no FIFO and **no starvation state**: when its DMA channel counts
out it goes on decoding nibbles out of whatever byte its data register still
holds (MAME `okim6258::sound_stream_update` reads `m_data_in` unconditionally
while PLAYING). So the interval between a channel counting out and the CPU
arming the next lump is not a gap — it is a **held nibble pair driving the
predictor**, and it is measurable in the capture as bytes that were played for
more than two nibbles.
Same container, same player, same 120 frames, one bit of DCR different:
| | stealing | held |
|---|---:|---:|
| audio service calls | **250,240** | **369** |
| ...per frame shown | 2,085 | 3.1 |
| lump boundaries with a seam | 4 of 10 | **10 of 10** |
| worst seam | 2 samples, **0.13 ms** | 1,138 samples, **72.83 ms** |
| total replayed | 8 samples, 0.51 ms | 3,694 samples, **236.42 ms** |
| ...as a share of the audio | 0.0051% | **2.31%** |
| bytes wrong | 0 | 0 |
**The bytes are identical and the sound is not.** Held is 673x fewer looks at
the channel and 463x the seam. A 72.8 ms seam is most of a 12 fps frame of one
byte pair repeating: not a dropout, a buzz, once every 0.92 s.
This is FINDINGS 64.3 arriving on the audio side. 64.3 measured a held channel
costing the frame CLOCK 47% of its ticks *without the clock being able to tell*;
the same halt costs the audio its service, and the audio can tell — in the only
place the interval exists, which is the sound.
**And it is why the seam is small in the stealing configuration at all.** The
one place a 68000 driving this video path has time is **inside the transfer
wait**. Of the 250,240 service calls, **240 are the frame loop's** two-a-frame
and the other **250,000 came from `DM_HOOK`**, dma.i's per-trip hook. Held, the
same player gets 369: the same 240, plus the 129 single trips round the wait
that 131 burst transfers (120 records and 11 lumps) still allow. Servicing the
audio only from the frame loop is a 90 ms seam by construction, in either
configuration — the hook is the whole of the difference.
### 68.3 The drift is measured now, not derived
67.2 computed that feeding the chip the whole 7,168 B lump instead of the
payload runs 0.09% fast, and priced it at 1.25 s of lip-sync over the game. The
accumulator (`acc += F*hz; n = acc/(2*fps); acc %= 2*fps`, three lines in
`pg_apay`) is now in a player and the capture prices what it avoided, over the
10 lumps that carry a whole group:
payload 71,614 B against 71,680 B of lump space
excess 66 B = 0.092% = 8.45 ms over 9.17 s
-> 1.26 s over the game's 22.8 min
**67.2 predicted 1.25 and the machine played 1.26.** The accumulator ends the
scene at 1/24, which is where the arithmetic says it should.
The gate does not merely *check* the payload lengths, it **cannot pass without
them**: the walk accounts for every byte of `DLXP.audio()`, and a whole-lump
player's stream contains 6.54 B of zero at the end of each group that are not in
that stream at all.
### 68.4 Two bugs, and neither is visible from inside the player
**(a) Clearing `DM_BARV` does not unchain a channel.** `dma.i` reads `DM_BARV`
to decide whether to write MAR and MTC, but the register the *channel* obeys is
**OCR bits 3-2**, which `pg_dmacfg` sets to %10 for the whole scene. So the
lump fetch wrote MAR and MTC and the channel ignored both and loaded them from
whatever BAR held (`hd63450.cpp` `dma_transfer_start`). It does not fail as a
bad address: the SPC's phase never completes and the read returns **POLL
TIMEOUT — on the lump and then on every record after it**. A wedged transport
that reads exactly like a disc problem.
**(b) The refill overwrote the lump that was playing.** The resident set is
`AKF - AK + 1` lumps, not `AKF - AK`: `AK` is the *next* lump to arm, so the one
channel 3 is reading is `AK-1` and its slot is not free. Off by one, the fetcher
ran three lumps ahead of a three-slot ring and lump 3 landed on top of lump 0
**while the channel was reading it**.
**Every counter in the player stayed right**: 11 of 11 armed, 11 fetched, 78,125
B of payload, no starve, no late re-arm — and the sound was wrong from **0.2 s**
in. This is 67.4's failure mode arriving on the audio side. Nothing parses a
packed container, so a wrong byte is not an error, it is a sound; the only
instrument that can see it is the stream the chip produced.
### 68.5 The rig fact that shapes the gate: a byte is not always two nibbles
MAME's `okim6258::data_w` sets `m_data_in` **and resets `m_nibble_shift` to 0**.
The DMA channel writes at 7,812.5 B/s and the stream decodes at 15,625 Hz, and
those are two independent clocks — so where a write lands inside a sound-stream
slice decides whether the byte before it was played as two nibbles, as one (the
high nibble dropped), or as three or more.
Measured over the stealing run: **78,120 of 78,125 bytes were played as exactly
two nibbles (99.994%)**, one as one, four as four. So a whole-stream
`decode(data) == capture` cannot be the check, and a tolerance would not be one
either. The gate's model is one line — *byte `b` was played as `c` nibbles from
the cycle `(b&15, b>>4)`, `c >= 1`* — and it finds `c` for every byte, requiring
all `c` samples and 32 samples of continuation to be exact. **Every `c > 2` in
both runs was the last byte of a lump**, asserted, which is what makes the same
walk both the correctness check and the seam measurement.
This is a property of the emulator's audio scheduling, not of the machine and
not of the format. On silicon the chip latches a byte and consumes both nibbles.
### 68.6 What was NOT established
* **Two slots was not tried.** Three lump slots and a prefill of two is what
ran; the minimum buffering depth is an experiment nobody has done, so 68's
49,152 B of slot (21,504 B of lump) is an upper bound and not a requirement.
The slot stride is 16,384 B for a 7,168 B lump to make the address a shift;
a shipping player should charge `A*512` and pay the `mulu`.
* **The seam figures are this apparatus's.** They are set by how often the CPU
looks at the channel, which is set by how long the emulated transport takes to
deliver a record (90.7 ms against a 12 fps slot of 83.3). The *ratio* between
held and stealing is the finding; the absolute milliseconds are not `W`.
* **Nothing has been listened to.** The capture is an instrument. 66's risk is
unchanged and so is 66.3's: **the level is still open downward and the loudest
passage on the disc is still unmeasured.**
* **A second pass does not replay the audio.** `PG_ITER > 1` repeats the video
and leaves `PG_AK` at `PG_NLUMP`; the run that matters is one pass.
* **The silicon is untouched.** All four decoder axes are still MAME's, and
session 34's fifth hardware item — play a known nibble stream on a real
MSM6258V and record the line out — is still the cheap way to settle them.