Take the player through a branch with sound, and find the predictor does not seek
FINDINGS 71, ROADMAP P6d. 70.3 named exactly what was missing -- packed.s starts PG_AK/PG_AKF at lump 0 and has no audio seek path -- and priced its absence at a mean 416.5 ms of silence over the arcade's 409 within-container seek targets. pg_aseek is that path: the lump index, the stream position, the remainder accumulator and the byte offset into the group, then the second READ(10) at the lump's own LBA and a re-arm part way into the buffer. Measured off a real volume: 132,162 B of spliced stream accounted for byte by byte in MAME's own capture, across a branch at frame 37 -- four frames into lump 3, deliberately NOT on a group boundary -- in both chip configurations. Skip computed 2,604 B, cadence says 2,604. THE PREDICTOR DOES NOT SEEK, AND THE ERROR IS DC. The MSM6258's accumulator is a pure integrator with no leakage term, so a branch that hands the chip bytes chosen for a state it is not in produces an offset that does not decay. Playing through: DC -355 of 511 with AC 0.00 -- the right shape from the wrong ground -- still -108 four seconds later. STOP and re-PLAY: all 62,500 post-seek samples are EXACTLY a decode from the container's own init, and the whole error is the single constant -65. A re-PLAY is 5.5x better and neither is zero, so PG_ARST is a mailbox with a number under it. The host computes -65 out of the container's bytes and the gate asserts the equality rather than printing both. AND THE ONLY FIX THAT REACHES ZERO IS THE ENCODER'S. A player cannot set the chip's accumulator, only reset it. Resetting the encoder's predictor every frame makes all 119 of the container's branch points exact for 0.33 dB (21.99 -> 21.66), because the step table's floor is a constant 16. That is a DLXP3 and it is deliberately not in tools/encoder. TWO SILENT BUGS, BOTH CAUGHT BY THE CAPTURE. pg_udiv32 trashes d4 and pg_aseek held hz there, so the offset came out 1 byte instead of 2,604 -- 166 ms of the wrong part of the scene at exactly the right rate, every counter agreeing. And one already in the tree that had passed this gate three times: pg_ainit waited on a READ-BACK MTC before PLAY, which is the same test as "a byte has left RAM" only if no byte leaves in between. One does, and the chip then plays the scene one byte in, forever. Found by locating the capture's opening samples in the container image: sector 1 + 1. The witness is now the count that was written. ALL GREEN, two new stages included. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -7709,3 +7709,206 @@ uniform would be a quarter out at F=3. At the shipped F=11 the ratio is
|
||||
6. **The 409/203 split is the graph's, not a container manifest's.** No scene
|
||||
has been encoded but one; the mapping from a scene to a DLXP2 is a design
|
||||
statement this tree has made and not yet built.
|
||||
|
||||
# Findings — session 39 (2026-08-25)
|
||||
|
||||
## 71. The player seeks with sound on it — and the chip's predictor does not seek with it (session 39)
|
||||
|
||||
**ROADMAP P6d**, and it is the item session 38's handoff put first. 70.3 named
|
||||
exactly what was missing — "`src/player/packed.s` starts `PG_AK`/`PG_AKF` at
|
||||
lump 0 and has no audio seek path at all" — and priced its absence at a **mean
|
||||
416.5 ms of silence** over the arcade's 409 within-container seek targets, worst
|
||||
833.3, with only 36 of the 409 free. This is that path, built and run.
|
||||
|
||||
bash tools/bench/packed_run.sh # runs 1, 5, 6, 7 -- 7 is this
|
||||
python3 tools/analysis/37_audio_seek.py # the host half
|
||||
./tools/bench/check.sh # ALL GREEN
|
||||
|
||||
`src/player/packed.s` (`pg_aseek`, `pg_udiv32`, `PG_SEEKF`, `PG_ARST`,
|
||||
`PG_APOS`, `PG_ASKIP`, `PG_AMTCA`), `src/player/adpcm.i` (`ad_abort`),
|
||||
`tools/bench/packed_run.sh` run 7, `tools/bench/verify_packed_audio.py`
|
||||
(`--seek`, the spliced stream, the split walk), `tools/analysis/37_audio_seek.py`.
|
||||
The player is **3,954 → 4,340 bytes**.
|
||||
|
||||
### 71.1 The seek path: four numbers and a second read
|
||||
|
||||
The video half is unchanged and is ROADMAP K3's whole claim arriving intact: a
|
||||
packed record is found by arithmetic from the frame index, so a video seek is
|
||||
the assignment `d7 = f`. The audio half is not arithmetic, because audio rides a
|
||||
**cadence** rather than a frame. A DLXP2 group is `lump k, then F records`, so
|
||||
lump k lies at a *lower* address than every record of its group but the first,
|
||||
and reading forward from record f picks up lump k+1.
|
||||
|
||||
What a seek to frame f has to rebuild is four things, and three of them are
|
||||
invisible if they are wrong:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `PG_AK`/`PG_AKF` | the lump index, `k = f/F`, and a **second READ(10)** at `PG_ALBA0 + k*(F*PG_RECS + PG_CADA)` |
|
||||
| `PG_APOS` | the stream POSITION, `floor(k*F*hz/(2*fps))` — what the tail limiter measures the last lump against |
|
||||
| `PG_AACC` | the remainder accumulator **at lump k**, which is that same division's remainder (67.2). Wrong, this is not an error, it is a *rate* |
|
||||
| `PG_ASKIP` | the byte offset of frame f inside lump k, `floor((acc + (f mod F)*hz)/(2*fps))` |
|
||||
|
||||
`PG_APOS` is a **new cell and not a rename**. `PG_ABYT` is what the chip was
|
||||
handed; `PG_APOS` is where the container has got to, and a seek moves one and
|
||||
not the other. One cell for both facts gives a last lump that is long by the
|
||||
skip, and the player's own byte count agrees with it.
|
||||
|
||||
`pg_udiv32` exists for one call. `floor(f*hz/(2*fps))` passes 65,535 after **101
|
||||
seconds of scene** at 12 fps and 15,625 Hz, and the 68000's `divu` does not fail
|
||||
on a quotient that does not fit — it sets V, leaves the destination alone and
|
||||
carries on. Every other division in this player is a bare `divu` and is right to
|
||||
be; this one is not.
|
||||
|
||||
**Measured, on the emulated machine, off a real MB89352 volume**: two passes,
|
||||
the second starting at **frame 37** — four frames into lump 3, chosen *not* to
|
||||
be a multiple of F, because a target on a group boundary needs no offset and 36
|
||||
of the arcade's 409 do land on one. 203 frames, 19 lumps armed and fetched, and
|
||||
the spliced stream of **132,162 B accounted for byte by byte in MAME's own
|
||||
capture**, in both chip configurations. The skip the player computed is **2,604 B**
|
||||
and the cadence says 2,604.
|
||||
|
||||
### 71.2 Two bugs, both silent, both caught by the capture and not by a counter
|
||||
|
||||
**The first was mine and took four minutes.** `pg_udiv32` uses `d4` as a work
|
||||
register and `pg_aseek` was still holding `hz` in it, so the offset came out
|
||||
**1 byte where the cadence says 2,604**. That is not an error and it is not
|
||||
silence: it is **166 ms of the wrong part of the scene, played at exactly the
|
||||
right rate**, with every counter in the player agreeing with it.
|
||||
|
||||
**The second was already in the tree and had passed this gate three times.**
|
||||
`pg_ainit` arms channel 3, then reads MTC back, then waits for that read-back
|
||||
value to change before telling the chip to PLAY — the point being that a byte
|
||||
must be in the data register first. That is the same test as "a byte has left
|
||||
RAM" **only if no byte leaves between the channel start and the read**. One
|
||||
does, whenever `#DRQ3`'s phase falls that way: the read-back already says 7,160,
|
||||
the loop waits for 7,159, and the PLAY lands after the channel has fetched byte
|
||||
1 **on top of byte 0**. The chip then plays the scene from one byte in, forever.
|
||||
|
||||
Nothing in the player can see it. Every lump armed, every byte delivered, every
|
||||
counter exact, and the sound is the whole stream shifted by one byte — 67.4's
|
||||
failure mode landing on the one byte of a scene that a player cannot check. It
|
||||
was found by taking the capture's opening samples and **locating them in the
|
||||
container image**: sector 1 + 1. The fix is one cell: the witness is now the
|
||||
count that was *written* (`PG_AMTCA`), not one read back.
|
||||
|
||||
**It is a race, so it did not reproduce on demand.** Sessions 36–38 passed with
|
||||
it, because the arm happened to land in the other half of a 128 µs window; 386
|
||||
bytes of new code moved the phase. What makes it a bug rather than bad luck is
|
||||
that the witness was a read.
|
||||
|
||||
### 71.3 THE FINDING: the predictor does not seek, and the error is DC
|
||||
|
||||
Every byte can arrive, in order, exactly — and the samples still be wrong. The
|
||||
MSM6258's accumulator is a **pure integrator of deltas with no leakage term**,
|
||||
so a branch that hands the chip bytes chosen for a state it is not in does not
|
||||
produce a transient with a time constant. It produces **a DC offset that does
|
||||
not decay**.
|
||||
|
||||
`verify_packed_audio.py` measures it against the state the *encoder* assumed —
|
||||
`adpcm.encode` runs its decoder inside its own search loop, so the state it
|
||||
chose byte B(f)'s nibbles for is the state a continuous play reaches at B(f).
|
||||
Both designs, same branch, same bytes:
|
||||
|
||||
**A. The chip PLAYS THROUGH the branch** (accumulator −318, step index 47 —
|
||||
where 1,054 ms of replayed byte had driven it):
|
||||
|
||||
| window | DC | AC |
|
||||
|---|---:|---:|
|
||||
| 0–6 ms | −269.3 | 158.49 |
|
||||
| 6–64 ms | −355.9 | 10.53 |
|
||||
| **64–320 ms** | **−355.0** | **0.00** |
|
||||
| 0.3–1.0 s | −274.3 | 45.21 |
|
||||
| 1–2 s | −212.1 | 39.21 |
|
||||
| 2–4 s | **−108.1** | 20.21 |
|
||||
|
||||
**AC 0.00**: the chip is decoding the right *shape* from the wrong *ground*. And
|
||||
four seconds later the ground is still 21% of full scale out. What decay there
|
||||
is comes from the signal's own clamping, not from the recursion forgetting.
|
||||
|
||||
**B. The chip is STOPPED and re-PLAYED** (`PG_ARST=1`; PLAY sets the accumulator
|
||||
to the container's `init` and the step index to 0). All **62,500 post-seek
|
||||
samples are EXACTLY a decode from `init` and step index 0** — not close, every
|
||||
sample — and the whole error against the encoder's intent is **the single
|
||||
constant −65**, which is `init − acc(target)` and nothing else. 12.7% of the
|
||||
10-bit clamp's headroom, permanent, inaudible as a tone, and one click at the
|
||||
branch.
|
||||
|
||||
**So a re-PLAY is 5.5x better and neither is zero.** −355 against −65. This is
|
||||
now a design decision with a number under it rather than an implementation
|
||||
detail, and `PG_ARST` is the mailbox that carries it.
|
||||
|
||||
**The host agrees with the machine to the digit.** `37_audio_seek.py` computes
|
||||
`init − acc(24,088)` from the container's own bytes and gets **−65**; MAME's
|
||||
capture measured the chip at −65, constant over 62,500 samples. One number, two
|
||||
layers, and the gate asserts the equality rather than printing both.
|
||||
|
||||
**The census, over all 119 frame boundaries of the container** — every point a
|
||||
branch in this design can land on, because 56.3's targets are frame indices:
|
||||
|
||||
| | |
|
||||
|---|---:|
|
||||
| mean \|DC\| a re-PLAY costs | **55.4 of 511 (10.8%)** |
|
||||
| median / p90 / worst | 41 / 126 / **313 (61.3%)** |
|
||||
| step index the encoder assumed there | 0..26 of 48, median 12 |
|
||||
|
||||
A re-PLAY sets the step index to 0 as well, so **a branch into a loud passage
|
||||
gets the offset *and* a step index that has to climb back**. The offset is the
|
||||
signal's own value at the cut, so it scales with the programme — and the disc
|
||||
peaks at 946 of 2048 (69.2).
|
||||
|
||||
### 71.4 The only fix that makes a branch free is the ENCODER'S, and it is cheap
|
||||
|
||||
A player cannot set the chip's accumulator; it can only reset it. So the way to
|
||||
make a branch cost nothing is to encode the stream with the predictor **reset at
|
||||
every point a branch can land on**, and then re-PLAY there — the two states are
|
||||
then the same state and the error is zero, not small.
|
||||
|
||||
Priced on the same ten seconds every audio figure in this tree is quoted on:
|
||||
|
||||
| reset every | SNR | vs shipped | branch points made free |
|
||||
|---|---:|---:|---:|
|
||||
| never (shipped) | 21.99 dB | — | 0 of 119 |
|
||||
| 11 frames (the cadence) | 21.98 dB | −0.01 | 10 of 119 |
|
||||
| **1 frame** | **21.66 dB** | **−0.33** | **119 of 119** |
|
||||
|
||||
**Making every frame in the container an exact branch target costs 0.33 dB.**
|
||||
The reason it is not twelve times anything is that the OKI step table's floor is
|
||||
a constant 16 and the recursion re-converges in a few samples — the same
|
||||
property FINDINGS 69 found working the other way, when attenuating walked the
|
||||
programme down toward a quantiser that stays put.
|
||||
|
||||
That is a container change (a DLXP3) and **it is not in `tools/encoder`**. It
|
||||
costs bytes nothing and SNR 0.33 dB, and what decides it is whether a branch is
|
||||
allowed to land anywhere or only on frames the encoder was told about — which
|
||||
the scene graph already knows (56.3).
|
||||
|
||||
### 71.5 What this does NOT establish
|
||||
|
||||
1. **The seek's own time cost is not separable under this apparatus.** The seam
|
||||
at the branch measures **1,054 ms**, and that is the *transport running 8.9%
|
||||
slow* (68): 120 frames at the measured 10.85 fps is 11.06 s of picture
|
||||
against 10.00 s of audio, so the chip had run out before the branch arrived.
|
||||
70.3's arithmetic — 7,168 B is 11.7 ms at 600 KB/s against a mean 416 ms of
|
||||
silence — stands and is still arithmetic. **The extra SCSI command's overhead
|
||||
is B1's and is still unmeasured.**
|
||||
2. **The chip's branch behaviour is MAME's `okim6258`**: PLAY sets the
|
||||
accumulator to −2, the step index to 0 and the nibble select to 0, *only if
|
||||
it was not already playing*. That is the model FINDINGS 66 fitted to the
|
||||
machine, not a measurement of an MSM6258V, and it joins session 34's fifth
|
||||
hardware item. **`ad_abort` is in the player for the silicon and this rig
|
||||
cannot tell**: MAME's `hd63450` restarts a channel whatever it was doing,
|
||||
and the MC68450 calls STR-while-active an operation timing error.
|
||||
3. **`#DRQ3` ticking while the chip is STOPPED is a rig fact.** On this machine
|
||||
it is the driver's own divider (`x68k.cpp`'s `adpcm_drq_tick`, 29.6), so the
|
||||
channel keeps fetching and the re-PLAY's MTC wait terminates. A real
|
||||
MSM6258V that stops requesting when stopped would leave that wait spinning;
|
||||
the bounded count is what turns it into a report rather than a hang.
|
||||
4. **One container, one branch point on the machine.** The census is host
|
||||
arithmetic over ten seconds of one −13.4 dBFS passage. The 409/203 split is
|
||||
still the graph's, and one container per *sequence* rather than per scene
|
||||
makes every seek land on frame 0 and empties 70.3, 70.4 and this.
|
||||
5. **The cadence question 70.4 reopened is now answerable and is not answered
|
||||
here.** With a working seek path the silence F=11 costs is zero and the trade
|
||||
is back to padding against RAM, where 32_audio_wire.py left it — plus one
|
||||
SCSI command per branch, which is B1's.
|
||||
|
||||
Reference in New Issue
Block a user