[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:
@@ -35,6 +35,14 @@ pub const XAUDIO_MAX_CLIENTS: usize = 8;
|
||||
/// no-op anyway).
|
||||
pub const XAUDIO_SYNTHETIC_HANDLE_BASE: u32 = 0xF000_0000;
|
||||
|
||||
/// The scheduler's deadlock force-wake skips waiters parked solely on
|
||||
/// handles at/above [`xenia_cpu::scheduler::SYNTHETIC_PARK_HANDLE_FLOOR`]
|
||||
/// so it never destroys a parked audio worker. Keep these in lockstep:
|
||||
/// every `synthetic_park_handle` must fall inside that protected range.
|
||||
const _: () = assert!(
|
||||
XAUDIO_SYNTHETIC_HANDLE_BASE >= xenia_cpu::scheduler::SYNTHETIC_PARK_HANDLE_FLOOR
|
||||
);
|
||||
|
||||
/// Compute the synthetic park-handle for client slot `i`.
|
||||
pub const fn synthetic_park_handle(i: usize) -> u32 {
|
||||
XAUDIO_SYNTHETIC_HANDLE_BASE | (i as u32)
|
||||
@@ -68,6 +76,16 @@ pub struct XAudioClient {
|
||||
/// [audio_system.cc:225-228](../../../../xenia-canary/src/xenia/apu/audio_system.cc#L225-L228)
|
||||
/// + [audio_system.cc:139-141](../../../../xenia-canary/src/xenia/apu/audio_system.cc#L139-L141).
|
||||
pub wrapped_callback_arg: u32,
|
||||
/// Count of frames the guest has handed us via
|
||||
/// `XAudioSubmitRenderDriverFrame` for this client. Canary's
|
||||
/// `AudioSystem::SubmitFrame` forwards the sample buffer to the client's
|
||||
/// driver, whose playback completion later releases the client semaphore
|
||||
/// — the pacing our callback ticker emulates. The guest mixer
|
||||
/// (`sub_824DC350`) discards SubmitFrame's return and reads no field it
|
||||
/// writes, so this counter is purely observational (logging / liveness),
|
||||
/// never read back by the guest. Deterministic: incremented only inside
|
||||
/// the guest-driven export call.
|
||||
pub submitted_frames: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -138,6 +156,35 @@ impl XAudioState {
|
||||
self.clients.get(index).copied().flatten()
|
||||
}
|
||||
|
||||
/// Faithful counterpart to canary `AudioSystem::SubmitFrame`: the guest
|
||||
/// driver client `index` handed us one frame of samples. Canary forwards
|
||||
/// `samples` to the client's `AudioDriver`, whose playback-completion
|
||||
/// callback later releases the client semaphore — the buffer-consumed
|
||||
/// pacing our [`tick_instr`]/[`try_inject_audio_callback`] path already
|
||||
/// emulates. SubmitFrame itself returns void and the guest mixer
|
||||
/// (`sub_824DC350`) reads no field from it, so all we faithfully need to
|
||||
/// do is validate the client and account the frame. Returns `true` iff
|
||||
/// `index` is a registered client (canary submits silence / warns
|
||||
/// otherwise). Deterministic — only the guest-driven export mutates this.
|
||||
pub fn record_submit(&mut self, index: usize) -> bool {
|
||||
match self.clients.get_mut(index) {
|
||||
Some(Some(c)) => {
|
||||
c.submitted_frames = c.submitted_frames.saturating_add(1);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submitted_frames(&self, index: usize) -> u64 {
|
||||
self.clients
|
||||
.get(index)
|
||||
.copied()
|
||||
.flatten()
|
||||
.map(|c| c.submitted_frames)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn any_registered(&self) -> bool {
|
||||
self.clients.iter().any(|c| c.is_some())
|
||||
}
|
||||
@@ -230,6 +277,7 @@ mod tests {
|
||||
callback_pc: 0x8200_0000 + arg,
|
||||
callback_arg: arg,
|
||||
wrapped_callback_arg: 0x4000_0000 + arg,
|
||||
submitted_frames: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user