diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index c0cfae00..1436ba9a 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -149 sections. Search this before re-deriving anything. +150 sections. Search this before re-deriving anything. * [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28) * [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28) @@ -160,6 +160,7 @@ dies, which is what this file is for. * [🔴 `verify-screen` was nondeterministic, and it looked fine most of the time](#verify-screen-was-nondeterministic-and-it-looked-fine-most-of-the-time) * [The JP title capture adjudicates `title_jp` — and it goes against the port](#the-jp-title-capture-adjudicates-title_jp--and-it-goes-against-the-port) * [🔴 CORRECTION: the port did not move away from the game — I scored the wrong frame](#correction-the-port-did-not-move-away-from-the-game--i-scored-the-wrong-frame) +* [The `rest()` flash defect reaches four screens I ship — and the port already survives it](#the-rest-flash-defect-reaches-four-screens-i-ship--and-the-port-already-survives-it) ## P0 — the exporter, 2026-08-28 @@ -8695,3 +8696,60 @@ would score the port against a 45 px shift. With it, `title_jp` reads **RMSE ⚠️ The row prints `no capture` until the Decoder's branch merges. Their capture is theirs to commit; it was staged locally to test the row and removed. + +## The `rest()` flash defect reaches four screens I ship — and the port already survives it + +The Decoder censused it from the file side while I was looking at one instance: +of 13 991 elements with ≥2 keyframes, **2 305** have no plateau so the dwell +fallback decides, and **1 697 (74 %)** of those get a *visible* pose. In +`GP_TITLE`, 5 fires and 4 are visible — **all four on the splash screens this +port ships**. + +✅ Confirmed in my own export, and it is exactly the JP-title shape on different +screens: + +| element | keyframes | `rest` | +|---|---|---| +| `palogo_sqex_eff` | `0:a0 15:a255 30:a212 45:a0` | t=30, **a=212** | +| `palogo_anima_eff` | `0:a0 15:a255 30:a212 45:a0` | t=30, **a=212** | +| `palogo_gamearts_eff` | `0:a0 15:a255 **30:a255** 45:a0` | **t=15, a=255** | +| `palogo_seta_eff` | `0:a0 15:a255 **30:a255** 45:a0` | **t=15, a=255** | + +📌 **A refinement to their description**, which named the `212` shape: two of the +four hold **255 through t=30**, so their fallback lands on the flash's *peak* +rather than its decay. Same defect, worse pose — full brightness, not +four-fifths. The logos themselves (`palogo_sqex` holds 255 from t=30 to t=235) +have a real plateau and are unaffected. + +### The port ships the right frame, and now there is a number for it + +Both poses of the publisher splash against the **committed oracle capture**: + +| pose | RMSE | differing | +|---|---|---| +| **timeline — what the port ships** | **2.17** | **0.01 %** | +| `--pose=rest` — the harness frame | 9.05 | 0.75 % | + +🔴 **75× the differing area on a screen this port ships.** So the rule I wrote +into `verify-screen`'s header after getting it wrong on `title_jp` is not a +special case — it generalises, and here it is demonstrated against an oracle +rather than argued. + +✅ The port's settled pose evaluates `pose_at(hold)`, not `rest`, so it skips the +flashes and agrees with the capture at 0.01 %. The defect is confined to the +harness pose. **Nothing shipped is wrong; nothing needed fixing in the render.** + +### What did need fixing: the port said "at rest" about a pose it never looked at + +`ScreenView` logged `"%s (transparent at rest)"` for every skipped element, +whatever instant it had posed. On the timeline path the pose is +`pose_at(time_units)` — so it reported `palogo_sqex_eff (transparent at rest)` +about an element whose **resting alpha is 212**. + +⚠️ That is not cosmetic. The rest-versus-posed-instant confusion is precisely what +made me score a `--pose=rest` frame against a capture and write up a drift that +did not exist. A log line that erases the distinction is that error pre-printed, +waiting to be believed. It now names the instant: `transparent at t=6`. + +Controlled both ways on one screen: timeline → `transparent at t=6` and the flash +skipped; `--pose=rest` → still `at rest`, and the flash **drawn**. diff --git a/port/scripts/screen_view.gd b/port/scripts/screen_view.gd index 2ae86eab..4b7f43ac 100644 --- a/port/scripts/screen_view.gd +++ b/port/scripts/screen_view.gd @@ -640,7 +640,22 @@ func _draw() -> void: else pose_at(element, time_units) var colour := modulate_of(pose) if colour.a <= 0.0: - skipped.append("%s (transparent at rest)" % id) + # 🔴 THIS LINE USED TO SAY "at rest" WHATEVER INSTANT IT HAD POSED. + # + # On the timeline path the pose is `pose_at(time_units)`, not + # `rest`, and on the screens where those differ the message named a + # pose it had not looked at. `palogo_sqex_eff` on the publisher + # splash is `[0:a0 15:a255 30:a212 45:a0]` -- a flash whose `rest` + # alpha is **212**. The port skips it correctly at the settled + # instant and then reported "transparent at rest" about a resting + # pose that is four-fifths opaque. + # + # ⚠️ That is not cosmetic. The rest-versus-posed-instant confusion is + # exactly what made me score a `--pose=rest` frame against a capture + # and write up a drift that did not exist (DECISIONS.md). A log line + # that erases the distinction is the same error, pre-printed. + skipped.append("%s (transparent %s)" % [id, + "at rest" if pose_mode == Pose.REST else "at t=%.0f" % time_units]) continue var pivot := _vec(element.get("pivot", [0, 0])) var pos := _vec(pose.get("pos", [0, 0]))