docs/re: the title screen's paint order, measured from the draw stream

BACKLOG's UI-ordering item ended at "the next step is no longer static — it is a
per-draw capture of the title screen showing the order the game submits". That
capture now exists.

The order, in submission order: a full-screen background layer, a rotated effect
pair, a second full-screen layer, ptlogo_back2eff + ptlogo_back2, ptlogo1 +
ptlogo_tm, ptlogo2, ptcopyright, and the PRESS (A) BUTTON plate — declaration
indices 13, 22|24, 23, 0, 11, 1, 28, and then two elements that are not in that
build at all.

Two more candidate orderings die on it (keyframe start time, resting-keyframe
time), and one structural fact reframes the item: the visible screen composites
TWO bundles — build 7 plus the one-element build 2 that is the button — so no
single build's element table can be the paint order whatever its order. The
button's quad lands at (384,551) against a declared rest of (383,550), which is
a one-pixel agreement between the static placement decode and the running game
on a bundle nothing had checked.

INDEX's "the screen's draw list is the bundle's declaration table (back-to-front)"
is demoted in place rather than edited away.

Still open and said so: the rule that produces the order; three of twelve quads
unidentified; one screen only.
This commit is contained in:
Sylpheed RE agent
2026-08-18 18:57:43 +00:00
parent 81b391d8d5
commit 9da79b92d6
5 changed files with 236 additions and 1 deletions

View File

@@ -122,6 +122,27 @@ the other two are — and therefore a `build-canary` run. That is the cost to
state up front rather than discover halfway in; it is not a container
limitation, just a long build plus a title-screen run.
### 2026-08-18 (later) — the hook was built and run; the order is now measured
`log_ui_draws` exists (Canary branch `auto/re-ui-draw-order`), and the title
screen's paint order is **ground truth** rather than a candidate:
[`ui-title-paint-order-capture.md`](ui-title-paint-order-capture.md).
Background first, then the `back2` glow pair, then `ptlogo1` + `ptlogo_tm`, then
`ptlogo2`, then `ptcopyright`, then the `PRESS Ⓐ BUTTON` plate — i.e.
declaration indices `13, 22|24, 23, 0, 11, 1, 28` and then two elements that are
**not in build 7 at all**. Two more orderings die on that evidence (keyframe
start time, resting-keyframe time), and one structural fact reframes the whole
item: the screen composites **two bundles** (build 7 plus the one-element build
2 that is the button), so no single build's element table can be the paint order
whatever its order.
**Still open, and now sharper:** the rule. The bundle's 60-byte declaration entry
carries no depth field (dumped, above); the per-element `.rat` record has not
been checked for one against this ground truth, and nothing yet explains how the
two bundles are sequenced. Both are static questions again — the oracle side is
answered.
## Capital ships assemble wrong in the viewer

View File

