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.
170 lines
7.4 KiB
Markdown
170 lines
7.4 KiB
Markdown
# ✅ 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 14–18 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 `0x00–0x7f`, 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 584–686, y 318–450.
|
||
|
||
### 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 584–686, y 318–450) | **+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.
|