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.
7.2 KiB
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_drawsis what separates them), and it writesxenia_re_ui_draws_NN.log— every draw of the next N frames, in submission order, undeduplicated; --ui_draw_capture_frames/--ui_draw_capture_maxbound 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 — butGraphicsSystem::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.screenshotwas grabbing a 10-pixel sliver. The app owns two windows of classxenia_canary— the 1280×745 game window and a 10×10 helper — and the wrapper took the first match fromxwininfo -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 largestxenia_canarywindow 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,
in submission order (full table):
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 afterptlogo_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
.ratrecord (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
ptbase2at 200 %, the other ispteff04or aPRMD; 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.