Files
Dragon-s-Lair-X68k/tools/bench/check.sh
T
prosolis 8b5f51704c Take the climb to a real branch point, and find the cadence has a third column
FINDINGS 70, and it closes ROADMAP P6.  65.6 recorded that "the slack table is
here, but 51.3's refill climb with a second consumer through a real branch point
is not", and 67.6 repeated it verbatim two sessions later.
tools/analysis/36_branch_audio.py is that run: 51.3's surplus model, 56.3's 612
real branch points, 32's cadence arithmetic, both containers' headers and the
buffer constants of the player 68 measured, in one place.  No machine -- all
five were already numbers.

AUDIO IS 1.7% OF THE CODEC WIRE AND UP TO 3.30x OF THE CLIMB.  Slack accrues out
of pipe - wire, which is a small difference of two large numbers, so a second
consumer is not priced by its share of the wire -- which is how every budget in
this tree since FINDINGS 8 has quoted it.  At 451.4 KB/s (the container's own
zero-prefill pipe, 49.5) in a 256 KB ring the climb goes 6.70 s -> 22.11 s and
the branch points arriving under it go 482/612 (79%) -> 603/612 (99%).  At 488
it is 1.19x, at 600 it is 1.05x.  Correction to 56.4: it charged audio at
ratectl.AUDIO_KBPS = 7.8, which is 7,812.5 B/s in decimal kB; in binary KB the
figure is 7.6294, so the placeholder was 2.24% high and every column moved in
the flattering direction by less than one part in six hundred of the wire.

AND ON THE PACKED BRANCH THERE IS NO CLIMB AT ALL.  A record goes straight into
GVRAM, so the video lookahead is zero records and 56.4's alarming column is
vacuous when every frame arrives with less lookahead than the one before it.
Acceptance there is a per-frame deadline, not an average, which is why B1 has to
name a burst rate.  The only consumer on that branch with any lookahead is the
audio one: 1.833 s of sound held against 0.000 s of picture.  And at 589.6 KB/s
-- the acceptance figure this project quotes -- the sounded container's surplus
is exactly zero: it plays and never banks a byte, while the silent one climbs
its record in 6.38 s.

THE COST NOBODY HAD COUNTED.  A DLXP2 group is `lump k, then F records`, so lump
k is at a LOWER address than every record of its group but the first, and a seek
to record i finds its audio behind it.  Measured on the 409 within-container
seek targets of the arcade's own graph at the shipped F=11: mean 416.5 ms of
silence entering a branch, median 416.7, p90 750.0, worst 833.3, and only 36 of
409 land on a group boundary.  The other 203 -- the scene changes -- are free by
construction, because lump 0 sits between the header and record 0.  The
container's own start is the one branch point the cadence costs nothing at, and
it was the only one anybody had looked at.

SO THE CADENCE PICK IS REOPENED, on a third column.  32_audio_wire chose F=11 on
padding and RAM; F=1, the cadence it called the worst one, has no group to enter
off-boundary at all, and gives back 12,288 B of RAM for +4.36 KB/s of wire
(0.74% of the acceptance figure).  The alternative is a second read: lump k is
up to 503,808 B behind, so it is a separate command of 7,168 B -- 11.7 ms
against 416, 36x cheaper in time -- and src/player/packed.s starts PG_AK/PG_AKF
at lump 0 and has no audio seek path at all.  Nothing here decides it; the
deciding number is what one more SCSI command costs, and that is B1's.  The
content is not uniform mod F either: the measured mean runs 0.67x..1.23x of a
uniform assumption across the sweep, and the 1.00x at F=11 is a coincidence.

New check.sh stage, ~10 s, and it gates the STRUCTURE and deliberately not the
milliseconds: audio never shortens a climb, the shipped cadence strands most
within-container branches, F=1 strands none, and the lump read is an order of
magnitude cheaper than the silence it removes.

The one thing 70.3 and 70.4 rest on is a DESIGN ASSUMPTION and is printed in the
tool's own output: one container per SCENE (53, 55.1, 56.3).  One container per
sequence makes every seek land on frame 0 and empties both sections.

No encoder, player or container byte moved.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-25 14:21:32 -07:00

