Files
Dragon-s-Lair-X68k/tools/bench/xfer_cost.sh
T
prosolis 5921fab118 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
2026-08-24 23:15:53 -07:00

113 lines
5.7 KiB
Bash
Executable File

#!/bin/bash
# What the PIO transport costs the 68000, by subtraction (ROADMAP P4b,
# FINDINGS 58.2).
#
# tools/bench/xfer_cost.sh [ring_kb]
#
# THE MEASUREMENT IS AN A/B AND HAS TO BE. Nothing in the machine can time
# itself finely enough: src/player/clock.i counts V-DISP at 56.69 Hz and the
# thing being priced is a per-BYTE cost. So the same 120 frames are decoded
# twice, from the same ring, by the same src/player/stream.s, with the same
# src/player/ring.i placing every record -- and the ONLY difference is which
# side of the XF_* mailbox answers:
#
# model tools/bench/stream.lua, an unlimited modelled pipe. Bytes appear
# in the ring for free; the emulated time is decode plus ring_poll.
# scsi src/player/xfer.i and src/player/scsi.i, a real MB89352 and a real
# volume. The CPU moves every byte itself.
#
# Both runs are FREE-RUNNING (DLX_PACE=0). A paced run would measure the pace,
# not the work, and the difference this script exists for would vanish into the
# wait loop.
#
# WHAT THE SUBTRACTION IS HONEST ABOUT, and it is worth stating twice: this is a
# count of 68000 CLOCKS, not a delivery rate. It says what the player pays to
# move a byte with its own instruction stream. It says nothing about how fast
# the medium can supply one -- MAME's device models are functional, not
# transfer-timing accurate (docs/BENCHMARK.md, 42.5), and `W`, the clocks a DMAC
# steals per delivered byte, is untouched by every line here.
#
# The two run boundaries are sampled on the machine-frame notifier, so each end
# is up to 17.64 ms coarse (54.5). Against a difference of tens of seconds that
# is under a tenth of a percent, and the analytic cross-check below is what
# actually establishes the figure -- the subtraction only has to agree with it.
set -e
cd "$(dirname "$0")/../.."
RING=${1:-256}
command -v chdman > /dev/null || { echo "needs chdman (mame-tools)"; exit 2; }
echo "=== modelled transport, unlimited pipe"
DLX_PACE=0 DLX_RINGOWN=1 DLX_QDEPTH=2 bash tools/bench/pace_run.sh "$RING" 0 \
> tmp/xfer_cost_model.log 2>&1
echo "=== real transport, a CZ-6BS1 and a real volume"
DLX_PACE=0 DLX_RINGOWN=1 DLX_QDEPTH=2 DLX_XFER=scsi \
bash tools/bench/pace_run.sh "$RING" 0 > tmp/xfer_cost_scsi.log 2>&1
python3 - <<'PY'
import re, sys
def one(tag, log):
t = open(log, "rb").read().decode("latin1")
m = re.search(r"decoded (\d+) frames in ([0-9.]+) s emulated", t)
if not m: sys.exit(f"{log}: no completion line -- the pass did not finish")
return int(m.group(1)), float(m.group(2)), t
nfr, t_model, _ = one("model", "tmp/pace_r256_k0_p0_own.log")
nfr2, t_scsi, ts = one("scsi", "tmp/pace_r256_k0_p0_own_scsi.log")
assert nfr == nfr2, "the two runs decoded different frame counts"
nb = int(re.search(r"REAL TRANSPORT: \d+ READ\(10\)s by the 68000, (\d+) B", ts).group(1))
nw = int(re.search(r"SECTOR OVERHEAD: (\d+) B off the disc", ts).group(1))
CPUHZ = 10_000_000
FPS = 12
budget = CPUHZ / FPS
d = (t_scsi - t_model) * CPUHZ
print(f" {nfr} frames, {nb:,} B of record, {nw:,} B off the disc")
print(f" decode + ring_poll alone {t_model:8.4f} s emulated "
f"= {100*t_model*CPUHZ/nfr/budget:5.1f}% of a {FPS} fps frame")
print(f" ...with the real transport {t_scsi:8.4f} s emulated "
f"= {100*t_scsi*CPUHZ/nfr/budget:5.1f}%")
print(f" TRANSPORT COST: {d:,.0f} clocks = {d/nb:.2f} per delivered byte, "
f"{d/nw:.2f} per byte off the FIFO")
print(f" {d/nfr:,.0f} clk/frame = "
f"{100*d/nfr/budget:.1f}% of a {FPS} fps frame")
# ---- THE CROSS-CHECK, and it is the half that makes the number portable.
# If the measured cost is the 68000's own instruction stream, it must equal the
# 68000's cycle table for the loop in src/player/scsi.i. If it does NOT, the
# difference is time spent waiting on MAME's SPC model -- which is emulator
# behaviour and would not survive contact with a board.
LOOP = [("move.l #SC_PATIENCE,d3", 12), ("move.b SC_SSTS,d0 (xxx).L->Dn", 16),
("btst #0,d0", 10), ("beq.s taken", 10),
("move.b SC_DREG,(a1)+ (xxx).L->(An)+", 20),
("subq.l #1,d7", 8), ("bne.s taken", 10)]
loop = sum(c for _, c in LOOP)
print(f"\n the keep loop in src/player/scsi.i, from the 68000's cycle table:")
for n, c in LOOP: print(f" {c:3d} {n}")
pred = loop * nw / nb
print(f" --- {loop} clk per byte off the FIFO, and the FIFO carries the "
f"dropped\n window bytes too: {pred:.2f} per DELIVERED byte")
err = 100 * abs(pred - d/nb) / (d/nb)
print(f" MEASURED {d/nb:.2f} vs PREDICTED {pred:.2f} -- {err:.1f}% apart, so "
f"the cost is the\n instruction stream and not MAME's SPC: it is a "
f"figure a real board would also pay.")
resid = d/nb - pred
print(f" residual {resid:+.2f} clk/B = {resid*nb/nfr:,.0f} clk/record of "
f"select, CDB, status,\n message and xf_service -- the per-COMMAND cost, "
f"which is the part that does not\n scale with the record.")
print(f"\n AGAINST THE LADDER, in the same units (clocks charged to the CPU "
f"per delivered\n byte). W is the DMAC's steal, 52.5's bracket:")
for W, name in [(5, "single address, bus HELD"), (9, "dual address, held"),
(12, "single address, arbitrated"),
(19, "dual address, arbitrated -- the IPL ROM's own disk ch")]:
print(f" W = {W:2d} {name:52s} {100*W*nb/nfr/budget:5.1f}% of the frame")
print(f" PIO {d/nb:.0f} this rig, measured "
f" {100*d/nfr/budget:15.1f}%")
print(f"\n So the PIO transport is {d/nb/19:.1f}x the WORST DMA configuration "
f"this project has\n found and {d/nb/5:.1f}x the best. P4a is not an "
f"optimisation of this; it is the\n difference between a player and a "
f"slideshow.")
PY