Files
Sylpheed/tools/port/verify-screen
Sylpheed port agent 9bd7297757 port: the P1 regression harness could not have run since the monorepo merge
verify-screen resolves its reference binary to a path build-reference-cli
stopped being able to produce: that script greps Cargo.toml for a
`Syplheed-Reborn.git", rev = "..."` pin, and 9fbb352 replaced it with a path
dependency. The grep returns nothing, the script exits 1, and the binary left
behind predates the merge -- three hours older than the sources, from a revision
nothing points at. The consumer only checked that the file EXISTED.

DECISIONS already carries "the reference renderer was stale for three diff runs"
from P2. This would have been the fourth, by a different mechanism: not a
forgotten rebuild but a build step that could no longer succeed.

The fix is a deletion. Revision-keying solved a two-repo problem -- /reborn's
target/ was a live mount that moved mid-run -- and the monorepo removed that
problem by construction. verify-screen now builds sylpheed-cli from the
workspace, which is the same decoder the exporter and the port read.

BASELINE, all 16 screens, after four milestones of renderer change: no new
drift. The only substantial disagreement is title_jp (max 155, 20498 px), which
is the same one P1 recorded and left open -- ptlogo_eff2 at 125 % scale, where
blit samples the destination pixel's corner and a GPU samples its centre. The
port still does not match it, because matching means reproducing a half-pixel
bias on purpose to make a number smaller.

`max` alone could not tell 2 pixels from 25 000: main_menu and title_jp produced
the same DIFFERS verdict. The script now also reports how many pixels are over
the bar. The bar is NOT raised -- tuning a threshold until things match is the
failure its own header warns about.

Stated plainly because sixteen OK rows look reassuring: the harness renders
--pose=rest, so NONE of the last four milestones' visible work is under test --
not the spinning ring, not the plate composite, not any timeline behaviour, not
audio.

REFUTATION: the RE agent's answer naming "builds 0/1 and 10/11" as the loading
screen is false in the index space this export uses -- entries 10 and 11 are
palogo_sqex and palogo_gamearts, the splashes, which the same agent identified
as such answering my ask 1. Over the twelve is_build bundles, ordinals 10/11 ARE
entries 12/15, so the finding is right and the enumeration is not mine.
authored/screen_names.json is keyed by ENTRY, and someone writing keys "10" and
"11" from that message would name the publisher wordmark and the developer logos
as loading screens, and everything would still validate and run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
2026-08-29 14:10:16 +00:00

