Files
Sylpheed/docs/re/ui-paint-order-third-permutation.md
Sylpheed RE agent a112bc9feb tools: fix four boot-harness bugs; find the swallowed-keystroke cause and refute it
Chasing why (A) does not advance the title turned up four harness bugs, one real
root cause, and a refutation of that root cause as the explanation.

FIXED, each verified:
  * boot_menu.sh sent the emulator stdout to /dev/null, which is why none of the
    rest was visible. It now keeps a log and prints its path.
  * skip_intro.sh probed pixel (625,618) for the green (A) glyph - a 1280x720
    coordinate. screenshot returns the 1279x675 game surface, where that point
    is on the copyright line and reads (8,17,31). Symptom: a 600s TIMEOUT with
    the title on screen. Now classified with screen_id.py; next boot printed
    "TITLE at 239s -> A".
  * skip_intro.sh no longer taps through the movies. Making that press real (the
    vgamepad fix) was a REGRESSION: the boot then reached the title in 90s and
    that title accepted nothing. menu_draw_capture.sh records the same finding
    independently.
  * boot_menu.sh signs in whichever profile exists instead of a hard-coded XUID.

ROOT CAUSE FOUND: naming a XUID with no profile behind it opens a sign-in
dialog, xam_dialogs_shown_ goes to 1, and IsUIActive() then discards every
XamInputGetKeystrokeEx. Measured 8,388,601 swallowed calls in one boot with the
pad log showing presses arriving normally throughout. Only B13EBABEBABEBABE
exists on disc; the script asked for E0300000EFBEA3D4. Signing in the real
profile takes the swallow count 8,388,601 -> 0.

AND REFUTED: that is not why the title is stuck. With a valid profile and zero
swallowed keystrokes, a single (A) on the title that ends the boot still does
nothing. The dialog bug was real and worth fixing; it is not the explanation.

Next probe is specific: --log_mask=13 records no kernel calls, so it is not even
known whether the game polls at the title. A log on the SUCCESSFUL
XamInputGetKeystrokeEx return, mirroring the one already on the swallow path,
answers it in one boot.
2026-08-19 08:21:08 +00:00

8.4 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.

The boot log states it outright, which is the only reason it was caught:

movie (rmse 1662) at 434s -> skip A
tools/re-capture/skip_intro.sh: line 44: vgamepad: command not found

Every "-> skip A" and every "TITLE -> A" this harness has ever printed pressed nothing. Note the timing too: the movie ran to 434 s because it was never skipped, which is most of why a boot takes seven minutes here.

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 log shows [file-pad] keystroke vk=5800 down/up for every press — the title screen does not advance. Holds of 0.10 s, 0.25 s and 0.40 s, Ⓐ and START, on a screen that unambiguously renders PRESS Ⓐ BUTTON (capture).

A root cause found, measured, and fixed — a sign-in dialog

Keeping the emulator's stdout (see below) produced the line immediately:

w> [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (dialogs=1 nui=0, 8388601 so far)

xam_dialogs_shown_ is 1, so XamState::IsUIActive() returns true and xam_input.cc discards every keystroke — 8 388 601 of them in one boot, while the pad log shows presses arriving normally the whole time.

The dialog is a sign-in UI opened because the requested profile does not exist. boot_menu.sh passed --logged_profile_slot_0_xuid=E0300000EFBEA3D4; the only profile on disc is B13EBABEBABEBABE, and the boot log says so at line 309: FindProfiles: Adding profile B13EBABEBABEBABE to profile list, ProfileManager: Found 1 Profiles. Naming a XUID with nothing behind it is not a harmless no-op.

Fixed — boot_menu.sh now signs in whichever profile exists. Swallowed keystrokes: 8 388 601 → 0.

🔴 Refuted: that was not sufficient

Fixing the swallow does not make the title accept Ⓐ. With a valid profile signed in and zero swallowed calls, the title still does not advance. So the dialog bug was real and worth fixing, and it is not the explanation.

That also means menu_draw_capture.sh's note — "the title that ends the boot sequence accepts a single (A) (2 of 2 runs)" — no longer reproduces. The run here reached the title at 239 s, pressed once, and stayed.

Three more harness bugs, all of which read as 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 — a hang deep in the emulator, to look at. run-canary's own header documents this trap; the scripts predate it.

vgamepad no longer exists. skip_intro.sh still called vgamepad tap A 250, and without set -e the call failed silently: the branch pressed nothing and still exit 0. The boot log states it outright —

movie (rmse 1662) at 434s -> skip A
tools/re-capture/skip_intro.sh: line 44: vgamepad: command not found

— so every "→ skip A" and "TITLE → A" this harness ever printed pressed nothing, and the intro ran to 434 s because it was never skipped.

Do not tap through the movies. Making that press real for the first time was a regression: the boot then reached the title in 90 s and that title accepted nothing at all. menu_draw_capture.sh records the same thing independently — "a run that tapped (A) every 4 s through the boot delivered 88 presses and ended on a black screen that never came back", and "the title the attract loop returns to accepts nothing at all, and 40 taps at 1/s do not change that". skip_intro.sh now waits the intro out and spends its single press on the title that ends the boot.

The title probe sampled a dead pixel. skip_intro.sh tested p{625,618} for the green Ⓐ glyph — a coordinate from a 1280×720 grab. screenshot returns the 1279×675 game surface, where that point is on the copyright line and reads (8,17,31). The symptom was a 600 s TIMEOUT with the title on screen the whole time. Now classified with screen_id.py, which called those same frames "title"; the next boot printed TITLE at 239s -> A.

And boot_menu.sh sent the emulator's stdout to /dev/null — which is why none of this was visible. A boot harness that discards the emulator's own account of itself can only ever report symptoms. It now keeps a log and prints its path.

What is not settled

  • 🔴 The third permutation. The reader is ready and validated; the screen is not reachable.
  • 🔴 Why the title does not advance. The sign-in dialog is excluded, the vgamepad no-op is excluded, and the pad is confirmed delivering. The next probe needs a log level that records kernel calls — at --log_mask=13 the only mentions of XamInputGetKeystrokeEx in an 84 KB log are import-table entries, so it is not even known whether the game polls at the title. A targeted log on the successful return path (mirroring the existing one on the swallow path) would answer it in one boot.
  • Only one screen object was resident when the harness believed the main menu was up, where the title has seven.