From d6acb21b2ee869aab461f9a89706a86dd3d6283b Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Fri, 5 Jun 2026 07:19:28 +0200 Subject: [PATCH] wip(probe): throwaway iterate-iterate-2AU-xaudio probe instrumentation Uncommitted experimental probe code preserved for handoff. Per running memory these probes are inert/throwaway diagnostics, not production fixes. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/xenia-app/src/main.rs | 16 +++++++++-- crates/xenia-kernel/src/exports.rs | 45 ++++++++++++++++++++++++++++++ crates/xenia-kernel/src/xaudio.rs | 24 ++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index a31754d..2af0703 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -2465,10 +2465,20 @@ fn coord_pre_round( } if kernel.xaudio_tick_enabled { - if kernel.parallel_active { - kernel.xaudio.tick_wallclock(); + let fired = if kernel.parallel_active { + kernel.xaudio.tick_wallclock() } else { - kernel.xaudio.tick_instr(stats.instruction_count); + kernel.xaudio.tick_instr(stats.instruction_count) + }; + // AUDIT-2AU Option β: on each audio period, re-signal the XAudio + // render loop's captured frame-event pair (buffer-ready / + // frame-done). Emulates canary's host XAudio2 OnBufferEnd firing + // those events every period; without it ours's render loop + // (tid=11) wedges on its second KeWait forever and starves the + // tid=9/10 mixers + tid=12 DPC downstream (2.AS cascade). Gated + // by the same instruction-count tick => deterministic. + if fired { + xenia_kernel::exports::pulse_xaudio_frame_events(kernel); } } diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index 1a8585a..e80aa78 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -5346,6 +5346,27 @@ fn emit_signal_match_if_waiters( crate::event_log::emit_signal_match(tid, cycle, signal_call, target_handle, n, &tids); } +/// AUDIT-2AU Option β: re-signal the XAudio render loop's frame-event +/// pair, emulating the host XAudio2 OnBufferEnd callback firing once per +/// audio period. Called from the round prologue gated by the same +/// instruction-count audio cadence that drives `tick_instr`, so timing +/// is deterministic (never host_ns). Mirrors `ke_set_event`'s signal + +/// wake sequence for each captured event handle (see +/// `do_wait_multiple` capture site + `XAudioState::frame_events`). +pub fn pulse_xaudio_frame_events(state: &mut KernelState) { + if state.xaudio.frame_events.is_empty() { + return; + } + let events = state.xaudio.frame_events.clone(); + for h in events { + if let Some(KernelObject::Event { signaled, .. }) = state.objects.get_mut(&h) { + *signaled = true; + emit_signal_match_if_waiters(state, "XAudioFramePulse", h); + wake_eligible_waiters(state, h); + } + } +} + fn ke_set_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) { // r3 = PKEVENT on Ke* (guest pointer). See `ensure_dispatcher_object` // for why we need the lazy-shadow step here. @@ -5652,6 +5673,30 @@ fn do_wait_multiple( Some(None) => None, None => None, }; + // AUDIT-2AU Option β: capture the XAudio render loop's frame-event + // pair at the wait site. Sylpheed's render-driver thread (tid=11, + // entry 0x824d2a94 = canary tid=4) blocks here on a WaitAny over two + // guest-address Events (the "buffer ready" manual-reset + "frame + // done" auto-reset pair). In canary these are signaled every audio + // period by the host XAudio2 OnBufferEnd callback; in ours nothing + // signals them after the first fast-path consumes the auto-reset + // member, so the loop wedges forever (2.AL). Record the pair (no + // hardcoded addresses) so the round-prologue audio-cadence ticker + // re-signals them. Discriminator: a *multi*-handle WaitAny whose + // members are all guest-address Events, with at least one XAudio + // client registered — tid=2's lone guest-Event wait goes through + // do_wait_single, so this won't catch it. + if !wait_all + && handles.len() >= 2 + && state.xaudio.any_registered() + && handles.iter().all(|&h| { + h >= 0x8000_0000 && matches!(state.objects.get(&h), Some(KernelObject::Event { .. })) + }) + { + for &h in &handles { + state.xaudio.note_frame_event(h); + } + } let current_ref = state.scheduler.current_ref(); for &h in &handles { handle_enqueue_waiter(state, h, current_ref); diff --git a/crates/xenia-kernel/src/xaudio.rs b/crates/xenia-kernel/src/xaudio.rs index cb09261..c376989 100644 --- a/crates/xenia-kernel/src/xaudio.rs +++ b/crates/xenia-kernel/src/xaudio.rs @@ -110,6 +110,20 @@ pub struct XAudioState { /// `xenia_cpu` (none currently) to keep this self-contained. pub worker_handles: [Option; XAUDIO_MAX_CLIENTS], pub worker_refs: [Option; XAUDIO_MAX_CLIENTS], + /// AUDIT-2AU Option β: guest-address Event handles that the XAudio + /// render-driver loop (Sylpheed tid=11, entry 0x824d2a94 = canary + /// tid=4) blocks on via `KeWaitForMultipleObjects(WaitAny)`. These + /// are the per-frame "buffer ready" / "frame done" events that, in + /// canary, are signaled by the host XAudio2 driver's OnBufferEnd + /// callback every audio period. In ours the render loop's *second* + /// KeWait blocks forever because the auto-reset member was consumed + /// by the first fast-path and the manual-reset member is never + /// signaled (2.AL: signal.match on these SIDs = 0 whole-run). We + /// discover the exact handle pair at the wait site (no hardcoded + /// guest addresses) and re-signal them at the audio cadence from the + /// round prologue so the render loop sustains. Deterministic: signal + /// timing is gated by the instruction-count ticker, never host_ns. + pub frame_events: Vec, } impl Default for XAudioState { @@ -124,6 +138,7 @@ impl Default for XAudioState { last_instant: None, worker_handles: [None; XAUDIO_MAX_CLIENTS], worker_refs: [None; XAUDIO_MAX_CLIENTS], + frame_events: Vec::new(), } } } @@ -160,6 +175,15 @@ impl XAudioState { self.clients.iter().any(|c| c.is_some()) } + /// AUDIT-2AU Option β: remember a guest-address Event handle the XAudio + /// render loop blocks on, so the cadence ticker can re-signal it. Dedup + /// to keep the set tiny (Sylpheed's render loop waits on exactly two). + pub fn note_frame_event(&mut self, handle: u32) { + if !self.frame_events.contains(&handle) { + self.frame_events.push(handle); + } + } + fn enqueue_all_active(&mut self) { for i in 0..XAUDIO_MAX_CLIENTS { if self.clients[i].is_none() {