diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 698872e..85c3108 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -29,7 +29,7 @@ authored version can be deleted. |---|---|---|---| | Q1 | keyframe time unit + ramp shape | ✅ answered | ramp is **linear**; the clock advances **2 units per rendered frame**; working conversion **1 unit = 1/60 s** — [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md) | | Q2 | which build is which screen state | ✅ answered | `GP_TITLE` is **8 screens shipped twice, EN/JP**: 4/7 title art, 2/3 the `PRESS Ⓐ` plate, 5/8 main menu, 6/9 `EXTRAS`, 0/1 and 10/11 two unidentified `DELTASABER` plates — [`ui-title-build-map.md`](../re/ui-title-build-map.md) | -| Q3 | paint order for the six screens | ❔ open | runtime-solved only; declaration table is refuted | +| Q3 | paint order for the six screens | ✅ answered | **decoded**: a `u16` layer key at `+0x0A` of each `T8aD` sprite header, stable-sorted with declaration index; unkeyed elements get an implied key. Confirmed on 5 measured orders + `EXTRAS` vs a capture. One residual: the **tie-break** is unknown and bites on one element of the title — [`structures/ui-paint-order-key.md`](../re/structures/ui-paint-order-key.md) | | Q4 | button → GamePart | ❔ open | labels are baked into sprites | | Q5 | navigation semantics | ❔ open | | | Q6 | boot sequence + what drives it | 🟡 partial | order observed, and the **attract cycle is timed**: ~8–10 s idle on the title → fade to black → ~85 s of video → title again, plate and all. The driver is still not decoded | @@ -76,6 +76,24 @@ authored version can be deleted. [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md) names the test. If it turns out the game presents at 60 Hz, every duration halves — nothing else on this page changes. +* **The paint order is derivable from the file.** Each `T8aD` sprite header + carries a **`u16` layer key at `+0x0A`** (the upper half of the 32-bit word at + `+0x08` is zero in all 21 184 sprites on the disc). Paint order is that key, + **stable-sorted** so equal keys keep declaration order; elements with no sprite + (`.prm` primitives, `.tbm`) have no key and take an implied one — the backdrop + and dim quads sort early, the screen-transition fade (`pteff00.prm`, + `pfeff00.prm`) sorts **last**. ✅ decoded. Checked against five paint orders read + off the running game, one of them (`GP_SAVE_LOAD`'s slot-list header, 6 + instances) **exact and independent of the screens the rule was fitted to**, and + against a fresh `EXTRAS` capture. It reorders 341 of the disc's 965 builds, so + it is not a no-op dressed as a rule. + 🟡 **The one residual: ties.** Where two elements share a key the game + sometimes paints them in an order nothing predicts — eight candidates refuted, + including declaration order, RATC child order, keyframe times, resting X/Y and + `kind`. Measured cost: on the three screens with ground truth it changes the + blend of **one element on one screen** (a title glow). Take the stable sort and + accept that. + * **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the executable's own registration strings. ✅ This is the screen vocabulary; which button reaches which entry is Q4 and is *not* part of it. diff --git a/docs/re/captures/ui-layout/extras-composited.png b/docs/re/captures/ui-layout/extras-composited.png new file mode 100644 index 0000000..3a4dc93 Binary files /dev/null and b/docs/re/captures/ui-layout/extras-composited.png differ diff --git a/docs/re/structures/ui-paint-order-key.md b/docs/re/structures/ui-paint-order-key.md index 729c53d..dc95602 100644 --- a/docs/re/structures/ui-paint-order-key.md +++ b/docs/re/structures/ui-paint-order-key.md @@ -395,3 +395,50 @@ 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.