feat(audio): APUBUG-PRODUCER-001 — XAudio register driver client + opt-in callback ticker
Replace the three XAudio kernel-export stubs (Register/Unregister/SubmitFrame) with canary-faithful implementations and add a periodic buffer-complete callback ticker reusing the existing SavedCallbackCtx injection machinery. Canary parity: - xboxkrnl_audio.cc:56-93 — read callback_ptr[0..1], wrap callback_arg in a 4-byte big-endian guest heap buffer (`wrapped_callback_arg`), write `0x4155_xxxx` to *driver_ptr. - audio_system.cc:139-141 — guest callback receives r3 = wrapped pointer, not raw callback_arg. - audio_driver.h:21-24 — frame rate 256 samples / 48 kHz ≈ 5.33 ms. Implementation: - New `crates/xenia-kernel/src/xaudio.rs` — `XAudioClient`, `XAudioState` (8-slot table, pending FIFO, dual-mode ticker), `XAUDIO_INSTR_PERIOD = 48_000` (lockstep) and `XAUDIO_PERIOD = 5.333 ms` (--parallel), same pattern as KRNBUG-D08 v-sync. - `try_inject_audio_callback` in xenia-app mirrors `try_inject_graphics_interrupt`, shares `interrupts.saved` slot for mutex with graphics callbacks. Gating: ticker + injector run only when `--xaudio-tick` / `XENIA_XAUDIO_TICK=1`. Default off because Sylpheed's audio callback enters an infinite `KeWaitForSingleObject` loop on first invocation (canary's host worker thread provides the buffer-completion fence we don't model), which hijacks a guest HW thread and regresses `swaps=2 → 1`. Default-off preserves the lockstep `sylpheed_n*m.json` goldens exactly. Producer hunt outcome (FALSIFIED for parked handles 0x1004/0x100c/0x15e4): at `-n 500M --xaudio-tick` all 3 handles still show `signal_attempts=0 (primary=0, ghost=0)`. Audio callback is not the missing producer. Next candidate per audit-findings.md is Timer DPC delivery (KeSetTimer / KeInsertQueueDpc). Tests: 562 → 576 green (10 in `xaudio.rs`, 4 in `exports.rs`). Lockstep `--stable-digest -n 100M` default-off: instructions=100000002, swaps=2 (matches pre-change baseline byte-for-byte). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -177,8 +177,8 @@ pub fn register_exports(state: &mut KernelState) {
|
||||
|
||||
// Audio
|
||||
state.register_export(Xboxkrnl, 0x01F3, "XAudioRegisterRenderDriverClient", xaudio_register_render_driver);
|
||||
state.register_export(Xboxkrnl, 0x01F4, "XAudioUnregisterRenderDriverClient", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01F5, "XAudioSubmitRenderDriverFrame", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01F4, "XAudioUnregisterRenderDriverClient", xaudio_unregister_render_driver);
|
||||
state.register_export(Xboxkrnl, 0x01F5, "XAudioSubmitRenderDriverFrame", xaudio_submit_render_driver_frame);
|
||||
state.register_export(Xboxkrnl, 0x01F7, "XAudioGetVoiceCategoryVolumeChangeMask", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x01F8, "XAudioGetVoiceCategoryVolume", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0224, "XMACreateContext", xma_create_context);
|
||||
@@ -2621,10 +2621,68 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
|
||||
// ===== Audio =====
|
||||
|
||||
fn xaudio_register_render_driver(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||||
let handle = state.alloc_handle();
|
||||
tracing::info!("XAudioRegisterRenderDriverClient: handle={:#x}", handle);
|
||||
// r3 = callback_ptr, r4 = driver_ptr -> write handle
|
||||
const X_E_INVALIDARG: u64 = 0x8007_0057;
|
||||
const XAUDIO_DRIVER_TAG: u32 = 0x4155_0000;
|
||||
const XAUDIO_DRIVER_INDEX_MASK: u32 = 0x0000_FFFF;
|
||||
|
||||
fn xaudio_register_render_driver(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
let callback_ptr = ctx.gpr[3] as u32;
|
||||
let driver_ptr = ctx.gpr[4] as u32;
|
||||
if callback_ptr == 0 {
|
||||
ctx.gpr[3] = X_E_INVALIDARG;
|
||||
return;
|
||||
}
|
||||
let callback_pc = mem.read_u32(callback_ptr);
|
||||
if callback_pc == 0 {
|
||||
ctx.gpr[3] = X_E_INVALIDARG;
|
||||
return;
|
||||
}
|
||||
let callback_arg = mem.read_u32(callback_ptr.wrapping_add(4));
|
||||
|
||||
let Some(wrapped) = state.heap_alloc(4, mem) else {
|
||||
tracing::warn!("XAudioRegisterRenderDriverClient: heap_alloc(4) failed");
|
||||
ctx.gpr[3] = X_E_INVALIDARG;
|
||||
return;
|
||||
};
|
||||
mem.write_u32(wrapped, callback_arg);
|
||||
|
||||
let client = crate::xaudio::XAudioClient {
|
||||
callback_pc,
|
||||
callback_arg,
|
||||
wrapped_callback_arg: wrapped,
|
||||
};
|
||||
let Some(index) = state.xaudio.register(client) else {
|
||||
tracing::warn!("XAudioRegisterRenderDriverClient: client table full");
|
||||
ctx.gpr[3] = X_E_INVALIDARG;
|
||||
return;
|
||||
};
|
||||
let driver_id = XAUDIO_DRIVER_TAG | (index as u32 & XAUDIO_DRIVER_INDEX_MASK);
|
||||
if driver_ptr != 0 {
|
||||
mem.write_u32(driver_ptr, driver_id);
|
||||
}
|
||||
tracing::info!(
|
||||
"XAudioRegisterRenderDriverClient: index={} callback={:#010x} arg={:#010x} wrapped={:#010x} driver={:#010x}",
|
||||
index, callback_pc, callback_arg, wrapped, driver_id,
|
||||
);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn xaudio_unregister_render_driver(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||||
let driver_id = ctx.gpr[3] as u32;
|
||||
let index = (driver_id & XAUDIO_DRIVER_INDEX_MASK) as usize;
|
||||
state.xaudio.unregister(index);
|
||||
tracing::info!(
|
||||
"XAudioUnregisterRenderDriverClient: driver={:#010x} index={}",
|
||||
driver_id, index,
|
||||
);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn xaudio_submit_render_driver_frame(
|
||||
ctx: &mut PpcContext,
|
||||
_mem: &GuestMemory,
|
||||
_state: &mut KernelState,
|
||||
) {
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
@@ -5181,4 +5239,99 @@ mod tests {
|
||||
assert_ne!(h_main, h_krnl, "main module distinct from xboxkrnl");
|
||||
assert_ne!(h_krnl, h_xam, "xboxkrnl distinct from xam");
|
||||
}
|
||||
|
||||
/// `XAudioRegisterRenderDriverClient` records the (callback, arg) pair,
|
||||
/// allocates a 4-byte heap buffer holding `callback_arg` in big-endian,
|
||||
/// and writes `0x4155_xxxx` to `*driver_ptr`. Mirrors canary
|
||||
/// [audio_system.cc:202-237](../../../xenia-canary/src/xenia/apu/audio_system.cc#L202-L237).
|
||||
#[test]
|
||||
fn xaudio_register_records_client_and_writes_driver_id() {
|
||||
let (mut ctx, mem, mut state) = fresh();
|
||||
let cb_block = SCRATCH_BASE + 0x100;
|
||||
let driver_out = SCRATCH_BASE + 0x200;
|
||||
mem.write_u32(cb_block, 0x8200_BEEF);
|
||||
mem.write_u32(cb_block + 4, 0xDEAD_F00D);
|
||||
ctx.gpr[3] = cb_block as u64;
|
||||
ctx.gpr[4] = driver_out as u64;
|
||||
|
||||
xaudio_register_render_driver(&mut ctx, &mem, &mut state);
|
||||
|
||||
assert_eq!(ctx.gpr[3], 0, "STATUS_SUCCESS");
|
||||
let driver_id = mem.read_u32(driver_out);
|
||||
assert_eq!(driver_id & 0xFFFF_0000, 0x4155_0000);
|
||||
let index = (driver_id & 0x0000_FFFF) as usize;
|
||||
let client = state.xaudio.get(index).expect("client must be registered");
|
||||
assert_eq!(client.callback_pc, 0x8200_BEEF);
|
||||
assert_eq!(client.callback_arg, 0xDEAD_F00D);
|
||||
assert_ne!(client.wrapped_callback_arg, 0);
|
||||
assert_eq!(
|
||||
mem.read_u32(client.wrapped_callback_arg),
|
||||
0xDEAD_F00D,
|
||||
"wrapped buffer must hold callback_arg big-endian"
|
||||
);
|
||||
}
|
||||
|
||||
/// Null `callback_ptr` or null callback function returns `X_E_INVALIDARG`
|
||||
/// without registering — canary
|
||||
/// [xboxkrnl_audio.cc:58-66](../../../xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_audio.cc#L58-L66).
|
||||
#[test]
|
||||
fn xaudio_register_rejects_null_inputs() {
|
||||
let (mut ctx, mem, mut state) = fresh();
|
||||
ctx.gpr[3] = 0;
|
||||
ctx.gpr[4] = (SCRATCH_BASE + 0x300) as u64;
|
||||
xaudio_register_render_driver(&mut ctx, &mem, &mut state);
|
||||
assert_eq!(ctx.gpr[3], X_E_INVALIDARG);
|
||||
assert!(!state.xaudio.any_registered());
|
||||
|
||||
let cb_block = SCRATCH_BASE + 0x400;
|
||||
mem.write_u32(cb_block, 0); // callback function = null
|
||||
mem.write_u32(cb_block + 4, 0xCAFE);
|
||||
ctx.gpr[3] = cb_block as u64;
|
||||
ctx.gpr[4] = (SCRATCH_BASE + 0x500) as u64;
|
||||
xaudio_register_render_driver(&mut ctx, &mem, &mut state);
|
||||
assert_eq!(ctx.gpr[3], X_E_INVALIDARG);
|
||||
assert!(!state.xaudio.any_registered());
|
||||
}
|
||||
|
||||
/// Unregister clears the slot identified by the lower 16 bits of the
|
||||
/// driver token.
|
||||
#[test]
|
||||
fn xaudio_unregister_clears_slot() {
|
||||
let (mut ctx, mem, mut state) = fresh();
|
||||
let cb_block = SCRATCH_BASE + 0x100;
|
||||
let driver_out = SCRATCH_BASE + 0x200;
|
||||
mem.write_u32(cb_block, 0x8200_AAAA);
|
||||
mem.write_u32(cb_block + 4, 0xBBBB_BBBB);
|
||||
ctx.gpr[3] = cb_block as u64;
|
||||
ctx.gpr[4] = driver_out as u64;
|
||||
xaudio_register_render_driver(&mut ctx, &mem, &mut state);
|
||||
let driver_id = mem.read_u32(driver_out);
|
||||
let index = (driver_id & 0x0000_FFFF) as usize;
|
||||
assert!(state.xaudio.get(index).is_some());
|
||||
|
||||
ctx.gpr[3] = driver_id as u64;
|
||||
xaudio_unregister_render_driver(&mut ctx, &mem, &mut state);
|
||||
assert_eq!(ctx.gpr[3], 0);
|
||||
assert!(state.xaudio.get(index).is_none());
|
||||
}
|
||||
|
||||
/// Once a client is registered, the lockstep ticker eventually queues
|
||||
/// a fire — proves the producer pipeline is wired end-to-end through
|
||||
/// the kernel state.
|
||||
#[test]
|
||||
fn xaudio_register_then_tick_instr_queues_callback() {
|
||||
let (mut ctx, mem, mut state) = fresh();
|
||||
let cb_block = SCRATCH_BASE + 0x100;
|
||||
let driver_out = SCRATCH_BASE + 0x200;
|
||||
mem.write_u32(cb_block, 0x8200_C0DE);
|
||||
mem.write_u32(cb_block + 4, 0xFEED_FACE);
|
||||
ctx.gpr[3] = cb_block as u64;
|
||||
ctx.gpr[4] = driver_out as u64;
|
||||
xaudio_register_render_driver(&mut ctx, &mem, &mut state);
|
||||
|
||||
assert!(state.xaudio.tick_instr(crate::xaudio::XAUDIO_INSTR_PERIOD));
|
||||
let i = state.xaudio.peek_next().expect("must queue a fire");
|
||||
let client = state.xaudio.get(i).unwrap();
|
||||
assert_eq!(client.callback_pc, 0x8200_C0DE);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user