The METHOD entry I wrote an hour ago says to read your tool's own --help as if a stranger wrote it. I had done that for ONE of sixteen leaf commands, which is the "a rule written down is not a rule applied" failure this corpus already records twice. Finished it across the whole surface. One survivor, and it fails in two ways at once. `screen render --settle` said a narrow window means the bundle never settles, "(42 % of them, mostly loop* fragments)". MISSING NOUN: inside `screen render`, "them" reads as the builds you would render. The 42 % is over composable bundles -- a different and much larger set including ~1 700 two-element fragments a user of that flag never renders. ui-settle-time.md states its population precisely; the help inherited the number without it. STALE: recomputed under the corrected reader, the composable figure is 862/2211 = 39 %, not 731/1758 = 42 %. The POPULATION GREW BY 453, which is the keyframe record-layout fix's signature -- it times a group's final pose, so bundles that previously showed one timed keyframe now show two and qualify. Third consequence of that fix not being swept, after fade_quads.py and screen-transitions.md's 0.87-4.08 s fade-in. And the share a --settle user actually faces is 38 %: 185 of 491 screen builds. Corrected in the help text with all three numbers and their populations, and in ui-settle-time.md, whose three-row table is marked pre-fix and superseded rather than edited in place. Verified by artifact -- the tool's --help output is quoted, not merely recompiled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
187 lines
9.6 KiB
Markdown
187 lines
9.6 KiB
Markdown
# The settled screen is one instant, not one hold per element
|
||
|
||
**Classification: decoded.** The value comes from the keyframe table alone —
|
||
no capture is consulted to compute it — and it is checked disc-wide. The
|
||
verification against the console capture is a *test* of the decode, not its
|
||
source.
|
||
|
||
## The claim
|
||
|
||
`Element::rest()` returns an element's last **hold** keyframe, chosen for that
|
||
element independently of every other element. A composite built from `rest()` is
|
||
therefore not a screen at any moment in time; it is a per-element maximum.
|
||
|
||
For an element that ends the screen settled, that is the same thing. For a
|
||
**transient** it is exactly wrong — a flash's last hold *is* the flash peak, so
|
||
`rest()` leaves it burning forever.
|
||
|
||
The settled screen is instead **one instant that every element is posed at**, and
|
||
the disc says which instant: gather every keyframe time in the build and take the
|
||
**longest interval containing none of them**. Inside that gap nothing has an
|
||
inflection, so every element is either holding or midway along a single linear
|
||
ramp. That is what "the screen has stopped changing" means, expressed in the only
|
||
vocabulary the file has.
|
||
|
||
`UiBuild::settle_time()` returns the midpoint of that interval;
|
||
`UiBuild::settle_window()` returns the interval, whose width is how much
|
||
confidence the midpoint deserves.
|
||
|
||
🔴 **Gather the times from the TOP-LEVEL elements only — not from nested leaf
|
||
records.** The port raised this after reproducing `[160, 236]` from its own
|
||
export, and it is worth stating because the implementation reads correctly either
|
||
way while only one is right. Including `GP_TITLE` build 4's `ptloop` leaves, whose
|
||
cycles run to 600 and 720, gives **`[269, 540]`** instead — a "settled instant"
|
||
that lies *past the end of every top-level element's timeline*, i.e. after the
|
||
screen has exited. Verified here: top-level `[160, 236]` width 76, with leaves
|
||
`[269, 540]` width 271. A leaf loops on its own clock and says nothing about when
|
||
the screen stops changing.
|
||
|
||
✅ **Confirmed against the running game.** The mechanism this page decodes — five
|
||
transient flashes that fire and vanish — is observed in the guest's own draw
|
||
stream, with `ptlogo_back2eff1` drawn in exactly two frames at t = 54.0 against a
|
||
decoded peak of t54–56: [`ui-title-buildin-measured.md`](ui-title-buildin-measured.md).
|
||
|
||
## The case that found it
|
||
|
||
`GP_TITLE` build 4. Seven elements share the light-arc band behind the logo:
|
||
|
||
| element | timeline | at rest | at t=198 |
|
||
|---|---|---|---|
|
||
| `ptlogo_back2eff1.t32` | a=0 → **255 at t54–56** → 0 by t58 | **255** | 0 |
|
||
| `ptlogo_back2eff2.t32` | a=0 → **255 at t58–60** → 0 by t62 | **255** | 0 |
|
||
| `ptlogo_back2eff3.t32` | staggered, same shape | **255** | 0 |
|
||
| `ptlogo_back2eff4.t32` | staggered, same shape | **255** | 0 |
|
||
| `ptlogo_back2eff5.t32` | 255 at t64–66 → 192 at t74 → **0 by t110** | **255** | 0 |
|
||
| `ptlogo_back2eff.t32` | 255 at t66, **holds to t238**, exits t244 | 255 | 255 |
|
||
| `ptlogo_back2.t32` | 255 at t80, **holds to t243**, exits t249 | 255 | 255 |
|
||
|
||
The five numbered ones are a single light sweep travelling left to right across
|
||
the logo, drawn as five staggered two-frame flashes. They are all extinguished by
|
||
t110. `rest()` draws them **simultaneously and permanently**, and five stacked
|
||
white glows (`ptlogo_back2eff5.t32` decodes to a mean opaque RGB of exactly
|
||
255,255,255) drive the arc to saturation.
|
||
|
||
## What it looks like
|
||
|
||

