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
This commit is contained in:
prosolis
2026-08-24 23:55:07 -07:00
parent 5921fab118
commit 621a5bb457
14 changed files with 1342 additions and 58 deletions
+132
View File
@@ -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"