formats: a keyless primitive that would hide the screen is forced to paint first

Partly closes ui-prm-primitives.md's standing blocker, "where an UNMEASURED
primitive paints". Raised by the port: build_12/build_15 composite to solid
black at every instant of their declared life, because pgloading_eff00.prm -- a
full-screen opaque quad -- sorts last.

The rule is a constraint read off the file, not a preference: 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.

pgloading_eff00.prm is opaque for 39 instants and all 9 other elements are
visible inside that span -> forced first, 4/4 instances.

Two controls, both measured orders from the running game, and the rule has to
survive both:

  * palogo_eff0.prm is measured painting FIRST -- opaque 211 instants, forced
    below 6 of 6. It is NAMED like an overlay, so a name-based rule sorts it
    wrong against a measured order. Occlusion gets it right.
  * pteff00.prm is measured painting LAST -- opaque for 2 instants at its
    screen's entry and exit, forced below only 3 of 23, so the constraint
    permits it on top where it belongs.

Disc-wide: 80 instances forced first, 50 constrained but not forced, 0
unconstrained. The split runs almost exactly along the names -- every *base* is
forced, every *eff00* is not -- with three families crossing it, which is
exactly why the name is not the rule.

It also explains 36 builds the corpus had recorded as "coming out one colour"
with no cause: pzeff00.prm is forced first in 32 of 32 instances, so they were
wiped by our own sort rather than by the game.

The rule's real limit was found by its own disc-wide test failing. Applied to
any element it claimed 22 .t32 SPRITES must sort first against their own layer
keys -- pneff01.t32 (key 0xd850, #8 of 13) and pbfriendly.t32 (0x9230, #17 of
49). A sprite's ELEMENT alpha says nothing about whether its TEXTURE covers the
screen, so forced_backdrop is now restricted to untextured primitives, which is
also the only case derived_paint_order consults it for.

Reach stated: assumes straight alpha-over (blend mode is still open, and an
additive quad at alpha 255 would not occlude); it is a lower bound, not an
ordering; and there is no new oracle measurement -- both controls are prior
ones, and a loading screen is not reachable from the title path.

3 new disc tests; the 13 paint-order tests are green, including
the_derived_order_matches_the_measured_ones_up_to_ties.

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 21:02:17 +00:00
parent 47faeaa7a9
commit cfcda5501c
10 changed files with 592 additions and 5 deletions

View File