|
||
|
||
The console's arc is a **thin white outline with a pink hooked tail**. `rest()`
|
||
renders a fat solid white blob that swallows the tail completely — five white
|
||
glows stacked. `--settle` reproduces the console.
|
||
|
||
⚠️ A measurement trap worth recording: sampling the *brightest 3 %* of that band
|
||
gives (252,245,239) for the console against (255,255,255) for `rest()` — nearly
|
||
neutral, and it reads as "no hue difference". That statistic samples the white
|
||
outline in both and never touches the pink tail. What it *did* expose was the
|
||
count: at a 97th-percentile threshold the console has 1 459 pixels above it and
|
||
`rest()` has 8 581, i.e. a saturated plateau. **The pixel count carried the
|
||
signal that the mean colour hid.**
|
||
|
||
## The measurement
|
||
|
||
Against
|
||
[`live-title-build4-no-plate.png`](../captures/title-builds/live-title-build4-no-plate.png),
|
||
whole frame and over the arc band (y 95–215, x 830–1230). The third column counts
|
||
pixels at or above the band's 97th-percentile luminance — a *saturation* statistic,
|
||
independent of the error being minimised, and not fitted.
|
||
|
||
| | mean abs diff | arc band | clipped px |
|
||
|---|---|---|---|
|
||
| **console capture** | — | — | **1 459** |
|
||
| `rest()` — the default | 14.07 | 33.22 | 8 581 |
|
||
| **`--at 198` — predicted from the disc** | **12.06** | **11.79** | **1 452** |
|
||
| `--at 100` — control, before the window | 19.13 | 20.46 | 1 468 |
|
||
| `--at 358` — control, past every exit | 28.80 | 53.37 | 1 447 |
|
||
|
||
**t=198 was computed before the render was scored.** `settle_window()` on build 4
|
||
returns `[160, 236]`; 198 is its midpoint. A separate sweep of t in 40…300 finds a
|
||
flat optimum over t ∈ [180, 238] at 12.06 / 11.79 / 1 452, which contains the
|
||
prediction.
|
||
|
||
⚠️ The clipped-pixel count discriminates `rest()` from *any* single instant — the
|
||
two controls also land near 1 459 — so it identifies the **blow-out**, not the
|
||
time. The time is identified by the mean, and by the disc.
|
||
|
||
## The controls
|
||
|
||
* ✅ **`at = None` is byte-identical.** `compose` with no `at` produces the same
|
||
bytes before and after this change (`cmp`, exact). The default path is untouched.
|
||
* ✅ **The change did not regress the resting composite.** Rendering build 4 at
|
||
`formats-pin-2026-08-29c` (pre-rotation, pre-leaf) and at the current tree both
|
||
give **14.07 / 33.22**.
|
||
* ✅ **A hand-picked visibility list reaches the same answer.** Leave-one-out over
|
||
all 24 elements, then hiding exactly `eff1`…`eff5` and keeping the two holders,
|
||
gives 12.06 / 11.79 / 1 452 — identical to `--at 198`. The principled rule
|
||
reproduces the hand-picked one with nothing hand-picked.
|
||
* ✅ **Disc-wide self-consistency.** For every bundle with a window, no element has
|
||
a keyframe strictly inside it, and the midpoint lies within it —
|
||
`tests/ui_settle_time_disc.rs`.
|
||
|
||
## 🔴 Reach: this does not apply to every bundle
|
||
|
||
Of the **1 758** composable bundles carrying two or more keyframe times:
|
||
|
||
| | count | share |
|
||
|---|---|---|
|
||
| settle window ≥ 30 units (0.5 s) | 524 | 30 % |
|
||
| settle window < 10 units | 731 | 42 % |
|
||
| mean window | 49 units | — |
|
||
|
||
🔴 **These three rows are PRE-FIX and are superseded (2026-08-30).** They were
|
||
computed before the keyframe record-layout fix, which times a group's **final**
|
||
pose — so bundles that previously showed one timed keyframe now show two and enter
|
||
the population. Recomputed under the corrected reader
|
||
([`data/settle-narrow-rate.txt`](../data/settle-narrow-rate.txt)):
|
||
|
||
| population, ≥2 keyframe times | n | narrow (<10 u) | share |
|
||
|---|---|---|---|
|
||
| **screen builds** (`is_build` — what `screen render` renders) | 491 | 185 | **38 %** |
|
||
| composable bundles (`is_composable` — what `--all` admits) | **2 211** | 862 | **39 %** |
|
||
| *the pre-fix figures above* | *1 758* | *731* | *42 %* |
|
||
|
||
⚠️ The **population grew by 453**, which is the fix's signature and the reason the
|
||
share moved. And the share a user of `--settle` actually faces is **38 %**, over
|
||
screen builds — not 42 % over a wider set that includes ~1 700 fragments they will
|
||
never render. The tool's own help said "42 % of them" without saying of *what*;
|
||
corrected there too.
|
||
|
||
The narrow ones are mostly `loop*` animation fragments, which are **meant** to be in
|
||
motion and have no settled pose to find. **Check the window width before trusting
|
||
the midpoint.** A narrow window is the data saying "this bundle never settles",
|
||
not a settle time with a small error bar.
|
||
|
||
## What this corrects elsewhere in the corpus
|
||
|
||
* 🔴 [`ui-rotation-implemented.md`](ui-rotation-implemented.md) records the pose
|
||
scan as **"Flat. No minimum."** over t = 0…600. The cause is now known:
|
||
`ComposeOptions::at` was posing **leaves only**, so the scan moved the light
|
||
sweeps and never touched the top-level flashes. No `t` could have helped. `at`
|
||
now poses every element.
|
||
* 🔴 The same document blames the residual on our renderer **"not drawing
|
||
`ptlogo1` / `ptlogo2` at all (four elements)"**. That is **withdrawn**. Build 4
|
||
declares *six* ptlogo elements: indices 0 and 1 are kind `0x0` at (184,193) and
|
||
(137,308), alpha 255, and are **drawn**; indices 2–5 are kind `0x4` ghost
|
||
instances at (−116,−7) and (437,508), alpha 0, and are skipped deliberately.
|
||
Hiding element 0 makes the error **worse** by +5.20 whole-frame and +7.61 in the
|
||
band; element 1 by +7.47. They are drawn and correctly placed.
|
||
* ❔ The **10.92** baseline in that document's table is **not reproducible**.
|
||
`screen render --build 4 --black` against that capture gives 14.07 at the
|
||
pre-change tag and 14.07 now. Some element of that recipe was not recorded.
|
||
Conclusions resting on 10.92 — including the "1.7 % better" verdict on rotation
|
||
— should be treated as unverified until the recipe is recovered.
|
||
|
||
## What is not settled
|
||
|
||
❔ The remaining **12.06**. It is broad and level-like rather than localised,
|
||
which is consistent with the tone term that
|
||
[`title-residual-tone-vs-geometry.md`](title-residual-tone-vs-geometry.md)
|
||
measures. The arc band is no longer where the error lives.
|
||
|
||
## Reproducing
|
||
|
||
```bash
|
||
cargo run -q -p sylpheed-cli -- screen render --build 4 --black --at 198 \
|
||
"$SYLPHEED_DISC/dat/GP_TITLE.pak" /tmp/title.png
|
||
SYLPHEED_DISC=/disc cargo test -p sylpheed-formats --test ui_settle_time_disc
|
||
```
|