Files
Sylpheed/tools/port/verify-motion
MechaCat02 a23c321831 port: land the play-tested work, and only that
Takes the port branch up to 77320d5e -- the state the human play-tested on
2026-09-02 -- for SOURCE paths only. Not a branch merge: `auto/port-p6-audio`
is 366 commits and 938 files, and most of that must not land.

WHAT COMES IN (76 files, all human-confirmed working):
  * the logo splash animation. 08ed3dd1 found it: `pose_at` ASSIGNED the settle
    instant instead of clamping to it, so the splash never animated at all --
    and the same bug manufactured a passing harness result, because the harness
    photographed t past the settle. Confirmed by play-test: "cannot notice any
    obvious difference from the actual game."
  * gamepad input -- (A)/(B) bound additively (`ui_accept` ships with NO joypad
    binding), stick latched with hysteresis at the game's own 61% digitise
    threshold. This is what made (A), video-skip and Extras work at all.
  * menu navigation and flow, menu audio, the exporter, the authored
    declarations, and 23 verification tools under tools/port/.

WHAT IS DELIBERATELY LEFT ON THE BRANCH:
  * everything after c0ae460a -- the F5/F6 title-timing investigation, whose own
    tip commit calls itself a "hand-off for one-minute human checks". Unchecked
    by definition; it goes through the new review gate like anything else.
  * the OPTIONS menu work of 2026-09-03. Real, probably good, NOT play-tested.
  * the F1 repeat mechanism, which its own commit calls "deliberately inert".

WHAT MUST NOT LAND, AND WHY THE .gitignore CHANGED:
  545 MB of extracted game content was committed on that branch -- 850 sprite,
  audio and transcoded video files under `export-probe/` and `export-probe2/`,
  plus 246 MB of loose .wav and .tsv at the repo root. This repository's own
  rule, in this file, is "never game content".

  The rule was not missing. It was written, and it was tightened on that very
  branch, with a careful comment explaining why BOTH `export/` and `data/base/`
  had to be listed -- while the exporter was writing to a third name that
  nobody had thought to list. Enumerating names is the thing that failed. So
  the ignore rules now describe the SHAPE: any top-level `export*/`, game media
  by extension, and loose capture output at the root. Verified both ways -- it
  catches all four offenders and ignores nothing currently tracked.

Verified: `cargo check --workspace` clean; all nine GDScript files parse in
project context, with a positive control (an injected syntax error is detected,
3 lines) so the clean result means something. `tools/port/check-all` was NOT
run -- it needs the container, the export tree and a display.
2026-09-04 16:17:14 +02:00

