Files
Sylpheed/docs/re/structures/title-a-press-fault.md
sylph-decoder e0de5c6f22 re: the A-press A/B is run -- signed-in profile, no swallow, menu opens
The debt from two iterations ago. Two boots, same binary and ISO, one A tap
each, fired only after the plate's pulse had been seen for 12 consecutive
samples. ARGV recorded per leg, because the config dump provably cannot say.

  leg A  no profile flag        3811 swallow lines and climbing
  leg B  --logged_profile_...   0 swallow lines, final glyph 327 = MAIN MENU

327 is the documented main-menu glyph count, reproduced by this instrument's own
control, so leg B's press opened the menu. Capture committed.

Leg A demonstrates the SWALLOW, not the crash: I stopped it at ~2.3 M swallowed
calls because kernel tracing at log_level=3 was eating the 300 MB budget the
crash dumps need. The fault itself remains measured once, historically. One run
per leg.

A void pair came first and is recorded, because it is why the detector is what
it is. The first version fired on a single frame over a glyph threshold and hit
the INTRO MOVIE -- green flashes of 1298..5433 lasting under a second -- about
6 s before the title, in both legs. The presses were real (each skipped the rest
of the movie, which is Q9's behaviour) but the pair tested nothing. The fixed
detector requires 12 consecutive in-band samples, and was replayed against the
void runs' own series as its control: it declines the movie flash at 84.8/85.5 s
and fires at 93.9/94.7 s inside the sustained pulse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 08:05:43 +00:00

15 KiB
Raw Permalink Blame History

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 0x701CF5F0below 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
11851252 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
125415242 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.

🔴 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 §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 0x824A90800x8284DABC = 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.

The A/B has now been run (2026-08-30), and it confirms the mechanism

Two boots, same binary, same ISO, one Ⓐ tap each, fired only after the plate's pulse had been seen for 12 consecutive samples. argv recorded per leg, because the config dump provably cannot say (see the retraction above).

leg profile flag swallow lines crash dumps final glyph outcome
A none 3 811 and climbing 0 (I stopped it) swallow storm
B --logged_profile_slot_0_xuid=B13EBABEBABEBABE 0 0 327 main menu

327 is the documented main-menu glyph count (live-main-menu.png), reproduced by this instrument's own control. So leg B's Ⓐ opened the menu: capture. Series for both legs: ../data/a-press-ab-legs.txt.

⚠️ Leg A demonstrates the SWALLOW, not the crash. I stopped it at 3 811 report lines — ~2.3 M swallowed calls — because kernel tracing at log_level=3 was eating the 300 MB size budget the crash dumps need. The crash arrives at ~13 982 report lines, and that part remains as it was: measured once, historically. One run per leg.

🔴 A void pair came first, and it is the reason the detector is what it is. The first attempt used a single frame over a glyph threshold and fired on the intro movie — which throws green flashes of 1 298…5 433 lasting under a second — about 6 s before the title. Both legs pressed into the movie, both showed zero swallow, and the pair meant nothing. (The presses were real: each skipped the rest of the movie, which is Q9's behaviour.) The detector now requires 12 consecutive samples inside a band the movie overshoots, and that rule was replayed against the void runs' own series as its control — it declines the movie flash at 84.8 / 85.5 s and fires at 93.9 / 94.7 s, inside the sustained pulse.

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.