Files
Sylpheed/docs/re/canary-scripted-input-traps.md
Sylpheed RE agent 8dbfeb97df docs/re: the boot-title recipe is 3/3 without my capture flag and 0/5 with it — and confounded
Using last iteration's recipe (first title, one (A)) four more times, it failed
every time. The nine-run table splits perfectly on --log_ui_draws: 3 of 3
successes without it, 0 of 5 with it. That would indict my own instrumentation
cvar, which is the one that captures paint order.

It is also perfectly confounded and the entry says so rather than picking the
exciting reading: every success had its title inside 147 s, every failure at
177 s or later. Flag and boot-time move together across all nine runs. A
mechanism is hard to see for the flag (it is read only when F10 arms a capture;
the per-draw hook is one relaxed atomic load), which argues for the boot-time
story — but "I cannot see how" is not evidence.

The separating experiment is named: ON runs with a fast boot. One that accepts
(A) exonerates the flag.

Also recorded: widening the menu signature last commit made screen_id classify
some boot frames as "menu", twice in one run before any menu existed. A dark-blue
movie frame and this menu are not separable from a single grab — navigation
scripts need the classification to hold across two grabs, which is a caller-side
fix, not a classifier one.
2026-08-19 00:00:43 +00:00

11 KiB
Raw Blame History

Getting past the title screen in the container — three traps and one blocker

Status: CONFIRMED for the three traps (each reproduced, and two of them fixed in the toolkit). 🔴 blocked on reaching the main menu: the guest reads Ⓐ at the title, and the step behind Ⓐ either crashes it or stalls it.

Written down because each of these presents as "the pad is dead" or "the game ignores input", and each has a completely different cause.

1. F10 opens the emulator's menu bar, and that swallows every guest keystroke

F10 is the RE hotkey (ship capture, and now log_ui_draws). It is also the toolkit key that opens the application menu bar, and it does both. With that menu open, XamState::IsUIActive() is true, so XamInputGetKeystrokeEx returns X_ERROR_SUCCESS with a zeroed keystroke before the driver is ever asked (xam_input.cc). The guest polls, gets an empty event, and does nothing — forever, and with no error anywhere.

This cost most of an iteration. Symptom: a run presses F10 to arm a capture, then every later Ⓐ does nothing; the title sits there while the pad log happily prints [file-pad] keystroke vk=5800 down. Screenshot captures/title-with-emulator-menu-open.png shows the giveaway — Open… / Open Recent / Install Content… / Exit over the title art. It is easy to miss because the menu is at the far left of the window and the game is still animating behind it.

Fix: after any F10, click once on the game surface to dismiss the menu. Escape does not close it. ui_draw_capture.sh now does this.

2. The pad is fine — the driver was never the problem

Worth stating because the file-pad's own history (--hid=file, GetKeystroke implemented specifically so "PRESS Ⓐ BUTTON" would work) makes it the natural suspect. It delivers: the log shows vk=5800 down/up per tap, and Ⓐ during a movie skips the movie, every time. When the title ignores Ⓐ, the pad is not why.

3. Ⓐ at the title needs a signed-in profile — and this container cannot type one in

With no profile, Ⓐ is handled: the guest calls XamShowSigninUI and Xenia pops its Sign In dialog (captures/title-signin-dialog.png), ProfileManager: Found 0 Profiles. That dialog is the proof the guest reads the button.

The dialog cannot be completed from here: "Create Profile" opens an ImGui text field for a gamertag, and synthetic X key events do not reach it. Tried, all with the window focused (xdotool getwindowfocus = the game window): xdotool type --window, xdotool type via XTEST, single xdotool key presses, clicking the field first. The field stays empty; Create with an empty gamertag is refused. Mouse clicks do work (they opened the dialog and the sub-panel), so it is text entry specifically.

Xenia's own Profile ▸ Show Profile Menu panel does have a working Create Profile button, and mouse clicks reach it — but the gamertag it then asks for is the same ImGui field, so the UI route dead-ends in the same place.

Fix, landed in Canary (auto/re-ui-draw-order): a --create_profile_if_none=<gamertag> cvar that calls ProfileManager::CreateProfile(tag, autologin, default_xuid) at startup when no profile exists. default_xuid is fixed at B13EBABEBABEBABE, so a script can then pass --logged_profile_slot_0_xuid=B13EBABEBABEBABE and be deterministic. Verified: ProfileManager: RE bootstrap profile 'SylphRE' -> created, then on the next boot Found 1 Profiles / Loaded SylphRE (GUID: B13EBABEBABEBABE) to slot 0, and the Profiles Menu panel shows the user (captures/xenia-profile-panel-signed-in.png). The profile persists on disk, so later runs need only --logged_profile_slot_0_xuid=B13EBABEBABEBABE.

Withdrawn: Ⓐ is not blocked — it works, intermittently

The main menu has been reached (captures/main-menu-reached.png — NEW GAME / LOAD GAME / TUTORIAL / OPTIONS / EXTRAS). So the previous version of this entry, which called the title's Ⓐ a hard blocker, is withdrawn. What is true is narrower and more annoying: Ⓐ advances the title only sometimes — one success in roughly four attempts, with the press verifiably delivered every time ([file-pad] keystroke vk=5800 down/up in the log) and no Xenia UI active.

The successful press landed on the first title after boot, before any attract cycle; the failures were on a title that had come round again. That is a hypothesis, not a rule — a later attempt on a first title also failed (the screen flickered to something else for one sample and came back), so timing is involved and the window has not been pinned down.

