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>
34 KiB
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:
- Parse XEX header (
parse_xex2_header) — extract image base, entry point, imports, TLS info. - Decompress + decrypt image (
load_image) and write into guest memory. - Resolve imports → build
thunk_map. - Allocate stack at fixed VA
0x70000000, size0x100000(1 MiB). main.rs:933-936 - Allocate PCR at fixed VA
0x7FFF_0000, size0x1000(4 KiB); TLS at fixed0x7FFE_0000, size0x1000. main.rs:939-942 - Write 3 PCR fields:
[+0x00]=tls_addr,[+0x100]=0x1000(fake),[+0x150]=0. main.rs:945-947 - Build CPU context:
PpcContext::new()+ overridepc,gpr[1],gpr[2],gpr[3..=7],gpr[13],msr. main.rs:953-966 - Construct
KernelStatewith GPU backend, register thunks. main.rs:1014-1028 - Install MMIO region (GPU). main.rs:1441
- Allocate
main_handle=0x1000and install the initial thread on HW slot 0. main.rs:1446-1467 Writepcr[+0x2C]=0(processor number). kernel.retain_handle(main_handle)— mirrors canary'sXThread::Create→RetainHandle(). main.rs:1476- If XISO, mount
d:device. main.rs:1480-1485 - Patch variable imports by ordinal — only the ones the XEX imports get non-zero values. main.rs:1496-1588
- (optional) DB writer + analysis.
- 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 firstExAllocateTls(state.rs:1539).scheduler.tls_slot_count = 0at scheduler.rs:360, 396; main thread receivestls_values = vec.- TLS block in guest memory at
0x7FFE_0000is 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_idstarts atINITIAL_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_000insns (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 (viaalloc_handle_for(KernelObject::Thread {...})). - No event/semaphore/mutex pre-creation.
8. XAM
Largely stub-only:
- No
UserProfilestruct ever instantiated. Queries returnERROR_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 filevec![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(mask0xFFFF_0000, 64 KiB window), registersCP_RB_WPTR,CP_RB_RPTR,CP_INT_STATUS,CP_INT_ACK,D1MODE_VBLANK_VLINE_STATUShooked. mmio_region.rs:23-27 - Backend: threaded (M1.9 default),
Arc-sharedGpuSystem. Vulkan/D3D12 device is not created at boot — it stays in the "no-presenter" mode unless--uiis used.
11. Audio (APU)
Largely a stub:
xenia-apuis described as stub-only (lib.rs:1-16). No XMA decoder.- The functional audio path is
xenia-kernel/src/xaudio.rs: 8 client slots (allNone), synthetic park-handle base0xF000_0000, empty pending-fire FIFO, no worker threads. - A periodic
xaudio_tick_enabled = truewill fire buffer-complete callbacks every 48,000 instructions (≈5.33 ms wall) but only after the guest callsXAudioRegisterRenderDriverClient.
12. HID, Network, Display, Clock
- HID: single
GamepadStatezeroed, all 4 slots disconnected. - Network:
WSAStartup/WSACleanupare no-ops; no IP/MAC value pre-written. No XNet stack. - Video mode: hardcoded HDMI 1280×720 widescreen via
XGetVideoMode/XGetAVPackexports (xam.rs:631-654). - Clock:
KeQuerySystemTimereturns fixed FILETIME132_500_000_000_000_000;KeQueryInterruptTimereturns fixed0x0000_0001_0000_0000(exports.rs:869-883). NoHighResolutionTimerrepeating 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:
KeTimeStampBundlelayout + static values + missing repeating-timer update. Games poll this. xenia-rs writes the wrong fields, never updates them, and[+0x08](canary'ssystem_time) is always 0. If anything games polls[+0x10](thetick_countslot) expecting forward progress, it sees the upper half of a fake FILETIME, not a monotonically-increasing tick count.KernelGuestGlobalsobject-type sub-structs are all-zero in xenia-rs. Canary'sInitializeKernelGuestGlobals(kernel_state.cc:1511+) populatesExEventObjectType,ExMutantObjectType,ExSemaphoreObjectType,ExTimerObjectType,IoCompletionObjectType,IoDeviceObjectType,IoFileObjectType,ObDirectoryObjectType,ObSymbolicLinkObjectType,UsbdBootEnumerationDoneEventwith real bytes (pool tag, vtable-ish, etc.). xenia-rs returns either nullptr (for unhandled ordinals) or a zero block (forExThreadObjectTypeonly). Any guest code that reads object-type fields will see 0 in ours.XboxHardwareInfonot 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.ExConsoleGameRegion = 0xFFFFFFFFnot set. Canary returns "region-free". xenia-rs returns 0 (no region). Could trip region-check codepaths.- 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. - PCR field gaps: missing
pcr_ptr(+0x30),host_stash(+0x38),stack_base_ptr(+0x70),stack_end_ptr(+0x74),prcb(+0x104); and[+0x100]holds the handle0x1000rather than the guest KTHREAD VA. Any kernel-thunk or HLE-helper that walks PCR will see garbage. - Stack size hardcoded to 1 MiB, XEX
XEX_HEADER_DEFAULT_STACK_SIZEignored. No guard pages. Stack overflow goes undetected. ExLoadedCommandLineempty instead of canary's"default.xex". Probably low-impact (rarely parsed), but observably different.- No
ExLoadedImageName(canary fills with module path). - GPU gamma ramps not preloaded. Cosmetic at worst, but a real divergence.
- 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
XamUserGetXUIDor profile settings will diverge. - Filesystem mount: canary always mounts
game:whatever the input format; xenia-rs only mounts if.iso/.xiso, and underd(nogame:symlink). Title code openinggame:\\…paths will fail on non-ISO inputs. - 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 (0x00010000NJ-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 uses0xF8000000but spacing matches). - Main thread refcount = 2 (creator + self).
- No pre-created sync primitives.
§E — Recommended verification & remediation order
Cheap, high-value first:
- Fix
KeTimeStampBundlelayout + repeating update. ~30 LOC: change init to writeinterrupt_time u64at[+0],system_time u64at[+8],tick_count u32at[+0x10], padding at[+0x14]; add atokio/std::threadrepeating tick at 1 ms to update tick_count (or per-host-tick on emulator wallclock). High likely impact. - Add
XboxHardwareInfo(0x017A) handler: 16B with[0]=0x20[4]=0x06. ~5 LOC. - Add
ExConsoleGameRegion(0x015F) handler: 4B =0xFFFF_FFFF. ~3 LOC. - Add
ExLoadedImageName(0x01AD) handler: 1024B containing module path. ~10 LOC. - Fill
KernelGuestGlobalsobject-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 canarykernel_state.cc:1511+. ~50-100 LOC. - Honor
XEX_HEADER_DEFAULT_STACK_SIZEinstead of hardcoded 1 MiB; allocate guard pages above and below the stack body. ~20-30 LOC. - Add
XEX_HEADER_TLS_INFOparsing in the boot-CPU path: set initialtls_slot_count = max(1024, header.slot_count), copyraw_data_addressinto the TLS block. ~30 LOC. - Fix PCR field initialization to include
pcr_ptr(+0x30),stack_base_ptr(+0x70),stack_end_ptr(+0x74),prcb(+0x104). Resize PCR to0x2D8. Write a real guest KTHREAD VA into[+0x100](allocate guest memory for X_KTHREAD struct, init withKTHREADdispatcher header + minimum required fields, store its VA). ~50-80 LOC. - Preload GPU gamma ramps in
GpuSystem::new()(or first init). ~20 LOC translating from canary'scommand_processor.cc:130-148. - Mount
game:symlink for any input type, not just XISO. AddHostPathDevice/StfsContainerDevicecases. 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 —
PpcContext::new,LR_HALT_SENTINEL,VSCR_NJ_MASK. - 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 —
KernelStatedefaults, TLS index, handle allocator. - xenia-rs/crates/xenia-kernel/src/xam.rs — XAM stubs.
- xenia-rs/crates/xenia-kernel/src/xaudio.rs — XAudio worker model.
- xenia-rs/crates/xenia-gpu/src/register_file.rs — GPU register file.
- xenia-rs/crates/xenia-gpu/src/mmio_region.rs — MMIO range.
- xenia-rs/crates/xenia-cpu/src/scheduler.rs — scheduler / TLS slot count / hw threads.
canary (reference):
- See inventory.md §17 for the canary file list.