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:
@@ -948,16 +948,35 @@ fn cmd_exec_inner(
|
||||
});
|
||||
let parallel_active = parallel || parallel_via_env;
|
||||
kernel.parallel_active = parallel_active;
|
||||
let xaudio_tick_via_env = std::env::var("XENIA_XAUDIO_TICK")
|
||||
.ok()
|
||||
.is_some_and(|v| {
|
||||
// AUDIT-032: default is `KernelState::xaudio_tick_enabled = true` now
|
||||
// that the dedicated worker eliminates HW-thread hijack regressions.
|
||||
// Treat `--xaudio-tick` / `XENIA_XAUDIO_TICK=...` as an explicit
|
||||
// override only when set: `0`/`false`/`no` forces it off; truthy
|
||||
// values keep it on. CLI flag is bool so `--xaudio-tick` (present)
|
||||
// is treated as the truthy override; absence is "use default".
|
||||
let xaudio_tick_env_raw = std::env::var("XENIA_XAUDIO_TICK").ok();
|
||||
let xaudio_tick_env_falsey = xaudio_tick_env_raw
|
||||
.as_deref()
|
||||
.map(|v| {
|
||||
let v = v.trim().to_ascii_lowercase();
|
||||
v == "0" || v == "false" || v == "no"
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let xaudio_tick_env_truthy = xaudio_tick_env_raw
|
||||
.as_deref()
|
||||
.map(|v| {
|
||||
let v = v.trim().to_ascii_lowercase();
|
||||
v == "1" || v == "true" || v == "yes"
|
||||
});
|
||||
kernel.xaudio_tick_enabled = xaudio_tick || xaudio_tick_via_env;
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if xaudio_tick || xaudio_tick_env_truthy {
|
||||
kernel.xaudio_tick_enabled = true;
|
||||
} else if xaudio_tick_env_falsey {
|
||||
kernel.xaudio_tick_enabled = false;
|
||||
}
|
||||
if kernel.xaudio_tick_enabled && !quiet {
|
||||
tracing::info!(
|
||||
"XAudio callback ticker enabled (--xaudio-tick / XENIA_XAUDIO_TICK=1)"
|
||||
"XAudio callback ticker enabled (AUDIT-032 default; toggle via --xaudio-tick / XENIA_XAUDIO_TICK)"
|
||||
);
|
||||
}
|
||||
if reservations_table || reservations_via_env || parallel_active {
|
||||
@@ -3247,12 +3266,15 @@ fn try_inject_graphics_interrupt(kernel: &mut xenia_kernel::KernelState) {
|
||||
);
|
||||
}
|
||||
|
||||
/// APUBUG-PRODUCER-001 — inject a pending XAudio buffer-complete callback
|
||||
/// into the next available HW thread. Mirrors
|
||||
/// [`try_inject_graphics_interrupt`] (same victim-selection policy, same
|
||||
/// SP-pad, same saved-context restore-on-sentinel) but pulls the callback
|
||||
/// PC + r3 argument from a registered [`xenia_kernel::xaudio::XAudioClient`]
|
||||
/// instead of the graphics-callback registration.
|
||||
/// AUDIT-032 Plan B — inject a pending XAudio buffer-complete callback
|
||||
/// into the **dedicated audio worker** registered for the head-of-queue
|
||||
/// client. Mirrors
|
||||
/// [`try_inject_graphics_interrupt`] (same SP-pad, same saved-context
|
||||
/// restore-on-sentinel) but the target thread is fixed at registration
|
||||
/// time instead of selected via the random-victim policy. The pre-fix
|
||||
/// random-victim path corrupted unrelated thread state
|
||||
/// (APUBUG-PRODUCER-001 "HW-thread hijack"); per-client workers eliminate
|
||||
/// that whole class of regression.
|
||||
///
|
||||
/// Mutual exclusion with the graphics path is via the shared
|
||||
/// `interrupts.saved` slot — if a graphics callback is already in flight,
|
||||
@@ -3273,39 +3295,39 @@ fn try_inject_audio_callback(kernel: &mut xenia_kernel::KernelState) {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut victim: Option<xenia_cpu::ThreadRef> = None;
|
||||
'outer_ready: for (hw_id, slot) in kernel.scheduler.slots.iter().enumerate() {
|
||||
for (idx, t) in slot.runqueue.iter().enumerate() {
|
||||
if matches!(t.state, HwState::Ready) {
|
||||
victim = Some(xenia_cpu::ThreadRef::new(hw_id as u8, idx as u16));
|
||||
break 'outer_ready;
|
||||
}
|
||||
// Resolve the dedicated worker thread. Cached `worker_refs[index]` is
|
||||
// stale-tolerant via `find_by_handle` (handle survives slot
|
||||
// compaction). If no worker is registered (spawn failed at register
|
||||
// time, or fix is disabled in a fork), drop the fire — no fallback to
|
||||
// random-victim, that regressed boot trajectory.
|
||||
let worker_handle = kernel.xaudio.worker_handles[index];
|
||||
let target_ref = match worker_handle.and_then(|h| kernel.scheduler.find_by_handle(h)) {
|
||||
Some(r) => {
|
||||
kernel.xaudio.worker_refs[index] = Some(r);
|
||||
r
|
||||
}
|
||||
}
|
||||
if victim.is_none() {
|
||||
'outer_blocked: for (hw_id, slot) in kernel.scheduler.slots.iter().enumerate() {
|
||||
for (idx, t) in slot.runqueue.iter().enumerate() {
|
||||
if matches!(t.state, HwState::Blocked(_)) {
|
||||
victim = Some(xenia_cpu::ThreadRef::new(hw_id as u8, idx as u16));
|
||||
break 'outer_blocked;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let _ = kernel.xaudio.take_next();
|
||||
kernel.xaudio.dropped += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let Some(target_ref) = victim else {
|
||||
let _ = kernel.xaudio.take_next();
|
||||
kernel.xaudio.dropped += 1;
|
||||
return;
|
||||
};
|
||||
|
||||
let t = kernel.scheduler.thread_mut(target_ref);
|
||||
let prev_state = t.state.clone();
|
||||
match prev_state {
|
||||
HwState::Ready => {}
|
||||
HwState::Blocked(reason) => {
|
||||
t.state = HwState::ServicingIrq(reason);
|
||||
}
|
||||
_ => unreachable!("victim selection above filtered out other variants"),
|
||||
// Worker normally re-blocks on synthetic via the LR_HALT restore
|
||||
// path. If we ever observe it elsewhere (e.g. deadlock force-wake
|
||||
// moved it to Ready), drop this fire rather than corrupt state —
|
||||
// the next tick will retry once the restore path runs.
|
||||
_ => {
|
||||
let _ = kernel.xaudio.take_next();
|
||||
kernel.xaudio.dropped += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = kernel.xaudio.take_next();
|
||||
@@ -3331,7 +3353,7 @@ fn try_inject_audio_callback(kernel: &mut xenia_kernel::KernelState) {
|
||||
idx = target_ref.idx,
|
||||
callback = format_args!("{:#010x}", client.callback_pc),
|
||||
wrapped = format_args!("{:#010x}", client.wrapped_callback_arg),
|
||||
"xaudio callback: injecting"
|
||||
"xaudio callback: injecting (dedicated worker)"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user