@@ -173,3 +173,4 @@ files, which is how the same ground got covered twice.
| [`structures/ui-focus-record-pulse-census.md`](structures/ui-focus-record-pulse-census.md) | Every focus record whose glow pulses, and where `rest()` puts it | ✅ **decoded**, disc-wide: **1 130** focus records, **2 664** timed elements, **210 with a varying alpha** — of which **202** have `rest()` == the **peak** (burns bright forever) and **8** land **mid-ramp**. By pak: `PILOTLOG` 116, `MOVIE_THEATER` 54, `HANGAR_ARSENAL` 30, `LEADERBOARD` 8, **`GP_TITLE` 2**. 🟡 Bounds rather than refutes the port's "34 in the export, 2 varying, nothing to fix" — correct, and correct *because* `GP_TITLE` has 2; the pathology sits in the screens a wider port needs next. 🔴 The 8 mid-ramp ones are the worse mode: `py_ranking_btn01f` swings 255→127→255 and `rest()` returns **244**, neither extreme, which looks entirely plausible and nothing reports it. ✅ Control: `ptbtn01f` is genuinely constant (255 throughout) and is **not** flagged; two hits verified keyframe by keyframe. ⚠️ A pulsing element has no resting pose — the question is malformed, not mis-answered; `pose_at(t)` inside the record's declared cycle ([`ui-record-loop-length.md`](structures/ui-record-loop-length.md)) is the only well-formed query. ⚠️ 210 is a **floor**: focus records are matched by the `Xf.rat` name rule, and varying scale/rotation/position is not counted |
| [`structures/ui-title-buildin-measured.md`](structures/ui-title-buildin-measured.md) | The title's build-in and the plate glow, read out of the guest's own draw stream | ✅ **measured** (Canary, `ARM=early` draw capture): the decoded *mechanism* is observed, not just its end state. **The five flashes fire in a six-frame window and are absent from all 155 other sampled frames**; `ptlogo_back2eff1` is drawn in exactly 2 frames at **t = 54.0** against a decoded peak of **t5456**, and `ptlogo1` first appears at **t = 42.2** against a decoded **t42** — with units/frame taken from the **glow's period alone**, a different element. The two holders (`ptlogo_back2eff`, `ptlogo_back2`) are continuous from frame 134. ✅ The glow's per-vertex colour alpha IS its fade alpha: **observed range 0…80 against a decoded peak of 80**, exact and unfitted; **period 51.158 presented frames** over 20 cycle starts; fitting the decoded ramp gives RMS **13.16** against **38.18 reversed** (2.9×), so the asymmetry is real and correctly directed. Structure: the settled title is 1011 draws naming no sprite — which is why arming at the title sees nothing. ⚠️ Frame **107** is a 27-draw spike between the movie's last frame and the title's first; calling it "the composite" was an **over-read** — it binds **no texture** and only 4 of its 27 draws log geometry. The second title entry has no such frame. ⚠️ The two entries are the same animation at **different sampling phases** (only 4 of 46 aligned frames match), which is what makes the `eff3` result robust. 🔴🔴 **RETRACTED — the game DOES draw `ptlogo_back2eff3`, and all five flashes fire in both entries in the declared stagger** (`eff3` at frames 133134 / 59575958, i.e. t=60.1 and 62.3, inside its declared t∈(58,64)). The absence was an **instrument artefact**: a draw batches several quads (`indices=8` is two) and the log dumps only the first 8 vertices, so min/max over a line **merges** them — and because the wipe is right-aligned, `eff3` (788…1196) lies entirely inside `eff4` (447…1196), making the union *exactly* `eff4`'s extent. The merged box matched `eff4` to 1 px. 🔴 Three explanations had been "ruled out" and all three were aimed at the wrong failure — notably the invisible-draw check counted draws with **no** geometry, where the hiding place was **partial** geometry. Superseded text follows: ~~three alternative explanations tested and failed: *phase* (its window is **6 units** against a **2.23-unit** step, so it cannot be missed — frames 133/134 sit at t=60.1/62.3 inside it and draw `eff2` and `eff4` instead), *an unlogged draw* (exactly 2 blind draws/frame, always the same full-screen-triangle shader, present when no wipe is active), and *a bad position guess* (dropping position entirely, **zero** quads anywhere have a width within ±30 of 408; the spectrum jumps 262 → 748). Draw counts across both entries: eff1 **4**, eff2 **3**, eff3 **0**, eff4 **6**.~~ (all from the merged-box parse, and wrong) 🔴 **The port draws `eff3` at t=6062 and the console does not.** ❔ Why is not established — nothing in its element record differs from its neighbours. ⚠️ An earlier "sub-frame phase" explanation and the advice that drawing all five "shows more sweep than the console" are both **withdrawn**. ⚠️ What a frame-by-frame build-in comparison *will* show is disagreement about which flash lands in which frame — 2 units/submitted frame against this run's 2.231 units/presented frame — and neither side is wrong. 🔴 **Trap:** matching a bound texture's dimensions to a sprite fails both ways — it missed every flash *and* read the intro movie's 640×360 YUV planes as `ptbase2`. ✅ A regression of five events' observed frames against their declared times (residuals ≤0.9 frames) recovers the intercept at frame **106.1** when the composite spike, not in the fit, is frame **107**. ⚠️ Per-vertex alpha = fade alpha holds for the **glow** and does not generalise — `eff4` reads 255/127/254 on consecutive frames. ❔ Frame rate not recorded, so nothing is in seconds; the glow's period implies a **114**-unit cycle against a declared 120, unexplained; `eff5` vs `ptlogo_back2eff` not separated |
| [`structures/boot-splash-gap-measured.md`](structures/boot-splash-gap-measured.md) | The black gap between the two boot splashes | ✅ **measured** in the guest's **draw stream**, which separates true black from a fade tail where luminance cannot: the publisher's last sprite is frame 125 (alpha 7), then **frames 126129 submit NO sprite quad at all**, then the developer fades in at alpha 34. **The gap is 4 presented frames.** Converted with the disc as its own clock — `palogo_sqex` declares alpha≥1 for **239.8 units** and is drawn in **105** frames → **2.284 units/frame** (the title capture independently gave 2.231) — that is **~9.1 units ≈ 0.152 s**, against the **12** the port authored; ⚠️ and the true black is *shorter*, since both boundary frames still carry picture. 🔴 **RETRACTED**: "the developer splash is ONE composited 525×259 quad" — the same batching artefact. It draws three logos and three glows as separate quads in one `indices=24` call; the 525×259 was `gamearts_eff` merged with `seta_eff`. The port refuted it with arithmetic (a 259-tall box cannot hold logos spanning y 164…585) before I checked. ⚠️ The gap measurement is unaffected — those glows are the developer splash's first draw. ❌ Not declared on the disc: `palogo_eff0.prm` is a single static keyframe, and the top-level `+0x08` is a **family constant** (300 / 60) whose slack ranges 12226 units. ❔ The executable is **not** looked at — named, not claimed. 🔴 The instrument was perturbing the measurement: the capture script taps Ⓐ on "screen changed a lot", which is also true of a fading splash — it tapped through the publisher and the developer never appeared. `GRACE=1` and `NOTAP=1` knobs added |
| [`structures/ui-forced-backdrop.md`](structures/ui-forced-backdrop.md) | Where a keyless primitive paints, when the file forces it | ✅ **decoded**, partly closing `ui-prm-primitives.md`'s standing blocker: **an element covering the screen and fully opaque at some instant cannot paint above anything visible then**, and where that set is *every* other element its position is **forced first**. Disc-wide **80** instances forced, 50 constrained but not forced, 0 unconstrained. ✅ **Two controls, both measured orders from the running game**: it reproduces `palogo_eff0.prm` = FIRST (opaque 211 instants, below 6/6) — which a **name**-based rule gets wrong, since it is named like an overlay — and permits `pteff00.prm` on top (opaque 2 instants, below 3/23), which is where it is measured. ✅ Answers the port's `build_12`/`build_15` blank-screen contradiction: `pgloading_eff00.prm` is forced first, 4/4. ✅ Explains 36 builds the corpus recorded as "one colour" with no cause — `pzeff00.prm` forced first 32/32, so **our own sort wiped them**. 🔴 The rule's limit was found by its own test failing: applied to `.t32` sprites it claimed 22 must sort first against their own keys (`pneff01` 0xd850 at #8/13, `pbfriendly` 0x9230 at #17/49) — a sprite's *element* alpha says nothing about its *texture*'s coverage, so it is now restricted to untextured primitives. ⚠️ Assumes straight alpha-over; blend mode is still ❔. ⚠️ A lower bound, not an ordering. ⚠️ No new oracle run — the controls are prior measurements |

