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:
@@ -3106,9 +3106,89 @@ fn xaudio_register_render_driver(ctx: &mut PpcContext, mem: &GuestMemory, state:
|
||||
if driver_ptr != 0 {
|
||||
mem.write_u32(driver_ptr, driver_id);
|
||||
}
|
||||
|
||||
// AUDIT-032 Plan B: spawn a dedicated audio-worker guest thread for
|
||||
// this client and park it on a synthetic `WaitAny` handle so
|
||||
// `try_inject_audio_callback` can flip it to `ServicingIrq` when
|
||||
// a buffer-complete fire is queued. Mirrors xenia-canary's
|
||||
// `apu/audio_system.cc:84-159` host worker without spawning a host OS
|
||||
// thread. Failure here is non-fatal (the client is still registered;
|
||||
// the periodic ticker will queue fires that the round prologue
|
||||
// simply drops with `dropped += 1` because there's no worker to pump).
|
||||
let worker_stack = 0x10_000u32; // 64 KiB — half of canary's 128 KiB.
|
||||
let worker_ref_handle = if let Some(image) =
|
||||
crate::thread::allocate_thread_image(state, mem, worker_stack, 0)
|
||||
{
|
||||
use std::sync::atomic::Ordering;
|
||||
let tid = state.next_thread_id.fetch_add(1, Ordering::Relaxed);
|
||||
let handle = state.alloc_handle_for(KernelObject::Thread {
|
||||
id: tid,
|
||||
hw_id: None,
|
||||
exit_code: None,
|
||||
waiters: Vec::new(),
|
||||
});
|
||||
let tls_slot_count = state.next_tls_index.load(Ordering::Relaxed);
|
||||
let params = SpawnParams {
|
||||
entry: callback_pc,
|
||||
start_context: wrapped,
|
||||
stack_base: image.stack_base,
|
||||
stack_size: image.stack_size,
|
||||
pcr_base: image.pcr_base,
|
||||
tls_base: image.tls_base,
|
||||
thread_handle: handle,
|
||||
guest_tid: tid,
|
||||
create_suspended: true,
|
||||
is_initial: false,
|
||||
tls_slot_count,
|
||||
affinity_mask: 0,
|
||||
priority: 0,
|
||||
ideal_processor: None,
|
||||
};
|
||||
match state.scheduler.spawn(params, &mut GuestMemoryPcr(mem)) {
|
||||
Ok(hw_id) => {
|
||||
if let Some(KernelObject::Thread { hw_id: slot, .. }) = state.objects.get_mut(&handle) {
|
||||
*slot = Some(hw_id);
|
||||
}
|
||||
// Flip from `Blocked(Suspended)` (set by spawn for
|
||||
// create_suspended=true) to a `Blocked(WaitAny)` on a
|
||||
// synthetic handle never owned by any kernel object.
|
||||
// `wake_eligible_waiters` looks the handle up in
|
||||
// `state.objects` and returns early on miss, so this
|
||||
// park-state is only released by audio-callback injection.
|
||||
let park_handle = crate::xaudio::synthetic_park_handle(index);
|
||||
let target = ThreadRef::new(
|
||||
hw_id,
|
||||
(state.scheduler.slots[hw_id as usize].runqueue.len() - 1) as u16,
|
||||
);
|
||||
// Both Blocked(Suspended) (set by spawn) and
|
||||
// Blocked(WaitAny) are non-runnable, so the
|
||||
// `non_empty_runnable` bitmask is unchanged — no need to
|
||||
// call the private `recompute_slot_runnable` helper.
|
||||
state.scheduler.thread_mut(target).state =
|
||||
xenia_cpu::scheduler::HwState::Blocked(BlockReason::WaitAny {
|
||||
handles: vec![park_handle],
|
||||
deadline: None,
|
||||
});
|
||||
Some((handle, target))
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!("XAudioRegisterRenderDriverClient: spawn failed for worker idx={}", index);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("XAudioRegisterRenderDriverClient: allocate_thread_image failed for worker idx={}", index);
|
||||
None
|
||||
};
|
||||
if let Some((h, r)) = worker_ref_handle {
|
||||
state.xaudio.worker_handles[index] = Some(h);
|
||||
state.xaudio.worker_refs[index] = Some(r);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"XAudioRegisterRenderDriverClient: index={} callback={:#010x} arg={:#010x} wrapped={:#010x} driver={:#010x}",
|
||||
"XAudioRegisterRenderDriverClient: index={} callback={:#010x} arg={:#010x} wrapped={:#010x} driver={:#010x} worker_handle={:?}",
|
||||
index, callback_pc, callback_arg, wrapped, driver_id,
|
||||
state.xaudio.worker_handles[index],
|
||||
);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user