This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/ui-prm-primitives.md
Sylpheed RE agent eb86142e4f formats/cli: draw the .prm primitives, opt-in, because their paint order is unsolved
fill_quad composites an untextured primitive as a solid rectangle of the
keyframes fade colour, pivot x 2 in size, placed and scaled exactly as a sprite
is. Behind ComposeOptions::include_primitives and screen render --primitives.

On the title screen, whose paint order is ground truth, it is measurably right:
mean luminance 76.30 -> 63.72 against the captures 64.58, i.e. from +18% to
-1.3%, and mean absolute difference 16.07 -> 13.08. The background was ~40% too
bright; pteff02.prm, a 25% black dim, was what was missing. The wordmark is not
dimmed by it because the measured order paints that quad at slot 4, beneath the
logo. Edge correlation moves 0.9538 -> 0.9480, which is not informative here: a
uniform dim scales gradients uniformly so a normalised edge score barely sees it.

OFF BY DEFAULT, and that is the finding. A primitive has no T8aD header, so no
layer key, and derived_paint_order sorts the keyless to the end. GP_DIALOGs
pzeff00.prm is a single keyframe of opaque black at full screen; painted last it
wipes the build. Of the 125 builds that draw a visible primitive, 36 come out
>99% one colour with the derived order.

No constant default works, because the two screens read off the running game
disagree: the splash paints its primitive FIRST (the black backdrop) while the
title paints one at slot 4 and another LAST (the fade-out). Declaration order
fails the title too. A disc test measures the damage rather than asserting the
feature works, so the number moves when the ordering is solved.

Also records a false alarm worth keeping: a first pass reported 36 GP_DIALOG
builds at "100% black", which was a crude near-black pixel threshold and not a
black screen - those dialogs are dimmed 50% and perfectly legible. The genuinely
wiped builds are a different set.
2026-08-19 07:21:32 +00:00

133 lines
6.5 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
## What is not settled
* ❔ **Where a primitive paints.** The blocker, described above. 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.