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-paint-order-derived-check.md
Sylpheed RE agent f7f9b555f6 re(ui): the paint-order tie-break is undecodable from the bundle
Q3 was delivered as "decoded: a u16 layer key at +0x0A". The audit last
iteration showed the key does not fully order a screen -- elements
sharing a key are tied, and on the title that tie-break decides two total
occlusions. This searches for what breaks the tie, and closes it as a
negative with reach.

The game paints the five tied ptlogo_back2eff glows in the order
eff1, eff2, eff5, eff3, eff4. Three static structures were searched:

1. The declaration table. Entries 14-18 are byte-identical apart from the
   pivot, which is only half the sprite's own size.

2. The T8aD headers. All five carry identical +0x04 (0x8832) and
   identical +0x08/+0x0A (32899, the key itself), differing only in
   position and tile count. Searched exhaustively -- every offset
   0x00-0x7f, u8/u16/u32, ascending and descending:

     fields sorting to the MEASURED order:              0
     fields sorting to the DECLARATION order (control): 64

   The control is the point: 64 fields can be found that reproduce a
   known ordering, so the scan finds ordering fields when they exist. It
   finds none for the order the game uses.

3. The RATC child order -- a genuinely different permutation on other
   screens -- gives eff1..eff5 here, declaration order again.

All three static orderings give eff1..eff5; the game gives eff1,2,5,3,4.
That agrees with ui-screen-runtime's conclusion from the other direction:
the game builds a reordered child list at load time and paints that.

Q3 now reads honestly: the layer key is decoded and orders 4 of the 5
measured bundles exactly; the tie-break within a key is undecodable, and
a consumer must use a measured order or accept declaration index as an
arbitrary stand-in. The port's exposure remains 2 overlapping tied pairs
on EXTRAS.

METHOD: an exhaustive field search needs a positive control, or "found
nothing" is worthless.
2026-08-29 02:26:19 +00:00

5.7 KiB
Raw Blame History

Does the derived paint order reproduce the measured ones? Mostly — and the gap is bounded

Status: checked, with numbers. compose uses a paint order measured from the running game for the builds that have one and falls back to derived_paint_order — a sort on each sprite's layer key — everywhere else. The code's doc comment claimed the derived order "reproduces both measured orders up to ties". That was a hedge with no measurement behind it, and it was stale: there are three measured orders now, not two.

Tool: cargo run -p sylpheed-formats --example paint_order_audit -- dat/GP_TITLE.pak. Output committed at data/paint-order-audit.txt.

The claim holds, and the exception is all ties

build derived == measured inverted pairs of which same-key ties
main menu (entries 5, 8) YES 0
developer splash (11, 14) YES 0
title (entry 4) NO 8 8 — every one

Four of the five measured bundles reproduce exactly. The title's eight disagreements are all between elements that share a layer key, which the sort cannot order and breaks by declaration index.

⚠️ Two of those ties are total occlusions

The tied family is the five ptlogo_back2eff glows, all key 32899:

measured paints: 14, 15, 18, 16, 17
derived  paints: 14, 15, 16, 17, 18

Two pairs flip, and they are not near-misses:

pair overlap
back2eff5 vs back2eff3 82 824 px² 100 % of the smaller
back2eff5 vs back2eff4 152 047 px² 100 % of the smaller

back2eff5 is 1133×280 and fully contains both. Derived paints it on top of two glows it completely covers; the game paints it underneath. So a tie-break by declaration index is not cosmetic — where it is wrong, it can be wrong by a whole layer. The title is unaffected in practice, because it has a measured order.

The port's actual exposure is two element pairs

Per build, counting tied pairs and how many of them overlap (only those can paint visibly differently):

entry the port's screen order used tied pairs overlapping
4 title measured
5 main menu measured (derived agrees exactly) 0 0
6 EXTRAS derived 15 2
10 publisher splash derived 0 0
11 developer splash measured

So of the five screens, one rests on an unverified derived order, and its risk is two overlapping tied pairs — not the 15 the raw tie count suggests. The publisher splash's derived order is fully determined (no ties at all).

🟡 For completeness, outside the port's set: entry 7 (the Japanese title) is the worst on the disc at 37 tied pairs, 16 overlapping.

Reach

  • This checks the derived order against the orders measured from the running game; it is not an independent derivation of what the game does. Where no measured order exists, agreement cannot be checked at all — only the tie exposure can, which is what the table above reports.
  • Overlap uses pivot × 2 as the element's size (documented as the sprite's own dimensions for a .t32) at its resting placement, so scaled or rotated elements are approximated.

The tie-break is undecodable from the bundle — searched, with reach

The audit above leaves one question: the layer key orders elements, but what orders elements that share a key? On the title that tie-break decides two total occlusions, so it is not academic.

The game paints the five tied ptlogo_back2eff glows in the order eff1, eff2, eff5, eff3, eff4. Three static structures were searched for anything that reproduces it.

1. The declaration table. Entries 1418 are byte-identical apart from the pivot, which is just half the sprite's own size:

14 ptlogo_back2eff1  00000000 ffffffff ffffffff 00000000 ffffffff 0000004e 0000003c 00000000
15 ptlogo_back2eff2  00000000 ffffffff ffffffff 00000000 ffffffff 00000074 0000005b 00000000
16 ptlogo_back2eff3  00000000 ffffffff ffffffff 00000000 ffffffff 000000aa 0000005b 00000000
17 ptlogo_back2eff4  00000000 ffffffff ffffffff 00000000 ffffffff 00000149 0000005b 00000000
18 ptlogo_back2eff5  00000000 ffffffff ffffffff 00000000 ffffffff 000001fb 0000007e 00000000

2. The T8aD headers. All five carry identical +0x04 (0x8832) and identical +0x08/+0x0A (32899, the layer key itself). They differ only in position and tile count. Searched exhaustively: every offset 0x000x7f, at u8, u16 and u32 width, sorted both ascending and descending —

fields sorting to the measured order 0
fields sorting to the declaration order (control) 64

The control matters: 64 fields can be found that reproduce a known ordering, so the scan is capable of finding an ordering field when one exists. It finds none for the order the game uses.

3. The RATC child order — the bundle's second element list, which is genuinely a different permutation from the declaration table on other screens. For this family it reads eff1, eff2, eff3, eff4, eff5: declaration order again.

The answer

Undecodable from the bundle, with reach. All three static orderings give eff1…eff5; the game gives eff1, eff2, eff5, eff3, eff4. That is consistent with what ui-screen-runtime already concluded from the other direction — the game builds a reordered child list at load time and paints that.

So Q3 resolves as: the layer key is decoded and orders 4 of the 5 measured bundles exactly; the tie-break within a key is undecodable, and a consumer must either use a measured order or accept declaration index as an arbitrary stand-in.