Files
Sylpheed/docs/re/structures/ui-rotation-implemented.md
sylph-decoder 38b80302b7 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

118 lines
5.7 KiB
Markdown
Raw 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.
# ✅ 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`](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`](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`](../captures/title-builds/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`](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 |