Files
Sylpheed/docs/re/structures/ui-settle-time.md
sylph-decoder 22846bf30e re: finish the --help audit -- a stale percentage and a missing noun, both shipped
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
2026-08-30 17:45:43 +00:00

187 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 t5456: [`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 t5456** → 0 by t58 | **255** | 0 |
| `ptlogo_back2eff2.t32` | a=0 → **255 at t5860** → 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 t6466 → 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 light arc: console, rest(), and --settle](../captures/title-builds/title-arc-rest-vs-settle.png)
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 95215, x 8301230). 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 25 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
```