Put the transport on the 68000, and find PIO costs 87 clocks a byte

ROADMAP P4b. src/player/xfer.i answers src/player/ring.i's XF_* mailbox with a
real READ(10) to a real MB89352 in place of tools/bench/stream.lua's modelled
transport: 120 records, 4,488,588 B, pixel-exact out of a 256 KB ring, with a
real mid-stream seek in a second pass. The tiling is the SAME 18 wraps and
14.7 KB mean hole that 49.4's host producer and 55.4's modelled transport
produced -- a third transport, same placement, which is the assertion that
ring.i could not tell which side of the seam answered it.

What it costs is the finding. tools/bench/xfer_cost.sh subtracts the same 120
frames run twice and gets 87.28 clocks per delivered byte, against the 68000's
own cycle table for the loop, which says 87.15 -- 0.2% apart, so the cost is the
instruction stream and not MAME's device model, and it is the first number this
rig has produced that survives leaving the emulator. That is 391.8% of a 12 fps
frame; the machine's own V-DISP clock agrees from the other end at 2.57 fps.
Against the ladder, W=5 held is 22.4% of a frame and W=19 is 85.3%, so P4a is
worth 4.6x the worst DMA configuration in this tree and 17.5x the best -- where
before this session it was worth 9 against 19. W itself did not move by a clock.

"UNDERRUNS: 0/120" is vacuous with a synchronous transport, and stream.lua now
prints that argument next to the zero: a frame cannot start before its record
has landed because the decoder IS the transport. The counter that means
something is NO IDLE, 119/120 with a worst overrun of 441 whole ticks. Same
class of error as 49.7.2's free-running ring passing at 48 KB.

58.3: a record is not a sector -- 117 of 120 start part way into one, and
reading whole sectors into the ring corrupts the neighbours rather than wasting
bytes (49.2, no bounds check). scsi.i reads the covering sectors and stores only
the window, which is free in PIO and stops being free the moment P4a succeeds.
tools/analysis/26_sector_align.py prices the three ways out and sector-aligned
records win on both axes: +0.43% wire and zero clocks, against +1.34% and a
bounce copy at +5 clk/B. ROADMAP now carries a four-item re-encode bundle and
P4a should be attempted against a sector-aligned container.

