From 9da79b92d6783e4d8a155247ec6596014e09deec Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 18 Aug 2026 18:57:43 +0000 Subject: [PATCH] docs/re: the title screen's paint order, measured from the draw stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/re/BACKLOG.md | 21 +++ docs/re/INDEX.md | 3 +- docs/re/captures/title-screen-draw-order.log | 70 ++++++++++ docs/re/captures/title-screen-paint-order.txt | 13 ++ docs/re/ui-title-paint-order-capture.md | 130 ++++++++++++++++++ 5 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 docs/re/captures/title-screen-draw-order.log create mode 100644 docs/re/captures/title-screen-paint-order.txt create mode 100644 docs/re/ui-title-paint-order-capture.md diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 9c057702..12259628 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -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 diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 4b2d5061..ba555bcd 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -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 000–350 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 `.t32` sprite has a `.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 `.t32` sprite has a `.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 diff --git a/docs/re/captures/title-screen-draw-order.log b/docs/re/captures/title-screen-draw-order.log new file mode 100644 index 00000000..35b9c99d --- /dev/null +++ b/docs/re/captures/title-screen-draw-order.log @@ -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] diff --git a/docs/re/captures/title-screen-paint-order.txt b/docs/re/captures/title-screen-paint-order.txt new file mode 100644 index 00000000..0c9f0ac9 --- /dev/null +++ b/docs/re/captures/title-screen-paint-order.txt @@ -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) diff --git a/docs/re/ui-title-paint-order-capture.md b/docs/re/ui-title-paint-order-capture.md new file mode 100644 index 00000000..6343cfb6 --- /dev/null +++ b/docs/re/ui-title-paint-order-capture.md @@ -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 | +| 1–8 | the UI sprite shader (PS **`E59B2B3D`**), quad lists, positions in NDC | +| 9–10 | 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.