146 lines
7.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Diff Godot's drawing of an exported screen against `sylpheed-cli screen
# render` of the same build.
#
# WHAT THIS IS, AND WHAT IT IS NOT.
#
# It is a CONSISTENCY check between two decoders that share their assumptions,
# and a REGRESSION detector: "did anything move since last commit". It is NOT a
# correctness check and agreement here is NOT evidence of correctness.
#
# `sylpheed-cli` is not the oracle. The oracle is the Xenia Canary capture and
# the game. Reborn is an explorer and extraction CLI for verifying decodes, and
# it can be wrong -- this corpus has been bitten three times by both renderers
# agreeing and both being wrong: pteff05 (the menu background, missing from
# both), scale-0, and rest(). Each time the capture caught it and neither
# renderer could have.
#
# So: a DIFFERS row means "we moved apart, go find out which of us moved". It
# does not mean the port is wrong. Where a capture and this tool disagree, the
# capture wins. Use `tools/verify-capture` for the correctness question.
#
# tools/verify-screen # every screen in the manifest
# tools/verify-screen main_menu title # named screens
#
# Writes <screen>.godot.png, <screen>.ref.png and <screen>.diff.png into
# $OUT (default: a directory under /tmp) and prints, per screen, the largest
# per-channel difference anywhere in the frame.
#
# The two renderers are held to the same inputs on purpose:
#
# * the COMPARISON CLI is the one built by `build-reference-cli`, from the same
# `sylpheed-formats` revision the exporter is pinned to. /reborn's own
# target/ is a live mount of the other agent's checkout and moves mid-run; a
# pixel disagreement against a moving decoder proves nothing.
# * `--black` because Godot clears to black and the screen carries its own
# background. The CLI's default dim slate stands in for a 3D scene behind an
# in-mission screen, which is not this screen.
# * `--primitives --animated` because those are what make the CLI draw the same
# element set. `--focus` is NOT passed: nothing is focused at rest (HANDOFF
# Q5 measured initial focus as unstable boot to boot, so choosing one is
# P5's decision).
# * `--pose=rest` on the Godot side. Since P2 the port's DEFAULT is to play the
# timeline, and the settled timeline is deliberately NOT what `rest` says --
# the export's `rest` misses `ptframe1`/`ptframe2` on the main menu, and the
# running game shows them (docs/DECISIONS.md). Both renderers read `rest`
# through the same decoder, so asking for it here keeps this a test of the
# PORT against the reference. It is not the test of whether `rest` is right;
# that one is the oracle capture, and the port already departs from it.
#
# A difference here is not automatically the port's fault, and it is not
# automatically a fault at all. Say which renderer moved and why -- do not tune
# until they match.
set -euo pipefail
cd "${PROJECT_DIR:-/work}"
# THE REFERENCE IS THE WORKSPACE'S OWN `sylpheed-cli`, and that is a change.
#
# It used to be a binary built per PINNED REVISION into `reference-cli/<rev>/`,
# because `sylpheed-formats` was a git dependency and /reborn's target/ was a
# live mount of the other agent's checkout that moved mid-run. A pixel
# disagreement against a moving decoder has a free variable in it.
#
# The monorepo merge (`65cefa7`) removed that problem by construction:
# `crates/sylpheed-export/Cargo.toml` now says
# `sylpheed-formats = { path = "../sylpheed-formats" }`, so the exporter, this
# reference and the port all read ONE decoder -- the working tree's.
#
# 🔴 It also silently broke the old machinery, and this script did not notice.
# `build-reference-cli` greps Cargo.toml for `Syplheed-Reborn.git", rev = "..."`;
# that line no longer exists, so the script exits 1 and the binary at
# `reference-cli/sylpheed-cli` is whatever was last built before the merge --
# here, three hours older than the sources and from a revision nothing points
# at any more. Running the diff against it would have compared the port to a
# decoder from another era and called the result a regression check. This
# corpus has already been bitten by a stale reference renderer three times.
#
# So: build it from the workspace. `SYLPHEED_CLI` still overrides, for anyone
# who does want to pin one deliberately.
CLI="${SYLPHEED_CLI:-}"
if [ -z "$CLI" ]; then
CLI="${CARGO_TARGET_DIR:-/sylph-home/port/target-container}/release/sylpheed-cli"
cargo build --release -p sylpheed-cli >/dev/null 2>&1 || true
fi
DISC="${SYLPHEED_DISC:-/disc}"
OUT="${OUT:-${TMPDIR:-/tmp}/verify-screen}"
export DISPLAY="${DISPLAY:-:97}"
[ -x "$CLI" ] || { echo "no reference CLI at $CLI -- \`cargo build --release -p sylpheed-cli\` failed?" >&2; exit 2; }
[ -f export/manifest.json ] || { echo "no export/manifest.json -- run build-export --run" >&2; exit 2; }
mkdir -p "$OUT"
# Godot needs one scan to register the `class_name` globals; without it every
# script fails to parse and the run dies with no frame drawn.
[ -d port/.godot ] || godot --headless --path port --import >/dev/null 2>&1
screens=("$@")
if [ ${#screens[@]} -eq 0 ]; then
mapfile -t screens < <(python3 -c '
import json; print("\n".join(s["name"] for s in json.load(open("export/manifest.json"))["screens"]))')
fi
status=0
for name in "${screens[@]}"; do
build=$(python3 -c '
import json,sys
m=json.load(open("export/manifest.json"))
f=next(s["file"] for s in m["screens"] if s["name"]==sys.argv[1])
print(json.load(open("export/"+f))["source"]["build"])' "$name")
# `--all` because the exporter now addresses by PAK ENTRY INDEX, which is the
# numbering `--all` uses; without it the CLI enumerates only the 12 bundles
# `is_build` accepts and `--build 10` would land on entry 12. `--all` widens
# the list, it does not change how any one bundle composites.
"$CLI" screen render "$DISC/dat/GP_TITLE.pak" "$OUT/$name.ref.png" \
--build "$build" --all --black --primitives --animated >/dev/null
godot --path port --resolution 1280x720 -- \
"--screen=$name" --pose=rest "--capture=$OUT/$name.godot.png" >"$OUT/$name.log" 2>&1
convert "$OUT/$name.godot.png" "$OUT/$name.ref.png" \
-compose difference -composite -colorspace Gray -auto-level "$OUT/$name.diff.png"
read -r max mean <<<"$(convert "$OUT/$name.godot.png" "$OUT/$name.ref.png" \
-compose difference -composite -format "%[fx:maxima*255] %[fx:mean*255]" info:)"
# HOW MANY pixels are over the bar, not just how far the worst one is. A
# single `max` cannot tell 2 pixels from 25 444, and this run produced both:
# `main_menu` trips the threshold on TWO pixels out of 921 600 while
# `title_jp` trips it on 2.8 % of the frame. Reporting only the max made those
# the same verdict, which is how a real disagreement hides behind a rounding
# one. The bar itself is NOT raised -- tuning a threshold until things match
# is the failure this script's own header warns about.
over=$(convert "$OUT/$name.godot.png" "$OUT/$name.ref.png" \
-compose difference -composite -colorspace Gray -threshold $((3*65535/255)) \
-format "%[fx:int(mean*w*h)]" info:)
# 3/255 is what integer-truncating compositing in the CLI and float rounding
# in a GPU differ by. Anything above that is a placement, order or colour
# disagreement and needs a reason, not a threshold.
verdict=OK
awk "BEGIN{exit !($max > 3)}" && { verdict=DIFFERS; status=1; }
printf '%-17s build %-3s max %-5s mean %-8s over3 %-7s %s\n' \
"$name" "$build" "$max" "${mean:0:6}" "$over" "$verdict"
done
echo "artifacts in $OUT"
exit $status