View File

@@ -0,0 +1,110 @@
# 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` | **paints FIRST** | 211 | **6 of 6** | ✅ 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.
## 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
```

View File

@@ -155,14 +155,26 @@ included, and matches element-for-element on four of the five bundle instances
open question). Pinned by
`the_derived_order_puts_every_element_in_the_right_layer_group`.
This does **not** make `include_primitives` safe by default: the 36 builds that
come out one colour are wiped by `pzeff00.prm` and `pceff00.prm`, whose positions
have never been measured, so they are not in the table.
This does **not** make `include_primitives` safe by default — but the 36 builds
that come out one colour are now explained: `pzeff00.prm` is **forced first** in
32 of 32 instances by the occlusion constraint, so they were wiped by *our own
sort*, not by the game. [`ui-forced-backdrop.md`](ui-forced-backdrop.md).
## What is not settled
* **Where an *unmeasured* primitive paints.** The blocker, unchanged for the
ones not in the table. It has no layer
* **Where an *unmeasured* primitive paints — PARTLY CLOSED 2026-08-29.** For
**80** instances the file forces it: an element covering the screen and fully
opaque at some instant cannot paint above anything visible then, and where that
is *every* other element its position is first. It reproduces `palogo_eff0.prm`
(measured FIRST, and named like an overlay, so a name-based rule fails it) and
permits `pteff00.prm` on top (measured LAST). ✅ It also explains the 36 builds
below that "come out one colour": `pzeff00.prm` is forced first in 32 of 32.
See [`ui-forced-backdrop.md`](ui-forced-backdrop.md).
***Where a *partly*-opaque primitive paints.** Still open for the 50 the
constraint does not bind — including `pteff00.prm`, whose place on top remains a
measured per-name entry rather than a decoded one.
* ~~❔ **Where an *unmeasured* primitive paints.** The blocker, unchanged for the
ones not in the table.~~ It has no layer
key and the two measured screens rule out every constant default. The cheapest
next step is a third measured order from a screen that carries a primitive —
the `GP_DIALOG` DIFFICULTY box is reachable from the main menu and has exactly