sylpheed-port proposed that a screen declaring a full-screen .prm at t=0 with fade == 0xff000000 is standalone, and one without it is composited: 12/4 across their sixteen exported screens, every exception independently known to be composited. They asked for it against archives they do not have. That is my lane. CONTROL: the predicate reproduces their split exactly. GP_TITLE's sixteen bundles give 12 with and 4 without, the four without being entries 0, 1, 2, 3 -- build_00, build_01, press_start, press_start_jp -- and the element names match (pteff00.prm, palogo_eff0.prm, pgloading_eff00.prm). Independent derivation from the disc, not a re-run of their tool. DISC-WIDE it is rare: 76 of 965 screen builds, 7.9 %. GP_STAGE_CLEAR 4/4, GP_SYSTEM 2/2 and GP_TUTORIAL 2/2 are all-yes; GP_HANGAR_ARSENAL is 0 of 390, and GP_READY_ROOM, GP_OPTIONS, GP_PAUSE_MENU and GP_GAMEOVER are all zero. So it is not a general standalone/composited test. GP_OPTIONS and GP_PAUSE_MENU are screens a player plainly sees as screens and declare no backdrop; read as "composited" the rule would make 92 % of the game's screens composited, which the archives do not support. What it appears to separate is narrower: screens that BEGIN FROM BLACK from everything else. A pause menu over gameplay, a hangar over a 3D scene and a plate over a title all lack a backdrop without being the same kind of thing -- the negative class is heterogeneous, which is what a two-way rule cannot express. For the port: exact within GP_TITLE, so --black for those twelve is justified from the file rather than assumed; do not carry it into the four archives they have yet to export, where in three of them it classifies every screen alike. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
517 lines
27 KiB
Markdown
517 lines
27 KiB
Markdown
# 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.
|
||
|
||
🔴 **REFUTED 2026-08-30 — a `.tbm` DRAWS, and the capture is now in hand.**
|
||
|
||
This paragraph used to say a second reading survived: that a `.tbm` contributes no
|
||
pixels, so its paint position is *inert* rather than wrong, leaving the verdicts
|
||
harmless instead of correct. **That reading is dead.**
|
||
|
||
The `TUTORIAL` screen was reached in the running game and captured
|
||
([`live-tutorial-screen.png`](../captures/title-builds/live-tutorial-screen.png)).
|
||
It carries a **full-screen blue circuit/hex background**. `GP_TUTORIAL` build 0 has
|
||
18 elements, and **element 0 is `pubase.tbm` with pivot (640, 360)** — 1280×720,
|
||
the only full-screen *textured* element in the bundle. The one other full-screen
|
||
element is `pueff00.prm`, an untextured primitive, and this page's own colour census
|
||
says every full-screen `*eff00*` primitive is **pure black**.
|
||
|
||
Our render of the same build
|
||
([committed beside it](../captures/title-builds/render-tutorial-build0-for-comparison.png))
|
||
is the **identical layout** — same title, same seven items, same footer — on **pure
|
||
black**: 6.0–6.4 % of the frame inked against the game's 99.7 %. The only difference
|
||
is the background, and the only thing that can be is the `.tbm`.
|
||
|
||
✅ **So the `.tbm` verdicts are CORRECT, not merely harmless.** They are load-bearing
|
||
in the full sense, and if the alpha-over assumption under this rule ever fails, those
|
||
24 deciders go with it for real.
|
||
|
||
⚠️ **Reach: one `.tbm`, `pubase.tbm`.** The other families —
|
||
`px_deb_base`, `px_bunk_base`, `pvbase`, `pqbase`, `pnbase`, `pjbgbase2`, `pcbase`,
|
||
`px_replay_base`, `px_movie_base`, `px_mission_base` — are not individually
|
||
observed. What is settled is the *class* question this paragraph posed: a `.tbm` is
|
||
not an element that draws nothing.
|
||
|
||
🔴 **And it means `screen render` is WRONG on every screen carrying a `.tbm`** — it
|
||
omits the background silently, with no diagnostic. That is a limitation of the
|
||
reference renderer, not of the game.
|
||
|
||
## 🔴 Stability is not necessity — 62 of the 80 are DECIDED by this rule (2026-08-30)
|
||
|
||
**Raised by the port agent, and it is right.** Every check this page has run — and
|
||
every re-check after a change elsewhere — measured whether a verdict *moved*. That
|
||
is the rule's **stability**. It never measured its **necessity**: an element whose
|
||
position is already fixed by a read `T8aD` key or an `implied_layer_key` is
|
||
*confirmed* by the rule, not *decided* by it, and on those screens removing the
|
||
rule entirely costs nothing.
|
||
|
||
So the question was asked directly. `derived_paint_order` was recomputed with the
|
||
`forced_backdrop` fallback removed and the two orders compared, over every
|
||
`dat/*.pak`:
|
||
|
||
| | instances |
|
||
|---|---|
|
||
| the rule **DECIDES** the order — it moves without it | **62** |
|
||
| the rule merely **AGREES** — order unchanged | 18 |
|
||
| **total forced** | **80** |
|
||
|
||
The 80 reproduces this page's own census exactly, which is the check that the probe
|
||
is looking at the same set. Data:
|
||
[`../data/forced-backdrop-necessity.txt`](../data/forced-backdrop-necessity.txt) ·
|
||
instrument: `crates/sylpheed-formats/examples/forced_backdrop_necessity.rs`.
|
||
|
||
**Every one of the 62 deciders is keyless — no keyed element is ever moved.** That
|
||
is the rule behaving as designed: it is a fallback, and it only ever fires where
|
||
nothing else can speak.
|
||
|
||
### 🔴 Not one of the 80 has a key read from the file
|
||
|
||
The necessity probe asked whether an element had *a* key, collapsing
|
||
`sprite_layer_key` — a `u16` read out of the `T8aD` header, **decoded** — with
|
||
`implied_layer_key` — this crate's per-name table, whose doc comment says outright
|
||
*"only names whose position has actually been measured"*. The port agent pointed out
|
||
that for describing what a *confirmation is made of* those are different claims, and
|
||
splitting them gives a sharper answer than either of us had
|
||
([`../data/forced-backdrop-key-source.txt`](../data/forced-backdrop-key-source.txt)):
|
||
|
||
| key source | instances |
|
||
|---|---|
|
||
| read from the `T8aD` header (decoded) | **0** |
|
||
| `implied_layer_key` (measured in the running game) | 14 |
|
||
| nothing at all | 66 |
|
||
|
||
**Zero.** There is no instance anywhere on the disc where a forced element also
|
||
carries a file-read layer key — which means **this rule has never been checked
|
||
against a decoded field, because there is no case in which both can speak.** That is
|
||
not a defect of the rule; it is what a fallback for keyless elements necessarily
|
||
looks like. But it removes a check a reader would reasonably assume exists, and the
|
||
page should not have let "own key" stand for it.
|
||
|
||
The 14 are two names: `pfbase.tbm` ×10 (`GP_SAVE_LOAD`) and `palogo_eff0.prm` ×4
|
||
(`GP_TITLE`), both implied `0x00000000`.
|
||
|
||
The 18 that merely agree split two ways, and the distinction matters:
|
||
|
||
* **14 have a key of their own, and it is an IMPLIED one** — `pfbase.tbm` (10) and
|
||
`palogo_eff0.prm` (4). ⚠️ Two questions come apart here and an earlier version of
|
||
this page ran them together:
|
||
* *Does the rule change the composite?* **No** — the sort already had the key, so
|
||
these contribute nothing to the necessity count. That much was right.
|
||
* *Does the rule get the right answer?* **Yes, and this is the only place that can
|
||
be asked.** `implied_layer_key` is a position measured in the running game, so
|
||
the rule forcing these 14 first is the rule agreeing with the **oracle**.
|
||
🔴 So "none of the 18 is evidence for the rule in any direction" — said to the port
|
||
agent on 2026-08-30 and corrected here — **was wrong about the 14**. They are not
|
||
a file-side check, and they are not independent of the sort, but they are the
|
||
rule's *only* external corroboration and there are 14 of them.
|
||
* **4 are keyless but inert** — `po_menu_base.tbm`, twice in each of two 2-element
|
||
builds. *Every* element on those screens is forced, so all keys collapse to the
|
||
same value and the declaration-index tie-break gives the identical order either
|
||
way.
|
||
|
||
### What actually holds the 62 up
|
||
|
||
Not a key — there is none — and not the `palogo_eff0.prm` control, which as above
|
||
is only decisive under a convention its own key already satisfies. **It is the
|
||
impossibility argument alone**: a full-screen quad that is opaque at some instant
|
||
cannot paint above everything visible then, or the screen is blank at that instant.
|
||
That argument is doing all the work on 62 instances, and its assumptions are
|
||
exactly the ones listed under *Reach* below — which is now a larger exposure than
|
||
this page previously implied.
|
||
|
||
By element, the 62:
|
||
|
||
| | count | kind |
|
||
|---|---|---|
|
||
| `pzeff00.prm` | 32 | `.prm`, colour census says pure black |
|
||
| `pgloading_eff00.prm` | 4 | `.prm` — 2 in `GP_TITLE` (entries 12/15), 2 in `GP_SAVE_LOAD` |
|
||
| `esrb_base.prm` | 2 | `.prm` |
|
||
| ten `*base*.tbm` families | 24 | `.tbm`, ❔ pixels never located |
|
||
|
||
⚠️ **24 of the 62 are `.tbm`**, and this page already records that a `.tbm`'s
|
||
pixels cannot be found anywhere on the disc. For those the two readings — "the rule
|
||
places it correctly" and "the element draws nothing, so its position is inert" —
|
||
remain indistinguishable, and both leave the composite right. The 38 `.prm`
|
||
deciders have no such escape: those are real quads with real colour, and the rule is
|
||
load-bearing on them in the full sense.
|
||
|
||
### For the port specifically
|
||
|
||
On the five menu screens the exposure is **two**: `GP_TITLE` entries **12 and 15**,
|
||
the dressed loading bundles, where `pgloading_eff00.prm` has neither a read nor an
|
||
implied key. Entries 10, 11, 13 and 14 — the four splashes — are unchanged with the
|
||
rule removed.
|
||
|
||
### The cost in pixels: 38 of the 62 turn the screen black without it
|
||
|
||
"The order moves" is a property of the sort. The tie-break work already found
|
||
overlapping reorders costing **zero** pixels, so it does not follow that the picture
|
||
moves. Each of the 62 deciding builds was therefore rendered twice — once in
|
||
`derived_paint_order`, once with the fallback removed — and diffed
|
||
([`../data/forced-backdrop-pixel-cost.txt`](../data/forced-backdrop-pixel-cost.txt),
|
||
instrument `examples/forced_backdrop_pixel_cost.rs`).
|
||
|
||
The split is perfect, and it falls exactly along the element kind:
|
||
|
||
| | builds | changed pixels |
|
||
|---|---|---|
|
||
| `.prm` deciders | **38** | **0.69 % … 94.41 %** of the frame |
|
||
| `.tbm` deciders | 24 | **0** — but see below |
|
||
|
||
**On all 38 `.prm` builds, `changed_px` equals the composite's total ink exactly.**
|
||
Not approximately — identically, 38 times out of 38. Without the rule the primitive
|
||
sorts last, paints over everything, and the screen composites to **pure black**.
|
||
That is the port's original contradiction argument, and it is now measured on 38
|
||
builds across seven archives rather than argued on two.
|
||
|
||
For the port's two: `GP_TITLE` entries 12 and 15 each move **49 771 px = 5.40 %** of
|
||
the frame, which is their entire ink.
|
||
|
||
### ✅ Second witness (2026-08-30): the strong form holds in Godot too
|
||
|
||
The port agent re-checked the 38-`.prm` result in **Godot**, which shares no code
|
||
with `compose`, applying this crate's fallback to its own element list and swapping
|
||
**only** `paint_order` on one screen file — renderer, textures and pose all held
|
||
fixed, so the order is the single variable. `GP_TITLE` entry 12 came out at
|
||
**59 530 px** ink with the rule and **exactly 0 without it**, at both of its
|
||
thresholds.
|
||
|
||
That is the claim that needed a second renderer, because it is the strong form: not
|
||
"a large difference" but *the screen ceasing to exist*. Two renderers, and this
|
||
time genuinely two — unlike the necessity census, where the port's re-run was this
|
||
crate's code executed twice.
|
||
|
||
#### ⚠️ …and the two ink figures were never counting the same pixels
|
||
|
||
The port reported 59 530 (>0) and 48 368 (>1); this page said 49 771. Counting the
|
||
same composite every way
|
||
([`../data/forced-backdrop-ink-thresholds.txt`](../data/forced-backdrop-ink-thresholds.txt)):
|
||
|
||
| threshold | this crate | Godot | gap |
|
||
|---|---|---|---|
|
||
| RGB > 0 | 49 771 | **59 530** | 16 % |
|
||
| RGB > 1 | **48 043** | **48 368** | **0.68 %** |
|
||
|
||
**The whole disagreement lives in pixels whose value is exactly 1.** Above that the
|
||
two renderers agree to 325 px on a 921 600-px frame. So it is a 1-LSB sampling
|
||
artefact — a different sampler putting a faint non-zero where this one puts exact
|
||
zero — and not a different set of inked pixels.
|
||
|
||
Two things to carry from that:
|
||
|
||
* **`> 0` is not a portable ink convention between renderers on a mostly-dark
|
||
frame; `> 1` is.** Any future cross-renderer ink comparison should say which.
|
||
* **The 49 771 in this page was never a threshold figure at all.** It is exact RGBA
|
||
inequality between the two paint orders, which over a black backdrop coincides
|
||
with ink > 0 — so it belongs against the port's 59 530, not its 48 368. Reading it
|
||
as the `>1` number would have made the two renderers look like they agreed for the
|
||
wrong reason.
|
||
|
||
✅ The *without-the-rule* column is **0 at every threshold** here as well, matching
|
||
Godot. The strong form is not threshold-sensitive in either renderer.
|
||
|
||
### 🔴 The zero on the 24 `.tbm` builds is my instrument, not a finding
|
||
|
||
**The control I wrote was the wrong control and it passed anyway.** It asked
|
||
whether the *composite* had ink — it always does, the rest of the screen draws —
|
||
when the question is whether *the element being reordered* has ink. This page
|
||
already records that `compose` draws **no pixels at all** for a `.tbm`, because it
|
||
has no resolvable sprite. So a `.tbm`'s paint position cannot change a pixel in our
|
||
renderer **by construction**, and those 24 zeros measure that and nothing else.
|
||
|
||
`tie_break_pixel_cost.rs` got this right and has the per-element `ink_mask` this
|
||
one needed. Reported rather than quietly patched, because the shape — a control
|
||
that cannot fail — is the one this corpus keeps paying for.
|
||
|
||
⚠️ So the `.tbm` half of the necessity result stands where it stood: **"correct or
|
||
inert", indistinguishable**, and no closer to being distinguished than before.
|
||
The 38 `.prm` are the part this measurement moves.
|
||
|
||
### 🔴 "Two renderers, same answer" was true of six instances, not eighty
|
||
|
||
That sentence stood here and it overstated the evidence. **The port agent caught it
|
||
and it is worth stating precisely, because the failure it guards against is the one
|
||
that started this whole thread** — `verify-screen` scoring two blank frames `OK`.
|
||
|
||
| | witnesses |
|
||
|---|---|
|
||
| the six `GP_TITLE` instances | **two, genuinely independent** — the port removed *its own* post-pass in its exporter and diffed its export; different code, different language, different layer. My crate-side run agrees |
|
||
| the other **74** | **one measurement, executed twice.** The port re-ran *this crate's* probe. A fault in the instrument reproduces identically for both of us |
|
||
|
||
So the disc-wide 62 is **not** independently confirmed and this page will not claim
|
||
it is. What the port's re-run does establish is that the probe is deterministic and
|
||
that I transcribed its output correctly — worth having, and much less than
|
||
agreement.
|
||
|
||
⚠️ **And the instrument had a real trap.** `forced_backdrop_necessity.rs` defaulted
|
||
to `GP_TITLE` when given no argument, so a bare run printed **6 instances in the
|
||
same format as 80**. The port hit it and nearly filed the discrepancy back at me. It
|
||
now walks every `dat/*.pak` by default and reports the archive count on stderr.
|
||
"I ran your instrument" has to mean the same thing to both of us.
|
||
|
||
## 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
|
||
```
|
||
|
||
|
||
## 🟡 The opaque-black backdrop as a PREDICATE — sound where it is used, not general
|
||
|
||
`sylpheed-port` turned the splash's `palogo_eff0` into a candidate rule: *a screen
|
||
declaring a full-screen `.prm` at `t=0` with `fade == 0xff000000` is **standalone**;
|
||
one without it is **composited***. On their sixteen exported screens it splits 12/4
|
||
with every exception independently known to be composited. They asked for it
|
||
against archives they do not have. Tested:
|
||
[`data/black-backdrop-predicate.txt`](../data/black-backdrop-predicate.txt).
|
||
|
||
✅ **The control reproduces their split exactly** — `GP_TITLE`'s sixteen bundles
|
||
give 12 with and 4 without, and the four without are entries **0, 1, 2, 3**:
|
||
`build_00`, `build_01`, `press_start`, `press_start_jp`. Element names match too
|
||
(`pteff00.prm`, `palogo_eff0.prm`, `pgloading_eff00.prm`).
|
||
|
||
🔴 **Disc-wide it is rare: 76 of 965 screen builds, 7.9 %.**
|
||
|
||
| archive | with / total |
|
||
|---|---|
|
||
| `GP_STAGE_CLEAR`, `GP_SYSTEM`, `GP_TUTORIAL` | 4/4, 2/2, 2/2 — **all** |
|
||
| `GP_SAVE_LOAD` | 10/18 |
|
||
| `GP_DIALOG` | 34/105 |
|
||
| `GP_HANGAR_ARSENAL` | **0 / 390** |
|
||
| `GP_READY_ROOM` | 0/60 · `GP_OPTIONS` 0/14 · `GP_PAUSE_MENU` 0/6 · `GP_GAMEOVER` 0/10 |
|
||
|
||
⚠️ **So it is not a general standalone/composited test.** `GP_OPTIONS` and
|
||
`GP_PAUSE_MENU` are screens a player plainly sees as screens, and they declare no
|
||
backdrop; `GP_HANGAR_ARSENAL` declares none across **390** builds. Read as
|
||
"composited", that would make 92 % of the game's screens composited, which the
|
||
archives do not support.
|
||
|
||
🟡 **What it does appear to separate is narrower and still useful: screens that
|
||
begin from BLACK from everything else.** A pause menu over gameplay, a hangar over
|
||
a 3D scene and a plate over a title all legitimately lack a black backdrop, but
|
||
they are not the same kind of thing — the negative class is **heterogeneous**, and
|
||
that is exactly what a two-way rule cannot express.
|
||
|
||
📌 **For the port: keep using it where you found it.** Within `GP_TITLE` it is
|
||
exact, and `--black` for those twelve is justified from the file rather than
|
||
assumed. Do **not** carry it into the four archives you have yet to export — in
|
||
three of them it would classify every screen the same way.
|