What was eliminated on the way, so nobody re-walks it

  • IsUIActive() is now observable. XamInputGetKeystrokeEx returns X_ERROR_SUCCESS with a zeroed keystroke before consulting any driver when a Xenia dialog is up — an invisible input blackout. Canary now logs [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (dialogs=N …) when that happens. On the runs where Ⓐ failed, it never fires, so the blackout is not the cause there. (It is the cause after F10 — trap 1 above.)
  • The driver filter is fine. The game polls with flags = 3, and InputSystem::FilterDrivers keeps a driver when flags & InputType is non-zero; the file pad reports Controller = 1, so it is selected.
  • The game does not query sign-in on the press. Tracing every Xam* call around a press shows only XamInputGetCapabilities, XamInputGetKeystrokeEx and XamInputGetState — no content, user or signin call at all.

Two traps in the measuring rig itself, which cost more than the bug

  • A FIFO trace consumer that exits stalls the emulator. --phase_a_event_log_path writes 1.5 GB a minute, so it was pointed at a named pipe with a grep on the other end. When that reader goes away, the emulator blocks on the write — and the symptom is exactly "the pad is dead": the guest stops polling, the driver never refreshes, nothing is logged. Two runs' worth of "the driver never reacts" was this, not the game.
  • The trace's return_value is a placeholder. phase_a_bridge::EmitReturn is called as EmitReturn(name, 0) — literally zero — so every kernel.return event reads "status": "0x00000000". "Every keystroke call returns SUCCESS" looked like hard evidence for the IsUIActive theory and means nothing. The theory had to be tested with a real log line instead.

The scripted reproduction was tried, and it failed

"Tap Ⓐ once per second and see whether the success rate goes to 1" was the next step this entry named. It was run (tools/re-capture/menu_draw_capture.sh), and the answer is no:

run what was pressed result
E one Ⓐ on the first title after boot main menu
F 40 × Ⓐ at 1/s on the title, then one each of START, B, BACK, X, Y title, unchanged
G Ⓐ every 4 s through the whole boot (88 presses) permanent black screen, no crash, never recovers

In run F every press was delivered — the driver logs keystroke vk=5800 down/up for each — and the new [RE-INPUT] line never fired, so nothing was swallowed. The guest received them and ignored them. No button does anything on that title: START, B, BACK, X and Y are as inert as Ⓐ (captures/title-unresponsive-attract.png).

Run G is worth its own warning: hammering Ⓐ through the boot is not merely useless, it breaks the run — 88 presses left a black screen that never came back (no crash, no throw, the emulator healthy). The script no longer taps during boot; the intro takes ~3.5 minutes and gets to the title by itself.

So the difference is not the number of presses or the button — it is which title you are looking at, and that is now measured rather than guessed:

  • the title that ends the boot sequence accepts a single Ⓐ and opens the main menu — 2 of 2, in independent runs, one of which never pressed F10;
  • the title the attract loop returns to accepts nothing — Ⓐ, START, B, BACK, X, Y, dozens of presses, every one delivered, none swallowed.

The recipe, then: launch, wait for the first title classification, tap Ⓐ once. Do not tap during the boot, and do not wait for a second title.

The tell is not visible in what the game draws — a log_ui_draws capture in each state gives 13 quads at identical rects, ptbtn00 (the PRESS Ⓐ BUTTON plate) included in both. See ui-title-paint-order-capture.md.

Still open

Why the attract title stops accepting input, given that it draws exactly the same thing. Nothing in the composition, the input path or the sign-in state differs; the difference is in the guest's own state machine, and reading it means reading that code.

Not open any more, and worth stating because it was the thing blocking the RE: reaching the main menu is now routine (first title, one Ⓐ), and the second screen's paint order has been captured with it.

2026-08-19 — the "first title accepts Ⓐ" rule is weaker than stated, and there is a confound

The entry above says the boot title accepts a single Ⓐ, 2 of 2. Continuing to use that recipe, it failed four times running. The full record, every run with the press verifiably delivered:

run --log_ui_draws title appeared at
crash-dir off ~83 s menu
boottitle off 147 s menu
A/B "off" off menu
menucap 1 on 292 s no (40 taps)
menucap 2 on 177 s no
optcap 1 on 241 s no
optcap 2 on 219 s no
A/B "on" 1 on never (black screen)
A/B "on" 2 on 186 s no

3 of 3 without the flag, 0 of 5 with it. That is a strong correlation, and it points at my own instrumentation cvar — which would be a serious finding, since --log_ui_draws is what captures the paint order in the first place.

It is also completely confounded, and the table shows it. Every success had its title inside 147 s; every failure had it at 177 s or later. Flag and boot-time are perfectly correlated across these nine runs, so this evidence cannot separate "the capture cvar breaks input" from "a title that arrives late is already in the attract state and does not accept input". Both stories fit all nine rows.

A mechanism is hard to see for the flag: log_ui_draws is read only when F10 arms a capture; the per-draw hook is one relaxed atomic load, and the swap hook another. Nothing there should reach the guest. That argues for the boot-time reading — but "I cannot see how" is not evidence, and the correlation is what was measured.

What would separate them, cheaply and next: run the flag ON several times and record (boot time → outcome). A single ON run whose title arrives inside 147 s and which then accepts Ⓐ exonerates the flag outright.

A defect I introduced, recorded rather than left to bite

Widening the menu signature (previous commit) made screen_id.py classify some boot/attract frames as menu — twice in one run, at t=54 s and t=146 s, long before any menu exists. The menu and a dark-blue movie frame are not separable from a single grab. Scripts that navigate on this must require the classification to hold across two grabs a second apart; the classifier itself cannot fix it.