Put the ring on the 68000, and find the disc stops whenever the player is not asking
ROADMAP P5. The loader moved in session 21 and the frame clock in 22; the ring producer was the last policy living outside the machine. src/player/ring.i does `aligned` placement, the descriptor ring, a prefill, 51.2's slack rule and a seek, and the host keeps only the transport. It needed a container change. `aligned` asks whether the next record fits before the end of the ring -- a length asked BEFORE the record is fetched -- and every reader in this tree answered that by walking the frame stream, which is exactly what a player streaming off a disc cannot do. DLX4 carries nframes u16 record lengths in the scene header. Frame payloads are byte-identical to the DLX3 encode, so no fitted constant moves; the scene header goes 5,920 to 6,164 B. The producer reproduces the host's tiling exactly: 18 wraps, 14.7 KB mean hole, pixel-exact, a third independent implementation of the same policy. What it exposed is bigger than the item. A channel only moves bytes while it has a request and only the CPU can issue one, so the disc stands still between records by an amount the PLAYER sets, not the medium -- and no host-filled run could see it. At 488 KB/s in a 256 KB ring a one-deep request queue gives away 6.8% of the pipe and underruns 59 of 120 frames; two-deep gives away 3.4% and underruns none. The container's whole surplus over the wire is 8.7%, so the player's own loop was spending most of the slack a branch point saves up. Prefill is the weaker lever: six records of it still leaves 24 underruns. Three silent bugs are recorded in FINDINGS 55.7 -- all produced wrong pixels or a desync rather than a fault -- plus a rig one: MAME renders a screen line by line, so snapshotting the frame the decoder finished in captures a tear that reads exactly like a decoder bug. check.sh gains the machine-owned ring and a seek with the decode after it. decode.bin is unchanged at 1,296 B and a host-filled run executes none of the new code, so every FINDINGS 49/51 figure stands. ALL GREEN before and after. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -4814,3 +4814,206 @@ accumulator fits 16 bits. `decode.s` and `frame.i` were checked for stack tricks
|
||||
before the mask was lowered: the only `a7` use in either is one `move.l a1,-(sp)`
|
||||
pair, so an interrupt cannot corrupt decoder state. The 120-frame self-paced
|
||||
decode being pixel-exact is the test of that, and it is gated.
|
||||
|
||||
---
|
||||
|
||||
## 55. The 68000 fills its own ring, and the player's request loop costs more than the medium does (session 23)
|
||||
|
||||
ROADMAP P5, the last M2 item this tree could build. FINDINGS 49 and 51 measured
|
||||
a ring that a HOST filled: `tools/bench/stream.lua` held the record index, chose
|
||||
where every record went, wrote the descriptor and advertised it. The 68000 only
|
||||
consumed. That is the same shape session 21 found in the loader and session 22
|
||||
in the frame clock — a policy living outside the machine that has to run inside
|
||||
it — and it was the last one in the delivery path.
|
||||
|
||||
`src/player/ring.i` is that policy on the 68000: `aligned` placement, the
|
||||
descriptor ring, a prefill, an accumulated-slack rule and a seek. The rig keeps
|
||||
only what is genuinely not the CPU's — a transport that answers one request at a
|
||||
time at a modelled rate, which is what an SPC and one DMAC channel are.
|
||||
|
||||
### 55.1 The container had to change: DLX4 carries a record index
|
||||
|
||||
`aligned` asks whether the NEXT record fits before the end of the ring, which is
|
||||
a question about a record's length asked **before it is fetched**. Every reader
|
||||
in this tree learned record boundaries by **walking** the frame stream — reading
|
||||
each record's length word to find the next — and that is exactly what a player
|
||||
streaming off a disc cannot do: the length word of record *i+1* is one of the
|
||||
bytes it has not fetched. A branch point needs the same table a second time, to
|
||||
seek to record *j* without reading what lies between.
|
||||
|
||||
DLX4 adds `nframes` u16 longword-counts to the scene header, ahead of the frame
|
||||
stream. Costs, measured on the gate container:
|
||||
|
||||
| | DLX3 | DLX4 |
|
||||
|---|---:|---:|
|
||||
| scene header | 5,920 B | **6,164 B** (+240 index, +4 header) |
|
||||
| frame payloads | — | **byte-identical**, all 120 |
|
||||
|
||||
The payloads being byte-identical is asserted rather than assumed: the same
|
||||
encode was written both ways and compared record for record, so **no constant
|
||||
fitted to the gate container moves**. `dlx.py` cross-checks the index against
|
||||
its own walk of the stream and refuses a container where they disagree, and
|
||||
`prep_stream.py` checks it again against the disk image it lays out. Lengths
|
||||
rather than offsets: 2 bytes a frame instead of 4, and the disc offsets are a
|
||||
running sum the player builds once at scene load (`ROFF`, 4 B/record of RAM).
|
||||
|
||||
In 53.5's currency the 244 bytes are small — 0.5 ms of pipe at 488 KB/s — but
|
||||
they are on the same side of the ledger as the 5,920 that section priced, and
|
||||
the scene header is now **6,164 B that must arrive before frame 0**.
|
||||
|
||||
### 55.2 It reproduces the host producer's tiling exactly
|
||||
|
||||
Third independent implementation of `aligned`, on the gate container in a 256 KB
|
||||
ring:
|
||||
|
||||
| producer | wraps | mean hole | pixel-exact |
|
||||
|---|---:|---:|:--:|
|
||||
| `19_ring_stream.py` (Python, from record sizes) | 18 | 14.7 KB | — |
|
||||
| `stream.lua` (host, driving the 68000) | 18 | 14.7 KB | yes |
|
||||
| **`ring.i` (the 68000 itself)** | **18** | **14.7 KB** | **yes** |
|
||||
|
||||
The host now **audits** rather than produces: every placement the machine makes
|
||||
is checked against the host's own index and its own list of records the decoder
|
||||
has not consumed, and the run is refused on the first disagreement. That is what
|
||||
makes the pixel-exact result a statement about `ring.i` and not about a new rig.
|
||||
|
||||
### 55.3 THE ONE THAT MOVES SOMETHING: the channel is idle whenever the player is not asking
|
||||
|
||||
A channel only moves bytes while it has a request, and only the CPU can give it
|
||||
one. Between the completion of record *i* and the issue of record *i+1* the disc
|
||||
**stands still**, and the length of that gap is a property of the player's loop,
|
||||
not of the medium. No host-filled run could see it — the host producer placed
|
||||
records whenever it liked — so **no rate table in this tree contains it**.
|
||||
|
||||
Measured on the machine, same container, same 256 KB ring, same 488 KB/s, the
|
||||
only difference being how many requests the player may have outstanding:
|
||||
|
||||
| queue | channel idle | gaps | underruns | slack ceiling | mean slack | bound by |
|
||||
|---:|---:|---:|---:|---:|---:|---|
|
||||
| **1 request** | **669.0 ms, 6.8%** | 119 | **59/120** | 2 | 1.0 | rate |
|
||||
| **2 requests** | **317.5 ms, 3.4%** | 9 | **0/120** | 5 | 3.5 | ring |
|
||||
|
||||
The surplus this container has over the wire at 488 KB/s is 8.7% of the pipe,
|
||||
and a one-deep request loop spends 6.8% of it on nothing. **That is most of the
|
||||
surplus 51.3's lookahead is accumulated out of**, which is why the same ring at
|
||||
the same rate goes from rate-bound with a ceiling of 2 to ring-bound with a
|
||||
ceiling of 5 on a change with no bytes in it at all.
|
||||
|
||||
A second queued slot costs the 68000 nothing per frame and is available on the
|
||||
hardware: the HD63450 has four channels and the IPL programs all of them (52.1).
|
||||
|
||||
### 55.4 Prefill is the weaker lever, and now there is a number for it
|
||||
|
||||
Prefill in whole records, at 488 KB/s in a 256 KB ring, every cell pixel-exact:
|
||||
|
||||
| prefill | 1 | 2 | 3 | 4 | 6 |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| underruns, 1-deep queue | 66 | 59 | 49 | 47 | 24 |
|
||||
| underruns, 2-deep queue | 1 | **0** | **0** | **0** | **0** |
|
||||
|
||||
**A prefill buys a one-off cushion that a rate-bound pipe spends immediately; a
|
||||
queued request buys the rate back every frame.** Six records of prefill is half
|
||||
a second of black screen at the start of every scene and still leaves 24
|
||||
underruns; a second slot leaves none for nothing. The policy `ring_prefill`
|
||||
implements is therefore small — 2 records — and the reason it is not 1 is 51.2:
|
||||
*n* resident records buy *n-1* frame times, so releasing at 1 starts a scene
|
||||
with a stall budget of zero.
|
||||
|
||||
### 55.5 The slack rule is in the player now, and so is a seek
|
||||
|
||||
`ring_may_seek` answers 51.2's rule as arithmetic the player can run — "resident
|
||||
minus one, against the frames this branch will cost" — instead of a line in a
|
||||
rig's log. `ring_seek` takes a record number, waits the channel quiet (an
|
||||
outstanding transfer is bytes already on their way to an address about to be
|
||||
declared free), takes the disc address out of the index, and empties the ring.
|
||||
|
||||
Rehearsed as a second pass over the same scene: **240 records placed, the seek
|
||||
at 12.87 s, the ring refilled from empty, 0 underruns after it, and the final
|
||||
frame of the second pass pixel-exact**. The seek's cost shows up exactly where
|
||||
55.3 says it would — as the worst channel gap of the run, **397.5 ms** — and
|
||||
that is the disc idle, not a mechanical seek, which is still unmodelled (51.7.5).
|
||||
|
||||
### 55.6 An independent model, and where it does and does not agree
|
||||
|
||||
`tools/analysis/24_ring_owner.py` is the same producer written from record
|
||||
sizes and per-frame decode costs, sharing no code with the rig — the 49.4/51.5
|
||||
arrangement. It reads the DLX4 index the machine reads, and it reproduces 54.4's
|
||||
4-or-5-refresh cadence rather than averaging it away.
|
||||
|
||||
| 488 KB/s | rig Q=1 | model Q=1 | rig Q=2 | model Q=2 |
|
||||
|---|---:|---:|---:|---:|
|
||||
| channel idle | 6.8% | 8.8% | 3.4% | 5.3% |
|
||||
| slack ceiling | 2 | 3 | 5 | 6 |
|
||||
| mean slack | 1.0 | 1.1 | 3.5 | 4.2 |
|
||||
| underruns | 59/120 | 11/120 | 0/120 | 0/120 |
|
||||
|
||||
The model runs **one record ahead** of the rig, which is the same one-record
|
||||
bracket 51.5 recorded and reported rather than tuned away. The underrun count at
|
||||
Q=1 is the one number that disagrees badly, and it is a threshold statistic on a
|
||||
quantity sitting at 1: with a mean slack of one record, whether each individual
|
||||
frame's record lands before or after its tick is decided by details neither model
|
||||
has. **The agreement that matters is the resource statement** — a one-deep queue
|
||||
loses 7-9% of the pipe and two-thirds of the lookahead — and on that they agree.
|
||||
|
||||
The model's mean decode cost, 561,126 clk/frame, lands within 0.07% of the
|
||||
561,532 the ring pass measured under MAME (49.7.5), from the encoder's own
|
||||
constants.
|
||||
|
||||
### 55.7 Three bugs and one instrument correction, recorded because they were all silent
|
||||
|
||||
1. **The reader's wrap rule was not the writer's.** Stepping the read cursor
|
||||
past record *i* lands on the end of record *i*, which is where record *i+1*
|
||||
went only if it FITTED there. Using the wrong record's length left the cursor
|
||||
inside the hole, and one more retirement pushed it past the end of the ring
|
||||
and wrapped it to an address unrelated to any record. The live span computed
|
||||
from that is *shorter* than the truth, so the producer places over a record
|
||||
the decoder has not read. Symptom: a bitstream desync, not a fault.
|
||||
2. **The free-space test decided the wrap before it knew the shape.** When the
|
||||
LIVE span is the one that wraps, the ring base is not free and `aligned` may
|
||||
not restart there. Deciding from `WCUR + len > SZ` alone overwrote live
|
||||
records. Same symptom.
|
||||
3. **The queue was gated on completion instead of retirement.** A slot stays in
|
||||
use until the descriptor has been read out of it, which happens one poll after
|
||||
the ack at the earliest. Gating on the ack let the CPU overwrite a slot whose
|
||||
descriptor had not been published; `DESC` for that frame stayed zero and the
|
||||
decoder decoded address zero. This one only exists at a queue depth above 1,
|
||||
and it is why the two-deep result took three attempts to obtain.
|
||||
4. **The rig was capturing a torn screen.** MAME renders a screen line by line
|
||||
and the machine-frame notifier fires at the END of that frame, so a bitmap for
|
||||
a frame in which GVRAM changed holds lines from before and after the change.
|
||||
Snapshotting it captures a tear, which reads as a pixel-exactness failure in
|
||||
the bottom blocks plus a broken double-scan pairing. It only bites when the
|
||||
decoder finishes its last frame late in a screen frame, so it appeared for the
|
||||
first time in a run with underruns. The rig now waits one whole frame before
|
||||
the capture. **No previously reported result is affected** — every one of them
|
||||
finished its last frame with idle to spare — but the check.sh gates would have
|
||||
been flaky under any future run that did not.
|
||||
|
||||
An instrument note, not a bug: under the self-clock the host's "records late"
|
||||
report grades arrivals against the tick times it OBSERVED, which are up to
|
||||
17.64 ms late (54.5), so it understates lateness — 6 records late where the
|
||||
68000 itself counted 59 frames that had to wait. The decoder's own stall counter
|
||||
is the sharp instrument. Host-paced runs take their deadlines from a host model
|
||||
and are exact, so 49.6's table is unaffected.
|
||||
|
||||
### 55.8 What this does NOT establish
|
||||
|
||||
1. **The transport is still a model.** It delivers at a chosen byte rate with an
|
||||
exact clock; it is not an MB89352. No command overhead, no arbitration, no
|
||||
sector granularity, no mechanical seek. `W` — the clocks the DMAC steals per
|
||||
delivered byte — is still undecided and still unmeasurable here (P4, 52.5).
|
||||
**A zero-underrun result means the bytes were in time, not that the frames
|
||||
fit.**
|
||||
2. **The rates are chosen inputs.** FINDINGS 50 stands: every column is a
|
||||
sensitivity, not a claim about a BlueSCSI.
|
||||
3. **One container, one scene, one ring size.** The ceilings are in whole
|
||||
records and move with record size (51.7.3).
|
||||
4. **The seek is a rewind, not a branch.** It exercises the machinery — quiet
|
||||
the channel, empty the ring, address record *j* out of the index, refill —
|
||||
against a container that has one scene in it. What the worst gap between two
|
||||
real decision points is still needs the scene graph (ROADMAP G1).
|
||||
5. **`decode.s` and `frame.i` are unchanged** and `decode.bin` is still 1,296 B
|
||||
at the same MD5. `stream.s` grew to 2,814 B: the ring producer, plus a
|
||||
two-instruction test at the top of the pace wait that routes a self-filled
|
||||
run into a polling wait loop. The legacy wait loops are byte for byte the ones
|
||||
FINDINGS 51 measured and a host-filled run executes none of the new code.
|
||||
|
||||
Reference in New Issue
Block a user