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
118 lines
5.7 KiB
Markdown
118 lines
5.7 KiB
Markdown
# ✅ 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 |
|