diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index f966583..9f8edc8 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -1497,16 +1497,28 @@ fn cmd_exec_inner( mem.write_u32(addr, block); } ("xboxkrnl.exe", 0x00AD) => { - // KeTimeStampBundle — 0x18 block with FILETIME at +0 and - // interrupt-time u64 at +0x10. Mirrors the clock used by - // KeQuerySystemTime so fast-path readers see consistent values. + // KeTimeStampBundle — X_TIME_STAMP_BUNDLE (canary layout, + // kernel_state.h): +0x00 interrupt_time u64, +0x08 + // system_time u64 (FILETIME 100ns), +0x10 tick_count u32 + // (milliseconds since boot), +0x14 padding. The guest's + // worker-hub channel-dispatch loop (sub_82450A68 @ + // 0x82450b10) polls [block+0x10] (tick_count) and gates + // dispatch on a `tick_count + 66` (ms) deadline. The block + // MUST be ticked over the run or that deadline never + // elapses (tid14 0x109c starvation gate). Initialize to a + // zero-uptime base; KernelState::update_timestamp_bundle + // ticks it every round from the deterministic global_clock. let block = alloc_zero(0x18, &mut mem, &mut kernel); if block != 0 { - let fake_time: u64 = 132_500_000_000_000_000; // ~2021 FILETIME - mem.write_u32(block, (fake_time >> 32) as u32); - mem.write_u32(block + 4, fake_time as u32); - mem.write_u32(block + 0x10, (fake_time >> 32) as u32); - mem.write_u32(block + 0x14, fake_time as u32); + // FILETIME base (~2021) so system_time is plausible. + let fake_time: u64 = 132_500_000_000_000_000; + mem.write_u32(block, 0); // interrupt_time hi + mem.write_u32(block + 4, 0); // interrupt_time lo + mem.write_u32(block + 0x08, (fake_time >> 32) as u32); // system_time hi + mem.write_u32(block + 0x0C, fake_time as u32); // system_time lo + mem.write_u32(block + 0x10, 0); // tick_count (ms) = 0 at boot + mem.write_u32(block + 0x14, 0); // padding + kernel.timestamp_bundle_addr = block; } mem.write_u32(addr, block); } @@ -2852,6 +2864,12 @@ fn run_execution( kernel .scheduler .advance_global_clock_to(stats.instruction_count); + // ITERATE-2J — tick the KeTimeStampBundle (ordinal 0x00AD) from the + // same deterministic clock so the guest's worker-hub tick_count + // deadline gate (`[block+0x10] + 66` ms) actually elapses. Without + // this the block is frozen at boot and the hub spins forever, + // starving tid14 on event 0x109c. + kernel.update_timestamp_bundle(mem, kernel.scheduler.global_clock()); kernel.fire_due_silph_autosignals(stats.instruction_count); dispatch_graphics_interrupts( kernel, @@ -3296,6 +3314,16 @@ fn run_execution_parallel( guard.fire_due_silph_autosignals(s.instruction_count); } + // ITERATE-2J — tick the KeTimeStampBundle (ordinal 0x00AD) from + // the parallel-mode coherent global_clock (summed per-block + // retired instructions). Same fix as the lockstep loop: keeps the + // guest's worker-hub tick_count deadline gate advancing so it + // dispatches channel-3 and unblocks tid14 on event 0x109c. + { + let clock = guard.scheduler.global_clock(); + guard.update_timestamp_bundle(mem, clock); + } + // Iterate-2.BE — host-driven synchronous ISR dispatch. // Runs under the kernel lock while workers are still parked // at the phaser B2 barrier (the coordinator hasn't published diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index 392401e..d7d7e74 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -486,12 +486,20 @@ fn ke_query_performance_frequency(ctx: &mut PpcContext, _mem: &GuestMemory, _sta ctx.gpr[3] = 50_000_000; // 50 MHz } -fn ke_query_system_time(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) { +fn ke_query_system_time(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) { let time_ptr = ctx.gpr[3] as u32; if time_ptr != 0 { - let fake_time: u64 = 132_500_000_000_000_000; // ~2021 FILETIME - mem.write_u32(time_ptr, (fake_time >> 32) as u32); - mem.write_u32(time_ptr + 4, fake_time as u32); + // ITERATE-2J — advance with the same deterministic clock the + // KeTimeStampBundle uses (1 global_clock unit ≈ 100 ns) so a guest + // that polls KeQuerySystemTime for elapsed time also sees forward + // progress instead of a frozen constant. FILETIME base (~2021) + + // 100-ns-unit clock. + const FILETIME_BASE: u64 = 132_500_000_000_000_000; + let hw_id = state.scheduler.current_hw_id().unwrap_or(0); + let now = state.now_basis_at(hw_id); + let system_time = FILETIME_BASE.wrapping_add(now); + mem.write_u32(time_ptr, (system_time >> 32) as u32); + mem.write_u32(time_ptr + 4, system_time as u32); } } diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index 83b37ab..0d6c8cb 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -354,6 +354,16 @@ pub struct KernelState { /// [`Self::fire_due_silph_autosignals`] on the first visit where /// the pending queue is non-empty but no entry is due yet. pub silph_autosignal_diag_logged: bool, + /// ITERATE-2J — guest VA of the `KeTimeStampBundle` block (xboxkrnl + /// data export ordinal 0x00AD). Set during the import-patch pass in + /// `xenia-app`. Zero until then. The guest's worker-hub channel + /// dispatch loop polls `[block+0x10]` (`tick_count`, milliseconds) and + /// gates dispatch on a `tick_count + 66` deadline; if the block is + /// never re-written that deadline never elapses and the hub spins + /// forever (the tid14 0x109c starvation gate). The run loop ticks this + /// block every round from the deterministic `global_clock` via + /// [`Self::update_timestamp_bundle`]. + pub timestamp_bundle_addr: u32, } /// ITERATE-2C Phase D — one queued auto-signal. `deadline_cycle` is @@ -444,6 +454,7 @@ impl KernelState { silph_autosignal_pending: Vec::new(), last_cycle_hint: 0, silph_autosignal_diag_logged: false, + timestamp_bundle_addr: 0, }; crate::exports::register_exports(&mut state); crate::xam::register_exports(&mut state); @@ -862,6 +873,57 @@ impl KernelState { self.last_cycle_hint = now_cycle; } + /// ITERATE-2J — tick the `KeTimeStampBundle` block (xboxkrnl ordinal + /// 0x00AD) from the deterministic monotonic clock so the guest sees a + /// clock that *advances*. + /// + /// `clock` is the scheduler's `global_clock` — a pure function of + /// retired guest instructions (see [`Self::now_basis_at`] / + /// `Scheduler::global_clock`). Lockstep floors it up to + /// `stats.instruction_count` each round; parallel sums per-block + /// retired counts. Using it (rather than wall-clock) keeps every + /// guest-visible time value a deterministic function of guest progress, + /// so lockstep stays byte-reproducible. + /// + /// ## Cadence + /// The existing kernel time math (`parse_timeout` in `exports.rs`) + /// already treats **1 `global_clock` unit ≈ 100 ns**: it converts a + /// signed 100-ns `LARGE_INTEGER` timeout to a deadline by dividing the + /// magnitude by 100 and adding it to `now` (= `global_clock`). To stay + /// coherent with that, this method uses the same scale: + /// + /// * `interrupt_time` / `system_time` (100-ns units): `clock` (with a + /// FILETIME epoch base added to `system_time`). + /// * `tick_count` (milliseconds): `clock / INSTRUCTIONS_PER_MS` where + /// `INSTRUCTIONS_PER_MS = 10_000` (10_000 × 100 ns = 1 ms). + /// + /// At 10_000 clock-units/ms, the guest's `tick_count + 66` ms hub + /// deadline elapses by ~660_000 retired instructions — very early in a + /// ~1 B-instruction boot — while a 16 ms `KeWait` timeout + /// (`parse_timeout`: 160_000 units) still resolves to 16 ms of + /// tick_count, so no timeout collapses to "instant". The two readers + /// share one scale. + pub fn update_timestamp_bundle(&self, mem: &GuestMemory, clock: u64) { + let block = self.timestamp_bundle_addr; + if block == 0 { + return; + } + const INSTRUCTIONS_PER_MS: u64 = 10_000; + // FILETIME epoch base (~2021) so `system_time` is a plausible + // absolute wall-clock; matches the constant used by + // `ke_query_system_time`. interrupt_time is "since boot" so it + // starts at the clock origin (no epoch offset). + const FILETIME_BASE: u64 = 132_500_000_000_000_000; + let interrupt_time: u64 = clock; + let system_time: u64 = FILETIME_BASE.wrapping_add(clock); + let tick_count: u32 = (clock / INSTRUCTIONS_PER_MS) as u32; + // BE writes (write_u64/write_u32 use to_be_bytes) — guest is BE. + mem.write_u64(block, interrupt_time); // +0x00 interrupt_time + mem.write_u64(block + 0x08, system_time); // +0x08 system_time + mem.write_u32(block + 0x10, tick_count); // +0x10 tick_count (ms) + mem.write_u32(block + 0x14, 0); // +0x14 padding + } + /// ITERATE-2C Phase D — register a freshly-allocated event for /// auto-signal after the configured delay, **iff** the creating /// thread matches the silph::UImpl tid=13 chain that wedges in