873 lines
52 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/56.69 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 "--- session 22: the 68000 keeps its own frame clock (FINDINGS 54) ---"
# ROADMAP P3. Until now the 12 fps tick came from tools/bench/stream.lua -- a
# host writing a word into emulated RAM. A player has no host. src/player/
# clock.i derives the tick from the CRTC's own V-DISP output through the MFP,
# with a remainder-keeping divider whose two constants are READ OUT OF THE CRTC
# at init, so the clock and the raster it counts cannot disagree.
#
# WHAT IS GATED, and it is deliberately structural rather than numeric:
# * the interrupt count equals the raster frame count -- the tick IS the
# raster, not something that merely resembles it;
# * the divider does not accumulate drift, stated in TICKS (a remainder can
# hold back at most one) rather than in ppm, which would let a longer
# window advertise a tighter clock for free;
# * every frame tick waits 4 or 5 refreshes and nothing else, which is what a
# remainder-keeping divider can produce and a broken one cannot.
# The interrupt COST is printed and not gated, for the same reason FINDINGS 53's
# cycle counts are not: it is a measurement, and a change in it is a
# re-derivation in FINDINGS 54 rather than a red light here.
bash tools/bench/clock_run.sh 3000 12 > tmp/clock_gate.log 2>&1 || {
echo "FAIL: the frame clock did not pass."; tail -12 tmp/clock_gate.log
exit 1; }
grep -aE "INTERRUPT:|PER FRAME:|DRIFT:|CADENCE:" tmp/clock_gate.log
grep -q "V-DISP interrupts 3000" tmp/clock_gate.log || {
echo "FAIL: the tick is not the raster -- the interrupt count and the frame"
echo " count disagree. Everything else in FINDINGS 54 rests on that."
exit 1; }
echo "--- session 22: 120 frames decoded on the machine's own clock (FINDINGS 54) ---"
# The strongest form of the claim: the same pixel-exact 120-frame decode out of
# the same 256 KB ring, with NOTHING outside the machine deciding when a frame
# may start. The pace gate in src/player/stream.s is byte-for-byte the one
# FINDINGS 51 measured -- it cannot tell a host-written tick from a machine-
# written one, which is why this is a test of the clock and not of a new rig.
DLX_PACE=2 bash tools/bench/pace_run.sh 256 0 > tmp/selfpace_check.log 2>&1 || {
echo "FAIL: the self-paced pass did not complete."; tail -8 tmp/selfpace_check.log
exit 1; }
grep -aE "decoder SELF-PACED|FRAME CLOCK|UNDERRUNS|NO IDLE" tmp/selfpace_check.log
grep -q "UNDERRUNS: 0/120" tmp/selfpace_check.log || {
echo "FAIL: the self-paced decoder underran."; exit 1; }
grep -q "^OK" tmp/selfpace_check.log || {
echo "FAIL: the self-paced pass was not pixel-exact. The clock changed WHEN"
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 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 "--- session 25: the 68000 reads the disc itself (FINDINGS 57) ---"
# ROADMAP P4, first half. Until now every byte the player consumed was placed in
# emulated RAM by a host: decode.lua preloaded a container, stream.lua answered a
# mailbox at a modelled rate. A player has no host. src/player/scsi.i selects a
# SCSI target on a real MB89352 and issues READ(10) itself.
#
# Session 21 recorded this as blocked -- "MAME's x68000 has no MB89352 path" --
# and that was wrong: `-exp1 cz6bs1` instantiates one, and FINDINGS 32.4 had
# already read its DMA glue in session 9. What was actually missing was the
# card's 8 KB boot ROM, which MAME requires to instantiate the device and which
# the player never executes. scsi_run.sh supplies a blank one on its own rompath.
#
# WHAT IS GATED: the register window (60 of 64 addresses -- the two holes ARE the
# MB89352's missing TMOD and EXBF, and they are what put DREG at $EA0015), and
# two READ(10)s verified byte-for-byte against the host's copy of the same image,
# one of them at a NON-ZERO LBA. Nothing here is gated on rate, and nothing here
# can be: MAME's device models are functional, not transfer-timing accurate.
# Skipped rather than failed when chdman is absent -- it ships with mame-tools.
if command -v chdman > /dev/null; then
bash tools/bench/scsi_run.sh "$DLX" > tmp/scsi_gate.log 2>&1 || {
echo "FAIL: the 68000 could not read the disc."; tail -14 tmp/scsi_gate.log
exit 1; }
grep -aE "ANSWERED|READ\(10\) OK" tmp/scsi_gate.log
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 -- 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. The WRAP COUNT is gated and the mean hole is only reported: DLX5's
# records are up to 511 B longer than DLX4's, so the hole moved (14.7 KB ->
# 13.5 KB) while the tiling did not. Gating a number that the container's
# record lengths move would gate the container, not the policy;
# * every record accounted for: 120 READ(10)s, and the bytes into the ring
# EQUAL to the bytes off the disc -- both read out of the container rather
# than written here. Under DLX4 they differed by 1.34% because a record was
# not a sector (58.3); DLX5 aligns records to sectors and the covering-sector
# read disappears, so the gate is now their IDENTITY;
# * 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; }
# THE BYTE COUNTS COME OUT OF THE CONTAINER, not out of this file. They were
# two hardcoded constants fitted to the DLX4 gate container, and session 28's
# re-encode went red on both of them for the right reason -- the container had
# changed and the expectation had not. A gate whose expected value is a
# literal tests the literal.
EXPECT_B=$(python3 -c "
import sys; sys.path.insert(0, 'tools/encoder')
from dlx import DLX
print(sum(DLX('$DLX').record_lengths()))")
grep -aq "REAL TRANSPORT: 120 READ(10)s by the 68000, $EXPECT_B B into the ring" \
tmp/p4b_check.log || {
echo "FAIL: the 68000 did not fetch all 120 records, or did not fetch"
echo " $EXPECT_B B of them. A short record is a desync, not a shortfall."
exit 1; }
# DLX5 MAKES THESE THE SAME NUMBER, and that identity IS the finding (59.4,
# and 58.3 option C): a sector-aligned container has no covering-sector read,
# so the disc moves exactly the records and nothing else. Under DLX4 they
# differed by 1.34% and both were gated so neither could drift into the other;
# under DLX5 the gate is that they are EQUAL. If a windowed read ever came
# back -- a container that was not aligned, or a layout that lost the
# alignment -- the disc figure would exceed the ring figure and this goes red.
grep -aq "SECTOR OVERHEAD: $EXPECT_B B off the disc for $EXPECT_B B of record = 0.00%" \
tmp/p4b_check.log || {
echo "FAIL: the disc no longer moves EXACTLY the records. On a sector-"
echo " aligned container (DLX5) there is no covering-sector read at"
echo " all, so these two counts must be the same $EXPECT_B B. If they"
echo " differ, either the container lost its alignment or scsi.i is"
echo " windowing again -- and a DMA channel cannot window (59.4)."
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 27: the DMAC drives the data phase, and holds the bus (FINDINGS 59) ---"
# ROADMAP P4a, the last item before M2. The two stages above have the CPU moving
# every byte itself, at the 87.28 clocks per delivered byte FINDINGS 58.2
# measured -- 391.8% of a 12 fps frame. This one hands the DATA IN phase to the
# HD63450 and gates on the thing 57.3 said would be hard to show: that the DMAC,
# and not the CPU, is driving it.
#
# IT IS GATED WITHOUT LOOKING AT $EA0015, and that is the design. With the
# DMAC's OWN asserted -- which it is at idle here -- MAME cannot distinguish a
# CPU-driven byte at that address from a DMAC-driven one, so watching it proves
# nothing. What is gated instead is THE CPU'S OWN PROGRESS:
# * the same 2,048 B off the disc three ways -- PIO, held, stealing -- all
# three byte-exact against the host's copy, so the configuration is being
# compared against a delivery that works and not against nothing;
# * MTC sampled by the INSTRUCTION AFTER the one that starts the channel: zero
# in the held configuration (the whole transfer happened between two
# instructions, because the 68000 did not execute in between) and the full
# count in the stealing one;
# * the CPU's own trip count round its wait loop: 1 against hundreds. A
# counter that CANNOT come out different is 58.3's vacuous "UNDERRUNS: 0/120"
# again, so the run asserts the contrast and not just the held value;
# * the channel's own CSR/CER/MTC/MAR, which must say it moved every byte
# without error;
# * THE PALETTE REGISTERS AT $E82000 (session 30, ROADMAP K1): the same
# transfer aimed at the palette, byte-exact into 256 register words read
# back by the 68000; the SAME transfer aimed 20 KB away leaving the palette
# as the CPU poisoned it, which is what attributes the first run to the
# channel's MAR; and ONE array-chained start crossing from the registers
# into GVRAM, which is the shape of a whole frame -- a palette entry and
# 192 row entries, started once. What this does NOT settle is the board:
# MAME models the palette as a generic palette_device over memory_array,
# whose write16 is a plain COMBINE_DATA, so it cannot tell a register file
# that takes byte writes from one that does not (FINDINGS 62.4);
# * and a WINDOWED read through the channel REFUSED. 117 of 120 records start
# part way into a sector (58.3); a channel writes a contiguous run and cannot
# drop the bytes in front of one, so it would write the neighbouring records
# into the ring with no bounds check to catch it (49.2). The refusal is what
# makes "sector-aligned container" a precondition the transport states.
#
# NOT GATED ON RATE, and it cannot be: MAME's DMAC runs on wall-clock attotimes
# (42.5) and models a held bus by HALTING the CPU rather than by charging it
# cycles per operand. `W` is untouched. tools/analysis/28_autorequest_cost.py
# prices what this configuration costs, from the datasheet and an explicit rate.
# Skipped rather than failed when chdman is absent.
if command -v chdman > /dev/null; then
bash tools/bench/dma_run.sh "$DLX" > tmp/dma_gate.log 2>&1 || {
echo "FAIL: the DMAC did not drive the SCSI data phase."
tail -16 tmp/dma_gate.log; exit 1; }
grep -aE "BYTES OK|MTC one instruction|trips round|REFUSED|PALETTE|ONE START" \
tmp/dma_gate.log \
| sed 's/^ *//;s/^/ /'
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
# outside projects exist; it writes tmp/scenegraph.json in this project's own
# DLXSCENE1 schema and everything downstream reads only that.
# What is gated is the IMPORT, not the numbers: 516 sequences and 906 input
# windows, and the four timing helpers still being the formulas the importer
# evaluates. Skipped when the checkout is absent.
DIRKSIMPLE=${DLX_DIRKSIMPLE:-tmp/scenegraph/DirkSimple}
if [ -f "$DIRKSIMPLE/data/games/lair/game.lua" ]; then
DLX_DIRKSIMPLE="$DIRKSIMPLE" python3 tools/import/scenegraph.py \
-o tmp/scenegraph.json > tmp/scenegraph_import.log 2>&1 \
|| { cat tmp/scenegraph_import.log; exit 1; }
sed "s/^/ /" tmp/scenegraph_import.log
grep -q "516 sequences, 906 input windows" tmp/scenegraph_import.log || {
echo "FAIL: the scene graph did not import to 516/906 -- upstream changed,"
echo " or the parser silently dropped branches."; exit 1; }
python3 tools/analysis/25_scene_graph.py --kbps 488 --ring 256 \
> tmp/scenegraph_check.log 2>&1 \
|| { tail -20 tmp/scenegraph_check.log; exit 1; }
grep -aE "^ WORST |^ ZERO-PLAY|^ BRANCH STRUCTURE" tmp/scenegraph_check.log
else
echo " SKIPPED: no DirkSimple checkout at $DIRKSIMPLE"
echo " (git clone --depth 1 https://github.com/icculus/DirkSimple)"
fi
echo "--- session 29: the packed paint, and what it does to the codec (FINDINGS 61) ---"
# tools/bench/blit.s gained V8/V9/V10 -- the packed full-frame blit, and the two
# ways a 4x4 BLOCK decoder could survive the packed layout. 47.6.1 had filed the
# packed paint's `movem` shape as an ASSUMPTION since session 16; this measures
# it, in the same run as V1/V3/V4 so it is quoted against numbers that have not
# moved since session 9.
#
# WHAT IS GATED IS STRUCTURAL, not numeric, for the reason the load stage gives:
# MAME samples these on a 1/56.69 s clock and no cost model in the tree depends
# on their exact value. What DOES depend on them is the ORDER, and the order is
# the whole of FINDINGS 61:
# V8 < V1 packing halves the full-frame literal paint
# V9 > V4 packing makes a BLOCK decoder DEARER, not cheaper
# V10 < V4 unless the blocks are paired, which costs SKIPs instead
# A tree where any of those flipped has a different answer to 44.7 and should
# say so out loud rather than let 29_packed_player.py narrate the old one.
python3 tools/bench/prep_frame.py tmp/fr_00020 tmp/frame256.bin 0 --reserve-black
rm -f tmp/blit_v8.log
( cd tmp && SDL_VIDEODRIVER=dummy stdbuf -oL timeout -k 5 300 mame x68000 \
-bios ipl10 -ramsize 2M -video soft -window -sound none -nothrottle -plugins \
-autoboot_script ../tools/bench/blit.lua -seconds_to_run 120 \
> blit_v8.log 2>&1 )
grep -aq "summary (instruction cycles only" tmp/blit_v8.log || {
echo "FAIL: the blit timing run produced no summary -- it did not finish."
tail -8 tmp/blit_v8.log; exit 1; }
python3 - <<'EOF' || exit 1
import re, sys
v = {}
for line in open("tmp/blit_v8.log", errors="replace"):
m = re.search(r"V(\d+)\s+(\d+) cyc", line)
if m: v[int(m.group(1))] = int(m.group(2))
need = (1, 2, 3, 4, 8, 9, 10)
missing = [n for n in need if n not in v]
if missing: sys.exit(f"FAIL: blit.lua reported no V{missing} -- run incomplete.")
for a, op, b, why in ((8, "<", 1, "packing did not halve the literal paint"),
(9, ">", 4, "packed BLOCK order came out CHEAPER than "
"unpacked -- 61.3's conclusion is inverted"),
(10, "<", 4, "pairing the blocks did not buy back the "
"movem shape")):
ok = v[a] < v[b] if op == "<" else v[a] > v[b]
if not ok:
sys.exit(f"FAIL: V{a} {v[a]:,} is not {op} V{b} {v[b]:,} -- {why}.")
print(f" V1 {v[1]:,} / V8 PACKED {v[8]:,} = {100*v[8]/v[1]:.0f}% -- "
f"and V3, the unpacked WRITE-ONLY floor, is {v[3]:,}")
print(f" V4 {v[4]:,} / V9 packed-block {v[9]:,} = {100*v[9]/v[4]:.0f}% -- "
f"packing costs a BLOCK decoder {100*v[9]/v[4]-100:.0f}%")
print(f" V10 paired blocks {v[10]:,} = {100*v[10]/v[4]:.0f}% of V4, and pairing "
f"is paid for in SKIPs")
EOF
python3 tools/analysis/29_packed_player.py "$DLX" > tmp/packed_player.log 2>&1 \
|| { tail -20 tmp/packed_player.log; exit 1; }
grep -aE "SKIP block PAIRS|free / DMAC->GVRAM / PACKED|^ CODEC, gate" \
tmp/packed_player.log
echo "--- session 31: the PACKED container, and the picture re-derived (FINDINGS 63) ---"
# ROADMAP K2. The container is REBUILT every run rather than reused when
# present, the way the codec's gate container is: a packed encode is 3 seconds
# because there is no k-means in it, so there is no reason to let a stale file
# stand between the encoder and the gate.
# The window's audio has to exist before the container can carry it, so the
# extraction that used to live in session 33's stage moves up here. Same seconds
# as the frames, and that is not a convenience: an audio stream that is not the
# same seconds as the picture is not this project's audio.
[ -f tmp/au_singe.raw ] || python3 tools/encoder/extract_audio.py 00223 tmp/au_singe.raw 15625 539.4 10.0
python3 tools/encoder/pack.py tmp/fr_singe tmp/packed_singe.dlxp \
--nframes "$NF" --audio tmp/au_singe.raw > tmp/pack_encode.log 2>&1 \
|| { cat tmp/pack_encode.log; exit 1; }
grep -aE "^ (record|wire|DLXP2|the four axes|lump payload)" tmp/pack_encode.log
# ...and the SILENT control beside it, which is what says the interleave moved no
# picture byte. It is the same encode with one flag off, and a packed encode is
# four seconds because there is no k-means in it, so the control is cheap enough
# to build every run rather than reason about.
python3 tools/encoder/pack.py tmp/fr_singe tmp/packed_singe_silent.dlxp \
--nframes "$NF" > tmp/pack_encode_silent.log 2>&1 \
|| { cat tmp/pack_encode_silent.log; exit 1; }
# WHAT IS GATED. Four format invariants that a DMA channel cannot check for
# itself -- it copies bytes and has no opinion about them (FINDINGS 62) -- and
# the three quality claims FINDINGS 61.9 rests the whole packed branch on:
# round-trip, sector geometry, index 0 unused, palette words agree
# packed > the shipping codec / > the codec's CEILING / > a SCENE-palette control
# A tree where any of the last three flipped has a different answer to ROADMAP K
# and should say so out loud rather than let the branch keep building.
python3 tools/analysis/30_packed_container.py tmp/packed_singe.dlxp \
--codec "$DLX" > tmp/packed_container.log 2>&1 \
|| { cat tmp/packed_container.log; exit 1; }
sed -n '/^PSNR/,$p' tmp/packed_container.log | grep -aE "RGB888|CODEC|CEILING|CONTROL|PER-FRAME|packed vs|worth|costs"
# And the container's OWN BYTES through px68k's real gvram.c, with the harness
# computing no interleave -- the only test that can catch an encoder whose byte
# order is wrong, because the container round-trips against its own inverse
# either way. Same skip-not-fail rule as the C68K stage: px68k is not in here.
if [ -f "$PX68K/x68k/gvram.c" ]; then
make -s -C tools/bench/gvpack PX68K="$PX68K"
# Frame 0 carries the two negative controls; the other two are there because
# one frame rendering does not say the container's 120th record is placed
# right, and record placement is arithmetic this format has no index to check.
python3 tools/bench/gvpack/verify_dlxp.py tmp/packed_singe.dlxp 0 --controls || exit 1
for f in $((NF / 2)) $((NF - 1)); do
# NOT piped into head: `set -e` reads a pipeline's status from its LAST
# command, so a piped verifier that failed would be reported by head's zero.
python3 tools/bench/gvpack/verify_dlxp.py tmp/packed_singe.dlxp "$f" \
> "tmp/dlxp_f$f.log" 2>&1 || { cat "tmp/dlxp_f$f.log"; exit 1; }
head -1 "tmp/dlxp_f$f.log"
done
else
echo " SKIPPED: no px68k at $PX68K -- the container's bytes were not rendered"
fi
echo "--- session 32: the PACKED PLAYER, end to end off the disc (FINDINGS 64) ---"
# ROADMAP K3. src/player/packed.s brings up its own display, builds its own
# 193-entry DMA chain, keeps its own frame clock off V-DISP and fetches every
# record itself with READ(10) off a CZ-6BS1. This script writes no picture byte
# and no palette entry.
#
# WHY EVERY FRAME IS COMPARED AND THE CODEC'S GATE COMPARES ONE. The codec is
# temporally recursive -- a SKIP block is a claim about the previous frame -- so
# its last frame audits all 120. A packed frame is a LITERAL and frame 119 says
# nothing about frame 60. The simplification that deleted the ring also deleted
# the gate's free lunch.
#
# AND WHY IT IS PACED AT HALF RATE. The write window has to be OPEN for the
# whole transfer and buffer mode blanks the graphics layer, so at the
# container's own 12 fps there is no instant at which a complete frame is
# displayable and there is nothing to snapshot (FINDINGS 64.2 -- that is the
# session's finding, not a rig limitation being worked around). Half rate opens
# a display interval without changing one byte of the transfer.
#
# GATE ONLY: runs 2-4 of packed_run.sh measure the apparatus rather than gate
# it, and they are three more MAME jobs for numbers that cannot change unless
# MAME does. tools/bench/packed_run.sh with no DLX_PK_GATE_ONLY runs all four.
if [ -f "$HOME/mame/roms/x68000.zip" ] || [ -d "$HOME/mame/roms/x68000" ]; then
DLX_PK_GATE_ONLY=1 DLX_PK_NFR="$NF" bash tools/bench/packed_run.sh \
tmp/packed_singe.dlxp > tmp/packed_gate_stage.log 2>&1 \
|| { cat tmp/packed_gate_stage.log; exit 1; }
# SPLIT AT RUN 5, because the audio run below is in the same log and its
# verifier emits OK lines too -- one grep over the whole file would print
# session 36's result under session 31's heading.
sed -n '1,/--- 5. THE AUDIO/p' tmp/packed_gate_stage.log | \
grep -aE "^ (FLAG|array|chain|frame clock|late frames|WRITE WINDOW)|^OK "
else
echo " SKIPPED: no x68000 romset -- the player was not run"
fi
echo "--- session 36: THE CONTAINER'S OWN AUDIO, OUT OF THE CHIP (FINDINGS 68) ---"
# ROADMAP P6c. Every piece of this existed before this stage did and none of it
# was joined up: the container has carried the lumps since 67, the transport is
# the IPL ROM's channel-3 configuration from 66, and what was missing was the
# lump buffer and the remainder accumulator in a player. This is the run where
# a byte of the container reaches the speaker.
#
# WHY THE GATE IS A WAV AND NOT A COUNTER. src/player/packed.s reports lumps
# armed, lumps fetched, payload bytes and starves, and EVERY ONE OF THOSE CAN BE
# RIGHT WHILE THE SOUND IS WRONG -- which is not hypothetical, it is the bug
# this session shipped: the refill ran one lump too far ahead and overwrote the
# buffer channel 3 was reading out of, and the player's account of it was
# 11 of 11 armed, 11 fetched, 78,125 B, no starve. Nothing parses a packed
# container (FINDINGS 67.4), so a wrong byte is not an error, it is a sound.
# tools/bench/verify_packed_audio.py accounts for all 78,125 bytes against
# MAME's own capture, one delivered byte at a time.
#
# The run is part of packed_run.sh's gate half, so DLX_PK_GATE_ONLY takes it.
if [ -f tmp/packed_gate_stage.log ] && \
grep -aq -- "--- 5. THE AUDIO" tmp/packed_gate_stage.log; then
sed -n '/--- 5. THE AUDIO/,$p' tmp/packed_gate_stage.log | \
grep -aE "^(OK|FAIL) |^ AUDIO:|^ {5}(nibbles per|worst|payload|-> |the player)"
else
echo " SKIPPED: no x68000 romset, or the container is silent"
fi
echo "--- session 33: AUDIO -- the encoder, and what it does to the wire (FINDINGS 65) ---"
# ROADMAP P6, everything in it except the bus half session 20 closed. The audio
# is the SAME WINDOW as the frames -- 00223 from 539.4 s for 10 s -- because an
# audio stream that is not the same seconds as the picture is not this project's
# audio, and a gate that lets the two drift apart would never say so.
# tmp/au_singe.raw was extracted by session 31's stage, which needs it to build
# the container.
# There is NO ffmpeg encoder for this format -- adpcm_ima_oki is decode-only --
# so the encoder cannot be checked against a reference. What is checked is that
# the decoder our encoder runs in its own loop IS ffmpeg's, sample for sample.
# An encoder that agrees with its own wrong decoder is the failure this catches.
python3 tools/bench/verify_adpcm.py tmp/au_singe.raw || exit 1
# And the container arithmetic. The interesting line is the padding: a packed
# record has no index BY DESIGN, so audio has to ride a fixed cadence, and the
# obvious cadence throws away a third of every audio sector.
python3 tools/analysis/32_audio_wire.py tmp/packed_singe.dlxp \
> tmp/audio_wire.log 2>&1 || { cat tmp/audio_wire.log; exit 1; }
grep -aE "^ ( 1| 11| 81) |THE FLOOR|F=1 |F=11|is ZERO|SOUND IS WHAT" tmp/audio_wire.log
echo "--- session 34: THE CHIP'S OWN DECODER, off the machine (FINDINGS 66) ---"
# ROADMAP P6a. 68000 code programs HD63450 channel 3 exactly as the IPL ROM
# programs it and feeds the MSM6258 a designed nibble stream at the chip's own
# pace; ONE of sixteen candidate decoder models reproduces MAME's capture
# sample-exact, and every axis has a negative control. The encoder disagreed
# with the chip on ALL FOUR axes, and the largest of them is not the delta
# formula 65 named -- it is the NIBBLE ORDER, at -25.7 dB.
bash tools/bench/adpcm_run.sh > tmp/adpcm_gate.log 2>&1 || {
cat tmp/adpcm_gate.log; exit 1; }
grep -aE "^(OK|FAIL) |^ (feed|nibbles|delta|clamp|accumulator)" tmp/adpcm_gate.log \
| sed 's/^/ /'
# And the bill, on the same ten seconds every other audio figure is quoted on.
python3 tools/analysis/33_adpcm_model.py tmp/au_singe.raw > tmp/adpcm_model.log 2>&1 \
|| { cat tmp/adpcm_model.log; exit 1; }
grep -aE "wrong only here|played on the chip|decoded on the encoder|headroom left" \
tmp/adpcm_model.log
echo "--- session 35: DLXP2 -- a packed container with sound in it (FINDINGS 67) ---"
# ROADMAP P6b. 65.3 did the arithmetic and wrote no byte; 66 measured which of
# sixteen decoder models the chip runs. This is the container both produce, and
# it needs a gate of its own because NOTHING PARSES A PACKED CONTAINER: a lump
# one sector out does not fail, it paints 512 B of audio and plays 512 B of
# picture, and a gate that only looked for errors would pass it.
#
# The picture side of it is gated twice over: here against a SILENT control
# built from the same frames, and above by the packed player itself, which now
# carries the `(i//F)*A` term and still gets all 120 frames pixel-exact off a
# real volume.
python3 tools/analysis/34_packed_audio.py tmp/packed_singe.dlxp \
> tmp/packed_audio.log 2>&1 || { cat tmp/packed_audio.log; exit 1; }
grep -aE "^ (OK|FAIL) |^ (order|variant|bits|init) |min of play ->" \
tmp/packed_audio.log
echo "--- session 37: THE AUDIO LEVEL, measured off the whole disc (FINDINGS 69) ---"
# ROADMAP P6, the item 66.3 reopened and two sessions deferred. The chip clamps
# its accumulator at 10 bits INSIDE the recursion, and the ten seconds every
# audio figure in this tree is quoted on peak at 435 of 511 -- which fits, and
# fits BY ACCIDENT, because that window is a -13.4 dBFS passage.
#
# So the level is measured against the loudest thing the game can play, which
# means every stream of the unique scene footage (00000-00201, FINDINGS 32.1)
# through extract_audio.py's own chain. The gate asserts the disc's peak and
# the census behind it; a different pressing is a legitimate reason for it to
# go red, a different ffmpeg downmix is not.
#
# ~18 s, and it needs the Blu-ray mounted like every other stage here.
python3 tools/analysis/35_audio_level.py --gate > tmp/audio_level.log 2>&1 \
|| { cat tmp/audio_level.log; exit 1; }
grep -aE "DISC PEAK|LOUDEST PASSAGE|THE CLAMP|LEVEL GATE|NO AUDIO TRACK|^ 1\.0000|^ 0\.5" \
tmp/audio_level.log
echo "--- session 38: the refill climb with a second consumer, through a real branch point (FINDINGS 70) ---"
# ROADMAP P6, the oldest item in it: 65.6 and 67.6 both recorded that the slack
# table existed and that 51.3's climb had never met a branch point with audio on
# the wire. This is that run. It needs the scene graph, so it skips with the
# session-24 stage when there is no checkout.
#
# WHAT IS GATED IS STRUCTURAL, and deliberately not the milliseconds: the
# silences move with the scene table and with the one-container-per-scene
# assumption the tool prints in its own section 5. What must not move is the
# ORDER and the SIGNS -- audio never shortens a climb, the shipped cadence
# strands most within-container branch points off a group boundary, F=1 strands
# none, and the lump read that removes the silence is an order of magnitude
# cheaper than the silence. A tree where any of those flipped has a different
# answer to the cadence pick.
if [ -f "$DIRKSIMPLE/data/games/lair/game.lua" ]; then
python3 tools/analysis/36_branch_audio.py --gate \
> tmp/branch_audio.log 2>&1 || { cat tmp/branch_audio.log; exit 1; }
grep -aE "^ (OK|FAIL) |SECOND CONSUMER IS|BRANCH-AUDIO GATE|^ mean |^ free " \
tmp/branch_audio.log
else
echo " SKIPPED: no DirkSimple checkout at $DIRKSIMPLE"
fi
echo "ALL GREEN"