Files
Sylpheed/tools/verify-screen
Sylpheed port agent 65fe6e04a2 tools: verify-screen -- diff every exported screen against the reference renderer
The P1 gate is a pixel comparison, so it needs to be a command rather than a
paragraph. `tools/verify-screen` renders each screen in the manifest both ways
and reports the largest per-channel difference anywhere in the frame.

Both renderers are held to the same inputs on purpose:

* the reference CLI is the one `build-reference-cli` builds from the revision
  the exporter is PINNED to -- not `/reborn/target/`, which is a live mount of
  the other agent's checkout and moves mid-iteration. A pixel disagreement
  against a moving decoder has a free variable in it and proves nothing.
* `--black`, because the screen carries its own background and the CLI's default
  dim slate stands in for a 3D scene that is not on this screen.
* `--primitives --animated`, because the port draws every element at rest and
  those two flags are what make the CLI draw the same set. NOT `--focus`:
  nothing is focused at rest.

The threshold is 3/255 -- what integer-truncating compositing in the CLI and
float rounding on a GPU differ by. Above that is a placement, order or colour
disagreement that needs a reason, and the script says DIFFERS rather than
pretending a wider tolerance is a result.
2026-08-28 19:31:21 +00:00

78 lines
3.5 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 the port draws every element at its
# resting pose, and those two flags are what make the CLI draw the same 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).
#
# 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}"
CLI="${SYLPHEED_CLI:-${CARGO_TARGET_DIR:-/sylph-home/port/target-container}/release/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" "--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