# 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 ```