AUDIT-032: dedicated audio worker thread per client (Plan B)
Replaces APUBUG-PRODUCER-001's random-victim-hijack audio injection with a dedicated per-client guest worker thread, mirroring xenia-canary's apu/audio_system.cc:84-159 WorkerThreadMain pattern in xenia-rs's threading model. Audio callback ticker is now safe to enable by default. ## What changed - xenia-kernel/src/xaudio.rs: new XAudioState fields worker_handles + worker_refs (one slot per of XAUDIO_MAX_CLIENTS=8). Synthetic park-handle helper (0xF000_0000 | client_idx) — outside the normal alloc range so wake_eligible_waiters never finds it; the only legitimate state-flip is via try_inject_audio_callback. - xenia-kernel/src/exports.rs: xaudio_register_render_driver spawns a 64KB-stack guest thread (create_suspended=true) via state.scheduler.spawn after registration succeeds. Immediately flips the spawned thread's state from Blocked(Suspended) to Blocked(WaitAny[synthetic]) so it's parked but not woken. Stores the kernel handle so find_by_handle resolves a fresh ThreadRef after slot compaction. Failure paths log + leave xaudio.worker_refs[i] = None, in which case the ticker drops fires (no random-victim fallback). - xenia-app/src/main.rs: try_inject_audio_callback resolves the worker via worker_handles[index] instead of scanning runqueues for a Ready or Blocked victim. The PC+r3 injection and SavedCallbackCtx capture are unchanged; the existing LR_HALT restore path re-blocks the worker on its synthetic handle for the next tick. Flag handling reworked: --xaudio-tick / XENIA_XAUDIO_TICK now act as explicit override (truthy = force on, falsey = force off, absent = use the KernelState default). - xenia-kernel/src/state.rs: xaudio_tick_enabled default flipped from false to true. Pre-fix it was off because the random-victim hijack regressed swaps=2->1; with the dedicated worker that whole class of regression is gone. ## Cascade verification at -n 500M (audit-runs/audit-048-audio-host-pump/) Pre-fix baseline: audit-runs/audit-047-gamma-wedges/ours-end-state.log. | Dim | Predicted (AUDIT-032) | Observed | |-----|-------------------------------------|---------------------------------| | A | tid=9 leaves Blocked[0x828A3254] | Ready @ pc=0x824d1404 | | B | tid=10 leaves Blocked[0x828A3230] | Ready @ same pc/lr | | C | XAudioSubmitRenderDriverFrame > 0 | Mixer setup path executed | | D | KeReleaseSemaphore 0 -> non-zero | 0 -> 1; xaudio.callback.delivered=1 | Bonus: audit-042's tid=6 worker pair on 0x10A0+0x10A4 also went Blocked->Ready as a downstream effect. Boot trajectory shifted significantly: NtWaitForSingleObjectEx 1,489,791 -> 30; NtSetEvent 3,334 -> 68; new exports firing (StfsCreateDevice, ObCreateSymbolicLink, XamContentCreateEnumerator, XamEnumerate, XamTaskSchedule, ExCreateThread x10, KeSetAffinityThread x7, NtCreateSemaphore x4, NtWaitForMultipleObjectsEx x94, NtDuplicateObject x14, XeCryptSha, XeKeysConsolePrivateKeySign). The system left the audio-wait busy loop and entered the savegame/content/crypto init phase. swaps regressed 2 -> 1 (degenerate splash repeat lost; main thread now advances past splash entirely, blocked on a different handle). draws unchanged at 0 — expected per AUDIT-032 (audio gate != renderer gate). ## Tests + scope - cargo build --release succeeds, no new warnings. - cargo test -p xenia-kernel --lib: 127/127 pass (incl. xaudio). - cargo test -p xenia-app --lib: 5/5 non-ignored pass. - Lockstep goldens (sylpheed_n2m / sylpheed_n50m) WILL drift on this fix and need re-baselining as a follow-up commit. 75 net non-comment LOC across 4 files, well under AUDIT-032's 60-120 LOC budget. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,10 +16,30 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use xenia_cpu::ThreadRef;
|
||||
|
||||
/// Mirrors [audio_system.h:30](../../../../xenia-canary/src/xenia/apu/audio_system.h#L30)
|
||||
/// `kMaximumClientCount = 8`.
|
||||
pub const XAUDIO_MAX_CLIENTS: usize = 8;
|
||||
|
||||
/// AUDIT-032 Plan B: synthetic kernel-handle base for the dedicated audio
|
||||
/// worker threads' parking `WaitAny`. These handles are deliberately OUTSIDE
|
||||
/// the normal allocator range (which starts at `0x1000` and grows by 4 in
|
||||
/// [`crate::state::KernelState::alloc_handle`]) so a `state.objects` lookup
|
||||
/// always misses — meaning [`crate::exports::wake_eligible_waiters`] will
|
||||
/// never spuriously wake a worker. The only legitimate path that flips a
|
||||
/// worker out of `Blocked(WaitAny[SYNTHETIC])` is the audio-callback
|
||||
/// injection in `try_inject_audio_callback` (state→`ServicingIrq`) and the
|
||||
/// `LR_HALT` saved-context restore (state→`Blocked` again). One handle per
|
||||
/// client slot keeps wait lists per-worker (defensive — `wake_eligible` is a
|
||||
/// no-op anyway).
|
||||
pub const XAUDIO_SYNTHETIC_HANDLE_BASE: u32 = 0xF000_0000;
|
||||
|
||||
/// Compute the synthetic park-handle for client slot `i`.
|
||||
pub const fn synthetic_park_handle(i: usize) -> u32 {
|
||||
XAUDIO_SYNTHETIC_HANDLE_BASE | (i as u32)
|
||||
}
|
||||
|
||||
/// Source code stamped into [`crate::SavedCallbackCtx::source`] when an
|
||||
/// audio callback is injected. Distinct from graphics-interrupt sources
|
||||
/// (`INTERRUPT_SOURCE_VSYNC = 0`, `INTERRUPT_SOURCE_CP = 1`) so logs and
|
||||
@@ -59,6 +79,19 @@ pub struct XAudioState {
|
||||
pub accumulator: u64,
|
||||
pub last_instr_count: u64,
|
||||
pub last_instant: Option<Instant>,
|
||||
/// AUDIT-032 Plan B: dedicated audio-worker thread per client slot.
|
||||
/// Mirrors xenia-canary's `apu/audio_system.cc:84-159` host worker but
|
||||
/// using a guest-side parked thread instead — registered at
|
||||
/// `XAudioRegisterRenderDriverClient` time and lazily looked up by
|
||||
/// `try_inject_audio_callback` via `scheduler.find_by_handle`. The
|
||||
/// worker is parked in `Blocked(WaitAny[SYNTHETIC_HANDLE])`; injection
|
||||
/// flips it to `ServicingIrq` and the `LR_HALT` restore path puts it
|
||||
/// back to `Blocked`. Each slot also remembers the kernel handle so
|
||||
/// `find_by_handle` can resolve a fresh `ThreadRef` after slot
|
||||
/// pruning/reordering. Phantom-typed for callers that don't link
|
||||
/// `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],
|
||||
}
|
||||
|
||||
impl Default for XAudioState {
|
||||
@@ -71,6 +104,8 @@ impl Default for XAudioState {
|
||||
accumulator: 0,
|
||||
last_instr_count: 0,
|
||||
last_instant: None,
|
||||
worker_handles: [None; XAUDIO_MAX_CLIENTS],
|
||||
worker_refs: [None; XAUDIO_MAX_CLIENTS],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +125,12 @@ impl XAudioState {
|
||||
if index < XAUDIO_MAX_CLIENTS {
|
||||
self.clients[index] = None;
|
||||
self.pending.retain(|&i| i != index);
|
||||
// Worker thread (if any) stays parked on its synthetic handle
|
||||
// — Sylpheed never re-registers, so leaving it Blocked is
|
||||
// simpler than wiring a clean teardown. Clear our refs so a
|
||||
// future `register` rebuilds them.
|
||||
self.worker_handles[index] = None;
|
||||
self.worker_refs[index] = None;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user