diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 4b4424c3..9267eaee 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -169 sections. Search this before re-deriving anything. +170 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) @@ -180,6 +180,7 @@ dies, which is what this file is for. * [Re-deriving `black_hold_units` against four measurements, not three](#re-deriving-black_hold_units-against-four-measurements-not-three) * [🔴 CORRECTION: my "the eras render identically" measurement was void](#correction-my-the-eras-render-identically-measurement-was-void) * [🔴 CORRECTION: my branch *is* the stale era, and the reference binary was never the workspace build](#correction-my-branch-is-the-stale-era-and-the-reference-binary-was-never-the-workspace-build) +* [`exit_ramp_units`: the refuted constant was living in a default](#exit_ramp_units-the-refuted-constant-was-living-in-a-default) ## P0 — the exporter, 2026-08-28 @@ -9707,3 +9708,54 @@ plateau **flat to 1.2 RMSE across 105 units**, so the 16.7 era margin is ~14× t flatness and decisive, while **settle-vs-rest at 1.5 is inside it**. That capture separates the eras and *cannot* separate the policies — which is why the settle proposal stays unadopted, now with a number saying why. + +## `exit_ramp_units`: the refuted constant was living in a default + +`ScreenView.exit_ramp_units` defaulted to **24.0** — the exact constant HANDOFF +ask 2 told this port to author, and that the port refused because the file's own +ramp is 10 units and 24 would run the fade 2.4× too long. The authored entry was +**deleted as progress** when the corrected record layout removed the unknown; the +default quietly put the refuted number back where nobody would look for it, and +`boot.gd`'s `timing.get("exit_ramp_units", 24.0)` made the deletion a no-op. + +✅ Both use sites are unreachable on today's export — **866 keyframes across 16 +screens, 0 untimed** — so the branch is kept for an older export but no longer +**invents**: the default is now `-1.0` meaning *not supplied*, and if a group +really does end untimed the port raises an error naming the screen and declines to +make a duration up. Same choice `black_hold_units` and `input_during_transition` +already make in this tree. + +### 🔴 My first verification was confounded, and it accused the change + +Before/after renders of four screens: `title` and `press_start` byte-identical, +**`main_menu` 641 941 px changed and `extras` 226 009** — 70 % of the frame, on a +change that raised no error and whose branch cannot execute. + +The cause was not the edit. **`--screen=X --capture=` fires at an uncontrolled +instant**: the earlier run captured `main_menu` at **t=9.00**, the later one at +**t=8.00**. One keyframe unit apart, mid-build-in, is most of the picture. Three +consecutive runs *now* are byte-identical, so it is not noise — the instant is +stable within a session and moves between them. + +✅ Re-run with the instant pinned (`--time=1.0`), old code against new: +**byte-identical on all four screens.** The change is a no-op, as the keyframe +census said it must be. + +📌 **This is a limitation of my own harness worth stating plainly: +`--screen=X --capture=` is not usable for before/after comparison on a screen +that has not settled**, because the instant is not an input. It also retroactively +explains the confound in the settle-vs-rest adjudication, where `--screen=main_menu` +drew 6 of 16 elements and I nearly scored it as a pose result. + +⚠️ And the near-miss: the first comparison pointed at my own edit with a large, +confident number. Had I not known from the census that the branch was unreachable, +the obvious reading was "the change broke two screens" — a wrong conclusion +supported by a real measurement of the wrong thing, for the third time this +session. + +### Correcting my own overstatement + +Last iteration I said the era guard "closes that for `verify-screen` only, not for +the other tools that call the CLI". ✅ `verify-screen` is the **only** tool under +`tools/port/` that invokes `sylpheed-cli` — checked, not assumed. The guard covers +every caller there is. diff --git a/port/scripts/boot.gd b/port/scripts/boot.gd index f7f03bb5..fe79382f 100644 --- a/port/scripts/boot.gd +++ b/port/scripts/boot.gd @@ -200,7 +200,12 @@ func _ready() -> void: # give a synthetic time to. The default below is now unreachable rather than # authored, and both of ScreenView's uses are dead branches kept only so an # older export still loads. - view.exit_ramp_units = float(timing.get("exit_ramp_units", 24.0)) + # ⚠️ The fallback is -1.0, NOT 24.0. It was 24.0 -- the constant HANDOFF ask 2 + # told this port to author and that it refused -- so deleting the authored + # entry as progress silently reinstated the refuted number as a default. + # Negative means "not supplied": ScreenView then declines to invent a duration + # and says so, rather than making one up. See ScreenView.exit_ramp_units. + view.exit_ramp_units = float(timing.get("exit_ramp_units", -1.0)) _black_hold = float(timing.get("black_hold_units", 0.0)) # Which focus records draw unconditionally and loop. Kept out of ScreenView's # own logic on purpose -- see `looping_focus` there for the census that says diff --git a/port/scripts/screen_view.gd b/port/scripts/screen_view.gd index 4b7f43ac..acd60090 100644 --- a/port/scripts/screen_view.gd +++ b/port/scripts/screen_view.gd @@ -40,7 +40,23 @@ var units_per_second: float = 60.0 ## Duration of the ramp into the final, untimed keyframe -- the screen playing ## itself out. Authored (`authored/timing.json`): the disc has no time slot on ## that keyframe, so this is the one unknown duration per screen. -var exit_ramp_units: float = 24.0 +## Synthetic duration for a group's final UNTIMED keyframe. +## +## 🔴 NEGATIVE MEANS "NOT SUPPLIED", AND THAT IS NOW THE DEFAULT. It used to +## default to **24.0** -- the exact constant HANDOFF ask 2 told this port to +## author and that the port refused, because the file's own ramp is 10 units and +## authoring 24 would run the fade 2.4x too long. The authored entry was deleted +## as progress when the corrected record layout removed the unknown; the default +## quietly put the refuted number back where nobody would look for it. +## +## The branch is kept so an older export still loads, but it no longer INVENTS a +## duration: if a group really does end untimed, the port says so and declines to +## make one up, which is the same choice `black_hold_units` and +## `input_during_transition` make in `authored/timing.json`. +## +## Unreachable on today's export -- 866 keyframes across 16 screens, 0 untimed. +var exit_ramp_units: float = -1.0 +var _warned_untimed := false ## Focus records this screen draws unconditionally, and the period each loops on. ## @@ -290,9 +306,17 @@ func pose_at(element: Dictionary, t: float) -> Dictionary: # was measured and refuted -- see authored/timing.json. var last_frame: Dictionary = frames[frames.size() - 1] if not last_frame.has("t"): - var exit_frame := last_frame.duplicate() - exit_frame["t"] = float(timed[timed.size() - 1]["t"]) + exit_ramp_units - timed.append(exit_frame) + if exit_ramp_units < 0.0: + if not _warned_untimed: + _warned_untimed = true + push_error("%s has an untimed final keyframe and no exit_ramp_units was supplied. " + % [screen.get("name", "?")] + + "Not inventing one: the group ends at its last timed frame. " + + "This export predates the corrected record layout -- re-export it.") + else: + var exit_frame := last_frame.duplicate() + exit_frame["t"] = float(timed[timed.size() - 1]["t"]) + exit_ramp_units + timed.append(exit_frame) if t <= float(timed[0]["t"]): return timed[0] @@ -420,7 +444,7 @@ func exit_time() -> float: for k: Dictionary in frames: if k.has("t"): timed_end = maxf(timed_end, float(k["t"])) - if not frames[frames.size() - 1].has("t"): + if not frames[frames.size() - 1].has("t") and exit_ramp_units >= 0.0: timed_end += exit_ramp_units last = maxf(last, timed_end) return last