diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index 004dad6..0f1aaec 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -3262,23 +3262,39 @@ fn dispatch_graphics_interrupts( return; }; - // Audio injection (audit-048 Plan B) still uses the asynchronous - // LR-sentinel path. If an audio callback is mid-flight we must not - // try to clobber the borrowed context — bail until the audio path - // returns through the worker_prologue restore. - if kernel.interrupts.is_in_callback() { - return; - } + // Iterate-2.BF.γ: graphics dispatch is fully synchronous (host-driven, + // iterate-2.BE) — it borrows a guest thread, runs the ISR to + // LR_HALT_SENTINEL, and restores all in-call before returning. So it + // CAN safely coexist with an audio callback mid-flight, *as long as we + // pick a different victim thread* than the one audio borrowed. The old + // blanket `is_in_callback()` gate caused 5.85M skipped dispatches in + // lockstep boot (vs 55 with-pending dispatches) — audio is essentially + // always mid-flight on its dedicated worker, which choked vsync + // delivery at ~54. Exclude only audio's borrowed thread; the queue + // drains synchronously and graphics ISR completion does not touch + // `interrupts.saved` (used exclusively by the async audio path). + let audio_borrowed = if kernel.interrupts.is_in_callback() { + kernel.interrupts.injected_ref + } else { + None + }; while let Some(source) = kernel.interrupts.peek_next() { // Victim selection: Ready first, then Blocked (canary's // `XThread::GetCurrentThread()` analog — any live thread will // do for borrowing context). Skip Idle/Exited/ServicingIrq. + // Skip the audio-borrowed thread (if any) to avoid clobbering + // its `SavedCallbackCtx` mid-flight. + let excluded = audio_borrowed; let mut victim: Option = None; 'outer_ready: for (hw_id, slot) in kernel.scheduler.slots.iter().enumerate() { for (idx, t) in slot.runqueue.iter().enumerate() { + let r = xenia_cpu::ThreadRef::new(hw_id as u8, idx as u16); + if excluded == Some(r) { + continue; + } if matches!(t.state, HwState::Ready) { - victim = Some(xenia_cpu::ThreadRef::new(hw_id as u8, idx as u16)); + victim = Some(r); break 'outer_ready; } } @@ -3286,8 +3302,12 @@ fn dispatch_graphics_interrupts( if victim.is_none() { 'outer_blocked: for (hw_id, slot) in kernel.scheduler.slots.iter().enumerate() { for (idx, t) in slot.runqueue.iter().enumerate() { + let r = xenia_cpu::ThreadRef::new(hw_id as u8, idx as u16); + if excluded == Some(r) { + continue; + } if matches!(t.state, HwState::Blocked(_)) { - victim = Some(xenia_cpu::ThreadRef::new(hw_id as u8, idx as u16)); + victim = Some(r); break 'outer_blocked; } }