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>
This commit is contained in:
MechaCat02
2026-06-05 07:19:08 +02:00
parent acd1656753
commit ef93a4fa14
620 changed files with 108303 additions and 1 deletions

View File

@@ -0,0 +1,157 @@
diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs
index a4dfa7d..56232fb 100644
--- a/crates/xenia-kernel/src/exports.rs
+++ b/crates/xenia-kernel/src/exports.rs
@@ -46,7 +46,13 @@ pub fn register_exports(state: &mut KernelState) {
state.register_export(Xboxkrnl, 0x81, "KeQueryBasePriorityThread", ke_query_base_priority_thread);
state.register_export(Xboxkrnl, 0x82, "KeQueryIdealProcessor", ke_query_ideal_processor);
state.register_export(Xboxkrnl, 0x83, "KeQueryPerformanceFrequency", ke_query_performance_frequency);
- state.register_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time);
+ // Canary declares `void KeQuerySystemTime_entry(lpqword_t time_ptr, ...)`
+ // (xboxkrnl_threading.cc:459); the time is delivered via the OUT
+ // pointer, not via gpr[3]. Phase A's `kernel.return.return_value`
+ // must be 0 (canary literal) — not r3 (which for ours is the input
+ // arg `time_ptr` left untouched). See `register_void_export` doc in
+ // state.rs.
+ state.register_void_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time);
state.register_export(Xboxkrnl, 0x85, "KeRaiseIrqlToDpcLevel", stub_return_zero);
state.register_export(Xboxkrnl, 0x88, "KeReleaseSemaphore", ke_release_semaphore);
state.register_export(Xboxkrnl, 0x89, "KeReleaseSpinLockFromRaisedIrql", ke_release_spinlock_from_raised_irql);
diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs
index b256fe7..b076ff7 100644
--- a/crates/xenia-kernel/src/state.rs
+++ b/crates/xenia-kernel/src/state.rs
@@ -50,6 +50,17 @@ pub const HMODULE_XAM: u32 = 0xFFFE_0002;
/// Central kernel state tracking all guest OS state.
pub struct KernelState {
exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>,
+ /// Phase A: kernel exports whose canary signature is `void` (no
+ /// dword_result_t / pointer_result_t). For symmetry with canary's
+ /// `if constexpr (std::is_void<R>::value)` trampoline branch
+ /// (see `xenia-canary/src/xenia/kernel/util/shim_utils.h`), the
+ /// Phase A `kernel.return` event for these exports emits
+ /// `return_value=0` instead of `gpr[3]` (which for void fns is
+ /// just the input arg pointer left untouched). Without this,
+ /// e.g. `KeQuerySystemTime` — declared `void` in canary, taking a
+ /// `lpqword_t time_ptr` — would report ours's r3=time_ptr but
+ /// canary's literal 0, producing a spurious diff. Cvar-OFF inert.
+ void_exports: std::collections::HashSet<(ModuleId, u32)>,
/// M2.4: bump allocator for kernel handles. `AtomicU32` so concurrent
/// HLE calls under M3 can `fetch_add` without a lock. `Relaxed` is
/// fine — the allocated value is a fresh ID with no prior payload to
@@ -264,6 +275,23 @@ pub struct KernelState {
pub dump_addrs: Vec<u32>,
/// `--dump-section=BASE:LEN:PATH` end-of-run snapshot, page-gated by `is_mapped`.
pub dump_section: Option<(u32, u32, std::path::PathBuf)>,
+ /// Phase B initial-state snapshot — directory under which a
+ /// `ours/{cpu_state,memory,kernel,vfs,config}.json` + `manifest.json`
+ /// snapshot is written at the moment immediately before the first
+ /// guest PPC instruction of the XEX entry_point. `None` (default) =
+ /// disabled, zero overhead. See
+ /// `xenia-rs/audit-runs/phase-b-state-equivalence/`.
+ pub phase_b_snapshot_dir: Option<std::path::PathBuf>,
+ /// Phase B: after writing the snapshot, exit the process immediately
+ /// so re-runs are byte-deterministic. Default false.
+ pub phase_b_snapshot_and_exit: bool,
+ /// Phase B: include raw bytes in `memory.json`'s `section_contents`.
+ /// Default false — per-region SHA-256 is enough for the routine diff.
+ pub phase_b_dump_section_content: bool,
+ /// Phase B: the XEX entry_point address — captured by the app at
+ /// `install_initial_thread` time and consulted by the snapshot hook
+ /// to validate the firing thread is the entry thread.
+ pub entry_pc: u32,
}
impl KernelState {
@@ -288,6 +316,7 @@ impl KernelState {
scheduler.set_reservation_table(Some(reservations.clone()));
let mut state = Self {
exports: HashMap::new(),
+ void_exports: std::collections::HashSet::new(),
next_handle: AtomicU32::new(0x1000),
scheduler,
next_tls_index: AtomicU32::new(0),
@@ -331,6 +360,10 @@ impl KernelState {
lr_trace_writer: None,
dump_addrs: Vec::new(),
dump_section: None,
+ phase_b_snapshot_dir: None,
+ phase_b_snapshot_and_exit: false,
+ phase_b_dump_section_content: false,
+ entry_pc: 0,
};
crate::exports::register_exports(&mut state);
crate::xam::register_exports(&mut state);
@@ -377,6 +410,22 @@ impl KernelState {
self.exports.insert((module, ordinal), (name, func));
}
+ /// Register a kernel export whose canary signature is `void`.
+ /// See `KernelState::void_exports` doc. Identical semantics to
+ /// `register_export` except the Phase A `kernel.return` payload's
+ /// `return_value` field is emitted as 0 instead of `gpr[3]`,
+ /// matching canary's `EmitReturn(name, 0)` branch.
+ pub fn register_void_export(
+ &mut self,
+ module: ModuleId,
+ ordinal: u32,
+ name: &'static str,
+ func: KernelExportFn,
+ ) {
+ self.exports.insert((module, ordinal), (name, func));
+ self.void_exports.insert((module, ordinal));
+ }
+
/// AUDIT-038 — install a host directory as the backing store for the
/// `cache:` mount. The directory is unconditionally cleared (and then
/// re-created) on entry so two consecutive runs see byte-identical
@@ -514,7 +563,49 @@ impl KernelState {
metrics::counter!("kernel.calls", "name" => name).increment(1);
tracing::trace!(target: "probe_calls", "hw={} call={} r3={:#x} r4={:#x} r5={:#x} lr={:#x}",
r.hw_id, name, ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.lr);
+ // Phase A event log — see crates/xenia-kernel/src/event_log.rs.
+ // Hot path: `is_enabled` is a relaxed atomic-bool load.
+ let phase_a_on = crate::event_log::is_enabled();
+ let (phase_a_tid, phase_a_cycle) = if phase_a_on {
+ let tid = self.scheduler.thread(r).tid;
+ let cycle = ctx.cycle_count;
+ (tid, cycle)
+ } else {
+ (0u32, 0u64)
+ };
+ if phase_a_on {
+ let module_name = match module {
+ ModuleId::Xboxkrnl => "xboxkrnl.exe",
+ ModuleId::Xam => "xam.xex",
+ ModuleId::Xbdm => "xbdm.xex",
+ };
+ crate::event_log::emit_import_call(
+ phase_a_tid,
+ phase_a_cycle,
+ module_name,
+ ordinal as u16,
+ name,
+ );
+ crate::event_log::emit_kernel_call(phase_a_tid, phase_a_cycle, name);
+ }
+ let is_void = self.void_exports.contains(&(module, ordinal));
func(&mut ctx, mem, self);
+ if phase_a_on {
+ // Mirror canary's `if constexpr (std::is_void<R>::value)`
+ // trampoline branch: void exports emit literal 0; non-void
+ // emit post-call gpr[3]. Without this, void exports that
+ // take a pointer arg (e.g. `KeQuerySystemTime`) would
+ // report ours=r3=arg_ptr vs canary=0 — a Phase A diff
+ // that is purely an emitter-framing asymmetry, not an
+ // engine semantic divergence.
+ let return_value = if is_void { 0 } else { ctx.gpr[3] };
+ crate::event_log::emit_kernel_return(
+ phase_a_tid,
+ ctx.cycle_count,
+ name,
+ return_value,
+ );
+ }
true
} else {
metrics::counter!("kernel.unimplemented").increment(1);