From e2640338e149acc903fac551f400a5319950afb5 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sat, 29 Aug 2026 19:04:03 +0000 Subject: [PATCH] formats: a scale-0 leaf must not claim the draw and blank its parent 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) Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd --- crates/sylpheed-formats/src/ui_layout.rs | 9 +++++ docs/port/HANDOFF.md | 40 +++++++++++++++++++ docs/re/structures/ui-rotation-implemented.md | 12 ++++++ 3 files changed, 61 insertions(+) diff --git a/crates/sylpheed-formats/src/ui_layout.rs b/crates/sylpheed-formats/src/ui_layout.rs index 5cc3faa3..3cba09e9 100644 --- a/crates/sylpheed-formats/src/ui_layout.rs +++ b/crates/sylpheed-formats/src/ui_layout.rs @@ -1158,6 +1158,15 @@ pub fn compose_with_order( let lsp = le.sprite.clone().unwrap_or_else(|| le.name.clone()); let Some(&(so, ss)) = build.sprites.get(&lsp) else { continue }; let Some(limg) = t8ad::parse(&bundle[so..so + ss]) else { continue }; + // 🔴 Only count it as drawn if it CAN draw. `blit` returns early + // on a zero scale — "collapsed to nothing", not "unset" — so + // setting the flag unconditionally would let a scale-0 leaf + // suppress its parent and blank the element outright. + // `pgloading_loop5`'s leaf is scale (0,0), and scale-0 is one of + // the failures this corpus is already named for. + if lk.scale_x == 0 || lk.scale_y == 0 { + continue; + } blit(&mut canvas, w, h, &limg, &lk, le.pivot_x, le.pivot_y); any = true; } diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 760f6870..2676c2af 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -835,6 +835,46 @@ so the **ratio** is the claim, not the absolute level. And our render draws the sweeps' parent only, so the geometry share here covers both the missing rotation and the missing leaf placement; your leaf fix has already closed part of it. +## ✅ 2026-08-29 — OPTION A IS DONE. The reference renderer rotates. + +**The human chose Option A.** `sylpheed-formats` now draws `rotation_deg`, so +Reborn's renderer and yours stay comparable and `verify-screen` keeps meaning +*"someone is wrong"* — [`ui-rotation-implemented.md`](../re/structures/ui-rotation-implemented.md). + +Three pieces, because rotation alone does nothing on the title: + +1. **`blit` has a rotated path** — inverse-mapped over the rotated bounding box, + turning about the pivot. ✅ `rotation_deg == 0` keeps the old forward-mapped + path **byte for byte**. +2. **`compose` draws a nested `.rat` leaf when it carries geometry the parent + does not.** ⚠️ Not a blanket rule — a button's leaf duplicates its parent and + the parent still wins, exactly as your `screen.rs` had it. +3. **`--at `** on `screen render`, and `ComposeOptions::at`. + +🔴 **A trap worth taking, because your `pose_at` can hit it too: `at` poses +LEAVES ONLY.** A top-level group's final keyframes are its **exit ramp**, and +`rest()` deliberately stops at the last hold keyframe before it. Posing the title +globally at t=358 walked every parent into its exit and drove the disagreement +from **10.92 to 61.74**. + +🔴 **And the verification did not show what it was meant to — you should have +this before you re-run anything.** Scanning the pose time against +`live-title-build4-no-plate.png`: **10.73 – 11.17** against a **10.92** baseline. +**Flat, no minimum.** Drawing the sweeps correctly does not measurably improve +that comparison. + +Two things explain it: the whole-frame mean is dominated by the tone curve, and +Reborn **still does not draw `ptlogo1` / `ptlogo2` at all** — four elements +reported as "not drawn", a far larger spatial gap than two translucent sweeps. + +⚠️ **So do not expect your 1.81 % to move much on this alone.** Your harness +poses deliberately and counts differing *pixels* rather than mean level, so it is +the place to judge it — but I would rather you knew my measurement was flat than +discovered it after re-exporting. ❔ The sweeps may simply be a small term, and +`ptlogo1`/`ptlogo2` may be the bigger one. + +✅ No regression: `main_menu` unchanged at 9.26, 116 lib tests pass. + ## Status | | Question | State | Answer / link | diff --git a/docs/re/structures/ui-rotation-implemented.md b/docs/re/structures/ui-rotation-implemented.md index 8acc3374..5c22b547 100644 --- a/docs/re/structures/ui-rotation-implemented.md +++ b/docs/re/structures/ui-rotation-implemented.md @@ -82,6 +82,18 @@ pixels differing** is **not established here** — that harness poses deliberate 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 |