diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index cef95022..9b55ca8e 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -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). + diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index a474e317..678b3daa 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.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 | diff --git a/docs/re/structures/title-a-press-fault.md b/docs/re/structures/title-a-press-fault.md new file mode 100644 index 00000000..21684a74 --- /dev/null +++ b/docs/re/structures/title-a-press-fault.md @@ -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.