Files
Sylpheed/docs/re/ui-paint-order-third-permutation.md
Sylpheed RE agent 52a77118ee docs+tools: screen_children.py, validated; the third paint order is blocked on input
screen_children.py walks every resident screen object (vtable 0x820b30b4), reads
the element array at +0x08 and the child array at +0x30, and prints the paint
permutation with each pivot. Validated in one run off the live title: it
reproduces BOTH previously measured orders character-for-character - the splash
[0,2,4,6,1,3,5] and the 24-element title permutation - so the next screen it is
pointed at can be trusted.

The screen that matters was not reached. GP_DIALOG DIFFICULTY carries exactly
one primitive and would say whether the game paints it first or last, which is
the bit that decides where primitives belong. But (A) does not advance the title
screen at holds of 0.10, 0.25 or 0.40s, and neither does START, with the pad
verified end to end: pad.py writes /tmp/xenia_pad.txt, the emulator runs with
--hid=file --pad_file pointing at it, and the mtime updates on every press. The
title is unambiguously the interactive one - PRESS (A) BUTTON is rendered.

Recorded as a blocker rather than worked around. Next step is specific: boot with
the emulator stdout kept and read the [RE-INPUT] IsUIActive log already present
on the canary branch, which would say whether the keystroke is being swallowed
emulator-side.
2026-08-19 07:48:58 +00:00

4.9 KiB
Raw Blame History

A third measured paint order — tool built and validated, screen not reached

Status: the reader works and is CONFIRMED against both previously measured screens. 🔴 BLOCKED on the emulator: Ⓐ does not advance the title screen, so GP_DIALOG's DIFFICULTY box — the screen that would answer the question — was never reached. Two harness bugs were found and fixed on the way.

Why a third permutation

Drawing the .prm primitives is implemented and measurably right on the title, but it is off by default because nothing derives where a primitive paints (structures/ui-prm-primitives.md). A primitive has no T8aD header, hence no layer key, and the two screens read off the running game disagree about the answer: the splash paints its primitive first, the title paints one at slot 4 and another last.

GP_DIALOG's DIFFICULTY box carries exactly one primitive, pceff00.prm, a 50 % black dim. Whether the game paints it first (dimming what is behind the dialog) or last (dimming the dialog too) is a single bit that would discriminate between the candidate rules. It is reachable from the main menu: NEW GAME.

The reader

tools/re-capture/screen_children.py walks every resident screen object (vtable 0x820b30b4), reads the element array at +0x08 (48-byte records, declaration order) and the child array at +0x30 (pointers to those records, paint order), and prints the permutation with each element's pivot — which is what identifies the build in the file.

Validated against both known-good screens in one run, off the live title:

== object 0xbcd25288: 7 elements, 7 children
   pivots: 0:(640,360) 1:(250,36) 2:(260,46) 3:(120,44) 4:(130,55) 5:(194,68) 6:(204,78)
   paint order: [0, 2, 4, 6, 1, 3, 5]

== object 0xbcd25488: 24 elements, 24 children
   paint order: [9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, 8]

Both are character-for-character what structures/ui-screen-runtime.md recorded from the hand-driven read. So the next screen this is pointed at can be trusted.

Two harness bugs found on the way

Both fail in ways that look like the game misbehaving.

--audio is not a cvar, and eight boot scripts passed it. Xenia calls ShowSimpleMessageBox from ParseLaunchArguments, before logging is initialised, so the symptom is a 10×10 window, an empty log, no guest memory, and a dialog blocking on XIfEvent forever — which reads as a hang deep in the emulator. run-canary's own header documents this trap; the scripts predate it. Fixed in all eight, and the boot now reaches the title.

vgamepad no longer exists. The uinput pad was replaced by the --hid=file driver plus pad.py, but skip_intro.sh still called vgamepad tap A 250. The script runs without set -e, so the call failed silently and the title branch pressed nothing while still exit 0 — reporting "TITLE → A" with the game sitting on the title. Now presses through pad.py and exits 6 if that fails.

That second bug looked like it would explain the standing "Ⓐ at the title works about half the time" note. It does not. See below.

🔴 The blocker: Ⓐ does not advance the title

With the pad verified end to end — pad.py writes /tmp/xenia_pad.txt, the emulator was launched with --hid=file --pad_file=/tmp/xenia_pad.txt, and the file's mtime updates on every press — the title screen does not advance:

press hold result
×2 0.10 s still title
0.25 s still title
×5 0.40 s still title
START 0.30 s still title

The screen is unambiguously the interactive one — PRESS Ⓐ BUTTON is rendered (capture). So the input path is wired and the game is waiting for a press it never sees.

The most likely suspect is already instrumented but was not captured this run: the [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive log added to xam_input.cc on the canary branch. boot_menu.sh sends the emulator's stdout to /dev/null, so nothing was recorded. That is the first step next time: boot with stdout kept and check whether the keystroke is being swallowed, which would make this an emulator-side bug rather than a game-side one.

What is not settled

  • 🔴 The third permutation itself. The tool is ready; the screen is not reachable.
  • Whether Ⓐ ever worked at this title screen, or whether the "half the time" note was always the silent vgamepad failure plus chance. The vgamepad bug is now excluded, and the title still does not advance, so something else is wrong.
  • Only one screen object was resident at the moment the main menu was believed to be up (the splash), where the title has seven. Unexplained, and possibly a symptom of the same thing — the menu may never have loaded.