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:
@@ -166,6 +166,16 @@ enum Commands {
|
||||
/// `XENIA_PARALLEL=1`.
|
||||
#[arg(long)]
|
||||
parallel: bool,
|
||||
/// **APUBUG-PRODUCER-001 diagnostic.** When set, the round
|
||||
/// prologue ticks the XAudio buffer-complete callback at the
|
||||
/// audio frame rate and injects via the same SavedCallbackCtx
|
||||
/// machinery as graphics interrupts. Default off — firing
|
||||
/// shifts the boot trajectory enough to regress `swaps=2→1`
|
||||
/// under Sylpheed, which would break the lockstep
|
||||
/// `sylpheed_n*m.json` goldens. Settable via
|
||||
/// `XENIA_XAUDIO_TICK=1`.
|
||||
#[arg(long)]
|
||||
xaudio_tick: bool,
|
||||
},
|
||||
/// Browse XISO disc image contents
|
||||
Browse {
|
||||
@@ -255,6 +265,11 @@ enum Commands {
|
||||
/// M3 — spawn HW-thread host workers under coarse Arc<Mutex<KernelState>>.
|
||||
#[arg(long)]
|
||||
parallel: bool,
|
||||
/// APUBUG-PRODUCER-001 diagnostic — enable the XAudio buffer-complete
|
||||
/// callback ticker. Default off; see the `Exec --xaudio-tick` doc for
|
||||
/// rationale.
|
||||
#[arg(long)]
|
||||
xaudio_tick: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -315,6 +330,7 @@ fn main() -> Result<()> {
|
||||
gpu_inline,
|
||||
reservations_table,
|
||||
parallel,
|
||||
xaudio_tick,
|
||||
} => cmd_exec(
|
||||
&path,
|
||||
max_instructions,
|
||||
@@ -332,6 +348,7 @@ fn main() -> Result<()> {
|
||||
gpu_inline,
|
||||
reservations_table,
|
||||
parallel,
|
||||
xaudio_tick,
|
||||
),
|
||||
Commands::Browse { path } => cmd_browse(&path),
|
||||
Commands::Info { path } => cmd_info(&path),
|
||||
@@ -347,6 +364,7 @@ fn main() -> Result<()> {
|
||||
gpu_inline,
|
||||
reservations_table,
|
||||
parallel,
|
||||
xaudio_tick,
|
||||
} => cmd_check(
|
||||
&path,
|
||||
max_instructions,
|
||||
@@ -357,6 +375,7 @@ fn main() -> Result<()> {
|
||||
gpu_inline,
|
||||
reservations_table,
|
||||
parallel,
|
||||
xaudio_tick,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -530,6 +549,7 @@ fn cmd_exec(
|
||||
gpu_inline: bool,
|
||||
reservations_table: bool,
|
||||
parallel: bool,
|
||||
xaudio_tick: bool,
|
||||
) -> Result<()> {
|
||||
cmd_exec_inner(
|
||||
path,
|
||||
@@ -548,6 +568,7 @@ fn cmd_exec(
|
||||
gpu_inline,
|
||||
reservations_table,
|
||||
parallel,
|
||||
xaudio_tick,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
@@ -566,6 +587,7 @@ fn cmd_check(
|
||||
gpu_inline: bool,
|
||||
reservations_table: bool,
|
||||
parallel: bool,
|
||||
xaudio_tick: bool,
|
||||
) -> Result<()> {
|
||||
cmd_exec_inner(
|
||||
path,
|
||||
@@ -584,6 +606,7 @@ fn cmd_check(
|
||||
gpu_inline,
|
||||
reservations_table,
|
||||
parallel,
|
||||
xaudio_tick,
|
||||
out,
|
||||
expect,
|
||||
stable_digest,
|
||||
@@ -607,6 +630,7 @@ fn cmd_exec_inner(
|
||||
gpu_inline: bool,
|
||||
reservations_table: bool,
|
||||
parallel: bool,
|
||||
xaudio_tick: bool,
|
||||
digest_out: Option<&str>,
|
||||
digest_expect: Option<&str>,
|
||||
stable_digest: bool,
|
||||
@@ -787,6 +811,18 @@ 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| {
|
||||
let v = v.trim().to_ascii_lowercase();
|
||||
v == "1" || v == "true" || v == "yes"
|
||||
});
|
||||
kernel.xaudio_tick_enabled = xaudio_tick || xaudio_tick_via_env;
|
||||
if kernel.xaudio_tick_enabled && !quiet {
|
||||
tracing::info!(
|
||||
"XAudio callback ticker enabled (--xaudio-tick / XENIA_XAUDIO_TICK=1)"
|
||||
);
|
||||
}
|
||||
if reservations_table || reservations_via_env || parallel_active {
|
||||
kernel.reservations.enable();
|
||||
if !quiet {
|
||||
@@ -1546,8 +1582,19 @@ fn coord_pre_round(
|
||||
.store(prev | 0x1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if kernel.xaudio_tick_enabled {
|
||||
if kernel.parallel_active {
|
||||
kernel.xaudio.tick_wallclock();
|
||||
} else {
|
||||
kernel.xaudio.tick_instr(stats.instruction_count);
|
||||
}
|
||||
}
|
||||
|
||||
kernel.fire_due_timers();
|
||||
try_inject_graphics_interrupt(kernel);
|
||||
if kernel.xaudio_tick_enabled {
|
||||
try_inject_audio_callback(kernel);
|
||||
}
|
||||
|
||||
RoundCtl::Continue
|
||||
}
|
||||
@@ -2805,6 +2852,94 @@ 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.
|
||||
///
|
||||
/// Mutual exclusion with the graphics path is via the shared
|
||||
/// `interrupts.saved` slot — if a graphics callback is already in flight,
|
||||
/// `is_in_callback()` returns true and we bail until it returns to the
|
||||
/// `LR_HALT_SENTINEL`.
|
||||
fn try_inject_audio_callback(kernel: &mut xenia_kernel::KernelState) {
|
||||
use xenia_cpu::scheduler::HwState;
|
||||
|
||||
if kernel.interrupts.is_in_callback() {
|
||||
return;
|
||||
}
|
||||
let Some(index) = kernel.xaudio.peek_next() else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = kernel.xaudio.get(index) else {
|
||||
let _ = kernel.xaudio.take_next();
|
||||
kernel.xaudio.dropped += 1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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"),
|
||||
}
|
||||
|
||||
let _ = kernel.xaudio.take_next();
|
||||
let t = kernel.scheduler.thread_mut(target_ref);
|
||||
let saved = xenia_kernel::SavedCallbackCtx::capture(
|
||||
&t.ctx,
|
||||
xenia_kernel::INTERRUPT_SOURCE_AUDIO,
|
||||
);
|
||||
kernel.interrupts.injected_ref = Some(target_ref);
|
||||
t.ctx.pc = client.callback_pc;
|
||||
t.ctx.lr = xenia_cpu::context::LR_HALT_SENTINEL;
|
||||
t.ctx.gpr[1] = t
|
||||
.ctx
|
||||
.gpr[1]
|
||||
.wrapping_sub(xenia_kernel::interrupts::CALLBACK_STACK_PAD as u64);
|
||||
t.ctx.gpr[3] = client.wrapped_callback_arg as u64;
|
||||
kernel.interrupts.saved = Some(saved);
|
||||
kernel.xaudio.delivered += 1;
|
||||
metrics::counter!("xaudio.callback.delivered").increment(1);
|
||||
tracing::debug!(
|
||||
index,
|
||||
hw_id = target_ref.hw_id,
|
||||
idx = target_ref.idx,
|
||||
callback = format_args!("{:#010x}", client.callback_pc),
|
||||
wrapped = format_args!("{:#010x}", client.wrapped_callback_arg),
|
||||
"xaudio callback: injecting"
|
||||
);
|
||||
}
|
||||
|
||||
fn print_summary(
|
||||
ctx: &xenia_cpu::PpcContext,
|
||||
debugger: &xenia_debugger::Debugger,
|
||||
|
||||
Reference in New Issue
Block a user