Files
Sylpheed/docs/re/structures/ui-prm-primitives.md
sylph-decoder 53f834562a 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
2026-08-29 21:02:17 +00:00

260 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `.prm` elements are untextured full-screen quads, and `kind` bit `0x10` says so
**Status:**`CONFIRMED` statically across all 965 screen builds on the disc.
🔴 **Drawing them at `Element::rest()` is refuted** — it would black out the
title screen. ❔ the resting rule for a fade group is unsolved, and that is the
blocker on actually compositing them.
## What they are
Every composite the port produces is missing its `.prm` elements: they resolve to
no sprite, so `compose` skips them. 369 of them exist. Swept over the disc:
| | |
|---|---|
| `.prm` elements in screen builds | **369** |
| …with a RATC child of their own name (a payload) | **0** |
| …with `kind & 0x10` set | **369** |
| non-`.prm` elements with `kind & 0x10` set | **0** |
| …with `pivot × 2 == 1280×720` (the design space) | **361** |
| …resting at scale 100 %, position (0,0) | 361 / 356 |
Three things follow, none of them inferred:
1. **They carry no texture.** Not one has a payload child; there is nothing in
the bundle for them to draw. `.prm` is a primitive, not a sprite.
2. **The format marks them.** `kind & 0x10` and the `.prm` extension agree with
**zero exceptions in either direction**. A port can classify an element as a
primitive from the declaration table alone, without parsing filenames — which
is what a decoded field is for.
3. **They are screen-sized colour fills.** 361 of 369 are exactly the design
space at 1:1 in the corner. The 8 that are not are small coloured quads
(844×600 at (291,60), and two degenerate 0×720).
## Where the colour is
The keyframe's `fade` word, ARGB — the field named for the alpha ramp it drives
on a sprite. On a `.prm` there is no sprite to modulate, so it *is* the fill.
Resting values across the disc:
```
189 0x00000000 (transparent — nothing on screen)
102 0xff000000 (opaque black)
32 0x7f000000 6 0x60ff0000 (red) 6 0x0000ffd4 (cyan)
20 other blacks 6 0x008000ff (violet) 2 0xf0ffffff (near-white flash)
```
Overwhelmingly **black at some alpha**: these are the screen's fade-to-black,
dim-behind-a-menu and flash layers. That matches the standing note on
`ComposeOptions::backdrop`, which describes the compositor's dim slate as a
stand-in for "the PRMD dim-quad" — this is that quad, and it is in the file.
## Drawing them (2026-08-19)
`fill_quad` composites a primitive as a solid rectangle of the keyframe's `fade`
colour, `pivot × 2` in size, placed and scaled exactly as a sprite is. It is
behind `ComposeOptions::include_primitives` / `screen render --primitives`, and
**off by default** — for a reason that is itself the result of this iteration.
On the title screen, whose paint order is ground truth, it is measurably right:
| composite | mean luminance | vs capture | mean abs diff |
|---|---|---|---|
| primitives off | 76.30 | **+18 %** | 16.07 |
| primitives on | **63.72** | **1.3 %** | **13.08** |
(capture mean 64.58). The composite was ~40 % too bright in the background
regions; `pteff02.prm`, a 25 % black dim, is what was missing. The wordmark is
*not* dimmed by it — the measured order paints that quad at slot 4, beneath the
logo — and the fraction of wordmark pixels above 200 moves 29.9 % → 28.2 %
against the capture's 26.8 %.
[The composite](../captures/ui-layout/title-composited-with-primitives.png).
Edge correlation goes 0.9538 → 0.9480, which sounds like a loss and is not
informative: a uniform dim scales gradients uniformly, so a normalised edge
correlation barely sees it. Brightness is the metric that discriminates here, and
it moves 20 percentage points toward the capture.
## Refuted twice, and the second one is the blocker
**Refuted: you cannot draw them at `Element::rest()`** — as the old longest-dwell
rule computed it. The title's `pteff00.prm` runs opaque → transparent →
transparent → opaque, a screen transition whose resting pose is the transparent
plateau, and it paints **last**; the old rule picked the opaque endpoint, i.e.
the whole screen. That is fixed — [`ui-resting-pose.md`](ui-resting-pose.md).
**Refuted: the derived paint order does not place them.** This is what keeps the
flag off. A primitive has no `T8aD` header, so it has **no layer key**, and
`derived_paint_order` sorts the keyless to the very end. `GP_DIALOG`'s
`pzeff00.prm` is a *single* keyframe of opaque black at full screen; painted last
it wipes the build. Measured: of the 125 builds that draw a visible primitive,
**36 come out more than 99 % one colour** with the derived order.
And no simple default fixes it, because the two screens read off the running game
disagree with each other:
* the developer-logo splash paints `palogo_eff0.prm` **first** — it is the black
backdrop the logos sit on;
* the title paints `pteff02.prm` at slot **4**, beneath the wordmark, and
`pteff00.prm` **last**, as the fade-out.
So a primitive's position is real, per-element, and not derivable from anything
decoded so far. "Primitives first" would break the title's fade-out; "primitives
last" wipes 36 builds; "keep declaration position" was checked against the title
and fails there too — `pteff00.prm` is element 8, declared among the wordmarks,
and the game paints it 23rd.
A disc test measures the damage (36 / 125 / 0-by-default) rather than asserting
the feature works, so the number stays honest and changes when the ordering is
solved.
Worth seeing anyway: the DIFFICULTY dialog
([capture](../captures/ui-layout/dialog-difficulty-with-primitives.png)) renders
legibly with its 50 % dim — an early false alarm said 36 `GP_DIALOG` builds went
"100 % black", which was a crude near-black pixel threshold, not a black screen.
The genuinely wiped ones are a different set, wiped by an *opaque* quad.
## What is not settled
## Where a primitive paints — refuted in the file, measured in the game (2026-08-19)
**🔴 Refuted: the key is not in the bundle.** Two places were checked and both
are empty:
* the **declaration entry**'s four unread words are constant across every
element of all three measured screens — `+28` = 0, `+36` = `0xffffffff`,
`+56` = 0, and `+44` is a button ordinal (1…5 on the menu's five buttons,
`0xffffffff` everywhere else). No key there.
* the **bundle carries no data at all** for a primitive. The menu build declares
`pteff00.prm`, `pteff02.prm` and `pteff05.t32` and has **zero** RATC children
for any of them — its 34 children are 21 `T8aD` sprites and 13 `.rat` records,
none of them named for a primitive.
So the layer a primitive draws on comes from the game's own code, not the file.
**✅ But it is consistent, which makes a per-name table honest.** Bracketing each
unkeyed element between its measured neighbours' keys:
| element | title screen | main menu | splash |
|---|---|---|---|
| `pteff05.t32` | — | (0x8010, 0x8040) | — |
| `pteff04.t32` | (0x8010, 0x8040) | — | — |
| `pteff02.prm` | **(0x8010, 0x8040)** | **(0x8010, 0x8040)** | — |
| `pteff00.prm` | (0x8100, end) | (0x8110, end) | — |
| `palogo_eff0.prm` | — | — | (start, 0xa100) |
`pteff02.prm` falls in the **same interval on both** screens it appears on, the
fade quad is past the maximum on both, and the splash backdrop is below the
minimum. `ui_layout::implied_layer_key` records exactly these, and nothing more —
an unlisted primitive keeps `u32::MAX` and still sorts last.
With it, `derived_paint_order` produces **the same layer-key sequence as the
order read off the running game on all three measured screens**, primitives
included, and matches element-for-element on four of the five bundle instances
(the fifth is the title, which differs only inside its tied groups — a separate
open question). Pinned by
`the_derived_order_puts_every_element_in_the_right_layer_group`.
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 — 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
one, so a runtime child-list read there would say whether its dim is first or
last.
***Blend mode.** A dim quad at `0x7f000000` is presumably straight alpha over
what is beneath, but the flash (`0xf0ffffff`) and the coloured ones
(`0x60ff0000`) may well be additive. Nothing measured.
***The 8 non-full-screen ones**, including two with a zero dimension.
* 🟡 `kind = 0x3010` (38 elements) is `0x10` plus `0x3000`, the button-record
bits — a primitive that is part of a button. Unexamined.
---
## ✅ Measured on the splash: the `.prm` paints FIRST, full-screen, opaque black
**2026-08-29.** This page recorded that where a `.prm` paints "on a screen
without a measured order is unsolved". For the developer splash it is now
measured, from the 235-frame draw capture.
Every frame begins with two full-screen draws before any sprite:
```
f120: [1280x720] [1280x720] [499x241] …
^clear ^this one
```
The second is **untextured** in all **212** frames, with a constant vertex colour
of **`FF000000`** — opaque black. That matches `palogo_eff0.prm`'s declaration
exactly:
| declared | observed |
|---|---|
| `kind = 0x10` (the primitive marker) | untextured draw |
| pivot (640,360) → **1280×720** | 1280×720 quad |
| **one** keyframe, `a = 255` | constant across 212 frames |
| — | vertex colour `FF000000` |
So the splash's backdrop is an **opaque black full-screen quad from the bundle
itself**, painted behind every sprite — not a clear colour, which is why our
splash renders need `--black` to match.
⚠️ **Not a general rule.** The measured *main menu* order puts `pteff02.prm` at
position 4 and `pteff00.prm` **last** — that one is the screen-transition fade.
A `.prm` paints where its screen's order says; the splash's happens to be first.
## ❌ WITHDRAWN (2026-08-29, same day): "the splash's measured order is a sequence, not depth"
This page briefly claimed that `measured_paint_order`'s `[0, 2, 4, 6, 1, 3, 5]`
records, between its glow half and its logo half, the *temporal* order they were
seen in rather than a depth order — on the grounds that the two halves never
share a frame (0 overlap in 235, which is
[measured and still true](../data/splash-phase-timeline.txt): glows f94115,
logos f116211).
**The measurement was right and the conclusion was wrong**, on two independent
grounds, either of which is fatal:
* **Wrong source.** The vector was never read off the draw capture. It is a read
of the live screen object's **child array**
[`ui-screen-runtime.md`](ui-screen-runtime.md) records it literally as
`paint order (child slots): 0 2 4 6 1 3 5`. A child list has a definite order
whether or not any two of its children are ever drawn together, so a
co-occurrence argument cannot touch it. The capture was the *cross-check*, not
the source. That page had already stated the limit honestly for the title
build: the `kind = 0x4` instances "draw at α=0 and so never appeared in the
capture, but their placement in the list is consistent with the grouping".
* **The order is in the file anyway.** `examples/paint_order_audit` on
`GP_TITLE.pak` entry 11: `derived == measured : YES`, `inverted pairs: 0`,
**`same-layer-key ties: 0`**. The glows and the logos carry *distinct* T8aD
layer keys (`0xa100` vs `0xa110` — [key](ui-paint-order-key.md)), so the glow
half sorts before the logo half **statically**, with no capture involved. The
relationship I called unobservable is decoded.
What survives is narrower and worth keeping: **a draw capture of this screen
cannot cross-check the glow-vs-logo half of the order**, because the two halves
never co-occur. The capture validates the order *within* each half only. So the
line in [`ui-paint-order-key.md`](ui-paint-order-key.md) — "read off the live
child list, checked against a draw capture" — has that reach limit on the splash.
The order itself is not in doubt; the *capture's* power to confirm all of it is.
The ✅ above for element 0 is unaffected: it is a direct draw observation.