@@ -24,7 +24,8 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
| Weapon fields defaulted on disc | ✅ | [runtime struct](structures/weapon-struct-runtime.md) · [DATA SHEET route](weapon-datasheet-runtime.md) | **Solved.** Canary maps guest RAM into `/dev/shm`, so the parsed `Weapon`/`Shell` objects are readable live; their layout is solved against disc ground truth (zero contradictions over 100+ records). All 126 weapons, exact numbers, no story progress needed — [4 393 values](captures/weapon-runtime-fields.csv) the disc does not carry. Supersedes the letter-bucket limit of the DATA SHEET route, which now serves as the independent cross-check |
| Unit (craft/vessel) fields defaulted on disc | ✅/🟡 | [runtime struct](structures/unit-struct-runtime.md) | The parsed `unit\UN_*.tbl` definition object, vtable `0x820af844`, ≥`0x380` bytes, one per unit — **discovered, not assumed** (`unit_discover.py`), and distinguished from the spawned-entity class `0x820af030` by being one-per-ID and byte-constant within a run. Across runs only pointer words move — `--crosscheck` proves **no reported field offset is run-dependent** (two words, `+0x2c8`/`+0x2d0`, are stage-dependent and remain unidentified). 27 fields ✅ (21 units, 7 runs); the `Maneuver` block is **schema declaration order, 4 bytes/field, base `0x9c` with a two-slot gap after `AA_Roll_Min`** (29 anchors, 0 conflicts), which also pins 5 fields *no* disc record ever values. Angles are **radians at runtime, degrees on disc**. **Re-derived independently 2026-08-13 from the loader's own key strings** (`sub_82341A20`; the field name for each store is a string in the image): **159 fields**, agreeing with this solver on **25 of 25 shared offsets**, verified at **406 values matching the disc and 0 disagreeing** over 11 live objects spanning UNIT and VESSEL — landed as `data/unit_definition_layout.txt` + `sylpheed_formats::unit_layout` + a no-emulator test, with **121 defaulted fields** read out ([live-unit-definitions](live-unit-definitions.md)). Unlike weapons, unit definitions are instantiated **per stage**, so coverage (21/110) grows by visiting missions — but a defaulted field is **not** a global constant: `Size_Y` provably inherits `Size_X` (7 independent units, 6 distinct values), and three more sibling rules are recorded ❔, recovering 65 values in units never visited — [values](captures/unit-runtime-fields.csv) |
| Arsenal develop economy | ✅/❔ | [arsenal-develop-economy](arsenal-develop-economy.md) + [conditions](captures/arsenal-develop-conditions.csv) | The Arsenal reads `weapon.tbl` (item ids, in the 8-category display order) and `strings.tbl` (names, descriptions, and a **"Conditions to obtain"** block per item) out of `GP_HANGAR_ARSENAL.pak`. All **60** conditions are extracted: gates are stage completion, a predecessor item, or an **ace kill**; costs run 3 000350 000 P and **20 items are free** once gated. `weapon.tbl`'s first record reproduces the in-game DATA SHEET exactly (Range D / Power E / Speed / Weight 0.3 = Light / 4000 P) — later records are unreadable from the string pool alone because IDXD **dedupes repeated values**. Used to identify the save blob's index space, now **solved**: the blob follows **`strings.tbl`'s** order — the display order *plus* the cut items only the localisation file lists (`Adhesive Mine B2A`, `Ballista GSH`, …) — pinned by four hand-written probe saves (9 Stiletto, 21 Falcon, 39 Tomahawk, 48 Jamming System) and closing exactly at index 53. `weapon.tbl`'s id list is **not** the index space; that it is also 54 long is a coincidence, and the two agree only to index 32. The retail save's five unexplained owned entries are the cut items, shipped owned and never rendered |
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` is decoded — it is a **looping sprite animation**, not a composition. The **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly |
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` is decoded — it is a **looping sprite animation**, not a composition. ⚠️ **DEMOTED 2026-08-18** — the declaration table is *not* the paint order: a per-draw capture of the running title screen ([ui-title-paint-order-capture](ui-title-paint-order-capture.md)) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly |
| UI screen paint order | ✅/🟡 | [title paint order](ui-title-paint-order-capture.md) | **Measured from the guest's draw stream** (new Canary hook `log_ui_draws`, F10): the title screen paints 11 draws a frame through one sprite shader, and the submission order is background → `back2` glow → `ptlogo1`+`tm``ptlogo2``ptcopyright` → the `PRESS Ⓐ BUTTON` plate. That refutes declaration order, reverse order, keyframe-start order and resting-time order, and shows the screen composites **two bundles** (build 7 + the one-element build 2). 🟡 the rule that produces the order is still unknown |
| Save file (`savedata`) | ✅/❔ | [savegame-format](structures/savegame-format.md) + [`tools/re-capture/savegame.py`](../../tools/re-capture/savegame.py) | `GDHA` container, zlib payload, chunk stream (`GDAA` / phase name / `GHAD` 122 B progress block / 16×20 B slot table / trailer). **Container and layout read off the title's own serializer `0x822C00E8` and verified by a byte-identical round-trip**; the whole save is 545 B. Payload offsets are also the live save object's offsets (`save+8` GHAD, `save+136` slots). A second save made in-game names **Points** (+24), **flight time in ms** (+4) and **clear ratio %** (+8) off the game's own Details panel; the payload is a **pure function of game state** (same state saved twice = byte-identical, only the header FILETIME and its uninitialised pointer padding move), and the 16 `SHAB` records are **not** the UI's 20 save slots. Difficulty vs stage is undecided — three fields hold 2. **A third save, taken after developing exactly one Arsenal weapon** (Light Machine Gun MG I, 4000 P), moves exactly three things: `+24` Points 4101→101 (which **separates it from `+28`**, that did not move), `+8` clear ratio 5→6 (so the ratio counts *collection*, not only stages), and two entries of the 54-byte blob — `2→4` for the item bought and `0→2` for the successor the game announced as newly developable, giving the blob its alphabet ✅ *0 locked / 2 developable / 4 developed* (only the `4`s are stored — `2` is re-derived at load). **Saves can also be written back**: three derived header fields (length at `+0x30`, payload length at `+0x8c`, `adler32` at `+0x8e`) are all that stand between a parse and a hand-written save that the title loads, and [`savegame_edit.py`](../../tools/re-capture/savegame_edit.py) re-wraps a real save byte-identically. That turned the blob's index space from blocked-on-story-progress into four probe saves — see the [economy note](arsenal-develop-economy.md) |
## Runtime / dynamic-capture technique