153 lines
6.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Does the boot ANIMATE, or does it draw the same picture very fast?
#
# tools/port/verify-motion # assert
# tools/port/verify-motion --control # can it fail?
#
# 🔴 WHY THIS EXISTS. A human on a 140 fps GPU: *"the port does no blur
# animation at all, the logos just switch."* Three checks this port already had
# were green at the time, and all three were blind the same way:
#
# frozen sweep (`--time=`) proves the renderer CAN draw pose N. It drives the
# clock by hand and never runs the animation.
# settled comparison scored 0.01 % against the oracle. A screen frozen
# 84 % of the time matches a settled reference
# PERFECTLY -- that is what frozen means.
# achieved-fps counter counts frames DRAWN. Drawing identical pixels 25
# times a second scores exactly like animating.
#
# Every one measured throughput or a pose. **None measured CHANGE.** Same shape
# as `InputEventAction` bypassing the input map: the instrument sat below the
# thing that was broken, so the breakage could not appear in it.
#
# This films a REAL boot -- no `--time`, no pinning -- and hands it to
# `tools/motion-census`, which measures change and nothing else.
#
# ⚠️ WHAT IT CANNOT DO. It is the liveness half only. A wrong ramp that moves
# every frame passes here. Correctness stays with `verify-capture` against the
# oracle, and the two are complementary: one screen can pass either alone.
set -euo pipefail
cd "${PROJECT_DIR:-/work}"
export DISPLAY="${DISPLAY:-:97}"
OUT="${OUT:-${TMPDIR:-/tmp}/verify-motion}"
INTERVAL=0.05
# The publisher splash declares its whole build-in over t=0..45 -- 0.75 s at
# 60 units/s -- and then holds. So the FIRST second of the boot is where a
# frozen build-in shows up, and it is the only window this asserts on.
#
# The bar is 60 % of adjacent frame-pairs moving in that window, and BOTH SIDES
# WERE MEASURED rather than one measured and one assumed -- the one operator in
# `ScreenView.pose_at` was reverted, this check run against the defect, and the
# operator restored:
#
# broken (pose_at ASSIGNED the settle instant) 40 % -- and it FAILED
# fixed (clamps to it) 86 % -- and it passed
#
# 60 sits mid-gap: 20 points above the defect, 26 below the fix. That is why it
# is a floor and not a tuned threshold, and it is deliberately NOT set near the
# passing value -- a check that only passes at exactly today's number fails on
# the next legitimate change and teaches people to edit the bar.
#
# 🔴 THE FIRST VERSION CLAIMED "~40 POINTS OF CLEARANCE ON BOTH SIDES" AND HAD
# NOT MEASURED THE BROKEN CASE. With a 1.0 s window the real clearance was 5
# points, because that window includes 0.25 s of legitimate hold and dilutes the
# signal. The window is now the DECLARED build-in -- publisher t=0..45, 0.75 s
# at 60 units/s -- so it asks about the interval the disc says is animating and
# nothing else. A bar justified by an unmeasured number is the same defect this
# whole check exists to catch, one level up.
WINDOW=0.75
BAR=60
films() { # $1 = dir
rm -rf "$1"; mkdir -p "$1"
timeout 120 godot --path port -- --boot --skip-at=1 \
--film="$1/f" --film-interval="$INTERVAL" >"$1/boot.log" 2>&1 || true
}
moving_pct_in_window() { # $1 = dir -- % of adjacent pairs that MOVED, first $WINDOW seconds
python3 - "$1" "$INTERVAL" "$WINDOW" <<'PY'
import sys, glob, os, importlib.util, importlib.machinery
d, interval, window = sys.argv[1], float(sys.argv[2]), float(sys.argv[3])
frames = sorted(glob.glob(os.path.join(d, "f_*.png")))
n = int(window / interval) + 1
frames = frames[:n]
if len(frames) < 3:
print("0"); raise SystemExit
# Reuse motion-census's own loader and floor rather than re-deriving them: a
# second implementation of "did it move" is a second thing to be wrong.
sys.path.insert(0, os.path.join(os.environ.get("PROJECT_DIR", "/work"), "tools"))
spec = importlib.util.spec_from_loader(
"mc", importlib.machinery.SourceFileLoader(
"mc", os.path.join(os.environ.get("PROJECT_DIR", "/work"), "tools", "motion-census")))
mc = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mc)
from pathlib import Path
prev, moved, total = None, 0, 0
for f in frames:
cur = mc.load(Path(f))
if prev is not None:
delta = sum(abs(a - b) for a, b in zip(cur, prev)) / len(cur)
total += 1
if delta > mc.MOVED:
moved += 1
prev = cur
print("%d" % (100 * moved / total if total else 0))
PY
}
echo "boot liveness: films a real boot and measures CHANGE, not throughput"
# 🔴 THE CONTROL RUNS FIRST AND IS NOT OPTIONAL. `motion-census --selftest`
# drives a synthetic fade, a switch and a frozen film through the same loader
# and the same floor this check uses. If it cannot separate those three, every
# number below is decoration.
if ! tools/motion-census --selftest >"$OUT.selftest.log" 2>&1; then
echo " 🔴 motion-census --selftest FAILED -- the detector cannot tell a fade"
echo " from a switch, so nothing it reports about the boot means anything."
sed 's/^/ /' "$OUT.selftest.log"
exit 2
fi
echo " census selftest ok (fade / switch / frozen separated)"
if [ "${1:-}" = "--control" ]; then
# A frozen film must FAIL this check. Built by repeating one real boot frame,
# so it has the port's own pixels and differs from a passing run in exactly
# one property: nothing changes.
films "$OUT/live"
ctl="$OUT/frozen"; rm -rf "$ctl"; mkdir -p "$ctl"
# NOT `ls | head`: under `set -o pipefail` head closes the pipe, ls takes
# SIGPIPE and the script exits 141 before it ever asserts anything. Cost one
# run to notice, and a check that dies before checking looks a lot like a
# check that passed.
local_frames=("$OUT"/live/f_*.png)
src="${local_frames[0]}"
for i in $(seq -w 0 24); do cp "$src" "$ctl/f_0$i.png"; done
pct=$(moving_pct_in_window "$ctl")
if [ "$pct" -lt "$BAR" ]; then
echo " frozen film is REJECTED ok ${pct}% moving, bar ${BAR}%"
echo
echo "the check fails on a film that does not move"
exit 0
fi
echo " frozen film is REJECTED 🔴 FAILED ${pct}% moving -- it passed, so"
echo " this check cannot detect the defect it was written for."
exit 1
fi
films "$OUT/live"
grep -m1 -E "fps achieved" "$OUT/live/boot.log" | sed 's/^ */ /' || true
pct=$(moving_pct_in_window "$OUT/live")
printf ' %-25s %s %d%% of pairs moved in the first %.1fs, bar %d%%\n' \
"build-in moves" "$([ "$pct" -ge "$BAR" ] && echo ok || echo '🔴 FAILED')" \
"$pct" "$WINDOW" "$BAR"
[ "$pct" -ge "$BAR" ] || {
echo
echo "🔴 the boot draws its first second without changing. That is the"
echo " 2026-09-02 defect: poses not advancing while the clock does."
echo " Films are in $OUT/live -- run tools/motion-census on them."
exit 1
}
echo
echo "the boot's build-in animates"