# Pressing โ’ถ on the title faults the guest โ€” SOLVED, and it is the emulator swallowing input **Classification: measured** (the mechanism, from the run's own retained log) on a **decoded** code path (the three functions, read out of the image). Xenia Canary, 2026-08-29. This is the blocker that gated every menu-side dynamic question in this container, and it is not a mystery any more. ## The one-line answer Xenia's `XamInputGetKeystrokeEx` returns **`X_ERROR_SUCCESS` with a zeroed keystroke, on every call, for as long as a XAM dialog is up**. The game's keystroke pump is `while (GetKeystrokeEx(...) == SUCCESS) queue.push_back(ks);` with **no bound**. Something raised a XAM dialog immediately after the third โ’ถ was delivered, and the pump then queued **8 388 608** empty keystrokes, grew its vector to 64 MB, asked for 128 MB, got a failed allocation back **unchecked**, and copied off the end of the guest thread stack. So the fault is a *symptom two levels down* from an emulator-side input blackout. Nothing is wrong with the disc, the title screen, or the โ’ถ button. ## ๐Ÿ”ด Retraction โ€” "`r9` is a wild pointer, above 4 GB, never a guest address" That is this page's own claim, written 2026-08-29 at `72e45a7`, and it is **wrong**. `Access Violation: write at 0x00000001701D0000` prints `ex->fault_address()`, which `exception_handler_posix.cc:154` fills from **`signal_info->si_addr`** โ€” a *host* address. Xenia maps the guest at `mapping_base_`, chosen in `memory.cc:193` as the first `1ull << n` from n=32 that maps, i.e. **`0x100000000`**. The register file proves the translation rather than assuming it: the faulting instruction is `sth r6, 0(r9)` and the dump shows ``` r9 = 00000000701D0000 Access Violation: write at 0x00000001701D0000 ``` `0x1701D0000 โˆ’ 0x100000000 = 0x701D0000 = r9`. So `r9` **is** a guest address, in the `v40000000` heap (`0x40000000 โ€ฆ 0x7EFFFFFF`), and the page is simply not committed. The distinction matters: "garbage pointer" pointed the next probe at memory corruption; the truth points it at an allocation that failed. โš ๏ธ **Generalise this.** Every `Access Violation: โ€ฆ at 0x1________` in a Canary log from this container is a guest address plus `0x100000000`. Subtract before reading. ## The code path, read out of the image (0 mismatches against `sylpheed.db`) All three functions were disassembled from `/image/sylpheed.pe` and cross-checked word-for-word against the database: **466 + 120 instructions, zero disagreements** across `sub_82457038`, `sub_82457780` and their callees. | | what it is | how that is known | |---|---|---| | `sub_824574C0` | lazy singleton getter for the **input manager** at guest `0x828F3888`, guarded by a bit-0 "constructed" flag at `0x828F3A70` | `lis r11,0x828F; addi r30,r11,14472` = `0x828F3888`; classic guard-variable shape | | `sub_82457038` | the **keystroke pump**: drains `XamInputGetKeystrokeEx` into a vector at `this+68` = `0x828F38CC` | calls `sub_824AA870`, which is `b 0x8284DBDC` = the **`XamInputGetKeystrokeEx`** import thunk (`imports`, ordinal 408) | | `sub_82457780` | that vector's **insert-with-grow** | `{ptr@+0, size@+4, capacity@+8}`; doubles capacity, clamps at `0x1FFFFFFF`, `slwi r3,r27,3` for the byte count | The element is **8 bytes copied as four halfwords** at offsets 0/2/4/6 โ€” which is exactly `X_INPUT_KEYSTROKE` `{u16 VirtualKey; u16 Unicode; u16 Flags; u8 UserIndex; u8 HidCode}`. That is what makes the vector identifiable as a keystroke queue and not some other 8-byte record. The pump, in C: ```c // sub_82457038, 0x82457174 โ€ฆ 0x824571C8 while (XamInputGetKeystrokeEx(&user, 3, &ks) == X_ERROR_SUCCESS) { if (v->size < v->capacity) v->data[v->size++] = ks; // 0x8245718C else insert_slow(v, end, &ks); // 0x824571B0 โ†’ sub_82457780 } ``` There is no iteration cap and no check on the allocator's return. ## The emulator half โ€” `xam_input.cc:197` ```cpp if (kernel_state()->xam_state()->IsUIActive()) { ... return X_ERROR_SUCCESS; // keystroke was zeroed above } ``` `IsUIActive()` is `is_xam_dialog_present_`, set to true by every non-headless `XamShow*UI` path in `xam_ui.cc` and cleared only by a dialog's close handler. While it is set, the guest's `== SUCCESS` loop can never terminate. โš ๏ธ This is **upstream Canary behaviour**, not one of this container's RE patches. The RE patch is only the `[RE-INPUT]` logging around it โ€” and that logging is what made the diagnosis possible, so it earned its keep. ## The number that closes it The instrumentation reports one line per 600 swallowed calls. Immediately before the first crash dump: ``` [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (ui_active=true, 8388601 so far) ``` and the crash dump's own registers say how many records the vector held: ``` r29 = 0000000000800000 = 8 388 608 elements to copy r26 = 0000000000800001 = new size r27 = 0000000001000000 = new capacity (doubled) r30 = FFFFFFFF828F38CC = the vector object โ€” the pump's queue r31 = 00000000A7AC0000 r7 = 00000000A3AC0000 โ†’ 0x04000000 = 64 MB of live data ``` **8 388 601 swallowed calls against 8 388 608 queued records โ€” a gap of 7, inside the 600-call reporting granularity.** One push per swallowed poll. The two numbers are independent instruments (a Canary log counter and a guest register file) and they agree; that is the whole argument, and it needs no further run. ## Why `r3` looked like a stack pointer `slwi r3, r27, 3` = `0x8000000` = **128 MB** requested from `sub_824F7240` (`b 0x82150000`, the game's `heap_alloc(*0x828E2B14, size, &out)` wrapper). It came back as `0x701CF5F0` โ€” **below** the pump thread's own `r1 = 0x701CF7B0`, i.e. a pointer into a stack frame that had already been popped. The copy then walked `+0xA18` and hit the top of the thread's 64 KB stack at `0x701D0000`. So: the allocation failed, the failure path left a stale `&local` in `r3`, and the caller never checked. A 128 MB request on top of a live 64 MB one, in a guest with 512 MB total, is not a surprising failure. ## The timeline, from the log | log line | event | |---|---| | 1149 | first `XamInputGetKeystrokeEx` reaches a driver | | 1185โ€“1252 | **three** โ’ถ press/release pairs delivered โ€” `vk=5800`, flags `0001` down / `0002` up | | 1253 | the third โ’ถ **up** is handed to the guest | | **1254** | `swallowed by IsUIActive (ui_active=true, 1 so far)` โ€” the blackout starts | | 1254โ€“15242 | 13 982 swallow reports = ~8.39 M swallowed calls | | 15243 | first `==== CRASH DUMP ====`, `PC 0x824578A0` | Evidence: [`../data/a-press-fault-log-extract.txt`](../data/a-press-fault-log-extract.txt). โš ๏ธ Note the feedback loop that produces **32 356** dumps rather than one: a guest crash makes Xenia call `ImGuiDialog::ShowMessageBox` (`emulator.cc:1487`), which is itself a UI โ€” so the swallow can only get worse after the first fault. ## โœ… ANSWERED โ€” it is the **sign-in** dialog, and the run had no profile signed in Not a new measurement: **the corpus already had this**, and this page failed to connect to it. [`canary-scripted-input-traps.md`](../canary-scripted-input-traps.md) ยง3 says it outright โ€” *"With no profile, โ’ถ **is** handled: the guest calls `XamShowSigninUI` and Xenia pops its Sign In dialog"* โ€” with a committed capture, [`title-signin-dialog.png`](../captures/title-signin-dialog.png). And `tools/re-capture/boot_menu.sh`'s own header has carried the mechanism, **including the 8.4 million figure**, since before this page was written. ### ๐Ÿ”ด RETRACTED โ€” "the run's config dump says the profile was not signed in" This section read the faulting run's `[Profiles]` block โ€” `logged_profile_slot_0_xuid = ""` โ€” as evidence that nobody was signed in. **That inference is wrong, and I refuted it with a direct test.** Xenia prints its config dump **before applying command-line overrides**. In a run launched with `--apu=sdl --hid=file --mute=true --log_mask=13`, the dump says: | dumped | actually passed | |---|---| | `apu = "any"` | `--apu=sdl` | | `hid = "any"` | `--hid=file` | | `mute = false` | `--mute=true` | | `log_mask = 0` | `--log_mask=13` | Four for four. **The dump is the config *file*, not the run.** So `logged_profile_slot_0_xuid = ""` says only that the file is empty; the faulting run may well have had the flag on its command line, and this page cannot tell. โš ๏ธ **Anything in this corpus that cites a Canary config dump as evidence of what a run did is making the same mistake.** The dump is a statement about `xenia-canary.config.toml`. To know a run's settings, record its **argv**. What survives untouched: the *mechanism* (swallow โ†’ unbounded pump โ†’ failed allocation โ†’ fault), which rests on the `[RE-INPUT]` log counter and the crash dump's register file, neither of which is a config dump; and [`canary-scripted-input-traps.md`](../canary-scripted-input-traps.md) ยง3's measured claim that a profile-less โ’ถ pops the sign-in dialog, which is somebody else's observation with a capture behind it. What does **not** survive: this page's claim to know the faulting run's profile state. It does not. ### And the call site is now located in the image, not only observed `sub_821D03A0` is the state machine that raises it, verified byte-for-byte (**85 instructions, 0 mismatches**): | state at `[[r31+8]+4]` | branch | call | |---|---|---| | **0** | `0x821D04CC` | `li r4,1; li r3,1; bl 0x824A9068` โ†’ thunk `0x8284DA8C` = **`XamShowSigninUI(1, 1)`** | | **3** | `0x821D04A4` | `bl 0x824A9080` โ†’ `0x8284DABC` = `XamShowDeviceSelectorUI` | | other | โ€” | nothing | So the two candidates this page listed are both real branches of one function, and the run took the **state-0** one. The device selector was already ruled out by `storage_selection_dialog = false`; this identifies the other by address rather than by elimination. Canary's `xeXamShowSigninUI` then sets the flag and dispatches `ui::SigninUI` **asynchronously with a no-op close handler** โ€” so in an unattended run nothing ever dismisses it, and the swallow is permanent. ### The correlation runs through the tooling, not just this one log | launcher | passes `--logged_profile_slot_0_xuid` | โ’ถ outcome | |---|---|---| | `boot_menu.sh` | **yes** | Q4/Q5 pressed all five menu buttons | | `frame_clock.sh` (this run) | **no** | faulted, 4/4 | ### ๐Ÿ”ด The process failure, which is the part worth keeping This page said *"it does not explain how Q4/Q5 pressed โ’ถ successfully; what differs is unfound."* **It was found, twice, and written down in two places this page did not read** โ€” a sibling `docs/re/` page and a tool header. The corpus knew the swallow and knew the profile requirement; nobody had joined either to the crash. What this session actually adds is the **join**: that the known input blackout is what drives an unbounded guest queue into a failed 128 MB allocation, with the counter and the register file agreeing to 7. Recorded in [`METHOD.md`](../METHOD.md). ## What this unblocks, and how The blocked list โ€” main-menu sweeps, whether a `.tbm` draws pixels, `pbafc.prm`'s blend โ€” needs a screen behind an โ’ถ press. โœ… **There is no blocker. Boot with `tools/re-capture/boot_menu.sh`**, which signs the existing profile in, and the state-0 branch never fires. That is the launcher Q4 and Q5 used, and it has been in the tree the whole time. โš ๏ธ **This is a prediction retrodicted, not an A/B I ran.** The mechanism is measured and the launcher difference is documented, and Q4/Q5 are the oracle evidence that โ’ถ works under `boot_menu.sh` โ€” but I have not myself booted both ways and pressed โ’ถ. Until someone does, treat "signing the profile in prevents the fault" as strongly supported rather than demonstrated. If it ever needs a belt-and-braces second route, patching `xam_input.cc:217` to return `X_ERROR_EMPTY` instead of `X_ERROR_SUCCESS` terminates the pump immediately and is closer to hardware โ€” a real Xbox does not hand a game an infinite run of empty keystrokes. That is an emulator change and must be recorded as one wherever it is used. โš ๏ธ **Whichever route is taken, keep `tools/re-capture/frame_clock.sh`'s size guard.** It killed this run at its 300 MB cap and worked exactly as designed; without it the next fault fills a filesystem that was already at 91 %. ## It was never the same failure as the other two | | PC | crash dumps | cause | |---|---|---|---| | cache-flush crash ([`../title-crash-stl-tree.md`](../title-crash-stl-tree.md)) | `0x82307128` | yes | different | | loader stall ([`../canary-scripted-input-traps.md`](../canary-scripted-input-traps.md)) | โ€” | **zero** | different | | **this** | `0x824578A0` | 32 356 | **emulator input blackout โ†’ unbounded guest queue** | And it explains the thing the old page could not: **why Q4 and Q5 pressed โ’ถ successfully and these runs did not.** Nothing about the game differs. What differs is whether a XAM dialog happened to be up, which is emulator state, not guest state โ€” so "it reproduced 4/4" and "it worked before" are both true and always were. ## ๐Ÿ”ด Also refuted: the earlier "unimplemented instruction" hypothesis Kept from the previous version of this page because the negative still stands. `break_on_unimplemented_instructions = true` looked like a one-flag fix; booting with it false faults identically, and **no `Unimplemented instr` line is ever logged**. That path emits its `XELOGE` *before* the guarded break, so its absence rules the mechanism out. The dump comes from `Emulator::ExceptionCallback`, which fires on a genuine guest exception.