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

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

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

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

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

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

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-24 23:15:53 -07:00
parent e935d8661c
commit 5921fab118
15 changed files with 1221 additions and 48 deletions
+150 -5
View File
@@ -125,6 +125,10 @@ local A_NPOLL, A_NISSUE = 0x18360, 0x18364
local A_SLKMIN, A_SLKAT = 0x18368, 0x1836C
local A_PFREC, A_PFDONE = 0x18370, 0x18374
local A_NSEEK, A_SKWAIT = 0x18378, 0x1837C
-- src/player/xfer.i, the transport that lives in the machine.
local A_XFSCSI, A_XSLBA0 = 0x18380, 0x18384
local A_XSNXFER, A_XSNBYTE = 0x18388, 0x1838C
local A_XSNWIRE, A_XSERR, A_XSERRAT = 0x18390, 0x18394, 0x18398
-- The scene header's record index, pushed into RAM before the CPU is launched.
-- It sits ABOVE the codebooks rather than in the low RAM around ring.i's own
-- tables: setup() runs one raster frame before launch() and the IPL is still
@@ -171,6 +175,25 @@ local ITERS = tonumber(os.getenv("DLX_ITER") or "") or 1
-- 2 keeps the next request queued so it never stops. Both are real designs and
-- the difference between them is what this rig measures.
local QDEPTH = tonumber(os.getenv("DLX_QDEPTH") or "") or 2
-- WHO MOVES THE BYTES. "model" is this script: a transport that delivers
-- XF_LEN bytes in XF_LEN/rate seconds and acks from emulated time, which is
-- what FINDINGS 49/51/55 were all measured through. "scsi" is ROADMAP P4b --
-- src/player/xfer.i and src/player/scsi.i on the 68000, a real MB89352, a real
-- volume, and NOTHING of this script in the transfer path.
--
-- The two are not interchangeable and the log must never let them look it. The
-- modelled transport OVERLAPS with the CPU, which is what a DMAC channel does;
-- the real one here does not, because 57.3 leaves the CPU moving every byte
-- itself. So a "scsi" run measures CORRECTNESS off a real volume and the CPU
-- cost of a PIO transport, and it measures NO rate: every rate-shaped number in
-- this script's report is suppressed rather than printed against a transport
-- that has no model behind it.
local XFER = os.getenv("DLX_XFER") or "model"
local SCSIX = (XFER == "scsi")
-- src/player/scsi.i's SCE_* codes, so a failure names itself.
local SCERRNAME = {[0]="OK", "SELECTION TIMEOUT -- no target answered",
"UNEXPECTED PHASE", "POLL TIMEOUT -- a phase never arrived",
"NON-ZERO SCSI STATUS"}
-- One snapshot per frame tick instead of one at the end of the run. This is a
-- DOCUMENTATION artefact -- 120 PNGs of a paced player, for a recording -- and
-- it is deliberately not on any path tools/bench/check.sh takes. Needs
@@ -548,7 +571,17 @@ local function setup()
SP:write_u32(A_RNG_SZ, RINGSZ)
SP:write_u32(A_PFREC, PREFILL_FR)
SP:write_u32(XF_QD, QDEPTH)
install_transport()
if SCSIX then
-- The stream begins at LBA 0 of the volume, because tools/bench/mkvol.sh
-- lays tmp/stream_disk.bin down from sector 0 and nothing else is on it.
-- It is a WORD the machine reads rather than a constant in xfer.i: a
-- shipping volume has a filesystem in front of the stream, and the base
-- LBA is the one number that changes when it does.
SP:write_u32(A_XFSCSI, 1)
SP:write_u32(A_XSLBA0, 0)
else
install_transport()
end
end
P(string.format("stream.bin=%d B, codebooks %d+%d B, disk %d B, %d frames",
#code, META.cb1_len, META.cb4_len, META.disk_len, META.nframes))
@@ -563,9 +596,20 @@ local function setup()
RINGOWN and (PREFILL_FR.." records (the machine's own)")
or ((PREFILL // 1024).." KB"), META.maxrec))
if RINGOWN then
P(string.format("ring OWNED BY THE 68000 (src/player/ring.i): this script "
.."is a transport with a %d-deep request queue, %d B of "
.."DLX4 index at %06X", QDEPTH, #idxblob, IDXRAM))
if SCSIX then
P(string.format("REAL TRANSPORT (ROADMAP P4b, src/player/xfer.i): the "
.."68000 fetches every record itself with READ(10) off a "
.."CZ-6BS1. This script moves NO bytes and models NO "
.."rate. %d B of DLX4 index at %06X.", #idxblob, IDXRAM))
P(" the queue depth cannot deepen anything here: the transport is "
.."SYNCHRONOUS (xfer.i), so a queued request is drained by the same "
.."instruction stream that would be decoding. FINDINGS 55.3's 1-vs-2 "
.."result is about a transport that OVERLAPS; this one does not.")
else
P(string.format("ring OWNED BY THE 68000 (src/player/ring.i): this script "
.."is a transport with a %d-deep request queue, %d B of "
.."DLX4 index at %06X", QDEPTH, #idxblob, IDXRAM))
end
end
if META.maxrec > RINGSZ then
P("RING TOO SMALL: one record does not fit. stream.s needs a whole record "
@@ -588,13 +632,48 @@ local st, t0, t_rel = "boot", nil, 0
-- would flatter every record by that much.
local tick_t = {}
local pace, min_ahead, min_at = -1, math.huge, -1
-- DLX_XFER=scsi: the machine's seek counter, and the transfer count it stood at
-- when that seek happened. -1 means "not sampled yet", so the first sample
-- rebases without announcing a pass that did not happen.
local xs_nseek, xs_base = -1, 0
local sum_ahead, n_ahead = 0, 0
local slack_series = {}
SUB = emu.add_machine_frame_notifier(function()
local ok, err = pcall(function()
local t = T()
if RINGOWN then transport_service(t) end
if RINGOWN and not SCSIX then transport_service(t) end
if SCSIX and st ~= "boot" then
-- The machine's own count of completed transfers, which is what `nsent`
-- means in a run where this script sends nothing. Sampled rather than
-- derived, so the slack window still closes when the producer runs out of
-- records to place instead of running on into the drain tail.
--
-- REBASED AT EVERY SEEK, and it has to be: XS_NXFER is cumulative over
-- the whole run, so on a second pass it is already >= nframes and the
-- slack sampling below -- gated on `nsent < META.nframes` -- would never
-- fire again. It did not, and the second pass reported a ceiling of 0
-- frames and a build time of -1 ticks, which is an empty series printing
-- as a result. ring.i's own seek counter is the exact place to rebase:
-- a seek discards every outstanding request by definition.
local ns = SP:read_u32(A_NSEEK)
if ns ~= xs_nseek then
-- ANNOUNCE ONLY A SEEK WITH PLAY BEHIND IT. A pass opens with two
-- seeks that are not branch points -- ring_init's, and stream.s's at
-- the top of `outer` -- and both happen before a single record has been
-- fetched. Announcing those numbered the first pass "SEEK PASS 2" and
-- the real one "3". XS_NXFER separates them exactly: a seek taken with
-- transfers already behind it is the one that threw a ring away.
if xs_nseek >= 0 and SP:read_u32(A_XSNXFER) > 0 then
npass = npass + 1
P(string.format("SEEK PASS %d: the machine seeked at %.3f s and is "
.."refilling from empty", npass, t))
end
xs_nseek, xs_base = ns, SP:read_u32(A_XSNXFER)
end
nsent = SP:read_u32(A_XSNXFER) - xs_base
if nsent < 0 then nsent = 0 end
end
if st == "boot" then
if t < 3.0 then return end
setup(); last_t, pf_t0 = t, t; st = "prefill"; return
@@ -794,6 +873,41 @@ SUB = emu.add_machine_frame_notifier(function()
-- PLAYER's loop, not of the medium, and no host-filled run could see
-- it -- the host producer placed records whenever it liked.
local span = (arrival[nissue] or t) - (t0 or t)
if SCSIX then
-- WHAT A REAL TRANSPORT CAN AND CANNOT BE ASKED HERE. The modelled
-- transport knew when every byte landed because it decided; this one
-- does not report, and MAME's device models are functional rather
-- than transfer-timing accurate, so even if it did the number would
-- not be a rate (docs/BENCHMARK.md, 42.5). So CHANNEL IDLE,
-- DEADLINE and REQUIRED PREFILL are not printed at all rather than
-- printed as zeros -- a zero here reads as "the channel never
-- stopped", which would be a claim about a medium this rig has
-- never timed.
local nb, nw = SP:read_u32(A_XSNBYTE), SP:read_u32(A_XSNWIRE)
local nx = SP:read_u32(A_XSNXFER)
P(string.format("REAL TRANSPORT: %d READ(10)s by the 68000, %d B "
.."into the ring", nx, nb))
-- THE COST THE CONTAINER PAYS FOR NOT BEING SECTOR-ALIGNED. A
-- record starts wherever the previous one ended, rounded up to 4;
-- a target answers in 512 B blocks. So the command covers the
-- sectors the record lies in and src/player/scsi.i's window drops
-- the rest. In PIO those bytes cost nothing but wire; they are
-- still bytes the disc moved that no frame contains.
P(string.format("SECTOR OVERHEAD: %d B off the disc for %d B of "
.."record = %.2f%% the medium moved and no frame "
.."contains", nw, nb,
nb > 0 and 100*(nw - nb)/nb or 0))
local e, eat = SP:read_u32(A_XSERR), SP:read_u32(A_XSERRAT)
if e ~= 0 then
P(string.format("TRANSPORT FAILED: SC_ERR=%d on request %d (%s)",
e, eat, SCERRNAME[e] or "see src/player/scsi.i"))
end
if skw > 0 then
P(string.format(" the last seek waited %d polls for the channel "
.."to go quiet before it could start", skw))
end
goto ringdone
end
P(string.format("CHANNEL IDLE: %.1f ms over %d gaps (worst %.1f ms) = "
.."%.1f%% of the %.2f s the transport was needed for; "
.."busy %.1f ms",
@@ -819,6 +933,7 @@ SUB = emu.add_machine_frame_notifier(function()
P(string.format(" the last seek waited %d polls for the channel "
.."to go quiet before it could start", skw))
end
::ringdone::
end
-- The decoder's own spin counter, kept for what it is: evidence that
-- stream.s outran the pipe, NOT evidence of an underrun. See the note
@@ -829,6 +944,24 @@ SUB = emu.add_machine_frame_notifier(function()
-- opposite thing, so the two are never printed in the same words.
P(string.format("UNDERRUNS: %d/%d frames waited past their %d fps slot "
.."(%d polls)", stalls, META.nframes, META.fps, spins))
if SCSIX then
-- AND IT IS VACUOUS HERE, WHICH IS WORTH MORE THAN THE ZERO IS.
-- A synchronous transport cannot underrun by construction: the
-- decoder does not go on until the record is in its hand, because
-- it fetched the record itself. What it does instead is MISS THE
-- SLOT, and that is the NO IDLE line above -- 442 whole ticks of
-- overrun is not a late present, it is a player running at a
-- fraction of its frame rate. Quoting "0 underruns" from a
-- DLX_XFER=scsi run without this line would be the most flattering
-- possible reading of the least flattering result in the tree.
local ticks = pace + 1
P(string.format(" 0 IS VACUOUS: the transport is synchronous, so a "
.."frame cannot start before its record has landed "
.."-- the decoder IS the transport. The honest rate "
.."is %d frames in %d slots of %d fps = %.2f fps.",
META.nframes, ticks, META.fps,
ticks > 0 and META.fps*META.nframes/ticks or 0))
end
-- The other way a paced frame can go wrong, and it is not the same
-- failure. An UNDERRUN is the pipe: the record was not there. This
-- is the CPU: the record was there, the slot was already open, and
@@ -906,6 +1039,18 @@ SUB = emu.add_machine_frame_notifier(function()
end
-- The delivery result. Deadline for record i is release + i/fps.
-- Only this script can compute it, because only this script knows when
-- a byte landed -- and in a DLX_XFER=scsi run it does not. Printing it
-- from an empty arrival table would report "0/120 records late", which
-- is the most flattering possible reading of no data at all.
if SCSIX then
P("DEADLINE: not measured. The transport is in the machine and does "
.."not report arrival times, and MAME's SCSI is not transfer-timing "
.."accurate, so there is no honest deadline to grade against here. "
.."The sharp instrument that DOES survive is the decoder's own "
.."UNDERRUNS count above -- the machine takes it, and it is exact.")
st = "settle"; return
end
local misses, worst, prefill_s = 0, 0.0, 0.0
for i = 0, META.nframes-1 do
local a = arrival[i+1]