A disc-wide census of the ARGB that keyless elements carry. Every full-screen *eff00* PRIMITIVE is pure black at its various alphas (ff000000, 7f000000, 40000000, b2000000, cc000000, d4000000, 00000000). Black at alpha a over content is exactly an alpha-over dim or fade, and an additive black quad would be a no-op nobody would author -- so this narrows the open blend question a long way. The only non-black primitive on the disc is pbafc.prm, RGB 00e8e0 cyan at alphas up to ff, and it is 844x600, NOT full-screen, so it sits outside forced_backdrop's geometry guard. It is now the sole additive candidate. The census also refutes my own argument for nearly half its verdicts. Of the 80 forced-first instances only 42 are .prm; 38 are .tbm carrying fade ffffffff. A SOLID white quad at alpha 255 painted first would make the screen white, and no screen is white -- so a .tbm is a white modulation on a texture, and element alpha does not establish its coverage. That is the .t32 error one file extension further out. I guarded that with el.sprite.is_some(), which fixed the symptom and not the cause: an element's alpha is not its texture's opacity, and only an untextured primitive makes the two the same fact. So 42 verdicts stay decoded and 38 drop to inferred -- still almost certainly right, since all are named *base*, all are full-screen, and pfbase.tbm's first position is measured in the running game, but that is a name-and-role argument which this page elsewhere calls the weaker kind. The code is deliberately unchanged. Restricting forced_backdrop to .prm would send eleven screens' backgrounds back to u32::MAX -- last -- which is the blank-screen bug the rule was written to fix. Downgrading the status is honest; reverting the position would be wrong. The 42/38 split is pinned by a test so anyone tightening the rule sees what it costs. Separately, on the port's black_hold_units ask: four more no-input boots yielded one usable log, which armed late and missed the publisher splash, so the sample is still two runs spanning 3 and 4 frames. Their 6.5-9.2 range stands. And a reason it may not be resolvable this way: the draw log DROPS frame numbers -- in the 3-frame run, frames 121 and 124 are absent entirely, so "frames with no sprite" and "span of frame numbers" are different quantities. Their statistical correction is taken: at n=3 the sample SD (3.893) is the estimator, not the population SD (3.179), making my run 1.88 sigma from the corpus mean rather than 2.31. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
9.9 KiB
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 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), and
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
.prmverdicts stand as decoded. For a solid colour quad the fade is the pixel, so opacity and coverage are the same fact. - 🟡 The 38
.tbmverdicts 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_backdropto.prmwould sendpcbase,pnbase,pqbase,pubase,pvbase,pjbgbase2,po_menu_baseand the fourpx_*_baseback tou32::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.
Reach
⚠️ Assumes straight alpha-over blending. Blend mode is ❔ on
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.
⚠️ 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
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