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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
This commit is contained in:
sylph-decoder
2026-08-29 22:04:31 +00:00
parent 29c99e1b3e
commit 159778faf1
2 changed files with 53 additions and 10 deletions

View File

@@ -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<u32> = (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();

View File

@@ -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