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 7a4e4333f4 docs: withdraw yesterday's "paint order is a sequence" -- wrong source
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.
2026-08-29 05:19:44 +00:00

12 KiB
Raw Blame History

.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.

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.

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) 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: 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 arrayui-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 0xa110key), 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 — "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.