The three orders the derived rule was built from all live in GP_TITLE.pak, so
they cannot confirm it - the rule was fitted to them. These two are from
GP_SAVE_LOAD.pak, read off the running game now that the Canary threading fix
makes the main menu dependable.
The 9-element slot-list header composites EXACTLY as the sort predicts, on all 6
instances of it, and nothing about this screen was fed into the rule:
measured 7 8 0 1 2 3 4 5 6
derived 7 8 0 1 2 3 4 5 6
including TWO tied groups (0xb102 x2 and 0xb210 x5) that both come out in
declaration order, and the unkeyed pfeff00.prm fade quad last.
The 13-element save/load frame differs in exactly the two open questions and no
new ones: two unkeyed pfbase.tbm backgrounds paint FIRST where the sort puts the
keyless last - the splash's palogo_eff0.prm behaviour in a different file type,
so implied_layer_key now covers it - and the 0xb100 group of four paints
10,11,8,12 where declaration order is 8,10,11,12.
That second point is a SECOND screen with a mis-ordered tie, which is what the
question needed, and it immediately kills a candidate: 10 and 11 are kind=0x2002
while 8 and 12 are 0x0000, so "descending kind then declaration index"
reproduces 10,11,8,12 exactly - and then fails both title groups, where every
element of 0x8083 is kind 0 and where 0x80a0 would predict 2,3,4,5,0,1,7 against
a measured 0,2,4,7,1,3,5. Seven candidates refuted now.
16 disc tests green.
15 KiB
The paint order comes from a layer key in the T8aD sprite header
Status: ✅ CONFIRMED on both screens whose paint order has been measured —
the key is non-decreasing in paint order on every element that has a sprite,
20 of 24 on one and 6 of 7 on the other. 🟡 ties are not explained. ❔ the field's
full meaning (it looks like flags, not a plain depth).
The field
Every T8aD sprite begins with a 44-byte header. The word at +0x08 —
untouched by this project's decoder, which reads width/height/tile-count at
+0x14/+0x18/+0x1c — sorts the screen.
Read in the order the game paints them (ui-screen-runtime.md has how that order
was measured — off the live child list, checked against a draw capture):
GP_TITLE build 4, the title screen
| paint slot | element | sprite | +0x08 |
|---|---|---|---|
| 0 | 9 | ptbase2 |
0x8000 |
| 3 | 10 | pteff04 |
0x8020 |
| 5 | 6 | pteff01 |
0x8040 |
| 6 | 20 | ptlogo_back2eff |
0x8081 |
| 7 | 19 | ptlogo_back2 |
0x8082 |
| 8–12 | 14,15,18,16,17 | back2eff1…5 |
0x8083 ×5 |
| 13–19 | 0,2,4,7,1,3,5 | ptlogo1/tm/ptlogo2 |
0x80a0 ×7 |
| 20 | 22 | ptlogoall_eff |
0x80a8 |
| 21 | 23 | ptlogoall_eff2 |
0x80a9 |
| 22 | 21 | ptcopyright |
0x8100 |
GP_TITLE entries 11/14, the developer-logo splash
| paint slot | sprite | +0x08 |
|---|---|---|
| 1–3 | the three _eff glows |
0x a100 ×3 |
| 4–6 | the three base logos | 0x a110 ×3 |
Both are ascending, with no inversion anywhere. On the splash it explains the
whole permutation — the glows sort before their logos because 0xa100 < 0xa110,
which is why the declaration table's interleaving (base, glow, base, glow) is not
what you see.
Why this matters
It is the first file-derivable account of the paint order. Everything checked before it failed: declaration order and its reverse, the placement region, the RATC child order, keyframe start and rest times, resting Y, the runtime element record's fields, and every other build's table. This is a per-sprite value the port can read directly.
What is not settled
- Ties. Two groups share a key (
0x8083×5 and0x80a0×7) and the game paints them in an order that is not the declaration order —14,15,18,16,17and0,2,4,7,1,3,5. Something breaks those ties and it is not known. For the port it may not matter (tied elements are same-layer, and the threeptlogo1/ptlogo2instances are the ghosts that are not drawn at rest), but it is unmeasured, not proven harmless. - The field's meaning.
0x8000,0x8020,0x8040,0x8081…0x8100on one screen and0xa100/0xa110on another look like flag words with a layer in some of the bits rather than a plain integer depth. Sorting on the whole word works on both screens; which bits actually carry the layer is unknown. - Two screens is two screens. A third measured permutation would either promote this to a rule or break it. The cheapest one available is any screen whose object is resident at the same time as the title's.
Landed in the compositor (2026-08-19)
ui_layout::compose now paints in the derived order — a stable sort of the
elements by sprite_layer_key — for every build except the two whose measured
order is hard-coded, which stay as the ground truth they are. Elements with no
sprite (the .prm primitives) have no key; they keep their declaration position
among themselves and compose skips them anyway.
Verified three ways rather than by a green build:
- a disc-gated test asserts the measured orders are non-decreasing in the key
and that the composite's key sequence comes out sorted — and it was checked
both ways: reading the word from
+0x0cinstead of+0x08makes it fail; - the title still composites identically (its measured order is used);
- a screen nobody has captured —
GP_MISSION_SELECT— now composites cleanly (capture).
Found on the way, and worth its own line: the developer-logo splash bundle
has no .rat child, so ui_layout::is_build rejects it and the compositor never
sees it. Its measured order is therefore inert in practice, and the splash cannot
be rendered by screen render at all. That is a separate gap in what counts as a
"build", not a paint-order question.
What the change did to the screens that were already verified (2026-08-19)
The derived order is applied to every build on the disc on the strength of
two measured screens, so the first thing owed to it is a check of what it did to
the screens the corpus had already validated against the running game. Two exist:
the tutorial PAUSE menu and the title main menu
(../captures/ui-layout/pause-tutorial-real-vs-rebuilt.png).
Rendered both ways — compose as committed, then with compose temporarily
reverted to declaration order — and diffed:
| screen | pixels differing | RMSE | max per-channel delta |
|---|---|---|---|
| tutorial PAUSE | 35 162 / 921 600 (3.8 %) | 0.52 % | 45 / 255 |
| title main menu | 9 911 / 921 600 (1.1 %) | 0.36 % | 34 / 255 |
No layout regression. Side by side the two renders are indistinguishable: every panel, label and glyph is in the same place at the same size. What moved is confined to pixels where translucent sprites overlap — the glows around PAUSE, the OBJECTIVE / DEFEAT CONDITION / HINT header bars, the menu underlines — i.e. the order the blends compose in, which is exactly what a paint-order change is supposed to touch and nothing else.
🟡 Which of the two is more faithful on these two screens is NOT settled. A difference of ≤45/255 on a few per cent of pixels is not decidable against the committed side-by-side oracle, and there is no fresh framebuffer capture of either screen to diff at that magnitude. The derived order is kept because it is the rule measured off the game on the two screens where the order is known, not because it was shown to be better here. If a capture of the PAUSE menu is ever taken, this is the first thing to check it against.
Corpus-wide, and not a no-op
A disc-gated test (every_composite_paints_in_layer_key_order) composes every
build on the disc and asserts the draw list is strictly increasing in
(layer key, declaration index). It also counts how far the rule reaches:
341 of 965 builds (35 %) are reordered by it.
That matters for how much credit the rule gets. Had it been a near-no-op, the two measured screens would be the entire evidence base; instead a third of the disc's screens now composite in an order no capture has checked. The test asserts the share stays above a quarter, so a future change that quietly collapses the rule back to declaration order fails here instead of passing silently.
A third measured permutation — the main menu (2026-08-19)
Read off the running game with tools/re-capture/screen_children.py, and
identified in the file by its pivot signature: GP_TITLE.pak ratc-index 8,
the NEW GAME / LOAD GAME / TUTORIAL / OPTIONS / EXTRAS screen, 16 elements.
paint order (child slots): 1 3 4 2 5 8 9 6 7 15 10 11 12 13 14 0
| slot | element | slot | element | |
|---|---|---|---|---|
| 0 | 1 ptbase.t32 |
8 | 7 ptframe2 |
|
| 1 | 3 ptloop01 |
9 | 15 ptmsg |
|
| 2 | 4 ptloop02 |
10–14 | 10–14 ptbtn01…ptbtn05 |
|
| 3 | 2 pteff05 |
15 | 0 pteff00.prm |
|
| 4 | 5 pteff02.prm |
|||
| 5–7 | 8 pteff10, 9 pteff12, 6 ptframe1 |
This is the first measured screen with TWO primitives, and they land in
different places — which is the point. pteff02.prm (the 25 % black dim) paints
4th, beneath the whole UI; pteff00.prm (the screen-transition fade, resting
transparent) paints last. Both match their positions on the title screen
exactly, where pteff02.prm is also slot 4 and pteff00.prm is also last.
So a primitive's place is per-element and stable by role across screens:
| primitive | role | measured position |
|---|---|---|
palogo_eff0.prm |
opaque black backdrop | first (splash) |
pteff02.prm |
25 % dim under the UI | slot 4 (title and menu) |
pteff00.prm |
screen-transition fade | last (title and menu) |
It is still not derived — a primitive has no T8aD header and so no layer key
— but there are now three permutations to test a candidate against instead of
two, and the candidate has to live in the 60-byte declaration entry.
Verified against the live capture
The order is wired into measured_paint_order (keyed by element names, so both
language builds get it). Composited with --primitives and edge-correlated
against a framebuffer capture taken in the same session
(capture,
composite):
0.9591 at shift (0, 0)
That is a third screen confirming the whole stack at once — paint order,
resting pose, fade alpha and primitives — on a capture this project had not
seen before.
Not settled
- ❔
ptframe1/ptframe2rest at0x00ffffff(alpha 0) and are therefore not drawn, but the capture shows the menu frame plainly. Either the resting rule picks the wrong plateau for them or the frame is drawn by something else. - ❔ The derivation for primitives. Three permutations now, still no rule.
The tie-break: still unsolved, and now measured down to the pixel (2026-08-19)
With the layer key and the primitives' implied keys in place, the tie-break — how the game orders elements that share a key — is the only thing left between the derived order and ground truth. Three measured screens now constrain it.
On the menu and the splash it does not bite. Every tied group there comes out in declaration order, which is what the stable sort already gives:
| screen | tied group | measured |
|---|---|---|
| menu | 0x8010 ×2 |
3, 4 |
| menu | 0x8050 ×2 |
6, 7 |
| menu | 0x8110 ×5 |
10, 11, 12, 13, 14 |
| splash | 0xa100 ×3 |
2, 4, 6 |
| splash | 0xa110 ×3 |
1, 3, 5 |
The title is the one screen that discriminates, and nothing predicts it:
0x8083 ×5 measured 14, 15, 18, 16, 17 (eff1, eff2, eff5, eff3, eff4)
0x80a0 ×7 measured 0, 2, 4, 7, 1, 3, 5 (logo1×3, tm, logo2×3)
Refuted
| candidate | result |
|---|---|
| declaration order | interleaves the logos (0,1,2,3,4,5,7); wrong |
| RATC child order | groups the logos correctly but puts tm last, and leaves 0x8083 in eff1..eff5; exact on the menu and splash, wrong on the title |
| first keyframe time | 0x8083 times are 52, 56, 62, 58, 60 — the measured order is not sorted by them |
| resting keyframe time | same shape, same failure |
| resting X or Y | 0x8083 rests at x = 938, 938, 64, 788, 447 — unsorted either way |
T8aD header +0x00, +0x04, +0x0c, +0x10 |
identical within a group, or unsorted |
Child order deserves a note: it is a strict improvement over declaration order (7 misplaced positions on the title instead of 9, and it recovers the logo1×3 / logo2×3 grouping), and it is exactly right on the two other screens. It was not adopted, because on the only screen that can tell the two apart it is still wrong, and a rule that is wrong there buys nothing a stable sort does not already give.
What it costs, exactly
order_disagreements_that_change_pixels_are_pinned measures the damage rather
than assuming it. Across all three measured screens:
- 3 disagreeing pairs of drawn elements (repeat instances and faded-out elements are skipped, so they cannot count);
- all 3 have overlapping bounding boxes;
- 2 actually share opaque pixels —
ptlogo_back2eff5againsteff3(22 568 px) and againsteff4(32 395 px). The game paintseff5third in the group, the sort paints it fifth, and those blended glow pixels differ.
The third pair is the instructive one. ptlogo2 and ptlogo_tm overlap by two
columns — the wordmark decodes 992 px wide and reaches x=1129, the trademark
starts at x=1127 — but the wordmark is fully transparent there, so the order
cannot matter. A bounding-box test called this a defect; reading the alpha says
it is not. That is why the test reads pixels.
So the residual error in the derived order is confined to one element's blend on one screen, and it is pinned: a change that makes it worse fails the test.
Two more measured orders — and the first independent confirmation (2026-08-19)
The three orders the derived rule was built from all live in GP_TITLE.pak, so
they cannot confirm it: the rule was fitted to them. These two are from
GP_SAVE_LOAD.pak, read off the running game after the Canary threading fix made
the main menu reachable (capture).
✅ The slot-list header — exact, without being told anything
GP_SAVE_LOAD ratc-indices 1/4/5/21/27, 9 elements:
measured 7 8 0 1 2 3 4 5 6
derived 7 8 0 1 2 3 4 5 6 EXACT, on all 6 instances
| element | key |
|---|---|
7 pffocus1.rat, 8 pfslot1f_scr.rat |
0xb102 tied ×2 |
0 pftitlebase |
0xb200 |
1–5 pfeff11…pftitle1 |
0xb210 tied ×5 |
6 pfeff00.prm |
none → last |
Both tied groups come out in declaration order, which is what the stable sort gives, and the unkeyed fade quad lands last. Nothing about this screen was fed into the rule. It is the first evidence that the layer key is a real ordering mechanism rather than a description of three screens in one pak.
🟡 The save/load frame — differs in exactly the two known ways
GP_SAVE_LOAD ratc-index 18/24, 13 elements (is_build rejects it, so the
compositor never sees it — but it is just as good as evidence):
measured 0 1 2 3 4 5 6 7 10 11 8 12 9
derived 2 3 4 5 6 7 8 10 11 12 9 0 1
- Unkeyed elements paint first, not last. Elements 0 and 1 are
pfbase.tbm— a.tbm, not a.prm, with no sprite and no key — and the game paints them before everything else. Exactly the splash'spalogo_eff0.prmbehaviour in a different file type, soimplied_layer_keynow covers it and the layer-key sequences agree. - A tied group in a non-declaration order.
0xb100holds 8pfwin, 10pfbtn0, 11pfbtn1, 12pfmsg, and the game paints 10, 11, 8, 12.
That second point is the tie-break question again — and it is now on a second screen, which is what it needed.
🔴 Refuted: the tie-break is not kind
The new group is suggestive: 10 and 11 are kind = 0x2002 and 8 and 12 are
0x0000, so "descending kind, then declaration index" reproduces
10, 11, 8, 12 exactly. It does not survive the title:
0x8083×5 — every element iskind = 0x0000, so the rule predicts declaration order14,15,16,17,18; the game paints14,15,18,16,17.0x80a0×7 — kinds are0x0and0x4, so the rule predicts2,3,4,5then0,1,7; the game paints0,2,4,7,1,3,5.
So kind is not it either, and the count of refuted candidates is now seven:
declaration order, RATC child order, first keyframe time, resting time, resting
X/Y, the T8aD header words, and kind.