check.sh gains two stages and was ALL GREEN before and after. decode.bin is
unchanged at 1,296 B and the same MD5.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-24 23:15:53 -07:00
parent e935d8661c
commit 5921fab118
15 changed files with 1221 additions and 48 deletions
+157
View File
@@ -5382,3 +5382,160 @@ watching that address.
**Does move the premise of every delivery rig in the tree.** Until now the
bytes came from a host. They now come off a disc, on the machine's own
instructions, byte-exact at two different LBAs.
---
## FINDINGS 58 — the player runs off the disc, and PIO costs 87 clocks a byte (session 25b/26)
**Emulated.** MAME 0.277, `x68000 -exp1 cz6bs1 -ramsize 2M`, a blank
`scsiexrom.bin` on a private rompath (57.1's substitution, unchanged). No real
hardware ran. `./tools/bench/check.sh` was ALL GREEN before this and ALL GREEN
after, with two new stages.
**ROADMAP P4b is DONE. P4a is not, and 58.2 is why it is now the item that
decides the project rather than one of two that do.**
### 58.1 The seam closed: 120 pixel-exact frames, off a real volume
`src/player/xfer.i` sits behind `src/player/ring.i`'s `XF_*` mailbox in place of
`tools/bench/stream.lua`'s modelled transport. `XF_GO` is answered by a real
READ(10) to a real MB89352 and `XF_ACK` is a word the 68000 bumps when the bytes
have landed — not one a host synthesises from emulated time.
| | |
|---|---|
| records fetched by the 68000 | **120**, one READ(10) each |
| bytes into the ring | **4,488,588**, and the decode is **pixel-exact** |
| bytes off the disc | **4,548,608** — see 58.3 |
| ring | 256 KB, **18 wraps, 14.7 KB mean hole** |
| a real mid-stream seek | **pass 2 pixel-exact**, ring thrown away and rebuilt |
**The 18 wraps are the load-bearing assertion, not the byte count.** They are
the same 18 the host producer produced in 49.4 and the same 18 `ring.i`
produced against a modelled transport in 55.4 — a *third* transport, same
tiling. `ring.i` is not supposed to be able to tell which side of the mailbox
answered it, and this is the number that says it could not.
**The change above the seam is two `bsr`s.** One in `ring_poll`, one in
`ring_seek`'s quiet-wait — and the second is not optional. With the transport
*inside* the machine, the only thing that can retire an outstanding request is
that wait loop itself, so a seek issued with a request in flight spins forever
without it. A host transport retired it on its own time. That is exactly the
kind of difference the seam exists to hide, and it is the one it could not.
### 58.2 What it costs: 87.28 clocks per delivered byte, and the number is portable
`tools/bench/xfer_cost.sh` decodes the same 120 frames twice — same ring, same
`stream.s`, same `ring.i` placing every record — and changes only which side of
the mailbox answers:
| | emulated | per frame |
|---|---|---|
| decode + `ring_poll` alone | 6.7737 s | **67.7%** of a 12 fps frame |
| ...with the real transport | 45.9516 s | **459.5%** |
| **the transport** | **391,779,000 clk** | **391.8%** |
**= 87.28 clocks per delivered byte**, 86.13 per byte off the FIFO.
**AND IT IS NOT MAME'S NUMBER, WHICH IS THE HALF THAT MATTERS.** The keep loop
in `src/player/scsi.i`, priced against the 68000's own cycle table — 12 patience
reload, 16 SSTS read, 10 `btst`, 10 branch, 20 DREG read to `(a1)+`, 8 `subq`,
10 branch — is **86 clocks**, and the FIFO also carries the dropped window bytes
of 58.3, which makes it **87.15 per delivered byte**. Measured 87.28. **0.2%
apart.** So the cost is the instruction stream and not a wait on MAME's SPC
model: it is a figure a real board would also pay, and it is the first number
this rig has produced that survives leaving the emulator. The residual, +0.13
clk/B = 4,989 clocks per record, is the per-*command* cost — select, CDB,
status, message, `xf_service` — and it is the part that does not scale with the
record.
**Against the ladder, in the same units** (clocks charged to the CPU per
delivered byte, at this container's 37,405 B mean record):
| | | share of a 12 fps frame |
|---|---|---|
| `W` = 5 | single address, bus HELD | 22.4% |
| `W` = 9 | dual address, held | 40.4% |
| `W` = 12 | single address, arbitrated | 53.9% |
| `W` = 19 | dual address, arbitrated — the IPL ROM's own disk channel (52.5) | 85.3% |
| **PIO 87** | **this rig, measured** | **391.8%** |
**The PIO transport is 4.6x the worst DMA configuration this project has found
and 17.5x the best.** P4a is not an optimisation of this. It is the difference
between a player and a slideshow, and it is now the *only* thing between the
tree and M2.
**The player's own clock says the same thing, independently.** Self-paced off
V-DISP, the machine decoded 120 frames in **560 slots** of a 12 fps clock:
**2.57 fps**. That agrees with 12 / 4.595 = 2.61 from the cycle accounting
above, from a completely different instrument.
**AND "UNDERRUNS: 0/120" IS VACUOUS IN THIS RUN.** A synchronous transport
cannot underrun by construction — a frame cannot start before its record has
landed, because the decoder *is* the transport. The counter that means anything
here is `NO IDLE`: **119 of 120 frames found their slot already open, worst
overrun 441 whole ticks.** `tools/bench/stream.lua` now prints that argument
next to the zero rather than leaving the zero to be quoted. This is the same
class of error as 49.7.2's free-running ring passing at 48 KB: a rig
configuration in which the failure it tests for cannot occur.
**The resource that binds also flipped, and it is worth naming.** Against a
modelled 488 KB/s pipe this container was rate-bound and the ring never filled
(55.4). Here the ring **fills — 90 refusals for space** — while the decoder
starves. Not of bytes: of *time*. Every previous delivery result in this tree
was measured on a rig where the transport cost the CPU nothing.
### 58.3 A record is not a sector, and the cheapest fix is a re-encode
`ring.i` asks for a byte offset and a length, both 4-byte aligned (28.3);
a target answers in 512 B blocks. **On the gate container 117 of 120 records
start part way into a sector.** This is not a rounding nuisance: the bytes on
either side of a record belong to *other* records the decoder may still be
reading, and the block loop walks `a0` with no bounds check (49.2), so a
transport that reads whole sectors straight into the ring **corrupts its
neighbours** — wrong pixels, not a fault.
`tools/analysis/26_sector_align.py` prices the three ways out:
| | wire | clocks |
|---|---|---|
| **A. windowed PIO** — read the covering sectors, store only the record | **+1.34%** | 86/B on every byte off the FIFO, and **a DMAC cannot do it at all** |
| **B. bounce buffer** — DMA whole sectors elsewhere, then copy | +1.34% | **+5/B on every delivered byte** = 22.4% of the frame, on top of `W` |
| **C. sector-aligned records** — pad to 512 in the container | **+0.43%** | **zero** |
A is what shipped in `scsi.i` and what 58.1 ran, and it is free *only* because
the CPU is already touching every byte — the property that disappears the moment
P4a succeeds. B is the cost `aligned` was chosen over `split` to avoid (49.3)
arriving by a different door, and on every byte instead of on a wrap.
**C wins on both axes**: it is cheaper on the wire than A and B by 0.91 points
of the payload (40,940 B on this scene), and it is the only one of the three a
DMA channel can run without a copy. What it costs is a **container revision**
a re-encode plus a re-measurement of every constant fitted to the gate
container. That is the class of change ROADMAP already has bundled with P2's
other half (reserve index 0 as black) and 55's two open re-encode questions.
**It should join that bundle**, and P4a should be attempted against a
sector-aligned container rather than against this one.
It also grows the largest record from 40,984 to 41,472 B, which a 256 KB ring
still holds six times over — so it costs nothing in ring size.
### 58.4 What this does and does not move
**Does not move `W`.** Again, and for the same reason 57.6 gives. Nothing here
programmed a DMAC channel; `xf_service` is the CPU standing in for one, through
the DMAC's own door (57.3).
**Does not measure a delivery rate,** and the rig now refuses to be asked. A
`DLX_XFER=scsi` run rejects a `--kbps` argument outright rather than ignoring
it, and `stream.lua` suppresses `CHANNEL IDLE`, `DEADLINE` and `REQUIRED
PREFILL` instead of printing them as zeros — a zero there reads as "the channel
never stopped", which would be a claim about a medium this tree has never timed.
**Does close P4b, and does hand P4a a sharper question than it had.** Before
this session, "get the DMAC to hold the bus" was worth 9 clk/B against 19. It is
now worth 87 against either.
**Does put a real number on the layer below every previous delivery result.**
Everything in FINDINGS 49, 51 and 55 was measured with the bytes arriving free.
They are not free, and 58.2 is the first measurement of what they cost.
+65 -15
View File
@@ -6,6 +6,8 @@ Amended end of session 22: P3 done (FINDINGS 54).
Amended end of session 23: P5 done (FINDINGS 55).
Amended end of session 24: G1 done (FINDINGS 56).
Amended end of session 25: P4 HALF done (FINDINGS 57).
Amended end of session 26: P4b done, P4a is the last open item before M2
(FINDINGS 58).
**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
@@ -124,7 +126,18 @@ the words read back **out of the palette registers**. 9.70 ms per scene, plus
**What is left is the other half of the sentence: reserve index 0 as black with
`I = 0` (23.4).** That is ENCODER-side, it changes the container, and it moves
every constant fitted to the gate container, so it is a re-encode plus a
re-measurement rather than an edit. Until then the letterbox gets the palette's
re-measurement rather than an edit.
**THE RE-ENCODE BUNDLE, collected here because it is now four items and they
share one re-measurement.** Nothing below is worth a container revision on its
own; together they are one:
1. reserve palette index 0 as black, `I = 0` (23.4, this item);
2. `--spans all` as the default (E2, and it is the loaded lever on the byte
side);
3. re-derive span selection jointly with `lam` (E3);
4. **sector-align every record (58.3, new in session 26)** — +0.43% on the
wire, zero clocks, and it is what lets P4a's DMA channel write straight
into the ring with no window and no bounce copy. Until then the letterbox gets the palette's
closest thing to black (index 255 on the gate container); `load.i` reports
whichever index that is and needs no change when it becomes 0.
@@ -160,7 +173,8 @@ period from `htotal - 8`), so the tree's "1/55.46 s granularity" was 1/56.69 s
throughout. No 68000 cycle figure moves — the CPU clock is unrelated to the
screen — but anything paced by the raster does. 54.5.
**P4. Real transport. HALF DONE, session 25 — FINDINGS 57.**
**P4. Real transport. P4b DONE, session 26 — FINDINGS 58. P4a OPEN, and it is
now the ONLY thing between this tree and M2.**
~~Drive the MB89352 instead of a host file.~~ `src/player/scsi.i` selects a SCSI
target and issues READ(10) on the 68000, with no IOCS and no host in the
transfer path: **4,096 B from LBA 0 and 2,048 B from LBA 1000, both byte-exact**
@@ -184,12 +198,48 @@ cannot then distinguish a CPU-driven byte from a DMAC-driven one at that
address**. "The DMAC held the bus" needs evidence that does not come from
watching `$EA0015`.
**P4b. `scsi.i` behind `ring.i`'s `XF_*` mailbox**, in place of `stream.lua`'s
modelled transport, gated on the same pixel-exact 120 frames. The seam already
exists: `XF_ACK` stops being a word a host synthesises from emulated time and
becomes one the 68000 bumps when a transfer lands.
~~**P4b. `scsi.i` behind `ring.i`'s `XF_*` mailbox.**~~ **DONE, session 26 —
FINDINGS 58.** `src/player/xfer.i` answers the mailbox with a real READ(10) per
record: **120 records, 4,488,588 B, pixel-exact, out of the same 256 KB ring,
with a real mid-stream seek in a second pass**. The tiling is the SAME 18 wraps
and 14.7 KB mean hole that 49.4's host producer and 55.4's modelled transport
produced — a third transport, same placement, which is the assertion that
`ring.i` could not tell which side of the seam answered it. The change above the
seam is two `bsr`s, and the one in `ring_seek`'s quiet-wait is not optional:
with the transport inside the machine, that loop is the only thing that can
retire an outstanding request.
`W` **did not move by one clock** and is still the largest open number here.
**What it cost is the finding, and it re-prices P4a.** `tools/bench/
xfer_cost.sh` subtracts the same 120 frames run twice and gets **87.28 clocks
per delivered byte** — against the 68000's own cycle table for the loop, which
says **87.15**. **0.2% apart**, so it is the instruction stream and not MAME's
device model, and it is therefore the first number this rig has produced that
survives leaving the emulator. At this container's 37,405 B mean record that is
**391.8% of a 12 fps frame**, and the machine's own V-DISP clock agrees from the
other end: **2.57 fps**.
W = 5 single address, bus HELD ............................ 22.4%
W = 9 dual address, held .................................. 40.4%
W = 12 single address, arbitrated .......................... 53.9%
W = 19 dual address, arbitrated -- the IPL ROM's own (52.5) . 85.3%
PIO 87 MEASURED, session 26 ................................ 391.8%
**So P4a is worth 4.6x the worst DMA configuration in this tree and 17.5x the
best**, where before this session it was worth 9 against 19. `W` itself **did
not move by one clock** and is still the largest open number — but what depends
on it just got much larger.
**One more thing P4a inherits (58.3).** A record is not a sector: 117 of 120
start part way into one. PIO absorbs that for free because the CPU is already
touching every byte and simply does not store the ones outside the window — a
property that **disappears the moment the DMAC takes over**, because a channel
writes a contiguous run and cannot drop bytes. The three ways out price as
+1.34% wire and no DMA (windowed PIO), +1.34% wire and **+5 clk/B of copy**
(bounce buffer, which is exactly the cost `aligned` was chosen over `split` to
avoid), or **+0.43% wire and zero clocks** (sector-aligned records in the
container). The last one wins on both axes and is a **re-encode**; see the
bundle under P2. **P4a should be attempted against a sector-aligned container,
not against this one.**
*(original item, still the standing description of the `W` question:)*
Drive the MB89352 instead of a host file. **Session 23
@@ -328,12 +378,12 @@ Listed for completeness; past M3 these are scope, not risk.
**Gated by E4.**
- **E4. `H.build` k-means**, 51 s of a 55 s run, once per scene. The thing to
attack before C1, and not anything in the per-frame path (27.6).
- **E2. `--spans all` as default.** Still a recommendation, not a measurement
- **E2. `--spans all` as default.** *(re-encode bundle item 2; see P2.)* Still a recommendation, not a measurement
(43.6.1), and the only loaded lever on the encoder's byte side (44.3). **It
spends every profitable byte, which raises `wire`, which shrinks `pipe - wire`,
which lengthens the refill climb after every branch.** That interaction is not
priced, and M3 is where it becomes measurable.
- **E3. Re-derive span selection jointly with `lam`** (39.3).
- **E3. Re-derive span selection jointly with `lam`** (39.3). *(bundle item 3.)*
- **C2. Framing** — crop vs squash vs wide (FINDINGS 12). Needs an eyeball
against arcade reference, not a measurement. Cheap; blocks only final encodes.
- **C3. Disk image packaging**, ~1.09 GiB at the candidate rate.
@@ -345,12 +395,12 @@ Listed for completeness; past M3 these are scope, not risk.
```
B1 seek+rate ─┐
B3 DTYP ──────┴─> P4 transport ─┐
├─> M2 ─> M3 (COMPLETION TARGET) ─> M4
P1 P2(half) P3 P5 P7 ─────────── ^
P6 (bus cost DONE, 52) ──────────────────┤
G1 scene graph (DONE, 56) ───────────────┘
B3 DTYP ──────┴─> P4a DMA HOLDS THE BUS ──┐ (P4b DONE, 58: the ring is
│ filled off a real volume, and
P1 P2(half) P3 P4b P5 P7 ─────────────────┤ PIO costs 87 clk/B)
├─> M2 ─> M3 (TARGET) ─> M4
P6 (bus cost DONE, 52) ──────────────────
G1 scene graph (DONE, 56) ───────────────
B2 blanking ─> (page 1; do not pre-build on it)
```
+117
View File
@@ -1,3 +1,120 @@
# Status & next-session handoff — end of session 26 (2026-08-24)
## Session 26: the player runs off the disc, and PIO costs 87 clocks a byte
**Green light first and last: `./tools/bench/check.sh` was ALL GREEN before any
of this and ALL GREEN after**, plus two new stages.
**ROADMAP P4b is DONE. P4a is the only item left before M2. FINDINGS 58.**
**Emulated** — MAME 0.277, `x68000 -exp1 cz6bs1 -ramsize 2M`. No real hardware
ran.
**1. THE SEAM IS CLOSED.** `src/player/xfer.i` sits behind `src/player/ring.i`'s
`XF_*` mailbox in place of `tools/bench/stream.lua`'s modelled transport. `XF_GO`
is answered by a real READ(10) to a real MB89352; `XF_ACK` is a word the 68000
bumps when the bytes have landed, not one a host synthesises from emulated time.
| | |
|---|---|
| 120 records fetched by the 68000 | one READ(10) each |
| 4,488,588 B into a 256 KB ring | **pixel-exact** over all 120 frames |
| a real mid-stream seek, pass 2 | **pixel-exact** after it too |
| 18 wraps, 14.7 KB mean hole | the SAME tiling as 49.4 and 55.4 |
The 18 wraps are the load-bearing assertion. A host producer made them in
session 18, `ring.i` made them against a modelled transport in session 23, and a
real SPC makes them now — **`ring.i` cannot tell which side of the mailbox
answered it**, which is what makes this a test of the transport rather than of a
new rig. 58.1.
**The change above the seam is two `bsr`s**, and the one in `ring_seek`'s
quiet-wait is not optional: with the transport *inside* the machine, that loop
is the only thing that can retire an outstanding request, so without it a seek
issued with a request in flight spins forever. A host transport retired it on
its own time. That is exactly the kind of difference the seam exists to hide,
and it is the one it could not.
**2. WHAT IT COSTS, AND THE NUMBER LEAVES THE EMULATOR.** `tools/bench/
xfer_cost.sh` runs the same 120 frames twice — same ring, same `stream.s`, same
`ring.i` placing every record — and changes only which side answers:
| | emulated | per frame |
|---|---|---|
| decode + `ring_poll` alone | 6.7737 s | 67.7% of a 12 fps frame |
| ...with the real transport | 45.9516 s | **459.5%** |
| **the transport alone** | **391,779,000 clk** | **391.8%** |
**87.28 clocks per delivered byte.** The keep loop in `scsi.i`, priced against
the **68000's own cycle table**, is 86 clocks, and the FIFO also carries the
dropped window bytes of 58.3 — **87.15 predicted, 87.28 measured, 0.2% apart.**
So this is the instruction stream and **not** MAME's SPC model: **the first
number this rig has produced that a real board would also pay.** The machine's
own V-DISP clock agrees from the other end — 120 frames in 560 slots of a 12 fps
clock is **2.57 fps**. 58.2.
**Against the ladder** (clocks charged to the CPU per delivered byte):
`W`=5 held is 22.4% of a frame, `W`=9 is 40.4%, `W`=19 — the IPL ROM's own disk
channel, 52.5 — is 85.3%, and **PIO is 391.8%**. **P4a is worth 4.6x the worst
DMA configuration in this tree and 17.5x the best**, where before this session
it was worth 9 against 19. `W` itself did not move by one clock.
**3. "UNDERRUNS: 0/120" IS VACUOUS HERE, and the rig now says so in place.** A
synchronous transport cannot underrun by construction — a frame cannot start
before its record has landed, because the decoder *is* the transport. The
counter that means something is `NO IDLE`: **119 of 120 frames found their slot
already open, worst overrun 441 whole ticks.** Same class of error as 49.7.2's
free-running ring passing at 48 KB: a rig configuration in which the failure
being tested for cannot occur. **The binding resource flipped too** — against a
modelled 488 KB/s pipe this container was rate-bound and the ring never filled;
here the ring **fills, 90 refusals for space**, while the decoder starves for
*time*.
**4. A RECORD IS NOT A SECTOR, and the cheapest fix is a re-encode.** 117 of 120
records start part way into a sector. Reading whole sectors into the ring
**corrupts the neighbouring records** — the block loop has no bounds check
(49.2) — so `scsi.i` reads the covering sectors and stores only the window.
That is free in PIO because the CPU is already touching every byte, **and it
stops being free the moment P4a succeeds**: a channel writes a contiguous run
and cannot drop bytes. `tools/analysis/26_sector_align.py` prices the three ways
out: windowed PIO **+1.34% wire, no DMA**; bounce buffer **+1.34% wire and
+5 clk/B of copy** (22.4% of the frame — the cost `aligned` was chosen over
`split` to avoid, arriving by a different door); **sector-aligned records
+0.43% wire and zero clocks.** The last wins on both axes and is a container
change. **ROADMAP now carries a four-item re-encode bundle** (index-0 black,
`--spans all`, joint `lam`, sector alignment) and **P4a should be attempted
against a sector-aligned container, not against this one.** 58.3.
**5. The rig refuses to be asked for a rate.** `DLX_XFER=scsi` rejects a
non-zero `--kbps` outright rather than ignoring it, and `stream.lua` suppresses
`CHANNEL IDLE`, `DEADLINE` and `REQUIRED PREFILL` rather than printing them as
zeros — a zero there reads as "the channel never stopped", which would be a
claim about a medium this tree has never timed.
**One rig bug found and fixed in the same sitting:** the host's `nsent` is now
rebased at every seek. `XS_NXFER` is cumulative, so on a second pass it was
already >= `nframes` and the slack sampling never fired — the second pass
reported a ceiling of 0 frames and a build time of **-1 ticks**, which is an
empty series printing as a result.
**New in the tree:** `src/player/xfer.i`, `tools/bench/mkvol.sh` (one copy of
the volume layout, shared by both SCSI rigs), `tools/bench/xfer_cost.sh`,
`tools/analysis/26_sector_align.py`, `DLX_XFER=scsi` and `DLX_SECONDS` in
`pace_run.sh`, and two `check.sh` stages. `src/player/scsi.i` gained
`sc_in_data` (the three-loop windowed receive) and `scsi_read_win`; `ring.i`
gained the two transport hooks; `stream.s` includes `scsi.i` and `xfer.i`.
**No decoder code changed.** `decode.bin` is still 1,296 B at the same MD5.
**Next: P4a, and it is now the whole of what is left before M2.** A DMAC
configuration that HOLDS THE BUS, shown by evidence that does not come from
watching `$EA0015` (57.3: with `exown` asserted, MAME cannot tell a CPU-driven
byte there from a DMAC-driven one). Do it against a sector-aligned container if
the re-encode bundle has been run by then, because 58.3 is otherwise a bounce
copy at +5 clk/B. **P2's remaining half** is unchanged and is now item 1 of
that bundle.
---
# Status & next-session handoff — end of session 25 (2026-08-24)
## Session 25: the 68000 reads the disc itself, and P4 was never blocked