Files
Sylpheed/docs/re/structures/ui-forced-backdrop.md
sylph-decoder 159778faf1 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
2026-08-29 22:04:31 +00:00

235 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Where a keyless primitive paints, when the file forces it
**Classification: decoded.** Derived from the keyframes and the element's own
geometry, checked disc-wide, and validated against **both** primitives whose
position was measured in the running game — one it must reproduce, one it must
not disturb.
## The question
A `.prm` / `.tbm` primitive carries **no layer key**: the key is read from a
sprite's header, and a primitive has no sprite.
[`ui-prm-primitives.md`](ui-prm-primitives.md) established that the key is not in
the bundle either — the declaration entry's unread words are constant, and the
bundle has no RATC child for a primitive — so `implied_layer_key` is a **measured
per-name table**, and anything not in it keeps `u32::MAX` and sorts **last**.
"❔ Where an *unmeasured* primitive paints" has been that page's standing blocker.
The port hit it: `build_12` / `build_15` composited to solid black at every
instant of their declared life, because `pgloading_eff00.prm` — a full-screen
opaque quad — sorted on top.
## The rule
> **An element that covers the screen and is fully opaque at some instant cannot
> paint above anything visible at that instant.** Where the elements visible
> during its opaque span are *all* of them, its position is forced to first.
This is a constraint read off the file, not a preference, and it is not an
analogy to a neighbouring screen.
`pgloading_eff00.prm` is opaque for **39** instants, and **all 9** other elements
on the loading screen are visible inside that span. **Forced first**, 4/4
instances.
## The controls — the rule has to survive both, and it does
| primitive | measured in the game | opaque instants | forced below | rule says |
|---|---|---|---|---|
| `palogo_eff0.prm` (entry 11, developer) | **paints FIRST** | 211 | **6 of 6** | ✅ forced first |
| `palogo_eff0.prm` (entry 10/13, publisher) | **paints FIRST** | 256 | 2 of 2 | ✅ forced first |
| `pteff00.prm` (title) | **paints LAST** | 2 | 3 of 23 | ✅ permitted on top |
| `pteff00.prm` (menu) | **paints LAST** | 2 | 7 of 15 | ✅ permitted on top |
🔴 **The first row is the one that matters.** `palogo_eff0.prm` is named like an
overlay, and a rule keyed on the *name* would sort it last — against a measured
order. Occlusion gets it right. `pteff00.prm` is opaque only for two instants, at
its screen's entry and exit, so the constraint never binds it: it is the fade
cover, and it belongs on top.
## The span, and what it costs to get wrong
The rule quantifies over "every instant the primitive is opaque" and "every
element visible then", so it depends on where a screen's timeline ends and on what
an element does after its own last keyframe. The port asked, having got **256**
opaque instants for `palogo_eff0.prm` where this page said 211.
⚠️ **That pair was a bundle mismatch, not a definitional one** — `palogo_eff0.prm`
appears on both splashes, and the publisher (entries 10, 13) runs to t=255 while
the developer (11, 14) runs to t=210. 256 and 211 are both right, for their own
screen. The definitions already agreed.
**The definition:** the span is `0 ..= max keyframe time over every element in the
build`, and an element **holds its final pose** past its own last keyframe — which
is what `pose_at` does, and it is decoded rather than assumed: a group holds at its
last keyframe rather than looping
([`ui-keyframe-time-unit.md`](../ui-keyframe-time-unit.md)), and
[`ui-record-loop-length.md`](ui-record-loop-length.md) shows the declared length
never falls short of the last keyframe, the slack being exactly that hold.
**How much rests on it — 130 keyless full-screen primitives with an opaque
interval, and how many verdicts change:**
| alternative convention | verdicts changed |
|---|---|
| span = the bundle header's declared `+0x08` | **0** |
| span = the primitive's **own** last keyframe | 72 |
| elements counted **gone** after their last keyframe (no hold) | **72** |
🔴 **The hold decides 55 % of the verdicts, and dropping it is refuted by a
measured order.** `palogo_eff0.prm` is a *single* keyframe at t=0. Without the
hold it would be opaque for one instant, no other element would be up yet, and the
rule would call it **free** — against the order measured in the running game,
which paints it first. Pinned by
`the_hold_after_a_final_keyframe_is_required_by_a_measured_order`.
✅ **And the header length is interchangeable with the elements' maximum**: zero
disagreements disc-wide. Either may be used.
✅ **The verdicts that matter are convention-independent.**
`pgloading_eff00.prm` comes out **first** under all four conventions;
`pteff00.prm` comes out **free** under all four. Only `palogo_eff0.prm` moves, and
only under the convention its own measured order rules out.
## Disc-wide
Keyless **full-screen** primitives with an opaque interval:
| | count |
|---|---|
| position **forced first** | **80** |
| constrained below some, not all | 50 |
| occluding nothing | 0 |
The split falls almost exactly along the names — every `*base*` is forced, every
`*eff00*` is not — with three families crossing it: `palogo_eff0.prm`,
`pgloading_eff00.prm` and `pzeff00.prm` are named like overlays and are forced
first. **That is precisely why the name is not the rule.**
✅ **And it explains a symptom the corpus had recorded without a cause.**
`ui-prm-primitives.md` notes 36 builds that "come out one colour ... wiped by
`pzeff00.prm` and `pceff00.prm`, whose positions have never been measured".
`pzeff00.prm` is forced first in **32 of 32** instances. Those builds were wiped by
our own sort, not by the game.
## 🔴 The rule's real limit, found by its own test
An earlier version applied to any element. Its disc-wide test asserted that no
*keyed* element is ever forced — and that assertion failed, on **22** of them:
`pneff01.t32` (key `0xd850`, paints #8 of 13) and `pbfriendly.t32` (key `0x9230`,
#17 of 49).
Both are `.t32` **sprites**, and that is the flaw: a sprite's *element* alpha
being 255 says nothing about whether its **texture** covers the screen. Most of it
may be transparent. The disagreements were the rule overreaching, not the keys
being wrong.
`forced_backdrop` is now restricted to elements with **no sprite** — untextured
primitives, which are solid quads and do occlude what they cover. That is also the
only case `derived_paint_order` consults it for.
## 🔴 Self-refutation: the argument is sound for only 42 of the 80
A disc-wide census of the *colour* these elements carry breaks the rule's premise
for nearly half its verdicts.
| the 80 forced-first instances | count | fade ARGB |
|---|---|---|
| `.prm` — untextured solid quads | **42** | pure black (`ff000000`, `7f000000`, `40000000`, …) |
| `.tbm` | **38** | **`ffffffff`** — white at full alpha |
**A solid white quad at alpha 255 painted first would make the screen white.** No
screen is white. So a `.tbm` is not a solid quad: `ffffffff` is a white
*modulation* on a texture, which is exactly what a background bitmap carries.
🔴 **And that means element alpha does not establish coverage for them** — the same
error the `.t32` guard already caught, one file extension further out. I fixed that
symptom (`el.sprite.is_some()`) rather than its cause: **an element's alpha is not
its texture's opacity, and only an untextured primitive makes the two the same
thing.**
**What this does and does not change:**
* ✅ 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
is a name-and-role argument, which this page elsewhere argues is the weaker kind.
* ⚠️ **The code is deliberately unchanged.** Restricting `forced_backdrop` to
`.prm` would send `pcbase`, `pnbase`, `pqbase`, `pubase`, `pvbase`, `pjbgbase2`,
`po_menu_base` and the four `px_*_base` back to `u32::MAX` — last — which is the
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
[`ui-prm-primitives.md`](ui-prm-primitives.md): an *additive* quad at alpha 255
would not occlude, and the rule would then be placing it wrongly. The
`palogo_eff0.prm` control is evidence the assumption holds at least there.
✅ **The colour census narrows this a long way.** Every full-screen `*eff00*`
primitive on the disc is **pure black** at its various alphas — `ff000000`,
`7f000000`, `40000000`, `b2000000`, `cc000000`, `d4000000`, `00000000`. Black at
alpha *a* over content is exactly what an alpha-over dim or fade looks like, and an
*additive* black quad would be a no-op, so a designer would not author one. The
single non-black primitive on the disc is **`pbafc.prm`**, RGB `00e8e0` (cyan) at
alphas up to `ff` — and it is **844×600, not full-screen**, so it is outside this
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
still a *measured* per-name entry, not a decoded one.
⚠️ **No new oracle measurement.** The two controls are orders measured previously;
nothing here was captured from a running game. A draw capture of a loading screen
would confirm it directly, and the loading screens are not reachable from the
title path.
## Reproducing
```bash
cargo run -p sylpheed-formats --example prm_forced_first
cargo run -p sylpheed-formats --example prm_occlusion_check
SYLPHEED_DISC=/disc cargo test -p sylpheed-formats --test ui_forced_backdrop_disc
```