Files
Dragon-s-Lair-X68k/tools/bench/dma.lua
T
prosolis 621a5bb457 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
2026-08-24 23:55:07 -07:00

132 lines
6.0 KiB
Lua

-- 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)