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:
@@ -433,6 +433,77 @@ else
|
||||
echo " SKIPPED: no chdman (ships with mame-tools) -- cannot build the volume"
|
||||
fi
|
||||
|
||||
echo "--- session 26: the ring is filled off a real SCSI volume (FINDINGS 58) ---"
|
||||
# ROADMAP P4b. The stage above shows the 68000 can READ the disc. This shows it
|
||||
# can RUN off it: src/player/xfer.i sits behind src/player/ring.i's XF_* mailbox
|
||||
# in place of tools/bench/stream.lua's modelled transport, and the same 120
|
||||
# frames are decoded out of the same 256 KB ring with NOTHING outside the
|
||||
# machine in the transfer path -- no host file, no modelled rate, no synthesised
|
||||
# ack.
|
||||
#
|
||||
# WHAT IS GATED, and it is correctness rather than rate on purpose:
|
||||
# * pixel-exact, which is the only test that can see a wrong record: the
|
||||
# window in scsi.i decides which of a sector's bytes reach the ring, and a
|
||||
# window off by one byte desyncs the bitstream rather than faulting (49.2);
|
||||
# * the SAME 18 wraps and 14.7 KB mean hole -- ring.i's placement policy must
|
||||
# not be able to tell which transport answered it, and this is the assertion
|
||||
# that says it could not;
|
||||
# * every record accounted for: 120 READ(10)s, 4,488,588 B into the ring, and
|
||||
# 4,548,608 B off the disc. The two byte counts differ by 1.34% because a
|
||||
# record is not a sector, and that gap is a delivery cost (58.3) -- gating
|
||||
# both numbers means neither can drift silently into the other;
|
||||
# * a real mid-stream SEEK with the real transport, in the second pass. This
|
||||
# is the one path that could not exist before: ring_seek waits for the
|
||||
# channel to go quiet, and with the transport INSIDE the machine the only
|
||||
# thing that can retire an outstanding request is that wait loop itself.
|
||||
#
|
||||
# NOTHING HERE IS GATED ON RATE and nothing here can be. What the run DOES cost
|
||||
# is printed by tools/bench/xfer_cost.sh and recorded in FINDINGS 58.2; it is a
|
||||
# measurement, and a change in it is a re-derivation there rather than a red
|
||||
# light here. Skipped rather than failed when chdman is absent.
|
||||
if command -v chdman > /dev/null; then
|
||||
DLX_PACE=0 DLX_RINGOWN=1 DLX_QDEPTH=2 DLX_XFER=scsi \
|
||||
bash tools/bench/pace_run.sh 256 0 > tmp/p4b_check.log 2>&1 || {
|
||||
echo "FAIL: the 68000 could not run the ring off a real SCSI volume."
|
||||
tail -12 tmp/p4b_check.log; exit 1; }
|
||||
grep -aE "REAL TRANSPORT:|SECTOR OVERHEAD|ring: " tmp/p4b_check.log \
|
||||
| sed "s/^ *//;s/^/ /"
|
||||
grep -aq "TRANSPORT FAILED" tmp/p4b_check.log && {
|
||||
echo "FAIL: a record's READ(10) reported an error."; exit 1; }
|
||||
grep -aq "REAL TRANSPORT: 120 READ(10)s by the 68000, 4488588 B into the ring" \
|
||||
tmp/p4b_check.log || {
|
||||
echo "FAIL: the 68000 did not fetch all 120 records, or did not fetch"
|
||||
echo " 4,488,588 B of them. A short record is a desync, not a shortfall."
|
||||
exit 1; }
|
||||
grep -aq "SECTOR OVERHEAD: 4548608 B off the disc" tmp/p4b_check.log || {
|
||||
echo "FAIL: the bytes the DISC moved are no longer 4,548,608. A record is"
|
||||
echo " not a sector; this is the covering-sector read, and if it moved"
|
||||
echo " then either the layout or scsi.i's window did. See FINDINGS 58.3."
|
||||
exit 1; }
|
||||
grep -aq "ring: 18 wraps" tmp/p4b_check.log || {
|
||||
echo "FAIL: the placement policy tiled this container differently with a"
|
||||
echo " real transport behind it than with a modelled one. ring.i is"
|
||||
echo " not supposed to be able to tell them apart."; exit 1; }
|
||||
grep -aq "^OK" tmp/p4b_check.log || {
|
||||
echo "FAIL: the pass off the SCSI volume was not pixel-exact."
|
||||
tail -4 tmp/p4b_check.log; exit 1; }
|
||||
DLX_PACE=2 DLX_RINGOWN=1 DLX_QDEPTH=2 DLX_ITER=2 DLX_XFER=scsi \
|
||||
DLX_SECONDS=240 bash tools/bench/pace_run.sh 256 0 \
|
||||
> tmp/p4b_seek_check.log 2>&1 || {
|
||||
echo "FAIL: the seek pass off the SCSI volume did not complete."
|
||||
tail -12 tmp/p4b_seek_check.log; exit 1; }
|
||||
grep -aE "SEEK PASS|IS VACUOUS" tmp/p4b_seek_check.log | sed "s/^ *//;s/^/ /"
|
||||
grep -aq "SEEK PASS 2" tmp/p4b_seek_check.log || {
|
||||
echo "FAIL: no real seek -- the second pass never threw its ring away, so"
|
||||
echo " ring_seek's quiet-wait was never asked to retire an outstanding"
|
||||
echo " transfer and this gated nothing."; exit 1; }
|
||||
grep -aq "^OK" tmp/p4b_seek_check.log || {
|
||||
echo "FAIL: the decode after a seek off the SCSI volume was not pixel-exact."
|
||||
tail -4 tmp/p4b_seek_check.log; exit 1; }
|
||||
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
|
||||
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# Build the SCSI VOLUME the P4 rigs read, and the blank card ROM MAME needs to
|
||||
# instantiate the card. Sourced-by-calling from tools/bench/scsi_run.sh and
|
||||
# tools/bench/pace_run.sh so there is ONE copy of the layout.
|
||||
#
|
||||
# tools/bench/mkvol.sh [container.dlx]
|
||||
#
|
||||
# ONE COPY, ON PURPOSE. The volume is tmp/stream_disk.bin -- byte for byte the
|
||||
# file the host-file ring rig reads -- laid out as 512 B sectors. If two scripts
|
||||
# each built it, a difference between the SCSI rig and the modelled-transport rig
|
||||
# could be a difference in what they were reading, and the whole value of running
|
||||
# both is that it cannot be. This tree has already paid twice for a transform
|
||||
# with two copies of itself (FINDINGS 49.7.5, and check.sh's dlxload note).
|
||||
#
|
||||
# THE BLANK BOOT ROM is the substitution session 25 argued for and it is
|
||||
# unchanged: MAME refuses to instantiate the CZ-6BS1 without an 8 KB
|
||||
# `scsiexrom.bin` (CRC 7be488de) that the player never executes, so a zero-filled
|
||||
# placeholder goes on a SEPARATE rompath and the user's romset is untouched.
|
||||
# MAME prints WRONG CHECKSUMS, as it should. DO NOT reuse this rompath for
|
||||
# anything that boots from the card or calls SCSI IOCS -- those DO execute it.
|
||||
set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
DLX=${1:-tmp/rc_fr_singe_scsi_span.dlx}
|
||||
|
||||
[ -f tmp/stream_disk.bin ] || python3 tools/bench/prep_stream.py "$DLX" > /dev/null
|
||||
if [ ! -f tmp/dlxdisk.chd ] || [ tmp/stream_disk.bin -nt tmp/dlxdisk.chd ]; then
|
||||
python3 - <<'PY'
|
||||
d = open("tmp/stream_disk.bin", "rb").read()
|
||||
n = (len(d) + 511) // 512
|
||||
open("tmp/dlxdisk.img", "wb").write(d + b"\0" * (n * 512 - len(d)))
|
||||
print(f" disc image: {len(d)} B of records -> {n} sectors")
|
||||
PY
|
||||
rm -f tmp/dlxdisk.chd
|
||||
chdman createhd -i tmp/dlxdisk.img -o tmp/dlxdisk.chd -ss 512 > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
mkdir -p tmp/p4roms/x68k_cz6bs1
|
||||
[ -f tmp/p4roms/x68k_cz6bs1/scsiexrom.bin ] || \
|
||||
head -c 8192 /dev/zero > tmp/p4roms/x68k_cz6bs1/scsiexrom.bin
|
||||
+42
-4
@@ -28,6 +28,13 @@
|
||||
# needs DLX_PACE=2, because rebasing the frame clock across a pass is the
|
||||
# machine's to do and a host-written tick would carry on counting.
|
||||
#
|
||||
# DLX_XFER=scsi replaces the MODELLED transport with a real one (ROADMAP P4b,
|
||||
# src/player/xfer.i): the machine gets a CZ-6BS1 and the same volume the SCSI
|
||||
# gate reads, this script stops moving bytes altogether, and every record is
|
||||
# fetched by the 68000 with READ(10). It needs DLX_RINGOWN=1 -- the mailbox it
|
||||
# answers is ring.i's -- and it FORBIDS a modelled rate, because there is no
|
||||
# longer anything for one to model.
|
||||
#
|
||||
# DLX_PACE selects WHO KEEPS THE TIME: 1 (default) is the host writing the tick,
|
||||
# 2 is the 68000 writing it off the CRTC's V-DISP (ROADMAP P3, FINDINGS 54).
|
||||
# Everything else about the run is identical, which is the whole point -- the
|
||||
@@ -41,6 +48,30 @@ DLX=${DLX:-tmp/rc_fr_singe_scsi_span.dlx}
|
||||
PACE=${DLX_PACE:-1}
|
||||
OWN=${DLX_RINGOWN:-0}
|
||||
ITERS=${DLX_ITER:-1}
|
||||
XFER=${DLX_XFER:-model}
|
||||
# EMULATED seconds the run is allowed. A pass that is cut short compares a
|
||||
# half-drawn screen and reads as a wrap bug, so this is raised deliberately
|
||||
# rather than left to a timeout: a DLX_XFER=scsi pass costs ~46 s of emulated
|
||||
# time against the modelled transport's ~7, because the CPU moves every byte
|
||||
# itself (FINDINGS 58.2), and two of them do not fit in 90.
|
||||
SECS=${DLX_SECONDS:-90}
|
||||
if [ "$XFER" = scsi ]; then
|
||||
[ "$OWN" = 1 ] || { echo "DLX_XFER=scsi needs DLX_RINGOWN=1: the transport in"
|
||||
echo "src/player/xfer.i answers src/player/ring.i's mailbox, and with the"
|
||||
echo "host owning the ring there is no mailbox to answer."; exit 2; }
|
||||
# A rate is not merely ignored here, it is REFUSED. The bytes now arrive on
|
||||
# the emulated machine's own time, and a run labelled "488 KB/s" that did not
|
||||
# deliver at 488 KB/s is exactly the kind of number this project has twice
|
||||
# paid for. There is no rate in a DLX_XFER=scsi run, and the log says so.
|
||||
[ "$KBPS" = 0 ] || { echo "DLX_XFER=scsi takes kbps 0. The transport is real,"
|
||||
echo "so nothing here delivers at a modelled rate -- and MAME's device"
|
||||
echo "models are functional, not transfer-timing accurate, so the rate it"
|
||||
echo "DOES deliver at is not a measurement either (docs/BENCHMARK.md)."
|
||||
exit 2; }
|
||||
command -v chdman > /dev/null || { echo "DLX_XFER=scsi needs chdman (ships"
|
||||
echo "with mame-tools) to build the volume."; exit 2; }
|
||||
bash tools/bench/mkvol.sh "$DLX"
|
||||
fi
|
||||
if [ "$OWN" = 1 ] && [ "$ITERS" != 1 ] && [ "$PACE" != 2 ]; then
|
||||
echo "DLX_ITER>1 needs DLX_PACE=2: the frame clock is rebased per pass by"
|
||||
echo "src/player/stream.s, and a host-written tick would go on counting"
|
||||
@@ -54,6 +85,11 @@ TAG="r${RING}_k${KBPS}${CUT_AT:+_cut${CUT_AT}x${CUT_FR}}"
|
||||
if [ "$PACE" != 1 ]; then TAG="${TAG}_p$PACE"; fi
|
||||
if [ "$OWN" = 1 ]; then TAG="${TAG}_own"; fi
|
||||
if [ "$ITERS" != 1 ]; then TAG="${TAG}_x$ITERS"; fi
|
||||
if [ "$XFER" != model ]; then TAG="${TAG}_$XFER"; fi
|
||||
MAMEX=()
|
||||
if [ "$XFER" = scsi ]; then
|
||||
MAMEX=(-exp1 cz6bs1 -rompath "$HOME/mame/roms;./p4roms" -hard dlxdisk.chd)
|
||||
fi
|
||||
|
||||
tools/vasm/vasmm68k_mot -Fbin -o tmp/stream.bin src/player/stream.s > /dev/null
|
||||
[ -f tmp/stream_disk.bin ] || python3 tools/bench/prep_stream.py "$DLX" > tmp/prep_stream.log
|
||||
@@ -63,13 +99,15 @@ mkdir -p "tmp/snap_pace_$TAG"; rm -f "tmp/snap_pace_$TAG/x68000"/*.png
|
||||
# command and the run dies with "SDL_VIDEODRIVER=dummy: command not found".
|
||||
CUTENV=(); [ -n "$CUT_AT" ] && CUTENV=(DLX_CUT_AT="$CUT_AT" DLX_CUT_FR="$CUT_FR")
|
||||
( cd tmp && env DLX_PACE=$PACE DLX_RING_KB=$RING DLX_STREAM_KBPS=$KBPS \
|
||||
DLX_RINGOWN=$OWN DLX_ITER=$ITERS \
|
||||
DLX_RINGOWN=$OWN DLX_ITER=$ITERS DLX_XFER=$XFER \
|
||||
${DLX_PREFILL_FR:+DLX_PREFILL_FR=$DLX_PREFILL_FR} \
|
||||
"${CUTENV[@]}" DLX_SLACK_CSV="slack_$TAG.csv" \
|
||||
SDL_VIDEODRIVER=dummy stdbuf -oL timeout -k 5 900 \
|
||||
mame x68000 -bios ipl10 -ramsize 2M -video soft -window -sound none \
|
||||
mame x68000 -bios ipl10 "${MAMEX[@]}" -ramsize 2M -video soft -window \
|
||||
-sound none \
|
||||
-nothrottle -plugins -autoboot_script ../tools/bench/stream.lua \
|
||||
-snapshot_directory "./snap_pace_$TAG" -snapview native -seconds_to_run 90 \
|
||||
-snapshot_directory "./snap_pace_$TAG" -snapview native \
|
||||
-seconds_to_run $SECS \
|
||||
> "pace_$TAG.log" 2>&1 )
|
||||
# The completion marker is not optional: a run killed mid-decode compares a
|
||||
# half-drawn screen and reads as a wrap bug rather than as a truncated run.
|
||||
@@ -77,6 +115,6 @@ grep -q "snapshot taken" "tmp/pace_$TAG.log" || {
|
||||
echo "FAIL($TAG): no snapshot marker -- the pass did not complete."
|
||||
tail -6 "tmp/pace_$TAG.log"; exit 1; }
|
||||
echo "=== $TAG"
|
||||
grep -aE "decoder (SELF-PACED|PACED|FREE)|FRAME CLOCK|ring: |UNDERRUNS|NO IDLE|SEEK SLACK|RING-BOUND|RATE-BOUND|BUILD TIME|PIPE CUT|DEADLINE|REQUIRED|MACHINE-OWNED|PREFILL:|CHANNEL IDLE|MISPLACED|SEEK PASS" \
|
||||
grep -aE "decoder (SELF-PACED|PACED|FREE)|FRAME CLOCK|ring: |UNDERRUNS|NO IDLE|SEEK SLACK|RING-BOUND|RATE-BOUND|BUILD TIME|PIPE CUT|DEADLINE|REQUIRED|MACHINE-OWNED|PREFILL:|CHANNEL IDLE|MISPLACED|SEEK PASS|REAL TRANSPORT|SECTOR OVERHEAD|TRANSPORT FAILED|IS VACUOUS" \
|
||||
"tmp/pace_$TAG.log" | sed "s/\[STR\] / /"
|
||||
python3 tools/bench/verify_decode.py "$DLX" --snap "tmp/snap_pace_$TAG" | tail -2
|
||||
|
||||
+4
-19
@@ -35,25 +35,10 @@ set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
DLX=${1:-tmp/rc_fr_singe_scsi_span.dlx}
|
||||
|
||||
# The container's frame records, laid out as a disc. prep_stream.py already
|
||||
# writes exactly this file for the ring rig, so the SCSI volume and the host-file
|
||||
# pipe carry byte-identical bytes and a difference between the two rigs cannot be
|
||||
# a difference in what they are reading.
|
||||
[ -f tmp/stream_disk.bin ] || python3 tools/bench/prep_stream.py "$DLX" > /dev/null
|
||||
if [ ! -f tmp/dlxdisk.chd ] || [ tmp/stream_disk.bin -nt tmp/dlxdisk.chd ]; then
|
||||
python3 - <<'PY'
|
||||
d = open("tmp/stream_disk.bin", "rb").read()
|
||||
n = (len(d) + 511) // 512
|
||||
open("tmp/dlxdisk.img", "wb").write(d + b"\0" * (n * 512 - len(d)))
|
||||
print(f" disc image: {len(d)} B of records -> {n} sectors")
|
||||
PY
|
||||
rm -f tmp/dlxdisk.chd
|
||||
chdman createhd -i tmp/dlxdisk.img -o tmp/dlxdisk.chd -ss 512 > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
mkdir -p tmp/p4roms/x68k_cz6bs1
|
||||
[ -f tmp/p4roms/x68k_cz6bs1/scsiexrom.bin ] || \
|
||||
head -c 8192 /dev/zero > tmp/p4roms/x68k_cz6bs1/scsiexrom.bin
|
||||
# The container's frame records, laid out as a disc, and the blank card ROM.
|
||||
# Both are tools/bench/mkvol.sh's, shared with tools/bench/pace_run.sh so the
|
||||
# SCSI rig and the host-file ring rig cannot be reading different bytes.
|
||||
bash tools/bench/mkvol.sh "$DLX"
|
||||
|
||||
tools/vasm/vasmm68k_mot -Fbin -o tmp/scsigate.bin src/player/scsigate.s > /dev/null
|
||||
|
||||
|
||||
+150
-5
@@ -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]
|
||||
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user