Files
Dragon-s-Lair-X68k/tools/bench/check.sh
T
prosolis e565dfbbab Take the disc mount out of the source, so someone else's copy of the media works
Three files hardcoded /media/reala-misaki/BDROM -- extract.py, 07_motion_survey.py
and check.sh -- which made the pipeline unrunnable for anyone whose disc mounts
anywhere else. DLX_BDROM now overrides it everywhere, defaulting to
/media/$USER/BDROM, so this box is unchanged and another one works. Verified by
running extract.py against a symlinked mount at a different path.

check.sh's failure message names the path it looked at and the variable to set,
instead of assuming udisks put it where this machine puts it.

README gains a "Reproducing this" section: no media ships here and none of it is
redistributable, so it says what you have to bring (the disc) and what is
already packaged (vasm is vendored as a binary with its source tarball; the
k-means is hand-rolled, so numpy and Pillow are the whole Python dependency).
It also names the two gates that SKIP rather than fail -- the px68k second-core
pass and the IPL ROM DMAC gate -- because both live outside this repo and a
silent skip is worth reading as a skip.

One trap called out rather than left to bite: scene selection is a hard-coded
stream number, not a search. A different pressing that numbers its .m2ts files
differently will extract the wrong footage and the green light will PASS on it.

check.sh ALL GREEN.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-24 18:19:54 -07:00

274 lines
16 KiB
Bash
Executable File

