diff --git a/crates/sylpheed-formats/src/ui_layout.rs b/crates/sylpheed-formats/src/ui_layout.rs index 41e86ce0..a789d5cf 100644 --- a/crates/sylpheed-formats/src/ui_layout.rs +++ b/crates/sylpheed-formats/src/ui_layout.rs @@ -960,13 +960,9 @@ pub fn forced_backdrop(build: &UiBuild, el: &Element) -> bool { if el.sprite.is_some() { return false; } - // ⚠️ The declared size is the pivot doubled, but SCALE modifies it, and the - // disc proves the difference matters: `pbafc.prm` has a 844x600 pivot and a - // scale of 2 %/3 %, so it draws about 17x18 px. Checking the pivot alone - // would call it full-screen. No current verdict depends on this — all 80 - // forced instances are at 100 % on every opaque instant — so this is a guard - // against data that has not been met, not a fix. - if (el.pivot_x * 2) < build.design_w as u32 || (el.pivot_y * 2) < build.design_h as u32 { + // The element must have a declared size at all; coverage itself is tested + // per instant below, against the SCALED size. + if el.pivot_x == 0 || el.pivot_y == 0 { return false; } let tmax = build @@ -978,12 +974,24 @@ pub fn forced_backdrop(build: &UiBuild, el: &Element) -> bool { if tmax == 0 { return false; } - // Opaque AND at full size: a quad scaled down does not cover what it would - // cover at 100 %, so it cannot occlude on the strength of its pivot. + // An instant counts only where the element is BOTH fully opaque AND actually + // covering — tested together, because both animate on the same ramp. + // + // ⚠️ Coverage is the SCALED size, not the declared one, and the test must be + // two-sided. A quad scaled down does not cover what its pivot suggests — + // `pbafc.prm` declares 844x600 and draws ~17x18 at 2 %/3 %. And a quad scaled + // *up* can cover from a smaller declared size, so rejecting on the declared + // size would replace one error with its mirror. Checked before adopting: + // across 921 keyless elements, **0** cover the screen only via scale, so the + // mirror case does not occur on this disc — the per-instant test is in + // because it does not need that to stay true. + let (dw, dh) = ((el.pivot_x * 2) as u64, (el.pivot_y * 2) as u64); let opaque: Vec = (0..=tmax) .filter(|&t| { el.pose_at(t).map_or(false, |k| { - k.fade >> 24 == 255 && k.scale_x >= 100 && k.scale_y >= 100 + k.fade >> 24 == 255 + && dw * k.scale_x as u64 / 100 >= build.design_w as u64 + && dh * k.scale_y as u64 / 100 >= build.design_h as u64 }) }) .collect(); diff --git a/docs/re/structures/ui-forced-backdrop.md b/docs/re/structures/ui-forced-backdrop.md index 99d5c6fb..bb8ab864 100644 --- a/docs/re/structures/ui-forced-backdrop.md +++ b/docs/re/structures/ui-forced-backdrop.md @@ -153,6 +153,10 @@ thing.** * ✅ The **42 `.prm`** verdicts stand as decoded. For a solid colour quad the fade *is* the pixel, so opacity and coverage are the same fact. +* ✅ **One of the 13 `.tbm` names is measured, not inferred.** `pfbase.tbm` is + element 0 of the save/load frame, and the order read off the running game + (`the_save_load_screens_match_what_the_running_game_paints`) starts `[0, 1, 2, + …]` — the game paints it first. The other twelve names rest on the rule. * 🟡 The **38 `.tbm`** verdicts are **not** decoded. They are almost certainly still right — every one is named `*base*`, is full-screen, and one of them (`pfbase.tbm`) has its first position **measured in the running game** — but that @@ -163,6 +167,29 @@ thing.** blank-screen bug this rule was written to fix. Downgrading their *status* is honest; reverting their *position* would be wrong. +## ❔ Where a `.tbm`'s pixels live — looked, not found + +If a `.tbm`'s texture could be decoded, its alpha coverage would settle the 38 +directly. It cannot be located: + +* **not in its bundle** — no RATC record and no sprite-table entry, for any of the + 13 names; +* **not a file** — there is no `.tbm` anywhere on the disc; +* **not a pak entry** — its own archive's hashed TOC contains none of `pfbase.tbm`, + its uppercase form, its stem, `.t32`/`.tga`/`.xpr` variants, or `ui\`/`tex\` + prefixes, across four archives; +* **not visible in our composite** — `compose` skips an element with no resolvable + sprite, so our renderer draws **no pixels at all** for a `.tbm`, and no committed + capture covers a screen that has one (`GP_SAVE_LOAD`, `GP_BUNK`, + `GP_DEBRIEFING_PILOTLOG`), so nothing says whether the game draws any either. + +⚠️ **So a second reading survives and is not excluded**: that a `.tbm` element +contributes no pixels, in which case its paint position is *inert* rather than +wrong. That would leave the 38 verdicts harmless instead of correct — a different +claim with the same consequence. Distinguishing them needs a capture of a screen +that carries one, which is behind the Ⓐ fault +([`ui-clock-freezes-at-settle.md`](ui-clock-freezes-at-settle.md)). + ## Reach ⚠️ **Assumes straight alpha-over blending.** Blend mode is ❔ on @@ -180,6 +207,14 @@ alphas up to `ff` — and it is **844×600, not full-screen**, so it is outside rule's geometry guard entirely. ❔ Whether *it* is additive is still open, and it is now the only candidate. +✅ **Coverage is tested per instant, against the SCALED size, and two-sidedly.** +An earlier version rejected on the *declared* size (pivot doubled), which the port +pointed out replaces one error with its mirror: an element scaled **above** 100 % +could cover the screen from a smaller declared size. Checked across **921** keyless +elements: **0** do, so the mirror case does not occur on this disc — the per-instant +test is in because it does not need that to stay true. Verdicts before and after: +80, split 42 `.prm` / 38 `.tbm`, unchanged. + ⚠️ **It gives a lower bound, not an ordering.** It settles the 80 instances where occlusion forces the position, and says nothing about the 50 where the primitive is opaque only part of the time — including `pteff00.prm`, whose place on top is