Files
Sylpheed/docs/re/structures/ui-paint-order-derived-check.md
Sylpheed RE agent 3eba09aae0 re: the port was still being told SE audio is undecodable -- it is not
A resolve-check on HANDOFF's own rows. Q8 read "SE audio is undecodable
from the disc -- no XACT container exists anywhere". menu-audio-cues.md
retracted exactly that ("### Retracting 'cannot be extracted'") and
locates three cues in Static.slb that decode to PCM: d-pad move 0x1ec0
(4 packets), (B) back 0x0ec0 (2), (A) confirm 0x5d6c0 (6), all mono
48 kHz. The retraction landed in docs/re/ and the page the port reads
kept the superseded text -- the fourth time in this corpus.

Writing the rule down has not worked, so there is a tool now.
handoff_lint.py flags every HANDOFF line making a strong negative claim
that links a doc containing retraction language. First run: found the Q8
row, plus one benign false positive (Q3 links a doc whose retraction is
about a sprite count, not about the tie-break -- checked, and HANDOFF
repeats none of the retracted figures). The lint also caught its own bug
first: it reported existing docs as missing because it joined a guessed
repo root, so it now resolves links relative to the file as markdown does.

Separately, EXTRAS's paint-order risk narrows twice more. Of its 15 tied
pairs only 2 overlap, and of those, ptloop01 x ptloop02 are loop*
animations compose skips by default -- so exactly ONE tie can be drawn:
ptframe3 x ptframe4, overlapping 102x132 px. Against live-extras.png that
contested region correlates +0.9622, better than the whole frame (+0.9440)
and inside the range of regions where order cannot matter (+0.8502 /
+0.9903). Consistent with our order, not proof: correlation cannot see a
swap between locally similar art.

15 -> 2 -> 1 -> consistent is now the whole paint-order risk on the five
screens, and HANDOFF says so.
2026-08-29 02:31:37 +00:00

170 lines
7.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ✅ 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`](../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](ui-screen-runtime.md) 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.
---
## ✅ `EXTRAS`'s exposure narrows to ONE pair — and the capture is consistent with it
The 15 tied pairs on `EXTRAS` reduce twice.
**2 of the 15 overlap** (only an overlapping tie can paint differently):
```
[0] ptframe3.t32 x [1] ptframe4.t32 key 32848 overlap 102x132
[14] ptloop01.rat x [15] ptloop02.rat key 32784 overlap 400x180
```
**1 of those 2 can be drawn.** `ptloop01`/`ptloop02` are `loop*` sprite
animations, which `compose` skips unless `--animated` is passed, so their tie is
unreachable in a default render. The real exposure is `ptframe3` × `ptframe4`,
overlapping **102 × 132 px** at x 584686, y 318450.
### The capture is consistent with our order
Correlating our render against
[`live-extras.png`](../captures/title-builds/live-extras.png):
| region | correlation |
|---|---|
| **the contested overlap** (x 584686, y 318450) | **+0.9622** |
| `ptframe3` alone, outside the overlap | +0.8502 |
| `ptframe4` alone, outside the overlap | +0.9903 |
| whole frame | +0.9440 |
The contested region matches **better than the frame as a whole** and sits inside
the range of regions where order *cannot* matter. Were our tie-break wrong, that
region should correlate worse than its neighbours.
🟡 **Consistent with, not proof.** Correlation cannot see a swap if the two
frames happen to be locally similar. But it is a real check, and it removes the
"entirely unverified" character of the one tie the port can actually hit.
**So the chain is: 15 tied pairs → 2 overlapping → 1 drawable → consistent with
the capture.** That is the whole paint-order risk on the port's five screens.