diff --git a/authored/timing.json b/authored/timing.json index 1a96504c..9270c214 100644 --- a/authored/timing.json +++ b/authored/timing.json @@ -73,73 +73,37 @@ ], "looping_focus_records": { "_": [ - "WHICH focus records the port draws unconditionally, and loops. CHOSEN, and", - "deliberately a LOOKUP rather than a rule -- delete an entry the day the disc", - "or a capture states it.", + "WHICH focus records the port draws unconditionally, and loops.", "", - "THE PRESS (A) PLATE GLOWS AND PULSES. Measured on the running game:", - "`ptbtn00f`, the plate's focus record, ramps alpha 0x00->0x50 and back, and", - "its period was timed four times at 2.12 / 2.19 / 2.34 / 2.31 s. You cannot", - "measure a period unless the thing repeats, so THAT IT LOOPS IS MEASURED. The", - "port drew nothing at all here, because press_start has no `buttons` and", - "nothing is focused.", + "🔴 EMPTY AS OF 2026-08-30, and the deletion is the finding.", "", - "WHY A LOOKUP AND NOT A RULE, which is the part worth keeping. The spinning", - "ring is a RULE in the renderer (`spin_period_units`) because it has a", - "disc-wide check: 16 of 212 elements match its shape and all 16 are focus", - "rings, zero false positives. The obvious rule for a pulse -- a group whose", - "keyframes vary only in alpha and whose first and last alpha are equal -- was", - "CENSUSED BEFORE BEING WRITTEN and it fails: 82 OF 212 ELEMENTS MATCH,", - "including ptcopyright, palogo_sqex, ptmsg and every _eff fade-in. A renderer", - "rule on that shape would make the copyright notice pulse.", + "The one entry here -- `press_start/ptbtn00`, period 120 -- was AUTHORED to", + "give the PRESS (A) plate a glow on a screen that has no buttons and focuses", + "nothing. Measured, it did the opposite: it made the plate INVISIBLE.", "", - "Narrowing to focus records leaves exactly ONE distinct element (ptbtn00f,", - "plus its JP twin), and a rule justified by n=1 is a special case wearing a", - "rule's clothes. So it is written here, per element, by name." - ], - "press_start/ptbtn00": { - "record_element": "ptbtn00f", - "period_units": 120, - "kind": "chosen", - "why": [ - "120 units, and it is DECODED rather than chosen -- but it is still written", - "here because the field is not in the pinned crate yet. DELETE THIS the day a", - "tag exposes it and the exporter can emit it.", - "", - "A nested record is itself a RATC bundle with its own header, and that header's", - "+0x08 IS THE LOOP LENGTH -- the same field `ui_header_time_disc` already tests", - "as an animation length at the top level. A record's keyframes need not fill", - "it, and the slack is a hold at the final pose. `ptbtn00f` is 105 UNITS OF RAMP", - "INSIDE A 120-UNIT CYCLE, so the glow rests dark for 15 units before repeating.", - "The five `ptbtn0Nf` records on the main menu fill their 120 exactly, which is", - "what shows the slack belongs to this record and not to the format.", - "", - "Disc-wide over 1781 timed nested records: 92.3% declare exactly their last", - "keyframe time, 7.7% declare more, and 0 DECLARE LESS. That last row is the", - "falsifier -- a cycle cannot restart before its own last pose -- and it never", - "fires.", - "", - "AND IT SURVIVES THE OBJECTION THIS ENTRY RAISED, using THIS PORT's number.", - "Both candidate periods need the same emulator pacing factor, and the ring", - "measures it independently: declared 120 units -> 2.177 s = 1.0885.", - "", - " 105 units = 1.750 s nominal, needs a factor of 1.211-1.337 -> EXCLUDES it", - " 120 units = 2.000 s nominal, needs a factor of 1.060-1.170 -> CONTAINS it", - "", - "So 105 cannot reach the corpus's measured 2.12-2.34 s under any pacing the", - "ring also satisfies. At 120 the prediction is 2.177 s. Different elements,", - "different bundles, separate runs, tied only by both declaring 120.", - "", - "HISTORY, because the sequence is what makes this checkable: this entry read", - "129 (last keyframe 105 + the since-deleted exit_ramp_units), then 105 (the", - "group length under the corrected layout, which I shipped knowing it was 17%", - "short of every measurement). The 123-vs-129 pair I once called unseparable", - "STRADDLED the right answer without containing it.", - "", - "The ~3% between 2.177 and the corpus's 2.24 mean is four wall-clock samples of", - "a 2 s period; the Decoder looked for a further declared hold and found none." - ] - } + "`_draw` treats a looping focus record the way it treats a focused button --", + "it draws the focus record INSTEAD of the base sprite and moves on. For a", + "focused button that is right and measured (the focused sprite covers the", + "base at 100.0 % of base-visible pixels). For the plate it substituted a dim", + "glow at the wrong phase for the element's own bright sprite:", + "", + " with the entry without it", + " press_start t=236 max 0 max 252.5", + " press_start t=240 max 0 max 252.5", + " press_start t=250 max 15.3 max 252.5", + "", + "So the plate could not be seen at any instant, on the screen whose entire", + "content is the plate. The element's own fade is DECODED -- alpha 255 at", + "t=236-238 -- and this authored guess was overriding decoded data with a", + "worse answer. That is the wrong way round, and an authored entry that", + "suppresses a decode is deleted rather than tuned.", + "", + "⚠️ What is NOT claimed: that the plate has no glow. The focus record", + "`ptbtn00f.rat` exists and may well be drawn ON TOP of the base in the real", + "game. Drawing both would be a rendering rule nobody has measured, so the", + "port draws what the disc's own fade says and leaves the glow out. See", + "docs/port/BLOCKED.md -- a capture of the idle title settles it." + ] }, "exit_ramp_deleted_why": [ "DELETED 2026-08-29, and the deletion is the point.", diff --git a/crates/sylpheed-export/src/screen.rs b/crates/sylpheed-export/src/screen.rs index ba1381ae..d74a65a1 100644 --- a/crates/sylpheed-export/src/screen.rs +++ b/crates/sylpheed-export/src/screen.rs @@ -651,9 +651,33 @@ fn settle_window(elements: &[Element]) -> Option<[i64; 3]> { if times.len() < 2 { return None; } + // 🔴 A GAP IN WHICH NOTHING IS VISIBLE IS NOT A SETTLE WINDOW. + // + // The widest keyframe-free interval is only a settled state if the screen is + // actually PRESENTING something across it. `press_start` is the case that + // proves it: its keyframes are 0, 214, 236, 238, 244, so the widest gap is + // 0..214 -- the dead stretch BEFORE the plate appears, where `ptbtn00` is + // alpha 0 throughout. Taking its midpoint gave a settle instant of t=107, + // and the runtime then answered every question about that screen at t=107. + // The result was that the PRESS (A) plate could not be drawn at any instant + // at all, including the boot's own end state, whose entire purpose is to + // show it. + // + // The fix is not a tuned threshold: it is that the heuristic was reading an + // interval where the screen is BLANK as the interval where it has arrived. + // Rejecting those leaves `press_start` with 214..236 (22 units), which is + // under the runtime's 30-unit bar, so it falls back to each element's own + // hold -- which is the plate, opaque, exactly as the disc declares it. + // + // ⚠️ This does not disturb the windows the settle instant was measured on. + // `title` keeps [160, 236]: elements are visible across it, and the + // Decoder's draw stream independently found the game's clock freezing in + // that same interval. + let visible_at = |t: i64| elements.iter().any(|e| alpha_at(e, t) > 0); let (a, b) = times .windows(2) .map(|w| (w[0], w[1])) + .filter(|(a, b)| visible_at((a + b) / 2)) .max_by_key(|(a, b)| b - a)?; Some([a, b, (a + b) / 2]) } diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index 992ec05b..f0fa3d74 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -536,6 +536,14 @@ pulses on a period (`authored/timing.json` already carries a *speculative* what a pulse would need); or it genuinely blinks once and vanishes, in which case the port is right and this closes ✅. -⚠️ Note the port already draws a looping focus record for this element. If the -answer is "blinks once", **that entry is wrong and should be deleted**, not kept -as harmless — it is the only thing making the plate reappear at all. +🔴 **Correction, 2026-08-30.** This row previously said the looping focus record +was "the only thing making the plate reappear at all". The opposite was true: the +entry was the only thing making it **disappear** — it drew a dim glow *instead of* +the plate's own sprite, max 0 against max 252.5. It has been deleted, and the port +now draws the plate from its own decoded fade. See `DECISIONS.md`. + +⚠️ So the question narrows rather than closes. The port shows the plate at its +declared instants and holds it. What a capture would still settle is whether the +real plate **pulses** after that, and whether `ptbtn00f.rat` — the focus record, +which the port now draws not at all — is a glow layered *over* the plate. Drawing +both would be a rendering rule nobody has measured. diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 539c8db1..0a18f9a7 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -5828,3 +5828,95 @@ did not match the flag I passed, and that was visible in my first `ls`.** A verbatim re-run produces six correctly-named per-step shots. I do not know where those 410 files came from, which is itself the point: I drew a conclusion from a file set whose provenance I never checked. + +## The `PRESS Ⓐ` plate: four bugs in a row, and a number I have been misquoting + +Last iteration I filed that the plate was visible for 8 units and simply missed by +the boot's capture instant. That was wrong in the direction that matters: **the +plate could not be drawn at any instant at all**, and three separate faults had to +be removed before it appeared. The fourth is a correction to figures I have quoted +to the Decoder repeatedly. + +### 1. `--time=` was silently ignored on half the screens + +`pose_at` did `if holding: t = settle_instant`. The requested instant was +**discarded** on every screen with a settle window ≥ 30 units — `title`, +`press_start`. The flag parsed, the log printed the time asked for, and the pose +came from somewhere else entirely. + +`ScreenView.frozen` now marks an explicitly pinned instant and skips both clamps. + +### 2. The settle window picked an interval where nothing is visible + +`press_start`'s keyframes are 0, 214, 236, 238, 244. The widest keyframe-free gap +is **0…214** — the dead stretch *before* the plate exists, where `ptbtn00` is +alpha 0 throughout — so its settle instant was **t=107**, and every question about +that screen was answered there. + +🔴 **A gap in which nothing is visible is not a settled state.** The exporter now +rejects those intervals. `press_start` becomes [214, 236] — 22 units, under the +runtime's 30-unit bar — so it falls back to each element's own hold, which is the +plate, opaque, as the disc declares it. + +⚠️ It disturbs no window the settle instant was measured on: `title` keeps +[160, 236], the interval the Decoder's draw stream independently found the game's +clock freezing in. + +### 3. An authored entry of mine was suppressing the decode + +Even then the plate stayed dark, because `authored/timing.json`'s +`looping_focus_records` entry for `press_start/ptbtn00` made `_draw` take the +focus path — which draws the focus record **instead of** the base sprite: + +| | with the entry | without | +|---|---|---| +| `press_start` t=236 | max **0** | max **252.5** | +| t=240 | max 0 | max 252.5 | +| t=250 | max 15.3 | max 252.5 | + +I authored that entry to give the plate a glow. It substituted a dim glow at the +wrong phase for the element's own bright sprite, on the screen whose entire +content is that sprite. **Deleted** — an authored guess that overrides decoded +data with a worse answer is removed, not tuned. The glow is not claimed either +way; drawing both would be a rendering rule nobody has measured. + +✅ The boot's end artifact now contains the plate: mean **95.7** in its region +against **33.6** for the title art alone, and the overlay reports `drew 1: +ptbtn00` where it reported `drew 0`. + +### 4. 🔴 `verify-capture` has been measuring a different pose than it reported + +It passed `--time=5.9617` for the title — t=357.7 units, the Decoder's refined +sweep fit — and **that value was never applied**, because of fault 1. Every title +figure this tool has printed, including the **0.26 %** I have quoted to the +Decoder more than once, was measured at the **settle instant, t=198**, under a +note claiming t=357.7. + +Honouring the flag made it visible: t=357.7 is past the title's own group, which +ends at t=269, so the whole screen posed at its faded-out final keyframes and the +disagreement went to **30.97 %**. The instant was only ever meant for the `ptloop` +leaf, which runs to t=600 and is looped separately by `loop_leaf`. Applying it to +the whole screen was always wrong and was harmless only while it was ignored. + +The splashes had the same shape: `--time=99` was an idiom for "settled" that +worked only because it was discarded. Both rows now pose by omission, and the +tool's note says what it does. + +| | before | now | +|---|---|---| +| `title` | 0.26 % *(labelled t=357.7, actually t=198)* | **0.21 %** at t=198, labelled t=198 | +| `publisher_logo` | 0.01 % | 0.01 % | +| `developer_logos` | 0.01 % | 0.01 % | +| `main_menu` | 0.07 % | 0.08 % | +| `extras` | 0.19 % | 0.19 % | + +The agreements were real; the **stated pose was not**. Corrected with the +Decoder, since they have those numbers. + +### The flag audit that started it + +All 16 flags `boot.gd` parses were exercised for an observable effect after last +iteration's two silent ones. `--pose=rest` (max 111 against the timeline), +`--play` (enters the menu), `--no-hold` (max 255 on two screens) all pass. `--time` +was the one that did not, and it took a screen whose content is a single late +spike to make the failure visible. diff --git a/port/scripts/boot.gd b/port/scripts/boot.gd index 3a262e6e..af167851 100644 --- a/port/scripts/boot.gd +++ b/port/scripts/boot.gd @@ -241,6 +241,8 @@ func _ready() -> void: if args.has("time"): _frozen = true + # An explicit instant beats the settle instant -- see `ScreenView.frozen`. + view.frozen = true view.time_units = float(args["time"]) * view.units_per_second view.queue_redraw() @@ -271,6 +273,7 @@ func _ready() -> void: _overlay_due = 0.0 _overlay_process(0.0) if overlay != null and args.has("time"): + overlay.frozen = true overlay.time_units = float(args["time"]) * overlay.units_per_second overlay.queue_redraw() diff --git a/port/scripts/screen_view.gd b/port/scripts/screen_view.gd index 95a0028f..2419a26e 100644 --- a/port/scripts/screen_view.gd +++ b/port/scripts/screen_view.gd @@ -77,6 +77,26 @@ var exit_ramp_units: float = 24.0 var settle_instant: float = -1.0 const SETTLE_WINDOW_MIN := 30.0 +## Set when a caller pinned an EXPLICIT instant (`--time=`), which then wins over +## `settle_instant`. +## +## 🔴 Without this, `--time=` was silently ignored on every screen with a settle +## window of 30 units or more, because `pose_at` overwrote the requested `t` with +## `settle_instant` whenever `holding` was true. The flag parsed, the log printed +## the time asked for, and the pose came from somewhere else. +## +## `press_start` is the case that exposed it. Its window is [0, 214] -- the long +## dead stretch BEFORE the plate appears -- so its settle instant is t=107, where +## `ptbtn00` is alpha 0. The plate's only opaque frames are t=236-238. The result +## was that the `PRESS (A)` plate could not be rendered **at any time at all**: +## every instant anyone asked for was answered at t=107, and the screen came back +## empty with `ptbtn00 (transparent at rest)`. +## +## The settle instant is still right for a screen that has ARRIVED and is sitting +## there, which is what it was measured for. It is not right as an answer to a +## question about a different instant. +var frozen := false + var looping_focus: Dictionary = {} ## Element ids whose nested `.rat` leaf the runtime actually draws. @@ -225,7 +245,9 @@ func pose_at(element: Dictionary, t: float) -> Dictionary: return frames[0] if not frames.is_empty() else element.get("rest", {}) # While holding, stop at the hold: past it the group is ramping out, and a # screen that has arrived and is sitting there is not leaving. - if holding: + # `frozen` means a caller pinned an EXPLICIT instant and wants THAT instant, + # not the settled pose and not a per-element clamp. Both clamps are skipped. + if holding and not frozen: # One instant for the whole screen where the disc gives a wide enough # window; otherwise each element's own hold, which is what this port did # everywhere until 2026-08-29. diff --git a/tools/port/verify-capture b/tools/port/verify-capture index a92bd518..16fdea6d 100755 --- a/tools/port/verify-capture +++ b/tools/port/verify-capture @@ -95,10 +95,36 @@ for row in "${MAP[@]}"; do "--shots=$OUT/$name" >"$OUT/$name.log" 2>&1 || true [ -f "$OUT/${name}_00_start.png" ] && cp "$OUT/${name}_00_start.png" "$OUT/$name.render.png" elif [ "$pose" = t357 ]; then - godot --path port --resolution 1280x720 -- "--screen=$name" --time=5.9617 \ + # 🔴 NO `--time` HERE EITHER, and the row's note used to claim otherwise. + # + # It passed `--time=5.9617` (t=357.7 units, the Decoder's refined sweep fit) + # and that value was NEVER APPLIED: `pose_at` replaced it with the screen's + # settle instant, t=198, on every run. Every title figure this tool has ever + # printed -- including the 0.26 % the port has quoted repeatedly -- was + # measured at the SETTLE, under a note saying t=357.7. + # + # Honouring it now makes that visible: t=357.7 is PAST the title's own group, + # which ends at t=269, so the whole screen poses at its faded-out final + # keyframes and the disagreement goes to 30.97 %. The instant was only ever + # meant for the `ptloop` LEAF, which runs to t=600 and is looped separately + # by `loop_leaf` (authored/rendering.json). Applying it to the whole screen + # was always wrong; it was harmless only while it was ignored. + # + # So: pose at the settle, which is what was actually being measured, and let + # the leaf loop carry the sweeps' phase. + godot --path port --resolution 1280x720 -- "--screen=$name" \ "--capture=$OUT/$name.render.png" >"$OUT/$name.log" 2>&1 || true else - godot --path port --resolution 1280x720 -- "--screen=$name" --time=99 \ + # NO `--time`. It used to pass `--time=99` as an idiom for "settled", and + # that worked only because `--time` was SILENTLY IGNORED on a screen with a + # settle window: `pose_at` overwrote the requested instant with + # `settle_instant` whenever `holding` was true. The tool asked for t=5940 + # units and was handed the settle instant, which is the pose it actually + # wants -- and the 0.01 % agreements on both splashes were measured through + # that accident. Now that `--time` is honoured, asking for it explicitly + # would pose past the end of every group, so the request is simply dropped + # and the settled pose asked for by omission. + godot --path port --resolution 1280x720 -- "--screen=$name" \ "--capture=$OUT/$name.render.png" >"$OUT/$name.log" 2>&1 || true fi [ -f "$OUT/$name.render.png" ] || { printf '%-17s %s\n' "$name" "render failed"; continue; } @@ -109,7 +135,7 @@ for row in "${MAP[@]}"; do note="" case "$name" in main_menu|extras) note="rendered with authored initial focus" ;; - title) note="posed at t=357.7, the Decoder's refined sweep fit" ;; + title) note="posed at the SETTLE (t=198); see the note in this file" ;; esac # Where the difference lives. This comes FIRST because it is what the gamma # sweep has to be protected from.