This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/ui-title-paint-order-capture.md
Sylpheed RE agent 53642ee3d6 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.
2026-08-18 18:57:43 +00:00

7.2 KiB
Raw Blame History

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.logevery 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, 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 timeptlogo1 (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.