re: diagnose the A-press fault -- a wild pointer, and a third failure mode
The blocker on all menu-side dynamic RE in this container, measured over four runs and traced to an instruction. A single A press on the title produces a Xenia CRASH DUMP with PC 0x824578A0 and "Access Violation: write at 0x00000001701D0000", repeating 32 356 times and writing 326 MB of register dump in about ten seconds. Four A runs faulted; four no-input runs in the same sessions completed. REFUTED, my own hypothesis: it is not an unimplemented instruction. The config carries break_on_unimplemented_instructions = true and Xenia's own message reads "to skip, disable break_on_unimplemented_instructions", so the flag looked like the fix. Booting with it false faults identically, and no "Unimplemented instr" line is ever logged on stdout or stderr in any run -- and since that path emits its XELOGE BEFORE the guarded DebugBreak, the absence rules the mechanism out rather than leaving it open. The dump comes from Emulator::ExceptionCallback, a genuine guest exception. The instruction, read from the image rather than the database: b0c90000 is sth r6, 0(r9), the first of four halfword stores at offsets 0/2/4/6 through r9 inside a bne- loop -- code filling an array of 8-byte records with four u16 fields each. So r9 is a wild pointer, and 0x1701D0000 is above 4 GB, 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 database agrees on the containing function, sub_82457780 at +0x120. It is a THIRD failure mode. Not the cache-flush crash at 0x82307128, and not the loader stall documented in canary-scripted-input-traps.md, which logs ZERO crash dumps. Unlike the stall it reproduced 4 of 4, so that page's "retry whole boots" does not obviously apply. It does not explain how Q4 and Q5 pressed A successfully; what differs between those runs and these has not been found. frame_clock.sh's 300 MB guard killed the run as designed -- the session log's "EMULATOR GONE at 56s" is the guard, not the crash. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
This commit is contained in:
@@ -2529,3 +2529,36 @@ 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
|
||||
|
||||
Not something you need, but it bounds what I can still answer, so it is worth
|
||||
having in one place.
|
||||
|
||||
**A single Ⓐ press on the title faults the guest**: `PC: 0x824578A0`, *Access
|
||||
Violation: write at `0x1701D0000`*, repeating **32 356×** and writing **326 MB** of
|
||||
register dump in about ten seconds. Four Ⓐ runs faulted; four no-input runs in the
|
||||
same sessions completed.
|
||||
|
||||
🔴 **I was wrong about the cause and the wrong guess was informative.** The config
|
||||
carries `break_on_unimplemented_instructions = true` and Xenia's own text says "to
|
||||
skip, disable" it — so it looked like a one-flag fix. Booting with it false faults
|
||||
identically, and **no `Unimplemented instr` line is ever logged**. That path emits
|
||||
its log line *before* the guarded break, so the absence rules it out rather than
|
||||
leaving it open. The dump is `Emulator::ExceptionCallback` — a real guest
|
||||
exception.
|
||||
|
||||
**Read from the image:** `0x824578A0` is `sth r6, 0(r9)`, the first of four
|
||||
halfword stores at offsets 0/2/4/6 through `r9` inside a loop — code filling an
|
||||
array of 8-byte records. **`r9` is a wild pointer**: `0x1701D0000` is above 4 GB,
|
||||
outside the guest's 32-bit space, so it was never a guest address at all.
|
||||
|
||||
**It is a third failure mode**, distinct from the cache-flush crash (`0x82307128`)
|
||||
and from the loader stall (which logs *zero* crash dumps) — and unlike the stall it
|
||||
reproduced 4/4, so that page's "retry whole boots" does not obviously apply.
|
||||
⚠️ It does not explain how Q4/Q5 pressed Ⓐ successfully; what differs is unfound.
|
||||
|
||||
**What stays blocked:** the main-menu sweeps, whether a `.tbm` draws pixels, and
|
||||
`pbafc.prm`'s blend — all need a screen behind that press.
|
||||
|
||||
Detail: [`docs/re/structures/title-a-press-fault.md`](../re/structures/title-a-press-fault.md).
|
||||
|
||||
|
||||
@@ -179,3 +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 |
|
||||
|
||||
91
docs/re/structures/title-a-press-fault.md
Normal file
91
docs/re/structures/title-a-press-fault.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Pressing Ⓐ on the title faults the guest — diagnosed, and it is a third failure
|
||||
|
||||
**Classification: measured.** Xenia Canary, 2026-08-29, four runs. This is the
|
||||
blocker that gates every menu-side dynamic question in this container.
|
||||
|
||||
## What happens
|
||||
|
||||
A single Ⓐ press on the title screen produces a Xenia **`==== CRASH DUMP ====`**:
|
||||
|
||||
```
|
||||
PC: 0x824578A0
|
||||
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.
|
||||
|
||||
## 🔴 Refuted: my own hypothesis, that it was an unimplemented instruction
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
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 |
|
||||
|---|---|---|
|
||||
| **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 |
|
||||
|
||||
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.
|
||||
|
||||
**`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.
|
||||
|
||||
## It is a *third* failure mode, not either known one
|
||||
|
||||
| | PC | crash dumps | this |
|
||||
|---|---|---|---|
|
||||
| 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 | — |
|
||||
|
||||
⚠️ 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 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.
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user