TWO FINDINGS, one mine and one handed to me, and the second retires a premise I built on twice. THE P1 BASELINE HAD ROWS THAT PROVED NOTHING. `build_12` and `build_15` render pure black in BOTH renderers -- mean 0, max 0 -- so the difference is zero and `verify-screen` scored them `max 0 over3 0 OK`, the strongest verdict it has. Two of sixteen rows were comparing nothing against nothing. Worse than a missing test, because a missing test is visible in the count. Cause isolated by a control, not by reading: `build_00`/`build_01` are the same loading screen minus three elements and render fine (mean 1.913, max 214.5). The dressed variants add `pgloading_eff00`, a 1280x720 primitive resting OPAQUE BLACK at t=38 inside its own opening black hold, with no layer key so paint order puts it last. The rule I was about to write -- "rest.t before the last timed keyframe is the pathology" -- was killed by running the census first: 152 of 212 elements in this export have rest.t earlier than their last timed keyframe. It is the norm. What is actually unusual is the CONTENT, and its reach is one: `pgloading_eff00` is the only element in the export whose resting pose is a fully opaque full-frame quad. One instance is not a rule, so the renderer is unchanged and the HARNESS is fixed: a blank pair now reports BLANK -- both renderers drew nothing; this row proves nothing. `status` is untouched, so an unrelated DIFFERS still fails. THE VOICE EXPORT IS KNOWN INCOMPLETE. The Decoder booted Canary with --xma_param_probe and the game decodes ALL THREE streams CONCURRENTLY, in three XMA contexts whose byte sizes match the disc payloads exactly. So "three presentations of one take, pick one" is refuted by the running game and the question I had been arguing -- WHICH presentation -- has no answer. This one no census could have caught. Every measurement was right: the streams are equal-duration, one is silence, one is 0.60x another with the residual 26.8 dB down. The frame around them was wrong, and the file says ChannelMask 0x0002 on all three. It took the running game -- which is the mission's own sentence arriving in practice. BEHAVIOUR HELD DELIBERATELY. An equal-gain 1/n sum of channel pairs is not a downmix either -- MISSION section 6 pins an explicit matrix for exactly that reason -- and summing cost S00A 6.02 dB when one stream was silence. Swapping one guess for another on a message is what produced this entry twice. What changed is that the wrongness is now LOUD, because this failure sounds like success: one stream is clean audible dialogue. A top-level manifest warning per movie, the console line, and the authored entry all say `1 of 3 streams`. "They are 5.1" is recorded as the Decoder's HYPOTHESIS with its own counter-evidence attached, and nothing builds on it. What settles it is asked: a recording of the game's own output over ADV through the null sink, which turns channel roles into a fit against an oracle. Refutation attempt, survived: the Decoder's loading-screen variant map. Entries 0/1 carry 7 elements and 12/15 carry those seven plus baseeff, eff00 and loop5 -- exact in count and identity, and it is what made build_00 a control. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
168 lines
8.6 KiB
Bash
Executable File
168 lines
8.6 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:)
|
|
|
|
# BOTH FRAMES BLANK IS NOT AGREEMENT, AND THIS SCRIPT USED TO SAY IT WAS.
|
|
#
|
|
# `build_12` and `build_15` -- the two dressed loading screens -- render as
|
|
# pure black in BOTH renderers, mean 0 and max 0, so the difference is 0 and
|
|
# the row read `max 0 over3 0 OK`. Two of the sixteen rows in the committed
|
|
# baseline were comparing nothing against nothing and reporting the strongest
|
|
# verdict this script has.
|
|
#
|
|
# That is worse than a missing test: it is a test that reports a pass. The
|
|
# screens are black because `pgloading_eff00` is a full-frame opaque black
|
|
# quad whose `rest.t` (38) sits inside its own opening black hold, and
|
|
# `--pose=rest` freezes it there -- see docs/port/DECISIONS.md. Whether that
|
|
# is the port's bug or the decoders' reading of `rest` is open; what is not
|
|
# open is that a blank pair may not be scored.
|
|
#
|
|
# So blankness is checked FIRST and reported as its own verdict. It is not a
|
|
# failure -- the port may legitimately have nothing to draw -- but it is not a
|
|
# pass either, and `status` is left alone so an unrelated screen's DIFFERS is
|
|
# still what fails the run.
|
|
ink=$(convert "$OUT/$name.godot.png" "$OUT/$name.ref.png" \
|
|
-evaluate-sequence max -colorspace Gray -format "%[fx:maxima*255]" info:)
|
|
verdict=OK
|
|
if awk "BEGIN{exit !($ink <= 0)}"; then
|
|
verdict="BLANK -- both renderers drew nothing; this row proves nothing"
|
|
else
|
|
awk "BEGIN{exit !($max > 3)}" && { verdict=DIFFERS; status=1; }
|
|
fi
|
|
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
|