Files
xenia-rs/audit-runs/iterate-2AU-xaudio-cadence/2AU.diff
MechaCat02 ef93a4fa14 handoff: VSync/event-wedge fixes + iterate 2.A–2.BC research notes
Source changes (dormant parity infra, retained from iterate 2.AI/2.AO):
- xenia-kernel/exports.rs: nt_create_event manual_reset polarity +
  related event wiring
- xenia-gpu/mmio_region.rs: D1MODE_VBLANK_VLINE_STATUS hardcode parity

Also lands the audit-runs/ analysis notes (.md/.txt/.json digests) for the
iterate 2.x VSync/0x10e8/0x1004 wedge investigation. Raw trace dumps
(.jsonl/.gz/.csv/.stdout) and agent worktrees (.claude/) are gitignored as
regenerable local artifacts — see memory + HANDOFF for the running findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 07:19:08 +02:00

141 lines
6.4 KiB
Diff

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<u32>; XAUDIO_MAX_CLIENTS],
pub worker_refs: [Option<ThreadRef>; 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<u32>,
}
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() {