agrees with the timeline P2's gate is the buttons sliding in, and `tools/screen-strip` renders the strip that shows it. But the useful result came out of checking where the animation settles. On 8 of 12 screens the settled timeline is BYTE-IDENTICAL to the declared `rest` pose -- the port walks the keyframes with an authored time unit and arrives, to the pixel, where the pinned decoders independently say the screen rests. On `main_menu` the two differ in exactly one region, 400x470 at (440,108): the bounding box of `ptframe1` and `ptframe2` and nothing else. `rest` puts both at their first keyframe, off-position and transparent. The capture of the running game shows them -- the bright circuit bracket around the menu. Cropping the same region from the capture and from both renders puts the ring and its elbow trace in the timeline render pixel-aligned with the game's, and absent from the rest render. Geometry, so it does not depend on the capture's gamma or on its having been taken with NEW GAME focused. `ui_layout::rest_plateau` excludes a trailing run of identical keyframes because it is normally the exit. On an element with NO exit animation the trailing run IS the hold. The condition that identifies these exactly, with no false positives here, is "the final untimed keyframe has the same pose as the last timed one" -- six elements, and `rest()` misses all six. Filed in BLOCKED.md for the RE agent: the decoders are pinned and are not this port's to fix, and `sylpheed-cli screen render` is missing the bracket too. Worth saying plainly what this does to P1: the port and the reference agreed on `main_menu` to 3/255 and BOTH were missing two elements the game draws. Two renderers reading one field through one decoder agreeing is not evidence the field is right. BLOCKED.md had already said that about the pivot; here it bit. The title is NOT settled and P2 does not claim it. `rest` and the timeline disagree there by 142-247/255, the only live title capture composites the PRESS A plate over build 4 so it cannot be diffed against the title alone, and both of the port's modes draw a cyan glow slab the game does not have -- a third problem, P3's. Recorded as an open question rather than resolved by tuning. Also reconciled against the RE agent's new work: Q8 is answered -- the SE waves are located in `Static.slb` (move/confirm/back), which unblocks P6's audio; and the title's transitions are a lookup by NAME, giving P3/P5 the game's own screen vocabulary as candidate `goto` targets, marked as the name match it is.
85 lines
4.0 KiB
Bash
Executable File
85 lines
4.0 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}"
|
|
|
|
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" --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
|