residual is smaller than I said Testing the candidate I raised last iteration rather than carrying it. Blending bit-0x02 sprites additively moves every measure the wrong way -- whole-frame mean diff +0.55 to +1.04, swoosh-band mean +1.83 to +3.98, band edge-correlation 0.6971 down to 0.5578. So the bit is real and independent but does not select an additive blend. I reverted the experiment and kept the word as T8adImage::flags, documented and not acted on; the render is byte-identical to before. Second refutation: the swoosh is not displaced. Shifting the band over plus or minus 80 by 8 pixels peaks sharply at zero, 0.7342, falling to 0.22 at 24 px. So the pivot story is dead twice over -- inert at scale 100, and no displacement to explain anyway. And I have restated the residual, because earlier sections overstated it. The +16 to +34 band tiles I quoted were measured WITHOUT --primitives. With the dim drawn the band's average is nearly right at +1.83; what is wrong is its structure, tiles running -38.6 then +33.8 and cancelling. Six candidates eliminated now and none confirmed. One caveat I owe the port agent about the capture I gave them: it is at t=4.0s, roughly 174 keyframe units into a screen whose elements have keyframes out to t=600. I judged "settled" from mean luminance, which cannot see a thin sprite still moving. It is settled for the bulk of the screen and not proven settled for every element -- which is a live alternative explanation for a structural difference in exactly the band the sweeps cross. METHOD: cargo build passing does not mean cargo test compiles. Adding the field built the library in 1.48s and broke two test-only struct literals; cargo test failed with exit 101.
499 lines
23 KiB
Markdown
499 lines
23 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 **default**
|
||
`screen render` never sees it.
|
||
|
||
⚠️ **Corrected 2026-08-28:** the sentence that used to end this paragraph — "the
|
||
splash cannot be rendered by `screen render` at all" — is **wrong**. It can:
|
||
`screen render --all` widens the enumeration past `is_build` and the splash
|
||
composites fine, both halves.
|
||
[`../ui-title-build-map.md`](../ui-title-build-map.md) has the renders. What is
|
||
true is narrower: it is invisible to the *default* listing, so anyone who does not
|
||
pass `--all` will conclude it is missing.
|
||
|
||
## 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](../captures/main-menu-oracle.png),
|
||
[composite](../captures/ui-layout/main-menu-composited.png)):
|
||
|
||
> **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`/`ptframe2` rest at `0x00ffffff` (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_back2eff5` against `eff3`
|
||
(22 568 px) and against `eff4` (32 395 px). The game paints `eff5` third 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](../captures/load-game-oracle.png)).
|
||
|
||
### ✅ 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
|
||
```
|
||
|
||
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's
|
||
`palogo_eff0.prm` behaviour in a different file type, so `implied_layer_key`
|
||
now covers it and the layer-key sequences agree.
|
||
2. **A tied group in a non-declaration order.** `0xb100` holds 8 `pfwin`,
|
||
10 `pfbtn0`, 11 `pfbtn1`, 12 `pfmsg`, 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 is `kind = 0x0000`, so the rule predicts
|
||
declaration order `14,15,16,17,18`; the game paints `14,15,18,16,17`.
|
||
* `0x80a0` ×7 — kinds are `0x0` and `0x4`, so the rule predicts
|
||
`2,3,4,5` then `0,1,7`; the game paints `0,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`.
|
||
|
||
## The corpus-wide census (2026-08-26)
|
||
|
||
`tools/re-capture/paint_key_census.py`, output in
|
||
[`../data/paint-key-census.txt`](../data/paint-key-census.txt). **All 21 184
|
||
`T8aD` sprites on the disc** — pak entries *and* `RATC` children.
|
||
|
||
### ⚠️ The first version of this section counted 4 525 sprites, and was retracted
|
||
|
||
It filtered pak entries whose own first four bytes are `T8aD`. But a sprite is
|
||
usually a **child of a `RATC` bundle**, and a bundle entry's magic is `RATC`, so
|
||
a top-level magic filter cannot see one:
|
||
|
||
| | sprites | distinct keys |
|
||
|---|---|---|
|
||
| top-level `T8aD` entries (what was counted) | 4 525 | 45 |
|
||
| `T8aD` inside `RATC` bundles (what was missed) | **16 659** | **204** |
|
||
| both | **21 184** | **216** |
|
||
|
||
**171 of the 216 keys exist only inside bundles.** The sharpest way to put it:
|
||
that census never saw **`GP_TITLE.pak` at all** — the pak holding both screens
|
||
this page's evidence comes from. Every number it produced described a fifth of
|
||
the corpus, chosen by an accident of container nesting.
|
||
|
||
Retracted from it: "45 values", "the keys are pak-local", and "each auxiliary pak
|
||
occupies its own narrow high-byte band". What survives is below.
|
||
|
||
### ✅ The field is a `u16` at `+0x0A`, not a word at `+0x08`
|
||
|
||
The upper half of the 32-bit word this page reads is **zero in 21 184 / 21 184**
|
||
sprites. Nothing above changes — `0x00008100` sorts identically to `0x8100` — but
|
||
the port should read two bytes, and a value with the high half set would mean
|
||
something has been misread rather than that the layer got deeper.
|
||
|
||
### ✅ It is an enumeration: 216 values for 21 184 sprites
|
||
|
||
Not a per-sprite depth. The commonest key (`0x8000`) covers 1 768 sprites,
|
||
`0x8100` another 1 524.
|
||
|
||
### 🔴 Refuted: the keys are **not** partitioned by pak
|
||
|
||
This was the previous section's headline and the fuller population kills it.
|
||
|
||
| | 4 525-sprite version | **all 21 184** |
|
||
|---|---|---|
|
||
| keys crossing a pak family | 4 / 45 = 9 % | **68 / 216 = 31 %** |
|
||
| keys confined to `GP_MAIN_GAME_2D` | 33 / 45 | **52 / 216** |
|
||
|
||
And the per-pak *ranges overlap heavily* rather than banding: `GP_BUNK`
|
||
`0x8000`–`0xa110`, `GP_CHALLENGE` `0x8000`–`0x9200`, `GP_HANGAR_ARSENAL`
|
||
`0x8000`–`0x9412`, `GP_LEADERBOARD` `0x8000`–`0xf100`, `GP_TITLE`
|
||
`0x8000`–`0xc150`. The tidy "each pak owns a high-byte band" picture was an
|
||
artifact of seeing one or two keys per pak. Only three paks are actually narrow
|
||
(`GP_GAMEOVER` `0xd840`–`0xd862`, `GP_MISSION_LOG` `0xa400`–`0xa420`,
|
||
`GP_SYSTEM` `0xa010`–`0xa112`).
|
||
|
||
So the key is closer to a **shared vocabulary** than to a per-screen depth — the
|
||
opposite of what the under-sampled census said. ❔ Which bits carry the layer
|
||
remains unknown, and is now a question about 216 values rather than 45.
|
||
|
||
### ✅ The language paks are one screen set six times
|
||
|
||
The six `GP_MAIN_GAME_*2D.pak` have **byte-for-byte identical key sets**. This is
|
||
why counting paks rather than pak *families* first reported "37 of 45 keys appear
|
||
in more than one pak" — a statement about localisation, not about the format.
|
||
|
||
### The recurring failure, stated once
|
||
|
||
Three published numbers on this page were wrong the same way: each was computed
|
||
over a population I had not checked was the population in question — the six
|
||
language copies, then the top-level-only sprites, then the size of that gap. The
|
||
analysis was never the problem. **Check the sampling frame before the statistic.**
|
||
|
||
## `EXTRAS` — the derived order against a capture nobody had taken (2026-08-28)
|
||
|
||
The five orders above are all **measured**: the derived rule is checked against a
|
||
live child list. This one is the other direction — a screen with **no measured
|
||
order at all**, composited purely from the derived rule and scored against a
|
||
framebuffer capture taken the same day
|
||
([capture](../captures/title-builds/live-extras.png),
|
||
[composite](../captures/ui-layout/extras-composited.png)).
|
||
|
||
`GP_TITLE.pak` build 6, 18 elements, two `.prm` primitives:
|
||
|
||
```
|
||
sylpheed-cli screen render --build 6 --black --primitives GP_TITLE.pak extras.png
|
||
align_to_capture.py live-extras.png extras.png --region 150 100 1150 600
|
||
```
|
||
|
||
| composite | best shift | score |
|
||
|---|---|---|
|
||
| **build 6, `--black --primitives`** | **(0, 0)** | **0.9620** |
|
||
| build 6, no primitives | (0, 0) | 0.9576 |
|
||
| *control:* main menu vs `main-menu-oracle.png`, same tooling | (0, 0) | 0.9657 |
|
||
|
||
The control was run first and reproduces the screen this rule was fitted to, so
|
||
the harness is known-good before the new screen is scored. `EXTRAS` lands within
|
||
0.004 of it, at zero shift, with the primitives helping rather than hurting.
|
||
|
||
### ⚠️ What this does and does not show
|
||
|
||
**It does show** that every other part of the stack transfers to an unfitted
|
||
screen: element enumeration, resting pose, pivots, `fade` alpha, and that the
|
||
two primitives land somewhere harmless under the derived `implied_layer_key`.
|
||
|
||
**It does not show that the paint order is right.** `align_to_capture.py`
|
||
correlates *gradient magnitude* — edges — and a paint-order change moves blend
|
||
values, not edges. The earlier derived-vs-declaration A/B on the pause menu and
|
||
main menu measured exactly that: 1.1–3.8 % of pixels differing by ≤45/255, in
|
||
translucent overlaps. A score computed this way cannot see a difference that
|
||
size, so **0.9620 is evidence for placement and only consistent with the order.**
|
||
|
||
**The sharp test, not run.** Composite build 6 twice — the committed derived
|
||
order, and `compose` temporarily reverted to declaration order — and diff both
|
||
against the capture over the pixels where they disagree. That needs a rebuild of
|
||
`sylpheed-formats`, which is why it is named here rather than done; it is the
|
||
same A/B this page already ran on the two screens that had oracles, and `EXTRAS`
|
||
would be the first screen where the two orders can be judged against ground
|
||
truth rather than declared indistinguishable.
|
||
|
||
|
||
## 🟡 A second flag in the same header: `+0x04`, bit `0x02` (2026-08-28)
|
||
|
||
This page reads the layer key at `+0x0A`. The word at **`+0x04`** is a different
|
||
flag word, and on the title screen it splits the sprites exactly along
|
||
**effect versus normal**:
|
||
|
||
| `+0x04` | title sprites |
|
||
|---|---|
|
||
| **`0x8832`** | `pteff01`, `pteff03a`, `ptlogo_back2eff1…5`, `ptlogoall_eff`, `ptlogoall_eff2` |
|
||
| **`0x8830`** | `ptlogo1`, `ptlogo2`, `ptlogo_tm`, `ptbase2`, `ptlogo_back2`, `ptcopyright`, `ptlogo_back2eff` |
|
||
|
||
One bit apart: **`0x02`**.
|
||
|
||
**Disc-wide** (19 216 `T8aD` sprites, top-level and `RATC`-nested): 18 distinct
|
||
values, and bit `0x02` is set in **27.1 %**. It varies independently of the rest
|
||
of the word — `0x8830`/`0x8832`, `0x0830`/`0x0832`, `0x0810`/`0x0812` and
|
||
`0x0030`/`0x0032` all occur — so it is a genuine flag, not part of a larger
|
||
enumeration.
|
||
|
||
**Why it matters:** the title's swoosh renders opaque white where the game draws
|
||
a thin coloured stroke, and every other candidate is eliminated — position and
|
||
size come from the texture and match, `fade` is white-with-alpha, `tint` is white,
|
||
and the texture itself is blue-leaning
|
||
([`../ui-title-build-map.md`](../ui-title-build-map.md)). A per-sprite **blend
|
||
mode** is what is left, and this is the only per-sprite field found that
|
||
distinguishes the elements involved.
|
||
|
||
### 🔴 Tested — it does NOT select an additive blend
|
||
|
||
Blending bit-`0x02` sprites additively and re-correlating against the title
|
||
capture moved **every** measure the wrong way:
|
||
|
||
| | alpha-over | additive |
|
||
|---|---|---|
|
||
| whole-frame mean diff | **+0.55** | +1.04 |
|
||
| swoosh-band mean diff | **+1.83** | +3.98 |
|
||
| swoosh-band edge-corr | **0.6971** | 0.5578 |
|
||
|
||
So the bit is real and independent, but **additive is refuted**. `T8adImage` now
|
||
carries the word as `flags`, documented and *not* acted on.
|
||
|
||
⚠️ **What remains is a correlation, not a decode.** The original test was against
|
||
[`../captures/title-builds/live-title-build4-no-plate.png`](../captures/title-builds/live-title-build4-no-plate.png).
|
||
Worth noting `ptlogo_back2eff` carries `0x8830` **despite** having `eff` in its
|
||
name — so the split is the field's, not a naming pattern's. What the bit *does*
|
||
mean is still unknown.
|