Files
xenia-rs/audit-runs/canary-boot-state-inventory/xenia-rs-inventory-and-comparison.md
MechaCat02 ef93a4fa14 handoff: VSync/event-wedge fixes + iterate 2.A–2.BC research notes
Source changes (dormant parity infra, retained from iterate 2.AI/2.AO):
- xenia-kernel/exports.rs: nt_create_event manual_reset polarity +
  related event wiring
- xenia-gpu/mmio_region.rs: D1MODE_VBLANK_VLINE_STATUS hardcode parity

Also lands the audit-runs/ analysis notes (.md/.txt/.json digests) for the
iterate 2.x VSync/0x10e8/0x1004 wedge investigation. Raw trace dumps
(.jsonl/.gz/.csv/.stdout) and agent worktrees (.claude/) are gitignored as
regenerable local artifacts — see memory + HANDOFF for the running findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 07:19:08 +02:00

437 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Xenia-rs Boot State Inventory + Canary Comparison
Companion to [`inventory.md`](inventory.md). Inventories xenia-rs's state immediately before guest XEX `EntryPoint`, then compares element-by-element against canary, calling out **missing** / **wrong** / **structurally different** initialization. All citations are paths relative to project root `/home/fabi/RE - Project Sylpheed/`.
The comparison output is the most important part — that's §B at the bottom. §A is the inventory itself.
---
# §A — Xenia-rs Inventory
## 0. The Boot Sequence
End-to-end path through [xenia-rs/crates/xenia-app/src/main.rs](xenia-rs/crates/xenia-app/src/main.rs):
1. Parse XEX header (`parse_xex2_header`) — extract image base, entry point, imports, TLS info.
2. Decompress + decrypt image (`load_image`) and write into guest memory.
3. Resolve imports → build `thunk_map`.
4. **Allocate stack** at fixed VA `0x70000000`, size `0x100000` (1 MiB). [main.rs:933-936](xenia-rs/crates/xenia-app/src/main.rs#L933-L936)
5. **Allocate PCR** at fixed VA `0x7FFF_0000`, size `0x1000` (4 KiB); **TLS** at fixed `0x7FFE_0000`, size `0x1000`. [main.rs:939-942](xenia-rs/crates/xenia-app/src/main.rs#L939-L942)
6. **Write 3 PCR fields**: `[+0x00]=tls_addr`, `[+0x100]=0x1000` (fake), `[+0x150]=0`. [main.rs:945-947](xenia-rs/crates/xenia-app/src/main.rs#L945-L947)
7. **Build CPU context**: `PpcContext::new()` + override `pc`, `gpr[1]`, `gpr[2]`, `gpr[3..=7]`, `gpr[13]`, `msr`. [main.rs:953-966](xenia-rs/crates/xenia-app/src/main.rs#L953-L966)
8. Construct `KernelState` with GPU backend, register thunks. [main.rs:1014-1028](xenia-rs/crates/xenia-app/src/main.rs#L1014-L1028)
9. Install MMIO region (GPU). [main.rs:1441](xenia-rs/crates/xenia-app/src/main.rs#L1441)
10. **Allocate `main_handle`** = `0x1000` and install the initial thread on HW slot 0. [main.rs:1446-1467](xenia-rs/crates/xenia-app/src/main.rs#L1446-L1467) Write `pcr[+0x2C]=0` (processor number).
11. `kernel.retain_handle(main_handle)` — mirrors canary's `XThread::Create``RetainHandle()`. [main.rs:1476](xenia-rs/crates/xenia-app/src/main.rs#L1476)
12. If XISO, mount `d:` device. [main.rs:1480-1485](xenia-rs/crates/xenia-app/src/main.rs#L1480-L1485)
13. **Patch variable imports** by ordinal — only the ones the XEX imports get non-zero values. [main.rs:1496-1588](xenia-rs/crates/xenia-app/src/main.rs#L1496-L1588)
14. (optional) DB writer + analysis.
15. **Begin execution**: interpreter loop dispatches at `ctx.pc = entry`.
There is **no `XThread::Create` / `X_CREATE_SUSPENDED` / `PreLaunch` / Resume** sequence — execution begins as soon as the run loop is entered.
---
## 1. PPC CPU State at entry
From [xenia-cpu/src/context.rs:148-182](xenia-rs/crates/xenia-cpu/src/context.rs#L148-L182) (`PpcContext::new`) + [main.rs:953-966](xenia-rs/crates/xenia-app/src/main.rs#L953-L966) (overrides).
| Reg | Value | Source |
|----|----|----|
| r0 | 0 | `PpcContext::new` |
| **r1** (SP) | `((stack_base + stack_size) - 0x100) & ~0xF` = **`0x700F_FF00`** | [main.rs:958-959](xenia-rs/crates/xenia-app/src/main.rs#L958-L959) |
| **r2** | `0x2000_0000` | [main.rs:960](xenia-rs/crates/xenia-app/src/main.rs#L960) |
| **r3..r7** | explicitly 0 (loop) | [main.rs:964](xenia-rs/crates/xenia-app/src/main.rs#L964) |
| r8..r12 | 0 | `PpcContext::new` |
| **r13** | `0x7FFF_0000` (fixed VA, no allocation) | [main.rs:965](xenia-rs/crates/xenia-app/src/main.rs#L965) |
| r14..r31 | 0 | `PpcContext::new` |
| **LR** | **`0xBCBC_BCBC`** (halt sentinel — `bclr` exits the interpreter) | [context.rs:55, 155](xenia-rs/crates/xenia-cpu/src/context.rs#L55) |
| CTR | 0 | `PpcContext::new` |
| **MSR** | `0x9030` | [main.rs:966](xenia-rs/crates/xenia-app/src/main.rs#L966) |
| FPSCR | 0 | `PpcContext::new` |
| XER (ca/ov/so/tbc) | 0/0/0/0 | `PpcContext::new` |
| CR0..CR7 | `CrField::default()` (=0) | `PpcContext::new` |
| FPRs | `0.0` × 32 | `PpcContext::new` |
| VRs | `Vec128::ZERO` × 128 | `PpcContext::new` |
| **VSCR** | `Vec128::from_u32x4(0, 0, 0, 0x0001_0000)` (NJ=1 only) | [context.rs:167](xenia-rs/crates/xenia-cpu/src/context.rs#L167) |
| **VRSAVE** | `0xFFFF_FFFF` | [context.rs:168](xenia-rs/crates/xenia-cpu/src/context.rs#L168) |
| DEC | 0 | `PpcContext::new` |
| timebase, cycle_count | 0 | `PpcContext::new` |
| reservation\_\* | unset / `None` (M3.7 table optional) | [context.rs:170-176](xenia-rs/crates/xenia-cpu/src/context.rs#L170-L176) |
| PC | `entry_point_` from XEX header | [main.rs:954](xenia-rs/crates/xenia-app/src/main.rs#L954) |
PpcContext is `#[repr(C, align(64))]` — same alignment expectation as canary's host-allocated context.
---
## 2. Memory Layout
| Region | xenia-rs VA | Size | Source |
|----|----|----|----|
| XEX image | from header (`base`) | `alloc_size` from XEX | [main.rs:905-907](xenia-rs/crates/xenia-app/src/main.rs#L905-L907) |
| Stack | `0x7000_0000 .. 0x7010_0000` | 1 MiB **fixed** | [main.rs:933-936](xenia-rs/crates/xenia-app/src/main.rs#L933-L936) |
| **PCR** | `0x7FFF_0000` | 4 KiB **fixed** | [main.rs:939-941](xenia-rs/crates/xenia-app/src/main.rs#L939-L941) |
| **TLS** | `0x7FFE_0000` | 4 KiB **fixed** | [main.rs:940-942](xenia-rs/crates/xenia-app/src/main.rs#L940-L942) |
| User heap (bump alloc) | `0x4000_0000+` | — | [state.rs:550](xenia-rs/crates/xenia-kernel/src/state.rs#L550) |
| Aux kernel stack alloc cursor | `0x7100_0000+` | — | [state.rs:551](xenia-rs/crates/xenia-kernel/src/state.rs#L551) |
**No guard pages** are allocated around the stack. **No physical-mirror aliases** (A0/C0/E0). **No XEX `LoadContinue`-style page-protection split** for `.text` (image is allocated as RW).
---
## 3. PCR layout in xenia-rs
Only **4 fields** are written ([main.rs:945-947, 1457](xenia-rs/crates/xenia-app/src/main.rs#L945-L947)):
| Offset | Field | Value | Note |
|----|----|----|----|
| +0x00 | tls_ptr | `0x7FFE_0000` | matches canary |
| +0x2C | processor_number | 0 | matches canary semantically |
| +0x100 | current_thread | `0x1000` (the main_handle, NOT a guest KTHREAD VA) | divergence — see §B |
| +0x150 | dpc_active | 0 | matches canary |
The rest of the 4 KiB is zero. No `pcr_ptr` self-reference at +0x30, no `host_stash` at +0x38, no `stack_base_ptr`/`stack_end_ptr` at +0x70/+0x74, no `prcb` pointer at +0x104.
---
## 4. TLS
- `kernel.next_tls_index = AtomicU32::new(0)` at [state.rs:546](xenia-rs/crates/xenia-kernel/src/state.rs#L546). Slots grow on first `ExAllocateTls` ([state.rs:1539](xenia-rs/crates/xenia-kernel/src/state.rs#L1539)).
- `scheduler.tls_slot_count = 0` at [scheduler.rs:360, 396](xenia-rs/crates/xenia-cpu/src/scheduler.rs#L360); main thread receives `tls_values = vec![0; 0]` ([scheduler.rs:682](xenia-rs/crates/xenia-cpu/src/scheduler.rs#L682)).
- TLS *block* in guest memory at `0x7FFE_0000` is 4 KiB zeroed; not parsed from XEX TLS info either.
---
## 5. Threading
- 6 HW slots (`HW_THREAD_COUNT = 6`), all empty except slot 0 (`HwSlot::Ready`, contains main thread tid=`INITIAL_GUEST_TID`=1).
- `next_thread_id` starts at `INITIAL_GUEST_TID + 1 = 2`.
- No background scheduler tick, no kernel dispatch worker, no GPU command-processor thread *parked on guest semaphores*. GPU runs on its own host worker (M1.9 default) but doesn't appear in the guest scheduler.
- Quantum = `QUANTUM_DEFAULT = 500_000` insns ([scheduler.rs:795](xenia-rs/crates/xenia-cpu/src/scheduler.rs#L795)).
---
## 6. Kernel guest-visible variable exports
Done **lazily, per-import-record** in `main.rs:1496-1588`. Only ordinals the XEX imports get matched; everything else falls through to `_ => mem.write_u32(addr, 0)`.
Handled ordinals (xboxkrnl.exe):
| Ord | Name | xenia-rs init |
|----|----|----|
| 0x001B | `ExThreadObjectType` | ptr to 0x40 zero block |
| 0x0059 | `KeDebugMonitorData` | ptr to 0x40 zero block |
| 0x00AD | `KeTimeStampBundle` | ptr to 0x18 block: `[+0]=FILETIME` `[+0x10]=FILETIME` (both = `132_500_000_000_000_000`) |
| 0x0158 | `XboxKrnlVersion` | u16×4: `{2, 0, 20000, 0}` written directly at import slot (no indirection) |
| 0x0193 | `XexExecutableModuleHandle` | `addr = base`; raw XEX header bytes copied to a separate heap allocation stashed in `kernel.xex_header_guest_ptr` |
| 0x01AE | `ExLoadedCommandLine` | ptr to 0x10 zero block (empty string) |
| 0x01BE | `VdGlobalDevice` | 0 |
| 0x01C0 | `VdGpuClockInMHz` | `500` |
| 0x01C1 | `VdHSIOCalibrationLock` | 0 |
| 0x0266 | `KeCertMonitorData` | ptr to 0x100 zero block |
| all others | — | 0 |
**Critically: no `XboxHardwareInfo` (0x017A), no `ExConsoleGameRegion` (0x015F), no `ExLoadedImageName` (0x01AD), no `KernelGuestGlobals` block, and `KeTimeStampBundle` has a structurally different layout from canary's.**
---
## 7. Object table / handles
- Empty at boot; `next_handle = 0x1000`, increments by 4. [state.rs:540-547](xenia-rs/crates/xenia-kernel/src/state.rs#L540-L547)
- First handle = `0x1000` = main thread (via `alloc_handle_for(KernelObject::Thread {...})`).
- No event/semaphore/mutex pre-creation.
---
## 8. XAM
Largely stub-only:
- No `UserProfile` struct ever instantiated. Queries return `ERROR_NOT_FOUND` / 0 / sentinel values.
- `XamUserGetXUID` → 0, `XamUserGetName` → empty string, `XamUserGetSigninState` → 1 for user_index==0 else 0. [xam.rs:354-381](xenia-rs/crates/xenia-kernel/src/xam.rs#L354-L381)
- Notification state: `has_notified_startup=false`, `has_notified_live_startup=false`. No listeners pre-registered.
- No 18-setting default profile.
---
## 9. Filesystem
`kernel.vfs: Option<Box<dyn VfsDevice>>` — at most ONE device. If `.iso`/`.xiso`, `DiscImageDevice::open("d", path)` is installed under `kernel.vfs`. Otherwise `None`. [main.rs:1480-1485](xenia-rs/crates/xenia-app/src/main.rs#L1480-L1485)
No `game:`/`d:` symbolic-link distinction, no `\Device\Harddisk0\Partition1`, no STFS/CON/PIRS/UDF, no `system:`/`hdd:`/`mu:`. Cache is a host tmpdir wiped on startup ([state.rs:611-636](xenia-rs/crates/xenia-kernel/src/state.rs#L611-L636)).
---
## 10. GPU
- `GpuSystem::new` → register file `vec![0u32; 0x6000]`, all zero. [register_file.rs:7-9](xenia-rs/crates/xenia-gpu/src/register_file.rs#L7-L9)
- **No gamma-ramp preload** — neither the 256-entry sRGB ramp nor the 128-entry PWL ramp from canary's `CommandProcessor::Initialize`.
- Ring buffer base/size = 0 until guest calls `VdInitializeRingBuffer`.
- MMIO region at `0x7FC8_0000` (mask `0xFFFF_0000`, 64 KiB window), registers `CP_RB_WPTR`, `CP_RB_RPTR`, `CP_INT_STATUS`, `CP_INT_ACK`, `D1MODE_VBLANK_VLINE_STATUS` hooked. [mmio_region.rs:23-27](xenia-rs/crates/xenia-gpu/src/mmio_region.rs#L23-L27)
- Backend: threaded (M1.9 default), `Arc`-shared `GpuSystem`. Vulkan/D3D12 device is **not** created at boot — it stays in the "no-presenter" mode unless `--ui` is used.
---
## 11. Audio (APU)
Largely a stub:
- `xenia-apu` is described as stub-only ([lib.rs:1-16](xenia-rs/crates/xenia-apu/src/lib.rs#L1-L16)). No XMA decoder.
- The functional audio path is `xenia-kernel/src/xaudio.rs`: 8 client slots (all `None`), synthetic park-handle base `0xF000_0000`, empty pending-fire FIFO, no worker threads.
- A periodic `xaudio_tick_enabled = true` will fire buffer-complete callbacks every 48,000 instructions (≈5.33 ms wall) **but only after the guest calls `XAudioRegisterRenderDriverClient`**.
---
## 12. HID, Network, Display, Clock
- HID: single `GamepadState` zeroed, all 4 slots disconnected.
- Network: `WSAStartup`/`WSACleanup` are no-ops; no IP/MAC value pre-written. No XNet stack.
- Video mode: hardcoded HDMI 1280×720 widescreen via `XGetVideoMode`/`XGetAVPack` exports ([xam.rs:631-654](xenia-rs/crates/xenia-kernel/src/xam.rs#L631-L654)).
- Clock: `KeQuerySystemTime` returns fixed FILETIME `132_500_000_000_000_000`; `KeQueryInterruptTime` returns fixed `0x0000_0001_0000_0000` ([exports.rs:869-883](xenia-rs/crates/xenia-kernel/src/exports.rs#L869-L883)). No `HighResolutionTimer` repeating update.
---
## 13. Interpreter / codegen
Pure interpreter (no JIT). Code blocks decoded on first execution. Reservation table is `Option<Arc<…>>` (gated by `--reservations-table` / `XENIA_RESERVATIONS_TABLE=1`). Import thunks not pre-patched into guest code; instead, the interpreter intercepts at the thunk PC and dispatches to a host `call_export(module, ordinal)`.
---
# §B — Comparison: where xenia-rs is missing or wrong
Tagged each row with:
- **= match** — bit-equivalent or semantically equivalent
- **≈ semantic** — different mechanism, same observable result
- **✗ missing** — guest-visible state canary provides that xenia-rs doesn't
- **!= wrong** — both engines set it but the values/layout/timing diverge
- **+ extra** — xenia-rs sets something canary doesn't (impact unknown)
## B.1 PPC CPU registers
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| r0 | 0 (memset) | 0 | **=** |
| r1 (SP) | `stack_base_` (top of allocated region from `0x70000000-0x7F000000`, page-aligned, sits between two guard pages) | `0x700F_FF00` (1 MiB stack ends at `0x7010_0000`, SP = top 0x100, 16B aligned) | **!= wrong** — value differs because stack size is hardcoded 1 MiB ignoring `XEX_HEADER_DEFAULT_STACK_SIZE`; no guard pages |
| r2 | `0x2000_0000` | `0x2000_0000` | **=** |
| r3 | `start_context` = 0 for main | 0 | **=** |
| r4..r7 | 0 (memset) | 0 (explicit) | **=** |
| r13 | `pcr_address_` = system-heap VA (variable, in `0x80000000+` range) | `0x7FFF_0000` (fixed) | **!= wrong** — guest code that walks PCR via r13 cannot assume a fixed VA, but most reads via `lwz rX, off(r13)` work fine; *however* canary places r13 in 0x80000000+ system-heap, ours in 0x7FFF_0000 user heap — any code that does `if (r13 >= 0x80000000) …` would diverge |
| r14..r31 | 0 (memset) | 0 | **=** |
| LR | 0 (memset) | **`0xBCBC_BCBC`** (halt sentinel) | **+ extra (and != wrong)** — canary's main thread enters with LR=0; ours uses a sentinel so `bclr` from the entry frame exits the interpreter. A guest function that reads LR before saving it (very rare) would see a different value. |
| CTR, XER, FPSCR, CR | 0 | 0 | **=** |
| MSR | `0x9030` | `0x9030` | **=** |
| VSCR | `0x0001_0000` (NJ=1) | `0x0001_0000` (NJ=1) | **=** |
| VRSAVE | `0xFFFF_FFFF` | `0xFFFF_FFFF` | **=** |
| FPRs | 0.0 × 32 (memset of bit pattern) | 0.0 × 32 | **=** |
| VRs | 0 × 128 | 0 × 128 | **=** |
| DEC | 0 (memset) | 0 | **=** |
| PC | `entry_point_` | `entry` | **=** |
**Net**: register-level state is essentially equivalent. The two *real* divergences are SP value (because stack-size is wrong) and LR (intentional design choice but observable). Everything else matches.
## B.2 Stack
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| Range | `0x70000000-0x7F000000` (240 MiB pool, multiple stacks bump-allocated) | `0x70000000-0x70100000` (1 MiB hardcoded) | **!= wrong** |
| Size | `XEX_HEADER_DEFAULT_STACK_SIZE` rounded to heap page size | `0x10_0000` (1 MiB) — XEX header ignored | **!= wrong** |
| Guard pages | 2× `page_size` (one above, one below), `kMemoryProtectNoAccess` | none | **✗ missing** — stack overflow on ours will silently corrupt adjacent VA; canary would fault on the guard page |
| stack_limit / stack_base recorded in KPCR | yes (+0x70 / +0x74) | not written | **✗ missing** (see B.4) |
If the title declares a stack size larger than 1 MiB (some larger XEXes do), xenia-rs will violate that contract. Worth checking Sylpheed's `XEX_HEADER_DEFAULT_STACK_SIZE`.
## B.3 TLS
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| Slot count when XEX has no TLS info | **1024** (`kDefaultTlsSlotCount`, `xthread.cc:335`) | **0** (`next_tls_index = AtomicU32::new(0)`, grows on `ExAllocateTls`) | **!= wrong** |
| Slot zeroing | `Memory::Fill(tls_static_address_, tls_total_size_, 0)` (4 KiB for default) | block at `0x7FFE_0000` is 4 KiB zero (allocation default), but the *runtime* `tls_values` vector starts empty | **!= wrong** for guest semantics — canary returns 0 from any of 1024 slots; ours returns 0 by lazy resize but if guest reads slot N via `lwz r3, (4*N)(r13)` it actually reads the **guest-memory TLS block at 0x7FFE_0000**, not the host-side `tls_values` Vec. Whether these stay coherent depends on which kernel API is used. |
| Extended TLS image (from `xex2_opt_tls_info.raw_data_address`) | copied into TLS block if `raw_data_size > 0` | **not parsed, never copied** | **✗ missing** — if Sylpheed has any `__declspec(thread)` static data, xenia-rs starts with all zeros. |
Highest-impact divergence in this section.
## B.4 PCR / KPCR
| Offset | Field | Canary writes | xenia-rs writes | Status |
|----|----|----|----|----|
| +0x000 | tls_ptr | yes | yes | **=** |
| +0x02C | processor_number | implicit (via SetActiveCpu) | yes (0) | **=** |
| +0x030 | pcr_ptr (self-ref) | yes (`pcr_ptr = pcr_address_`) | **no** | **✗ missing** |
| +0x038 | host_stash (`uint64` host pointer) | yes | **no** | **✗ missing** (host-side stash; xenia-rs doesn't need it because it has the `PpcContext` Rust struct out-of-band, but anything reading `[r13+0x38]` would see 0 in ours and a host pointer in canary) |
| +0x070 | stack_base_ptr | yes | **no** | **✗ missing** |
| +0x074 | stack_end_ptr | yes | **no** | **✗ missing** |
| +0x100 | prcb_data.current_thread (real guest KTHREAD VA) | yes (a VA in the system-heap KTHREAD allocation) | yes, but `0x1000` (the *handle*, not a guest KTHREAD VA) | **!= wrong** — any guest code that dereferences this expecting to read KTHREAD fields will read garbage from `0x1000` (probably zero memory or invalid). Multiple ntdll-style helpers in xboxkrnl walk this. |
| +0x104 | prcb (=`pcr+offsetof(prcb_data)`) | yes | **no** | **✗ missing** |
| +0x150 | dpc_active | implicit (init writes `prcb_data.dpc_active=0`) | yes (0) | **=** |
| Size | 0x2D8 | 0x1000 (over-allocated) | **+ extra** (no functional impact) |
| Base VA | dynamic from system heap | fixed `0x7FFF_0000` | **!= wrong** value but compatible layout |
This is the **most consequential structural divergence**: any guest code path that touches PCR fields beyond `[r13+0]` and `[r13+0x100]` will diverge.
## B.5 Kernel variable exports
| Export (ord) | Canary | xenia-rs | Status |
|----|----|----|----|
| `XboxHardwareInfo` (0x017A) | 16B: `[0]=0x20` HDD bit, `[4]=0x06` CPU count, rest 0 | **falls through to default 0** — not handled | **✗ missing** — games that probe HDD-present bit or CPU-count will see 0/0. |
| `XboxKrnlVersion` (0x0158) | 8B from `kernel_state_->GetKernelVersion()` (=`{2, 0xFFFF, 0xFFFF, 0x80}`-ish per canary `KernelVersion`) | 8B inline `{2, 0, 20000, 0}`*written at the import-slot address directly, not via pointer indirection* | **!= wrong** — canary writes a *pointer* to the version struct into the import slot; xenia-rs writes the version bytes directly into the import slot. If the XEX import declares it as `ptr-to-data` (which is how canary's `SetVariableMapping` semantics work) and the guest dereferences it, ours will deref `0x00020000` and crash. The XEX import record type 0 is "data" but the canonical pattern is still indirection. Worth verifying which side the game expects. |
| `XexExecutableModuleHandle` (0x0193) | pointer-to-pointer chain that ultimately leads to the XEX header base | direct write of `base` at the import slot, with header bytes stashed separately at `kernel.xex_header_guest_ptr` for `RtlImageXexHeaderField` to consume | **!= wrong** but per the in-source comment ([main.rs:1532-1556](xenia-rs/crates/xenia-app/src/main.rs#L1532-L1556)) the previous "proper" indirection caused divergence at idx=0; current direct-write workaround is intentional. |
| `ExLoadedImageName` (0x01AD) | 1024-aligned buffer filled with module path after `SetExecutableModule` | **not handled** — falls through to default 0 | **✗ missing** |
| `ExLoadedCommandLine` (0x01AE) | 1024-aligned buffer containing `"default.xex"` (with literal quotes) + cvar `cl` | 0x10 zero block (empty string) | **!= wrong** — empty string vs `"default.xex"`. Could cascade if anything parses it. |
| `ExConsoleGameRegion` (0x015F) | `0xFFFF_FFFF` (region-free) | **not handled** — falls through to default 0 | **✗ missing** — region check will fail in any title that branches on this. |
| `KeDebugMonitorData` (0x0059) | 4B `0` (or 4B + struct when cvar on) | 0x40 zero block pointer | **≈ semantic** — both effectively zero; size differs harmlessly |
| `KeCertMonitorData` (0x0266) | 4B `0` (or struct when cvar on) | 0x100 zero block pointer | **≈ semantic** |
| `KeTimeStampBundle` (0x00AD) | 0x18 block: `+0x00=interrupt_time u64`, `+0x08=system_time u64`, `+0x10=tick_count u32` (uptime ms), `+0x14=padding u32`; updated every tick by `HighResolutionTimer::CreateRepeating` | 0x18 block: `+0x00=FILETIME hi/lo u32×2`, `+0x10=FILETIME hi/lo u32×2` again; `+0x08` is **never written** (stays 0); no repeating timer | **!= wrong** — (a) `[+0x08]` (canary's `system_time u64`) is 0 in ours, should be the time. (b) `[+0x10]` should be `tick_count u32` (ms since boot) — ours writes the high half of a 64-bit FILETIME there instead. (c) values are static; any code that polls this expecting forward progress (game loops do) will see a frozen tick-count. **High-impact.** |
| `ExThreadObjectType` (0x001B) | pointer into `KernelGuestGlobals` block, with object-type bytes populated by `InitializeKernelGuestGlobals` | 0x40 zero block pointer | **!= wrong** — object-type sub-struct bytes (header, pool tag, vtable-ish) are *not* zero in canary. Any guest code that compares `*hType` against expected magic bytes will diverge. |
| `ExEventObjectType`, `ExMutantObjectType`, `ExSemaphoreObjectType`, `ExTimerObjectType`, `IoCompletionObjectType`, `IoDeviceObjectType`, `IoFileObjectType`, `ObDirectoryObjectType`, `ObSymbolicLinkObjectType`, `UsbdBootEnumerationDoneEvent` | all populated as part of `KernelGuestGlobals` | **all fall through to default 0** | **✗ missing** — same class of bug as ExThreadObjectType, multiplied across all type tags. |
| `VdGpuClockInMHz` (0x01C0) | 500 (`xenia_main.cc:661`) | 500 | **=** |
| `VdGlobalDevice` (0x01BE) | 0 | 0 | **=** |
| `VdHSIOCalibrationLock` (0x01C1) | 0 | 0 | **=** |
## B.6 Object table / sync primitives
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| Pre-created kernel objects | none (the executable module + dispatch worker thread + main thread are created during LaunchModule) | none (main thread handle 0x1000 created in `install_initial_thread`) | **=** |
| Main thread refcount | 2 (creator + self via `RetainHandle()`) | 2 (creator + `retain_handle`) | **=** |
| **Kernel dispatch worker thread** (canary `SetExecutableModule` creates one to dispatch guest async callbacks) | **yes** | **no** | **✗ missing** — guest async callback paths may behave differently |
| `kHandleBase` / handle spacing | `0xF8000000`, step 4 | `0x1000`, step 4 | **!= wrong** value but compatible if the guest doesn't hard-compare handle values |
## B.7 XAM
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| User profile | XUID `0xB13E_BABE_BABE_BABE`, gamertag `"User"`, 18 default settings (per `user_profile.cc:32-92`) | no profile; `XamUserGetXUID` returns 0; `XamUserGetName` returns "" | **!= wrong** — any title that branches on `XamUserGetSigninState(0) == eSignedInLive`/`eSignedInLocally` will likely treat user as signed-out in ours, signed-in (locally) in canary. |
| `XamUserGetSigninState` | returns 1 (signed-in locally) for slot 0 | returns 1 for slot 0 | **=** |
| Notification listeners | empty until first registration; first registration triggers synthesized `XN_SYS_UI` / `SIGNINCHANGED` / etc. burst | empty; no synthesized burst | **!= wrong** — guests subscribing to startup events will not receive them |
| `XamGetExecutionId` | implemented (returns title-info struct) | stub (returns 0) | **!= wrong** |
## B.8 Filesystem
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| `game:` symlink → DiscImageDevice / StfsContainerDevice / HostPathDevice | always mounted from input | mounted only if input is `.iso`/`.xiso`, under device name `d` (not `game`) | **!= wrong** — guests opening `game:\\…` paths against a non-ISO input will fail in ours. |
| `d:` symlink | always present pointing to same device as `game:` | identical to `game:` in xenia-rs (device name is `"d"`) | **≈ semantic** |
| `cache:` / `cache0:` / `cache1:` | optional via cvar (`mount_cache`) | optional via env (tmpdir default, wiped on boot) | **≈ semantic** but content differs (canary persists, ours wipes) |
| `system:`, `hdd:`, `mu:`, `udf` | optional | absent | **✗ missing** |
| `\Device\Harddisk0\Partition1` / `\Device\Cdrom0` device path | yes | no | **✗ missing** — code that opens by NT device path won't work |
## B.9 GPU
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| Register file zeroed | yes | yes | **=** |
| **Gamma ramps preloaded** (256-entry sRGB + 128-entry PWL) at `CommandProcessor::Initialize` | **yes** | **no** | **✗ missing** — games that read gamma registers before writing them will see linear-zero, not the canary sRGB ramp. Likely cosmetic, unless gamma is queried during init. |
| MMIO range hooked | `[0x7FC8_0000, 0xFFFF_0000]` | same range | **=** |
| CP thread parked | parked on `write_ptr_index_event_` | xenia-rs GPU worker is on the host side, not modeled as a guest scheduler thread | **≈ semantic** |
| Ringbuffer pre-allocated | no (guest does it via `VdInitializeRingBuffer`) | no | **=** |
| Vsync / interrupts | parked until callback registered | parked until callback registered | **=** |
## B.10 Audio (APU)
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| `XmaDecoder` instantiated | yes | no (apu crate is stub) | **✗ missing** |
| Worker thread spawned | yes, parked on semaphores | not at boot; xaudio worker(s) spawn on `XAudioRegisterRenderDriverClient` | **!= wrong** but covered by lazy mechanism |
| 256 client semaphores | yes, count=0 each | 8 client slots (None), synthetic handle base `0xF000_0000` | **!= wrong** — different architecture (host workers + 256 sems vs. guest worker park-on-synthetic-handle) but same observable effect at entry |
| Periodic buffer-complete tick | driven by host audio device callback | driven by `xaudio_tick_enabled` every 48,000 insns | **≈ semantic** |
## B.11 HID
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| `connected_slots` = 0 (all 4 disconnected) | yes | yes | **=** |
| `XInputGetCapabilities` returns `ERROR_DEVICE_NOT_CONNECTED` | yes | yes (when no UI) | **=** |
| Rumble stub | yes | yes | **=** |
## B.12 Network
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| `WSAStartup` etc. | full kernel exports | stub returning success | **!= wrong** semantically but at entry both = uninitialized |
| `XNetGetTitleXnAddr` | returns IP=127.0.0.1, MAC=`CC CC CC CC CC CC`, `XNET_GET_XNADDR_STATIC` | not exported (returns 0/error) | **✗ missing** — any code that probes XNet status will see different result |
| Sockets pre-created | none | none | **=** |
## B.13 Clock
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| `KeQueryPerformanceFrequency` | host CPU tick frequency | fixed | **!= wrong** for any title that uses this for real-time timing |
| `KeQuerySystemTime` | wall clock since emulator startup | fixed `132_500_000_000_000_000` | **!= wrong** for save-game timestamps, anything time-dependent |
| `KeQueryInterruptTime` | host-derived | fixed `0x0000_0001_0000_0000` | **!= wrong** |
| `KeTimeStampBundle` updates | repeating `HighResolutionTimer` every 1 ms | static | **✗ missing** — main loop polling for forward progress will hang |
| `KeTimeStampBundle.tick_count` location | `[+0x10]` u32 | not at `[+0x10]`; ours writes FILETIME hi there | **!= wrong** layout |
| `KeTimeStampBundle.system_time` | `[+0x08]` u64 | `[+0x08]` is **never written** (0) | **✗ missing** |
## B.14 Threading
| Element | Canary | xenia-rs | Status |
|----|----|----|----|
| Main thread starts `X_CREATE_SUSPENDED` then resumes after `PreLaunch` | yes | no (interpreter loop starts running immediately) | **≈ semantic** (no debugger attach hook) |
| Kernel dispatch worker thread | yes, host-side | no | **✗ missing** |
| GPU command processor thread | yes, host-side, parked | yes (M1.9 default), but no guest visibility | **=** |
| Audio worker | yes, parked on 256 sems | xaudio fires via guest workers parked on synthetic handles | **!= wrong** architecture |
---
# §C — Highest-impact divergences (ranked)
These are the items most likely to cause guest behavior divergence. Sorted by likely blast radius:
1. **`KeTimeStampBundle` layout + static values + missing repeating-timer update.** Games poll this. xenia-rs writes the wrong fields, never updates them, and `[+0x08]` (canary's `system_time`) is always 0. If anything games polls `[+0x10]` (the `tick_count` slot) expecting forward progress, it sees the upper half of a fake FILETIME, not a monotonically-increasing tick count.
2. **`KernelGuestGlobals` object-type sub-structs are all-zero in xenia-rs.** Canary's `InitializeKernelGuestGlobals` (`kernel_state.cc:1511+`) populates `ExEventObjectType`, `ExMutantObjectType`, `ExSemaphoreObjectType`, `ExTimerObjectType`, `IoCompletionObjectType`, `IoDeviceObjectType`, `IoFileObjectType`, `ObDirectoryObjectType`, `ObSymbolicLinkObjectType`, `UsbdBootEnumerationDoneEvent` with real bytes (pool tag, vtable-ish, etc.). xenia-rs returns either nullptr (for unhandled ordinals) or a zero block (for `ExThreadObjectType` only). Any guest code that reads object-type fields will see 0 in ours.
3. **`XboxHardwareInfo` not initialized.** Canary writes `[0]=0x20` (HDD bit) and `[4]=0x06` (CPU count). xenia-rs writes 0. Games that branch on HDD-present or CPU-count will diverge.
4. **`ExConsoleGameRegion = 0xFFFFFFFF` not set.** Canary returns "region-free". xenia-rs returns 0 (no region). Could trip region-check codepaths.
5. **TLS slot count = 0 vs 1024 default**, and **no extended TLS image copy**. If Sylpheed has `__declspec(thread)` data, xenia-rs starts with all zeros instead of the XEX-provided initial values.
6. **PCR field gaps**: missing `pcr_ptr` (+0x30), `host_stash` (+0x38), `stack_base_ptr` (+0x70), `stack_end_ptr` (+0x74), `prcb` (+0x104); and `[+0x100]` holds the *handle* `0x1000` rather than the guest KTHREAD VA. Any kernel-thunk or HLE-helper that walks PCR will see garbage.
7. **Stack size hardcoded to 1 MiB**, XEX `XEX_HEADER_DEFAULT_STACK_SIZE` ignored. No guard pages. Stack overflow goes undetected.
8. **`ExLoadedCommandLine` empty** instead of canary's `"default.xex"`. Probably low-impact (rarely parsed), but observably different.
9. **No `ExLoadedImageName`** (canary fills with module path).
10. **GPU gamma ramps not preloaded.** Cosmetic at worst, but a real divergence.
11. **User profile**: canary has a populated profile (XUID, gamertag, 18 settings); xenia-rs has none. Title-side branches on signed-in state are equivalent (both return 1), but any code reading `XamUserGetXUID` or profile settings will diverge.
12. **Filesystem mount**: canary always mounts `game:` whatever the input format; xenia-rs only mounts if `.iso`/`.xiso`, and under `d` (no `game:` symlink). Title code opening `game:\\…` paths will fail on non-ISO inputs.
13. **Kernel dispatch worker thread absent.** Guest async callbacks routed differently.
# §D — Things xenia-rs gets right
For completeness, these are bit-equivalent / verified-matching:
- All CPU registers except r1 value, r13 base, and LR.
- MSR (`0x9030`), VSCR (`0x00010000` NJ-only), VRSAVE (`0xFFFFFFFF`).
- Stack VA base (`0x70000000`), TLS VA base (`0x7FFE_0000`), PCR VA base (`0x7FFF_0000`).
- GPU register file zero-init; MMIO range; ring-buffer NOT pre-allocated.
- HID: 4 disconnected slots.
- Handle allocator starts at `0x1000`, step 4 (canary uses `0xF8000000` but spacing matches).
- Main thread refcount = 2 (creator + self).
- No pre-created sync primitives.
# §E — Recommended verification & remediation order
Cheap, high-value first:
1. **Fix `KeTimeStampBundle` layout + repeating update**. ~30 LOC: change init to write `interrupt_time u64` at `[+0]`, `system_time u64` at `[+8]`, `tick_count u32` at `[+0x10]`, padding at `[+0x14]`; add a `tokio`/`std::thread` repeating tick at 1 ms to update tick_count (or per-host-tick on emulator wallclock). High likely impact.
2. **Add `XboxHardwareInfo` (0x017A)** handler: 16B with `[0]=0x20` `[4]=0x06`. ~5 LOC.
3. **Add `ExConsoleGameRegion` (0x015F)** handler: 4B = `0xFFFF_FFFF`. ~3 LOC.
4. **Add `ExLoadedImageName` (0x01AD)** handler: 1024B containing module path. ~10 LOC.
5. **Fill `KernelGuestGlobals` object-type bytes** for the 10 ordinals listed in B.5 (Ev/Mu/Sem/Tim/IoCompl/IoDev/IoFile/ObDir/ObSym/Usbd). Sizing + bytes need to be read from canary `kernel_state.cc:1511+`. ~50-100 LOC.
6. **Honor `XEX_HEADER_DEFAULT_STACK_SIZE`** instead of hardcoded 1 MiB; allocate guard pages above and below the stack body. ~20-30 LOC.
7. **Add `XEX_HEADER_TLS_INFO` parsing** in the boot-CPU path: set initial `tls_slot_count = max(1024, header.slot_count)`, copy `raw_data_address` into the TLS block. ~30 LOC.
8. **Fix PCR field initialization** to include `pcr_ptr` (+0x30), `stack_base_ptr` (+0x70), `stack_end_ptr` (+0x74), `prcb` (+0x104). Resize PCR to `0x2D8`. Write a real guest KTHREAD VA into `[+0x100]` (allocate guest memory for X_KTHREAD struct, init with `KTHREAD` dispatcher header + minimum required fields, store its VA). ~50-80 LOC.
9. **Preload GPU gamma ramps** in `GpuSystem::new()` (or first init). ~20 LOC translating from canary's `command_processor.cc:130-148`.
10. **Mount `game:` symlink for any input type**, not just XISO. Add `HostPathDevice`/`StfsContainerDevice` cases. Larger change in VFS layer.
Each step is independently testable via `--phase-b-snapshot` (the existing kernel-state dump hook).
---
# §F — Source-of-truth files
xenia-rs:
- [xenia-rs/crates/xenia-cpu/src/context.rs](xenia-rs/crates/xenia-cpu/src/context.rs) — `PpcContext::new`, `LR_HALT_SENTINEL`, `VSCR_NJ_MASK`.
- [xenia-rs/crates/xenia-app/src/main.rs](xenia-rs/crates/xenia-app/src/main.rs) — stack/PCR/TLS alloc, CPU context override, variable-export patching.
- [xenia-rs/crates/xenia-kernel/src/state.rs](xenia-rs/crates/xenia-kernel/src/state.rs) — `KernelState` defaults, TLS index, handle allocator.
- [xenia-rs/crates/xenia-kernel/src/xam.rs](xenia-rs/crates/xenia-kernel/src/xam.rs) — XAM stubs.
- [xenia-rs/crates/xenia-kernel/src/xaudio.rs](xenia-rs/crates/xenia-kernel/src/xaudio.rs) — XAudio worker model.
- [xenia-rs/crates/xenia-gpu/src/register_file.rs](xenia-rs/crates/xenia-gpu/src/register_file.rs) — GPU register file.
- [xenia-rs/crates/xenia-gpu/src/mmio_region.rs](xenia-rs/crates/xenia-gpu/src/mmio_region.rs) — MMIO range.
- [xenia-rs/crates/xenia-cpu/src/scheduler.rs](xenia-rs/crates/xenia-cpu/src/scheduler.rs) — scheduler / TLS slot count / hw threads.
canary (reference):
- See [inventory.md](inventory.md) §17 for the canary file list.