From 8ae0ec2287bbafd6563c42000055ef8f58ee7040 Mon Sep 17 00:00:00 2001 From: Sylpheed port agent Date: Sun, 30 Aug 2026 11:29:19 +0000 Subject: [PATCH] port: verify-screen was nondeterministic; pin the pulse phase in the harness Running the full set after the plate fix, press_start returned over3 5021, 8919, 5021 on three identical runs. The plate's looping focus record takes its phase from time_units, which free-runs, so the captured frame lands wherever the grab fell -- while the reference renderer cannot pulse at all. The port is not the thing that is wrong: the pulse is measured and a thing that pulses does not stop because the screen arrived. ScreenView.loop_phase_units pins it, negative means free-running and stays the default everywhere, and only the harness passes --loop-phase=0. Controlled: pinned, 3 runs identical; free-running, 3 of 4 identical and one different. That 3-of-4 is why it survived -- it looks deterministic most of the time, and without the negative control a no-op flag would have been indistinguishable from a fix. With the phase pinned press_start reads max 1 / over3 0 OK -- the recorded baseline exactly. Fifteen of sixteen rows now match. The sixteenth, title_jp, has genuinely drifted: 155/20498 -> 233/61208, deterministic, on the Godot side, localized to one 350x396 block at (405,74). There is no capture of the Japanese title, so I can say the renderers moved apart but not which moved. Recorded as an ask, not resolved. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF --- docs/port/BLOCKED.md | 15 ++++++++++ docs/port/DECISIONS.md | 60 +++++++++++++++++++++++++++++++++++++ port/scripts/boot.gd | 12 ++++++++ port/scripts/screen_view.gd | 16 +++++++++- tools/port/verify-screen | 18 ++++++++++- 5 files changed, 119 insertions(+), 2 deletions(-) diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index 05aee79c..46ff767f 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -810,3 +810,18 @@ roughly a second unaccounted for. detector could see? If the latter, what is the first-paint time? I am not authoring a delay to close this - a `2.13 s` authored delay in this same block was already refuted once by arithmetic. + +### A capture of the Japanese title (`GP_TITLE` build 7) + +**Derived from Decoder `daf8f47`.** + +`verify-screen` has `title_jp` at max 233 / over3 61208 against a committed +baseline of 155 / 20498. Established: deterministic, on the Godot side (your +byte-identical-renders finding rules out the reference), and localized to one +350x396 block at (405, 74) - the logo stack, where the port draws `ptlogo_jp`, +`ptlogo3a/b/c` and five `ptlogo_back2eff*` layers. + +**The ask:** a capture of the Japanese title at rest. Without one there is no +oracle for this screen, and agreement with `sylpheed-cli` is not correctness - +so I can say the two renderers moved apart but not which one moved, and I am not +going to pick a direction. diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 9a6f107d..e0736608 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -8508,3 +8508,63 @@ arithmetic. Left alone; recorded as an ask. * `title/on_cancel` — Ⓑ on a **settled** title does nothing, twenty seconds confirmed. This cell has now been `MEASURED` (wrongly), `AUTHORED` (honestly), and `MEASURED` (truly), with the value `null` the whole way through. + +## 🔴 `verify-screen` was nondeterministic, and it looked fine most of the time + +Running the full set after the plate fix, two rows had moved off the committed +baseline. One of them was not a regression at all — it was the harness. + +`press_start` returned `over3` **5021, 8919, 5021** on three identical runs. The +plate's looping focus record takes its phase from `time_units`, which free-runs, +so the captured frame lands wherever the grab happened to fall — while the +reference renderer cannot pulse at all. **A detector that answers differently +each run is worse than one that fails**: it teaches its reader to ignore it. + +⚠️ **The port is not the thing that was wrong.** A thing that pulses does not stop +because the screen has arrived, and the pulse is measured. What was wrong was +comparing a moving frame against a static one and calling the difference a +regression. So `ScreenView.loop_phase_units` pins the phase, negative means +free-running, that stays the default everywhere, and only the harness passes +`--loop-phase=0`. + +✅ Controlled, and the control is what makes the fix trustworthy: + +| | | +|---|---| +| pinned, 3 runs | **identical md5** | +| free-running, 4 runs | 3 identical, **1 different** | + +🟡 That 3-of-4 is the finding worth keeping. **It is usually stable**, which is +exactly why it survived — a flake that fires one run in four reads as a real +regression that "went away", and a `--loop-phase` that changed nothing would have +been indistinguishable from a fix without that negative control. + +✅ With the phase pinned, `press_start` reads **max 1 / over3 0 OK** — *the +recorded baseline exactly*, not some new number. Fifteen of sixteen rows now +match the committed baseline. + +### The sixteenth: `title_jp` has genuinely drifted, and I cannot say which side + +| | max | over3 | +|---|---|---| +| committed baseline | 155 | 20 498 | +| now | **233** | **61 208** | + +What is established: + +* ✅ **deterministic** — 233 / 61 208 twice, so not the phase. +* ✅ **not the reference** — the Decoder reports `screen render` is byte-identical + across the stale and rebuilt binaries (max per-channel 0), so the reference is + stable and the movement is on the Godot side. +* ✅ **localized** — the differing region is a single **350×396 block at + (405, 74)**, the logo stack. `title` is untouched at max 6 / over3 790, which + rules out anything shared by both title screens (the forced-backdrop rule + among them). +* The port draws `ptlogo_jp`, `ptlogo3a/b/c` and the five `ptlogo_back2eff*` + layers here that are transparent at rest on the English title. + +🔴 **What I cannot do is say which renderer is right.** There is no capture of the +Japanese title in the corpus, and this script's own header is explicit that +agreement with the reference is not correctness and a `DIFFERS` is not +automatically the port's fault. Guessing a direction here is precisely the move +the mission forbids. Asked, not resolved. diff --git a/port/scripts/boot.gd b/port/scripts/boot.gd index 67890af5..f7f03bb5 100644 --- a/port/scripts/boot.gd +++ b/port/scripts/boot.gd @@ -89,6 +89,10 @@ func _ready() -> void: if mods != "": print(mods) + # Parsed HERE, before the first `ScreenView` is configured -- it was briefly + # read further down and every `--screen` run silently ignored it. + if args.has("loop-phase"): + _loop_phase = float(args["loop-phase"]) _flow = export_tree.authored("flow.json") if _flow == null and (args.has("boot") or args.has("menu")): push_error(export_tree.error) @@ -224,6 +228,7 @@ func _ready() -> void: viewport.add_child(view) view.looping_focus = _looping_for(name) + view.loop_phase_units = _loop_phase view.draw_leaf_for = _draw_leaf_for view.loop_leaf = _loop_leaf_screens.has(name) view.focused_id = _force_focus @@ -469,6 +474,7 @@ func _advance() -> void: view.holding = true view.time_units = 0.0 view.looping_focus = _looping_for(name) + view.loop_phase_units = _loop_phase view.draw_leaf_for = _draw_leaf_for view.loop_leaf = _loop_leaf_screens.has(name) if not view.load_screen(view.tree, name): @@ -725,6 +731,7 @@ func _menu_arrive() -> void: view.holding = true view.time_units = 0.0 view.looping_focus = _looping_for(name) + view.loop_phase_units = _loop_phase view.draw_leaf_for = _draw_leaf_for view.loop_leaf = _loop_leaf_screens.has(name) if not view.load_screen(view.tree, name): @@ -1072,6 +1079,10 @@ var _overlay_t0 := 0.0 var _overlay_view_t0 := 0.0 ## Wall-clock second at which it is raised, measured from the screen's settle. var _overlay_due: float = 0.0 +## `--loop-phase=` pins the looping record's phase. Negative is +## free-running, which is the default and what a player gets. Only the +## regression harness passes it -- see `ScreenView.loop_phase_units`. +var _loop_phase: float = -1.0 func _overlay_process(delta: float) -> void: @@ -1166,6 +1177,7 @@ func _raise_overlay(name: String) -> void: overlay.units_per_second = view.units_per_second overlay.exit_ramp_units = view.exit_ramp_units overlay.looping_focus = _looping_for(name) + overlay.loop_phase_units = _loop_phase overlay.draw_leaf_for = _draw_leaf_for overlay.loop_leaf = _loop_leaf_screens.has(name) overlay.holding = true diff --git a/port/scripts/screen_view.gd b/port/scripts/screen_view.gd index d947af8e..2ae86eab 100644 --- a/port/scripts/screen_view.gd +++ b/port/scripts/screen_view.gd @@ -99,6 +99,19 @@ var frozen := false var looping_focus: Dictionary = {} +## Pin the looping record's phase instead of taking it from `time_units`. +## +## 🔴 WHY THIS EXISTS. The pulse is CORRECT -- a thing that pulses does not stop +## because the screen has arrived -- but it rides the wall clock, so a captured +## frame lands wherever the grab happened to fall. `verify-screen press_start` +## returned `over3` **5021, 8919, 5021** on three identical runs: a regression +## detector that answers differently each time teaches its reader to ignore it. +## +## The port is not the thing that is wrong here, so the port's behaviour does not +## change: negative means "free-running", which stays the default everywhere. The +## HARNESS pins a phase so the comparison is deterministic. +var loop_phase_units: float = -1.0 + ## Element ids whose nested `.rat` leaf the runtime actually draws. ## ## The exporter flags `leaf_carries_geometry` on 15 elements -- a census fact. @@ -593,7 +606,8 @@ func _draw_focus(element: Dictionary) -> void: and String(loop.get("record_element", "")) == String(fe.get("id", "")): var was := holding holding = false - pose = pose_at(fe, fposmod(time_units, float(loop["period_units"]))) + var lt: float = time_units if loop_phase_units < 0.0 else loop_phase_units + pose = pose_at(fe, fposmod(lt, float(loop["period_units"]))) holding = was var pivot := _vec(fe.get("pivot", [0, 0])) var pos := _vec(pose.get("pos", [0, 0])) diff --git a/tools/port/verify-screen b/tools/port/verify-screen index 3bca02d6..d78cc2c7 100755 --- a/tools/port/verify-screen +++ b/tools/port/verify-screen @@ -138,8 +138,24 @@ print(json.load(open("export/"+f))["source"]["build"])' "$name") tbm=$("$CLI" screen info "$DISC/dat/GP_TITLE.pak" --build "$build" --all 2>/dev/null \ | grep -ioc '\.tbm' || true) + # 🔴 `--loop-phase=0` PINS THE PULSE, AND WITHOUT IT THIS SCRIPT WAS + # NONDETERMINISTIC. `press_start` returned `over3` **5021, 8919, 5021** on + # three identical runs: the plate's looping focus record rides `time_units`, + # so the captured frame lands wherever the grab fell, while the reference + # renderer cannot pulse at all. + # + # ⚠️ The port is NOT the thing that is wrong. A thing that pulses does not + # stop because the screen has arrived, and the pulse is measured. What was + # wrong is comparing a moving frame against a static one and calling the + # difference a regression -- a detector that answers differently each run + # teaches its reader to ignore it, which is worse than one that fails. + # + # So the phase is pinned HERE, in the harness, and nothing about playback + # changes: `loop_phase_units` defaults to free-running everywhere else. + # ⚠️ It is usually stable -- 3 of 4 control runs agreed -- which is exactly + # why this survived: it looks deterministic most of the time. godot --path port --resolution 1280x720 -- \ - "--screen=$name" --pose=rest "--capture=$OUT/$name.godot.png" >"$OUT/$name.log" 2>&1 + "--screen=$name" --pose=rest --loop-phase=0 "--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"