#!/bin/bash
# Green-light check: re-runs both display regression tests AND the rate-control
# drift test, from the Blu-ray. ~2 min. Run from the repo root. Any non-zero
# exit means something drifted.
set -e
cd "$(dirname "$0")/../.."
# No media ships with this repo. Bring your own disc; DLX_BDROM overrides the
# mount point, and every tool that reads the disc honours the same variable.
DLX_BDROM=${DLX_BDROM:-/media/${USER:-$(id -un)}/BDROM}
export DLX_BDROM
[ -d "$DLX_BDROM" ] || {
echo "Blu-ray not mounted at $DLX_BDROM."
echo " udisksctl loop-setup -r -f DRAGONS_LAIR.iso"
echo " or set DLX_BDROM to where yours is mounted."; exit 2; }
python3 tools/encoder/extract.py 00020 tmp/fr_00020 12 crop
mkdir -p tmp/snap_verify tmp/snap256
# ---------------------------------------------------------------------------
# THE ONE PLACE THE RETIRED PIPE FIGURE STILL LIVES. Session 18 removed it as
# a default from every analysis tool and from tools/bench/stream.lua, because it
# was never a bus measurement -- a user-supplied "4 Mbps" with no provenance,
# 10% of SCSI-1's asynchronous rating (FINDINGS 42.1) -- and a default let table
# after table be scored against it without anyone restating what it was.
#
# It survives HERE and only here because the gate container was ENCODED with it,
# and every per-block and span constant in FINDINGS 41/43/45/49 is fitted to that
# container. Changing this number is not an edit, it is a re-encode plus a
# re-measurement of all of them.
#
# It is a CONTAINER RECIPE, not a claim about any medium. Do not read a delivery
# rate out of it, do not copy it into a tool, and do not add a default anywhere
# that would resurrect it. When the pipe is finally measured, this becomes an
# ordinary encoder setting and the comment goes.
GATE_SPAN_KBPS=488
# ---------------------------------------------------------------------------
run() { # run <script> <snapdir>
rm -f "tmp/$2/x68000"/*.png
( cd tmp && SDL_VIDEODRIVER=dummy timeout -k 5 120 mame x68000 -bios ipl10 \
-video soft -window -sound none -nothrottle -plugins \
-autoboot_script "../tools/bench/$1" \
-snapshot_directory "./$2" -snapview native -seconds_to_run 6 >"$2.log" 2>&1 )
}
echo "--- session 3: 768-wide IPL timing (FINDINGS 22) ---"
python3 tools/bench/prep_frame.py tmp/fr_00020 tmp/frame.bin 0
run show_frame.lua snap_verify
python3 tools/bench/verify_frame.py
echo "--- session 4: real 256x256 mode (FINDINGS 23) ---"
python3 tools/bench/prep_frame.py tmp/fr_00020 tmp/frame256.bin 0 --reserve-black
run show_frame256.lua snap256
python3 tools/bench/verify_frame256.py
echo "--- session 6: rate-control drift (FINDINGS 26/27) ---"
# The codec is temporally recursive, so a rate controller can report quality for
# a reconstruction no decoder will ever produce -- silently. This asserts that a
# decoder replaying the emitted stream rebuilds exactly what the encoder
# recorded. ~55 s, nearly all of it k-means in H.build.
[ -d tmp/fr_singe ] || python3 tools/encoder/extract.py 00223 tmp/fr_singe 12 crop 539.4 10.0
# NOT piped into tail: a pipeline's exit status is the last command's, which
# would swallow the failure this whole script exists to catch.
python3 tools/analysis/09_ratectl_drift.py > tmp/drift_check.log 2>&1 \
|| { cat tmp/drift_check.log; exit 1; }
tail -9 tmp/drift_check.log
echo "--- session 12: the DLX3 span container round-trips (FINDINGS 41) ---"
# 09 above replays SKIP semantics in Python and never reads a container. A v7
# span breaks exactly that shortcut -- a spanned block reads SKIP in the mode
# header and is painted by the span section instead -- so this encodes, WRITES
# the container, reads it back with the reference decoder and compares. It also
# asserts that it emitted enough spans to have tested anything.
# --kbps is required now (session 18): the tool has no default rate, so the gate
# has to say which one it is testing at. Same recipe constant as the container.
python3 tools/analysis/16_span_roundtrip.py --kbps $GATE_SPAN_KBPS \
> tmp/span_roundtrip.log 2>&1 \
|| { cat tmp/span_roundtrip.log; exit 1; }
tail -4 tmp/span_roundtrip.log
echo "--- session 7: display-path coherency (FINDINGS 28.1) ---"
# 10_pathmix_drift.py is a COUNTEREXAMPLE, kept runnable: the dual-path plan of
# FINDINGS 24.5/25.6 must still be shown to corrupt frames, and the strategy the
# player actually uses must still be clean. A green light here means the reason
# decode.s has one display path is still demonstrable, not just asserted.
python3 tools/analysis/10_pathmix_drift.py > tmp/pathmix.log 2>&1 \
&& { echo "FAIL: the dual-path plan no longer reproduces its own defect"; \
cat tmp/pathmix.log; exit 1; }
grep -a "frames displaying pixels" tmp/pathmix.log
python3 tools/analysis/10_pathmix_drift.py --fix direct > tmp/pathmix_direct.log 2>&1 \
|| { echo "FAIL: direct-to-GVRAM is no longer coherent"; cat tmp/pathmix_direct.log; exit 1; }
echo "--- session 7: 68000 decoder is pixel-exact (FINDINGS 28) ---"
# The strongest display test in the tree: 120 frames decoded in sequence by
# 68000 code, every block mode, full temporal recursion. A SKIP block is a claim
# about the previous frame still being on screen, so the last frame is only
# right if all 120 were.
# The gate container is the HEAVIEST stream the encoder emits: the scsi mode
# decision (the only profile left after session 9 dropped sasi on capacity,
# FINDINGS 32) with the span pass drawing on a byte ceiling wide enough that
# every frame carries a span table and all four block modes are still exercised.
# That ceiling is GATE_SPAN_KBPS above -- a recipe, not a delivery rate.
# Spans are the newest and least-proven path in decode.s; gating on a container
# where they are rare would be gating on the old decoder. FINDINGS 41.
DLX=tmp/rc_fr_singe_scsi_span.dlx
[ -f "$DLX" ] || python3 tools/encoder/encode.py tmp/fr_singe "$DLX" --profile scsi \
--kbps 280 --span-kbps $GATE_SPAN_KBPS --spans all
# RIG_RAM is the EMULATED MACHINE's memory, and it is not a claim about the
# target. The rig preloads the whole container into RAM at 0x30000; the shipping
# player streams from disk into a ring buffer and never holds a window at once,
# so preloading is unlike the player at ANY size. At the 2 MB of a stock machine
# this gate covered 37 of 120 frames (FINDINGS 44.6.4) -- the span-heavy
# container is 5,261,814 B of stream, ending at 0x534BF6. 6 MB covers all 120.
#
# Raising it is licensed by measurement, not by convenience: at 2M and 6M the
# five synthetic anchors come out BIT-IDENTICAL (40,729 / 921,187 / 1,376,881 /
# 1,229,883 / 506,533 cycles) despite sitting at different addresses in the two
# layouts, so MAME's cycle model does not depend on ramsize over this range.
# FINDINGS 45. What is still NOT tested, at either size, is the streaming path.
RIG_RAM=${RIG_RAM:-6}
python3 tools/bench/prep_dlx.py "$DLX" --ram $((RIG_RAM * 0x100000)) > tmp/prep_dlx.log
# Verify against exactly the frame list prep_dlx emitted. It no longer truncates
# at the default RIG_RAM, but the guard stays: lower RIG_RAM, or a heavier
# container, brings truncation straight back and it must stay announced.
NF=$(sed -n 's/.*nframes=\([0-9]*\),.*/\1/p' tmp/decode_meta.lua)
grep -a "TRUNCATED" tmp/prep_dlx.log || true
tools/vasm/vasmm68k_mot -Fbin -o tmp/decode.bin src/player/decode.s > /dev/null
mkdir -p tmp/snap_decode
rm -f tmp/snap_decode/x68000/*.png
# stdbuf -oL: a FILE is block-buffered too, so without it a long MAME run is
# unobservable until it exits and a run that is merely finishing looks exactly
# like one that is wedged (FINDINGS 34.1).
# -seconds_to_run must cover the WHOLE sequential pass. The scsi container is
# 2.7x the payload of the session-7 one this gate used to run on, and at 20 s
# the pass was truncated -- MAME exited mid-decode and verify_decode.py then
# compared a partially drawn screen and reported 49,005 differing pixels, which
# reads as a decoder bug and is not one.
( cd tmp && DLX_VERIFY_ONLY=1 SDL_VIDEODRIVER=dummy stdbuf -oL timeout -k 5 300 mame x68000 \
-bios ipl10 -ramsize ${RIG_RAM}M -video soft -window -sound none -nothrottle -plugins \
-autoboot_script ../tools/bench/decode.lua \
-snapshot_directory ./snap_decode -snapview native -seconds_to_run 60 \
> decode_check.log 2>&1 )
# A truncated run must fail as a truncated run. Without this the only symptom is
# a pixel diff against a half-drawn frame.
grep -q "snapshot taken" tmp/decode_check.log || {
echo "FAIL: the 68000 sequential pass did not complete -- no snapshot marker."
echo " Raise -seconds_to_run; the pass needs the whole container decoded."
tail -5 tmp/decode_check.log; exit 1; }
python3 tools/bench/verify_decode.py "$DLX" --nframes "$NF"
echo "--- session 10: the same decode on a second CPU core (FINDINGS 37) ---"
# A SECOND emulator, and the cheapest strong test in the tree: seconds, no MAME,
# no ROMs. px68k's C68K core has its own cycle table and its own memory model,
# so a pass here says decode.s is pixel-exact under two independent cores and
# that the harness's byte-swapped RAM / high-byte-discarding GVRAM is right --
# which is what licenses its cycle and bus numbers.
# Skipped rather than failed when px68k is not checked out: it is an external
# tree, not part of this repo.
PX68K=${PX68K:-$HOME/src/px68k}
if [ -f "$PX68K/m68000/c68k.c" ]; then
make -s -C tools/bench/c68k PX68K="$PX68K"
bash tools/bench/c68k/run.sh tmp/c68k_frames.csv 2>tmp/c68k.log
grep -a "sequential pass" tmp/c68k.log
python3 tools/bench/c68k/verify_c68k.py "$DLX" --nframes "$NF"
echo "--- session 10: the bus model still matches the machine (FINDINGS 38) ---"
# 15_bus_occupancy.py derives instruction prefetch, which no emulator here can
# report, and validates itself against the DATA accesses the harness counts.
# If that check ever stops holding, every bus figure in FINDINGS 38/39 is
# unfounded -- so it is a gate, not a report.
python3 tools/analysis/15_bus_occupancy.py "$DLX" | sed -n '3,7p'
else
echo " SKIPPED: no px68k at $PX68K (set PX68K= to point at a checkout)"
fi
echo "--- session 20: the DMAC config, read out of the IPL ROM (FINDINGS 52) ---"
# The audio and disk per-byte debits are no longer a recollection about the
# HD63450: they are bytes at named addresses in the ROM MAME boots this rig
# with. This gate re-reads them. It is cheap, it needs no emulator, and if a
# different ROM revision is ever pointed at it, it says so rather than decoding
# some other code and reporting a number.
# Skipped rather than failed when the ROM is not where MAME keeps it: that is a
# path outside this repo.
IPLROM=${IPLROM:-$HOME/mame/roms/iplrom.dat}
if [ -f "$IPLROM" ]; then
python3 tools/analysis/21_iplrom_dmac.py "$IPLROM" > tmp/iplrom_dmac.log 2>&1 \
|| { cat tmp/iplrom_dmac.log; exit 1; }
grep -ac "^ OK " tmp/iplrom_dmac.log | xargs printf " %s evidence sites hold; "
sed -n 's/^ = \(.*clocks per audio byte\)/audio is \1/p' tmp/iplrom_dmac.log
else
echo " SKIPPED: no IPL ROM at $IPLROM (set IPLROM= to point at it)"
fi
echo "--- session 18: the shared-body split is a no-op (FINDINGS 49.7.5) ---"
# src/player/decode.s and src/player/stream.s assemble from ONE copy of the block
# loop and the span chain (src/player/frame.i) so that the two front-ends cannot
# drift apart. The drift would be silent -- both would still decode correctly,
# and only the cost model would be wrong, because the 66.0 clocks/span, 9.143
# clocks/coarse pixel and every per-block constant in FINDINGS 24/30/40/41 are
# fitted to those exact bytes. So the split is asserted to be a no-op rather than
# assumed to be one.
DECODE_MD5=7a7a06f8c6d097ee0041bca4aefa3eb2 # decode.bin before the split, 1296 B
GOT=$(md5sum tmp/decode.bin | cut -d" " -f1)
[ "$GOT" = "$DECODE_MD5" ] || {
echo "FAIL: decode.bin is $GOT, expected $DECODE_MD5 ($(stat -c%s tmp/decode.bin) B)."
echo " The block loop or the span chain changed. That is allowed -- but"
echo " every cycle constant in FINDINGS 24/30/40/41 is fitted to the old"
echo " bytes, so re-measure them and move this hash, do not just move it."
exit 1; }
echo " decode.bin unchanged at $(stat -c%s tmp/decode.bin) B ($DECODE_MD5)"
# Same argument for the loader maths, which prep_dlx.py and prep_stream.py now
# share via tools/bench/dlxload.py: a second copy of the palette packing would
# drift and the symptom would be wrong colours in one rig only.
python3 tools/bench/prep_dlx.py "$DLX" --ram $((RIG_RAM * 0x100000)) --out tmp/_pdchk > /dev/null
cmp -s tmp/_pdchk_data.bin tmp/decode_data.bin || {
echo "FAIL: prep_dlx.py is not reproducible"; exit 1; }
echo " prep_dlx.py blob reproducible ($(stat -c%s tmp/decode_data.bin) B)"
rm -f tmp/_pdchk_data.bin tmp/_pdchk_meta.lua
echo "--- session 18: 120 frames through a bounded RING (FINDINGS 49) ---"
# The gate above preloads the whole container into RAM and proves the DECODER.
# This proves the DELIVERY path: the same 120 frames decoded out of a 256 KB
# ring on a STOCK 2 MB machine, with the container in a host file. The block
# loop reads with a monotonically increasing a0 and no bounds check, so a record
# placed wrongly by the wrap policy corrupts pixels rather than faulting -- which
# is why this is gated on the same pixel-exact comparison and not on a checksum.
tools/vasm/vasmm68k_mot -Fbin -o tmp/stream.bin src/player/stream.s > /dev/null
python3 tools/bench/prep_stream.py "$DLX" > tmp/prep_stream.log
mkdir -p tmp/snap_stream
rm -f tmp/snap_stream/x68000/*.png
( cd tmp && DLX_STREAM_KBPS=0 SDL_VIDEODRIVER=dummy stdbuf -oL timeout -k 5 600 \
mame x68000 -bios ipl10 -ramsize 2M -video soft -window -sound none \
-nothrottle -plugins -autoboot_script ../tools/bench/stream.lua \
-snapshot_directory ./snap_stream -snapview native -seconds_to_run 90 \
> stream_check.log 2>&1 )
# Same truncation trap as the decode stage: without this, a run that exited
# mid-decode is compared against a half-drawn screen and reads as a wrap bug.
grep -q "snapshot taken" tmp/stream_check.log || {
echo "FAIL: the ring-buffer pass did not complete -- no snapshot marker."
tail -5 tmp/stream_check.log; exit 1; }
grep -a "ring: \|DEADLINE" tmp/stream_check.log | sed "s/\[STR\] / /"
python3 tools/bench/verify_decode.py "$DLX" --snap tmp/snap_stream
echo "--- session 19: the PACED ring, and what a branch point costs (FINDINGS 51) ---"
# The stage above runs the ring FREE-RUNNING, which is right for what it gates:
# an unlimited pipe removes delivery as a variable and leaves the wrap policy
# alone under test. It cannot see buffering, because a decoder that never waits
# never lets the ring back up -- 49.7.2, and it is why 48 KB passed while
# holding one record. This runs the same 120 frames with the decoder held to
# 12 fps, which is the only configuration in which FR_HEAD-FR_TAIL means what
# it is read to mean.
#
# Gated on: pixel-exact, zero UNDERRUNS, and a ceiling that has not moved. The
# ceiling is a property of THIS container in a 256 KB ring; it is asserted
# rather than printed because a change in it is a change in how much a branch
# point can afford, and that should not slip through as a line in a log.
bash tools/bench/pace_run.sh 256 0 > tmp/pace_check.log 2>&1 || {
echo "FAIL: the paced ring pass did not complete."; tail -8 tmp/pace_check.log
exit 1; }
grep -aE "SEEK SLACK|UNDERRUNS" tmp/pace_check.log
grep -q "UNDERRUNS: 0/120" tmp/pace_check.log || {
echo "FAIL: the paced decoder underran -- a frame's slot arrived before its"
echo " record did. Free-running this is earliness (49.6); paced it is not."
exit 1; }
grep -q "ceiling 8 frames" tmp/pace_check.log || {
echo "FAIL: the 256 KB seek-slack ceiling is no longer 8 frames (FINDINGS 51)."
echo " Re-run tools/bench/pace_sweep.sh and re-derive 51 before editing"
echo " this number -- it is what a branch point can spend."
exit 1; }
grep -q "^OK" tmp/pace_check.log || { echo "FAIL: paced pass not pixel-exact";
tail -4 tmp/pace_check.log; exit 1; }
echo "ALL GREEN"