re: the A-press fault is SOLVED -- Xenia swallows input, the guest pump is unbounded
The 326 MB log from the failing run was still on disk, so this needed no emulator time at all. Mechanism: Xenia's XamInputGetKeystrokeEx returns X_ERROR_SUCCESS with a zeroed keystroke on every call while a XAM dialog is up (xam_input.cc:197, upstream Canary). The game's keystroke pump -- sub_82457038, read out of the image -- is an unbounded 'while (GetKeystrokeEx() == SUCCESS) queue.push_back()'. It 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 top of the guest stack. Two independent instruments agree to within 7: the Canary counter's last report before the crash says 8 388 601 swallowed calls; the crash dump's r29 says the vector held 8 388 608. The reporting granularity is 600. Retracts this page's own 'r9 is a wild pointer above 4 GB'. Xenia prints si_addr, a host address; the guest is mapped at 0x100000000, so the fault address is guest 0x701D0000 -- which is exactly r9 in the register dump. Also refutes nothing of the port's, but answers its ask #3: the two press-a captures are different frames (40.84 % of the band's pixels differ at the best alignment, which has a sharp minimum), so its 0.301 % is not an instrument floor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
@@ -2529,7 +2529,69 @@ whatever it returns — the same reason a truncated log and a t=0 render both lo
|
||||
fine from inside. Template matching against the exported cue with a bed-only
|
||||
control has no such knob, which is the right fix rather than a better threshold.
|
||||
|
||||
## 2026-08-29 — the Ⓐ blocker, diagnosed
|
||||
## 2026-08-30 — the Ⓐ blocker is SOLVED, and it was the emulator, not the game
|
||||
|
||||
**This supersedes the section below, which stands as the record of the wrong
|
||||
diagnosis it corrects.** Two things in it were wrong and one of them would have
|
||||
sent the next probe to the wrong place.
|
||||
|
||||
**What actually happens.** Xenia's `XamInputGetKeystrokeEx` returns
|
||||
`X_ERROR_SUCCESS` with a *zeroed* keystroke on every call, for as long as any XAM
|
||||
dialog is up (`xam_input.cc:197` — upstream Canary, not one of our patches). The
|
||||
game's keystroke pump is an unbounded `while (GetKeystrokeEx(...) == SUCCESS)
|
||||
queue.push_back(ks);`. A XAM dialog went up right after the third Ⓐ was delivered,
|
||||
the pump queued **8 388 608** empty keystrokes, its vector reached 64 MB, it asked
|
||||
for 128 MB, the allocation failed, the failure path left a stale stack pointer in
|
||||
`r3` **unchecked**, and the copy walked off the top of the guest thread stack.
|
||||
|
||||
**The number is the argument.** The Canary log's own counter says **8 388 601**
|
||||
swallowed calls at the last report before the crash; the crash dump's `r29` says
|
||||
the vector held **8 388 608** records. Two independent instruments, seven apart,
|
||||
inside the 600-call reporting granularity. No further boot was needed — the
|
||||
326 MB log from the failing run was still on disk.
|
||||
|
||||
🔴 **The retraction that matters to anyone reading a Canary crash.** This page said
|
||||
`r9` was "a wild pointer above 4 GB, never a guest address". It is a *host* address:
|
||||
Xenia prints `si_addr`, and the guest is mapped at `0x100000000`. `0x1701D0000 −
|
||||
0x100000000 = 0x701D0000`, which is exactly `r9` in the register dump — an ordinary
|
||||
guest heap address on an uncommitted page. **Subtract `0x100000000` from every
|
||||
`Access Violation … at 0x1________` before reading it.**
|
||||
|
||||
**And it explains the thing that had no explanation:** why Q4 and Q5 pressed Ⓐ
|
||||
successfully and four later runs did not. Nothing about the game differs. Whether a
|
||||
XAM dialog happens to be up is *emulator* state — so "reproduced 4/4" and "it worked
|
||||
before" were both true all along.
|
||||
|
||||
**For you, concretely:** nothing you ship changes. No disc fact moved, no screen,
|
||||
no timing. What changes is that the questions parked behind this — the BGM loop
|
||||
point (Q10) and whether menu music ducks under a cutscene — are no longer parked
|
||||
behind an unexplained crash. 🟡 They are not unblocked yet either: the route out is
|
||||
one of three (dismiss the dialog, `--headless`, or patch the swallow to return
|
||||
`X_ERROR_EMPTY`), none of them tried, and *which* dialog raises the flag is still
|
||||
unknown. Do not plan around a date.
|
||||
|
||||
Detail, with the disassembly and the log extract:
|
||||
[`docs/re/structures/title-a-press-fault.md`](../re/structures/title-a-press-fault.md)
|
||||
· [log extract](../re/data/a-press-fault-log-extract.txt)
|
||||
|
||||
## ✅ 2026-08-30 — your ask #3: the two `press-a` captures are DIFFERENT FRAMES, so 0.301 % is not a floor
|
||||
|
||||
You asked whether `live-title-press-a.png` and
|
||||
`live-attract-title-press-a-band.png` came from different configurations, because
|
||||
if they did not, the 0.301 % between them would be a floor under every full-frame
|
||||
comparison in the corpus. **They are different frames of a moving screen, and your
|
||||
own preferred reading is the right one.**
|
||||
|
||||
Sliding the 1279×120 band down every row of the 1279×675 capture gives a sharp,
|
||||
unambiguous minimum at **y = 520** (mean |Δ| 4.016, against 7.87/7.89 at ±1 row) —
|
||||
the sharpness is the instrument's control. At that alignment they disagree on
|
||||
**40.84 %** of pixels, max Δ **169**. A crop of the same frame would be zero.
|
||||
|
||||
⚠️ Reach: this shows the two *captures* differ, not that the two *configurations*
|
||||
agree — a moving background makes that unanswerable from these two images.
|
||||
[`ui-title-build-map.md`](../re/ui-title-build-map.md)
|
||||
|
||||
## 2026-08-29 — the Ⓐ blocker, diagnosed (superseded above; the wrong diagnosis, kept)
|
||||
|
||||
Not something you need, but it bounds what I can still answer, so it is worth
|
||||
having in one place.
|
||||
|
||||
@@ -179,4 +179,4 @@ files, which is how the same ground got covered twice.
|
||||
| [`structures/boot-splash-dwells-are-declared.md`](structures/boot-splash-dwells-are-declared.md) | How long each boot splash is shown | ✅ **decoded**: the dwells are the bundles' own declared timelines — publisher **t=0…255 = 4.250 s**, developer **t=0…210 = 3.500 s** at 60 units/s. The corpus's independent screenshot timing over 3 cold boots gives 4.30/4.60/4.37 and **3.51/3.50/3.37** — the developer agreeing to **1.1 %**, two of its three runs to 0.3 %. 🔴 **Wall clock is the wrong unit to author**: a fresh no-input boot measured the same two dwells at **5.10–5.61 s** and 3.83–4.30 s, 15–20 % longer than both the declared values and the corpus's runs, on the same disc — so a seconds figure is one run's emulator pacing. Boundaries from the draw stream: publisher wordmark frames 6–119, **3 frames with no sprite drawn**, developer glows 123, wordmarks 140–209, intro video 216. 🔴 **The frame→wall-clock instrument resolves to one BUFFER FLUSH, not one frame** — 69 of 125 samples showed no advance and the rest jumped 7–15 frames, making the apparent rate swing 0.0164–0.0316 s/frame; frames 119 and 123 fall in one burst, so the inter-splash gap is **not separable** by it. Quoted as brackets; sub-flush estimates withdrawn before reporting. ⚠️ `palogo_anima` never appears — almost certainly the 8-vertex cap (7 elements batched, 2 logged), the same trap as the `eff3` false negative, so it is named not reported. ❔ the publisher's 4.1 % error vs the developer's 1.1 % is unexplained |
|
||||
| [`structures/ui-forced-backdrop.md`](structures/ui-forced-backdrop.md) *(colour census + self-refutation)* | What colour a keyless element is, and which forced verdicts the argument actually supports | ✅ **decoded, disc-wide**: every full-screen `*eff00*` **primitive** is **pure black** at its various alphas (`ff000000`, `7f000000`, `40000000`, `b2000000`, `cc000000`, `d4000000`, `00000000`) — exactly an alpha-over dim or fade, and an *additive* black quad would be a no-op nobody would author. The **only** non-black primitive on the disc is `pbafc.prm`, RGB `00e8e0` cyan at alphas to `ff`, and it is **844×600, not full-screen**, so outside the backdrop rule's geometry guard — ❔ it is now the sole additive candidate. 🔴 **Self-refutation: of the 80 forced-first instances only 42 are `.prm`; 38 are `.tbm` carrying fade `ffffffff`.** A *solid* white quad painted first would make the screen white and no screen is white, so a `.tbm` is a white **modulation on a texture** — and element alpha does not establish its coverage. That is the `.t32` error one extension further out: I had fixed the symptom (`el.sprite.is_some()`) not the cause, **an element's alpha is not its texture's opacity, and only an untextured primitive makes the two the same fact**. So 42 verdicts stay **decoded**, 38 drop to 🟡 (still almost certainly right — all named `*base*`, full-screen, and `pfbase.tbm`'s first position is *measured* — but on a name-and-role argument this page elsewhere calls the weaker kind). ⚠️ Code deliberately unchanged: restricting to `.prm` would send eleven screens' backgrounds back to last, the blank-screen bug the rule fixed. Split pinned by a test |
|
||||
| [`structures/ui-prm-blend-mode.md`](structures/ui-prm-blend-mode.md) | Whether a primitive blends additively or alpha-over | ❔ **undecodable, with reach** — but the consequence is closed. Looked in **the bundle** (no field: the declaration words are constant and a primitive has no RATC child at all), **the colour census** (every full-screen `*eff00*` primitive is **pure black**; the only non-black primitive on the disc is `pbafc.prm`, cyan `00e8e0`), **the occlusion constraint** (inapplicable — `pbafc.prm` strobes 255/124 every 2 units, travels, and is scaled **2 %×3 %**, so it draws ~**17×18 px**, not its declared 844×600), and **the oracle** (`GP_READY_ROOM` is a recorded no-go and gameplay needs the Ⓐ that faults the guest). ✅ **Why it stopped mattering:** for a *black* quad the hypotheses differ only in whether it hides what is beneath — drawn **first** it is correct under **both**, drawn **last** only under additive. So `forced_backdrop`'s verdict is robust to the open question, and the port's original "layerless sorts last" was wrong under alpha-over and merely pointless under additive. ⚠️ This is not evidence *for* alpha-over. 🔴 The investigation found `forced_backdrop` judged coverage from the **pivot alone**, ignoring scale; checked first, **all 80 forced instances are at 100 %**, so no verdict moved and the added guard is defensive |
|
||||
| [`structures/title-a-press-fault.md`](structures/title-a-press-fault.md) | Why a single Ⓐ on the title faults the guest — the blocker on all menu-side dynamic RE | ✅ **measured**, 4 runs: `==== CRASH DUMP ====` with **`PC: 0x824578A0`**, *Access Violation: write at `0x00000001701D0000`*, repeating **32 356×** and writing **326 MB** in ~10 s. Four Ⓐ runs faulted; four no-input runs in the same sessions completed. 🔴 **Refuted, my own hypothesis**: it is NOT an unimplemented instruction — `--break_on_unimplemented_instructions=false` faults identically and **no `Unimplemented instr` line is ever logged**, and since that path emits its `XELOGE` *before* the guarded `DebugBreak()`, its absence rules the mechanism out. The dump comes from `Emulator::ExceptionCallback`, i.e. a genuine guest exception. ✅ **Instruction read from the image** (primary): `b0c90000` = **`sth r6, 0(r9)`**, first of four halfword stores at 0/2/4/6 through `r9` inside a `bne-` loop filling 8-byte records — so **`r9` is a wild pointer**, and `0x1701D0000` is above 4 GB, outside the guest's 32-bit space entirely. DB agrees: `sub_82457780`, +0x120. 🔴 **A THIRD failure mode**: not the cache-flush crash (`0x82307128`) and not the loader stall (which logs **zero** crash dumps) — and unlike the latter it reproduced 4/4, so "retry whole boots" does not obviously apply. ⚠️ Does **not** explain the corpus's earlier successes (Q4/Q5 pressed Ⓐ fine); what differs is unfound. ✅ `frame_clock.sh`'s 300 MB guard killed the run as designed |
|
||||
| [`structures/title-a-press-fault.md`](structures/title-a-press-fault.md) | Why a single Ⓐ on the title faults the guest — the blocker on all menu-side dynamic RE | ✅ **SOLVED 2026-08-30, and it is the emulator, not the game.** Xenia returns `X_ERROR_SUCCESS` with a *zeroed* keystroke on every `XamInputGetKeystrokeEx` while a XAM dialog is up (`xam_input.cc:197`, upstream); the game's pump is an **unbounded** `while (GetKeystrokeEx()==SUCCESS) queue.push_back()`, so it 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 top of the guest thread stack. ✅ **The number is the argument**: the Canary counter reports **8 388 601** swallowed calls at the last report before the crash, the dump's `r29` says the vector held **8 388 608** — two independent instruments, 7 apart, inside the 600-call reporting granularity. No new boot: the failing run's 326 MB log was still on disk. 🔴 **RETRACTED — "`r9` is a wild pointer above 4 GB"**. Xenia prints `si_addr`, a *host* address, and the guest is mapped at `0x100000000`: `0x1701D0000 − 0x100000000 = 0x701D0000`, which **is** `r9` in the dump — an ordinary guest heap address on an uncommitted page. Subtract `0x100000000` from every `Access Violation … at 0x1________` before reading it. ✅ **Decoded code path**, image-checked with **0 mismatches** over 586 instructions: `sub_824574C0` the input-manager singleton at `0x828F3888`, `sub_82457038` the keystroke pump, `sub_82457780` its `vector<X_INPUT_KEYSTROKE>` insert-with-grow. ✅ **It explains the earlier successes**: whether a XAM dialog is up is *emulator* state, so "reproduced 4/4" and "Q4/Q5 pressed Ⓐ fine" were both always true. 🟡 **Which** dialog is still open — `XamShowDeviceSelectorUI` is ruled out (`storage_selection_dialog = false` takes the headless path), `XamShowSigninUI` / `XamShowMessageBoxUIEx` are not; the settling experiment is one log line per `is_xam_dialog_present_.store(true)` site, not another blind boot. 🟡 Three untried routes out: dismiss the dialog, `--headless`, or return `X_ERROR_EMPTY` from the swallow. ✅ `frame_clock.sh`'s 300 MB guard killed the run as designed — keep it |
|
||||
|
||||
138
docs/re/data/a-press-fault-log-extract.txt
Normal file
138
docs/re/data/a-press-fault-log-extract.txt
Normal file
@@ -0,0 +1,138 @@
|
||||
# Extract from the Ⓐ-press fault run's xenia.log (2026-08-29)
|
||||
#
|
||||
# Source: /sylph-home/re/canary-build/bin/Linux/Release/xenia.log, 326 921 343 bytes,
|
||||
# mtime 2026-08-29 22:15. 32 356 '==== CRASH DUMP ====' blocks. This file is the
|
||||
# part that carries the diagnosis; the whole log is not committed (326 MB).
|
||||
#
|
||||
# ---- 1. the three real Ⓐ keystrokes, then the swallow begins (log lines 1180-1262)
|
||||
|
||||
i> 0100000C [UI-CAP] writing xenia_re_ui_draws_01.log (from frame 0)
|
||||
!> 0001278F MEM-WATCH rss=590MB (peak 590MB) vsz=19185MB malloc_inuse=313MB mmap=90MB cache_deque=40 cache_list=40
|
||||
!> 0001278F MEM-WATCH rss=590MB (peak 590MB) vsz=19189MB malloc_inuse=313MB mmap=90MB cache_deque=40 cache_list=40
|
||||
i> F8000008 [file-pad] #3 buttons=1000 lt=0 rt=0 lx=0 ly=0 rx=0 ry=0
|
||||
i> F8000008 [file-pad] keystroke vk=5800 down
|
||||
i> F8000008 [RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5800 flags=0001 (call flags 00000003)
|
||||
i> F8000008 [file-pad] #4 buttons=0000 lt=0 rt=0 lx=0 ly=0 rx=0 ry=0
|
||||
i> F8000008 [file-pad] keystroke vk=5800 up
|
||||
i> F8000008 [RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5800 flags=0002 (call flags 00000003)
|
||||
!> 0001278F MEM-WATCH rss=594MB (peak 594MB) vsz=19189MB malloc_inuse=314MB mmap=90MB cache_deque=40 cache_list=40
|
||||
!> 0001278F MEM-WATCH rss=595MB (peak 595MB) vsz=19189MB malloc_inuse=313MB mmap=90MB cache_deque=40 cache_list=40
|
||||
i> F8000008 XThreadF80000B8 (1A) Stack: 70880000-70900000
|
||||
K> F80000B8 XThread::Execute thid 26 (handle=F80000B8, 'XThread76FFE6C0 (F80000B8)', native=76FFE6C0)
|
||||
F> F80000B8 HostPathDevice::ResolvePath(\aab216c3\5\c10eae6)
|
||||
F> F80000B8 HostPathDevice::ResolvePath(\aab216c3\5)
|
||||
F> F8000084 DiscImageDevice::ResolvePath(\dat)
|
||||
F> F8000008 DiscImageDevice::ResolvePath(\dat\movie)
|
||||
F> F8000008 DiscImageDevice::ResolvePath(\dat\movie)
|
||||
i> F8000008 XThreadF8000154 (1B) Stack: 70880000-70890000
|
||||
i> F8000008 XThreadF8000158 (1C) Stack: 708B0000-708C0000
|
||||
i> F8000008 XThreadF800015C (1D) Stack: 708E0000-708F0000
|
||||
i> F8000008 XThreadF8000160 (1E) Stack: 70910000-70920000
|
||||
K> F8000158 XThread::Execute thid 28 (handle=F8000158, 'XThread6FFFF6C0 (F8000158)', native=6FFFF6C0)
|
||||
K> F8000154 XThread::Execute thid 27 (handle=F8000154, 'XThread75FFD6C0 (F8000154)', native=75FFD6C0)
|
||||
K> F800015C XThread::Execute thid 29 (handle=F800015C, 'XThread6EFFE6C0 (F800015C)', native=6EFFE6C0)
|
||||
!> 0001278F MEM-WATCH rss=660MB (peak 660MB) vsz=19473MB malloc_inuse=324MB mmap=90MB cache_deque=40 cache_list=40
|
||||
!> 0001278F MEM-WATCH rss=661MB (peak 661MB) vsz=19473MB malloc_inuse=324MB mmap=90MB cache_deque=40 cache_list=40
|
||||
!> 0001278F MEM-WATCH rss=661MB (peak 661MB) vsz=19473MB malloc_inuse=324MB mmap=90MB cache_deque=40 cache_list=40
|
||||
i> F8000008 [file-pad] #5 buttons=1000 lt=0 rt=0 lx=0 ly=0 rx=0 ry=0
|
||||
i> F8000008 [file-pad] keystroke vk=5800 down
|
||||
i> F8000008 [RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5800 flags=0001 (call flags 00000003)
|
||||
i> F8000008 [file-pad] #6 buttons=0000 lt=0 rt=0 lx=0 ly=0 rx=0 ry=0
|
||||
i> F8000008 [file-pad] keystroke vk=5800 up
|
||||
i> F8000008 [RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5800 flags=0002 (call flags 00000003)
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F8000154
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F8000158
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F800015C
|
||||
K> F8000160 XThread::Execute thid 30 (handle=F8000160, 'XThread6DFFD6C0 (F8000160)', native=6DFFD6C0)
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F8000154
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F8000158
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F800015C
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F8000160
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F8000160
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F8000160
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F8000160
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F8000160
|
||||
w> F8000008 XThread::Resume: host resume was refused for thread F8000160
|
||||
!> F8000008 BaseHeap::Release failed because address is not a region start: addr=1E4B0E00 heap_base=00000000 page=124080 owning_region_start=1C220000 region_page_count=14976 state=03
|
||||
!> F8000008 PhysicalHeap::Release failed due to parent heap failure
|
||||
!> F8000008 BaseHeap::Release failed because address is not a region start: addr=1E7A8F00 heap_base=00000000 page=124840 owning_region_start=1C220000 region_page_count=14976 state=03
|
||||
!> F8000008 PhysicalHeap::Release failed due to parent heap failure
|
||||
F> F8000084 DiscImageDevice::ResolvePath(\dat)
|
||||
!> 0001278F MEM-WATCH rss=830MB (peak 830MB) vsz=19501MB malloc_inuse=349MB mmap=90MB cache_deque=40 cache_list=40
|
||||
!> 0001278F MEM-WATCH rss=831MB (peak 831MB) vsz=19501MB malloc_inuse=349MB mmap=90MB cache_deque=40 cache_list=40
|
||||
!> 0001278F MEM-WATCH rss=831MB (peak 831MB) vsz=19501MB malloc_inuse=349MB mmap=90MB cache_deque=40 cache_list=40
|
||||
!> 0001278F MEM-WATCH rss=831MB (peak 831MB) vsz=19501MB malloc_inuse=349MB mmap=90MB cache_deque=40 cache_list=40
|
||||
!> 0001278F MEM-WATCH rss=831MB (peak 831MB) vsz=19501MB malloc_inuse=349MB mmap=90MB cache_deque=40 cache_list=40
|
||||
!> 0001278F MEM-WATCH rss=832MB (peak 832MB) vsz=19501MB malloc_inuse=348MB mmap=90MB cache_deque=40 cache_list=40
|
||||
!> 0001278F MEM-WATCH rss=832MB (peak 832MB) vsz=19501MB malloc_inuse=348MB mmap=90MB cache_deque=40 cache_list=40
|
||||
i> F8000008 [file-pad] #7 buttons=1000 lt=0 rt=0 lx=0 ly=0 rx=0 ry=0
|
||||
i> F8000008 [file-pad] keystroke vk=5800 down
|
||||
i> F8000008 [RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5800 flags=0001 (call flags 00000003)
|
||||
i> F8000008 XThreadF80000D4 (1F) Stack: 70880000-70900000
|
||||
K> F80000D4 XThread::Execute thid 31 (handle=F80000D4, 'XThread9BFFF6C0 (F80000D4)', native=9BFFF6C0)
|
||||
F> F80000D4 HostPathDevice::ResolvePath(\aab216c3\a\c7e701e)
|
||||
F> F80000D4 HostPathDevice::ResolvePath(\aab216c3\a)
|
||||
F> F80000D4 HostPathDevice::ResolvePath(\d5faa9db\e\b80b1a0)
|
||||
F> F80000D4 HostPathDevice::ResolvePath(\d5faa9db\e)
|
||||
F> F80000D4 HostPathDevice::ResolvePath(\d5faa9db\c\6dea48b)
|
||||
F> F80000D4 HostPathDevice::ResolvePath(\d5faa9db\c)
|
||||
i> F8000008 [file-pad] #8 buttons=0000 lt=0 rt=0 lx=0 ly=0 rx=0 ry=0
|
||||
i> F8000008 [file-pad] keystroke vk=5800 up
|
||||
i> F8000008 [RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5800 flags=0002 (call flags 00000003)
|
||||
!> 0001278F MEM-WATCH rss=833MB (peak 833MB) vsz=19501MB malloc_inuse=349MB mmap=90MB cache_deque=40 cache_list=41
|
||||
w> F8000008 [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (ui_active=true, 1 so far)
|
||||
!> F8000008 BaseHeap::Release failed because address is not a region start: addr=1DA98C80 heap_base=00000000 page=121496 owning_region_start=1C220000 region_page_count=14976 state=03
|
||||
!> F8000008 PhysicalHeap::Release failed due to parent heap failure
|
||||
w> F8000008 [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (ui_active=true, 601 so far)
|
||||
w> F8000008 [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (ui_active=true, 1201 so far)
|
||||
w> F8000008 [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (ui_active=true, 1801 so far)
|
||||
w> F8000008 [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (ui_active=true, 2401 so far)
|
||||
w> F8000008 [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (ui_active=true, 3001 so far)
|
||||
w> F8000008 [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (ui_active=true, 3601 so far)
|
||||
|
||||
# ---- 2. the last swallow report before the first crash dump (log line <15243)
|
||||
15236:w> F8000008 [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (ui_active=true, 8388001 so far)
|
||||
15237:w> F8000008 [RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive (ui_active=true, 8388601 so far)
|
||||
# total 'swallowed' report lines before the first crash dump:
|
||||
13982
|
||||
# they are emitted every 600th call, so swallowed calls ~= 600 x that count
|
||||
|
||||
# ---- 3. the first crash dump's GPRs (log line 15243+)
|
||||
!> F8000008 ==== CRASH DUMP ====
|
||||
Thread ID (Host: 0xEEFFE6C0 / Guest: 0x00000006)
|
||||
Thread Handle: 0xF8000008
|
||||
PC: 0x824578A0
|
||||
Access Violation: write at 0x00000001701D0000
|
||||
Registers:
|
||||
r0 = 0000000000000000
|
||||
r1 = 00000000701CF7B0
|
||||
r2 = 0000000020000000
|
||||
r3 = 00000000701CF5F0
|
||||
r4 = 0000000000000000
|
||||
r5 = 0000000000000000
|
||||
r6 = 0000000000000000
|
||||
r7 = 00000000A3AC0000
|
||||
r8 = 00000000701D0008
|
||||
r9 = 00000000701D0000
|
||||
r10 = 0000000000000000
|
||||
r11 = 00000000A3AC0A18
|
||||
r12 = 0000000082457864
|
||||
r13 = 000000003001E000
|
||||
r14 = 0000000000000000
|
||||
r15 = 0000000000000000
|
||||
r16 = 0000000000000000
|
||||
r17 = 0000000000000000
|
||||
r18 = 00000000BCE24BFC
|
||||
r19 = 0000000000000001
|
||||
r20 = 0000000000000000
|
||||
r21 = FFFFFFFFFFFFFFFF
|
||||
r22 = 00000000BC65D540
|
||||
r23 = FFFFFFFF828F3844
|
||||
r24 = 000000000000052F
|
||||
r25 = FFFFFFFF828E0000
|
||||
r26 = 0000000000800001
|
||||
r27 = 0000000001000000
|
||||
r28 = 00000000701CF898
|
||||
r29 = 0000000000800000
|
||||
r30 = FFFFFFFF828F38CC
|
||||
r31 = 00000000A7AC0000
|
||||
@@ -1,91 +1,205 @@
|
||||
# Pressing Ⓐ on the title faults the guest — diagnosed, and it is a third failure
|
||||
# Pressing Ⓐ on the title faults the guest — SOLVED, and it is the emulator swallowing input
|
||||
|
||||
**Classification: measured.** Xenia Canary, 2026-08-29, four runs. This is the
|
||||
blocker that gates every menu-side dynamic question in this container.
|
||||
**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.
|
||||
|
||||
## What happens
|
||||
## The one-line answer
|
||||
|
||||
A single Ⓐ press on the title screen produces a Xenia **`==== CRASH DUMP ====`**:
|
||||
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
|
||||
|
||||
```
|
||||
PC: 0x824578A0
|
||||
Access Violation: write at 0x00000001701D0000
|
||||
r9 = 00000000701D0000 Access Violation: write at 0x00000001701D0000
|
||||
```
|
||||
|
||||
It repeats **32 356 times** in one run, writing **326 MB** of register dump to
|
||||
stdout in roughly ten seconds. Four runs that pressed Ⓐ on the title all faulted;
|
||||
four runs in the same sessions that pressed nothing all completed normally.
|
||||
`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.
|
||||
|
||||
## 🔴 Refuted: my own hypothesis, that it was an unimplemented instruction
|
||||
⚠️ **Generalise this.** Every `Access Violation: … at 0x1________` in a Canary log
|
||||
from this container is a guest address plus `0x100000000`. Subtract before reading.
|
||||
|
||||
The config dump carries `break_on_unimplemented_instructions = true`, and Xenia's
|
||||
own message for that path reads *"report the game to Xenia developers; to skip,
|
||||
disable break_on_unimplemented_instructions"* — so the flag looked like the fix.
|
||||
## The code path, read out of the image (0 mismatches against `sylpheed.db`)
|
||||
|
||||
**It is not.** Booting with `--break_on_unimplemented_instructions=false` faulted
|
||||
identically, and **no `Unimplemented instr` line is ever logged**, on stdout or
|
||||
stderr, in any run. That path emits its `XELOGE` *before* the guarded
|
||||
`DebugBreak()`, so its absence rules the mechanism out rather than leaving it open.
|
||||
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.
|
||||
|
||||
The dump comes from `Emulator::ExceptionCallback` (`emulator.cc:1468`), which fires
|
||||
on a **genuine guest exception** — an access violation or illegal instruction
|
||||
inside guest code — not on a translation failure.
|
||||
|
||||
## The faulting instruction, read from the image
|
||||
|
||||
`sylpheed.db` puts the PC inside `sub_82457780` (0x82457780…0x82457958, non-leaf,
|
||||
frame 144), +0x120 in. The image is primary and gives the instruction itself:
|
||||
|
||||
| address | word | instruction |
|
||||
| | what it is | how that is known |
|
||||
|---|---|---|
|
||||
| **0x824578A0** | `b0c90000` | **`sth r6, 0(r9)`** ← faults |
|
||||
| 0x824578A4 | `b0a90002` | `sth r5, 2(r9)` |
|
||||
| 0x824578A8 | `b0890004` | `sth r4, 4(r9)` |
|
||||
| 0x824578AC | `b1490006` | `sth r10, 6(r9)` |
|
||||
| 0x824578B0 | `409affcc` | `bne-` — loops back |
|
||||
| `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 |
|
||||
|
||||
Four consecutive halfword stores at offsets 0/2/4/6 through **`r9`**, inside a
|
||||
loop: the code is filling an array of 8-byte records with four `u16` fields each.
|
||||
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.
|
||||
|
||||
**`r9` is a wild pointer.** The faulting address `0x1701D0000` is above 4 GB and so
|
||||
outside the guest's 32-bit address space entirely — not a null dereference and not
|
||||
a small overrun, but a base that was never a guest address.
|
||||
The pump, in C:
|
||||
|
||||
## It is a *third* failure mode, not either known one
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
| | PC | crash dumps | this |
|
||||
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.
|
||||
|
||||
## 🟡 What is still open: *which* dialog
|
||||
|
||||
`is_xam_dialog_present_` is a single bool with no logging on the setting side, so the
|
||||
log says a XAM dialog exists and not which one. The static reach:
|
||||
|
||||
* `XamShowDeviceSelectorUI` — **ruled out**. The run's own config dump carries
|
||||
`storage_selection_dialog = false`, and `xam_ui.cc:550` takes the headless path in
|
||||
that case, which never sets the flag.
|
||||
* `XamShowSigninUI` and `XamShowMessageBoxUIEx` — both set it, both are imported, and
|
||||
both reach the guest. `sub_821D03A0` calls the game's `XamShowSigninUI` **and**
|
||||
`XamShowDeviceSelectorUI` wrappers, which is the shape of a "sign in, then pick
|
||||
storage" flow — exactly what a title screen's Ⓐ would start.
|
||||
* `XamShowKeyboardUI`, `XamShowDirtyDiscErrorUI` — imported; not excluded, but neither
|
||||
fits the moment.
|
||||
|
||||
**The experiment that settles it** is one line of Canary, not another blind boot: log
|
||||
the function name at each `is_xam_dialog_present_.store(true)` site in `xam_ui.cc`.
|
||||
Recorded rather than done, because it is an emulator change and this iteration's
|
||||
budget went to the diagnosis.
|
||||
|
||||
## 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. Three routes now exist where before there
|
||||
were none, in cost order:
|
||||
|
||||
1. **Dismiss the dialog.** It is an ImGui window on the emulator surface; the run had
|
||||
a display. If it can be clicked or key-dismissed, the flag clears and the pump
|
||||
drains normally. Cheapest, and testable in one boot.
|
||||
2. **`--headless`.** Both `xeXamShowSigninUI` and `XamShowMessageBoxUIEx` take a
|
||||
dispatch-headless path that never sets the flag. ⚠️ It also removes the window the
|
||||
capture harness grabs, so this trades the fault for a different blocker.
|
||||
3. **Patch the swallow.** Returning `X_ERROR_EMPTY` instead of `X_ERROR_SUCCESS` at
|
||||
`xam_input.cc:217` terminates the pump immediately and is closer to hardware
|
||||
(a real Xbox does not hand a game an infinite run of empty keystrokes). This is
|
||||
an emulator change and needs to 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 PC |
|
||||
| loader stall ([`canary-scripted-input-traps.md`](../canary-scripted-input-traps.md)) | — | **zero** | ❌ this has 32 356 |
|
||||
| **this** | **`0x824578A0`** | 32 356 | — |
|
||||
| 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** |
|
||||
|
||||
⚠️ So the advice in the input-traps page — *"retry whole boots"*, because that
|
||||
failure is not deterministic — does not obviously apply: this one reproduced on
|
||||
**4 of 4** attempts.
|
||||
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.
|
||||
|
||||
⚠️ **And it does not explain the corpus's earlier successes.** Q4 and Q5 measured
|
||||
all five main-menu buttons, and the focus ring was timed on the menu, so Ⓐ worked
|
||||
then. What differs between those runs and these has not been found.
|
||||
## 🔴 Also refuted: the earlier "unimplemented instruction" hypothesis
|
||||
|
||||
## The disk hazard, quantified
|
||||
|
||||
A guest fault writes an **unbounded** register dump: 32 register lines, 32 float
|
||||
lines and 128 vector lines per fault, at ~30 MB/s, on a filesystem at 91 %.
|
||||
|
||||
✅ `tools/re-capture/frame_clock.sh`'s size guard killed this run at its 300 MB cap
|
||||
and worked exactly as designed — the session log's `EMULATOR GONE at 56s` is the
|
||||
guard, not the crash. **Any scripted run that presses a button needs it.**
|
||||
|
||||
## What this blocks
|
||||
|
||||
The main-menu sweeps; whether a `.tbm` draws pixels (needs `GP_SAVE_LOAD`,
|
||||
`GP_BUNK` or `GP_DEBRIEFING_PILOTLOG`); `pbafc.prm`'s blend (needs
|
||||
`GP_READY_ROOM`); and any other menu-side question.
|
||||
|
||||
## Where the next probe goes
|
||||
|
||||
At **`sub_82457780`** and what sets `r9`. The function is non-leaf with a 144-byte
|
||||
frame; the loop writes 8-byte records. Whether `r9` comes from an allocation that
|
||||
failed, a table the guest expects the loader to have filled, or a pointer read back
|
||||
from a structure, is the question — and the corpus's note that the *loader thread*
|
||||
does no file I/O in a failed run is suggestive but is a different failure.
|
||||
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.
|
||||
|
||||
@@ -273,6 +273,36 @@ This is consistent with, and adds nothing to, the draw-quad comparison in
|
||||
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md): the plate is
|
||||
not the tell that distinguishes the boot title from the attract title.
|
||||
|
||||
### ✅ Refutation attempt (2026-08-30) — the two `press-a` captures are DIFFERENT FRAMES
|
||||
|
||||
The port asked whether `live-title-press-a.png` and
|
||||
`live-attract-title-press-a-band.png` were captured the same way, because it
|
||||
measures **0.301 %** between them and — if they were the same frame — that number
|
||||
would be a **floor under every full-frame comparison in this corpus**. That is the
|
||||
expensive reading, so it is the one worth attacking.
|
||||
|
||||
**The attempt to confirm it failed; they are two different moments.** The band is
|
||||
1279×120 and the full capture 1279×675, so the band was slid down every row of the
|
||||
full frame and scored by mean |Δ|. The alignment is unambiguous — a sharp minimum,
|
||||
which is the instrument's own control:
|
||||
|
||||
| y offset | mean abs Δ |
|
||||
|---|---|
|
||||
| 519 | 7.887 |
|
||||
| **520** | **4.016** |
|
||||
| 521 | 7.872 |
|
||||
|
||||
At that best alignment the two disagree on **40.84 %** of the band's pixels
|
||||
(32.56 % by more than 1), mean |Δ| **4.02**, max **169**. A crop of the same frame
|
||||
would be zero. So the band is a different instant of a moving screen — the movie
|
||||
still running behind the plate — and the port's own preferred reading is right.
|
||||
|
||||
**So 0.301 % is not an instrument floor**, and no full-frame figure in this corpus
|
||||
needs to be discounted by it. ⚠️ Reach: this says the two *captures* differ; it
|
||||
says nothing about whether the two *configurations* differ, because a moving
|
||||
background makes that unanswerable from these two images. A configuration
|
||||
comparison needs two captures of a static screen taken deliberately.
|
||||
|
||||
**`EXTRAS` is the only main-menu destination inside `GP_TITLE`.** Ⓐ on `EXTRAS`
|
||||
opens build 6 — measured. The other four destinations leave the archive: Ⓐ on
|
||||
`LOAD GAME` opened a `LOAD GAME` slot list, and Ⓐ on `MISSION SELECT` inside
|
||||
|
||||
Reference in New Issue
Block a user