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>
111 lines
4.3 KiB
Diff
111 lines
4.3 KiB
Diff
diff --git a/crates/xenia-kernel/src/xaudio.rs b/crates/xenia-kernel/src/xaudio.rs
|
|
index c20fe94..cb09261 100644
|
|
--- a/crates/xenia-kernel/src/xaudio.rs
|
|
+++ b/crates/xenia-kernel/src/xaudio.rs
|
|
@@ -58,6 +58,24 @@ pub const XAUDIO_PERIOD: Duration = Duration::from_nanos(5_333_333);
|
|
/// queueing unbounded callbacks while injection is starved.
|
|
pub const XAUDIO_QUEUE_CAP: usize = 16;
|
|
|
|
+/// Phase HostAudioEager (2026-05-19): initial seeded fire count at
|
|
+/// `XAudioRegisterRenderDriverClient` time. Mirrors xenia-canary
|
|
+/// [`audio_system.cc:210`](../../../../xenia-canary/src/xenia/apu/audio_system.cc#L210)
|
|
+/// `client_semaphore->Release(queued_frames_=8, nullptr)` — the moment
|
|
+/// canary's `RegisterClient` returns, its already-running host worker
|
|
+/// thread has 8 buffer-complete fires queued to drain.
|
|
+///
|
|
+/// In ours, the dedicated guest audio worker (spawned at the same
|
|
+/// register call) can't be HOST-threaded; instead we seed the pending
|
|
+/// FIFO so the round prologue's `try_inject_audio_callback` injects
|
|
+/// the first callback on the very next round — well before tid=1
|
|
+/// reaches `ExCreateThread` for the XAudio worker threads (tid=14/15
|
|
+/// in canary, tid=9/10 in ours). This fixes the ordering issue where
|
|
+/// the 48k-instruction ticker delay let tid=9/10 spawn and enter
|
|
+/// their spin loop on the uninitialized voice struct before the
|
|
+/// callback could modify it.
|
|
+pub const XAUDIO_REGISTER_SEED_FIRES: usize = 8;
|
|
+
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct XAudioClient {
|
|
pub callback_pc: u32,
|
|
@@ -155,6 +173,28 @@ impl XAudioState {
|
|
}
|
|
}
|
|
|
|
+ /// Phase HostAudioEager: enqueue `n` buffer-complete fires for a
|
|
+ /// specific client slot. Used by `XAudioRegisterRenderDriverClient`
|
|
+ /// to mirror canary's `client_semaphore->Release(queued_frames_)`
|
|
+ /// at register time. Capped by [`XAUDIO_QUEUE_CAP`] to avoid
|
|
+ /// unbounded growth if the caller seeds aggressively. Returns the
|
|
+ /// actual number of fires enqueued.
|
|
+ pub fn seed_fires_for(&mut self, index: usize, n: usize) -> usize {
|
|
+ if index >= XAUDIO_MAX_CLIENTS || self.clients[index].is_none() {
|
|
+ return 0;
|
|
+ }
|
|
+ let mut queued = 0;
|
|
+ for _ in 0..n {
|
|
+ if self.pending.len() >= XAUDIO_QUEUE_CAP {
|
|
+ self.dropped += 1;
|
|
+ break;
|
|
+ }
|
|
+ self.pending.push_back(index);
|
|
+ queued += 1;
|
|
+ }
|
|
+ queued
|
|
+ }
|
|
+
|
|
pub fn peek_next(&self) -> Option<usize> {
|
|
self.pending.front().copied()
|
|
}
|
|
@@ -320,6 +360,51 @@ mod tests {
|
|
assert!(s.last_instant.is_some());
|
|
}
|
|
|
|
+ #[test]
|
|
+ fn seed_fires_for_registered_slot_enqueues_n() {
|
|
+ let mut s = XAudioState::default();
|
|
+ let i = s.register(dummy_client(1)).unwrap();
|
|
+ let queued = s.seed_fires_for(i, XAUDIO_REGISTER_SEED_FIRES);
|
|
+ assert_eq!(queued, XAUDIO_REGISTER_SEED_FIRES);
|
|
+ assert_eq!(s.pending.len(), XAUDIO_REGISTER_SEED_FIRES);
|
|
+ // All enqueued fires reference our slot.
|
|
+ for _ in 0..XAUDIO_REGISTER_SEED_FIRES {
|
|
+ assert_eq!(s.take_next(), Some(i));
|
|
+ }
|
|
+ assert!(s.pending.is_empty());
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn seed_fires_for_unregistered_slot_is_noop() {
|
|
+ let mut s = XAudioState::default();
|
|
+ // Slot 3 is empty.
|
|
+ let queued = s.seed_fires_for(3, 8);
|
|
+ assert_eq!(queued, 0);
|
|
+ assert!(s.pending.is_empty());
|
|
+ assert_eq!(s.dropped, 0);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn seed_fires_for_caps_at_queue_cap_and_counts_drops() {
|
|
+ let mut s = XAudioState::default();
|
|
+ let i = s.register(dummy_client(1)).unwrap();
|
|
+ let queued = s.seed_fires_for(i, XAUDIO_QUEUE_CAP * 4);
|
|
+ assert_eq!(queued, XAUDIO_QUEUE_CAP);
|
|
+ assert_eq!(s.pending.len(), XAUDIO_QUEUE_CAP);
|
|
+ // Excess fires are counted as dropped (per
|
|
+ // existing `enqueue_all_active` discipline).
|
|
+ assert!(s.dropped >= 1);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn seed_fires_for_out_of_range_index_is_noop() {
|
|
+ let mut s = XAudioState::default();
|
|
+ s.register(dummy_client(1)).unwrap();
|
|
+ let queued = s.seed_fires_for(XAUDIO_MAX_CLIENTS + 5, 4);
|
|
+ assert_eq!(queued, 0);
|
|
+ assert!(s.pending.is_empty());
|
|
+ }
|
|
+
|
|
#[test]
|
|
fn tick_wallclock_fires_after_period() {
|
|
let mut s = XAudioState::default();
|