Files
Sylpheed/tools/port/verify-dwell
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

144 lines
6.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Check the port's boot pacing against captures of the real game.
#
# tools/port/verify-dwell
#
# WHY THIS IS A TOOL AND NOT A ONE-OFF. Doing it by hand once already refuted a
# 🔴 I had filed myself: `docs/port/BLOCKED.md` said `rest.t` was the wrong settle
# landmark AND that "everything the sequencer paces off it is therefore late".
# The first half is true; the second was wrong, and I nearly re-paced screens
# that already matched the game to 0.05 s.
#
# ⚠️ A PORT'S TRANSITION INTERVAL IS NOT THE ORACLE'S VISIBLE SPAN. They differ
# by the black hold between screens, and confusing the two cost this corpus 0.6 s
# once and 0.48 s on the plate delay. So the comparison here is explicit: the
# port's interval is checked against the oracle's span PLUS the measured hold.
#
# 🔴 AND THE VERDICT DOES NOT COME FROM THE FILMSTRIP ANY MORE. It used to
# measure ink spans from `--film` frames. The boot's black hold is 0.17-0.23 s
# (HANDOFF Q7) -- shorter than the 0.25 s cadence meant to observe it -- so when
# the black frame fell between samples two screens merged into one span and this
# tool reported `developer logos` as 93 s against an oracle of 3.5 s. Filming at
# 0.1 s made it WORSE: 2.5x the screenshots slows the run enough that the capture
# catches up in bursts, and the publisher span came back as 7.80 s.
#
# The sequencer already knows exactly when it changed screens and prints it.
# Sampling a picture to rediscover a number the program can state is how this
# went wrong. The filmstrip is kept, and marked advisory.
#
# THE EXPECTED NUMBERS ARE THE ORACLE'S, NOT THE PORT'S: three cold boots from
# `docs/re/boot-order-and-splash-dwell.md`, quoted as a test fixture. Nothing in
# the port derives them and nothing may.
set -euo pipefail
cd "${PROJECT_DIR:-/work}"
export DISPLAY="${DISPLAY:-:97}"
OUT="${OUT:-$(mktemp -d)}"
mkdir -p "$OUT"
INTERVAL="${INTERVAL:-0.25}"
echo "running the boot (the intro is skipped -- the splashes are what this measures)"
timeout "${TIMEOUT:-300}" godot --path port --resolution 1280x720 -- \
--boot --skip-at=1 "--film-interval=$INTERVAL" "--film=$OUT/f" \
>"$OUT/boot.log" 2>&1 || true
INTERVAL="$INTERVAL" python3 - "$OUT" <<'PYEOF'
import glob, os, re, subprocess, sys
out = sys.argv[1]
INTERVAL = float(os.environ.get("INTERVAL", "0.25"))
# The boot's black gap, measured in the DRAW STREAM (4 presented frames at
# 2.284 units/frame = 9.1 units), not from luminance -- luminance cannot separate
#
# ⚠️ 2.284 IS NOT A GENERAL RATE AND THIS LINE USED TO READ AS IF IT WERE.
# It is the disc used as its own clock ON ONE CAPTURE, which ran at 13.1 fps
# against ~28 elsewhere: `palogo_sqex` declares alpha >= 1 for 239.8 units and
# was drawn in 105 frames of that run. Correct for converting THAT run's frame
# count; not a constant, and not HANDOFF Q1's 2 units per rendered frame, which
# is a different quantity measured at normal speed. See DECISIONS.md.
# the outgoing fade's tail from true black. The +/-1 frame range is 6.9-11.4
# units = 0.114-0.190 s. HANDOFF Q7's luminance figure of 0.17-0.23 s overlaps
# only at the top, and the draw-stream number is the one to use.
HOLD_LO, HOLD_HI = 0.114, 0.190
# 🔴 THAT IS THE GAME'S GAP. THE PORT'S IS AUTHORED AND IS CURRENTLY 0.
#
# This tool built its target as `oracle span + the GAME's black gap` and compared
# the port against it -- correct only while the port inserted that gap. It does
# not: `black_hold_units` went to 0 (four measured gaps, 0/6/4/6 units, no rule;
# see authored/timing.json). So the port is expected to run SHORT by the gap, and
# on `publisher_logo` it does -- 0.131 s below the unslacked target, which the
# 0.15 s wall-clock slack was quietly absorbing into an "agrees".
#
# Read from the authored file so it cannot drift again, and REPORT the shortfall
# rather than hide it. A verdict that passes because the slack happens to exceed
# a known omission is not a verdict.
#
# 🔴 AND THE RATE WAS HARDCODED WHILE THE VALUE WAS NOT. This line read
# `black_hold_units` from the file -- so it "cannot drift again" -- and then
# divided by a literal 60.0. The value could not drift; the conversion could,
# and would have gone silently wrong the moment `keyframe_units_per_second`
# moved. It is under active dispute right now (60 vs 120), so this is a live
# hazard rather than a tidy-up. Harmless only because the hold is currently 0.
import json as _json
_timing = _json.load(open("authored/timing.json"))
_UPS = float(_timing.get("keyframe_units_per_second", 60))
PORT_HOLD = float(_timing.get("black_hold_units", 0)) / _UPS
marks = []
for line in open(os.path.join(out, "boot.log"), errors="replace"):
m = re.match(r"\s+-> (\S+) at ([0-9.]+) s", line)
if m:
marks.append((m.group(1), float(m.group(2))))
if not marks:
print("no transitions in the boot log -- see", os.path.join(out, "boot.log"))
raise SystemExit(2)
ORACLE = [
("publisher wordmark", [4.297, 4.604, 4.370]),
("developer logos", [3.508, 3.503, 3.366]),
]
starts = [0.0] + [t for _, t in marks]
print()
print("%-20s %-14s %-26s %s" % ("screen", "port interval", "oracle span (3 boots)", "verdict"))
bad = 0
for k, (name, runs) in enumerate(ORACLE):
if k + 1 >= len(starts):
print("%-20s %-14s %s" % (name, "-", "no such transition this run")); continue
d = starts[k + 1] - starts[k]
lo, hi = min(runs) + PORT_HOLD, max(runs) + PORT_HOLD
game_lo, game_hi = min(runs) + HOLD_LO, max(runs) + HOLD_HI
ok = lo - 0.15 <= d <= hi + 0.15
bad += 0 if ok else 1
print("%-20s %-14s %-26s %s"
% (name, "%.2f s" % d, "%.3f / %.3f / %.3f" % tuple(runs),
"agrees" if ok else "DIFFERS"))
print()
print(" Target = oracle SPAN + the PORT's authored hold (%.3f s); the GAME's" % PORT_HOLD)
print(" measured gap is %.3f-%.3f s, so a port with hold 0 runs short by that." % (HOLD_LO, HOLD_HI))
print(" 0.15 s of slack for wall-clock jitter -- which is LARGER than the gap,")
print(" so a shortfall of that size passes unless it is reported separately:")
print(" transitions:", ", ".join("%s@%.2f" % m for m in marks[:4]))
frames = sorted(glob.glob(os.path.join(out, "f_*.png")))[:120]
if frames:
means = [float(subprocess.run(["convert", f, "-colorspace", "Gray", "-format",
"%[fx:mean*255]", "info:"], capture_output=True, text=True).stdout or 0)
for f in frames]
ink = [m > 0.0 for m in means]
spans, i = [], 0
while i < len(ink):
if ink[i]:
j = i
while j < len(ink) and ink[j]: j += 1
spans.append((i * INTERVAL, j * INTERVAL)); i = j
else:
i += 1
print()
print(" advisory -- filmstrip ink spans at %.2f s, which CANNOT resolve a" % INTERVAL)
print(" %.2f-%.2f s hold and merges screens whenever it misses one:" % (HOLD_LO, HOLD_HI))
for a, b in spans[:4]:
print(" %6.2f - %6.2f s (%.2f s)" % (a, b, b - a))
raise SystemExit(1 if bad else 0)
PYEOF
rc=$?
echo "artifacts in $OUT"
exit $rc