Answers the open half of the A-press diagnosis, and most of the answer was already in the tree. The faulting runs booted with logged_profile_slot_0_xuid = "" -- their own config dump -- while a profile existed (Found 1 Profiles). With nobody signed in, A takes the state-0 branch of sub_821D03A0 and calls XamShowSigninUI(1,1); Canary raises its Sign In dialog with a no-op close handler and nothing in an unattended run dismisses it. 85 instructions verified against the image, 0 mismatches; the state-3 branch is XamShowDeviceSelectorUI, already ruled out by storage_selection_dialog = false. The correlation runs through the tooling: boot_menu.sh passes the profile flag and Q4/Q5 pressed all five buttons; frame_clock.sh, which produced the faulting run, does not. So there is no blocker -- boot with boot_menu.sh. Flagged as retrodicted rather than A/B tested, since I have not myself booted both ways and pressed A. The uncomfortable half: canary-scripted-input-traps.md section 3 already named the sign-in dialog WITH a committed capture, and boot_menu.sh's header already carried the mechanism and the 8.4 million figure. The fault page searched for the cause it had hypothesised and never searched for its own symptom. Added to METHOD.md, along with the more expensive lesson -- a measurement whose only record is a script comment is invisible to the document that needs it. What this session did add is the join: that the known blackout is what drives an unbounded guest queue into a failed 128 MB allocation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
12 KiB
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:
// 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
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.
⚠️ 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
§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. 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.
The run's own config dump says the profile was not signed in
[Profiles]
create_profile_if_none = ""
logged_profile_slot_0_xuid = "" ← nobody logged in
...
i> FindProfiles: Adding profile B13EBABEBABEBABE to profile list
i> ProfileManager: Found 1 Profiles ← a profile EXISTS, it is just not signed in
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.
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) |
0x82307128 |
yes | different |
loader stall (../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.