View File

@@ -0,0 +1,70 @@
# every draw in SUBMISSION ORDER, undeduplicated, frames 2..7
# tex dimensions identify the sprite; base is the guest address
0 prim=8 indices=3 vs=0x0A6D1DD7767FDF27 ps=0x2E372EA28CC404B7
vb=0x14FF75B8 stride=28 attrs=[57@0 38@12 ] fmt0=57 v: [BF000000,BF000000=-0.50,-0.50] [449FF000,BF000000=1279.50,-0.50] [449FF000,4433E000=1279.50,719.50]
1 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BA60000 1280x768 fmt=6]
vb=0x14FF7610 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF80199A,3F802D83=-1.00,1.00] [3F7FCCCC,3F802D83=1.00,1.00] [3F7FCCCC,BF7FA4FA=1.00,-1.00] [BF80199A,BF7FA4FA=-1.00,-1.00]
2 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C30000 1280x768 fmt=6]
vb=0x14FF7670 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BEC6F6C7,3FCA029D=-0.39,1.58] [3E1AF152,3F8313AE=0.15,1.02] [BF3143AC,BFC97A15=-0.69,-1.57] [BF9DBDB2,BF828B25=-1.23,-1.02] [BE39CFBA,3F82E9FB=-0.18,1.02] [3E84CD83,3FE73A98=0.26,1.81] [3FECD8F3,BF82CF51=1.85,-1.02] [3FB46B9B,BFE71FED=1.41,-1.81]
3 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B690000 1280x768 fmt=6]
vb=0x14FF7730 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF80199A,3F802D83=-1.00,1.00] [3F7FCCCC,3F802D83=1.00,1.00] [3F7FCCCC,BF7FA4FA=1.00,-1.00] [BF80199A,BF7FA4FA=-1.00,-1.00]
4 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
vb=0x14FF7790 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF66999A,3F2D27D3=-0.90,0.68] [3F5E999A,3F2D27D3=0.87,0.68] [3F5E999A,BDCFA4F8=0.87,-0.10] [BF66999A,BDCFA4F8=-0.90,-0.10] [BF63CCCD,3F26C16C=-0.89,0.65] [3F5B6667,3F26C16C=0.86,0.65] [3F5B6667,BD9C71C8=0.86,-0.08] [BF63CCCD,BD9C71C8=-0.89,-0.08]
5 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C30000 1280x768 fmt=6]
vb=0x14FF7850 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF36999A,3EEE38E4=-0.71,0.47] [3F390000,3EEE38E4=0.72,0.47] [3F390000,3E1B05B0=0.72,0.15] [BF36999A,3E1B05B0=-0.71,0.15] [3F42999A,BD471C72=0.76,-0.05] [3F516667,BD471C72=0.82,-0.05] [3F516667,BDC44444=0.82,-0.10] [3F42999A,BDC44444=0.76,-0.10]
6 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BE30000 1280x768 fmt=6]
vb=0x14FF7910 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF496667,3E155556=-0.79,0.15] [3F436667,3E155556=0.76,0.15] [3F436667,BE127D28=0.76,-0.14] [BF496667,BE127D28=-0.79,-0.14]
7 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C30000 1280x768 fmt=6]
vb=0x14FF7970 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF0B0000,BF516C17=-0.54,-0.82] [3F0A999A,BF516C17=0.54,-0.82] [3F0A999A,BF5FA4FB=0.54,-0.87] [BF0B0000,BF5FA4FB=-0.54,-0.87] [BECE0000,BF06C16C=-0.40,-0.53] [3ECC6666,BF06C16C=0.40,-0.53] [3ECC6666,BF2A4FA5=0.40,-0.67] [BECE0000,BF2A4FA5=-0.40,-0.67]
8 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
vb=0x14FF7A30 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BED7999A,BEFB05B1=-0.42,-0.49] [3ED60000,BEFB05B1=0.42,-0.49] [3ED60000,BF338E39=0.42,-0.70] [BED7999A,BF338E39=-0.42,-0.70]
9 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
10 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
--- frame 3 ---
11 prim=8 indices=3 vs=0x0A6D1DD7767FDF27 ps=0x2E372EA28CC404B7
vb=0x15017B38 stride=28 attrs=[57@0 38@12 ] fmt0=57 v: [BF000000,BF000000=-0.50,-0.50] [449FF000,BF000000=1279.50,-0.50] [449FF000,4433E000=1279.50,719.50]
12 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BA60000 1280x768 fmt=6]
vb=0x15017B90 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF80199A,3F802D83=-1.00,1.00] [3F7FCCCC,3F802D83=1.00,1.00] [3F7FCCCC,BF7FA4FA=1.00,-1.00] [BF80199A,BF7FA4FA=-1.00,-1.00]
13 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C30000 1280x768 fmt=6]
vb=0x15017BF0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BEC3C393,3FCA029D=-0.38,1.58] [3E2157BA,3F8313AE=0.16,1.02] [BF2FAA12,BFC97A15=-0.69,-1.57] [BF9CF0E5,BF828B25=-1.23,-1.02] [BE404FBA,3F82E9FB=-0.19,1.02] [3E818D83,3FE73A98=0.25,1.81] [3FEC08F3,BF82CF51=1.84,-1.02] [3FB39B9B,BFE71FED=1.40,-1.81]
14 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B690000 1280x768 fmt=6]
vb=0x15017CB0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF80199A,3F802D83=-1.00,1.00] [3F7FCCCC,3F802D83=1.00,1.00] [3F7FCCCC,BF7FA4FA=1.00,-1.00] [BF80199A,BF7FA4FA=-1.00,-1.00]
15 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
vb=0x15017D10 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF66999A,3F2D27D3=-0.90,0.68] [3F5E999A,3F2D27D3=0.87,0.68] [3F5E999A,BDCFA4F8=0.87,-0.10] [BF66999A,BDCFA4F8=-0.90,-0.10] [BF63CCCD,3F26C16C=-0.89,0.65] [3F5B6667,3F26C16C=0.86,0.65] [3F5B6667,BD9C71C8=0.86,-0.08] [BF63CCCD,BD9C71C8=-0.89,-0.08]
16 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C30000 1280x768 fmt=6]
vb=0x15017DD0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF36999A,3EEE38E4=-0.71,0.47] [3F390000,3EEE38E4=0.72,0.47] [3F390000,3E1B05B0=0.72,0.15] [BF36999A,3E1B05B0=-0.71,0.15] [3F42999A,BD471C72=0.76,-0.05] [3F516667,BD471C72=0.82,-0.05] [3F516667,BDC44444=0.82,-0.10] [3F42999A,BDC44444=0.76,-0.10]
17 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BE30000 1280x768 fmt=6]
vb=0x15017E90 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF496667,3E155556=-0.79,0.15] [3F436667,3E155556=0.76,0.15] [3F436667,BE127D28=0.76,-0.14] [BF496667,BE127D28=-0.79,-0.14]
18 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C30000 1280x768 fmt=6]
vb=0x15017EF0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF0B0000,BF516C17=-0.54,-0.82] [3F0A999A,BF516C17=0.54,-0.82] [3F0A999A,BF5FA4FB=0.54,-0.87] [BF0B0000,BF5FA4FB=-0.54,-0.87] [BECE0000,BF06C16C=-0.40,-0.53] [3ECC6666,BF06C16C=0.40,-0.53] [3ECC6666,BF2A4FA5=0.40,-0.67] [BECE0000,BF2A4FA5=-0.40,-0.67]
19 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
vb=0x15017FB0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BED7999A,BEFB05B1=-0.42,-0.49] [3ED60000,BEFB05B1=0.42,-0.49] [3ED60000,BF338E39=0.42,-0.70] [BED7999A,BF338E39=-0.42,-0.70]
20 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
21 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
--- frame 4 ---
22 prim=8 indices=3 vs=0x0A6D1DD7767FDF27 ps=0x2E372EA28CC404B7
vb=0x150380B8 stride=28 attrs=[57@0 38@12 ] fmt0=57 v: [BF000000,BF000000=-0.50,-0.50] [449FF000,BF000000=1279.50,-0.50] [449FF000,4433E000=1279.50,719.50]
23 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BA60000 1280x768 fmt=6]
vb=0x15038110 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF80199A,3F802D83=-1.00,1.00] [3F7FCCCC,3F802D83=1.00,1.00] [3F7FCCCC,BF7FA4FA=1.00,-1.00] [BF80199A,BF7FA4FA=-1.00,-1.00]
24 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C30000 1280x768 fmt=6]
vb=0x15038170 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BEC229FA,3FCA029D=-0.38,1.58] [3E248AEC,3F8313AE=0.16,1.02] [BF2EDD46,BFC97A15=-0.68,-1.57] [BF9C8A7F,BF828B25=-1.22,-1.02] [BE438FBA,3F82E9FB=-0.19,1.02] [3E7FDB06,3FE73A98=0.25,1.81] [3FEBA0F3,BF82CF51=1.84,-1.02] [3FB3339B,BFE71FED=1.40,-1.81]
25 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B690000 1280x768 fmt=6]
vb=0x15038230 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF80199A,3F802D83=-1.00,1.00] [3F7FCCCC,3F802D83=1.00,1.00] [3F7FCCCC,BF7FA4FA=1.00,-1.00] [BF80199A,BF7FA4FA=-1.00,-1.00]
26 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
vb=0x15038290 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF66999A,3F2D27D3=-0.90,0.68] [3F5E999A,3F2D27D3=0.87,0.68] [3F5E999A,BDCFA4F8=0.87,-0.10] [BF66999A,BDCFA4F8=-0.90,-0.10] [BF63CCCD,3F26C16C=-0.89,0.65] [3F5B6667,3F26C16C=0.86,0.65] [3F5B6667,BD9C71C8=0.86,-0.08] [BF63CCCD,BD9C71C8=-0.89,-0.08]
27 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C30000 1280x768 fmt=6]
vb=0x15038350 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF36999A,3EEE38E4=-0.71,0.47] [3F390000,3EEE38E4=0.72,0.47] [3F390000,3E1B05B0=0.72,0.15] [BF36999A,3E1B05B0=-0.71,0.15] [3F42999A,BD471C72=0.76,-0.05] [3F516667,BD471C72=0.82,-0.05] [3F516667,BDC44444=0.82,-0.10] [3F42999A,BDC44444=0.76,-0.10]
28 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BE30000 1280x768 fmt=6]
vb=0x15038410 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF496667,3E155556=-0.79,0.15] [3F436667,3E155556=0.76,0.15] [3F436667,BE127D28=0.76,-0.14] [BF496667,BE127D28=-0.79,-0.14]
29 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C30000 1280x768 fmt=6]
vb=0x15038470 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF0B0000,BF516C17=-0.54,-0.82] [3F0A999A,BF516C17=0.54,-0.82] [3F0A999A,BF5FA4FB=0.54,-0.87] [BF0B0000,BF5FA4FB=-0.54,-0.87] [BECE0000,BF06C16C=-0.40,-0.53] [3ECC6666,BF06C16C=0.40,-0.53] [3ECC6666,BF2A4FA5=0.40,-0.67] [BECE0000,BF2A4FA5=-0.40,-0.67]
30 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
vb=0x15038530 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BED7999A,BEFB05B1=-0.42,-0.49] [3ED60000,BEFB05B1=0.42,-0.49] [3ED60000,BF338E39=0.42,-0.70] [BED7999A,BF338E39=-0.42,-0.70]
31 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
32 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
--- frame 5 ---
33 prim=8 indices=3 vs=0x0A6D1DD7767FDF27 ps=0x2E372EA28CC404B7
vb=0x15058638 stride=28 attrs=[57@0 38@12 ] fmt0=57 v: [BF000000,BF000000=-0.50,-0.50] [449FF000,BF000000=1279.50,-0.50] [449FF000,4433E000=1279.50,719.50]
34 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BA60000 1280x768 fmt=6]
vb=0x15058690 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [BF80199A,3F802D83=-1.00,1.00] [3F7FCCCC,3F802D83=1.00,1.00] [3F7FCCCC,BF7FA4FA=1.00,-1.00] [BF80199A,BF7FA4FA=-1.00,-1.00]
35 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C30000 1280x768 fmt=6]

