[iterate-4A] Milestone-2: XMA audio decoder + RE tooling (dispatch recorder, analyzer vtable-fix, non-perturbing probes)
Milestone-2 (intro video dat/movie/ADV.wmv) audio path + major RE tooling. XMA AUDIO (built, working, deterministic, tested): - APU MMIO 0x7FEA0000 + 320x64B register-mapped context array; real XMACreateContext/Release (xma.rs); real FFmpeg xma2 decoder XMA_CONTEXT_DATA->S16BE PCM (xma_decode.rs, xma2_codec.rs, ffmpeg-sys-next). Decode runs synchronously on the CPU thread (deterministic, no host thread). - Audio-worker scheduler fix (main.rs LR_HALT restore + scheduler.rs): the XAudio render-callback worker was wrongly exited after ~2 deliveries; now survives -> guest drives XMA decode (70 kicks). - XAudioSubmitRenderDriverFrame made faithful. Golden sylpheed_n50m re-baselined; tests pass. RE TOOLING: - Runtime indirect-dispatch recorder (dispatch_rec.rs): records (call-site->target, r3, lr); env-gated XENIA_DISPATCH_REC, filters XENIA_DISPATCH_REC_TARGETS/_SITES; deterministic, observe-only. - Repaired static analyzer (vtables.rs): vtable extraction silently fragmented vtables with non-function head slots (missed the XMV engine vtable). Fixed via vptr-write-anchoring -> engine fully typed (vtables 722->1150 on rebuild). - Fixed probe HEISENBUG (main.rs run_superblock): --audit-pc-probe-hex/--mem-watch no longer disable superblock chaining; probes fire inside the chain loop -> scheduling identical armed-vs-unarmed, movie subsystem now observable. Fixed a --quiet bug swallowing armed trace reports. VIDEO still doesn't play (B, guest-side): the XMV engine never issues begin-playback (sub_825076F0, vtable 0x8200a1e8 slot21) -> never primes -> 2000ms timeout. Narrowed to the ARM2 engine-setup wrappers; no honest our-side gate-fix (masking forbidden). See HANDOFF-iterate-4A-milestone2.md for new-machine setup (incl. the FFmpeg apt deps + sylpheed.db regeneration) and continuation pointers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -182,7 +182,7 @@ pub fn register_exports(state: &mut KernelState) {
|
||||
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);
|
||||
state.register_export(Xboxkrnl, 0x0226, "XMAReleaseContext", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0226, "XMAReleaseContext", xma_release_context);
|
||||
|
||||
// Crypto
|
||||
state.register_export(Xboxkrnl, 0x0192, "XeCryptSha", stub_success);
|
||||
@@ -3398,6 +3398,7 @@ fn xaudio_register_render_driver(ctx: &mut PpcContext, mem: &GuestMemory, state:
|
||||
callback_pc,
|
||||
callback_arg,
|
||||
wrapped_callback_arg: wrapped,
|
||||
submitted_frames: 0,
|
||||
};
|
||||
let Some(index) = state.xaudio.register(client) else {
|
||||
tracing::warn!("XAudioRegisterRenderDriverClient: client table full");
|
||||
@@ -3506,18 +3507,75 @@ fn xaudio_unregister_render_driver(ctx: &mut PpcContext, _mem: &GuestMemory, sta
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
/// Mirrors canary `XAudioSubmitRenderDriverFrame_entry` →
|
||||
/// `AudioSystem::SubmitFrame(driver_ptr & 0xFFFF, samples)`:
|
||||
/// the guest render-driver mixer (`sub_824DC350`) calls this once per audio
|
||||
/// frame with `r3 = driver_id` (`0x4155_xxxx`) and `r4 = sample buffer`.
|
||||
/// Canary forwards `samples` to the client's `AudioDriver`; the driver's
|
||||
/// playback-completion callback later releases the client semaphore, which is
|
||||
/// the buffer-consumed pacing our XAudio callback ticker
|
||||
/// (`tick_instr` + `try_inject_audio_callback`) already drives. SubmitFrame
|
||||
/// returns void and the caller discards r3 / reads no field SubmitFrame
|
||||
/// writes, so faithfully we validate the client index and account the frame
|
||||
/// (observational; never read back by the guest). Always returns
|
||||
/// `X_ERROR_SUCCESS`, matching canary. Deterministic: only this guest-driven
|
||||
/// export mutates state; no wall-clock, no host thread.
|
||||
fn xaudio_submit_render_driver_frame(
|
||||
ctx: &mut PpcContext,
|
||||
_mem: &GuestMemory,
|
||||
_state: &mut KernelState,
|
||||
state: &mut KernelState,
|
||||
) {
|
||||
let driver_id = ctx.gpr[3] as u32;
|
||||
let index = (driver_id & XAUDIO_DRIVER_INDEX_MASK) as usize;
|
||||
let registered = state.xaudio.record_submit(index);
|
||||
if !registered {
|
||||
// Canary logs and submits silence to keep the callback chain alive
|
||||
// for an unregistered/invalid index; our ticker keeps the chain
|
||||
// alive independently, so a debug log suffices.
|
||||
tracing::debug!(
|
||||
driver_id = format_args!("{driver_id:#010x}"),
|
||||
index,
|
||||
"XAudioSubmitRenderDriverFrame: unregistered client index"
|
||||
);
|
||||
} else if state.xaudio.submitted_frames(index) == 1 {
|
||||
tracing::info!(
|
||||
driver_id = format_args!("{driver_id:#010x}"),
|
||||
index,
|
||||
"XAudioSubmitRenderDriverFrame: first frame submitted by guest mixer"
|
||||
);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn xma_create_context(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||||
let handle = state.alloc_handle();
|
||||
tracing::info!("XMACreateContext: handle={:#x}", handle);
|
||||
ctx.gpr[3] = handle as u64;
|
||||
/// Mirrors xenia-canary `XMACreateContext_entry(lpdword_t context_out_ptr)`:
|
||||
/// allocate a context from the register-mapped array, write its guest pointer
|
||||
/// to `*context_out_ptr`, and return `X_STATUS_SUCCESS` (or `X_STATUS_NO_MEMORY`
|
||||
/// when the 320-slot array is exhausted).
|
||||
fn xma_create_context(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
let out_ptr = ctx.gpr[3] as u32;
|
||||
let context_ptr = state.xma.lock().unwrap().allocate_context();
|
||||
if out_ptr != 0 {
|
||||
mem.write_u32(out_ptr, context_ptr);
|
||||
}
|
||||
tracing::info!(
|
||||
out_ptr = format_args!("{out_ptr:#010x}"),
|
||||
context_ptr = format_args!("{context_ptr:#010x}"),
|
||||
"XMACreateContext"
|
||||
);
|
||||
ctx.gpr[3] = if context_ptr == 0 {
|
||||
0xC000_0017 // X_STATUS_NO_MEMORY
|
||||
} else {
|
||||
0 // X_STATUS_SUCCESS
|
||||
};
|
||||
}
|
||||
|
||||
/// Mirrors xenia-canary `XMAReleaseContext_entry(lpvoid_t context_ptr)`:
|
||||
/// free the context slot and return 0.
|
||||
fn xma_release_context(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||||
let context_ptr = ctx.gpr[3] as u32;
|
||||
state.xma.lock().unwrap().release_context(context_ptr);
|
||||
tracing::info!(context_ptr = format_args!("{context_ptr:#010x}"), "XMAReleaseContext");
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Xex =====
|
||||
@@ -4413,7 +4471,8 @@ fn nt_yield_execution(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut Ker
|
||||
}
|
||||
|
||||
fn ke_resume_thread(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||||
let handle = resolve_pseudo_handle(state, ctx.gpr[3] as u32);
|
||||
let raw = ctx.gpr[3] as u32;
|
||||
let handle = resolve_pseudo_handle(state, raw);
|
||||
match state.scheduler.find_by_handle(handle) {
|
||||
Some(r) => {
|
||||
state.scheduler.resume_ref(r);
|
||||
@@ -4429,13 +4488,18 @@ fn nt_resume_thread(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelS
|
||||
// r3 = handle, r4 = prev_suspend_count_ptr
|
||||
let handle = ctx.gpr[3] as u32;
|
||||
let prev_ptr = ctx.gpr[4] as u32;
|
||||
let prev = state
|
||||
.scheduler
|
||||
.find_by_handle(handle)
|
||||
.map(|r| state.scheduler.resume_ref(r))
|
||||
.unwrap_or(0);
|
||||
if prev_ptr != 0 {
|
||||
mem.write_u32(prev_ptr, prev);
|
||||
match state.scheduler.find_by_handle(handle) {
|
||||
Some(r) => {
|
||||
let prev = state.scheduler.resume_ref(r);
|
||||
if prev_ptr != 0 {
|
||||
mem.write_u32(prev_ptr, prev);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if prev_ptr != 0 {
|
||||
mem.write_u32(prev_ptr, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.gpr[3] = STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user