Last iteration I claimed the splash's measured_paint_order [0,2,4,6,1,3,5]
records, between its glow and logo halves, the temporal order they were
seen in rather than depth -- because the halves never share a frame.
The no-overlap measurement is right (glows f94-115, logos f116-211). The
inference is wrong, on two independent grounds:
* Wrong source. That vector is not a read of the draw capture. It is a
read of the live screen object's CHILD ARRAY -- ui-screen-runtime.md
records it literally as "paint order (child slots)". A child list has
a definite order whether or not its children are ever drawn together,
so co-occurrence does not bear on it. The capture was the CHECK.
* The order is in the file anyway. paint_order_audit on GP_TITLE entry
11: derived == measured, 0 inverted pairs, 0 same-layer-key ties. The
glows and logos carry distinct T8aD keys (0xa100 < 0xa110), so the
file orders the halves statically, no capture involved.
I asked the question that started this iteration -- do the title and menu
orders have the same problem -- and the answer is that none of the three
does, for the same reason.
What survives is narrower and now recorded with numbers: how much of each
order its capture actually cross-checks. The title capture is stable (8
draws / 12 quads / 5 textures, identical in all five captured frames
across two logs) and confirms 7 of 24 positions; the menu capture is not
(texture 0x11C30000 present in frame 0, gone by frame 3); the splash
capture cannot cross-check its middle at all.
A counting trap worth the tool: count QUADS, not draws. The menu's draw 9
is indices=24 -- six quads batched from one texture. Counting draws reads
9 where 16 are on screen, and an earlier pass of this analysis briefly
"found" three quads for six declarations that way and concluded elements
were missing. They were batched.
METHOD: check what a "measured" value was measured FROM before reasoning
about its limits. The co-occurrence rule is real, and it is specific to
orders read from draw captures.
248 lines
12 KiB
Markdown
248 lines
12 KiB
Markdown
# `.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: 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.
|
||
|
||
## What is not settled
|
||
|
||
* ❔ **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 f94–115,
|
||
logos f116–211).
|
||
|
||
**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.
|