Files
prosolis ba966efe7e Take the player through a branch with sound, and find the predictor does not seek
FINDINGS 71, ROADMAP P6d.  70.3 named exactly what was missing -- packed.s starts
PG_AK/PG_AKF at lump 0 and has no audio seek path -- and priced its absence at a
mean 416.5 ms of silence over the arcade's 409 within-container seek targets.
pg_aseek is that path: the lump index, the stream position, the remainder
accumulator and the byte offset into the group, then the second READ(10) at the
lump's own LBA and a re-arm part way into the buffer.  Measured off a real volume:
132,162 B of spliced stream accounted for byte by byte in MAME's own capture,
across a branch at frame 37 -- four frames into lump 3, deliberately NOT on a
group boundary -- in both chip configurations.  Skip computed 2,604 B, cadence
says 2,604.

THE PREDICTOR DOES NOT SEEK, AND THE ERROR IS DC.  The MSM6258's accumulator is a
pure integrator with no leakage term, so a branch that hands the chip bytes chosen
for a state it is not in produces an offset that does not decay.  Playing through:
DC -355 of 511 with AC 0.00 -- the right shape from the wrong ground -- still -108
four seconds later.  STOP and re-PLAY: all 62,500 post-seek samples are EXACTLY a
decode from the container's own init, and the whole error is the single constant
-65.  A re-PLAY is 5.5x better and neither is zero, so PG_ARST is a mailbox with a
number under it.  The host computes -65 out of the container's bytes and the gate
asserts the equality rather than printing both.

AND THE ONLY FIX THAT REACHES ZERO IS THE ENCODER'S.  A player cannot set the
chip's accumulator, only reset it.  Resetting the encoder's predictor every frame
makes all 119 of the container's branch points exact for 0.33 dB (21.99 -> 21.66),
because the step table's floor is a constant 16.  That is a DLXP3 and it is
deliberately not in tools/encoder.

TWO SILENT BUGS, BOTH CAUGHT BY THE CAPTURE.  pg_udiv32 trashes d4 and pg_aseek
held hz there, so the offset came out 1 byte instead of 2,604 -- 166 ms of the
wrong part of the scene at exactly the right rate, every counter agreeing.  And
one already in the tree that had passed this gate three times: pg_ainit waited on
a READ-BACK MTC before PLAY, which is the same test as "a byte has left RAM" only
if no byte leaves in between.  One does, and the chip then plays the scene one
byte in, forever.  Found by locating the capture's opening samples in the
container image: sector 1 + 1.  The witness is now the count that was written.

ALL GREEN, two new stages included.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-25 15:10:20 -07:00

291 lines
16 KiB
Bash
Executable File

