mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
* ci: route windows-build to self-hosted runner (G2); land ROM-gated sweep workflow (G1)
G2: windows-build now runs on the self-hosted box (corpyvt6agent01) for
push/workflow_dispatch, using local ccache instead of the actions/cache
round-trip and skipping msys2/setup-msys2's install once packages are
already present. pull_request always stays on windows-latest — fork PRs
must never reach a box that will hold a ROM.
G1: adds .github/workflows/selfhosted.yml, a dispatch-only workflow that
builds + runs the 21-level headless sweep (tools_pc/verify.sh sweep --json)
against a runner-cached ROM. Inert until manually dispatched.
Still pending (manual, physical steps on the box — not done by this
commit, see docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md rollout table):
- register corpyvt6agent01 as a runner against this repo
- confirm MSYS2 at C:\msys64 with the required mingw64 package set
- one-time ROM seed into {runner_home}/_rom-seed/
- first workflow_dispatch sweep + parity check against a local run
- flip G1 from workflow_dispatch to push:[main] once green
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): remove invalid concurrency.max-parallel key
Found by testing: dispatching the workflow failed with a hard parse
error (HTTP 422, "Unexpected value 'max-parallel'"). GitHub Actions'
workflow-level `concurrency` block only supports `group` and
`cancel-in-progress` — `max-parallel` is a job `strategy:` key for
matrix jobs, not a concurrency-block key. Serialization was already
achieved via group + cancel-in-progress:false; the stray key just
blocked every dispatch.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): register the msys2 shell wrapper (setup-msys2 step)
Found by testing: the first fixed dispatch failed every msys2-shell step
with "msys2: command not found". Unlike ci.yml's windows-build, this
workflow used `shell: msys2 {0}` without ever running msys2/setup-msys2,
which is what registers that shell alias for the runner — MSYS2 being
pre-installed on the box doesn't help if Actions doesn't know the shell
exists. Adds the same setup-msys2 step ci.yml uses, with update:false
since the packages are expected already present (fast no-op per plan).
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* ci: stop reinstalling MSYS2 per run on the self-hosted box
The self-hosted paths (selfhosted.yml; ci.yml windows-build's push/
dispatch route) no longer call msys2/setup-msys2 or pacman -S at all.
MSYS2 + the mingw64 package set are now assumed hand-maintained on
corpyvt6agent01 (docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md), invoked by
calling its bash directly (`C:\msys64\usr\bin\bash.exe -leo pipefail
"{0}"`, the same invocation setup-msys2's generated wrapper uses) with
MSYSTEM=MINGW64 so /etc/profile still puts mingw64/bin on PATH.
Adds tools_pc/check-selfhosted-deps.sh: a fast, install-free check that
exits 0 immediately when the required packages (toolchain, cmake,
SDL2, zlib, python, ccache, zip, unzip) are present, or fails with the
exact `pacman -S --needed ...` line to run otherwise — so a missing
package on the box surfaces immediately with a clear fix, instead of
as a confusing failure deeper in the build.
ci.yml's windows-latest (PR) path is unchanged: ephemeral runners still
get a full msys2/setup-msys2 install every time, since there's nothing
to persist there.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix: use msys2/setup-msys2 release:false instead of a raw bash-path shell
The previous attempt (invoking C:\msys64\usr\bin\bash.exe directly as a
custom `shell:` command) failed on the actual runner with "Second path
fragment must not be a drive or UNC name" — a real limitation, not a
typo; it failed identically in both files, including the one with a
static (non-templated) shell string, so it wasn't the ${{ }} expression
either.
Root-caused by reading msys2/setup-msys2's actual source: `release:
false` makes it reuse the existing C:\msys64 as-is (hardcoded path in
that branch, confirmed in main.js) instead of downloading+installing a
fresh copy, and — verified in the same source — omitting `update` and
`install` skips every pacman call entirely (just one `uname -a` + a
pacman.conf sed tweak, no network). This is the officially documented
mechanism for self-hosted runners with a pre-existing MSYS2 install
(see the action's README, "release" section).
ci.yml's windows-build now runs setup-msys2 unconditionally but with
release/update/install all keyed off github.event_name: full install on
windows-latest (PR, ephemeral), reuse-only on the self-hosted box
(push/dispatch). selfhosted.yml always uses release:false. Both then
run tools_pc/check-selfhosted-deps.sh to verify the box's hand-
maintained packages are actually present.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): cp -r for the ROM sidecar directories
Found by testing with the ROM seed actually in place: pcmodels-ntsc-final
and pccg-ntsc-final are directories (not single files, unlike
ge007.ntsc-final.z64), so a plain `cp` failed with "omitting directory"
and left the seed step failing on every run even with the seed populated.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* ci: drive MSYS2 MINGW64 directly via ci-shell.cmd, drop msys2 shell shim
The self-hosted runs were dying with 'msys2: command not found' because the
msys2/setup-msys2 action's shell shim was never registered on the box. Replace
the dependency on that shim with a small committed launcher (tools_pc/ci-shell.cmd)
that headlessly reproduces the local 'MSYS2 MINGW64' terminal:
- MSYSTEM=MINGW64 + /mingw64/bin on PATH
- NON-login bash, so it keeps the workspace cwd (a login shell cd's to $HOME
and would break every relative path in the workflow)
- -e -o pipefail for GitHub-style error propagation
Both workflows now set defaults.run.shell to C:\msys64\ci-shell.cmd {0}; a
bootstrap step (shell: cmd) refreshes the launcher from the repo into C:\msys64
each run. selfhosted.yml drops setup-msys2 entirely; ci.yml keeps it only for
the ephemeral windows-latest PR path (which installs MSYS2 at C:\msys64).
.gitattributes: force CRLF for *.cmd so cmd.exe parses the launcher correctly.
* Revert "ci: drive MSYS2 MINGW64 directly via ci-shell.cmd, drop msys2 shell shim"
This reverts commit a976b2589c.
* docs(D244): log self-hosted-runner link failure — undefined reference to objectiveGetStatus_WEAK
First real build attempt on the reinstalled corpyvt6agent01 box (run
34707798707) fails at link with an unresolved #pragma weak alias. A local
repro of the identical alias pattern links fine on current MSYS2
(gcc 16.2.0/binutils 2.47); the box reports gcc 16.1.0 — likely a stale
toolchain, consistent with rollout step 2's earlier package-hygiene gap.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(verify.sh): surface run.log tail on NO-FRAMES instead of silently discarding it
sweep mode's per-level run.log (the game process's own stdout/stderr, where
an SDL/GL init failure would show up) was captured into a mktemp capdir but
unconditionally rm -rf'd with no diagnostic ever printed for the NO-FRAMES
(no-crash) case -- every level failed this way on the self-hosted runner's
first real sweep attempt with zero clue why.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(selfhosted.yml): stop deleting the ROM seed dir -- cache save silently fails on this box
actions/cache/save has been failing to persist the ROM cache entry on
corpyvt6agent01 (WARNING annotation, tar.exe exit code 2) but doesn't fail
the step -- by design, a cache-save failure is non-fatal, so the workflow
carried on and the now-unconditional "Delete one-time seed dir" step wiped
the only copy of the ROM every single run. Every dispatch since has failed
at "Seed ROM cache" with an empty seed dir and an empty cache -- exactly
the user-reported "it wipes out the ROM data each run".
Fix: don't delete the seed dir. It's cheap disk (three files, a few hundred
MB) and the existing restore-then-seed-if-missing logic already treats it as
the fallback source whenever the cache comes back empty, so leaving it in
place is safe and makes the workflow tolerant of the cache-save bug until
that's root-caused separately.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(verify.sh): sweep --json never actually produced a verdict array; fix NO-FRAMES diagnostic clipping it further
Two bugs, found together while chasing the self-hosted runner's first real
sweep attempt:
1. sweep mode's `out=$(verify_level ...); echo "$out" | head -1` always
discarded emit_verdict's second (--json) output line -- RESULTS_JSON was
declared for assembling a verdict array but never populated or printed,
so `sweep --json` has never produced anything but `[]` for the
workflow's "Extract JSON verdict" step. Now builds and prints a real
`[{...}, {...}]` array before the RC exit.
2. My own D244-session addition (printing a NO-FRAMES level's run.log tail)
wrote to stdout, which the same `out=$(...)/head -1` capture turned into
the *new* first line, clipping the real verdict summary. Moved to stderr,
which bypasses that capture.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* docs: regenerate findings-index.csv for D244
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
387 lines
15 KiB
Bash
387 lines
15 KiB
Bash
#!/bin/bash
|
|
# verify.sh — one command -> one machine-readable verdict (speed-ups plan Step 2).
|
|
#
|
|
# Wraps the manual recipe every session was re-deriving by hand (build,
|
|
# 640x480 ini pin, GE_PCDUMP capture, crash-symbolication, pixcount,
|
|
# framediff): see docs/dev/notes/AGENTIC-SPEEDUPS-PLAN.md Step 2 / N6.
|
|
#
|
|
# Usage:
|
|
# verify.sh <level> single level: name (dam, bunker1, "surface 1")
|
|
# or -level_XX number (09, 33, ...)
|
|
# verify.sh sweep [subset] all 21 solo levels, or SWEEP_LEVELS-style
|
|
# "name:XX name:XX ..." subset
|
|
# verify.sh parity <level> --against <dir>
|
|
# framediff this platform's capture against a
|
|
# capture directory from the other platform
|
|
# (there is no cross-OS launch here — feed it
|
|
# a directory of PPMs/PNGs pulled off that box)
|
|
#
|
|
# Options:
|
|
# --platform win|linux default: autodetect via `uname -s`
|
|
# --dump lo-hi:step GE_PCDUMP window (default: 200-440:120 single level,
|
|
# 80-400:40 sweep — matches the existing golden set
|
|
# and level_sweep.sh respectively)
|
|
# --script "..." GE_INPUTSCRIPT passthrough (scripted input)
|
|
# --json also emit one JSON object (or array, for sweep) to
|
|
# stdout: {level, platform, status, frames,
|
|
# worst_cell, crash_sym}
|
|
# --against DIR parity mode only: the other platform's capture dir
|
|
#
|
|
# Status values: PASS | CRASH | NO-FRAMES | STALLED | REGRESSION
|
|
# (STALLED is this tool's extension of the plan's 4-value enum — see
|
|
# level_sweep.sh's D117/D134 note: a level that renders a few frames then
|
|
# stops moving is a load-sensitivity flake, not a real pass or fail.)
|
|
#
|
|
# Known limitation (N6 / M-48): on --platform linux, GE_PCDUMP reads back
|
|
# black frames on llvmpipe/WSLg (a glReadPixels tooling bug, not a render
|
|
# bug — the live window renders fine). pixcount/framediff are therefore
|
|
# skipped on linux and the verdict is crash-detection only, flagged NOTE.
|
|
set -u
|
|
cd "$(dirname "$0")/.."
|
|
ROOT="$(pwd)"
|
|
|
|
# --- arg parsing -------------------------------------------------------------
|
|
MODE=""; LEVEL_ARG=""; PLATFORM=""; DUMP=""; SCRIPT=""; JSON=0; AGAINST=""
|
|
POSARGS=()
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--platform) PLATFORM="$2"; shift 2 ;;
|
|
--dump) DUMP="$2"; shift 2 ;;
|
|
--script) SCRIPT="$2"; shift 2 ;;
|
|
--against) AGAINST="$2"; shift 2 ;;
|
|
--json) JSON=1; shift ;;
|
|
-h|--help) sed -n '2,32p' "$0"; exit 0 ;;
|
|
*) POSARGS+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
set -- "${POSARGS[@]:-}"
|
|
|
|
case "${1:-}" in
|
|
sweep) MODE=sweep; shift; SWEEP_SUBSET="${*:-}" ;;
|
|
parity) MODE=parity; LEVEL_ARG="${2:-}"; shift 2 2>/dev/null || true ;;
|
|
""|-h) sed -n '2,32p' "$0"; exit 0 ;;
|
|
*) MODE=single; LEVEL_ARG="$1" ;;
|
|
esac
|
|
|
|
if [ -z "$PLATFORM" ]; then
|
|
case "$(uname -s)" in
|
|
Linux*) PLATFORM=linux ;;
|
|
*) PLATFORM=win ;;
|
|
esac
|
|
fi
|
|
|
|
if [ "$PLATFORM" = win ]; then
|
|
BUILD_DIR=build-pc
|
|
EXE="$ROOT/build-pc/ge007.x86_64.exe"
|
|
export PATH="/c/msys64/mingw64/bin:$PATH" # addr2line + runtime DLLs
|
|
KILL() { taskkill //F //IM ge007.x86_64.exe >/dev/null 2>&1; }
|
|
else
|
|
BUILD_DIR=build-linux
|
|
EXE="$ROOT/build-linux/ge007.x86_64"
|
|
KILL() { pkill -f ge007.x86_64 >/dev/null 2>&1; }
|
|
fi
|
|
|
|
# name -> -level_XX (mission order; matches playtest.sh / level_sweep.sh)
|
|
LEVELS=(
|
|
"dam:33" "facility:34" "runway:35" "surface1:36" "bunker1:09" "silo:20"
|
|
"frigate:26" "surface2:43" "bunker2:27" "statue:22" "archives:24"
|
|
"streets:29" "depot:30" "train:25" "jungle:37" "control:23" "caverns:39"
|
|
"cradle:41" "aztec:28" "egypt:32" "cuba:54"
|
|
)
|
|
norm() { echo "$1" | tr 'A-Z' 'a-z' | tr -d ' _-'; }
|
|
resolve_level() {
|
|
local arg="$1"
|
|
if [[ "$arg" =~ ^[0-9]+$ ]]; then echo "num:$(printf '%02d' "$arg")"; return; fi
|
|
local key; key=$(norm "$arg")
|
|
for e in "${LEVELS[@]}"; do
|
|
[ "$(norm "${e%%:*}")" = "$key" ] && { echo "${e%%:*}:${e##*:}"; return; }
|
|
done
|
|
echo ""
|
|
}
|
|
name_for_num() {
|
|
local num="$1"
|
|
for e in "${LEVELS[@]}"; do [ "${e##*:}" = "$num" ] && { echo "${e%%:*}"; return; }; done
|
|
echo "level_$num"
|
|
}
|
|
|
|
# --- build-if-stale ------------------------------------------------------
|
|
if [ ! -d "$BUILD_DIR" ]; then
|
|
echo "error: $BUILD_DIR missing — configure it first (./build-pc.sh for win;" >&2
|
|
echo " cmake -S . -B build-linux -G Ninja -DROMID=ntsc-final for linux)" >&2
|
|
exit 2
|
|
fi
|
|
BUILD_LOG=$(mktemp)
|
|
if ! cmake --build "$BUILD_DIR" -j 8 >"$BUILD_LOG" 2>&1; then
|
|
echo "BUILD FAILED"
|
|
cat "$BUILD_LOG" >&2
|
|
rm -f "$BUILD_LOG"
|
|
exit 2
|
|
fi
|
|
rm -f "$BUILD_LOG"
|
|
if [ ! -x "$EXE" ] && [ ! -f "$EXE" ]; then
|
|
echo "error: build produced no $EXE" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# --- golden lookup: tools_pc/golden/<level>/<platform>/, else the flat
|
|
# level_09 set (today's only golden — Step 3 fills the rest in) ----------
|
|
golden_dir_for() {
|
|
local name="$1"
|
|
local per="$ROOT/tools_pc/golden/$name/$PLATFORM"
|
|
if [ -d "$per" ] && ls "$per"/*.png >/dev/null 2>&1; then echo "$per"; return; fi
|
|
if [ "$name" = "bunker1" ] && ls "$ROOT/tools_pc/golden"/*.png >/dev/null 2>&1; then
|
|
echo "$ROOT/tools_pc/golden"; return
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
# --- ini pin: golden frames are captured at 640x480; back up + restore -----
|
|
INI="$ROOT/data/ge007.ini"
|
|
INI_BAK=""
|
|
pin_ini_640x480() {
|
|
[ -f "$INI" ] || return
|
|
INI_BAK=$(mktemp)
|
|
cp "$INI" "$INI_BAK"
|
|
python3 - "$INI" <<'EOF'
|
|
import re, sys
|
|
p = sys.argv[1]
|
|
s = open(p, encoding="utf-8").read()
|
|
s = re.sub(r'(\[Window\]\s*\nWidth = )\d+', r'\g<1>640', s, count=1)
|
|
s = re.sub(r'(\[Window\]\s*\nWidth = 640\s*\nHeight = )\d+', r'\g<1>480', s, count=1)
|
|
open(p, "w", encoding="utf-8").write(s)
|
|
EOF
|
|
}
|
|
restore_ini() { [ -n "$INI_BAK" ] && cp "$INI_BAK" "$INI" && rm -f "$INI_BAK"; }
|
|
trap 'restore_ini; KILL' EXIT INT TERM
|
|
|
|
# --- run one level -> sets globals: R_STATUS R_FRAMES R_SYM R_BRIEF -------
|
|
run_one() {
|
|
local name="$1" num="$2" dump="$3" watchdog="${4:-90}"
|
|
local capdir; capdir=$(mktemp -d)
|
|
R_STATUS=""; R_FRAMES=0; R_SYM=""; R_BRIEF=""
|
|
|
|
rm -f "$ROOT/ge007.crash.log"
|
|
KILL; sleep 0.3
|
|
|
|
# Speed-up: a level only needs to render its intro pan + spawn-in once --
|
|
# everything after the last requested GE_PCDUMP frame is wasted wall-clock
|
|
# (the process used to always idle out the full watchdog, ~30s/level of
|
|
# nothing on top of the ~15s that actually matters -- 21 levels x that
|
|
# waste is most of a sweep's runtime). Poll for the last frame's PPM (or a
|
|
# crash) and kill the instant it lands; `watchdog` remains the safety net
|
|
# for a level that never gets there (hang / load-sensitivity flake).
|
|
local lo hi step lastframe target=""
|
|
IFS='-:' read -r lo hi step <<< "$dump"
|
|
if [[ "$lo" =~ ^-?[0-9]+$ && "$hi" =~ ^-?[0-9]+$ ]]; then
|
|
[[ "$step" =~ ^[0-9]+$ && "$step" -gt 0 ]] || step=1
|
|
lastframe=$(( lo + step * ( (hi - lo) / step ) ))
|
|
target=$(printf "%s/ppm/frame_%06d.ppm" "$ROOT" "$lastframe")
|
|
fi
|
|
|
|
( cd "$ROOT" && export GE_PCDUMP="$dump"
|
|
[ -n "$SCRIPT" ] && export GE_INPUTSCRIPT="$SCRIPT"
|
|
if [ -n "$target" ]; then "$EXE" "-level_$num"; else timeout "$watchdog" "$EXE" "-level_$num"; fi ) \
|
|
>"$capdir/run.log" 2>&1 &
|
|
local gamepid=$!
|
|
|
|
if [ -n "$target" ]; then
|
|
local ticks=0 max_ticks=$(( watchdog * 5 )) # 0.2s ticks
|
|
while [ "$ticks" -lt "$max_ticks" ]; do
|
|
if [ -f "$target" ] || [ -f "$ROOT/ge007.crash.log" ]; then
|
|
sleep 0.2 # let the just-written frame's fclose() settle
|
|
break
|
|
fi
|
|
kill -0 "$gamepid" 2>/dev/null || break # exited/crashed on its own
|
|
sleep 0.2
|
|
ticks=$((ticks + 1))
|
|
done
|
|
KILL
|
|
fi
|
|
wait "$gamepid" 2>/dev/null
|
|
mv "$ROOT"/ppm "$capdir/ppm" 2>/dev/null || mkdir -p "$capdir/ppm"
|
|
|
|
R_FRAMES=$(ls "$capdir/ppm"/*.ppm 2>/dev/null | wc -l)
|
|
|
|
if [ -f "$ROOT/ge007.crash.log" ]; then
|
|
R_STATUS=CRASH
|
|
cp "$ROOT/ge007.crash.log" "$capdir/crash.log"
|
|
# Step 9 (R5): crash_brief.py owns address extraction + symbolication
|
|
# (both platforms -- the old inline `0x1[0-9a-fA-F]{8,}` grep here
|
|
# assumed the Windows 0x140000000 image base and would never match a
|
|
# Linux no-PIE 0x20000000-based address) and prints a dispatch-ready
|
|
# brief with known-parked-signature and porting-notes/findings hits.
|
|
R_BRIEF=$(python "$ROOT/tools_pc/crash_brief.py" \
|
|
--crash-log "$capdir/crash.log" --exe "$EXE" --platform "$PLATFORM" \
|
|
--level "$name" --json 2>&1)
|
|
R_SYM=$(echo "$R_BRIEF" | python -c "
|
|
import json,sys
|
|
s=sys.stdin.read(); i=s.find('{')
|
|
try:
|
|
d,_=json.JSONDecoder().raw_decode(s[i:]) if i>=0 else (None,0)
|
|
f=d['frames'][0]
|
|
print('%s %s' % (f.get('func') or '??', f.get('loc') or ''))
|
|
except Exception:
|
|
print('')
|
|
" 2>/dev/null)
|
|
elif [ "$R_FRAMES" -eq 0 ]; then
|
|
R_STATUS=NO-FRAMES
|
|
else
|
|
R_STATUS=PASS
|
|
fi
|
|
CAPDIR="$capdir"
|
|
}
|
|
|
|
# --- verdict emission --------------------------------------------------------
|
|
emit_verdict() {
|
|
local name="$1" status="$2" frames="$3" worst="$4" sym="$5" note="${6:-}"
|
|
echo "$name: $status frames=$frames${worst:+ worst_cell=$worst}${sym:+ crash_sym=\"$sym\"}${note:+ ($note)}"
|
|
if [ "$JSON" = 1 ]; then
|
|
python3 - "$name" "$PLATFORM" "$status" "$frames" "$worst" "$sym" "$note" <<'EOF'
|
|
import json, sys
|
|
name, plat, status, frames, worst, sym, note = sys.argv[1:8]
|
|
obj = {"level": name, "platform": plat, "status": status, "frames": int(frames)}
|
|
if worst: obj["worst_cell"] = float(worst)
|
|
if sym: obj["crash_sym"] = sym
|
|
if note: obj["note"] = note
|
|
print(json.dumps(obj))
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
verify_level() {
|
|
local name="$1" num="$2" dump="$3" watchdog="${4:-90}"
|
|
run_one "$name" "$num" "$dump" "$watchdog"
|
|
local status="$R_STATUS" frames="$R_FRAMES" sym="$R_SYM" worst="" note=""
|
|
|
|
if [ "$status" = PASS ]; then
|
|
if [ "$PLATFORM" = linux ]; then
|
|
note="GE_PCDUMP reads black on llvmpipe/WSLg — pixcount/framediff skipped, crash-detect only"
|
|
else
|
|
local last; last=$(ls "$CAPDIR"/ppm/*.ppm 2>/dev/null | tail -1)
|
|
if [ -n "$last" ] && ! python3 "$ROOT/tools_pc/pixcount.py" "$last" >/dev/null 2>&1; then
|
|
status=NO-FRAMES; note="frames present but all-clear (pixcount 0)"
|
|
else
|
|
local gdir; gdir=$(golden_dir_for "$name")
|
|
if [ -n "$gdir" ]; then
|
|
local fdiff; fdiff=$(python3 "$ROOT/tools_pc/framediff.py" "$CAPDIR/ppm" --golden "$gdir" --json 2>&1)
|
|
local fexit=$?
|
|
# framediff --json sandwiches the JSON blob between human-readable
|
|
# per-frame lines and a summary line -- pull it out by scanning
|
|
# from the first '{' with a raw JSON decoder instead of assuming
|
|
# the whole stream parses.
|
|
worst=$(echo "$fdiff" | python3 -c "
|
|
import json,sys
|
|
s = sys.stdin.read()
|
|
i = s.find('{')
|
|
try:
|
|
d, _ = json.JSONDecoder().raw_decode(s[i:]) if i >= 0 else (None, 0)
|
|
print(max((r.get('worst_cell',0) for r in d['results']), default=0))
|
|
except Exception:
|
|
print('')
|
|
" 2>/dev/null)
|
|
# exit 1 = a real per-frame threshold fail (pixel/cell/hash delta).
|
|
# exit 2 = a structural mismatch (missing/size) -- most commonly the
|
|
# golden's frame stems don't line up with this run's --dump window
|
|
# (today's only golden, level_09, was captured at 200-440:120; a
|
|
# sweep using a different stride has nothing to compare against
|
|
# rather than a real regression). Only exit 1 is a verdict fail.
|
|
case "$fexit" in
|
|
1) status=REGRESSION; note="framediff vs $gdir" ;;
|
|
2) note="golden frame stems don't match --dump window ($dump) for $gdir" ;;
|
|
esac
|
|
else
|
|
note="no golden for $name/$PLATFORM yet (Step 3)"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
local brief="$R_BRIEF"
|
|
if [ "$status" = NO-FRAMES ] && [ -f "$CAPDIR/run.log" ]; then
|
|
# stderr, not stdout: sweep mode captures verify_level's stdout via
|
|
# `out=$(...)` and only keeps its first line (`head -1`) as the verdict
|
|
# summary -- anything printed here on stdout would become that first
|
|
# line and clip the real verdict (and, worse, the --json line further
|
|
# down). stderr bypasses the capture and reaches the terminal/log file
|
|
# directly.
|
|
echo " -- $name run.log tail (NO-FRAMES, no crash -- likely startup/init failure) --" >&2
|
|
tail -n 30 "$CAPDIR/run.log" | sed 's/^/ | /' >&2
|
|
fi
|
|
rm -rf "$CAPDIR"
|
|
emit_verdict "$name" "$status" "$frames" "$worst" "$sym" "$note"
|
|
if [ "$status" = CRASH ] && [ -n "$brief" ]; then
|
|
# crash_brief.py's --json output sandwiches the JSON blob after the
|
|
# human-readable brief -- print only the brief part here.
|
|
echo "$brief" | awk '/^\{/{exit} {print}'
|
|
fi
|
|
case "$status" in PASS) return 0 ;; *) return 1 ;; esac
|
|
}
|
|
|
|
# --- dispatch ----------------------------------------------------------------
|
|
case "$MODE" in
|
|
single)
|
|
resolved=$(resolve_level "$LEVEL_ARG")
|
|
[ -z "$resolved" ] && { echo "unknown level '$LEVEL_ARG'" >&2; exit 2; }
|
|
name="${resolved%%:*}"; num="${resolved##*:}"
|
|
[ "${resolved%%:*}" = num ] && name=$(name_for_num "$num")
|
|
pin_ini_640x480 2>/dev/null || true
|
|
verify_level "$name" "$num" "${DUMP:-200-440:120}"
|
|
;;
|
|
|
|
sweep)
|
|
RESULTS_JSON=()
|
|
RC=0
|
|
subset="${SWEEP_LEVELS:-${SWEEP_SUBSET:-}}"
|
|
entries=("${LEVELS[@]}")
|
|
[ -n "$subset" ] && read -ra entries <<< "$subset"
|
|
for e in "${entries[@]}"; do
|
|
n="${e%%:*}"; num="${e##*:}"
|
|
pin_ini_640x480 2>/dev/null || true
|
|
out=$(verify_level "$n" "$num" "${DUMP:-80-400:40}" 45)
|
|
rc=$?
|
|
[ "$rc" -ne 0 ] && RC=1
|
|
# Pre-existing bug (found alongside D244's diagnostics): `head -1` here
|
|
# silently discarded emit_verdict's --json line (its 2nd line of
|
|
# output, starting with `{`) on every sweep run -- RESULTS_JSON above
|
|
# was declared for exactly this and never actually populated, so
|
|
# `--json` sweeps have never produced a usable verdict array; the
|
|
# workflow's "Extract JSON verdict" step (scans for the last `[{` in
|
|
# sweep.log) always got an empty `[]`.
|
|
echo "$out" | head -1
|
|
if [ "$JSON" = 1 ]; then
|
|
j=$(echo "$out" | grep -m1 '^{')
|
|
[ -n "$j" ] && RESULTS_JSON+=("$j")
|
|
fi
|
|
done
|
|
echo "SWEEP DONE"
|
|
if [ "$JSON" = 1 ] && [ "${#RESULTS_JSON[@]}" -gt 0 ]; then
|
|
{ printf '['
|
|
for i in "${!RESULTS_JSON[@]}"; do
|
|
[ "$i" -gt 0 ] && printf ','
|
|
printf '%s' "${RESULTS_JSON[$i]}"
|
|
done
|
|
printf ']\n'
|
|
}
|
|
fi
|
|
exit $RC
|
|
;;
|
|
|
|
parity)
|
|
[ -z "$LEVEL_ARG" ] && { echo "usage: verify.sh parity <level> --against DIR" >&2; exit 2; }
|
|
[ -z "$AGAINST" ] && { echo "parity needs --against DIR (the other platform's capture)" >&2; exit 2; }
|
|
resolved=$(resolve_level "$LEVEL_ARG")
|
|
[ -z "$resolved" ] && { echo "unknown level '$LEVEL_ARG'" >&2; exit 2; }
|
|
name="${resolved%%:*}"; num="${resolved##*:}"
|
|
pin_ini_640x480 2>/dev/null || true
|
|
run_one "$name" "$num" "${DUMP:-200-440:120}" 90
|
|
if [ "$R_STATUS" != PASS ]; then
|
|
emit_verdict "$name" "$R_STATUS" "$R_FRAMES" "" "$R_SYM"
|
|
rm -rf "$CAPDIR"; exit 1
|
|
fi
|
|
python3 "$ROOT/tools_pc/framediff.py" "$CAPDIR/ppm" --golden "$AGAINST" --tol 2 --tol-pct 0.5 --exact ${JSON:+--json}
|
|
rc=$?
|
|
rm -rf "$CAPDIR"
|
|
exit $rc
|
|
;;
|
|
esac
|