Found by inspection while the disc tests ran. 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 counted 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; the loading builds render afterwards at 4.0 percent non-black. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
4.8 KiB
✅ Option A implemented — the reference renderer rotates. ⚠️ It does not close the title.
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:
blitgained a rotated path.rotation_deg != 0draws 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 == 0keeps the original forward-mapped path byte for byte, so screens that do not rotate cannot regress.composedraws a nested.ratleaf 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.--at <units>/ComposeOptions::at, because the sweeps hold off-screen atx = 1521and 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.mdmeasures as the larger part of the level difference even where geometry is right; - our renderer still does not draw
ptlogo1/ptlogo2at 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 |