View File

@@ -0,0 +1,13 @@
frame 2 draw 0 ( 0, 0) 1280x720 → 8AX 1280x720
frame 2 draw 1 ( 0, 0) 1280x720 → 8AX 1280x720
frame 2 draw 2 (-147,-209) 883x1134 ROT → ?
frame 2 draw 2 ( 525,-292) 1299x1303 ROT → ?
frame 2 draw 3 ( 0, 0) 1280x720 → 8AX 1280x720
frame 2 draw 4 ( 64, 115) 1133x281 → ptlogo_back2eff.t32 1133x280 (±1), ptlogo_back2eff5.t32 1133x280 (±1)
frame 2 draw 4 ( 70, 126) 1120x263 → ptlogo_back2.t32 1118x262 (±2)
frame 2 draw 5 ( 186, 191) 915x115 → ptlogo1.t32 919x113 (±4)
frame 2 draw 5 (1126, 378) 38x18 → ptlogo_tm.t32 37x17 (±1)
frame 2 draw 6 ( 134, 306) 992x104 → ptlogo2.t32 992x104
frame 2 draw 7 ( 294, 655) 691x18 → ptcopyright.t32 694x20 (±3)
frame 2 draw 7 ( 384, 551) 512x50 → ptbtn00.t32 513x50 (±1)
frame 2 draw 8 ( 371, 536) 538x76 → ptbtn00f.t32 537x76 (±1)