#!/bin/bash
# THE PACKED PLAYER, END TO END, OFF A REAL VOLUME. ROADMAP K3.
#
# tools/bench/packed_run.sh [container.dlxp]
#
# Four runs of src/player/packed.s, and each answers a different question. They
# are separate runs because the questions interfere: 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 the pixel gate has nothing to sample. Pacing slower opens a
# display interval without changing one byte of the transfer. Reporting the
# rate off the gate run instead would have been the flattering shortcut, and it
# reports the PACE rather than the disc -- the first cut of packed.lua did
# exactly that and printed 297 KB/s off a 6 fps run.
#
# 1. GATE, stealing, paced at half rate: 120 records, 120 snapshots, every one
# compared. A packed frame is a LITERAL, so unlike the codec's gate the last
# frame audits nothing and all 120 have to be checked (verify_packed.py).
# 2. RATE, stealing, FREE-RUNNING: the loop is transfer-bound, so the mean
# inter-frame IS the emulated transport's time for a record.
# 3. RATE, held, FREE-RUNNING: the same, with the bus held.
# 4. CLOCK, held, paced at the container's fps: what holding the bus does to a
# frame clock built on counting V-DISP interrupts.
#
# DLX_PK_GATE_ONLY=1 runs 1 alone. That is what tools/bench/check.sh takes: the
# green light's job is to catch a regression in the PLAYER, and runs 2-4 measure
# the apparatus rather than gate it -- three more MAME jobs for numbers that
# cannot change unless MAME does.
#
# THE APPARATUS is tools/bench/dma_run.sh's -- `x68000 -exp1 cz6bs1` and a
# zero-filled scsiexrom.bin on a private rompath -- and the volume is
# tools/bench/mkvol.sh's, which for a DLXP container is the container itself.
#
# WHAT NO RUN HERE MEASURES: `W`, and any rate a real medium would deliver.
# MAME's device models carry no transfer timing (docs/BENCHMARK.md, 42.5). What
# is measured is the SHAPE -- one channel start, 193 destinations, 120 times,
# on a clock the machine keeps itself, with every frame pixel-exact.
set -e
cd "$(dirname "$0")/../.."
DLXP=${1:-tmp/packed_singe.dlxp}
NFR=${DLX_PK_NFR:-120}
GATE_FPS=${DLX_PK_GATE_FPS:-6}
bash tools/bench/mkvol.sh "$DLXP"
python3 tools/bench/prep_packed.py "$DLXP"
# WHICH ORDER THIS CONTAINER USES, read out of the container rather than
# assumed. FINDINGS 62.5/63.4 priced palette-first and palette-last at -12.8 dB
# for one paint apiece and could not choose between them, so the format records
# it (dlxp.py flags bit 1) and BOTH have to pass this gate. The chain assertion
# below is the only place the difference is visible from outside the machine,
# and hard-coding either order there would turn "K3 ran both" into "K3 ran one
# and the other could not have failed".
PALLAST=$(sed -n 's/.*palette_last = \([01]\),.*/\1/p' tmp/packed_meta.lua)
if [ "$PALLAST" = "1" ]; then
CHAIN0='chain\[0\] = MAR \$C08000 MTC 256'
ORDER="palette LAST -- the 193rd entry"
else
CHAIN0='chain\[0\] = MAR \$E82000 MTC 512 ; chain\[1\] = MAR \$C08000 MTC 256'
ORDER="palette FIRST -- entry 0, then 192 rows"
fi
echo " container order: $ORDER"
tools/vasm/vasmm68k_mot -Fbin -o tmp/packed.bin src/player/packed.s > /dev/null
# One run. $1 names the log, the rest are environment.
run() {
local tag=$1; shift
rm -rf "tmp/snap_packed_$tag"; mkdir -p "tmp/snap_packed_$tag"
# stdbuf -oL: 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 (34.1).
( cd tmp && env SDL_VIDEODRIVER=dummy "$@" stdbuf -oL timeout -k 5 900 \
mame x68000 -bios ipl10 -exp1 cz6bs1 \
-rompath "$HOME/mame/roms;./p4roms" -hard dlxpdisk.chd \
-ramsize 2M -video soft -window $SOUNDARGS -nothrottle -plugins \
-autoboot_script ../tools/bench/packed.lua \
-snapshot_directory "./snap_packed_$tag" -snapview native \
-seconds_to_run "$SECS" > "packed_$tag.log" 2>&1 )
grep -aq "^\[PK\] done" "tmp/packed_$tag.log" || {
echo "FAIL: the $tag run did not finish -- no completion marker."
tail -12 "tmp/packed_$tag.log"; exit 1; }
grep -a "^\[PK\]" "tmp/packed_$tag.log" | sed 's/^\[PK\] / /'
}
fail() { echo "FAIL: $1"; exit 1; }
# -sound none for every run that is not about sound, which is all of them until
# run 5. 15,625 is not a preference there: it is the chip's own stream rate
# (8 MHz / 512), and equal rates are what keep MAME's resampler from filtering
# the thing being measured (FINDINGS 66, adpcm_run.sh).
SOUNDARGS="-sound none"
echo "--- 1. THE GATE: $NFR records, paced at $GATE_FPS fps, channel stealing ---"
SECS=$(( NFR / GATE_FPS + 25 ))
run gate DLX_PK_HELD=0 DLX_PK_PACE=1 DLX_PK_FPS=$GATE_FPS DLX_PK_NFR=$NFR
cp tmp/packed_snaps.csv tmp/packed_snaps_gate.csv
# THE ASSERTIONS. Printing a result and gating on it are different things.
grep -aq "^\[PK\] FLAG=\$FF" tmp/packed_gate.log || \
fail "the player did not reach the end of the scene. FLAG=\$E1 is a CRTC mode
the frame clock cannot divide, \$E2 is a transport failure -- and the
TRANSPORT FAILED line above names which."
grep -aq "array: the 68000 built 193 entries (the container wants 193)" \
tmp/packed_gate.log || \
fail "the 68000 built a chain of the wrong length. One entry short delivers a
picture with its last row missing, which looks like a decode bug and is a
layout bug; the container's geometry and the player's arithmetic are two
independent statements of one number and they have to agree."
grep -aq "$CHAIN0" tmp/packed_gate.log || \
fail "the chain does not have the shape this container asks for ($ORDER).
The crossing from the palette registers into GVRAM IS the packed frame
(FINDINGS 62) -- a palette entry and 192 row entries, one start, the CPU
halted throughout -- and an array built the other way round from the
record feeding it does not fail: it paints 192 rows of picture into the
palette registers and 512 B of palette across the top of the screen."
grep -aq "late frames (tick already past at the gate): 0$" tmp/packed_gate.log || \
fail "a frame missed its slot in the GATE run, which is paced at half rate on
purpose. That is not a rate result -- it means the transfer did not fit in
a slot twice as long as the container's, and the pixel comparison below is
then sampling frames the player was still overwriting."
grep -aq "0 frames NOT SAMPLED" tmp/packed_gate.log || \
fail "the gate run could not sample every frame: the write window reopened
before the snapshot on at least one. At half the container's rate the
transfer must fit inside the display interval with room to spare, and if
it does not the comparison below is checking frames the player was still
overwriting."
python3 tools/bench/verify_packed.py "$DLXP" --snap tmp/snap_packed_gate \
--map tmp/packed_snaps_gate.csv --min-frames "$NFR"
# ---------------------------------------------------------------------------
# 5. THE AUDIO. ROADMAP P6c: the container's own bytes, out of channel 3,
# beside the video channel. Paced at the CONTAINER's rate rather than the
# gate's half rate, because the audio was cut at 12 fps and a 6 fps run would
# starve the chip for half of every group -- the picture can be slowed down and
# a crystal cannot. Nothing is snapshotted; the instrument is the WAV, and
# tools/bench/verify_packed_audio.py accounts for every byte of the stream in
# it. The tag is `aud` and not `audio` because tmp/packed_audio.log is
# tools/analysis/34_packed_audio.py's, in check.sh.
#
# CYCLE STEALING, and that is a result rather than a setting -- run 6 below is
# the same run with the bus held and it is a CONTRAST, not a gate.
AUDIO_ON=$(sed -n 's/.*has_audio = \([01]\),.*/\1/p' tmp/packed_meta.lua)
CFPS=$(sed -n 's/^ fps = \([0-9]*\),.*/\1/p' tmp/packed_meta.lua)
if [ "$AUDIO_ON" = "1" ]; then
echo
echo "--- 5. THE AUDIO: the container's own lumps, out of channel 3, while"
echo " the video channel is on the same bus (ROADMAP P6c) ---"
SECS=$(( NFR / 8 + 30 ))
SOUNDARGS="-samplerate 15625 -wavwrite packed_aud.wav"
run aud DLX_PK_HELD=0 DLX_PK_PACE=1 DLX_PK_FPS="$CFPS" DLX_PK_NFR=$NFR \
DLX_PK_AUD=1 DLX_PK_AJSON=packed_aud.json
SOUNDARGS="-sound none"
grep -aq "^\[PK\] FLAG=\$FF" tmp/packed_aud.log || \
fail "the audio run did not reach the end of the scene."
python3 tools/bench/verify_packed_audio.py "$DLXP" tmp/packed_aud.wav \
tmp/packed_aud.json || \
fail "the chip did not play the container. The counters above can all be
right while this fails -- nothing parses a packed container, so a lump
fetched into a buffer that is still being read is not an error, it is a
sound (FINDINGS 67.4, and it is the bug session 36 shipped and caught)."
fi
# ---------------------------------------------------------------------------
# 7. THE SEEK, WITH SOUND ON IT. FINDINGS 70.3 asked for this and named what
# was missing: "src/player/packed.s starts PG_AK/PG_AKF at lump 0 and has no
# audio seek path at all". This is that path, run.
#
# THE FRAME IS CHOSEN NOT TO BE A MULTIPLE OF THE CADENCE, and that is the whole
# design of the run. A DLXP2 group is `lump k, then F records`, so a branch
# that lands on a group boundary needs no offset and no second read -- and 36 of
# the arcade's 409 within-container seek targets do land on one. A run that
# picked one of those would exercise the arithmetic that was already there and
# report success. DLX_PK_SEEK defaults below to a frame `f mod F != 0`, so the
# byte offset inside the lump is load-bearing: get it wrong and the chip is fed
# a stream that starts up to F frames early, which is not an error, it is a
# rate, and only verify_packed_audio.py's spliced walk can see it.
#
# AND THE SECOND RESULT IS ONE NO COUNTER CAN REACH: the chip's predictor does
# not seek. Every byte can arrive, in order, exactly -- and the samples still
# be wrong, because the encoder chose them for a state a continuous play would
# have been in. The verifier measures that against its own control.
if [ "$AUDIO_ON" = "1" ] && [ "${DLX_PK_NOSEEK:-0}" != "1" ]; then
SEEKF=${DLX_PK_SEEK:-37}
CADF=$(sed -n 's/^ cad_f = \([0-9]*\),.*/\1/p' tmp/packed_meta.lua)
[ $((SEEKF % CADF)) -ne 0 ] || \
fail "the seek frame $SEEKF is a multiple of the cadence $CADF, so it lands
ON a group boundary -- the one case that needs no byte offset and would
pass with the offset arithmetic deleted (FINDINGS 70.3)."
echo
echo "--- 7. THE SEEK: two passes, the second starting at frame $SEEKF --"
echo " lump $((SEEKF / CADF)), $((SEEKF % CADF)) frame(s) into its group"
echo " of $CADF (FINDINGS 70.3/71) ---"
TOTFR=$(( NFR + NFR - SEEKF ))
SECS=$(( TOTFR / 8 + 35 ))
# BOTH CONFIGURATIONS, and they are not two speeds of one thing. The chip's
# accumulator is an integrator with no leak, so what a branch costs is set by
# the state it lands in: play THROUGH and the chip keeps whatever the previous
# scene left it in; STOP and re-PLAY and it goes to the container's own `init`
# with the step index at 0. Neither is zero and they are 5.5x apart, so the
# run measures both and FINDINGS 71.3 chooses.
for M in 0 1; do
TAG=seek; [ "$M" = 1 ] && TAG=seek_rst
echo " -- the chip $([ "$M" = 1 ] && echo 'STOPPED and re-PLAYED' \
|| echo 'PLAYING THROUGH') the branch"
SOUNDARGS="-samplerate 15625 -wavwrite packed_$TAG.wav"
run $TAG DLX_PK_HELD=0 DLX_PK_PACE=1 DLX_PK_FPS="$CFPS" DLX_PK_NFR=$NFR \
DLX_PK_AUD=1 DLX_PK_AJSON=packed_$TAG.json \
DLX_PK_ITER=2 DLX_PK_SEEK=$SEEKF DLX_PK_ARST=$M
SOUNDARGS="-sound none"
grep -aq "^\[PK\] FLAG=\$FF" "tmp/packed_$TAG.log" || \
fail "the $TAG run did not reach the end of the second pass."
python3 tools/bench/verify_packed_audio.py "$DLXP" "tmp/packed_$TAG.wav" \
"tmp/packed_$TAG.json" --seek "$SEEKF" --iters 2 || \
fail "the player did not play the container ACROSS A BRANCH ($TAG).
Nothing here parses anything (FINDINGS 67.4): a seek that fetched the
wrong lump plays 7,168 B of the wrong part of the scene, and one that
dropped the byte offset plays the right lump from up to $((CADF-1))
frames too early -- neither is an error and both are a sound."
done
fi
if [ "${DLX_PK_GATE_ONLY:-0}" = "1" ]; then exit 0; fi
echo
echo "--- 2/3. THE RATE: free-running, both channel configurations ---"
SECS=$(( NFR / 8 + 25 ))
run free_steal DLX_PK_HELD=0 DLX_PK_PACE=0 DLX_PK_NFR=$NFR
run free_held DLX_PK_HELD=1 DLX_PK_PACE=0 DLX_PK_NFR=$NFR
for t in free_steal free_held; do
grep -aq "FREE-RUNNING, so the mean IS the transport" "tmp/packed_$t.log" || \
fail "the $t run did not report a transport time, so there is no rate here
to read -- and a rate read off a PACED run is the pace."
done
echo
echo "--- 4. THE CLOCK: held, paced at the container's own rate ---"
SECS=$(( NFR / 12 + 25 ))
run held_paced DLX_PK_HELD=1 DLX_PK_PACE=1 DLX_PK_NFR=$NFR
# THE FINDING THIS RUN EXISTS FOR, asserted rather than admired. A held channel
# halts the 68000, and the frame clock is an INTERRUPT off V-DISP whose pending
# bit is ONE BIT -- so every edge that falls inside a transfer spanning two of
# them is an edge the machine can never count. If this ever comes back at zero,
# either the transfer got short enough to fit between two rasters or the held
# configuration stopped halting the CPU, and both change what the run means.
LOST=$(sed -n 's/.*-> \([0-9]*\) edges LOST.*/\1/p' tmp/packed_held_paced.log | head -1)
[ -n "$LOST" ] && [ "$LOST" -gt 0 ] || \
fail "the held run lost no V-DISP edges (${LOST:-none}). Either the bus is no
longer being held for the transfer, or the transfer now fits between two
rasters -- and the comparison with the stealing run below is then a
comparison of two configurations that do the same thing."
LOSTS=$(sed -n 's/.*-> \([0-9]*\) edges LOST.*/\1/p' tmp/packed_gate.log | head -1)
# ---------------------------------------------------------------------------
# 6. THE SAME AUDIO WITH THE BUS HELD, and this is the interaction ROADMAP P6c
# said neither half had met. A burst channel HALTS the 68000 for the whole
# 88 ms record, so the audio service cannot run during it -- src/player/dma.i's
# DM_HOOK is never reached and the chip is looked at twice a frame instead of
# two thousand times. The bytes are the same bytes either way; what changes is
# WHEN the next lump is armed, and the chip has no starvation state: what it
# does in between is replay the byte pair it is holding.
#
# NOT A GATE. Both configurations play the container byte for byte and the
# verifier passes on both; the difference is entirely in the seams, and a seam
# is a design cost rather than a correctness one.
if [ "$AUDIO_ON" = "1" ]; then
echo
echo "--- 6. THE AUDIO AGAIN, WITH THE BUS HELD (the contrast, not a gate) ---"
SECS=$(( NFR / 6 + 30 ))
SOUNDARGS="-samplerate 15625 -wavwrite packed_aud_held.wav"
run aud_held DLX_PK_HELD=1 DLX_PK_PACE=1 DLX_PK_FPS="$CFPS" DLX_PK_NFR=$NFR \
DLX_PK_AUD=1 DLX_PK_AJSON=packed_aud_held.json
SOUNDARGS="-sound none"
python3 tools/bench/verify_packed_audio.py "$DLXP" tmp/packed_aud_held.wav \
tmp/packed_aud_held.json || fail "the held run did not play the
container. The bytes are not what holding the bus was expected to cost."
echo
echo " THE SEAM, STEALING AGAINST HELD -- audio does not merely cost clocks:"
for t in aud aud_held; do
printf ' %-9s ' "$t"
python3 tools/bench/verify_packed_audio.py "$DLXP" "tmp/packed_$t.wav" \
| sed -n 's/^ worst \(.*\)$/\1/p' | head -1
done
echo " Stealing, the 68000 sees the channel from inside dma.i's transfer"
echo " wait. Held, it is halted for the whole record and cannot look at all."
fi
echo
echo " V-DISP edges lost: $LOST held at 12 fps, $LOSTS stealing at $GATE_FPS fps."
echo " A player keeps a clock, reads a stick and feeds ADPCM. Which of the two"
echo " configurations can do any of that is a DESIGN question, and it is the"
echo " one this run answers; neither figure is W."
exit 0