Files
Sylpheed/tools/verify-screen
Sylpheed port agent b36dddaeb5 build: key the reference CLI's target dir by the pinned revision
The reference renderer was a revision behind for three consecutive diff runs and
nothing said so.

After the pin bump, `build-reference-cli` reported success at rev 5414db3 and
`verify-screen` showed main_menu jumping 3/255 -> 72/255. The natural reading was
that the port had regressed. It had not: the port was right and the REFERENCE was
stale. The shared CARGO_TARGET_DIR still held a `sylpheed-cli` built from
8b6dbcf, cargo said "Finished in 0.13s" and left it there. A clean target dir
built a binary resolving ptframe1 to (440,108) t=62; the shared one still said
(620,108) t=16.

The old check -- does `screen list` run -- cannot catch this. A stale binary runs
perfectly.

So: build into `$CARGO_TARGET_DIR/reference-cli/$rev`, keyed by the pin, where a
new revision has no artifacts to reuse. Then verify the binary AGAINST export/:
both come from the same pin, so if the CLI resolves ptframe1's rest differently
from what the exporter wrote, the two halves of the verification are not the same
revision and it fails loudly. It compares the two rather than asserting a
literal, so it stays true when the pin moves again.

docker/bin is baked into the image, so this needs an image rebuild to reach
PATH; until then invoke the repo copy by path.
2026-08-28 21:44:08 +00:00

88 lines
4.2 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 -- the P1 gate.
#
# 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 REFERENCE 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. Say which renderer is
# wrong and why -- do not tune until they match.
set -euo pipefail
cd "${PROJECT_DIR:-/work}"
# `reference-cli/`, not `release/`: the reference binary is built per pinned
# revision so a pin change cannot silently reuse the previous revision's build.
# See docker/bin/build-reference-cli.
CLI="${SYLPHEED_CLI:-${CARGO_TARGET_DIR:-/sylph-home/port/target-container}/reference-cli/sylpheed-cli}"
DISC="${SYLPHEED_DISC:-/disc}"
OUT="${OUT:-${TMPDIR:-/tmp}/verify-screen}"
export DISPLAY="${DISPLAY:-:97}"
[ -x "$CLI" ] || { echo "no reference CLI at $CLI -- run build-reference-cli" >&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")
"$CLI" screen render "$DISC/dat/GP_TITLE.pak" "$OUT/$name.ref.png" \
--build "$build" --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:)"
# 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 '%-16s build %-3s max %-5s mean %-8s %s\n' "$name" "$build" "$max" "${mean:0:6}" "$verdict"
done
echo "artifacts in $OUT"
exit $status