Files
Sylpheed/docs/re/structures/ui-rotation-implemented.md
sylph-decoder 6f4ba93c5c re: the title's light arc is five transient flashes, not a tone or rotation error
Records the settle-time decode, and corrects two claims it overturns.

docs/re/structures/ui-settle-time.md is the finding: a settled screen is one
instant every element is posed at, the disc names it (the midpoint of the
longest keyframe-free interval), and the title's arc closes from 33.22 to
11.79 with the clipped-pixel count landing on the console's 1459 within 0.5%.
Includes the disc-wide reach -- only 30% of bundles have a window wide enough
to trust -- and a three-way figure.

Withdrawn in ui-rotation-implemented.md:

  * "Flat. No minimum." was not a property of rotation. `at` posed LEAVES ONLY,
    so the scan moved the light sweeps and never touched the top-level flashes.
  * "our renderer does not draw ptlogo1/ptlogo2 at all" is wrong. Both are
    drawn; the four elements the diagnostic named are kind-0x4 ghosts sharing
    their template's name. Hiding the real ones makes the error WORSE by
    +5.20 and +7.47.
  * Its 10.92 baseline is not reproducible -- 14.07 at its own pre-change tag
    and 14.07 today -- so the "1.7% better" verdict rests on an unrecorded
    recipe. Flagged in title-residual-tone-vs-geometry.md too.

METHOD.md gains two traps: a shared CARGO_TARGET_DIR makes a `git worktree`
build silently replace the binary you run next (it cost three renders here,
caught only because a missing flag was a hard error rather than a wrong
number); and an aggregate computed per-element is not a state of the system.

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

5.7 KiB
Raw Blame History

Option A implemented — the reference renderer rotates. ⚠️ It does not close the title.

🔴 Two claims below are withdrawn (2026-08-29), and the baseline is unreproducible. See ui-settle-time.md.

  • "Flat. No minimum." is explained, not a property of rotation: 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 and the scan has a clear optimum.
  • "our renderer still does not draw ptlogo1 / ptlogo2 at all" is wrong. Both are drawn. The four elements the diagnostic reported are kind 0x4 ghost instances, skipped deliberately. Hiding the real ones makes the error worse.
  • The 10.92 baseline is not reproducible — the same command gives 14.07 at this document's own pre-change tag and 14.07 today — so the "1.7 % better" verdict rests on a recipe that was not recorded.

Decision: the human chose Option A (2026-08-29) — teach sylpheed-formats' own renderer to draw rotation_deg, so it and the port stay comparable and verify-screen keeps meaning "someone is wrong".

Status: implemented and controlled. 🔴 and it does not measurably improve the title against the capture we hold — reported here rather than quietly, because the improvement was the reason for doing it.

What changed

Three pieces, because rotation alone does nothing on the title:

  1. blit gained a rotated path. rotation_deg != 0 draws by inverse mapping over the rotated bounding box; forward-mapping a rotation leaves gaps. Rotation turns about the element's pivot, whose absolute position (kf.x + pivot_x, kf.y + pivot_y) is invariant under scale. rotation_deg == 0 keeps the original forward-mapped path byte for byte, so screens that do not rotate cannot regress.
  2. compose draws a nested .rat leaf when the leaf carries geometry the parent does not — the title's sweeps are exactly that case (parent fixed at (441,270) scale 100 %, leaf holding 600 %/800 % and ±30°/45°). ⚠️ Not a blanket rule: a button's leaf duplicates its parent and the parent wins (ui-leaf-vs-parent-alpha.md), so the leaf is used only when its pose genuinely differs. ⚠️ A leaf element resolves no sprite of its own — names resolve against the bundle a build was parsed from, and a leaf is parsed from its own slice. Its name is the sprite name, looked up in the parent bundle's table.
  3. --at <units> / ComposeOptions::at, because the sweeps hold off-screen at x = 1521 and a resting composite therefore omits them.

🔴 A trap this found, and it cost a wrong number first

Posing everything at one global time is wrong. A top-level group's final keyframes are its exit ramp — the fade-out played when the screen leaves — and rest() deliberately stops at the last hold keyframe before it. Posing the title at t=358 walked every parent into its exit and drove the disagreement from 10.92 to 61.74.

at therefore poses leaves only; top-level elements keep rest(). That follows the decoded rule directly: the leaf runs on its own timeline and the parent's does not gate it.

The controls

0° and 360° vs the unrotated path byte-identical
90° on a 10×4 sprite extents swap to 4×10
covered area under rotation conserved to < 15 %
centroid under rotation stays on the pivot (< 1 px)

rotation_control_known_angles pins all four. 116 lib tests pass.

🔴 The verification, which did not show what it was meant to

Rendering the title against live-title-build4-no-plate.png and scanning the pose time:

mean abs difference
before (rest, no leaves, no rotation) 10.92
after, scanned t = 0 … 600 10.73 11.17
best (t = 420) 10.73 — 1.7 % better

Flat. No minimum. Drawing the sweeps correctly does not measurably improve this comparison, and two things explain why without rescuing it:

  • the whole-frame mean is dominated by the tone curve, which title-residual-tone-vs-geometry.md measures as the larger part of the level difference even where geometry is right;
  • our renderer still does not draw ptlogo1 / ptlogo2 at all (four elements, reported as "not drawn"), and that is a far larger spatial gap than two translucent sweeps.

⚠️ So the honest claim is narrow: rotation is implemented and correct in isolation, and no screen regressed. Whether it closes the port's 1.81 % of pixels differing is not established here — that harness poses deliberately and counts differing pixels rather than mean level, and it is the place to judge it. The sweeps may simply be a small term.

🔴 A bug in this change, found by inspection before the tests found it

The leaf branch set its "something was drawn" flag unconditionally after calling blit — but blit returns early on a zero scale ("collapsed to nothing", not "unset"). So a scale-0 leaf would have been treated as drawn, its parent skipped, and the element blanked outright.

⚠️ pgloading_loop5's leaf is scale (0, 0), so this was live on all four loading screens, and scale-0 is one of the failures this corpus is already named for. Fixed by skipping a zero-scale leaf pose before it can claim the draw. Loading builds render afterwards at 4.0 % non-black, unchanged in character.

No regression

screen before after
main_menu 9.26 9.26
extras 9.75