Files
virtualjaguar-libretro/scripts/ocgrid.sh
Joseph MattielloandClaude Opus 5 2b180cb6d4 fix(ocgrid): stop on INT/TERM, refuse a saturated stock cell as baseline
Two review findings, both in the measurement guards the script exists to
enforce.

The trap cleaned up but never stopped. On Ctrl-C the handler ran rm and the
sweep carried on: the signal killed only the current cells frame_hash_ab, that
cell was recorded FAILED, and six more multi-minute cells followed. TERM from
timeout was ignored the same way. Split into an EXIT trap that cleans up and an
INT/TERM trap that exits 130.

A saturated stock cell still became the baseline. The doc is emphatic that
SATURATED means unmeasured and arms must not be compared -- but any
non-saturated cell would still print an authoritative-looking percentage
against a ceiling-pinned number. Only a measured stock cell can set the
baseline now; otherwise every row reports NO USABLE STOCK BASELINE.

Super Burnout hit exactly this case in the same run that prompted the fix:
1799/1800 in all seven cells, stock included.

Also records gate 1 complete across all eight candidates. One passes.

  Cybermorph          -48.7% / +44.7% / +72.8%   PASSES
  I-War               -18.2% / +6.3%  / +1.1%    noise, capped at stock
  Missile Command 3D   -0.2% / -14.3% / -11.2%   overclock actively harmful
  Doom, Club Drive, Iron Soldier                 flat locks
  Super Burnout                                  saturated at stock
  Val D Isere                                    still needs a gameplay save

The structural-null hypothesis holds as the general rule but not as a law:
Cybermorph disproves the strong form. Doom reads 593/1800 = 19.8 fps in
gameplay against real hardwares ~20 renders/s, which is a lead for #401 --
the 2x-too-fast figure came from the attract demo, a different code path.

Refs #378

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 23:38:37 -04:00

107 lines
4.4 KiB
Bash
Executable File

#!/bin/bash
# Overclock gate-1 grid (#378): does a clock scale actually lift the INTERNAL
# frame rate of this title?
#
# FRAMES=900 scripts/ocgrid.sh out.txt "<rom>" "<state>"
#
# Results are written to <out.txt> and echoed to stdout. Pass /dev/stdout (or
# /dev/null) if you only want one of those.
#
# Measures framebuffer TRANSITIONS -- frames whose hash differs from the
# previous frame -- starting from a gameplay savestate. Two guards, both
# earned the hard way (see docs/overclock-gate1-checklist.md):
#
# * transitions == window length means the counter SATURATED. That is
# UNMEASURED, not a null: every frame differed, so a gain has nowhere to
# show. Reported as SATURATED, never as a number.
# * the 0.5x underclock arm runs FIRST and always. A flat 1x/1.5x/2x row
# cannot distinguish a genuine frame cap from a dead measurement; if 0.5x
# does not drop, the metric was never responding and the whole sweep is
# void. This is the control that both retracted result tables lacked.
#
# CORE may be overridden for a non-macOS library name, e.g.
# CORE=./virtualjaguar_libretro.so scripts/ocgrid.sh ...
set -u
CORE=${CORE:-./virtualjaguar_libretro.dylib}
OUT=$1; shift
ROM=$1; shift
STATE=$1; shift
FRAMES=${FRAMES:-900}
TMP=$(mktemp -d)
# EXIT does the cleanup; INT/TERM must additionally *stop*. Without the
# second trap a Ctrl-C killed only the current cell's frame_hash_ab (the
# process group gets the signal), that cell was recorded FAILED, and the
# sweep carried on through six more multi-minute cells.
trap 'command rm -rf "$TMP"' EXIT
trap 'exit 130' INT TERM
emit() { printf '%s\n' "$*" | tee -a "$OUT"; }
: > "$OUT"
emit "rom: $ROM"
emit "state: $STATE"
emit "frames: $FRAMES (saturation ceiling $((FRAMES - 1)))"
emit ""
emit "$(printf '%-22s %-12s %s' cell transitions verdict)"
# --- phase 1: measure every cell -------------------------------------------
# Two phases so the stock cell is known before anything is quoted against it:
# the 0.5x control is more useful printed as a percentage than as a raw count,
# and it has to run regardless of ordering.
cells="risc=0.5x,m68k=1x risc=1x,m68k=1x risc=1.5x,m68k=1x risc=2x,m68k=1x \
risc=1x,m68k=1.5x risc=1.5x,m68k=1.5x risc=2x,m68k=1.5x"
base=""
for cell in $cells; do
r=${cell#risc=}; r=${r%%,*}
m=${cell##*m68k=}
tag=$(printf '%s' "$cell" | tr ',=.' '___')
csv=$TMP/$tag.csv
if ! "./test/tools/frame_hash_ab" "$CORE" "$ROM" --csv "$csv" \
--load-state "$STATE" --frames "$FRAMES" \
--option "virtualjaguar_risc_clock_scale=$r" \
--option "virtualjaguar_m68k_clock_scale=$m" >"$TMP/$tag.log" 2>&1; then
printf 'FAILED\n' > "$TMP/$tag.res"
continue
fi
t=$(awk -F, 'NR>1{if(p!=""&&$6!=p)n++;p=$6}END{print n+0}' "$csv")
n=$(awk 'NR>1' "$csv" | wc -l | tr -d ' ')
printf '%s %s\n' "$t" "$n" > "$TMP/$tag.res"
# Only a MEASURED stock cell may become the baseline. A saturated one
# is pinned to the ceiling, so quoting other arms against it would
# print authoritative-looking percentages against a meaningless
# number -- the exact trap this script exists to make un-missable.
if [ "$cell" = "risc=1x,m68k=1x" ] && [ "$n" -gt 0 ] && [ "$t" -lt "$((n - 1))" ]; then
base=$t
fi
done
# --- phase 2: report --------------------------------------------------------
for cell in $cells; do
tag=$(printf '%s' "$cell" | tr ',=.' '___')
read -r t n < "$TMP/$tag.res" 2>/dev/null || { t=FAILED; n=""; }
if [ "$t" = "FAILED" ]; then
emit "$(printf '%-22s %-12s %s' "$cell" "-" "RUN FAILED")"
tail -3 "$TMP/$tag.log" 2>/dev/null | sed 's/^/ /' | tee -a "$OUT"
continue
fi
if [ "$n" -eq 0 ]; then
v="NO FRAMES (state rejected? see checklist section 2)"
elif [ "$t" -ge "$((n - 1))" ]; then
v="SATURATED -- unmeasured, lengthen the window"
elif [ -z "$base" ] || [ "$base" -eq 0 ]; then
v="NO USABLE STOCK BASELINE -- stock cell saturated or static; nothing here is comparable"
else
v="$(awk -v a="$t" -v b="$base" 'BEGIN{printf "%+.1f%% vs stock", (a-b)*100.0/b}')"
[ "$cell" = "risc=0.5x,m68k=1x" ] && v="$v <- CONTROL: must drop, or the sweep is void"
fi
emit "$(printf '%-22s %-12s %s' "$cell" "$t/$n" "$v")"
done
emit ""
emit "Reminder: a pass here is gate 1 only. Gate 2 is a human play-test --"
emit "a title can render more frames while its logic runs too fast (#401)."