From 285635f542298d0e9d8105784ccaffd5498e580f Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sat, 29 Aug 2026 22:04:31 +0000 Subject: [PATCH] formats: test backdrop coverage per instant, and record where the .tbm hunt reached Two things, neither of which moves a verdict. The port pointed out that rejecting on DECLARED size replaces one error with its mirror: an element scaled ABOVE 100% could cover the screen from a smaller declared size, and my guard would silently exclude it. Checked against the disc first: across 921 keyless elements, ZERO cover the screen only via scale, so the mirror case does not occur here. Adopted anyway, because the construction does not need that to stay true -- coverage is now tested per instant against the scaled size, alongside the opacity test, since both animate on the same ramp. 80 forced instances before and after, split 42 .prm / 38 .tbm, unchanged. Second: an attempt to upgrade the 38 .tbm verdicts from inferred to decoded by finding the texture and measuring its alpha coverage. It cannot be located. Not in its bundle (no RATC record, no sprite-table entry, for any of the 13 names); not a file (no .tbm anywhere on the disc); not a pak entry (its archive's hashed TOC contains none of the name, its uppercase form, its stem, .t32/.tga/.xpr variants, or ui\\ and tex\\ prefixes, across four archives); and not visible in our composite, since compose skips an element with no resolvable sprite, so we draw no pixels for a .tbm at all -- and no committed capture covers a screen that has one. So a second reading survives and is recorded rather than excluded: a .tbm may contribute no pixels, in which case its paint position is INERT rather than correct. That leaves the 38 harmless instead of right -- a different claim with the same consequence. Distinguishing them needs a capture of GP_SAVE_LOAD, GP_BUNK or GP_DEBRIEFING_PILOTLOG, all behind the A fault. One upgrade: pfbase.tbm's first position is MEASURED, not inferred -- it is element 0 of the save/load frame and the order read off the running game starts [0, 1, 2, ...]. Twelve of the thirteen .tbm names still rest on the rule. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd --- crates/sylpheed-formats/src/ui_layout.rs | 28 ++++++++++++------- docs/re/structures/ui-forced-backdrop.md | 35 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 10 deletions(-) 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