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

34 KiB
Raw Blame History

Xenia-rs Boot State Inventory + Canary Comparison

Companion to 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:

  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
  5. Allocate PCR at fixed VA 0x7FFF_0000, size 0x1000 (4 KiB); TLS at fixed 0x7FFE_0000, size 0x1000. main.rs:939-942
  6. Write 3 PCR fields: [+0x00]=tls_addr, [+0x100]=0x1000 (fake), [+0x150]=0. main.rs:945-947
  7. Build CPU context: PpcContext::new() + override pc, gpr[1], gpr[2], gpr[3..=7], gpr[13], msr. main.rs:953-966
  8. Construct KernelState with GPU backend, register thunks. main.rs:1014-1028
  9. Install MMIO region (GPU). main.rs:1441
  10. Allocate main_handle = 0x1000 and install the initial thread on HW slot 0. main.rs:1446-1467 Write pcr[+0x2C]=0 (processor number).
  11. kernel.retain_handle(main_handle) — mirrors canary's XThread::CreateRetainHandle(). main.rs:1476
  12. If XISO, mount d: device. main.rs:1480-1485
  13. Patch variable imports by ordinal — only the ones the XEX imports get non-zero values. main.rs:1496-1588
  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 (PpcContext::new) + main.rs:953-966 (overrides).

Reg Value Source
r0 0 PpcContext::new
r1 (SP) ((stack_base + stack_size) - 0x100) & ~0xF = 0x700F_FF00 main.rs:958-959
r2 0x2000_0000 main.rs:960
r3..r7 explicitly 0 (loop) main.rs:964
r8..r12 0 PpcContext::new
r13 0x7FFF_0000 (fixed VA, no allocation) main.rs:965
r14..r31 0 PpcContext::new
LR 0xBCBC_BCBC (halt sentinel — bclr exits the interpreter) context.rs:55, 155
CTR 0 PpcContext::new
MSR 0x9030 main.rs:966
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
VRSAVE 0xFFFF_FFFF context.rs:168
DEC 0 PpcContext::new
timebase, cycle_count 0 PpcContext::new
reservation_* unset / None (M3.7 table optional) context.rs:170-176
PC entry_point_ from XEX header main.rs:954

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
Stack 0x7000_0000 .. 0x7010_0000 1 MiB fixed main.rs:933-936
PCR 0x7FFF_0000 4 KiB fixed main.rs:939-941
TLS 0x7FFE_0000 4 KiB fixed main.rs:940-942
User heap (bump alloc) 0x4000_0000+ state.rs:550
Aux kernel stack alloc cursor 0x7100_0000+ state.rs:551

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):

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. Slots grow on first ExAllocateTls (state.rs:1539).
  • scheduler.tls_slot_count = 0 at scheduler.rs:360, 396; main thread receives tls_values = vec![0; 0] (scheduler.rs:682).
  • 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).

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
  • 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
  • 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

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).


10. GPU

  • GpuSystem::new → register file vec![0u32; 0x6000], all zero. register_file.rs:7-9
  • 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
  • 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). 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).
  • Clock: KeQuerySystemTime returns fixed FILETIME 132_500_000_000_000_000; KeQueryInterruptTime returns fixed 0x0000_0001_0000_0000 (exports.rs:869-883). 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) 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:

canary (reference):