[iterate-2J] KeTimeStampBundle deterministic tick: fix frozen+mislaid guest clock
The xboxkrnl data export KeTimeStampBundle (ordinal 0x00AD, import slot
0x820007d0 — confirmed via sylpheed.db imports table) was set up with TWO
defects in the import-patch pass:
1. FROZEN: the block was written once at boot and never updated, so every
field stayed a constant for the whole run (observed: the guest's clock
reader sub_824AA830 = [[0x820007d0]+0x10] returned a constant
0x01d6bc0c from 5M..150M instructions).
2. WRONG LAYOUT: it stuffed the FILETIME high-dword at +0x10. The canonical
X_TIME_STAMP_BUNDLE (xenia-canary kernel_state.h) is:
+0x00 interrupt_time u64 (100ns since boot)
+0x08 system_time u64 (FILETIME 100ns since 1601)
+0x10 tick_count u32 (milliseconds since boot)
+0x14 padding
so [block+0x10] is tick_count in ms, not a FILETIME dword.
Fix (deterministic, no wall-clock):
* Initialize the block with the correct field layout (tick_count = 0 at
boot, system_time = FILETIME base, interrupt_time = 0).
* Store the block VA on KernelState::timestamp_bundle_addr during the
import patch.
* Add KernelState::update_timestamp_bundle(mem, clock) and call it every
round in BOTH the lockstep (run_execution) and parallel
(run_execution_parallel) outer loops, right where the deterministic
Scheduler::global_clock is advanced. The clock is the retired-instruction
monotonic global_clock, so every guest-visible time value stays a pure
function of guest progress (lockstep byte-reproducible).
* Cadence: 1 global_clock unit = 100ns (coherent with parse_timeout, which
divides 100ns timeouts by 100 onto the same basis), so
INSTRUCTIONS_PER_MS = 10_000. tick_count now advances 0 -> ~4999ms over
a 50M-instruction window. Also make KeQuerySystemTime read the same
100ns clock instead of a frozen FILETIME constant.
Verification: tick_count at 0x40002010 now advances (deadline arm at
0x82450d0c stores clock+66 = 0x260,0x269,...,0x51d,... advancing, vs the
frozen 0x01d6bc4e before the fix). Determinism: two cold --stable-digest
runs are byte-identical; the n50m golden is UNCHANGED (the clock-affected
counter is not in the stable digest). 672/672 tests pass.
HONEST CAVEAT — the predicted render cascade did NOT materialize on this
branch. The diagnosed consuming gate at 0x82450b10 (the clock-vs-deadline
compare in the worker-hub channel loop sub_82450A68) is unreachable here:
the loop always branches away at 0x82450b0c ([this+220] >= channel-index),
so the hub already dispatches sub_82450B68 342x in BOTH the frozen and
fixed builds. Guest trajectory (imports 339766@50M / 1738001@200M /
9212446@1B), draws (0), swaps (2) and thread topology (tid14 Ready, not
blocked on 0x109c) are identical frozen-vs-fixed. This commit is therefore
a correct latent-clock-bug fix and determinism-safe prerequisite, NOT the
render unblock. The 0x109c/tid14 starvation premise was not reproduced at
f75bc96; the next gate must be re-localized.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user