compose now sorts elements by the word at +0x08 of their sprite's T8aD header instead of painting in declaration order, for every build except the two whose measured order is hard-coded. That word is non-decreasing in the order the game actually paints both measured screens, so every screen nobody has captured now gets its layering from the file rather than from the declaration table, which is provably not the paint order. Verified with artifacts and both ways, not by a green build: the disc test asserts the measured orders never invert the key and that the composite's key sequence is sorted, and reading the word from +0x0c instead makes it fail; the title composites identically; and GP_MISSION_SELECT — uncaptured — now composites cleanly, committed as a capture. Two things recorded rather than smoothed over: ties keep declaration order because the game breaks them some other way that is not known, and the developer-logo splash has no .rat child, so is_build rejects it and the compositor never sees that bundle at all — its measured order is inert in practice and screen render cannot draw it.
91 lines
4.3 KiB
Markdown
91 lines
4.3 KiB
Markdown
# 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 and `0x80a0` ×7) and the game
|
||
paints them in an order that is *not* the declaration order —
|
||
`14,15,18,16,17` and `0,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
|
||
three `ptlogo1`/`ptlogo2` instances are the ghosts that are not drawn at rest),
|
||
but it is unmeasured, not proven harmless.
|
||
* **The field's meaning.** `0x8000`, `0x8020`, `0x8040`, `0x8081`… `0x8100` on
|
||
one screen and `0xa100`/`0xa110` on 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 `+0x0c` instead of `+0x08` makes it fail;
|
||
* the title still composites identically (its measured order is used);
|
||
* a screen nobody has captured — `GP_MISSION_SELECT` — now composites cleanly
|
||
([capture](../captures/mission-select-derived-order.png)).
|
||
|
||
**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.
|