forked from Ports/goldeneye-pc-port
Issue fixes: - D643 (#145 b/c, #149): Original control style is a strict virtual N64 pad (native buttons in stage/menu/watch/tank, per-seat Input.N64Pad remap, Input.N64StickFeel) - D605 + D645 (#145 a): Invert look reaches the original styles; in Original it is the game's own Look up/down, synced with the F10 row - D606 (#144): optional always-visible mission timer - D644 (#146): IA4 decodes as the RDP's I3:A1 (file-select crest drew on a dark square) - D581 (#151): open on the real primary monitor (SDL display 0) and centre every window placement in the display's global bounds; the earlier "display at (0,0)" rule still picked the left monitor - D580 (#153, #156): runtime check of the propDef type/end-of-list fix - D646: F10 dropdown popup stays inside the settings box Frame rate (#148): - D578: Auto follows the display up to 240 (4 presents per sim tick, the renderer's ceiling); 165 and 240 in the Frame rate cap row Rolled in from release/v0.7.0: - D632: release the GL context + window before exit (BSOD 0x119) - D626: Windows per-monitor DPI awareness - D639: exclusive fullscreen uses the desktop mode - verify.sh: a frame-size mismatch fails; demobatch/flicker_scan harness Release gate, licenses, docs: - NOTICE: macOS packaging and pi/ coverage; runtime libraries bundled in the downloads (SDL2/SDL3, zlib, MinGW runtime, macOS GCC runtime) - macOS package ships SDL3, GCC COPYING.RUNTIME and the GPL-3.0 text (tools_pc/dist/GPL-3.0.txt) - README: release date 2026-10-13, "Why play this port", Legal (Apple, runtime licenses), frame-rate text; site date tokens - tools_pc/release_gate.sh: one-command win/deck/mac release checks - release notes, findings D578/D580/D581/D643-D646 + index, porting notes
186 lines
8.2 KiB
Bash
186 lines
8.2 KiB
Bash
#!/usr/bin/env bash
|
|
# demobatch.sh -- batch-replay maintainer-recorded pad demos through
|
|
# tools_pc/levelbench.sh --replay, collect frames / flicker flags / pacing
|
|
# stats, and write one report per batch. Sequential only (parallel runs
|
|
# invalidate frame pacing).
|
|
#
|
|
# demobatch.sh [options] DEMODIR [ENV=VAL ...]
|
|
# --dir GAMEDIR game dir holding data/ (default: cwd)
|
|
# --exe PATH game binary (default: levelbench's autodetect)
|
|
# --out ROOT results root (default: <GAMEDIR>/demobatch/<YYYYMMDD-HHMMSS>)
|
|
# --spot f1,f2 dump just these game frames (GE_PCDUMP lo-hi:step with
|
|
# step = gcd of the spacing; extra frames are deleted)
|
|
# --flicker lo-hi dump every frame in the window, run flicker_scan.py
|
|
# (--spot and --flicker are mutually exclusive: one dump
|
|
# spec per run)
|
|
# --only a,b only these demo names (file name without .demo)
|
|
# --dry-run print the commands, launch nothing
|
|
# demobatch.sh --compare ROOT_A ROOT_B
|
|
# side-by-side report rows with deltas for numeric columns
|
|
#
|
|
# Demo harness (maintainer workflow). Record once, per level:
|
|
# tools_pc/levelbench.sh --dir <game dir> --secs N --record demos/<name>.demo <level>
|
|
# You play the level with the pad; input is saved to the .demo and a
|
|
# <name>.demo.meta (level, secs, seed, binary md5, date) is written beside it.
|
|
# Demos live in <game dir>/demos/ -- personal input recordings, gitignored,
|
|
# never committed. Then `demobatch.sh --dir <game dir> <game dir>/demos`
|
|
# replays each with the recorded level/secs/seed (GE_RSEED pinned) and writes
|
|
# <ROOT>/report.txt. Replays drift slightly (D117): compare runs within one
|
|
# session (--compare), never as pixel gates. Extra ENV=VAL args go to every run.
|
|
set -u
|
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
absdir() { (cd "$1" 2>/dev/null && pwd); }
|
|
|
|
compare() {
|
|
local A=$1/report.txt B=$2/report.txt
|
|
if [ ! -f "$A" ] || [ ! -f "$B" ]; then echo "missing report.txt in $1 or $2" >&2; exit 2; fi
|
|
awk '
|
|
function d(a, b) {
|
|
if (a ~ /^-?[0-9.]+$/ && b ~ /^-?[0-9.]+$/) return sprintf("%+.2f", b - a)
|
|
return "-"
|
|
}
|
|
FNR == NR { if ($1 != "name" && NF >= 9) { a[$1] = $0; order[++n] = $1 } next }
|
|
$1 != "name" && NF >= 9 { b[$1] = $0 }
|
|
END {
|
|
f = "%-16s %-5s | %-5s %-5s | %-4s %-4s %-6s | %-8s %-8s %-7s | %-6s %-6s %-6s | %-5s %-5s %-6s\n"
|
|
printf f, "name", "level", "exitA", "exitB", "wdA", "wdB", "dWd", "pres/sA", "pres/sB", "dPres", "unevA", "unevB", "dUnev", "slowA", "slowB", "dSlow"
|
|
for (i = 1; i <= n; i++) {
|
|
k = order[i]
|
|
split(a[k], x, " ")
|
|
if (!(k in b)) { printf "%-16s (only in A)\n", k; continue }
|
|
split(b[k], y, " ")
|
|
printf f, k, x[2], x[3], y[3], x[5], y[5], d(x[5], y[5]), x[6], y[6], d(x[6], y[6]), x[7], y[7], d(x[7], y[7]), x[8], y[8], d(x[8], y[8])
|
|
if (x[9] != y[9]) printf "%-16s flicker flags: %s -> %s (%s)\n", "", x[9], y[9], d(x[9], y[9])
|
|
}
|
|
for (k in b) if (!(k in a)) printf "%-16s (only in B)\n", k
|
|
}' "$A" "$B"
|
|
}
|
|
|
|
if [ "${1:-}" = --compare ]; then
|
|
[ $# -eq 3 ] || { echo "usage: $0 --compare ROOT_A ROOT_B" >&2; exit 2; }
|
|
compare "$2" "$3"; exit 0
|
|
fi
|
|
|
|
GAME=$PWD; EXE=; OUT=; SPOT=; FLK=; ONLY=; DRY=0
|
|
while [ $# -gt 0 ]; do
|
|
case $1 in
|
|
--dir) GAME=$2; shift 2;;
|
|
--exe) EXE=$2; shift 2;;
|
|
--out) OUT=$2; shift 2;;
|
|
--spot) SPOT=$2; shift 2;;
|
|
--flicker) FLK=$2; shift 2;;
|
|
--only) ONLY=$2; shift 2;;
|
|
--dry-run) DRY=1; shift;;
|
|
-h|--help) sed -n 2,26p "$0"; exit 0;;
|
|
*) break;;
|
|
esac
|
|
done
|
|
[ $# -ge 1 ] || { echo "usage: $0 [options] DEMODIR [ENV=VAL ...]" >&2; exit 2; }
|
|
DEMODIR=$1; shift
|
|
if [ -n "$SPOT" ] && [ -n "$FLK" ]; then echo "--spot and --flicker are mutually exclusive" >&2; exit 2; fi
|
|
[ -d "$DEMODIR" ] || { echo "no demo dir: $DEMODIR" >&2; exit 2; }
|
|
GAME=$(absdir "$GAME") || { echo "bad --dir" >&2; exit 2; }
|
|
DEMODIR=$(absdir "$DEMODIR")
|
|
[ -n "$OUT" ] || OUT="$GAME/demobatch/$(date +%Y%m%d-%H%M%S)"
|
|
case $OUT in /*|[A-Za-z]:*) ;; *) OUT=$PWD/$OUT;; esac
|
|
|
|
# Dump spec: spot frames -> lo-hi:step (step = gcd of gaps); flicker -> lo-hi.
|
|
DUMP=; SPOTLIST=
|
|
if [ -n "$SPOT" ]; then
|
|
SPOTLIST=$(echo "$SPOT" | tr ',' '\n' | sort -n -u | tr '\n' ' ')
|
|
read -r -a FR <<< "$SPOTLIST"
|
|
lo=${FR[0]}; hi=${FR[$((${#FR[@]} - 1))]}; g=0
|
|
for ((i = 1; i < ${#FR[@]}; i++)); do
|
|
a=$((FR[i] - FR[i-1])); b=$g
|
|
while [ "$b" -ne 0 ]; do t=$((a % b)); a=$b; b=$t; done
|
|
g=$a
|
|
done
|
|
DUMP="$lo-$hi"
|
|
[ "$g" -gt 1 ] && DUMP="$lo-$hi:$g"
|
|
elif [ -n "$FLK" ]; then
|
|
DUMP=$FLK
|
|
fi
|
|
|
|
LB=("$HERE/levelbench.sh" --dir "$GAME")
|
|
[ -n "$EXE" ] && LB+=(--exe "$EXE")
|
|
PY=
|
|
for c in python3 python py; do
|
|
if command -v "$c" > /dev/null 2>&1 && "$c" -c 'import sys' > /dev/null 2>&1; then PY=$c; break; fi
|
|
done
|
|
[ -n "$PY" ] || PY=python3
|
|
[ "$DRY" = 1 ] || mkdir -p "$OUT"
|
|
|
|
metaget() { sed -n "s/^$2=//p" "$1" | head -1 | tr -d '\r'; }
|
|
|
|
NAMES=()
|
|
for demo in "$DEMODIR"/*.demo; do
|
|
[ -f "$demo" ] || continue
|
|
n=$(basename "$demo" .demo)
|
|
if [ -n "$ONLY" ]; then case ",$ONLY," in *",$n,"*) ;; *) continue;; esac; fi
|
|
if [ ! -f "$demo.meta" ]; then echo "skip $n: no $n.demo.meta" >&2; continue; fi
|
|
NAMES+=("$n")
|
|
lvl=$(metaget "$demo.meta" level); secs=$(metaget "$demo.meta" secs); seed=$(metaget "$demo.meta" seed)
|
|
CMD=("${LB[@]}" --secs "$secs" --replay "$demo" --out "$OUT/$n")
|
|
[ -n "$DUMP" ] && CMD+=(--dump "$DUMP")
|
|
CMD+=("$lvl" "GE_RSEED=${seed:-0123456789abcdef}" "$@")
|
|
if [ "$DRY" = 1 ]; then
|
|
echo "[$n] ${CMD[*]}"
|
|
[ -n "$SPOTLIST" ] && echo "[$n] then keep only frames: $SPOTLIST"
|
|
[ -n "$FLK" ] && echo "[$n] then: $PY -I $HERE/flicker_scan.py $OUT/$n/ppm --csv $OUT/$n/flicker.csv"
|
|
continue
|
|
fi
|
|
echo "=== $n (level $lvl, ${secs}s)"
|
|
"${CMD[@]}" > "$OUT/$n.console" 2>&1
|
|
tail -3 "$OUT/$n.console"
|
|
if [ -n "$SPOTLIST" ] && [ -d "$OUT/$n/ppm" ]; then
|
|
for f in "$OUT/$n"/ppm/frame_*.ppm; do
|
|
[ -e "$f" ] || continue
|
|
num=$((10#$(basename "$f" .ppm | sed 's/frame_//')))
|
|
case " $SPOTLIST " in *" $num "*) ;; *) rm -f "$f";; esac
|
|
done
|
|
fi
|
|
if [ -n "$FLK" ] && [ -d "$OUT/$n/ppm" ]; then
|
|
"$PY" -I "$HERE/flicker_scan.py" "$OUT/$n/ppm" --csv "$OUT/$n/flicker.csv" > "$OUT/$n/flicker.txt" 2>&1
|
|
tail -1 "$OUT/$n/flicker.txt"
|
|
fi
|
|
done
|
|
if [ "$DRY" = 1 ]; then echo "(dry run: no report written; root would be $OUT)"; exit 0; fi
|
|
|
|
# ---- report ----------------------------------------------------------------
|
|
# presents/s: mean over "D578 interp: X presents/s, ..." lines.
|
|
# uneven%: from "D578 cadence: gaps <7:a 7-9.5:b 9.5-12:c 12-20:d >20:e | ...",
|
|
# bucket counts summed over all lines; uneven = share outside the modal bucket.
|
|
R="$OUT/report.txt"
|
|
fmt="%-16s %-5s %-5s %-5s %-9s %-10s %-8s %-5s %-7s\n"
|
|
{
|
|
printf "$fmt" name level exit crash watchdog presents/s uneven% slow flicker
|
|
for n in "${NAMES[@]}"; do
|
|
d="$OUT/$n"; L="$d/ge007.log"; [ -f "$L" ] || L="$d/stdout.txt"
|
|
lvl=$(metaget "$DEMODIR/$n.demo.meta" level)
|
|
ex=n; crash=n; wd=0; slow=0; stats="- -"
|
|
if [ -f "$L" ]; then
|
|
grep -q -E 'quit requested|video: exiting' "$L" && ex=y
|
|
wd=$(grep -c 'no frame rendered' "$L")
|
|
slow=$(grep -c 'D583 SLOW' "$L")
|
|
stats=$(awk '
|
|
/D578 interp:/ { for (i = 2; i <= NF; i++) if ($i ~ /^presents\/s/) { p += $(i-1); pn++ } }
|
|
/D578 cadence:/ {
|
|
for (i = 1; i <= NF; i++) if ($i == "gaps") {
|
|
for (j = 1; j <= 5; j++) { s = $(i + j); sub(/^.*:/, "", s); g[j] += s + 0 }
|
|
break
|
|
}
|
|
}
|
|
END {
|
|
tot = 0; mx = 0
|
|
for (j = 1; j <= 5; j++) { tot += g[j]; if (g[j] > mx) mx = g[j] }
|
|
printf "%s %s", (pn ? sprintf("%.1f", p / pn) : "-"), (tot ? sprintf("%.1f", 100 * (tot - mx) / tot) : "-")
|
|
}' "$L")
|
|
fi
|
|
[ -f "$d/ge007.crash.log" ] && crash=y
|
|
fl=-; [ -f "$d/flicker.txt" ] && fl=$(grep -c '^FLICKER' "$d/flicker.txt")
|
|
printf "$fmt" "$n" "${lvl:--}" "$ex" "$crash" "$wd" "${stats% *}" "${stats#* }" "$slow" "$fl"
|
|
done
|
|
} > "$R"
|
|
cat "$R"
|
|
echo "report: $R"
|