Files
Sylpheed/docs/re/structures/ui-forced-backdrop.md
sylph-decoder b6ae95a732 re: the forced-backdrop span -- 256 vs 211 is a bundle mismatch, and the hold
decides 55% of verdicts

The port implemented the forced-backdrop rule and reported a discrepancy:
palogo_eff0.prm at 256 opaque instants against this corpus's 211.

There is no discrepancy. palogo_eff0.prm appears on BOTH splashes -- the
publisher (entries 10, 13) runs to t=255, giving 256 instants; the developer
(11, 14) runs to t=210, giving 211. Same definition, different bundle. The page
now names the entries so it cannot recur.

The definition, stated: 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 which is decoded rather than assumed (a group
holds at its last keyframe rather than looping; the declared +0x08 never falls
short of the last keyframe, the slack being that hold).

The port's instinct that the hold was load-bearing was right. Over the 130
keyless full-screen primitives with an opaque interval:

  * span = the header's declared +0x08          ->  0 verdicts change
  * span = the primitive's own last keyframe    -> 72 change
  * elements GONE after their last keyframe     -> 72 change

So the hold decides 55% of verdicts -- and dropping it is REFUTED by a measured
order. palogo_eff0.prm is a single keyframe at t=0: without the hold it is
opaque for one instant, no other element is up yet, and the rule calls it free,
against a game measured painting it first. Pinned by a new test that spells out
the counterfactual rather than importing it.

The verdicts that matter are convention-independent: pgloading_eff00.prm is
FIRST under all four conventions and pteff00.prm FREE under all four. And the
header length is interchangeable with the elements' maximum -- zero
disagreements disc-wide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 21:14:58 +00:00

156 lines
7.3 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.
## 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.
⚠️ **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
```