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.
+34 -9
View File
@@ -129,9 +129,26 @@ minutes**. The payload is a remainder, `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 and the gate
flips each one to prove they earn it (**order 31.99 dB, formula 24.86**). The
wire is **589.6 KB/s**, 65.3's figure to the tenth. **What is left of P6 is the
wire is **589.6 KB/s**, 65.3's figure to the tenth. ~~**What is left of P6 is the
last quarter: no audio has been played out of this container on any layer, and
two DMA channels have never run at once.**
two DMA channels have never run at once.**~~
Amended end of session 36: **P6c IS DONE, AND HOLDING THE BUS COSTS THE AUDIO
463x THE SEAM (FINDINGS 68).** All 78,125 B of the container's audio reached the
chip, in order, sample-exact against the four axes in its own header, out of a
player that was fetching records off the same disc at the same time. **Two DMA
channels have now run at once and the interaction is not in the bytes, it is in
the timing**: the MSM6258 has no starvation state, so the interval between a
channel counting out and the CPU arming the next lump is a held nibble pair
driving the predictor. Stealing, the seam is **0.51 ms over ten seconds**;
**held, it is 236 ms — 2.31% of the audio, every one of the ten lump boundaries,
worst 72.8 ms**, because a burst channel halts the 68000 and the only place a
68000 driving this video path has time is inside the transfer wait (250,000 of
250,240 service calls). That is FINDINGS 64.3 reaching the audio: **the held
configuration cannot feed a second consumer**, and B1's answer now decides a
sound as well as a picture. 67.2's drift is measured rather than derived —
predicted 1.25 s, played **1.26 s**. **What is left of P6 is the level (66.3)
and the refill climb with a second consumer through a real branch point.**
**THE COMPLETION TARGET IS M3, THE VERTICAL SLICE** (USER DECISION): one scene
tree — a decision point, two outcomes, a death clip — with audio, streaming from
@@ -774,12 +791,18 @@ unchanged (F=11, A=14, 589.6 KB/s) and the **payload** did not: a lump's audio i
7,161 or 7,162 B of a 7,168 B sector run, and feeding the chip the whole lump
drifts 1.25 s over the game.
**P6c. AUDIO OUT OF THE CONTAINER, ON THE MACHINE (new, session 35).** Every
**P6c. AUDIO OUT OF THE CONTAINER, ON THE MACHINE. DONE, session 36 — FINDINGS
68.** The lump buffer is `PG_ABUF` (three slots, prefill two, and the minimum
depth is still unmeasured), the remainder accumulator is `pg_apay`, and the
service runs from inside `dma.i`'s transfer wait through `DM_HOOK` because once
a frame is a 90 ms seam by construction. **Two channels have run at once**: the
bytes are identical in both DMAC configurations and the sound is not — held
costs 236 ms of replayed byte against stealing's 0.51 ms. ~~Every
piece exists and none is joined up. The container carries the bytes (67); the
transport is `src/player/adpcm.i`'s channel-3 configuration, which is the IPL
ROM's own and worked first time (66.1). What is missing is **the lump buffer**
— 14,336 B, double-buffered, allocated by nobody — and **the remainder
accumulator**, which is three instructions and is not optional.
accumulator**, which is three instructions and is not optional.~~
**And the interaction neither half has met: TWO CHANNELS AT ONCE.** The video
channel holds the bus and halts the 68000, which already costs the frame clock
@@ -915,11 +938,13 @@ B2 blanking ─┬─ NOT blanked ─> K3's DMAC-DIRECT player is the one: 54.9%
B1 BURST rate (NEW, 64.2) ──> which of the two K3/K4 wins, if B2 blanks
P6 audio: encoder gated (65), P6a the chip's own decoder measured (66),
P6b DLXP2 written and gated, 589.6 KB/s on the wire (67)
└─> P6c AUDIO OUT OF THE CONTAINER ─┬─ the bytes exist, the transport
needs no board: the transport │ exists, the lump buffer does not
is the IPL ROM's own (66.1) └─> TWO CHANNELS AT ONCE, which is
the one thing nothing has run
P6b DLXP2 written and gated, 589.6 KB/s on the wire (67),
P6c PLAYED -- 78,125 B to the chip beside the video channel (68)
└─> and the second consumer is now ANOTHER INPUT TO B1/B2:
stealing -> 0.51 ms of seam over 10 s \ same bytes,
held -> 236 ms, every lump boundary / different sound
└─> what is LEFT of P6: the LEVEL (66.3, oldest open item) and the
refill climb with the second consumer through a branch point
```
**Read that top-left branch as the project's live question.** Everything else
+125
View File
@@ -1,3 +1,128 @@
# Status & next-session handoff — end of session 36 (2026-08-25)
## Session 36: the container is played, and holding the bus costs the audio
**Green light first and last: `./tools/bench/check.sh` was ALL GREEN before any
of this (`tmp/check_s36_start.log`) and ALL GREEN after
(`tmp/check_s36_end.log`)** — the same stages, plus one new one.
**FINDINGS 68. ROADMAP P6c is DONE.** `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 lumps to the **MSM6258 as
MAME models it** through the IPL ROM's own channel 3. The instrument is MAME's
`-wavwrite`. **No board ran.** What is new is that a byte of this container has
now been *heard* on some layer — 67.6 recorded that none had.
**1. IT PLAYS.** All **78,125 B** of the container's audio reached the chip, in
order, and every sample is exact against `adpcm.py` run with the four axes out
of the container's own header — **156,257 samples**, worst residual 1 count on
`signal = sample/8`. 11 of 11 lumps armed and fetched, 0 starves.
**2. THE HEADLINE, AND IT IS 64.3 ARRIVING ON THE AUDIO SIDE.** The chip has no
FIFO and **no starvation state**: when its channel counts out it goes on
decoding the byte its data register still holds. So the gap between a count-out
and the next arm is a **held nibble pair driving the predictor**, and it is in
the capture. Same container, same player, one bit of DCR:
| | stealing | held |
|---|---:|---:|
| service calls | 250,240 | 369 |
| boundaries with a seam | 4 of 10 | **10 of 10** |
| worst seam | 0.13 ms | **72.83 ms** |
| total replayed | 0.51 ms (0.005%) | **236.42 ms (2.31%)** |
| bytes wrong | 0 | 0 |
**The bytes are identical and the sound is not.** 673x fewer looks at the
channel, 463x the seam. And the reason stealing wins is `DM_HOOK`: **250,000 of
the 250,240 service calls came from inside dma.i's transfer wait**, which is the
only time a 68000 driving this video path has. Once a frame is a 90 ms seam by
construction, in either configuration.
**3. THE DRIFT IS MEASURED NOW.** `pg_apay` is 67.2's accumulator in a player,
and the capture prices what it avoided over the 10 lumps that carry a whole
group: 71,614 B against 71,680 B of lump space, **0.092%, 8.45 ms over 9.17 s ->
1.26 s over the game's 22.8 min. 67.2 predicted 1.25.** The accumulator ends at
1/24, where the arithmetic says.
**4. TWO BUGS, AND NEITHER IS VISIBLE FROM INSIDE THE PLAYER.** Clearing
`DM_BARV` does not unchain a channel — the register the channel obeys is OCR
bits 3-2 — and the symptom is POLL TIMEOUT on the lump *and every record after
it*, which reads like a disc problem. And the refill ran one lump too far ahead
and **overwrote the buffer channel 3 was reading**: 11 of 11 armed, 11 fetched,
78,125 B, no starve, and the sound wrong from 0.2 s in. That is 67.4's failure
mode on the audio side and it is why the gate is a WAV.
**5. THE RIG FACT THAT SHAPES THE GATE.** MAME's `okim6258::data_w` resets the
nibble select, and the DMA (7,812.5 B/s) and the stream (15,625 Hz) are
independent clocks — so a delivered byte is *usually* two nibbles and sometimes
one or three. Measured: **78,120 of 78,125 (99.994%)** were exactly two. So the
gate walks the stream **one delivered byte at a time**, and every stretched byte
in both runs was the last byte of a lump.
**RISKS IN THIS SESSION'S RESULT:**
- **Two slots was not tried.** Three lump slots and a prefill of two is what
ran; the minimum depth is unmeasured, so the RAM figure is an upper bound.
- **The seam milliseconds are the apparatus's**, set by how long the emulated
transport takes a record. The held/stealing RATIO is the finding.
- **Nothing has been listened to.** The capture is an instrument, not a listen.
- **The level is still open downward** (66.3) and the loudest passage on the
disc is still unmeasured. Unchanged since session 34.
- **The silicon is untouched.** All four axes are still MAME's.
## HANDOFF — start here
**THE TREE IS ALL GREEN**, session 36's stage included.
### The work, in the order it should be done
**1. THE AUDIO LEVEL, which 66.3 reopened and two sessions have now deferred.**
The 10-bit clamp is 12.1 dB below where the encoder was clamping and it is
INSIDE the recursion, so a scene that clips does not merely distort, it drives
the predictor. **Measure the loudest passage on the disc before choosing a
level.** This is the oldest open item in P6 and it needs no new machinery.
**2. THE REFILL CLIMB WITH A SECOND CONSUMER, through a real branch point**
(51.3, 55.4). The last quarter of P6 that is not P6c. The slack table is in
`32_audio_wire.py`; nothing has been run. Session 36 gives it the thing it was
missing — a player that actually holds two streams at once — so this is now a
matter of pointing it at a scene change rather than of building anything.
**3. THE BUFFERING DEPTH, which is cheap now.** `PG_ANBUF` is 3 and `PG_APRE` is
a mailbox. Two slots is one constant and one run, and it is 7,168 B of a
machine that has 99,328 B of record buffer to find elsewhere.
### What is still BLOCKED, so it is not picked up by mistake
**K4 — the packed player that is on screen — is conditional on B2**, a board
question. **E7, E4 and C1** are parked (61.8), and **P4a's wiring** is parked
with the ring K3 deleted.
**The hardware list is unchanged and is the user's**: B1 (sustained AND the
data-phase BURST rate, 64.2 — acceptance is **589.6 KB/s**), B2 (blanking), B3
(`#EXREQ`), B4 (a byte write to a palette register), and session 34's fifth:
play a known nibble stream on a real MSM6258V and record the line out. **Session
36 adds nothing to that list and strengthens the case for the fifth**: every
decoder axis the player now depends on is still MAME's.
### Reproducing this session
./tools/bench/check.sh # ALL GREEN
bash tools/bench/packed_run.sh tmp/packed_singe.dlxp
# run 5 is the audio gate, run 6 the held contrast
python3 tools/bench/verify_packed_audio.py tmp/packed_singe.dlxp \
tmp/packed_aud.wav tmp/packed_aud.json
**WHAT IS NEXT.** The level, and then the refill climb with the second consumer
that now exists.
---
# Status & next-session handoff — end of session 35 (2026-08-25)
## Session 35: the packed container gets sound, and the padding is drift