From 621a5bb4579e865880b055bbffb54cfde71fbb7f Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:55:07 -0700 Subject: [PATCH] Put the data phase on the DMAC, and find auto-request is charged by time ROADMAP P4a. src/player/dma.i programs HD63450 channel 1 and takes the SCSI DATA IN phase off the CPU; src/player/dmagate.s reads the same 2,048 B at LBA 1000 three ways -- PIO, the channel with the bus held, the channel stealing cycles -- and all three are byte-exact against the host's copy of the volume. The evidence never reads $EA0015, because 57.3 established that it cannot: with the DMAC's OWN asserted MAME cannot tell a CPU-driven byte there from a DMAC-driven one. The discriminator is the CPU's own progress. MTC is sampled by the INSTRUCTION AFTER the one that starts the channel, and held it reads 0 of 2,048 -- the whole transfer happened between two instructions, because the 68000 did not execute in between -- against the full count and 426 loop trips for the stealing configuration. Put the stealing registers in the held slot and every byte still arrives and tools/bench/dma_run.sh goes RED, which is what says the counter can come out different; 58.3's vacuous "UNDERRUNS: 0/120" is the trap being avoided. tools/analysis/27_dmac_config.py decodes the four register bytes out of the player's own source, with the MC68450 field tables now in one copy (tools/analysis/mc68450.py) shared with 21_iplrom_dmac.py, so the player's configuration and the IPL ROM's 16..19 clk/B one are the same decoding. Three bounds on the apparatus, read out of MAME 0.277 rather than inferred: the CZ-6BS1 has NO request line to the DMAC (its flow control is DTACK), so external request cannot be run; single address cannot be run either, because only channel 0 has device callbacks; and only burst is modelled as held. Of the four rows of the W ladder exactly one -- dual address held -- has a code path here, and it is the one demonstrated. W did not move by one clock, for the third session running. What outlives the emulator is the currency. Every W in this project is clocks per DELIVERED byte, which presumes the device asks; an auto-requested channel spends its share of the bus whether or not a byte is there, so a record costs what it costs to ARRIVE -- halve the delivery rate and the CPU cost of the same record doubles. tools/analysis/28_autorequest_cost.py prices it from MC68450 3.8 and 5.2.3.3.2, gating its formulas against Table 5-3's sixteen rows first. At 37,405 B and an explicit 460 KB/s: max rate costs the whole 95.3% of a frame the record takes to land, and of the GCR's four bus shares only BR=00, 50%, carries the rate -- 10.61 clk/B, 47.6% of a frame, against 40.4% for the W=9 row and 391.8% measured for PIO. The GCR is a design lever nothing in this tree had named. 59.4 changes what is left. sc_in_data now REFUSES a windowed read when the data phase is the channel's (SCE_WINDOW), because a channel writes a contiguous run and cannot drop the 300 B in front of a record. 117 of 120 records need one, so sector-aligned records have gone from a preference in ROADMAP's re-encode bundle to the precondition the transport enforces -- and that bundle is now the only thing between this tree and M2. One collision, recorded because the procedure is the finding: DM_USE first sat at $18300, which is ring.i's XF_SLOT mailbox, and the P4b stage -- untouched by this work -- went red on a run that never reached its snapshot. check.sh was ALL GREEN before any of this, which is what made that red unambiguous. ALL GREEN after too, with one new stage. decode.bin is unchanged at 1,296 B and the same MD5. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 --- README.md | 34 ++++- docs/FINDINGS.md | 192 +++++++++++++++++++++++ docs/ROADMAP.md | 67 ++++++-- docs/STATUS.md | 86 +++++++++++ src/player/dma.i | 211 ++++++++++++++++++++++++++ src/player/dmagate.s | 132 ++++++++++++++++ src/player/scsi.i | 30 ++++ tools/analysis/21_iplrom_dmac.py | 46 +----- tools/analysis/27_dmac_config.py | 111 ++++++++++++++ tools/analysis/28_autorequest_cost.py | 172 +++++++++++++++++++++ tools/analysis/mc68450.py | 59 +++++++ tools/bench/check.sh | 44 ++++++ tools/bench/dma.lua | 131 ++++++++++++++++ tools/bench/dma_run.sh | 85 +++++++++++ 14 files changed, 1342 insertions(+), 58 deletions(-) create mode 100644 src/player/dma.i create mode 100644 src/player/dmagate.s create mode 100644 tools/analysis/27_dmac_config.py create mode 100644 tools/analysis/28_autorequest_cost.py create mode 100644 tools/analysis/mc68450.py create mode 100644 tools/bench/dma.lua create mode 100755 tools/bench/dma_run.sh diff --git a/README.md b/README.md index 1960ddf..f8e0e1e 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,35 @@ configuration this project has found and 17.5x the best.** Getting the DMAC to hold the bus is no longer worth 9 against 19; it is worth 87 against either, and it is the only thing left before a player (FINDINGS 58). +**The DMAC drives the data phase now, and it holds the bus.** `src/player/dma.i` +programs an HD63450 channel and hands it the SCSI data phase: **the same 2,048 +bytes come off the disc three ways — PIO, the channel with the bus held, the +channel stealing cycles — and all three are byte-exact.** The evidence that the +DMAC and not the CPU is driving it never looks at the data register, which +cannot answer the question: with the DMAC's OWN asserted, MAME cannot tell a +CPU-driven byte at `$EA0015` from a DMAC-driven one. What it looks at instead is +**the CPU's own progress**. MTC is sampled by the instruction *after* the one +that starts the channel; held, it reads **zero of 2,048** — the whole transfer +happened between two instructions, because the 68000 did not execute in between +— while the stealing configuration reads the full count and the CPU then goes +round its own loop 426 times. Put the stealing registers in the held slot and +the run still delivers every byte and the gate goes **red**, which is what says +the counter can come out different (FINDINGS 59.1). + +**And auto-request is charged by time, not by byte.** The card as MAME models it +has **no request line to the DMAC at all** — its flow control is DTACK — so +every configuration that can be run against it is auto-request, and an +auto-requested channel does not know whether the device is ready: it spends its +share of the bus either way. Every `W` in this project is clocks per *delivered* +byte, which presumes the device asks; here the cost scales with **how long the +record takes to arrive**, so halving the delivery rate *doubles* the CPU cost of +the same record. Priced from the MC68450's own limited-rate constants against an +explicit 460 KB/s: max rate costs the whole **95.3% of a frame** the record takes +to land, and of the four bus shares the GCR can be programmed for — 50, 25, +12.5, 6.25% — **only 50% carries the rate**, at 10.61 clocks a byte and 47.6% of +a frame. The GCR is a design lever nothing in this tree had named (FINDINGS +59.3). + **A record is not a sector, and the cheapest fix is a re-encode.** 117 of 120 records start part way into a 512 B block, and reading whole blocks into the ring corrupts the neighbouring records rather than merely wasting bytes — the @@ -152,7 +181,10 @@ moment a DMA channel takes over. Priced three ways: windowed PIO is +1.34% on the wire and cannot be done by a channel at all; a bounce buffer is +1.34% and **+5 clocks on every delivered byte**, 22.4% of a frame; sector-aligning records in the container is **+0.43% and zero clocks**. The last wins on both axes and -joins the re-encode bundle (FINDINGS 58.3). +joins the re-encode bundle (FINDINGS 58.3). **Session 27 made it a +precondition rather than a preference**: the transport now *refuses* a windowed +read when the data phase is the channel's, so the container has to meet it +before the DMAC can sit behind the ring (FINDINGS 59.4). **The scene graph is in, and the worst gap between two decision points is zero.** `tools/import/scenegraph.py` imports the arcade scene graph — 40 scenes, diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md index ad78a3f..61dbe36 100644 --- a/docs/FINDINGS.md +++ b/docs/FINDINGS.md @@ -5539,3 +5539,195 @@ 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. + +--- + +## FINDINGS 59 — the DMAC drives the data phase, and auto-request is charged by time (session 27) + +**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 one new stage. + +**ROADMAP P4a is DONE at the transport level.** What is left before M2 is not a +DMAC question any more; it is the re-encode bundle, because 59.4 makes +sector-aligned records a precondition the transport now enforces rather than a +preference the roadmap recorded. + +### 59.1 The channel drives the data phase, and the evidence is the CPU's own progress + +`src/player/dma.i` programs HD63450 channel 1 and hands it the DATA IN phase; +`src/player/dmagate.s` reads **the same 2,048 B at LBA 1000 three ways** and the +host compares all three against its own copy of the image: + +| | bytes | MTC one instruction after START | CPU trips round its wait loop | +|---|---|---|---| +| PIO, the path 58.2 measured | byte-exact | — | — | +| **DMA, bus HELD** (DCR `$00`, OCR `$81`) | **byte-exact** | **0 of 2048** | **1** | +| **DMA, cycle STEALING** (DCR `$80`, OCR `$80`) | **byte-exact** | 2048 of 2048 | 426 | + +Both channels reported `CSR = $E0` (COC, BTC, NDT), `CER = $00`, `MTC = 0` and a +memory address exactly `+2048` from where it started. + +**THE DISCRIMINATOR NEVER READS `$EA0015`, and that is the whole design.** 57.3 +established that watching the data register cannot answer this question: with +the DMAC's `OWN` asserted — which it is at idle on this machine — MAME cannot +tell a CPU-driven byte there from a DMAC-driven one. So what separates the two +configurations is **whether the 68000 executed anything while the bytes were +arriving**: + + move.b #CCR_START,DM_CCR ; the channel is told to go + move.w DM_MTC,d0 ; <- sampled by the VERY NEXT instruction + +Held, `d0` is **zero**: the entire 2,048-byte transfer happened between two +instructions, because the CPU did not run in between. Stealing, `d0` is the full +count and the CPU then goes round its own loop 426 times while the bytes +trickle in. That is what "holds the bus" means, and it is a fact about the CPU +rather than about the data register. + +**The mechanism, named so the claim is not over-read.** MAME models a held bus +by asserting `INPUT_LINE_HALT` for burst + max-rate and clearing it at +end-of-transfer (`hd63450.cpp`). It is not inventing that semantic: MC68450 +§5.2.3.3.1 says of maximum-rate auto-request that "all operands in the data +block will be transferred in one burst, so that the DMAC will use **100% of the +available bus bandwidth**". The model and the datasheet agree about what this +configuration does to the CPU. **They do not agree about anything per-operand, +and no `W` is claimed here** (42.5: MAME's DMAC runs on wall-clock attotimes). + +**The gate was checked against its own negative.** With the *stealing* register +pair put in the *held* slot, the run still delivers all 2,048 bytes byte-exact +— and `tools/bench/dma_run.sh` goes **red**, on the trip count and on the MTC +sample. A counter that cannot come out different is 58.3's vacuous +"UNDERRUNS: 0/120" again; this one can, and was made to. + +`tools/analysis/27_dmac_config.py` decodes the four register bytes out of +`src/player/dma.i` itself, with the same MC68450 field tables +`21_iplrom_dmac.py` reads the IPL ROM with (now one copy, `mc68450.py`). So +"dual address, 8-bit port, burst, auto-request at max rate" is a decode of the +bytes the player programs, not a comment next to them — and it is directly +comparable with Sharp's own disk channel, which 52.5 read as `DCR $80 / OCR $B2` +and priced at 16..19 clk/B. + +### 59.2 Three things this model cannot be asked, and they bound the result + +Read out of MAME 0.277's source rather than inferred from behaviour: + +1. **The card has no request line to the DMAC.** `x68k_scsiext.cpp`'s `drq_w` + only stores a flag; the expansion slot carries no request to the HD63450 at + all (`x68k.cpp` wires `drq0` from the FDC and `drq3` from ADPCM, and nothing + else). The card's flow control is **DTACK**: on a DMAC cycle with DRQ low it + negates DTACK and the channel discards that operand and retries. So **REQG = + 10, external request — the mode the ladder's `W`=5 and `W`=12 rows assume — + cannot be run here at all.** +2. **Single address cannot be run either.** `hd63450.cpp` takes the implicit + path only for a channel with a device callback, and on this machine only + channel 0 (the FDC) has one. `DTYP` = 10/11 on channels 1..3 falls through to + the dual-address code. +3. **Only burst is modelled as held.** The device tests `(dcr & 0xc0) == 0`, so + XRM = 10 (cycle steal *without* hold) and XRM = 11 (cycle steal **with** + hold) are one code path. + +**So of the four rows of the per-byte ladder, exactly one — dual address, bus +held, 9 clk/B — has a code path in this model, and it is the one demonstrated.** +That is a bound on the apparatus, not a result about the board. The slot's own +pinout has `#EXREQ` at B36, so a real CZ-6BS1 plausibly drives it; whether it +does is ROADMAP B3, and it is now a sharper question than "which DTYP". + +### 59.3 Auto-request is charged by TIME, not by byte — and the GCR is the lever + +This is the finding that outlives the emulator. Every `W` in this project is +**clocks per delivered byte**, which presumes the device asks for each one. An +auto-requested channel does not know whether the device is ready: it takes the +share of the bus it was told to take and spends it either way. So the cost of a +record scales with **how long the record takes to arrive** — halve the delivery +rate and the CPU cost of the same record **doubles**. No `W` does that. + +`tools/analysis/28_autorequest_cost.py` prices it from MC68450 §3.8 and +§5.2.3.3.2, and gates its formulas against Table 5-3's sixteen printed rows +before printing anything. At the gate container's 37,405 B mean record and +**460 KB/s, an explicit rate and not a measurement** (FINDINGS 50): + +| configuration | sustains | charged to the 68000 | % of a 12 fps frame | +|---|---|---|---| +| REQG 01, max rate — what 59.1 demonstrated | the wire | 21.23 clk/B | **95.3%** | +| REQG 00, LRAR, BR = 00, 50% of the bus | 534 KB/s | 10.61 clk/B | **47.6%** | +| REQG 00, LRAR, BR = 01, 25% | 267 KB/s | 5.31 | 23.8% — **does not carry the rate** | +| REQG 00, LRAR, BR = 10, 12.5% | 133 KB/s | 2.65 | 11.9% — **does not carry the rate** | +| *(ladder, for comparison)* `W`=9 dual held | — | 9 | 40.4% | +| *(measured, 58.2)* PIO | — | 87.28 | 391.8% | + +**The held configuration is the cheapest per byte MOVED and the dearest per byte +DELIVERED**, and the gap between those two is the device's own slowness: 9 +clocks of DMAC work inside 21.2 clocks of waiting means **42.4% of the held bus +does anything at all**. Holding the bus is only cheap when the hold is ended by +the device, which is what external request is for. + +**BT and BR are two bits each and they set what fraction of the bus the player +gives away** (burst time `2^(BT+4)` clocks, sample period `2^(BT+BR+5)`, share +`2^-(BR+1)`). Nothing in this tree had named the GCR as a design choice; it is +the same kind of lever as `aligned` vs `split` and it belongs in the same list. +**At 460 KB/s only BR = 00 carries the rate**, so the fallback plan — if a real +card turns out not to drive `#EXREQ` — is 50% of the bus for the duration of +every record, or 47.6% of a frame slot per record. That is affordable and it is +not free, and it is the first cost model in this project that gets *worse* when +the disc gets slower. + +**59.3's one load-bearing assumption**, stated because the whole table rests on +it: that the channel *spends* its allotted share whether or not the device has a +byte. Under auto-request a request is pending until MTC is exhausted, so the +DMAC takes the bus during every window it is entitled to; when the device is not +ready the cycle is stretched by wait states (a real card negating DTACK) or +retried later (MAME's model discards the operand), and either way the window is +gone from the CPU's point of view. **If a real CZ-6BS1 instead lets the DMAC off +the bus early when no byte is there, 59.3's figures are upper bounds.** That is +a board question, and it is B3's. + +### 59.4 The window is refused, and that makes the re-encode a precondition + +58.3 found that 117 of 120 records start part way into a sector, and that PIO +absorbs it for free because the CPU is already touching every byte. A channel +cannot: it writes a contiguous run and cannot be told to drop the 300 bytes in +front of the record. `sc_in_data` now **refuses** a windowed read when the data +phase is the DMAC's — a new error, `SCE_WINDOW` — rather than quietly delivering +the neighbouring records' bytes into the ring, where the block loop has no +bounds check to catch them (49.2). The gate asserts the refusal. + +**So "sector-aligned records" has stopped being a preference in ROADMAP's +re-encode bundle and become the thing standing between P4a and the ring.** The +transport states its own precondition; the container does not meet it yet. + +### 59.5 What this does and does not move + +**Does not move `W`.** Not by one clock, for the third session running, and for +the reason 57.6 and 58.4 give. + +**Does not put the DMAC behind `ring.i`'s mailbox.** 59.4 is why: `xfer.i` asks +for records, and every record but three needs a window. That work is now +downstream of the re-encode bundle rather than of a DMAC question. + +**Does close the question ROADMAP called P4's first job.** A configuration that +holds the bus exists, runs, delivers the disc's bytes byte-exact, and is +demonstrated by evidence that does not come from watching `$EA0015` — which is +exactly what 57.3 said would be needed. + +**Does change what the fallback looks like.** Before this session the fallback +below a held bus was `W` = 16..19, the IPL ROM's own arbitrated configuration. +It is now limited-rate auto-request at a share the player chooses, priced in a +currency the project did not have, and the arithmetic says a 50% share carries +this container at 460 KB/s. + +### 59.6 One collision, and it was caught by the half of `check.sh` that runs first + +`DM_USE` — the word that tells `sc_in_data` whether the data phase is the +DMAC's — was first placed at `$18300`. `scsi.i`'s trace ends at `$182FF` and the +next 160 bytes are the **ring's**: `$18300` is `ring.i`'s `XF_SLOT` mailbox, and +`tools/bench/stream.lua` reads the same addresses from outside the machine. So +the ring rig's first record request wrote a non-zero word into what the +transport now read as "use the DMAC", and **the P4b stage — a stage this session +did not otherwise touch — went red on a run that never reached its snapshot.** + +Recorded because the procedure is the finding: `check.sh` was ALL GREEN before +any of this work, so the red was unambiguously new, and the failure was in a +stage nobody would have re-run on suspicion. Both halves of "green before and +green after" earned their place; the map is now `$18500`, clear of everything +the streaming rig owns. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 01cf0bc..6eaeaf1 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -8,6 +8,10 @@ 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). +Amended end of session 27: P4a done at the transport level; THE RE-ENCODE +BUNDLE under P2 is now the only thing between this tree and M2, because 59.4 +made sector-aligned records a precondition the transport enforces rather than a +preference (FINDINGS 59). **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 @@ -34,7 +38,7 @@ these units: | **68000 clocks** | measured, and the rate controller binds on them. | | **Delivery rate** | **no working figure, deliberately** (FINDINGS 50, USER DECISION). Every tool REQUIRES an explicit rate. | | **Seek time** | **no figure at all, and never had one.** 51.3/51.4 made it matter. | -| **W, clocks stolen per delivered byte** | 5 single-address held, 9 dual held, 12 single arbitrated; the IPL ROM's own disk channel is **16..19** (52.5). **The largest open number in the project.** | +| **W, clocks stolen per delivered byte** | 5 single-address held, 9 dual held, 12 single arbitrated; the IPL ROM's own disk channel is **16..19** (52.5). **The largest open number in the project.** Session 27 added the row underneath it: with **no external request line** on the card (59.2) the channel is auto-requested and is charged **by time rather than by byte**, so at 460 KB/s a 50% bus share costs **10.61 clk/B** and a smaller share cannot carry the rate at all (59.3). | --- @@ -135,7 +139,9 @@ own; together they are one: 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 + 4. **sector-align every record (58.3; PROMOTED TO A PRECONDITION in session + 27, 59.4 — `sc_in_data` refuses a windowed read under the DMAC, so this is + what the channel is waiting for)** — +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 @@ -173,8 +179,10 @@ 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. P4b DONE, session 26 — FINDINGS 58. P4a OPEN, and it is -now the ONLY thing between this tree and M2.** +**P4. Real transport. P4b DONE, session 26 — FINDINGS 58. P4a DONE at the +transport level, session 27 — FINDINGS 59. What is now between this tree and M2 +is THE RE-ENCODE BUNDLE under P2, because the channel refuses a windowed read +(59.4) and 117 of 120 records need one.** ~~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** @@ -189,14 +197,45 @@ wants the real ROM's bytes** and is untouched by this. **What is left is the half that decides the project**, and it is now two pieces: -**P4a. A DMAC configuration that HOLDS THE BUS.** Unchanged as the first job, -but 57.3 changes how it can be shown: `x68k_scsiext.cpp` glues `$EA0015` so that -a write is discarded when `exown()` is asserted and DRQ is low, and on this -machine `exown()` is asserted at idle. Every transfer therefore runs the SPC in -DMA mode with the CPU moving the bytes through the DMAC's own door — and **MAME -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`. +~~**P4a. A DMAC configuration that HOLDS THE BUS.**~~ **DONE at the transport +level, session 27 — FINDINGS 59.** `src/player/dma.i` programs HD63450 channel 1 +and takes the DATA IN phase: **the same 2,048 B off the disc three ways — PIO, +held, stealing — all three byte-exact.** 57.3's warning was met rather than +worked around: the evidence never reads `$EA0015`. **MTC is sampled by the +instruction after the one that starts the channel, and held it reads zero of +2,048** — the whole transfer happened between two instructions, because the +68000 did not execute in between — against the full count and 426 CPU loop trips +for the stealing configuration. Put the stealing registers in the held slot and +every byte still arrives and the gate goes **red**, which is what says the +counter can come out different (58.3's vacuous-counter trap, avoided +deliberately). + +**Three bounds on the apparatus, read out of MAME's source and not inferred** +(59.2): the card has **no request line to the DMAC** (its flow control is +DTACK), so external request — the mode the `W`=5 and `W`=12 rows assume — +cannot be run; **single address** cannot be run either (only channel 0 has +device callbacks); and **only burst is modelled as held**. Of the four rows of +the ladder exactly one, dual address held, has a code path here, and it is the +one demonstrated. The slot pinout has `#EXREQ` at B36, so a real card plausibly +drives it — **that is now B3's sharpest form**. + +**What is left of P4a is downstream of the container, not of the DMAC** (59.4): +`sc_in_data` **refuses** a windowed read when the data phase is the channel's, +because a channel writes a contiguous run and cannot drop the 300 B in front of +a record. So putting the channel behind `ring.i`'s mailbox waits on the +re-encode bundle. + +**P4c (new, and it is a DESIGN CHOICE the tree had not named).** Auto-request is +charged **by time, not by byte** — the channel spends its share of the bus +whether or not a byte is there, so halving the delivery rate DOUBLES the CPU +cost of the same record. The MC68450's GCR sets that share: `BT`/`BR`, four +values, 50/25/12.5/6.25%. `tools/analysis/28_autorequest_cost.py` prices it +against an explicit rate; at 460 KB/s **only the 50% share carries this +container**, at 10.61 clk/B and 47.6% of a frame per record, against 40.4% for +the `W`=9 row and 391.8% measured for PIO. **If B3 comes back saying the real +card drives `#EXREQ`, the ladder applies and this is the fallback; if it does +not, this IS the cost model** and the GCR pair is a number the player has to +choose. ~~**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 @@ -239,7 +278,9 @@ writes a contiguous run and cannot drop bytes. The three ways out price as 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.** +not against this one.** *(Session 27: it was, in the only sense that mattered — +the transport now REFUSES the windowed case rather than being trusted not to +reach it, so the bundle is a precondition rather than a plan. 59.4.)* *(original item, still the standing description of the `W` question:)* Drive the MB89352 instead of a host file. **Session 23 diff --git a/docs/STATUS.md b/docs/STATUS.md index f8df448..5eb71bc 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,3 +1,89 @@ +# Status & next-session handoff — end of session 27 (2026-08-24) + +## Session 27: the DMAC drives the data phase, and auto-request is charged by time + +**Green light first and last: `./tools/bench/check.sh` was ALL GREEN before any +of this and ALL GREEN after**, plus one new stage. + +**ROADMAP P4a is DONE at the transport level. FINDINGS 59.** **Emulated** — +MAME 0.277, `x68000 -exp1 cz6bs1 -ramsize 2M`. No real hardware ran. + +**1. THE CHANNEL DRIVES THE DATA PHASE.** `src/player/dma.i` programs HD63450 +channel 1 and `src/player/dmagate.s` reads the same 2,048 B at LBA 1000 three +ways, with the host comparing all three against its own copy of the image: + +| | bytes | MTC one instruction after START | CPU trips round its wait loop | +|---|---|---|---| +| PIO, the path 58.2 measured | byte-exact | — | — | +| **DMA, bus HELD** (DCR `$00`, OCR `$81`) | **byte-exact** | **0 of 2048** | **1** | +| **DMA, cycle STEALING** (DCR `$80`, OCR `$80`) | **byte-exact** | 2048 of 2048 | 426 | + +Both channels reported `CSR = $E0`, `CER = $00`, `MTC = 0`, `MAR` exactly +`+2048`. + +**2. THE EVIDENCE NEVER READS `$EA0015`, which is the whole design.** 57.3 said +it could not: with the DMAC's OWN asserted, MAME cannot tell a CPU-driven byte +at that address from a DMAC-driven one. So the discriminator is **the CPU's own +progress** — MTC sampled by the instruction *after* the one that starts the +channel. Held, the whole transfer happened between two instructions. **The gate +was checked against its own negative**: with the stealing register pair in the +held slot, every byte still arrives and `dma_run.sh` goes RED. A counter that +cannot come out different is 58.3's vacuous "UNDERRUNS: 0/120" again. + +**3. THREE THINGS THIS MODEL CANNOT BE ASKED**, read out of MAME's source: the +card has **no request line to the DMAC** (its flow control is DTACK, so external +request cannot be run); **single address** cannot be run either (only channel 0 +has device callbacks); and **only burst is modelled as held** (`(dcr & 0xc0) == +0`, so cycle-steal-with-hold shares the not-held path). Of the four rows of the +`W` ladder, exactly one — dual address, held, 9 clk/B — has a code path here, +and it is the one demonstrated. The slot pinout has `#EXREQ` at B36, so a real +card plausibly drives it; that is ROADMAP B3, sharpened. + +**4. AUTO-REQUEST IS CHARGED BY TIME, NOT BY BYTE — and this outlives the +emulator.** Every `W` in this project presumes the device asks for each byte. An +auto-requested channel spends its share of the bus whether or not a byte is +there, so a record's cost scales with **how long it takes to arrive**: halve the +rate and the CPU cost of the same record doubles. `tools/analysis/ +28_autorequest_cost.py` prices it from MC68450 §3.8/§5.2.3.3.2, gating its +formulas against Table 5-3's sixteen rows first. At 37,405 B and **460 KB/s, an +explicit rate**: max rate costs the whole **95.3% of a frame**; of the GCR's +four bus shares only **BR = 00, 50%, carries the rate**, at 10.61 clk/B and +47.6% of a frame. **The GCR is a design lever nothing in this tree had named.** + +**5. THE WINDOW IS REFUSED, so the re-encode is now a precondition.** A channel +writes a contiguous run and cannot drop the 300 B in front of a record, so +`sc_in_data` refuses a windowed read when the data phase is the DMAC's +(`SCE_WINDOW`) rather than writing the neighbours into the ring (49.2). 58.3's +"sector-aligned records should join the bundle" has become "the transport will +not run without them". + +**ONE COLLISION, AND THE BEFORE-HALF OF `check.sh` CAUGHT IT.** `DM_USE` was +first placed at `$18300`, which is `ring.i`'s `XF_SLOT` mailbox — `scsi.i`'s +trace ends at `$182FF` and the next 160 bytes are the ring's. The ring rig's +first record request therefore wrote a non-zero word into what the transport +read as "use the DMAC", and **the P4b stage, which this session did not touch, +went red**. The map is now `$18500`. Green-before is not a formality: it is what +made a red in an untouched stage unambiguous. 59.6. + +**New in the tree:** `src/player/dma.i`, `src/player/dmagate.s`, +`tools/bench/dma.lua`, `tools/bench/dma_run.sh`, +`tools/analysis/27_dmac_config.py` (decodes what the player programs, out of the +player's own source), `tools/analysis/28_autorequest_cost.py`, +`tools/analysis/mc68450.py` (the MC68450 field tables, now ONE copy, shared with +`21_iplrom_dmac.py`), and one `check.sh` stage. `src/player/scsi.i` gained the +`DM_USE` dispatch and `SCE_WINDOW`. + +**No decoder code changed.** `decode.bin` is still 1,296 B at the same MD5. + +**Next: THE RE-ENCODE BUNDLE, and it is now the only thing between this tree and +M2.** Four items sharing one re-measurement (ROADMAP P2): index-0 black, +`--spans all` as default, joint `lam`, and **sector-aligned records**, which +59.4 promoted from preference to precondition. After it, P4a's channel goes +behind `ring.i`'s mailbox and the 120-frame pixel-exact pass runs with the CPU +free — which is the shape of M2. + +--- + # 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 diff --git a/src/player/dma.i b/src/player/dma.i new file mode 100644 index 0000000..b06b0e5 --- /dev/null +++ b/src/player/dma.i @@ -0,0 +1,211 @@ +; The HD63450 driving the SCSI data phase. ROADMAP P4a. +; +; WHAT P4a HAS TO SHOW, and why it needed a new kind of evidence. FINDINGS 58 +; measured the CPU moving every byte itself at 87.28 clocks per delivered byte +; -- 391.8% of a 12 fps frame, against 22.4% for the cheapest DMA row of the +; ladder and 85.3% for the dearest. So the whole of what is left before M2 is a +; DMAC configuration that HOLDS THE BUS. 57.3 is why it could not simply be +; watched into existence: x68k_scsiext.cpp glues $EA0015 so that with the DMAC's +; OWN asserted -- which it is at idle on this machine -- MAME CANNOT DISTINGUISH +; a CPU-driven byte at that address from a DMAC-driven one. Watching the data +; register cannot answer the question it looks like it answers. +; +; THE DISCRIMINATOR USED HERE IS THE CPU'S OWN PROGRESS, and it never reads +; $EA0015 at all. A DMAC that holds the bus is one the CPU is not running +; against; so the witness is a single instruction: +; +; move.b #CCR_START,DM_CCR ; the channel is told to go +; move.w DM_MTC,d0 ; <- sampled by the VERY NEXT instruction +; +; If the bus was held for the transfer, the whole transfer happened between +; those two instructions and d0 reads ZERO. If it was not, d0 reads very nearly +; the full count and the CPU goes on to spin thousands of times while the +; channel trickles. Both configurations deliver the same bytes; what separates +; them is whether the 68000 got to execute anything meanwhile, which is exactly +; what "holds the bus" means and is not a fact about $EA0015. +; +; WHAT MAME CAN AND CANNOT BE ASKED, stated here because it bounds the claim and +; it is not obvious from the outside: +; +; * THE CARD HAS NO EXREQ PATH. x68k_scsiext.cpp's drq_w only stores a flag; +; the expansion slot has no request line to the DMAC at all (x68k.cpp wires +; drq0 from the FDC and drq3 from ADPCM, and nothing else). The card's flow +; control is DTACK: on a DMAC cycle with DRQ low the card NEGATES DTACK and +; the HD63450 discards that operand and retries. So every configuration +; below is AUTO-REQUEST; REQG=10, external request -- the mode the ladder's +; W=5 and W=12 rows assume -- has no wiring in this model and cannot be run. +; * SINGLE ADDRESS CANNOT BE RUN EITHER. hd63450.cpp only takes the implicit +; path when a channel has a dma_read/dma_write callback, and on this machine +; only channel 0 (the FDC) has one. DTYP=10/11 on channels 1..3 falls +; through to the dual-address code. +; * ONLY BURST IS MODELLED AS HELD. The device tests `(dcr & 0xc0) == 0`, so +; XRM=10 (cycle steal without hold) and XRM=11 (cycle steal WITH hold) are +; one code path. The bus is held, and the CPU halted, only for XRM=00 burst +; with REQG=01 max rate. +; +; So of the four rows of the per-byte ladder, exactly ONE -- dual address, bus +; held, 9 clk/B -- has a code path in this model, and it is the one demonstrated +; below. That is a bound on the apparatus and not a result about the board. +; +; AND IT IS STILL NOT A RATE. MAME's DMAC is configured in wall-clock attotimes +; (42.5), not per-operand cycles: set_burst_clocks gives channel 1 450 ns an +; operand no matter what the 68000 is doing. `W` is untouched by every line in +; this file and still wants a board (ROADMAP B1/B3). + +; ---- the channel. 1, not 0: channel 0 is the FDC's and is the one channel +; with device callbacks, which would silently take the implicit-address path. +; Channel 1 is also the channel the IPL ROM points at the SASI data register +; (52.5), so this is the machine's own disk channel programmed differently. +DMA_CH = 1 +DMACB = DMAC+DMA_CH*DMAC_CH ; $E84040 +DM_CSR = DMACB+$00 ; channel status (write 1s to clear) +DM_CER = DMACB+$01 ; channel error (read only) +DM_DCR = DMACB+$04 ; device control +DM_OCR = DMACB+$05 ; operation control +DM_SCR = DMACB+$06 ; sequence control +DM_CCR = DMACB+$07 ; channel control +DM_MTC = DMACB+$0A ; memory transfer count, WORD +DM_MAR = DMACB+$0C ; memory address, LONG +DM_DAR = DMACB+$14 ; device address, LONG +DM_MFC = DMACB+$29 +DM_CPR = DMACB+$2D +DM_DFC = DMACB+$31 + +; CSR bits +CSR_COC = $80 ; channel operation complete +CSR_BTC = $40 +CSR_NDT = $20 ; normal device termination +CSR_ERR = $10 ; channel error -- CER says which +CSR_ACT = $08 ; channel active +CCR_START = $80 + +; ---- the two configurations, as (DCR, OCR) pairs. Both are decoded by +; tools/analysis/27_dmac_config.py out of THESE bytes, using the same MC68450 +; field tables 21_iplrom_dmac.py reads the IPL ROM's channels with -- so what +; the run claims it programmed and what it programmed cannot drift apart. +; +; HELD : DCR $00 = XRM 00 burst, DTYP 00 dual address, DPS 0 8-bit port +; OCR $81 = DIR device->memory, SIZE byte, no chain, REQG 01 max rate +; STEAL: DCR $80 = XRM 10 cycle steal WITHOUT hold, otherwise identical +; OCR $80 = REQG 00 auto-request at limited rate +DM_HELD_DCR = $00 +DM_HELD_OCR = $81 +DM_STEAL_DCR = $80 +DM_STEAL_OCR = $80 + +; ---- what the run reports. Every one of these is a DMAC register or a count +; of the CPU's own instructions; none of them is a read of $EA0015. +; $18500 AND NOT $18300, WHICH IS WHERE THIS FIRST WENT. scsi.i's trace ends at +; $182FF and the next 160 bytes are the RING's: $18300 is ring.i's XF_SLOT +; mailbox, and tools/bench/stream.lua reads the same addresses from outside. +; dma.i is included by stream.s as well as by the gate, so DM_USE landed on the +; transfer request slot and the ring rig's first record request read as "use the +; DMAC" -- P4b's stage went red on a run that never reached its snapshot. The +; symptom was in a stage this session did not touch, which is the whole argument +; for check.sh being run before and after rather than only after. +DM_USE = $18500 ; u32 0 = PIO data phase, 1 = this file +DM_DCRV = $18504 ; u32 the DCR byte to program +DM_OCRV = $18508 ; u32 the OCR byte to program +DM_MTC0 = $1850C ; u32 MTC one instruction after START +DM_SPIN = $18510 ; u32 times the CPU went round the wait +DM_CSRF = $18514 ; u32 CSR when the channel finished +DM_CERF = $18518 ; u32 CER with it +DM_MTCF = $1851C ; u32 MTC with it +DM_MARF = $18520 ; u32 MAR with it -- where it stopped +DM_LEN = $18524 ; u32 bytes the channel was asked for +DM_PATIENCE = 4000000 ; the wait is bounded like every other + +; ---------------------------------------------------------------- sc_in_dma +; Receive d1 bytes into (a1) in phase d2, WITHOUT the CPU touching one of them. +; Entered from sc_in_data when DM_USE is set; same registers, same contract. +; +; ORDER MATTERS AND IT IS NOT THE OBVIOUS ONE. The SPC is put into DMA transfer +; BEFORE the channel is started, because in the held configuration the 68000 +; stops executing at the CCR write and does not run again until the transfer is +; over -- so anything the SPC needs to be told has to have been told already. +sc_in_dma: + movem.l d3-d5,-(sp) + move.l d1,d5 ; keep the length for the report + move.l d5,DM_LEN.l + move.b d2,SC_PCTL + move.l d1,d0 + bsr sc_settc ; the SPC counts the same bytes down + + ; ---- the channel, quiet first: CSR is write-one-to-clear and a stale + ; COC from a previous record would pass the wait loop instantly. + move.b #$FF,DM_CSR + move.l DM_DCRV.l,d0 + move.b d0,DM_DCR + move.l DM_OCRV.l,d0 + move.b d0,DM_OCR + move.b #$04,DM_SCR ; MAC 01 memory increment, DAC 00 none: + ; the device address is a REGISTER and + ; must not walk off it. + move.b #$05,DM_MFC ; the function codes the IPL ROM uses + move.b #$05,DM_DFC + move.b #$01,DM_CPR + move.w d5,DM_MTC + move.l a1,DM_MAR + move.l #SC_DREG,DM_DAR ; $EA0015 -- the DMAC's door, and now + ; the DMAC is the one going through it + move.b #SCMD_XFER,SC_SCMD ; no PROGRAM bit: the SPC raises DRQ + move.l #11,SC_TAG.l ; 11 = channel armed, SPC in DMA mode + bsr sc_snap + + ; ---- START, and the witness immediately after it + move.b #CCR_START,DM_CCR + move.w DM_MTC,d0 ; THE DISCRIMINATOR. Held: zero. + andi.l #$FFFF,d0 + move.l d0,DM_MTC0.l + + ; ---- wait for the channel, counting the CPU's own trips round the loop. + ; In the held configuration this is one trip, because the CPU did not + ; get to run until the transfer was over. In the stealing one it is + ; thousands, and every one of them is a 68000 instruction that executed + ; while the disc was delivering -- which is the whole point of P4a. + clr.l DM_SPIN.l + move.l #DM_PATIENCE,d3 +.wait: addq.l #1,DM_SPIN.l + move.b DM_CSR,d4 + btst #4,d4 ; ERR + bne.s .err + btst #7,d4 ; COC + bne.s .fin + subq.l #1,d3 + bne.s .wait + bsr .report + movem.l (sp)+,d3-d5 + move.l #SCE_TIMEOUT,SC_ERR.l + moveq #-1,d0 + rts +.err: bsr .report + movem.l (sp)+,d3-d5 + move.l #SCE_TIMEOUT,SC_ERR.l ; a channel error is a dead transport + moveq #-1,d0 + rts +.fin: bsr .report + move.b #$FF,DM_CSR ; leave the channel as we found it + move.l #12,SC_TAG.l ; 12 = channel reported COC + bsr sc_snap + movem.l (sp)+,d3-d5 + bsr sc_xferend ; the SPC's own transfer, not the DMAC's + move.l d0,-(sp) + move.l #9,SC_TAG.l + bsr sc_snap + move.l (sp)+,d0 + rts + +; ---- the channel's own account of what it did, read out of its registers +.report: + moveq #0,d0 + move.b DM_CSR,d0 + move.l d0,DM_CSRF.l + moveq #0,d0 + move.b DM_CER,d0 + move.l d0,DM_CERF.l + move.w DM_MTC,d0 + andi.l #$FFFF,d0 + move.l d0,DM_MTCF.l + move.l DM_MAR,d0 + move.l d0,DM_MARF.l + rts diff --git a/src/player/dmagate.s b/src/player/dmagate.s new file mode 100644 index 0000000..041488b --- /dev/null +++ b/src/player/dmagate.s @@ -0,0 +1,132 @@ +; Front-end for the HD63450 DATA PHASE (ROADMAP P4a), for the rig. +; +; THE QUESTION. FINDINGS 58 put the transport on the 68000 and priced it: the +; CPU moving every byte itself costs 87.28 clocks per delivered byte, 391.8% of +; a 12 fps frame. Against that, the cheapest DMA row of the ladder is 22.4% and +; the dearest is 85.3%, so everything left before M2 turns on getting the DMAC +; to drive the data phase with the bus HELD. 57.3 is why it cannot be shown by +; watching the data register: with the DMAC's OWN asserted, which it is at idle +; here, MAME cannot tell a CPU-driven byte at $EA0015 from a DMAC-driven one. +; +; THE EVIDENCE THIS GATE PRODUCES, and none of it is a read of $EA0015: +; +; 1. THE SAME BYTES. The same sectors are read three times -- once by the PIO +; path FINDINGS 58 measured, once by the channel with the bus held, once by +; the channel stealing cycles -- and the HOST compares all three against its +; own copy of the image. A transport that returns the wrong bytes without +; saying so is the failure a checksum-free ring cannot survive (49.2). +; 2. THE CPU'S OWN PROGRESS. MTC is sampled by the INSTRUCTION AFTER the one +; that starts the channel. Held, it reads zero: the entire transfer +; happened between two instructions, because the 68000 did not execute in +; between. Stealing, it reads nearly the full count and the CPU then goes +; round its wait loop thousands of times while the bytes arrive. That +; difference IS "the DMAC held the bus", and it is a fact about the CPU. +; 3. THE CHANNEL'S OWN ACCOUNT. CSR, CER, the final MTC and the final MAR: +; the channel says it completed without error, moved every byte, and left +; its memory pointer exactly one transfer-length past where it started. +; 4. THE WINDOW IS REFUSED. A windowed read (58.3: 117 of 120 records start +; part way into a sector) is rejected by the transport rather than silently +; delivering the neighbouring records' bytes into the ring. P4a's +; precondition is stated by the code that has it, not by a comment. +; +; WHAT IT DOES NOT SHOW. Not `W`. Not one clock of it. MAME's DMAC runs on +; wall-clock attotimes (42.5) and its burst mode halts the CPU outright rather +; than costing it cycles per operand, so this gate settles WHICH CONFIGURATION +; WORKS and not what one costs. See src/player/dma.i for the three ways this +; model bounds the question -- no EXREQ wiring, no single-address path, and only +; burst modelled as held. + +DGFLAG = $18600 ; 0 idle / 1 done +DGREC = $18610 ; 3 x 32 B: rc, err, mtc0, spin, csr, cer, mtc, mar +DGREC_SZ = 32 +DGWIN = $18680 ; u32 return of the WINDOWED dma read (want -1) +DGWERR = $18684 ; u32 SC_ERR after it (want SCE_WINDOW) +DGLBA = 1000 ; a NON-ZERO LBA throughout: a driver that emits + ; a malformed LBA field still passes LBA 0 +DGBLK = 4 ; 4 x 512 = 2,048 B +DGDST0 = $20000 ; PIO +DGDST1 = $24000 ; DMA, bus held +DGDST2 = $28000 ; DMA, cycle stealing + + org $10000 +start: + clr.l DGFLAG.l + +; ---- 1. the PIO path, unchanged, as the reference the other two are measured +; against. It is here so that a DMA failure cannot be confused with a SCSI +; protocol failure: if this one is wrong, nothing below is about the DMAC. + bsr scsi_init + move.l #DGLBA,d3 + moveq #DGBLK,d4 + lea DGDST0,a1 + bsr scsi_read + lea DGREC,a0 + bsr dg_save + +; ---- 2. the channel, WITH THE BUS HELD + bsr scsi_init + move.l #DM_HELD_DCR,DM_DCRV.l + move.l #DM_HELD_OCR,DM_OCRV.l + move.l #1,DM_USE.l ; after scsi_init, which clears it + move.l #DGLBA,d3 + moveq #DGBLK,d4 + lea DGDST1,a1 + bsr scsi_read + lea DGREC+DGREC_SZ,a0 + bsr dg_save + +; ---- 3. the channel, STEALING CYCLES. Same bytes, same code, two register +; values different -- which is what makes the comparison a comparison. + bsr scsi_init + move.l #DM_STEAL_DCR,DM_DCRV.l + move.l #DM_STEAL_OCR,DM_OCRV.l + move.l #1,DM_USE.l + move.l #DGLBA,d3 + moveq #DGBLK,d4 + lea DGDST2,a1 + bsr scsi_read + lea DGREC+2*DGREC_SZ,a0 + bsr dg_save + +; ---- 4. and a WINDOWED read through the channel, which must be REFUSED. This +; is the one test here that is expected to fail, and it has to fail LOUDLY: the +; alternative is a channel writing a whole sector into a ring that has room for +; a record, over the top of records the decoder has not finished with. + bsr scsi_init + move.l #DM_HELD_DCR,DM_DCRV.l + move.l #DM_HELD_OCR,DM_OCRV.l + move.l #1,DM_USE.l + move.l #300,SC_WSKIP.l ; a record that starts 300 B into a sector + move.l #1024,SC_WKEEP.l + move.l #DGLBA,d3 + moveq #DGBLK,d4 + lea DGDST2,a1 + bsr scsi_read_win + move.l d0,DGWIN.l + move.l SC_ERR.l,DGWERR.l + + move.l #1,DGFLAG.l +hold: bra.s hold + +; ---- one config's result, copied out of the shared reporting words before the +; next run overwrites them. d0 = scsi_read's return; a0 = where it goes. +dg_save: + move.l d0,(a0)+ + move.l SC_ERR.l,(a0)+ + move.l DM_MTC0.l,(a0)+ + move.l DM_SPIN.l,(a0)+ + move.l DM_CSRF.l,(a0)+ + move.l DM_CERF.l,(a0)+ + move.l DM_MTCF.l,(a0)+ + move.l DM_MARF.l,(a0)+ + ; and clear them, so a config that never reached the channel reports + ; zeros of its own rather than the previous config's numbers. + clr.l DM_MTC0.l + clr.l DM_SPIN.l + clr.l DM_CSRF.l + clr.l DM_CERF.l + clr.l DM_MTCF.l + clr.l DM_MARF.l + rts + + include "src/player/scsi.i" diff --git a/src/player/scsi.i b/src/player/scsi.i index 12b60d7..8be8013 100644 --- a/src/player/scsi.i +++ b/src/player/scsi.i @@ -125,6 +125,8 @@ SCE_SELTMO = 1 ; the target never answered selection SCE_PHASE = 2 ; the bus went somewhere unexpected SCE_TIMEOUT = 3 ; a poll loop ran out of patience SCE_STATUS = 4 ; the target returned non-zero status +SCE_WINDOW = 5 ; a WINDOWED read was asked of the DMAC, + ; which cannot drop bytes (58.3/P4a) SC_ERR = $18200 ; u32 last error SC_STAT = $18204 ; u32 SCSI status byte from the last cmd @@ -237,6 +239,8 @@ sci1: nop move.b #$FF,SC_INTS ; INTS is cleared by writing its bits move.b #0,SC_PCTL clr.l SC_ERR.l + clr.l DM_USE.l ; PIO unless a caller asks otherwise, + ; AFTER this call (src/player/dma.i) clr.l SC_TRN.l move.l #0,SC_TAG.l bsr sc_snap @@ -439,6 +443,26 @@ sip3: move.b SC_DREG,(a1)+ ; the same limitation sc_in_pio's caller already carries -- and the fix is the ; same one: d5 has to become what each phase actually delivered. sc_in_data: + ; ---- P4a: the DATA IN phase can be handed to the HD63450 instead, and + ; when it is, the CPU touches none of these bytes. src/player/dma.i. + ; The window is REFUSED rather than ignored: a channel writes a + ; contiguous run and cannot be told to drop the first 300 bytes, so a + ; windowed DMA read would deliver the neighbours' bytes into the ring + ; and the block loop has no bounds check to catch it (49.2, 58.3). + ; Refusing it here is what makes "sector-aligned records" a PRECONDITION + ; the transport states rather than an assumption it carries. + tst.l DM_USE.l + beq.s .pio + tst.l SC_WSKIP.l + bne.s .nowin + move.l SC_WKEEP.l,d0 + cmp.l d1,d0 + bne.s .nowin + bra sc_in_dma +.nowin: move.l #SCE_WINDOW,SC_ERR.l + moveq #-1,d0 + rts +.pio: movem.l d6-d7,-(sp) move.b d2,SC_PCTL move.l d1,d0 @@ -657,3 +681,9 @@ scr_phase: move.l #SCE_PHASE,SC_ERR.l moveq #-1,d0 bra.s scr_out + +; ---- the DMAC side of the data phase, ROADMAP P4a. Included unconditionally +; so that there is ONE transport file: sc_in_data dispatches on DM_USE, which +; scsi_init clears, so a front-end that never sets it assembles the same PIO +; path FINDINGS 58 measured and executes not one instruction of the following. + include "src/player/dma.i" diff --git a/tools/analysis/21_iplrom_dmac.py b/tools/analysis/21_iplrom_dmac.py index 17538f3..d733ab7 100644 --- a/tools/analysis/21_iplrom_dmac.py +++ b/tools/analysis/21_iplrom_dmac.py @@ -31,6 +31,7 @@ registers itself. It is evidence about what Sharp's engineers could get the board to do, from the vendor, for these exact devices. """ import sys, os, argparse, hashlib +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) BASE = 0xFE0000 # where the IPL ROM is mapped (and its 0xFF0000 alias) @@ -41,50 +42,7 @@ KNOWN = { "IPL 1.0 (MAME x68000 -bios ipl10), 131,072 B", } -# --- MC68450 register map, by offset inside a channel's 0x40 block ---------- -REG = {0x00: "CSR", 0x01: "CER", 0x04: "DCR", 0x05: "OCR", 0x06: "SCR", - 0x07: "CCR", 0x0A: "MTC", 0x0C: "MAR", 0x14: "DAR", 0x1A: "BTC", - 0x1C: "BAR", 0x25: "NIV", 0x27: "EIV", 0x29: "MFC", 0x2D: "CPR", - 0x31: "DFC", 0x39: "BFC"} - -XRM = {0: "burst", - 1: "UNDEFINED", - 2: "cycle steal WITHOUT hold (bus released between operands)", - 3: "cycle steal with hold"} -DTYP = {0: "68000-compatible, EXPLICITLY addressed -> DUAL ADDRESS", - 1: "6800-compatible, EXPLICITLY addressed -> DUAL ADDRESS", - 2: "device with ACK, implicitly addressed -> SINGLE ADDRESS", - 3: "device with ACK and RDY, implicit -> SINGLE ADDRESS"} -DPS = {0: "8-bit port", 1: "16-bit port"} -PCL = {0: "status input", 1: "status input with interrupt", - 2: "start pulse", 3: "abort input"} -SIZE = {0: "byte", 1: "word", 2: "long word", 3: "byte, unpacked"} -CHAIN= {0: "none", 1: "UNDEFINED", 2: "array", 3: "linked array"} -REQG = {0: "auto-request at limited rate", 1: "auto-request at max rate", - 2: "EXTERNAL request (one operand per device request)", - 3: "auto-request first operand, external thereafter"} - - -def dcr(v): - return [f"XRM = {v>>6&3:02b} {XRM[v>>6&3]}", - f"DTYP = {v>>4&3:02b} {DTYP[v>>4&3]}", - f"DPS = {v>>3&1:b} {DPS[v>>3&1]}", - f"PCL = {v&3:02b} {PCL[v&3]}"] - - -def ocr(v): - return [f"DIR = {v>>7&1:b} " + - ("device -> memory (read)" if v & 0x80 else "memory -> device (write)"), - f"SIZE = {v>>4&3:02b} {SIZE[v>>4&3]}", - f"CHAIN= {v>>2&3:02b} {CHAIN[v>>2&3]}", - f"REQG = {v&3:02b} {REQG[v&3]}"] - - -def scr(v): - m = {0: "no count", 1: "increment", 2: "decrement", 3: "UNDEFINED"} - return [f"MAC = {v>>2&3:02b} memory address {m[v>>2&3]}", - f"DAC = {v&3:02b} device address {m[v&3]}"] - +from mc68450 import REG, XRM, DTYP, DPS, PCL, SIZE, CHAIN, REQG, dcr, ocr, scr # --- the evidence ---------------------------------------------------------- # (address, expected bytes, one-line description). Every register value quoted diff --git a/tools/analysis/27_dmac_config.py b/tools/analysis/27_dmac_config.py new file mode 100644 index 0000000..db95467 --- /dev/null +++ b/tools/analysis/27_dmac_config.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""What the PLAYER programs into the DMAC -- read out of the assembler source. + + python3 tools/analysis/27_dmac_config.py [src/player/dma.i] + +ROADMAP P4a asks for a DMAC configuration that HOLDS THE BUS, and the whole +weight of the claim is in four register bytes. tools/analysis/21_iplrom_dmac.py +already reads the IPL ROM's four channels the same way, out of the shipping +image, and found Sharp's own disk channel at 16..19 clocks a byte (FINDINGS +52.5) -- above the entire bracket this project costs P4 in. This is the other +half of that comparison: the same MC68450 field tables (tools/analysis/ +mc68450.py, one copy) applied to the bytes src/player/dma.i actually programs. + +IT PARSES THE SOURCE RATHER THAN RESTATING IT. A constant typed into this file +would be a claim about the player that the player could quietly stop honouring; +the equates are read out of src/player/dma.i, so a change there changes what is +printed here and a mismatch between the two is not expressible. + +IT IS A GATE. Each configuration is checked against what it is FOR -- held +must decode as a mode that keeps the bus, stealing must decode as one that does +not -- and a disagreement exits non-zero rather than printing a paragraph. + +WHAT IT IS NOT: a rate. Nothing here is a measurement of anything. It says +which mode the player asks the chip for; tools/bench/dma_run.sh shows the +machine doing it, and `W` -- the clocks it costs on real silicon -- remains the +project's largest open number (ROADMAP B1/B3). +""" +import sys, os, re +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from mc68450 import dcr, ocr, scr, XRM, DTYP, REQG + +src = sys.argv[1] if len(sys.argv) > 1 else "src/player/dma.i" +if not os.path.exists(src): + sys.exit(f"missing {src} -- run from the repo root.") +text = open(src).read() + +def equ(name): + m = re.search(rf"^{name}\s*=\s*\$([0-9A-Fa-f]+)", text, re.M) + if not m: + sys.exit(f"{src} no longer defines {name}. This script reads the " + f"player's own equates; it does not keep a copy of them.") + return int(m.group(1), 16) + +# The SCR the channel is given is written inline rather than equated, because it +# is the same for both configurations and there is nothing to choose about it. +m = re.search(r"move\.b\s+#\$([0-9A-Fa-f]+),DM_SCR", text) +if not m: + sys.exit(f"{src} no longer writes DM_SCR with a literal.") +SCR = int(m.group(1), 16) + +CFG = [("BUS HELD", "DM_HELD_DCR", "DM_HELD_OCR"), + ("CYCLE STEALING", "DM_STEAL_DCR", "DM_STEAL_OCR")] + +print(f"WHAT src/player/dma.i PROGRAMS -- decoded from {src}\n") +bad = 0 +for label, dn, on in CFG: + D, O = equ(dn), equ(on) + print(f" {label} DCR = ${D:02X} OCR = ${O:02X} SCR = ${SCR:02X}") + for line in dcr(D): + print(f" {line}") + for line in ocr(O): + print(f" {line}") + for line in scr(SCR): + print(f" {line}") + holds = (D >> 6 & 3) in (0, 3) # burst, or cycle steal WITH hold + dual = (D >> 4 & 3) in (0, 1) + tomem = bool(O & 0x80) + checks = [ + (dual, "DTYP must be explicitly addressed: only channel 0 has device " + "callbacks in this machine, so an implicit-address DTYP on " + "channel 1 falls through to the dual-address path anyway"), + (tomem, "OCR DIR must be device -> memory; this is a READ"), + ((O >> 4 & 3) == 0, "OCR SIZE must be byte: the SPC's port is 8 bits"), + ((O >> 2 & 3) == 0, "OCR CHAIN must be none until P5a picks a chaining " + "scheme for the two-deep request queue (FINDINGS 55.3)"), + ((SCR & 3) == 0, "SCR DAC must not count: the device address is a " + "REGISTER at $EA0015 and must not walk off it"), + ((SCR >> 2 & 3) == 1, "SCR MAC must increment: the record is contiguous"), + ((O & 3) in (0, 1), "OCR REQG must be an AUTO-request mode: the " + "expansion slot has no request line to the DMAC in " + "this machine, so external request cannot be run"), + ] + if label == "BUS HELD": + checks.append((holds, "the held configuration must decode as a mode " + "that KEEPS the bus between operands")) + checks.append(((O & 3) == 1, "and as max-rate auto-request: MAME models " + "a held bus only for burst + REQG 01")) + else: + checks.append((not holds, "the stealing configuration must decode as a " + "mode that RELEASES the bus between operands " + "-- otherwise the two have no contrast")) + for ok, why in checks: + if not ok: + print(f" FAIL: {why}") + bad += 1 + print() + +print("""AGAINST THE MACHINE'S OWN DISK CHANNEL (21_iplrom_dmac.py, FINDINGS 52.5) + + IPL ROM ch1, SASI DCR $80 OCR $B2 dual address, 8-bit port, cycle steal + WITHOUT hold, EXTERNAL request + -> a full arbitration per byte, 16..19 + player, held DCR $00 OCR $81 dual address, 8-bit port, BURST, + auto-request at max rate + -> the ladder's dual-address held row, 9 + +Sharp's own configuration and the player's differ in exactly the field that +decides the project. That is 52.5's finding read the other way round: a cheaper +configuration IS reachable for an explicitly-addressed 8-bit port, and what it +costs on real silicon is still ROADMAP B3's question and not this file's.""") +sys.exit(1 if bad else 0) diff --git a/tools/analysis/28_autorequest_cost.py b/tools/analysis/28_autorequest_cost.py new file mode 100644 index 0000000..88f359e --- /dev/null +++ b/tools/analysis/28_autorequest_cost.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""What AUTO-REQUEST DMA costs the 68000, when there is no request line. + + python3 tools/analysis/28_autorequest_cost.py --kbps 460 [--record 37405] + +WHY THIS EXISTS. The project's per-byte ladder -- W = 5 single-address held, 9 +dual held, 12 single arbitrated, 16..19 dual arbitrated (FINDINGS 42.4, 52.5) -- +prices a transfer that the DEVICE asks for: one external request, one operand, +a known number of stolen clocks per delivered byte. Session 27 found that the +CZ-6BS1 as MAME models it has NO REQUEST LINE to the DMAC at all (FINDINGS +59.2): the card's flow control is DTACK, and every configuration that can be run +against it is AUTO-REQUEST, where the channel transfers because its own counter +says so and not because a byte has arrived. + +THAT CHANGES THE CURRENCY, and it is the reason this file is not a line in +another one. An externally requested transfer is charged PER DELIVERED BYTE. +An auto-requested one is charged PER UNIT OF TIME THE CHANNEL IS ACTIVE, because +the channel has no way to know the device is not ready: it takes its allotted +share of the bus and spends it whether or not a byte comes back. So the cost of +delivering a record depends on HOW LONG THE RECORD TAKES TO ARRIVE -- i.e. on +the delivery rate, the figure this tree deliberately has no default for (FINDINGS +50) -- and the tool REQUIRES one rather than assuming it. + +SOURCED: MC68450 Direct Memory Access Controller, Motorola, Jul 1989 +(bitsavers), sections 3.8 and 5.2.3.3, the same document buscost.py's transfer +timings come from. Section 5.2.3.3.1: under maximum-rate auto-request "all +operands in the data block will be transferred in one burst, so that the DMAC +will use 100% of the available bus bandwidth" -- which is the datasheet saying, +in its own words, what session 27 measured MAME's model doing when it HALTED the +68000 for the whole data phase (FINDINGS 59.1). + +THE ONE LOAD-BEARING ASSUMPTION, stated because the whole table rests on it: +that the channel SPENDS its allotted share whether or not the device has a byte. +Under auto-request a request is pending until MTC is exhausted, so the DMAC +takes the bus during every burst window it is entitled to; when the device is +not ready the cycle is stretched by wait states (a real CZ-6BS1 negating DTACK) +or retried later (MAME's model discards the operand), and either way the window +is gone from the CPU's point of view. If a real card instead lets the DMAC off +the bus early when no byte is there, these figures are UPPER BOUNDS. That is a +board question and it is ROADMAP B3's. + +NOT A MEASUREMENT. Every figure below is arithmetic over datasheet constants +and an explicit rate. `W` is still unmeasured and still wants a board. +""" +import sys, os, argparse +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from buscost import DMA_DUAL_BYTE_CLK, DMA_READ_CLK, DMA_WRITE_CLK + +CPU_HZ = 10_000_000.0 # the X68000 the whole tree is costed against +FPS = 12.0 + +ap = argparse.ArgumentParser() +ap.add_argument("--kbps", type=float, required=True, + help="delivery rate in KB/s. REQUIRED: this tree has no default " + "rate and the whole answer scales with it (FINDINGS 50).") +ap.add_argument("--record", type=int, default=37405, + help="mean record size in bytes (default: the gate container's)") +a = ap.parse_args() +RATE = a.kbps * 1024.0 + +# --- 3.8 GENERAL CONTROL REGISTER, decoded from the formulas in 5.2.3.3.2 --- +# burst time = 2^(BT + 4) clocks +# sample period = 2^(BT + BR + 5) clocks +# DMAC's share = 2^-(BR + 1) +# and Table 5-3 prints all sixteen combinations, so the formulas are GATED +# against the table rather than trusted. +TABLE = { # (BR, BT): (burst, MPU period, share, sample period) + (0,0):(16,16,.5,32), (0,1):(32,32,.5,64), (0,2):(64,64,.5,128), (0,3):(128,128,.5,256), + (1,0):(16,48,.25,64), (1,1):(32,96,.25,128), (1,2):(64,192,.25,256),(1,3):(128,384,.25,512), + (2,0):(16,112,.125,128),(2,1):(32,224,.125,256),(2,2):(64,448,.125,512),(2,3):(128,896,.125,1024), + (3,0):(16,240,.0625,256),(3,1):(32,480,.0625,512),(3,2):(64,960,.0625,1024), + (3,3):(128,1920,.0625,2048), +} +bad = 0 +for (br, bt), (burst, mpu, share, sample) in sorted(TABLE.items()): + f_burst, f_sample, f_share = 2**(bt+4), 2**(bt+br+5), 2.0**-(br+1) + for got, want, what in ((f_burst, burst, "burst time"), + (f_sample, sample, "sample period"), + (f_share, share, "bandwidth share"), + (f_sample - f_burst, mpu, "MPU period")): + if got != want: + print(f" FAIL BR={br:02b} BT={bt:02b} {what}: formula {got}, " + f"Table 5-3 {want}") + bad += 1 +if bad: + sys.exit(f"\n{bad} disagreements between 5.2.3.3.2's formulas and Table 5-3. " + "Everything below\nis those formulas, so it is not printed.") +print(f"MC68450 5.2.3.3.2's formulas reproduce all 16 rows of Table 5-3.\n") + +BYTE_CLK = DMA_DUAL_BYTE_CLK # dual address, 8-bit port: a 4-clock read of + # $EA0015 and a 5-clock write to the ring +frame_clk = CPU_HZ / FPS +wire_s = a.record / RATE # how long the record takes to land +wire_clk = wire_s * CPU_HZ # ...in 68000 clocks +per_byte_wire = wire_clk / a.record # clocks of wall time per byte + +print(f"THE RECORD: {a.record:,} B at {a.kbps:g} KB/s = {wire_s*1000:.2f} ms " + f"= {wire_clk:,.0f} clocks = {100*wire_clk/frame_clk:.1f}% of a " + f"{FPS:g} fps frame") +print(f" one byte of WIRE TIME is {per_byte_wire:.2f} clocks; one byte of DMAC " + f"WORK is {BYTE_CLK} ({DMA_READ_CLK} read + {DMA_WRITE_CLK} write, " + f"buscost.py)\n") + +print("REQG 01, AUTO-REQUEST AT MAXIMUM RATE -- what session 27 demonstrated") +print(f" The channel holds the bus until MTC is exhausted (5.2.3.3.1: 100% of " + f"the\n bandwidth), so the CPU gets NOTHING for the whole delivery:") +print(f" cost to the 68000 = the whole {100*wire_clk/frame_clk:.1f}% of a " + f"frame, or {per_byte_wire:.2f} clk/B") +print(f" It is the cheapest configuration per BYTE MOVED and the dearest per " + f"byte\n DELIVERED, and the gap between those is the device's own " + f"slowness:\n {BYTE_CLK} clocks of work in {per_byte_wire:.1f} clocks " + f"of waiting = {100*BYTE_CLK/per_byte_wire:.1f}% of the held bus does " + f"anything.\n") + +print("REQG 00, LIMITED-RATE AUTO-REQUEST -- the lever the GCR actually gives") +print(" The DMAC takes its programmed share of the bus and spends it whether " + "or not\n a byte is there, so the CPU pays the SHARE for the WHOLE " + "delivery -- and the\n share must also be big enough to carry the rate. " + "Both, or it does not fit.\n") +print(" BR share sustains clk/B charged % of a frame fits " + f"{a.kbps:g} KB/s?") +fits_any = [] +for br in range(4): + burst, mpu, share, sample = TABLE[(br, 3)] # BT=11, the longest burst + # bytes the channel can move inside one burst window, and how often that + # window comes round + bytes_per_burst = burst // BYTE_CLK + sustains = bytes_per_burst * CPU_HZ / sample + charged = share * per_byte_wire # clocks the CPU loses per + # DELIVERED byte + pct = 100 * share * wire_clk / frame_clk + ok = sustains >= RATE + if ok: + fits_any.append((br, share, charged, pct)) + print(f" {br:02b} {share*100:5.2f}% {sustains/1024:7.1f} KB/s " + f"{charged:9.2f} {pct:8.1f}% {'yes' if ok else 'NO'}") +print(f"\n (BT = 11 throughout: the longest burst, 128 clocks, which is the " + f"most\n favourable row -- a shorter burst moves fewer bytes per window " + f"at the same\n share and sustains proportionally less.)") + +if not fits_any: + print(f"\n NOTHING FITS. At {a.kbps:g} KB/s no limited-rate share can " + f"carry the record,\n so the only auto-request configuration that " + f"delivers is maximum rate --\n and that one stops the CPU for the " + f"whole {100*wire_clk/frame_clk:.1f}% of a frame the record takes.") +else: + br, share, charged, pct = fits_any[0] + print(f"\n CHEAPEST THAT FITS: BR = {br:02b}, {share*100:g}% of the bus, " + f"{charged:.2f} clk/B charged to the\n 68000 -- {pct:.1f}% of a frame " + f"per record.") + print(f" Against the ladder: W=5 held costs {5*a.record/frame_clk*100:.1f}%, " + f"W=9 dual held {9*a.record/frame_clk*100:.1f}%,\n W=19 the IPL ROM's " + f"own {19*a.record/frame_clk*100:.1f}%, and PIO measured " + f"{87.28*a.record/frame_clk*100:.1f}% (FINDINGS 58.2).") + +print(f""" +WHAT THIS SETTLES, AND WHAT IT DOES NOT + +1. AUTO-REQUEST IS CHARGED BY TIME, NOT BY BYTE. Every W in this project is + clocks per DELIVERED byte, which presumes the device asks. With no request + line the channel spends its share of the bus at a rate it was told, so the + record's cost scales with how long the disc takes -- halve the delivery rate + and the CPU cost of the same record DOUBLES. No W does that. + +2. THE GCR IS A DESIGN LEVER NOBODY HAD NAMED. BT and BR are two bits each and + they set what fraction of the bus the player gives away. That is the same + kind of choice as `aligned` vs `split` and it belongs in the same list. + +3. IT IS STILL NOT A MEASUREMENT. These are datasheet constants and an explicit + rate. Whether the real CZ-6BS1 drives #EXREQ (pin B36 exists on the slot, and + MAME's model simply does not connect it) is ROADMAP B3's question, and if it + does, the ladder applies and this file is the fallback rather than the plan.""") diff --git a/tools/analysis/mc68450.py b/tools/analysis/mc68450.py new file mode 100644 index 0000000..a0a7f0d --- /dev/null +++ b/tools/analysis/mc68450.py @@ -0,0 +1,59 @@ +"""MC68450 / HD63450 register field layouts, in ONE copy. + +Read by tools/analysis/21_iplrom_dmac.py, which decodes what the X68000's IPL +ROM programs into the DMAC, and by tools/analysis/27_dmac_config.py, which +decodes what src/player/dma.i programs into it. The two exist to be COMPARED +-- the ROM's own disk channel costs 16..19 clocks a byte (FINDINGS 52.5) and +the player's job is to be cheaper -- and a comparison between two decodings +that used two copies of these tables would not be one. This tree has already +paid twice for a transform with two copies of itself (FINDINGS 49.7.5). + +SOURCED: MC68450 Direct Memory Access Controller, Motorola, Jul 1989 +(bitsavers) -- the same document FINDINGS 39 cites for the transfer timings in +tools/analysis/buscost.py. +""" +# --- MC68450 register map, by offset inside a channel's 0x40 block ---------- +REG = {0x00: "CSR", 0x01: "CER", 0x04: "DCR", 0x05: "OCR", 0x06: "SCR", + 0x07: "CCR", 0x0A: "MTC", 0x0C: "MAR", 0x14: "DAR", 0x1A: "BTC", + 0x1C: "BAR", 0x25: "NIV", 0x27: "EIV", 0x29: "MFC", 0x2D: "CPR", + 0x31: "DFC", 0x39: "BFC"} + +XRM = {0: "burst", + 1: "UNDEFINED", + 2: "cycle steal WITHOUT hold (bus released between operands)", + 3: "cycle steal with hold"} +DTYP = {0: "68000-compatible, EXPLICITLY addressed -> DUAL ADDRESS", + 1: "6800-compatible, EXPLICITLY addressed -> DUAL ADDRESS", + 2: "device with ACK, implicitly addressed -> SINGLE ADDRESS", + 3: "device with ACK and RDY, implicit -> SINGLE ADDRESS"} +DPS = {0: "8-bit port", 1: "16-bit port"} +PCL = {0: "status input", 1: "status input with interrupt", + 2: "start pulse", 3: "abort input"} +SIZE = {0: "byte", 1: "word", 2: "long word", 3: "byte, unpacked"} +CHAIN= {0: "none", 1: "UNDEFINED", 2: "array", 3: "linked array"} +REQG = {0: "auto-request at limited rate", 1: "auto-request at max rate", + 2: "EXTERNAL request (one operand per device request)", + 3: "auto-request first operand, external thereafter"} + + +def dcr(v): + return [f"XRM = {v>>6&3:02b} {XRM[v>>6&3]}", + f"DTYP = {v>>4&3:02b} {DTYP[v>>4&3]}", + f"DPS = {v>>3&1:b} {DPS[v>>3&1]}", + f"PCL = {v&3:02b} {PCL[v&3]}"] + + +def ocr(v): + return [f"DIR = {v>>7&1:b} " + + ("device -> memory (read)" if v & 0x80 else "memory -> device (write)"), + f"SIZE = {v>>4&3:02b} {SIZE[v>>4&3]}", + f"CHAIN= {v>>2&3:02b} {CHAIN[v>>2&3]}", + f"REQG = {v&3:02b} {REQG[v&3]}"] + + +def scr(v): + m = {0: "no count", 1: "increment", 2: "decrement", 3: "UNDEFINED"} + return [f"MAC = {v>>2&3:02b} memory address {m[v>>2&3]}", + f"DAC = {v&3:02b} device address {m[v&3]}"] + + diff --git a/tools/bench/check.sh b/tools/bench/check.sh index 6a987c4..69509e0 100755 --- a/tools/bench/check.sh +++ b/tools/bench/check.sh @@ -504,6 +504,50 @@ else echo " SKIPPED: no chdman (ships with mame-tools) -- cannot build the volume" fi +echo "--- session 27: the DMAC drives the data phase, and holds the bus (FINDINGS 59) ---" +# ROADMAP P4a, the last item before M2. The two stages above have the CPU moving +# every byte itself, at the 87.28 clocks per delivered byte FINDINGS 58.2 +# measured -- 391.8% of a 12 fps frame. This one hands the DATA IN phase to the +# HD63450 and gates on the thing 57.3 said would be hard to show: that the DMAC, +# and not the CPU, is driving it. +# +# IT IS GATED WITHOUT LOOKING AT $EA0015, and that is the design. With the +# DMAC's OWN asserted -- which it is at idle here -- MAME cannot distinguish a +# CPU-driven byte at that address from a DMAC-driven one, so watching it proves +# nothing. What is gated instead is THE CPU'S OWN PROGRESS: +# * the same 2,048 B off the disc three ways -- PIO, held, stealing -- all +# three byte-exact against the host's copy, so the configuration is being +# compared against a delivery that works and not against nothing; +# * MTC sampled by the INSTRUCTION AFTER the one that starts the channel: zero +# in the held configuration (the whole transfer happened between two +# instructions, because the 68000 did not execute in between) and the full +# count in the stealing one; +# * the CPU's own trip count round its wait loop: 1 against hundreds. A +# counter that CANNOT come out different is 58.3's vacuous "UNDERRUNS: 0/120" +# again, so the run asserts the contrast and not just the held value; +# * the channel's own CSR/CER/MTC/MAR, which must say it moved every byte +# without error; +# * and a WINDOWED read through the channel REFUSED. 117 of 120 records start +# part way into a sector (58.3); a channel writes a contiguous run and cannot +# drop the bytes in front of one, so it would write the neighbouring records +# into the ring with no bounds check to catch it (49.2). The refusal is what +# makes "sector-aligned container" a precondition the transport states. +# +# NOT GATED ON RATE, and it cannot be: MAME's DMAC runs on wall-clock attotimes +# (42.5) and models a held bus by HALTING the CPU rather than by charging it +# cycles per operand. `W` is untouched. tools/analysis/28_autorequest_cost.py +# prices what this configuration costs, from the datasheet and an explicit rate. +# Skipped rather than failed when chdman is absent. +if command -v chdman > /dev/null; then + bash tools/bench/dma_run.sh "$DLX" > tmp/dma_gate.log 2>&1 || { + echo "FAIL: the DMAC did not drive the SCSI data phase." + tail -16 tmp/dma_gate.log; exit 1; } + grep -aE "BYTES OK|MTC one instruction|trips round|REFUSED" tmp/dma_gate.log \ + | sed 's/^ *//;s/^/ /' +else + echo " SKIPPED: no chdman (ships with mame-tools) -- cannot build the volume" +fi + echo "--- session 24: the scene graph, and the gap between branch points (FINDINGS 56) ---" # The arcade scene graph is not in this repo and is not redistributable from # here. tools/import/scenegraph.py is the ONE file in the tree that knows the diff --git a/tools/bench/dma.lua b/tools/bench/dma.lua new file mode 100644 index 0000000..965fd87 --- /dev/null +++ b/tools/bench/dma.lua @@ -0,0 +1,131 @@ +-- Drive src/player/dmagate.s: does the HD63450 drive the SCSI data phase, and +-- does it HOLD THE BUS? (ROADMAP P4a) +-- +-- THE APPARATUS is tools/bench/scsi_run.sh's, unchanged and stated again +-- because it is two substitutions deep: `x68000 -exp1 cz6bs1` (the board 42.5 +-- says to benchmark, never x68ksupr, whose internal SCSI is PIO-only in MAME), +-- and a ZERO-FILLED scsiexrom.bin on a private rompath, which is honest only +-- because the player drives the SPC registers directly and never executes a +-- byte of that ROM. +-- +-- WHAT THIS RIG DOES NOT DO, and it is the point of the whole design: it never +-- looks at $EA0015. 57.3 showed that address cannot answer the question -- +-- with the DMAC's OWN asserted MAME cannot tell a CPU-driven byte there from a +-- DMAC-driven one. What separates the two configurations below is whether the +-- 68000 EXECUTED ANYTHING while the bytes were arriving, which is a fact about +-- the CPU and is read out of the DMAC's own registers plus a counter the +-- machine incremented itself. +-- +-- AND IT IS NOT A RATE. MAME's DMAC is configured in wall-clock attotimes +-- (42.5); its burst mode halts the CPU outright rather than charging it cycles +-- per operand. `W` is untouched here and still wants a board. +local M = manager.machine +local SP = M.devices[":maincpu"].spaces["program"] +local function P(s) print("[DMA] "..s) end +local function T() local t=M.time; return t.seconds + t.attoseconds/1e18 end + +local DGFLAG, DGREC, DGREC_SZ = 0x18600, 0x18610, 32 +local DGWIN, DGWERR = 0x18680, 0x18684 +local DGLBA, DGBLK = 1000, 4 +local DST = {0x20000, 0x24000, 0x28000} +local NAME = {"PIO (the path FINDINGS 58 measured)", + "DMA, BUS HELD (DCR $00 burst, OCR $81 max rate)", + "DMA, STEALING (DCR $80 cycle steal, OCR $80 limited)"} +local SHORT = {"pio", "held", "steal"} +local ERRNAME = {[0]="OK", "SELECTION TIMEOUT -- no target answered", + "UNEXPECTED PHASE", "POLL TIMEOUT -- a phase never arrived", + "NON-ZERO SCSI STATUS", + "WINDOWED READ REFUSED -- a channel cannot drop bytes"} +local DISK = os.getenv("DLX_SCSI_IMG") or "dlxdisk.img" + +local code do local f=io.open("dmagate.bin","rb"); code=f:read("a"); f:close() end + +-- the disc's own bytes, once, for all three comparisons +local want do + local f = io.open(DISK, "rb") + if f then f:seek("set", DGLBA*512); want = f:read(DGBLK*512); f:close() end +end + +local st = "boot" +SUB = emu.add_machine_frame_notifier(function() + local ok, err = pcall(function() + if st == "boot" then + if T() < 3.0 then return end + for i = 1, #code do SP:write_u8(0x10000+i-1, string.byte(code,i)) end + SP:write_u32(DGFLAG, 0) + local cpu = M.devices[":maincpu"] + cpu.state["SR"].value = 0x2700 + cpu.state["SP"].value = 0x8000 + cpu.state["PC"].value = 0x10000 + P(string.format("dmagate.bin=%d B loaded at $10000; reading LBA %d, %d B, " + .."three ways", #code, DGLBA, DGBLK*512)) + st = "wait"; return + end + if st == "wait" then + if SP:read_u32(DGFLAG) ~= 1 then + if T() > 60 then P("TIMEOUT: the gate never finished"); P("done"); M:exit() end + return + end + if not want then P("no "..DISK.." to check against"); P("done"); M:exit(); return end + local LEN = DGBLK*512 + for i = 0, 2 do + local b = DGREC + i*DGREC_SZ + local rc = SP:read_u32(b) + local e = SP:read_u32(b+4) + local mtc0 = SP:read_u32(b+8) + local spin = SP:read_u32(b+12) + local csr = SP:read_u32(b+16) + local cer = SP:read_u32(b+20) + local mtcf = SP:read_u32(b+24) + local marf = SP:read_u32(b+28) + P(NAME[i+1]) + if rc ~= 0 then + P(string.format(" FAILED: err=%d (%s)", e, ERRNAME[e] or "?")) + else + local bad, first = 0, nil + for k = 1, LEN do + if SP:read_u8(DST[i+1]+k-1) ~= string.byte(want, k) then + bad = bad + 1; first = first or (k-1) + end + end + if bad == 0 then + P(string.format(" BYTES OK: %d B from LBA %d match %s byte for byte " + .."[%s]", LEN, DGLBA, DISK, SHORT[i+1])) + else + P(string.format(" BYTES WRONG [%s]: %d of %d differ, first at +%d", + SHORT[i+1], bad, LEN, first)) + end + end + if i > 0 then + -- THE DISCRIMINATOR. MTC as the instruction after START saw it, and + -- the number of times the CPU went round its own wait loop. + P(string.format(" MTC one instruction after START: %d of %d -> the " + .."CPU %s while the transfer ran [%s]", + mtc0, LEN, + (mtc0 == 0) and "NEVER EXECUTED" or "kept executing", + SHORT[i+1])) + P(string.format(" CPU trips round the wait loop: %d [%s]", spin, SHORT[i+1])) + P(string.format(" channel: CSR=$%02X (%s%s%s) CER=$%02X MTC=%d " + .."MAR=$%06X (+%d) [%s]", + csr, + ((csr & 0x80) ~= 0) and "COC " or "", + ((csr & 0x10) ~= 0) and "ERR " or "", + ((csr & 0x08) ~= 0) and "ACT" or "idle", + cer, mtcf, marf, marf - DST[i+1], SHORT[i+1])) + end + end + -- The refusal. Expected to fail, and the run is only green if it did. + local w, we = SP:read_u32(DGWIN), SP:read_u32(DGWERR) + if w == 0xFFFFFFFF and we == 5 then + P("WINDOWED DMA READ REFUSED, as it must be: a channel writes a " + .."contiguous run and cannot drop the 300 B in front of the record " + .."(58.3). P4a's precondition is a SECTOR-ALIGNED container.") + else + P(string.format("WINDOW NOT REFUSED: rc=%d err=%d -- the transport would " + .."have written the neighbours' bytes into the ring.", w, we)) + end + P("done"); M:exit(); return + end + end) + if not ok then P("LUA ERROR: "..tostring(err)); P("done"); M:exit() end +end) diff --git a/tools/bench/dma_run.sh b/tools/bench/dma_run.sh new file mode 100755 index 0000000..277729d --- /dev/null +++ b/tools/bench/dma_run.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# One HD63450 data-phase run: does the DMAC drive the SCSI data phase, and does +# it HOLD THE BUS? (ROADMAP P4a, the last item before M2.) +# +# tools/bench/dma_run.sh [container.dlx] +# +# The apparatus is tools/bench/scsi_run.sh's -- `x68000 -exp1 cz6bs1` and a +# zero-filled scsiexrom.bin on a private rompath -- and the volume is +# tools/bench/mkvol.sh's, the same bytes the host-file ring rig reads. +# +# WHAT A GREEN RUN MEANS: the same 2,048 B came off the disc three ways -- PIO, +# the channel with the bus held, the channel stealing cycles -- all three +# byte-exact against the host's copy; and in the held configuration THE WHOLE +# TRANSFER HAPPENED BETWEEN TWO INSTRUCTIONS, which is what holding the bus +# means and is not a claim about $EA0015 (57.3). +# +# WHAT IT DOES NOT MEAN: anything about `W`. MAME's DMAC runs on wall-clock +# attotimes (42.5) and models a held bus by HALTING the CPU rather than by +# charging it cycles per operand. This settles which configuration works. +set -e +cd "$(dirname "$0")/../.." +DLX=${1:-tmp/rc_fr_singe_scsi_span.dlx} + +bash tools/bench/mkvol.sh "$DLX" + +tools/vasm/vasmm68k_mot -Fbin -o tmp/dmagate.bin src/player/dmagate.s > /dev/null + +# What the player will program, decoded out of the same constants it programs. +python3 tools/analysis/27_dmac_config.py + +# stdbuf -oL: without it a long MAME run is unobservable until it exits, and a +# run that is merely finishing looks exactly like one that is wedged (34.1). +( cd tmp && SDL_VIDEODRIVER=dummy stdbuf -oL timeout -k 5 300 \ + mame x68000 -bios ipl10 -exp1 cz6bs1 \ + -rompath "$HOME/mame/roms;./p4roms" -hard dlxdisk.chd \ + -ramsize 2M -video soft -window -sound none -nothrottle -plugins \ + -autoboot_script ../tools/bench/dma.lua \ + -seconds_to_run 90 > dma_run.log 2>&1 ) +grep -aq "^\[DMA\] done" tmp/dma_run.log || { + echo "FAIL: the DMA gate did not finish -- no completion marker." + tail -8 tmp/dma_run.log; exit 1; } +grep -a "^\[DMA\]" tmp/dma_run.log | sed 's/^\[DMA\] / /' + +# THE ASSERTIONS. Printing a result and gating on it are different things. +fail() { echo "FAIL: $1"; exit 1; } +grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[pio\]" tmp/dma_run.log || \ + fail "the PIO reference read did not match -- nothing below is about the DMAC." +grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[held\]" tmp/dma_run.log || \ + fail "the bus-held DMA read did not deliver the disc's bytes." +grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[steal\]" tmp/dma_run.log || \ + fail "the cycle-stealing DMA read did not deliver the disc's bytes." +grep -aq "MTC one instruction after START: 0 of 2048 .*NEVER EXECUTED .*\[held\]" \ + tmp/dma_run.log || \ + fail "the bus was NOT held: the CPU executed while the channel ran, so this is + not the configuration ROADMAP P4a asks for. That MTC is the whole of the + evidence that does not come from watching \$EA0015 (57.3)." +grep -aq "CPU trips round the wait loop: 1 \[held\]" tmp/dma_run.log || \ + fail "the held configuration's CPU went round its wait loop more than once -- + it was running, so the bus was not held for the whole transfer." +# A NEGATIVE ASSERTION IS WRITTEN AS AN `if`, not as `grep ... && fail`: under +# `set -e` a failing grep in an AND-list takes the whole script's exit status +# with it, so the run would report the failure it was looking for as a pass. +SPIN=$(sed -n 's/.*CPU trips round the wait loop: \([0-9]*\) \[steal\].*/\1/p' \ + tmp/dma_run.log) +[ -n "$SPIN" ] && [ "$SPIN" -ge 100 ] || \ + fail "the cycle-stealing configuration did not leave the CPU running (spin + = ${SPIN:-none}) -- the two configurations are meant to DIFFER in exactly + that, and a contrast of one against one is not a contrast." +if grep -aq "MTC one instruction after START: 0 of 2048 .*\[steal\]" tmp/dma_run.log +then + fail "the cycle-stealing configuration also finished between two instructions, + so the comparison has no contrast in it and the discriminator is measuring + something other than bus ownership." +fi +grep -aq "COC .*CER=\$00 MTC=0 .*(+2048) \[held\]" tmp/dma_run.log || \ + fail "the held channel did not report a clean completion of every byte." +grep -aq "COC .*CER=\$00 MTC=0 .*(+2048) \[steal\]" tmp/dma_run.log || \ + fail "the stealing channel did not report a clean completion of every byte." +grep -aq "WINDOWED DMA READ REFUSED" tmp/dma_run.log || \ + fail "a WINDOWED read through the channel was not refused. 117 of 120 records + start part way into a sector (58.3), and a channel cannot drop the bytes + in front of one -- so it would write the neighbouring records into the + ring, over data the decoder has not finished with, with no bounds check + to catch it (49.2)." +exit 0