Put the ring on the 68000, and find the disc stops whenever the player is not asking
ROADMAP P5. The loader moved in session 21 and the frame clock in 22; the ring producer was the last policy living outside the machine. src/player/ring.i does `aligned` placement, the descriptor ring, a prefill, 51.2's slack rule and a seek, and the host keeps only the transport. It needed a container change. `aligned` asks whether the next record fits before the end of the ring -- a length asked BEFORE the record is fetched -- and every reader in this tree answered that by walking the frame stream, which is exactly what a player streaming off a disc cannot do. DLX4 carries nframes u16 record lengths in the scene header. Frame payloads are byte-identical to the DLX3 encode, so no fitted constant moves; the scene header goes 5,920 to 6,164 B. The producer reproduces the host's tiling exactly: 18 wraps, 14.7 KB mean hole, pixel-exact, a third independent implementation of the same policy. What it exposed is bigger than the item. A channel only moves bytes while it has a request and only the CPU can issue one, so the disc stands still between records by an amount the PLAYER sets, not the medium -- and no host-filled run could see it. At 488 KB/s in a 256 KB ring a one-deep request queue gives away 6.8% of the pipe and underruns 59 of 120 frames; two-deep gives away 3.4% and underruns none. The container's whole surplus over the wire is 8.7%, so the player's own loop was spending most of the slack a branch point saves up. Prefill is the weaker lever: six records of it still leaves 24 underruns. Three silent bugs are recorded in FINDINGS 55.7 -- all produced wrong pixels or a desync rather than a fault -- plus a rig one: MAME renders a screen line by line, so snapshotting the frame the decoder finished in captures a tear that reads exactly like a decoder bug. check.sh gains the machine-owned ring and a seek with the decode after it. decode.bin is unchanged at 1,296 B and a host-filled run executes none of the new code, so every FINDINGS 49/51 figure stands. ALL GREEN before and after. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -338,4 +338,72 @@ grep -q "^OK" tmp/selfpace_check.log || {
|
||||
echo " frames start; if it changed WHAT they draw, the interrupt is"
|
||||
echo " corrupting decoder state."; tail -4 tmp/selfpace_check.log; exit 1; }
|
||||
|
||||
echo "--- session 23: the 68000 fills its own ring (FINDINGS 55) ---"
|
||||
# ROADMAP P5. Until now the RING was filled by tools/bench/stream.lua: the host
|
||||
# held the record index, chose where each record went, wrote the descriptor and
|
||||
# advertised it. A player has no host. src/player/ring.i does all of that on the
|
||||
# 68000, out of the DLX4 record index in the scene header, and this script keeps
|
||||
# only the part that is not the CPU's -- a transport that answers one request at
|
||||
# a time at a modelled rate.
|
||||
#
|
||||
# WHAT IS GATED:
|
||||
# * pixel-exact, which is the only test that can see a wrong placement: the
|
||||
# block loop reads with a monotonically increasing a0 and no bounds check,
|
||||
# so a record placed over one the decoder has not finished corrupts pixels
|
||||
# rather than faulting (49.2);
|
||||
# * the host AUDITS every placement against its own index and its own list of
|
||||
# live records, and refuses the run on the first disagreement;
|
||||
# * the wrap policy still produces the SAME 18 wraps and 14.7 KB mean hole the
|
||||
# host producer produced in FINDINGS 49.4 -- a third independent
|
||||
# implementation of `aligned` landing on the same tiling;
|
||||
# * zero underruns at a two-deep request queue, which is the finding: a
|
||||
# one-deep queue leaves the channel idle between records and underran 59 of
|
||||
# 120 frames on this same container and rate.
|
||||
DLX_PACE=2 DLX_RINGOWN=1 DLX_QDEPTH=2 bash tools/bench/pace_run.sh 256 488 \
|
||||
> tmp/ringown_check.log 2>&1 || {
|
||||
echo "FAIL: the machine-owned ring pass did not complete."
|
||||
tail -10 tmp/ringown_check.log; exit 1; }
|
||||
grep -aE "MACHINE-OWNED|PREFILL:|CHANNEL IDLE|UNDERRUNS|SEEK SLACK" tmp/ringown_check.log \
|
||||
| sed "s/\[STR\] / /"
|
||||
grep -q "MISPLACED" tmp/ringown_check.log && {
|
||||
echo "FAIL: the 68000 placed a record over one the decoder still owned."
|
||||
exit 1; }
|
||||
grep -q "UNDERRUNS: 0/120" tmp/ringown_check.log || {
|
||||
echo "FAIL: the machine-owned ring underran at a two-deep queue. That is the"
|
||||
echo " configuration FINDINGS 55 says keeps the channel busy; if it no"
|
||||
echo " longer does, the poll site in src/player/stream.s moved."
|
||||
exit 1; }
|
||||
grep -q "ring: 18 wraps" tmp/ringown_check.log || {
|
||||
echo "FAIL: the machine's own \`aligned\` no longer tiles this container the"
|
||||
echo " way FINDINGS 49.4's host producer did (18 wraps). The policy is"
|
||||
echo " meant to be the SAME policy in a different place."
|
||||
exit 1; }
|
||||
grep -q "^OK" tmp/ringown_check.log || {
|
||||
echo "FAIL: the machine-owned ring pass was not pixel-exact -- a record was"
|
||||
echo " placed or described wrongly."; tail -4 tmp/ringown_check.log
|
||||
exit 1; }
|
||||
|
||||
echo "--- session 23: a seek, and the decode after it (FINDINGS 55) ---"
|
||||
# The branch point rehearsed. A second pass over the scene begins with a real
|
||||
# seek in src/player/ring.i: the channel is waited quiet, the ring is declared
|
||||
# empty, the disc address of record 0 comes out of the index rather than from a
|
||||
# walk, and the whole lookahead 51.3 says takes seconds of play to accumulate is
|
||||
# thrown away and rebuilt from the prefill. What is gated afterwards is the one
|
||||
# thing that can see a wrong seek: the last frame of the SECOND pass has to be
|
||||
# pixel-exact, and a SKIP block is a claim about the previous frame, so it is
|
||||
# only right if every frame after the seek was.
|
||||
DLX_PACE=2 DLX_RINGOWN=1 DLX_QDEPTH=2 DLX_ITER=2 \
|
||||
bash tools/bench/pace_run.sh 256 488 > tmp/ringseek_check.log 2>&1 || {
|
||||
echo "FAIL: the seek pass did not complete."; tail -10 tmp/ringseek_check.log
|
||||
exit 1; }
|
||||
grep -aE "SEEK PASS|CHANNEL IDLE|UNDERRUNS" tmp/ringseek_check.log | sed "s/\[STR\] / /"
|
||||
grep -q "SEEK PASS 2" tmp/ringseek_check.log || {
|
||||
echo "FAIL: no second pass -- the seek never happened, so this gated nothing."
|
||||
exit 1; }
|
||||
grep -q "UNDERRUNS: 0/120" tmp/ringseek_check.log || {
|
||||
echo "FAIL: the pass after the seek underran."; exit 1; }
|
||||
grep -q "^OK" tmp/ringseek_check.log || {
|
||||
echo "FAIL: the decode after the seek was not pixel-exact."
|
||||
tail -4 tmp/ringseek_check.log; exit 1; }
|
||||
|
||||
echo "ALL GREEN"
|
||||
|
||||
+25
-1
@@ -16,6 +16,18 @@
|
||||
# kbps 0 = unlimited pipe. There is no default rate anywhere in this tree
|
||||
# (FINDINGS 50) and there is none here either.
|
||||
#
|
||||
# DLX_RINGOWN=1 hands the RING to the 68000 as well (ROADMAP P5,
|
||||
# src/player/ring.i): this script's Lua stops placing records and becomes a
|
||||
# transport that answers one request at a time. DLX_PREFILL_FR is then the
|
||||
# prefill policy, in whole records. It needs a DLX4 container, because the
|
||||
# machine cannot learn a record's length by walking a stream it has not fetched.
|
||||
#
|
||||
# DLX_ITER=2 runs the scene TWICE, which under DLX_RINGOWN means a real seek
|
||||
# between the passes: the channel goes quiet, the ring is declared empty and the
|
||||
# whole accumulated lookahead is thrown away and rebuilt from the prefill. It
|
||||
# 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_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
|
||||
@@ -27,11 +39,21 @@ RING=${1:?ring KB}; KBPS=${2:?pipe KB/s, or 0 for unlimited}
|
||||
CUT_AT=$3; CUT_FR=${4:-1}
|
||||
DLX=${DLX:-tmp/rc_fr_singe_scsi_span.dlx}
|
||||
PACE=${DLX_PACE:-1}
|
||||
OWN=${DLX_RINGOWN:-0}
|
||||
ITERS=${DLX_ITER:-1}
|
||||
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"
|
||||
echo "through the seek and open every slot of the second pass at once."
|
||||
exit 2
|
||||
fi
|
||||
TAG="r${RING}_k${KBPS}${CUT_AT:+_cut${CUT_AT}x${CUT_FR}}"
|
||||
# The default tag is left ALONE when the host keeps the time: tools/bench/
|
||||
# pace_sweep.sh reads tmp/pace_r<ring>_k<kbps>.log by name, and renaming the
|
||||
# host-paced logs would break a sweep that has nothing to do with this option.
|
||||
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
|
||||
|
||||
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
|
||||
@@ -41,6 +63,8 @@ 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_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 \
|
||||
@@ -53,6 +77,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" \
|
||||
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" \
|
||||
"tmp/pace_$TAG.log" | sed "s/\[STR\] / /"
|
||||
python3 tools/bench/verify_decode.py "$DLX" --snap "tmp/snap_pace_$TAG" | tail -2
|
||||
|
||||
@@ -34,7 +34,7 @@ a = ap.parse_args()
|
||||
|
||||
d = DLX(a.container)
|
||||
if d.version < 3:
|
||||
sys.exit(f"{a.container} is DLX{d.version}: load.i checks for the 'DLX3' magic")
|
||||
sys.exit(f"{a.container} is DLX{d.version}: load.i wants DLX3 or DLX4")
|
||||
if d.idx_bytes != 1:
|
||||
sys.exit("2-byte codebook indices: load.i expands one source byte per pixel")
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ This writes three files instead:
|
||||
filesystem and feeds it into a bounded ring, so the emulated
|
||||
machine's RAM stops bounding how much of a window can be
|
||||
tested. A stock 2 MB machine can now run all 120 frames.
|
||||
<out>_idx.bin the DLX4 RECORD INDEX, nframes u16 big-endian, straight out
|
||||
of the container's scene header. This is what the 68000
|
||||
producer reads (src/player/ring.i); it is not derived here,
|
||||
because deriving it is precisely what a player cannot do.
|
||||
<out>_meta.lua geometry, and the record index.
|
||||
|
||||
THE RECORD INDEX IS NOT A CONVENIENCE. src/player/stream.s takes each frame's
|
||||
@@ -71,12 +75,34 @@ for (o, n) in d.frames:
|
||||
index.append((start, len(disk) - start))
|
||||
open(a.out + "_disk.bin", "wb").write(bytes(disk))
|
||||
|
||||
# THE CONTAINER'S OWN INDEX, and it is checked against this layout rather than
|
||||
# regenerated from it. src/player/ring.i walks the disk with a running sum of
|
||||
# these lengths and never reads a record's length word before fetching it, so a
|
||||
# container index that disagreed with the disk image by one byte would place
|
||||
# every later record at the wrong address -- and the block loop reads without a
|
||||
# bounds check (49.2), so the symptom would be wrong pixels, not a fault.
|
||||
if d.has_index:
|
||||
want = [ln // 4 for _, ln in index]
|
||||
if d.index != want:
|
||||
bad = next(i for i in range(len(want)) if d.index[i] != want[i])
|
||||
sys.exit(f"{a.container}: the DLX4 index disagrees with this disk "
|
||||
f"layout at record {bad}: {d.index[bad]} vs {want[bad]} "
|
||||
f"longwords")
|
||||
open(a.out + "_idx.bin", "wb").write(
|
||||
b"".join(q.to_bytes(2, "big") for q in d.index))
|
||||
else:
|
||||
if os.path.exists(a.out + "_idx.bin"):
|
||||
os.remove(a.out + "_idx.bin") # a stale index is worse than none
|
||||
|
||||
rec = np.array([n for _, n in index])
|
||||
with open(a.out + "_meta.lua", "w") as fh:
|
||||
fh.write("-- generated by tools/bench/prep_stream.py -- do not edit\nreturn {\n")
|
||||
fh.write(f" W={d.W}, H={d.H}, fps={d.fps}, nframes={d.nframes}, dark={dark},\n")
|
||||
fh.write(f" cb1_len={cb1.nbytes}, cb4_len={cb4.nbytes}, pal_len={palb.nbytes},\n")
|
||||
fh.write(f" disk_len={len(disk)}, maxrec={int(rec.max())},\n")
|
||||
fh.write(f" dlx_version={d.version}, "
|
||||
f"has_index={'true' if d.has_index else 'false'},\n")
|
||||
fh.write(f" padrec={{{','.join(str(ln) for _, ln in index)}}},\n")
|
||||
fh.write(" index={\n")
|
||||
for off, ln in index:
|
||||
fh.write(f" {{off={off}, len={ln}}},\n")
|
||||
@@ -89,5 +115,8 @@ print(f" disk image {len(disk):,} B -> {a.out}_disk.bin "
|
||||
f"(records: min {rec.min():,} median {int(np.median(rec)):,} "
|
||||
f"max {rec.max():,})")
|
||||
print(f" wire rate {rec.mean()*d.fps/1024:.1f} KB/s video at {d.fps} fps")
|
||||
if d.has_index:
|
||||
print(f" DLX4 record index: {2*d.nframes:,} B of scene header, and it "
|
||||
f"agrees with the disk image on all {d.nframes} records")
|
||||
print(f" A ring must hold one whole record contiguously: >= {rec.max():,} B "
|
||||
f"({rec.max()/1024:.1f} KB) before any policy or prefill.")
|
||||
|
||||
+421
-17
@@ -52,7 +52,18 @@
|
||||
-- no provenance that was never a bus measurement at all
|
||||
-- (FINDINGS 42.1). A default is how a folklore number ends up
|
||||
-- silently underneath a table nobody restates it in.
|
||||
-- DLX_PREFILL_KB bytes to deliver before releasing the CPU (default 0)
|
||||
-- DLX_PREFILL_KB bytes to deliver before releasing the CPU (default 0).
|
||||
-- Host-owned ring only: with DLX_RINGOWN the machine does
|
||||
-- its own prefill and DLX_PREFILL_FR is the knob.
|
||||
-- DLX_RINGOWN 1 = THE 68000 OWNS THE RING (ROADMAP P5,
|
||||
-- src/player/ring.i). This script stops placing records and
|
||||
-- becomes a TRANSPORT: it answers one request at a time,
|
||||
-- delivering XF_LEN bytes from XF_OFF to XF_DST in
|
||||
-- len/rate seconds, and the machine decides which record,
|
||||
-- where in the ring, and when. It also AUDITS every
|
||||
-- placement the machine makes against its own index.
|
||||
-- DLX_PREFILL_FR whole records the machine's prefill waits for before it
|
||||
-- releases the decoder (default 2; see ring_prefill)
|
||||
-- DLX_PACE 1 = hold the decoder to META.fps, with the HOST writing
|
||||
-- the tick (default 0 = free-run)
|
||||
-- 2 = hold it to META.fps with the 68000 writing its own
|
||||
@@ -101,6 +112,26 @@ local STALLS, SPINS, DESC = 0x1802C, 0x18030, 0x18100
|
||||
local PACE, PACEON, CLKON = 0x18034, 0x18038, 0x1803C
|
||||
local CLK_VDISP, CLK_FPS, CLK_ERR = 0x18064, 0x18068, 0x1806C
|
||||
local LATEFR, LATEMAX, LATE1ST = 0x18080, 0x18084, 0x18088
|
||||
-- src/player/ring.i. The transport mailbox, then the producer's state and its
|
||||
-- instruments. Nothing below 0x18300 changed: the decoder's handshake with the
|
||||
-- ring (FR_HEAD/FR_TAIL/DESC) is the same one FINDINGS 49 and 51 measured, which
|
||||
-- is what makes a self-filled run a test of ring.i and not of a new rig.
|
||||
local XF_SLOT, XF_SLSZ = 0x18300, 16
|
||||
local XF_GO, XF_ACK, XF_QD = 0x18320, 0x18324, 0x18328
|
||||
local A_RINGOWN, A_RNG_B, A_RNG_SZ, A_IDX_B = 0x1832C, 0x18330, 0x18334, 0x18338
|
||||
local A_RQ_NEXT, A_WCUR, A_RCUR = 0x1833C, 0x18340, 0x18344
|
||||
local A_NHOLE, A_NHOLEB, A_NFULL = 0x18354, 0x18358, 0x1835C
|
||||
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
|
||||
-- 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
|
||||
-- executing in that gap, so anything written below ~$20000 can be overwritten
|
||||
-- before the 68000 ever sees it. That cost an hour: the machine read a table of
|
||||
-- zeroes and asked the transport for a 0-byte record.
|
||||
local IDXRAM = 0x30000 -- the DLX4 index, in RAM
|
||||
local DESCN = 64
|
||||
local CB1, CB4 = 0x20000, 0x22000
|
||||
local RING = 0x40000
|
||||
@@ -125,6 +156,21 @@ local PACED = (os.getenv("DLX_PACE") == "1") or SELFCLK
|
||||
local CUT_AT = tonumber(os.getenv("DLX_CUT_AT") or "")
|
||||
local CUT_FR = tonumber(os.getenv("DLX_CUT_FR") or "") or 1
|
||||
local SLACK_CSV = os.getenv("DLX_SLACK_CSV")
|
||||
local RINGOWN = (os.getenv("DLX_RINGOWN") == "1")
|
||||
local PREFILL_FR = tonumber(os.getenv("DLX_PREFILL_FR") or "") or 2
|
||||
-- Passes over the scene. With DLX_RINGOWN each pass after the first begins
|
||||
-- with a REAL seek in src/player/stream.s: the channel is waited quiet, the
|
||||
-- ring is declared empty and the lookahead 51.3 says takes seconds of play to
|
||||
-- accumulate is thrown away and rebuilt from the prefill. The check that
|
||||
-- matters afterwards is the one this rig always makes -- the last frame of the
|
||||
-- last pass has to be pixel-exact, and a SKIP block is a claim about the
|
||||
-- previous frame, so it is only right if every frame after the seek was.
|
||||
local ITERS = tonumber(os.getenv("DLX_ITER") or "") or 1
|
||||
-- How many requests the player may have outstanding at once. 1 is the obvious
|
||||
-- loop and leaves the channel idle from every completion until the next poll;
|
||||
-- 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
|
||||
-- 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
|
||||
@@ -140,6 +186,16 @@ local BPS = (KBPS > 0) and (KBPS - AUDIO_KBPS) * 1024 or math.huge
|
||||
local code do local f=io.open("stream.bin","rb"); code=f:read("a"); f:close() end
|
||||
local cb do local f=io.open("stream_cb.bin","rb"); cb=f:read("a"); f:close() end
|
||||
local DISK = assert(io.open("stream_disk.bin","rb"))
|
||||
local idxblob do
|
||||
local f = io.open("stream_idx.bin","rb")
|
||||
if f then idxblob = f:read("a"); f:close() end
|
||||
end
|
||||
if RINGOWN and not idxblob then
|
||||
print("[STR] DLX_RINGOWN needs the DLX4 record index (stream_idx.bin). "
|
||||
.."Re-run tools/bench/prep_stream.py on a DLX4 container: the machine "
|
||||
.."cannot derive record lengths by walking a stream it has not fetched.")
|
||||
manager.machine:exit(); return
|
||||
end
|
||||
|
||||
local YOFF = (MODE.height - META.H) // 2
|
||||
local function T() local t=M.time; return t.seconds + t.attoseconds/1e18 end
|
||||
@@ -264,6 +320,202 @@ local function produce(now)
|
||||
end
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------- transport
|
||||
-- THE OTHER HALF OF ROADMAP P5. With DLX_RINGOWN the producer above is not
|
||||
-- used at all: src/player/ring.i decides which record to fetch, where to put it
|
||||
-- and when it is safe, and this becomes the part that is genuinely not the
|
||||
-- CPU's -- an SPC and one DMAC channel moving bytes at a rate.
|
||||
--
|
||||
-- IT IS BUILT OUT OF MEMORY TAPS, not out of the machine-frame notifier, and
|
||||
-- that is a measurement decision rather than a stylistic one. A notifier sees
|
||||
-- the machine once per raster frame, 17.64 ms (54.5), and the thing being
|
||||
-- measured here is the gap between a transfer COMPLETING and the CPU issuing
|
||||
-- the next one -- which is a fraction of a frame slot. Sampling it at 17.64 ms
|
||||
-- would have quantised the very quantity in question, and in the flattering
|
||||
-- direction if the ack were early or the pessimistic one if late.
|
||||
--
|
||||
-- write tap on XF_GO fires inside the 68000's write, so the issue time is
|
||||
-- exact. The bytes are copied in there and then: the
|
||||
-- CPU cannot observe them before its ack anyway, because
|
||||
-- it does not advertise the record until it retires.
|
||||
-- read tap on XF_ACK synthesises the completion word from emulated time --
|
||||
-- `done` iff now >= t_done -- so the CPU learns of the
|
||||
-- completion on the exact cycle it happens, the way it
|
||||
-- would from a status register.
|
||||
-- A 68000 bus access is 16 bits, so a longword is two tap calls: the write is
|
||||
-- acted on at the LOW half (written second, so the whole value is there) and
|
||||
-- the read latches its answer at the HIGH half so the two halves cannot
|
||||
-- straddle a completion.
|
||||
local xf = {go = 0, ack = 0, busy = 0.0, gap = 0.0, gapmax = 0.0, ngap = 0,
|
||||
latch = 0, tfree = 0, copylate = 0.0, ncopylate = 0}
|
||||
-- q[n] = the n-th request (n counts from 1): when the CPU issued it, when the
|
||||
-- transport will have finished it, and whether its bytes have been moved yet.
|
||||
local q = {}
|
||||
local npass = 1
|
||||
local function xf_deliver(off, dst, len, now)
|
||||
DISK:seek("set", off)
|
||||
push(dst, DISK:read(len), 1, len)
|
||||
end
|
||||
|
||||
-- The host as AUDITOR rather than as producer. Every placement the machine
|
||||
-- makes is checked against this script's own record index and its own list of
|
||||
-- records the decoder has not finished with -- the same overlap test the host
|
||||
-- producer used to make its decisions with, now used only to grade them. A
|
||||
-- wrong placement corrupts pixels rather than faulting (49.2), so it has to be
|
||||
-- caught where it is made.
|
||||
local function audit(idx, off, dst, len)
|
||||
local rec = META.index[idx + 1]
|
||||
if not rec then return string.format("record %d does not exist", idx) end
|
||||
if off ~= rec.off then
|
||||
return string.format("record %d: machine asked for disc offset %d, the "
|
||||
.."index says %d", idx, off, rec.off) end
|
||||
if len ~= rec.len then
|
||||
return string.format("record %d: machine asked for %d B, the index says %d",
|
||||
idx, len, rec.len) end
|
||||
local w = dst - RING
|
||||
if w < 0 or w + len > RINGSZ then
|
||||
return string.format("record %d: placed at ring offset %d..%d, outside a "
|
||||
.."%d B ring", idx, w, w + len, RINGSZ) end
|
||||
local tail = SP:read_u32(FR_TAIL)
|
||||
local i = 1
|
||||
while i <= #live do
|
||||
if live[i].idx < tail then table.remove(live, i) else i = i + 1 end
|
||||
end
|
||||
for _, r in ipairs(live) do
|
||||
if w < r.off + r.len and r.off < w + len then
|
||||
return string.format("record %d at %d..%d overlaps record %d at %d..%d, "
|
||||
.."which the decoder has not consumed",
|
||||
idx, w, w + len, r.idx, r.off, r.off + r.len) end
|
||||
end
|
||||
live[#live+1] = {idx = idx, off = w, len = len}
|
||||
return nil
|
||||
end
|
||||
|
||||
-- THE TAP OBJECTS ARE KEPT ALIVE HERE ON PURPOSE. install_*_tap returns a
|
||||
-- handle and the tap dies with it: dropping it on the floor leaves the taps
|
||||
-- working until Lua's collector next runs, and then the mailbox silently stops
|
||||
-- answering. That looks exactly like a wedged producer -- the machine polled
|
||||
-- 2.4 million times for an ack that had already been computed.
|
||||
-- THE TAP OBJECTS ARE KEPT ALIVE HERE ON PURPOSE. install_*_tap returns a
|
||||
-- handle and the tap dies with it: dropping it on the floor leaves the taps
|
||||
-- working until Lua's collector next runs, and then the mailbox silently stops
|
||||
-- answering. That looks exactly like a wedged producer -- the machine polled
|
||||
-- 2.4 million times for an ack that had already been computed.
|
||||
local TAPS = {}
|
||||
|
||||
local function install_transport()
|
||||
-- NOTHING IN A TAP CALLBACK TOUCHES THE MEMORY SPACE. MAME 0.277 segfaults
|
||||
-- if a tap reads or writes the space it was triggered from -- a nested access
|
||||
-- part way through the CPU's own -- and it does it intermittently, which is
|
||||
-- the worst way to find out. So the taps do pure Lua: the write tap stamps
|
||||
-- the EXACT emulated time the 68000 issued a request, the read tap answers
|
||||
-- the completion count from those stamps, and every actual memory access is
|
||||
-- done from the machine-frame notifier.
|
||||
--
|
||||
-- What that buys is the thing worth having: issue and completion times are
|
||||
-- both exact, so the gap between a transfer finishing and the next one
|
||||
-- starting -- the disc standing still because nobody has asked it for
|
||||
-- anything -- is measured at cycle resolution rather than at 17.64 ms.
|
||||
TAPS[#TAPS+1] = SP:install_write_tap(XF_GO, XF_GO + 3, "dlx_xf_go",
|
||||
function(offset, data, mask)
|
||||
if offset ~= XF_GO + 2 then return data end -- low half, written second
|
||||
-- ...and it must be the INCREMENT, not any other write to the word.
|
||||
-- ring_init clears the mailbox at boot, which lands on this address with
|
||||
-- the request fields still empty; without this test the transport answered
|
||||
-- that clear as if it were a request and audited a 0-byte record 0.
|
||||
if data ~= ((xf.go + 1) & 0xFFFF) then return data end
|
||||
xf.go = xf.go + 1
|
||||
q[xf.go] = {tissue = T(), served = false}
|
||||
return data
|
||||
end)
|
||||
TAPS[#TAPS+1] = SP:install_read_tap(XF_ACK, XF_ACK + 3, "dlx_xf_ack",
|
||||
function(offset, data, mask)
|
||||
if offset == XF_ACK then
|
||||
-- Latched on the high half so the two halves of one longword read cannot
|
||||
-- straddle a completion. Requests complete IN ORDER: one channel, one
|
||||
-- transfer at a time, however many are queued.
|
||||
local now = T()
|
||||
while true do
|
||||
local r = q[xf.ack + 1]
|
||||
if not (r and r.served and now >= r.tdone) then break end
|
||||
xf.ack = xf.ack + 1
|
||||
end
|
||||
xf.latch = xf.ack
|
||||
return (xf.latch >> 16) & 0xFFFF
|
||||
end
|
||||
return xf.latch & 0xFFFF
|
||||
end)
|
||||
end
|
||||
|
||||
-- Service every request the machine has issued but this script has not yet
|
||||
-- looked at: read what was asked for, grade the placement, move the bytes, and
|
||||
-- work out when the channel will have finished it. Called from the
|
||||
-- machine-frame notifier, which is where this script may touch memory.
|
||||
local function transport_service(t)
|
||||
local n = xf.ack + 1
|
||||
while q[n] do
|
||||
local r = q[n]
|
||||
if r.served then n = n + 1; goto continue end
|
||||
local slot = XF_SLOT + ((n - 1) % 2) * XF_SLSZ
|
||||
local off, dst = SP:read_u32(slot), SP:read_u32(slot + 4)
|
||||
local len, idx = SP:read_u32(slot + 8), SP:read_u32(slot + 12)
|
||||
if idx == 0 and n > 1 then
|
||||
-- A new pass: everything the auditor knows about the ring is about the
|
||||
-- scene we just left. The decoder has not consumed the new records and
|
||||
-- the old ones are no longer anybody's.
|
||||
live, nsent, arrival = {}, 0, {}
|
||||
npass = npass + 1
|
||||
P(string.format("SEEK PASS %d: the machine seeked back to record 0 at "
|
||||
.."%.3f s and is refilling from empty", npass, r.tissue))
|
||||
end
|
||||
local err = audit(idx, off, dst, len)
|
||||
if err then
|
||||
P("MISPLACED: "..err)
|
||||
P(" The machine owns the ring in this run, so this is a policy bug in "
|
||||
.."src/player/ring.i, not a rig one -- and it would have shown up as "
|
||||
.."wrong pixels, because the block loop reads without a bounds check.")
|
||||
M:exit(); return
|
||||
end
|
||||
-- ONE CHANNEL: a transfer starts when the channel is free AND the request
|
||||
-- exists, so a queued request starts the instant its predecessor lands and
|
||||
-- a late one starts when it is issued. The difference is the gap, and the
|
||||
-- gap is the player's, not the medium's -- no host-filled run could see it,
|
||||
-- because the host producer placed records whenever it liked.
|
||||
local startt = math.max(r.tissue, xf.tfree)
|
||||
if xf.tfree > 0 then
|
||||
local g = startt - xf.tfree
|
||||
if g > 0 and nsent < META.nframes then
|
||||
xf.gap, xf.ngap = xf.gap + g, xf.ngap + 1
|
||||
if g > xf.gapmax then xf.gapmax = g end
|
||||
end
|
||||
end
|
||||
local dur = (BPS == math.huge) and 0 or (len / BPS)
|
||||
local done = startt + dur
|
||||
-- A cut is a seek: the transfer freezes for its duration rather than
|
||||
-- carrying on invisibly. Freezing is the conservative reading and the one
|
||||
-- 51.6 settled on for the host producer -- a drive that is repositioning is
|
||||
-- not banking bytes it will burst on arrival.
|
||||
if cut_t0 and done > cut_t0 and startt < cut_t1 then
|
||||
done = done + (cut_t1 - math.max(startt, cut_t0))
|
||||
end
|
||||
xf_deliver(off, dst, len, t)
|
||||
r.tdone, r.served = done, true
|
||||
xf.tfree = done
|
||||
xf.busy = xf.busy + dur
|
||||
nsent = math.max(nsent, idx + 1)
|
||||
arrival[idx + 1] = done -- resident when the last byte lands
|
||||
delivered = delivered + len
|
||||
-- The one place this rig is coarser than the machine: a record whose
|
||||
-- modelled transfer was shorter than the wait for this notifier was already
|
||||
-- "done" by the time the bytes could be moved. Counted, not absorbed.
|
||||
if t > done then
|
||||
xf.copylate, xf.ncopylate = xf.copylate + (t - done), xf.ncopylate + 1
|
||||
end
|
||||
n = n + 1
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------ setup
|
||||
local function setup()
|
||||
MODE.apply(SP)
|
||||
@@ -280,10 +532,24 @@ local function setup()
|
||||
end
|
||||
for i = 1, #code do SP:write_u8(0x10000+i-1, string.byte(code,i)) end
|
||||
SP:write_u32(FLAG, 0); SP:write_u32(FR_HEAD, 0); SP:write_u32(FR_TAIL, 0)
|
||||
SP:write_u32(ITER, 1); SP:write_u32(NFR, META.nframes)
|
||||
SP:write_u32(ITER, ITERS); SP:write_u32(NFR, META.nframes)
|
||||
SP:write_u32(PACE, 0); SP:write_u32(PACEON, PACED and 1 or 0)
|
||||
SP:write_u32(CLKON, SELFCLK and 1 or 0)
|
||||
SP:write_u32(CLK_FPS, META.fps)
|
||||
SP:write_u32(A_RINGOWN, RINGOWN and 1 or 0)
|
||||
if RINGOWN then
|
||||
-- The scene header the machine reads: the DLX4 record index, exactly the
|
||||
-- bytes the container carries, pushed into RAM the way a loaded scene
|
||||
-- header would be. ring_init turns it into the disc-offset table a seek
|
||||
-- needs; nothing here derives a record boundary for the machine.
|
||||
push(IDXRAM, idxblob, 1, #idxblob)
|
||||
SP:write_u32(A_IDX_B, IDXRAM)
|
||||
SP:write_u32(A_RNG_B, RING)
|
||||
SP:write_u32(A_RNG_SZ, RINGSZ)
|
||||
SP:write_u32(A_PFREC, PREFILL_FR)
|
||||
SP:write_u32(XF_QD, QDEPTH)
|
||||
install_transport()
|
||||
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))
|
||||
P(string.format("decoder %s%s", SELFCLK
|
||||
@@ -292,9 +558,15 @@ local function setup()
|
||||
or "FREE-RUNNING (tests wrap, not buffering -- 49.7.2)",
|
||||
CUT_AT and string.format(", pipe cut at tick %d for %.2f fr",
|
||||
CUT_AT, CUT_FR) or ""))
|
||||
P(string.format("ring %d KB at %06X, pipe %s, prefill %d KB, maxrec %d B",
|
||||
P(string.format("ring %d KB at %06X, pipe %s, prefill %s, maxrec %d B",
|
||||
RING_KB, RING, (KBPS > 0) and (KBPS.." KB/s") or "unlimited",
|
||||
PREFILL // 1024, META.maxrec))
|
||||
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))
|
||||
end
|
||||
if META.maxrec > RINGSZ then
|
||||
P("RING TOO SMALL: one record does not fit. stream.s needs a whole record "
|
||||
.."contiguous."); M:exit()
|
||||
@@ -322,13 +594,18 @@ 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 st == "boot" then
|
||||
if t < 3.0 then return end
|
||||
setup(); last_t, pf_t0 = t, t; st = "prefill"; return
|
||||
end
|
||||
if st == "prefill" then
|
||||
produce(t)
|
||||
if delivered >= PREFILL then
|
||||
if not RINGOWN then produce(t) end
|
||||
-- RINGOWN: there is nothing for the host to prefill. The machine issues
|
||||
-- its own requests, so it has to be running first; ring_prefill then
|
||||
-- holds the decoder until PF_REC records are resident, which is the same
|
||||
-- policy in the place a player would keep it.
|
||||
if RINGOWN or delivered >= PREFILL then
|
||||
P(string.format("prefill done: %.1f KB in %.3f s, releasing the CPU",
|
||||
delivered/1024, t - pf_t0))
|
||||
launch(); t_rel = t; st = "running"
|
||||
@@ -344,6 +621,14 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
-- which is the point: the same bytes are gated either way.
|
||||
local tick = SELFCLK and SP:read_u32(PACE)
|
||||
or math.floor((t - t_rel) * META.fps)
|
||||
if tick < pace then
|
||||
-- src/player/stream.s rebased the clock for a new pass: tick 0 is the
|
||||
-- instant the decoder was released after the seek's prefill, not the
|
||||
-- start of the run. Everything sampled per tick starts again with it.
|
||||
pace, tick_t = -1, {}
|
||||
min_ahead, min_at = math.huge, -1
|
||||
sum_ahead, n_ahead, slack_series = 0, 0, {}
|
||||
end
|
||||
if tick > pace then
|
||||
pace = tick
|
||||
if not SELFCLK then SP:write_u32(PACE, pace) end
|
||||
@@ -372,7 +657,12 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
if nsent < META.nframes then
|
||||
if ahead < min_ahead then min_ahead, min_at = ahead, tick end
|
||||
sum_ahead, n_ahead = sum_ahead + ahead, n_ahead + 1
|
||||
slack_series[#slack_series+1] = {tick, ahead, n_ring, n_rate}
|
||||
-- Column 3 is "has the producer been refused for space yet",
|
||||
-- which is what makes a later minimum meaningful: before the first
|
||||
-- refusal the ring is still filling. With the ring owned by the
|
||||
-- machine that counter is ring.i's, not this script's.
|
||||
slack_series[#slack_series+1] =
|
||||
{tick, ahead, RINGOWN and SP:read_u32(A_NFULL) or n_ring, n_rate}
|
||||
end
|
||||
if CUT_AT and tick >= CUT_AT and not cut_t0 then
|
||||
cut_t0, cut_t1 = t, t + CUT_FR / META.fps
|
||||
@@ -382,12 +672,38 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
end
|
||||
end
|
||||
end
|
||||
produce(t)
|
||||
if not RINGOWN then produce(t) end
|
||||
if RINGOWN and os.getenv("DLX_RINGDBG") == "1" then
|
||||
dbg_n = (dbg_n or 0) + 1
|
||||
if dbg_n % 30 == 0 then
|
||||
P(string.format("DBG t=%.2f flag=%X go=%d ack=%d tfree=%.3f queued=%s "
|
||||
.."head=%d tail=%d rq=%d poll=%d full=%d wcur=%d rcur=%d",
|
||||
t, SP:read_u32(FLAG), xf.go, xf.ack, xf.tfree,
|
||||
tostring(q[xf.ack+1] ~= nil), SP:read_u32(FR_HEAD),
|
||||
SP:read_u32(FR_TAIL), SP:read_u32(A_RQ_NEXT),
|
||||
SP:read_u32(A_NPOLL), SP:read_u32(A_NFULL),
|
||||
SP:read_u32(A_WCUR), SP:read_u32(A_RCUR)))
|
||||
end
|
||||
end
|
||||
if cut_t0 and not cut_done and t >= cut_t1 then cut_done = true end
|
||||
local fl = SP:read_u32(FLAG)
|
||||
if fl == 1 and not t0 then t0 = t; return end
|
||||
if fl == 0xEE then
|
||||
P("BITSTREAM DESYNC -- the decoder consumed the wrong number of bytes.")
|
||||
if RINGOWN then
|
||||
local tail, head = SP:read_u32(FR_TAIL), SP:read_u32(FR_HEAD)
|
||||
P(string.format(" tail=%d head=%d rq=%d wcur=%d rcur=%d go=%d ack=%d",
|
||||
tail, head, SP:read_u32(A_RQ_NEXT), SP:read_u32(A_WCUR),
|
||||
SP:read_u32(A_RCUR), xf.go, xf.ack))
|
||||
P(string.format(" DESC[%d]=%08X, the index says record %d is at ring "
|
||||
.."offset ? len %d", tail,
|
||||
SP:read_u32(DESC + (tail % 64)*4), tail,
|
||||
META.index[tail+1] and META.index[tail+1].len or -1))
|
||||
local ls = {}
|
||||
for _, r in ipairs(live) do
|
||||
ls[#ls+1] = string.format("%d@%d+%d", r.idx, r.off, r.len) end
|
||||
P(" host thinks live: "..table.concat(ls, " "))
|
||||
end
|
||||
P(" Under a ring that is the whole point: it means a record was placed "
|
||||
.."or described wrongly, not that the codec changed.")
|
||||
M:exit(); return
|
||||
@@ -400,6 +716,11 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
" (fps*VTOTAL does not fit the 16-bit accumulator)" or ""))
|
||||
M:exit(); return
|
||||
end
|
||||
if fl == 0xE3 then
|
||||
P("RING INIT REFUSED: the record index has more entries than "
|
||||
.."src/player/ring.i's disc-offset table (ROFFMAX) can hold.")
|
||||
M:exit(); return
|
||||
end
|
||||
if fl == 0xE1 then
|
||||
P("PRODUCER STALLED OUT -- stream.s spun SPINMAX times with no new "
|
||||
.."record. Delivered "..nsent.."/"..META.nframes..".")
|
||||
@@ -443,10 +764,62 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
vd, pace + 1, vd/(pace + 1), META.fps, vt,
|
||||
31500/(META.fps*vt), 181.35*vd/(pace+1)))
|
||||
end
|
||||
local rholes, rhole_b = holes, hole_bytes
|
||||
if RINGOWN then
|
||||
rholes, rhole_b = SP:read_u32(A_NHOLE), SP:read_u32(A_NHOLEB)
|
||||
end
|
||||
P(string.format("ring: %d wraps, %d B of hole (mean %.1f KB, %.1f%% of "
|
||||
.."the ring)", holes, hole_bytes,
|
||||
holes > 0 and hole_bytes/holes/1024 or 0,
|
||||
100*(holes > 0 and hole_bytes/holes or 0)/RINGSZ))
|
||||
.."the ring)", rholes, rhole_b,
|
||||
rholes > 0 and rhole_b/rholes/1024 or 0,
|
||||
100*(rholes > 0 and rhole_b/rholes or 0)/RINGSZ))
|
||||
if RINGOWN then
|
||||
-- The producer's own account of itself. These are read out of the
|
||||
-- machine's RAM, not kept here: the point of the run is that this
|
||||
-- script did not make any of these decisions.
|
||||
local pf = SP:read_u32(A_PFDONE)
|
||||
local npoll, nissue = SP:read_u32(A_NPOLL), SP:read_u32(A_NISSUE)
|
||||
local nfull = SP:read_u32(A_NFULL)
|
||||
local slk, slkat = SP:read_u32(A_SLKMIN), SP:read_u32(A_SLKAT)
|
||||
local nseek, skw = SP:read_u32(A_NSEEK), SP:read_u32(A_SKWAIT)
|
||||
P(string.format("MACHINE-OWNED RING: %d records placed by the 68000, "
|
||||
.."%d polls, %d refused for space (ring-bound), "
|
||||
.."%d seeks", nissue, npoll, nfull, nseek))
|
||||
P(string.format("PREFILL: released the decoder at %d records "
|
||||
.."(policy: %d); LEAST SLACK %d records at frame %d",
|
||||
pf, PREFILL_FR, slk, slkat))
|
||||
-- THE NUMBER THIS RIG EXISTS TO PRODUCE. The channel only moves
|
||||
-- bytes while a request is outstanding and only the CPU can issue the
|
||||
-- next one, so the disc stands still between the completion of one
|
||||
-- record and the issue of the next. That gap is a property of the
|
||||
-- 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)
|
||||
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",
|
||||
xf.gap*1000, xf.ngap, xf.gapmax*1000,
|
||||
span > 0 and 100*xf.gap/span or 0, span,
|
||||
xf.busy*1000))
|
||||
-- THE RIG'S OWN RESOLUTION, PRINTED RATHER THAN LEFT IMPLICIT.
|
||||
-- The transport's timing is exact (memory taps), but the BYTES are
|
||||
-- moved from the machine-frame notifier, so a record whose modelled
|
||||
-- transfer is shorter than the wait for that notifier is acked later
|
||||
-- than the model says it landed. Every millisecond here is a
|
||||
-- millisecond the decoder may have waited that the medium would not
|
||||
-- have made it wait.
|
||||
if xf.ncopylate > 0 then
|
||||
P(string.format("RIG RESOLUTION: %d of %d transfers were acked late "
|
||||
.."because the bytes are moved on the notifier, by "
|
||||
.."%.1f ms in total (mean %.1f ms) -- an artefact of "
|
||||
.."this rig, not of the player",
|
||||
xf.ncopylate, nissue, xf.copylate*1000,
|
||||
xf.copylate*1000/xf.ncopylate))
|
||||
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
|
||||
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
|
||||
-- on `arrival` above.
|
||||
@@ -490,15 +863,22 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
for _,e in ipairs(slack_series) do
|
||||
if e[3] > 0 and e[2] < ss_min then ss_min, ss_at = e[2], e[1] end
|
||||
end
|
||||
if ss_min == math.huge then ss_min, ss_at = -1, -1 end
|
||||
P(string.format("SEEK SLACK: ceiling %d frames (%.0f ms), first "
|
||||
.."reached at tick %d; mean %.1f over the window",
|
||||
ceiling, 1000*ceiling/META.fps, ceil_at,
|
||||
sum_ahead/math.max(1,n_ahead)))
|
||||
if n_ring > 0 then
|
||||
-- WHOSE REFUSAL COUNT. With the ring owned by the machine the host
|
||||
-- producer's counters are not merely stale, they are zero -- it never
|
||||
-- placed anything -- so the classification has to come from
|
||||
-- src/player/ring.i's own N_FULL. Reading n_ring here in a RINGOWN
|
||||
-- run would have reported RATE-BOUND on every run by construction.
|
||||
local ring_ref = RINGOWN and SP:read_u32(A_NFULL) or n_ring
|
||||
if ring_ref > 0 then
|
||||
P(string.format(" RING-BOUND: the ring filled (%d refusals). Once "
|
||||
.."full it survives delivery stopped dead for %d "
|
||||
.."frames = %.0f ms; min after first fill %d "
|
||||
.."(tick %d)", n_ring, ceiling,
|
||||
.."(tick %d)", ring_ref, ceiling,
|
||||
1000*ceiling/META.fps, ss_min, ss_at))
|
||||
else
|
||||
P(string.format(" RATE-BOUND: the ring NEVER filled in %d frames. "
|
||||
@@ -530,6 +910,16 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
for i = 0, META.nframes-1 do
|
||||
local a = arrival[i+1]
|
||||
if a then
|
||||
-- INSTRUMENT NOTE, and it only bites under the self-clock.
|
||||
-- tick_t[] is when this script OBSERVED the machine's tick, which
|
||||
-- is the next machine-frame boundary after it -- up to 17.64 ms
|
||||
-- late (54.5). So these deadlines are generous by that much and
|
||||
-- this count UNDERSTATES lateness. The sharp instrument for the
|
||||
-- same question is the decoder's own stall counter, which is exact
|
||||
-- because the machine takes it: at 488 KB/s with a one-deep request
|
||||
-- queue this reported 6 records late where the 68000 counted 59
|
||||
-- frames that had to wait. Host-paced (DLX_PACE=1) the deadline is
|
||||
-- a host model and is exact, so FINDINGS 49.6's table is unaffected.
|
||||
local due = SELFCLK and tick_t[i+1] or (t_rel + i / META.fps)
|
||||
if not due then due = t_rel + i / META.fps end
|
||||
local late = a - due
|
||||
@@ -550,14 +940,28 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
.."at %s", prefill_s*1000, prefill_s*META.fps,
|
||||
(KBPS > 0) and (prefill_s * BPS / 1024) or 0.0,
|
||||
(KBPS > 0) and (KBPS.." KB/s") or "an unlimited pipe"))
|
||||
M.video:snapshot()
|
||||
P("snapshot taken after the sequential pass -- last frame, decoded "
|
||||
.."entirely out of a "..RING_KB.." KB ring")
|
||||
st = "snapped"; return
|
||||
-- ONE FRAME OF SETTLE BEFORE THE CAPTURE, and it is not cosmetic.
|
||||
-- MAME renders a screen scanline by scanline and the machine-frame
|
||||
-- notifier fires at the END of that frame, so a bitmap for a frame in
|
||||
-- which GVRAM changed holds some lines drawn before the change and some
|
||||
-- after. Snapshotting it captures a TEAR -- and the symptom is a
|
||||
-- pixel-exactness failure in the bottom blocks plus a broken double-scan
|
||||
-- pairing, which reads exactly like a decoder bug and is not one. It
|
||||
-- only bites when the decoder finishes its last frame LATE in a screen
|
||||
-- frame, which is why it never showed up until a run with underruns in
|
||||
-- it. Waiting one whole frame renders the finished picture with nothing
|
||||
-- writing to GVRAM.
|
||||
st = "settle"; return
|
||||
end
|
||||
if t > 900 then P("TIMEOUT flag="..string.format("%08X",fl)); M:exit() end
|
||||
return
|
||||
end
|
||||
if st == "settle" then
|
||||
M.video:snapshot()
|
||||
P("snapshot taken after the sequential pass -- last frame, decoded "
|
||||
.."entirely out of a "..RING_KB.." KB ring")
|
||||
st = "snapped"; return
|
||||
end
|
||||
if st == "snapped" then M:exit(); return end
|
||||
end)
|
||||
if not ok then print("[STR] LUA ERROR: "..tostring(err)); M:exit() end
|
||||
|
||||
Reference in New Issue
Block a user