Files
Sylpheed/docs/re/structures/ui-settle-time.md
sylph-decoder d98c8214cc re: the title's build-in measured in the guest's draw stream -- the flashes are real
The settle-time decode was confirmed only against a SETTLED frame, which shows
the end state is right and says nothing about whether the five flashes ever
happen. This runs the oracle: a draw capture armed before the title exists,
so the window contains the frames in which the screen is built.

The flashes fire in a six-frame window and are absent from all 155 other
sampled frames. `ptlogo_back2eff1` is drawn in exactly two frames at t = 54.0
against a decoded peak of t54-56; `ptlogo1` first appears at t = 42.2 against
a decoded t42. Units-per-frame was taken from the GLOW's period alone, a
different element, so the timings are not circular. The two holders are
continuous from frame 134.

The plate glow's quad carries a per-vertex colour whose alpha IS the element's
fade alpha, so the ramp is read straight out of the guest: observed range
0..80 against a decoded peak of 80, exact and unfitted; period 51.158
presented frames over 20 cycle starts. Fitting the decoded ramp gives RMS
13.16 alpha levels against 38.18 for the same ramp REVERSED -- if the shape
carried no information those would be equal, so the asymmetry is real and
correctly directed. Further controls: symmetric triangle 15.73, flat 31.13.

`ptlogo_back2eff3` was never drawn, and that is expected rather than a miss: a
2-unit flash peak is 0.85 of a presented frame, so catching one is a matter of
phase. A port drawing all five every time shows more sweep than the console.

METHOD.md gains the trap this cost: a 2D draw's identity is its vertex
geometry, not its bound texture. These sprites sample shared pages, and
matching texture dimensions produced a false negative (no flash is ever drawn)
and a false positive (the intro movie's 640x360 YUV planes read as `ptbase2`)
in the same pass.

Also records the top-level restriction on the settle window, which the port
raised and which is verified here: top-level [160,236] width 76, including the
`ptloop` leaves [269,540] width 271 -- an instant past the end of every
top-level element's timeline.

Evidence committed as a derived per-frame series, not the 7 MB raw log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 19:56:34 +00:00

8.6 KiB
Raw Blame History

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.

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

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, 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 eff1eff5 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

The 42 % 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 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 measures. The arc band is no longer where the error lives.

Reproducing

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