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>
This commit is contained in:
MechaCat02
2026-06-05 07:19:08 +02:00
parent acd1656753
commit ef93a4fa14
620 changed files with 108303 additions and 1 deletions

View File

@@ -0,0 +1,110 @@
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();