diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index 3a20f47..a31754d 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -2473,6 +2473,24 @@ fn coord_pre_round( } kernel.fire_due_timers(); + // 2.AF — fire expired wait-deadlines under load. Without this call + // `advance_to_next_wake_if_due` only runs in `coord_idle_advance` + // (no Ready threads), so a thread whose `KeWait*` deadline expires + // while other threads keep the scheduler busy sits Blocked forever + // (2.AD: tid=5's 42.95ms deadline expired by ~22s but never fired + // for 29s+). Drain every entry whose deadline `<=` the current + // guest timebase (the same `now` `fire_due_timers` uses, so the + // two stay in lock-step) and let `handle_timeout_wake` stamp + // `STATUS_TIMEOUT` and scrub the waiter from each handle. + // Deterministic: `Scheduler::ctx(0).timebase` is the guest-cycle + // timebase, not host_ns. + loop { + let now = kernel.scheduler.ctx(0).timebase; + let Some((r, reason)) = kernel.scheduler.advance_to_next_wake_if_due(now) else { + break; + }; + kernel.handle_timeout_wake(r, reason); + } try_inject_graphics_interrupt(kernel); if kernel.xaudio_tick_enabled { try_inject_audio_callback(kernel); diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index 7582fa0..1a8585a 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -3038,9 +3038,23 @@ pub(crate) fn close_handle_internal(state: &mut KernelState, handle: u32) { } fn nt_create_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) { - // r3 = handle_ptr, r4 = obj_attrs, r5 = event_type, r6 = initial_state + // r3 = handle_ptr, r4 = obj_attrs, r5 = event_type, r6 = initial_state. + // + // 2.AI — Xenon DISPATCHER_HEADER `Type` (NT convention): + // 0 = NotificationEvent (manual-reset) + // 1 = SynchronizationEvent (auto-reset) + // Canary mirrors this at `xboxkrnl_threading.cc:620` + // (`ev->Initialize(!event_type, !!initial_state)`) and our own + // `ensure_dispatcher_object` (above, type=0→manual, type=1→auto). + // + // The prior polarity here was inverted (`event_type != 0` → manual), + // which silently mis-classified Sylpheed's per-frame VSync gate as + // manual-reset+initial-signaled. `handle_consume` is a no-op for + // manual-reset events, so the wait fast-path returned SUCCESS every + // call (1.05 M iterations on the wedge handle 0x10e8 in 2.AF, zero + // signal.match) instead of blocking ~17 ms for the next VSync. let handle_ptr = ctx.gpr[3] as u32; - let manual_reset = ctx.gpr[5] != 0; + let manual_reset = ctx.gpr[5] == 0; let signaled = ctx.gpr[6] != 0; let handle = state.alloc_handle_for(KernelObject::Event { manual_reset,