src/player/load.i expands both codebooks to word-per-pixel form and packs the palette to GGGGGRRRRRBBBBBI out of the RAW container header, byte-exact against tools/bench/dlxload.py on both CPU cores. The palette half is gated on words read back out of the palette registers at $E82000, so "the words reached the hardware" is part of what passes. ROADMAP P1 is done; P2's encoder half (a reserved black entry, 23.4) is not, and is a re-encode rather than an edit. A scene change costs 18.96 ms of 68000 time, 22.8% of one 12 fps frame; boot costs 24.70 ms. The scratch tables describe the CRTC, not the scene, so pal_tables is a separate entry point built once at boot -- 5.29 ms off every scene change. The one that moves something: the scene header is 5,920 B that no rate table in this tree included, because it belongs to no frame record. In FINDINGS 51.3's currency it is divided by the surplus pipe - wire, so it is hypersensitive: 138 ms of extra refill climb at 488 KB/s and 1.099 s at 451.4 KB/s, for the same bytes. tools/analysis/22_scene_load.py prices it across explicit rates. Recorded as open: the two CPU cores agree to <3% on every stage but the table build, where they differ by 16.4%. px68k's C68K charges a flat 50 clocks for MULU/MULS (c68kmacro.h:1869) where the 68000 charges 38+2n, which explains 4,608 of the 8,703 clock gap. 4,095 clocks are unexplained. Nothing else in src/player/ multiplies, so no figure in FINDINGS 24-52 is affected. decode.s and stream.s are untouched; decode.bin is still 1,296 B at the same MD5. check.sh gains a stage that gates byte-exactness on both cores and deliberately does not gate the cycle counts -- MAME's clock is 1/55.46 s and a wall timing would make the green light host-sensitive. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
298 lines
17 KiB
Bash
Executable File
298 lines
17 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 "--- session 21: the 68000 builds its own codebooks and palette (FINDINGS 53) ---"
|
|
# ROADMAP P1+P2. Until now tools/bench/dlxload.py expanded the codebooks and
|
|
# packed the palette HOST-SIDE and the rigs pushed the result into emulated RAM.
|
|
# A player has no host. src/player/load.i does both on the 68000, out of the RAW
|
|
# container header, and this gates it byte-for-byte against dlxload.py -- which
|
|
# stays the reference, because what changed is where the transforms RUN, not
|
|
# what they produce.
|
|
#
|
|
# Byte-for-byte and not "close enough": a wrong codebook byte is a wrong colour
|
|
# in every block that uses that codeword, and a wrong shared LSB is a slightly
|
|
# wrong colour that looks like a codec artefact rather than a loader bug.
|
|
# The palette half is read back out of the PALETTE REGISTERS at $E82000, so
|
|
# "the words reached the hardware" is part of what passes.
|
|
#
|
|
# NOT gated on the cycle counts, and the reason is NOT the one blit.s has. These
|
|
# are emulated time and reproduce exactly run to run; what they are not is
|
|
# sharp, because MAME samples them on a 1/55.46 s clock and the job takes
|
|
# milliseconds. Nothing in the tree's cost models depends on them either. A
|
|
# change in them is a re-derivation in FINDINGS 53, not a red light here.
|
|
bash tools/bench/load_run.sh "$DLX" > tmp/load_gate.log 2>&1 || {
|
|
echo "FAIL: the load-time transforms did not pass."; tail -12 tmp/load_gate.log
|
|
exit 1; }
|
|
grep -aE "^ *OK|both CPU cores|SCENE CHANGE" tmp/load_gate.log | sed 's/^ *//;s/^/ /'
|
|
|
|
echo "ALL GREEN"
|