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>
437 lines
34 KiB
Markdown
437 lines
34 KiB
Markdown
# 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.
|