Three findings and two corrections, all against the same capture. Corrections first, because one of them invalidates numbers already written down: * **the running screen is build 4, not build 7.** `screen info` defaults to the largest build; the game runs a different one, and the two disagree on sprite sizes (ptlogo1 822x100 vs 919x113 — the capture says 915x115). Every declaration index in the previous entry was against the wrong table. The conclusions survive unchanged; the indices are restated. * **the ship capture does NOT skip these UI draws.** This entry justified a new hook partly on "it requires an f32x3 position stream, which a 2D quad does not have". These quads have one: armed on the title, xenia_ship_capture_01.log holds 1150 records of which 965 are the UI sprite shader. The new hook is still the right tool — frame brackets, submission order, texture identity — but it was justified with a wrong reason. Findings: * **no depth in the geometry.** Attribute 0 is k_32_32_32_FLOAT, so every quad carries a Z, and every Z is 0.00000. Submission order is the whole ordering. * **no ordering field in the bundle.** Every word of build 4's declaration table dumped (committed as evidence): the unknowns are constant. The placement region's per-group lead word — never read before — is 0 for all 24 groups, and the region is followed straight by the RATC child stream, so no table hides behind it. * **the batching rule**, which is what makes the order readable: a draw holds one or two quads, and a two-quad draw always holds two elements adjacent in paint order that share a texture page. The engine flushes when the page changes; it does not sort by page (pages alternate). Reproduced in an independent run — same order, same rects — which is the second observation the CONFIRMED bar asks for.
11 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.
Correction (same day, next iteration): the running screen is build 4, not build 7
Everything above was matched against GP_TITLE.pak build 7, because that is
the largest build and what screen info defaults to. The game does not run it.
screen info --geometry prints each element's decoded sprite size, and the two
builds disagree:
| sprite | build 7 | build 4 (a60fcb85) |
captured quad |
|---|---|---|---|
ptlogo1.t32 |
822×100 | 919×113 | 915×115 |
ptlogo2.t32 |
888×93 | 992×104 | 992×104 |
ptlogo_back2.t32 |
1000×234 | 1118×262 | 1120×263 |
So the quads come from build 4 — a 24-element build — and the earlier index mapping was against the wrong table. The conclusion does not change, only the numbers: the paint order in build 4's declaration indices is
draw 1,2,4 full-screen layers 9 ptbase2 / 10 pteff04 (not separable — see below)
draw 3 two rotated effect quads ?
draw 5 ptlogo_back2eff(5) 18|20, then ptlogo_back2 19
draw 6 ptlogo1 0, then ptlogo_tm 7
draw 7 ptlogo2 1
draw 8 ptcopyright 21, then ptbtn00 ← ptbtn00 is build 2
draw 9 ptbtn00f ← build 2
Declaration order is still refuted (9 first; 0/1 after 18/19), reverse
order still refuted (21 second-to-last), and the two-bundle finding is
unchanged — build 2 is a one-element bundle holding ptbtn00.rat at rest
(383,550), against a captured (384,551).
Reproduced in an independent run (fresh boot, a signed-in profile, different
frame numbers): byte-for-byte the same order and the same rects. That is the
second observation the CONFIRMED bar asks for.
The batching rule, which is what makes the order readable
Draws carry one or two quads, and a two-quad draw always holds two elements that
are adjacent in paint order and share a texture page. Pages alternate
(0B2C, 11C3, 0BE3, 11C3, 0B2C), so this is not a sort by page: the engine
accumulates quads while the bound page stays the same and flushes when it
changes. The paint order is therefore the concatenation of the draws' quads, in
vertex order within each draw.
Two more orderings refuted, and one non-place to look
There is no depth in the geometry. Attribute 0 of a UI quad is
k_32_32_32_FLOAT — a float3 — so the stream carries a Z. Every Z in the capture
is 0.00000, on every vertex of every quad of every frame. The submission
order is the whole of the ordering; nothing is left to a depth test.
There is no ordering field in the bundle either. Dumping build 4's whole
declaration table word by word (docs/re/captures/title-build4-decl-words.txt):
+28 is 0 on all 24 entries, +32 is 0xffffffff (no parent) on all 24, +36
is 0xffffffff except on the three kind = 0x4 repeat instances where it is
0/1, +44 is 0xffffffff on all 24, +56 is 0 on all 24. The placement
region's per-group lead word — the one word of that header this project had never
read — is 0 for every group. And the region is followed immediately by the
RATC child stream (opt + ptlogo1.t32 + T8aD), so there is no table after
it to hold a draw order.
So the paint order is not carried by the file. It is produced by the guest's
screen code, and settling it means reading that code (the splash path noted in
the emulator-era work — sub_821CC7A0, item vtable 0x820b30b4 — draws with
exactly the PS hash E59B2B3D this capture sees).
A correction to this entry's own premise
It says the F10 ship capture "explicitly drops UI draws … it requires an f32x3
position attribute, which a 2D quad stream does not have". These quads have
one. Armed on the title screen, xenia_ship_capture_01.log holds 1 150 draw
records of which 965 are the UI sprite shader (stride=24 prim=13,
VS E0BAFB4F).
The new hook is still the right tool, for reasons that are about the record and not the filter: the ship capture writes no frame boundaries, no submission sequence across frames, no bound-texture identity, and de-dups on a key that happens not to collide here only because the game rebuilds its UI vertex buffer every frame. But "it cannot see UI draws" was wrong, and the cost of the new hook was justified with a wrong reason.