View File

@@ -0,0 +1,130 @@
# The title screen's paint order, measured from the guest's draw submissions
**Status:**`CONFIRMED` — the order in which the running game paints the title
screen's sprites, read off the guest's own draw stream. 🟡 the *rule* that
produces that order is still unknown; four candidate rules are refuted below.
❔ two full-screen layers and one rotated effect pair are unidentified.
This closes the measurement gap named in `BACKLOG.md` — *"the next step is no
longer static: it is a per-draw capture of the title screen showing the order the
game submits"* — and it does so without the guest-code read that entry expected.
## What it took: a third RE hook in Canary
Neither existing hook can answer this (`BACKLOG.md` has the detail): `log_draws`
de-dups by vertex declaration, so a screen's sprites collapse into one record,
and the F10 ship capture drops any draw without an `f32x3` position stream —
which is every UI quad. So `log_ui_draws` was added on the Canary branch
`auto/re-ui-draw-order`:
* F10 arms it (shared with the ship capture; `log_ui_draws` is what separates
them), and it writes `xenia_re_ui_draws_NN.log`**every** draw of the next
*N* frames, in submission order, undeduplicated;
* `--ui_draw_capture_frames` / `--ui_draw_capture_max` bound it;
* each record carries the primitive type, index count, VS/PS hashes, every bound
texture's base and dimensions, and **the quad's vertex positions**.
Two traps were paid for on the way, both worth stating because both produced
confident nonsense rather than an error:
* **`counter_` is not a frame number.** It looks like one — the VdSwap packet
increments it — but `GraphicsSystem::MarkVblank()` increments it too, from the
vblank thread. A capture bounded by it ended **after one draw**, having
"covered 6 frames", because six vblanks landed between two draws of the same
frame. The hook now counts VdSwap packets itself.
* **`screenshot` was grabbing a 10-pixel sliver.** The app owns two windows of
class `xenia_canary` — the 1280×745 game window and a 10×10 helper — and the
wrapper took the first match from `xwininfo -root -children`, which lists only
*direct* children of the root and so never contained the real, reparented game
window at all. Every screen classification for a whole session was noise from a
10×10 image; the movie-skip heuristic fired on it repeatedly and drove the
guest into a save-data probe that crashed it. Fixed to take the largest
`xenia_canary` window from the full tree.
## The measurement
Boot to the title screen, arm the capture, decode the quads. The title screen's
steady state is **11 draws per frame**, every frame, identically:
| draw | what |
|---|---|
| 0 | untextured full-screen triangle (VS `0A6D1DD7`, PS `2E372EA2`), positions already in pixels |
| 18 | the UI sprite shader (PS **`E59B2B3D`**), quad lists, positions in NDC |
| 910 | two full-screen triangles sampling a 1×1 texture — a fade/tint pass |
Sprite draws sample **1280×768** texture pages (`fmt=6`, i.e. 720 rounded up to
64), several pages alive at once — so the bound texture does *not* name the
sprite. The **quad's size does**: the disc's decoded sprites have near-unique
dimensions. `tools/re-capture/ui_draw_order.py` converts NDC → pixels and matches
each quad against `sylpheed-cli pak textures` output.
Frame 2 of [`captures/title-screen-draw-order.log`](captures/title-screen-draw-order.log),
in submission order ([full table](captures/title-screen-paint-order.txt)):
```
draw 1 ( 0, 0) 1280x720 full-screen layer
draw 2 (-147,-209) 883x1134 ROT rotated effect ┐ two rotated quads,
draw 2 ( 525,-292) 1299x1303 ROT rotated effect ┘ off-screen extents
draw 3 ( 0, 0) 1280x720 full-screen layer
draw 4 ( 64, 115) 1133x281 ptlogo_back2eff / ptlogo_back2eff5 [22 or 24]
draw 4 ( 70, 126) 1120x263 ptlogo_back2 [23]
draw 5 ( 186, 191) 915x115 ptlogo1 [0]
draw 5 (1126, 378) 38x18 ptlogo_tm [11]
draw 6 ( 134, 306) 992x104 ptlogo2 [1]
draw 7 ( 294, 655) 691x18 ptcopyright [28]
draw 7 ( 384, 551) 512x50 ptbtn00 ← build 2, not build 7
draw 8 ( 371, 536) 538x76 ptbtn00f ← build 2, not build 7
```
Bracketed numbers are `GP_TITLE.pak` build 7's declaration indices
(`sylpheed-cli screen info … --build 7`). Sizes match to ≤4 px; the quads run a
few pixels under their sprite (919×113 → 915×115), whose cause is **unmeasured**
— a scale just under 1, or a UV inset.
### Two things fall out of it immediately
**1. Declaration order is refuted per-draw, not just by a framebuffer.** The
background paints **first** and `ptlogo1`/`ptlogo2` (elements 0 and 1) paint
*after* elements 22/23 — an order no reading of the table produces. Reverse
order dies with it: `ptcopyright` (28) is second-to-last, not first.
**2. The visible title screen is more than one bundle.** `ptbtn00` (513×50, the
`PRESS Ⓐ BUTTON` plate) and `ptbtn00f` (537×76) are **not among build 7's 30
elements** — they are the whole of `GP_TITLE.pak` **build 2** (and its language
twin build 3), a one-element bundle: `ptbtn00.rat`, kind `0x73002`, focus-linked
to `ptbtn00f.rat`, resting at **(383,550)**. The capture puts that quad at
**(384,551)**, which is a one-pixel agreement between a static placement decode
and the running game, on a bundle nothing had checked before. So the
screen the player sees is a composite of at least two builds, and **no single
build's element table can be the paint order**, whatever its order. Any rule has
to sequence *bundles* as well as elements inside one.
This also independently corroborates the pivot fix landed earlier the same day:
the full-screen layers cover exactly (0,0)(1280,720), which is where
pivot-anchored scaling puts `ptbase2` at 200 % and not where corner-anchored
scaling puts it.
## Refuted, with this capture as the ground truth
* **declaration order** and **reverse declaration order** — above.
* **keyframe start time** — `ptlogo1` (first keyframe t=26) paints *after*
`ptlogo_back2` (t=112).
* **resting-keyframe time** — same pair: rest t=42 after rest t=126.
* the placement-region order and the RATC child order were already refuted
statically (`BACKLOG.md`); nothing here rehabilitates them.
## Not settled
* **The rule.** Nothing yet found in the bundle orders these draws. What has
*not* been checked against this new ground truth: a depth/priority field inside
the per-element `.rat` record (as opposed to the 60-byte declaration entry,
which was dumped and carries no such field), and whatever sequences the two
bundles.
* **Three of the twelve quads are unidentified**: the two full-screen layers
(one is `ptbase2` at 200 %, the other is `pteff04` or a `PRMD`; the capture
cannot tell them apart because both are full-screen and both sample a page)
and the rotated pair, whose 883×1134 and 1299×1303 bounding boxes match no
sprite because the quads are rotated.
* **One screen only.** This is `GP_TITLE`. A rule needs a second screen with a
background and overlapping foreground; the same hook now makes that a
ten-minute run rather than a static argument.