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.
8.4 KiB
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
vgamepadno-op is excluded, and the pad is confirmed delivering. The next probe needs a log level that records kernel calls — at--log_mask=13the only mentions ofXamInputGetKeystrokeExin 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.