diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index e992eb6a..49897e04 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -27,6 +27,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes | UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `.t32` sprite has a `.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` is decoded — it is a **looping sprite animation**, not a composition. ⚠️ **DEMOTED 2026-08-18** — the declaration table is *not* the paint order: a per-draw capture of the running title screen ([ui-title-paint-order-capture](ui-title-paint-order-capture.md)) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly | | UI screen paint order | ✅/🟡 | [title paint order](ui-title-paint-order-capture.md) | **Measured from the guest's draw stream** (new Canary hook `log_ui_draws`, F10): the title screen paints 11 draws a frame through one sprite shader, and the submission order is background → `back2` glow → `ptlogo1`+`tm` → `ptlogo2` → `ptcopyright` → the `PRESS Ⓐ BUTTON` plate. That refutes declaration order, reverse order, keyframe-start order and resting-time order, and shows the screen composites **two bundles** (build 7 + the one-element build 2). 🟡 the rule that produces the order is still unknown | | Scripted input / profile traps | ✅/🔴 | [canary-scripted-input-traps](canary-scripted-input-traps.md) | Three reasons a scripted run "cannot press Ⓐ": **F10 opens the emulator menu bar**, which makes `IsUIActive()` true and zeroes every guest keystroke; the file-pad is not the problem (it delivers, and Ⓐ skips movies); and the title needs a **signed-in profile**, which the container cannot type into ImGui — hence Canary's new `--create_profile_if_none`. 🔴 Past that, Ⓐ crashes the guest with `--mem_watch=true` and stalls it with `--mem_watch=false` — a cheap second reproduction of the Ready-Room crash, 40 s from a cold boot | +| Title-screen guest crash | ✅/🟡 | [title-crash-stl-tree](title-crash-stl-tree.md) | `0x82307128` is inside an MSVC `std::map`/`set` **erase** — the function builds and throws `'invalid map/set iterator'`, and its node offsets (`_Left` 0, `_Right` 8, `_Isnil` 25) are the MSVC red-black node. It crashes on the first dereference after that check, at `0x1_0000000C` (a 32-bit value with bit 32 set). Two runs, ~40 s from a cold boot with `--mem_watch=true`; **none** with `--mem_watch=false` — so probably the Ready-Room crash, reproducible in seconds instead of a mission | | Save file (`savedata`) | ✅/❔ | [savegame-format](structures/savegame-format.md) + [`tools/re-capture/savegame.py`](../../tools/re-capture/savegame.py) | `GDHA` container, zlib payload, chunk stream (`GDAA` / phase name / `GHAD` 122 B progress block / 16×20 B slot table / trailer). **Container and layout read off the title's own serializer `0x822C00E8` and verified by a byte-identical round-trip**; the whole save is 545 B. Payload offsets are also the live save object's offsets (`save+8` GHAD, `save+136` slots). A second save made in-game names **Points** (+24), **flight time in ms** (+4) and **clear ratio %** (+8) off the game's own Details panel; the payload is a **pure function of game state** (same state saved twice = byte-identical, only the header FILETIME and its uninitialised pointer padding move), and the 16 `SHAB` records are **not** the UI's 20 save slots. Difficulty vs stage is undecided — three fields hold 2. **A third save, taken after developing exactly one Arsenal weapon** (Light Machine Gun MG I, 4000 P), moves exactly three things: `+24` Points 4101→101 (which **separates it from `+28`**, that did not move), `+8` clear ratio 5→6 (so the ratio counts *collection*, not only stages), and two entries of the 54-byte blob — `2→4` for the item bought and `0→2` for the successor the game announced as newly developable, giving the blob its alphabet ✅ *0 locked / 2 developable / 4 developed* (only the `4`s are stored — `2` is re-derived at load). **Saves can also be written back**: three derived header fields (length at `+0x30`, payload length at `+0x8c`, `adler32` at `+0x8e`) are all that stand between a parse and a hand-written save that the title loads, and [`savegame_edit.py`](../../tools/re-capture/savegame_edit.py) re-wraps a real save byte-identically. That turned the blob's index space from blocked-on-story-progress into four probe saves — see the [economy note](arsenal-develop-economy.md) | ## Runtime / dynamic-capture technique diff --git a/docs/re/canary-scripted-input-traps.md b/docs/re/canary-scripted-input-traps.md index f9bf97fc..380a7df9 100644 --- a/docs/re/canary-scripted-input-traps.md +++ b/docs/re/canary-scripted-input-traps.md @@ -104,11 +104,18 @@ C++ exception in a cache/save path. This is a second, far cheaper reproduction o it: at the **title screen**, ~40 s from a cold boot, instead of in the Ready Room after a mission. -**Next step, for whoever picks this up.** Two threads to pull, in this order: +**The first of those next steps is done**, and it moves the crash out of this +entry into its own: `0x82307128` is inside an MSVC `std::map`/`set` **erase** — +the function carries the string `'invalid map/set iterator'` and the node +offsets to prove it — which makes this a 40-second reproduction of the crash the +Canary handoff blames on the `mem_watch` probe. See +[`title-crash-stl-tree.md`](title-crash-stl-tree.md). -1. `0x82307128` and `0x824578A0` in `xenia-rs/sylpheed.db` (the container has it) - — what function is the title's Ⓐ path in, and what is at `+0xC` of the - structure it dereferences; +**Next step, for whoever picks this up.** What is left: + +1. dump the tree nodes from guest memory at the throw, to separate "the guest's + tree is corrupt" from "the emulator left a stale high word in a register" — + the fault address `0x1_0000000C` is consistent with either; 2. the content exports this title imports — `XamContentCreateEnumerator`, `XamContentGetDeviceState`, `XamUserReadProfileSettings` — logged around the press, to see whether the guest is handed a bad enumerator or a device state diff --git a/docs/re/title-crash-stl-tree.md b/docs/re/title-crash-stl-tree.md new file mode 100644 index 00000000..ad4bbea1 --- /dev/null +++ b/docs/re/title-crash-stl-tree.md @@ -0,0 +1,98 @@ +# The title-screen crash is an STL `map`/`set` erase on a bad iterator + +**Status:** ✅ `CONFIRMED` for the identification (the guest function is +`std::_Tree::erase`-shaped, and it says so in its own diagnostic string). +🟡 `PROBABLE` that this is the same defect as the Ready-Room crash the Canary +handoff blames on the `mem_watch` probe. ❔ whether the bad pointer originates in +the guest or in the emulator's 64-bit register handling. + +Found while trying to get past the title screen for a second UI screen's paint +order ([`canary-scripted-input-traps.md`](canary-scripted-input-traps.md)). It is +worth a page of its own because it is a **40-second reproduction** of a crash the +project has so far only seen after a whole mission. + +## The reproduction + +Boot Canary on the disc with the default `--mem_watch=true` and a profile on +disc. Roughly 40 s in — while the title screen is up — the guest dies: + +``` +Access Violation: read at 0x000000010000000C +PC: 0x82307128 guest thread 9 +… preceded by HostPathDevice::ResolvePath(\aab216c3\5\c10eae6) + and RtlRaiseException(702DF7F0(E06D7363), ContextArg) + "Guest attempted to throw a C++ exception!" +``` + +Two runs, same PC, same fault address. With `--mem_watch=false` it does not +happen at all. Xenia pauses itself and stacks crash dialogs — 991 in one run. + +## What the code is + +`0x82307128` is inside `sub_823070B0` (`0x823070B0..0x823074D0`, has EH), and the +function identifies itself: it references the string +`'invalid map/set iterator'` at `0x82062A8C`, builds it with the string +helpers at `0x8216E7E8` / `0x8216E5C8`, and throws it through `0x825F23D8`. + +The node layout in the prologue is MSVC's `std::_Tree_node` exactly: + +``` +823070C8 lbz r10, 25(r5) ; iterator->_Ptr->_Isnil (offset 25) +… ; if set -> build the string and THROW +82307124 bl 0x8244E2A8 ; (iterator helper) +82307128 lwz r11, 0(r25) ; node->_Left (offset 0) <-- FAULT +8230713C lwz r27, 8(r25) ; node->_Right (offset 8) +``` + +`_Left` 0, `_Parent` 4, `_Right` 8, `_Color` 24, `_Isnil` 25 — that is the MSVC +red-black tree node, so this is a `std::map`/`std::set` **erase** (it validates +the iterator, then walks the node). The crash is the very first dereference +after the validation. + +## What the fault address says + +The effective address is `r25 + 0`, so `r25 = 0x00000001_0000000C`. That is a +32-bit value `0x0000000C` **with bit 32 set** — a pointer whose high word is +garbage, not a wild 32-bit pointer. Two readings, and this note does not choose +between them: + +* the tree really is corrupt and the guest is erasing through a freed/racing + node — which is what the + [`crash-oracle handoff`](../../xenia-canary/HANDOFF-crash-oracle-2026-07-16.md) + concluded for the Ready-Room crash (a concurrent cache-add landing inside an + unlocked flush iteration), and it names the `mem_watch` polling thread as + suspect #1 for perturbing that timing. The `--mem_watch=false` result here is + consistent with that; +* or a high-word extension bug on the emulator side leaves a stale 1 in the + upper half of a 64-bit register. Canary is the *reference* emulator, so this is + the less likely of the two, but `0x1_0000000C` is exactly the shape such a bug + produces and nothing here rules it out. + +**What would separate them:** dump the tree's root and a few nodes from guest +memory (`tools/re-capture/gmem.py`) at the moment of the throw, and check whether +the parent node's `_Left` field on the guest heap really contains `0x0000000C` — +if the guest memory holds a clean 32-bit value and only the register is wrong, +it is the emulator. + +## The other crash PC, for completeness + +The one unreproduced crash after an Ⓐ press was at `0x824578A0`, in +`sub_82457780` (`0x82457780..0x82457958`, one caller, `sub_82457038`). It is an +unrolled **4 × 16-bit copy loop**: + +``` +82457890 lhz r6, 0(r10) 82457898 lhz r4, 4(r10) +82457894 lhz r5, 2(r10) 8245789C lhz r10, 6(r10) +824578A0 sth r6, 0(r9) <-- FAULT (a STORE, not a load) +``` + +so a bad *destination*, in what looks like a small block copy — a different +failure from the tree erase above, and with one observation it stays at that. + +## Why this matters beyond the blocker + +The Ready-Room crash costs a full mission to reproduce, which is why its +bisection plan in the handoff is written as "one build + one Ready-Room run" per +suspect. If this title-screen crash is the same defect, each suspect costs **40 +seconds** instead, and suspect #1 (`--mem_watch=false`) is already measured here: +it removes the crash.