Files
xenia-rs/crates/xenia-apu/src/xma2_codec.rs
MechaCat02 23189b95af [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>
2026-06-21 21:38:19 +02:00

218 lines
8.3 KiB
Rust

//! Thin unsafe wrapper around the mainline FFmpeg `AV_CODEC_ID_XMA2` decoder.
//!
//! Unlike canary's vendored `XMAFRAMES` (one frame per packet, custom padding
//! header), the distro xma2 decoder consumes whole 2 KB XMA2 packets
//! (`block_align == 2048`), needs `extradata` declaring the channel/stream
//! layout, and buffers samples internally across packets. We drive it with the
//! guest's raw 2 KB packets and pull whatever 512-sample float-planar frames it
//! emits, returning them as interleaved S16 big-endian PCM (canary `ConvertFrame`).
use std::os::raw::c_int;
use std::ptr;
use ffmpeg_sys_next as ff;
/// One xma2 decoder instance, configured for a fixed (sample_rate, channels).
pub struct Xma2Codec {
codec: *const ff::AVCodec,
ctx: *mut ff::AVCodecContext,
frame: *mut ff::AVFrame,
packet: *mut ff::AVPacket,
extradata: Vec<u8>,
channels: u32,
}
// FFmpeg objects are not Send/Sync by default; the decoder is only ever touched
// on the CPU scheduler thread (decode_pending), so this is sound for our use.
unsafe impl Send for Xma2Codec {}
impl Xma2Codec {
/// Build XMA2WAVEFORMATEX extradata (34 bytes) for a single XMA2 stream.
/// Layout (little-endian, per FFmpeg `xma_decode_init` / xma2defs.h):
/// [0..2] NumStreams (u16) = 1
/// [2..6] ChannelMask (u32) = mono/stereo mask
/// [6..34] remaining XMA2WAVEFORMATEX fields (unused by the decoder)
fn build_extradata(channels: u32) -> Vec<u8> {
let mut e = vec![0u8; 34];
// NumStreams = 1
e[0..2].copy_from_slice(&1u16.to_le_bytes());
// ChannelMask: 0x3 (FL|FR) for stereo, 0x4 (FC) for mono.
let mask: u32 = if channels >= 2 { 0x3 } else { 0x4 };
e[2..6].copy_from_slice(&mask.to_le_bytes());
e
}
pub fn new(sample_rate: u32, channels: u32) -> Result<Self, String> {
unsafe {
let codec = ff::avcodec_find_decoder(ff::AVCodecID::AV_CODEC_ID_XMA2);
if codec.is_null() {
return Err("xma2 decoder not found in libavcodec".into());
}
let ctx = ff::avcodec_alloc_context3(codec);
if ctx.is_null() {
return Err("avcodec_alloc_context3 failed".into());
}
let mut extradata = Self::build_extradata(channels);
// FFmpeg requires extradata to be allocated with av_malloc and
// padded; copy our bytes into an av_malloc'd buffer.
let pad = ff::AV_INPUT_BUFFER_PADDING_SIZE as usize;
let raw = ff::av_mallocz(extradata.len() + pad) as *mut u8;
if raw.is_null() {
ff::avcodec_free_context(&mut (ctx as *mut _));
return Err("av_mallocz extradata failed".into());
}
ptr::copy_nonoverlapping(extradata.as_ptr(), raw, extradata.len());
(*ctx).extradata = raw;
(*ctx).extradata_size = extradata.len() as c_int;
(*ctx).sample_rate = sample_rate as c_int;
(*ctx).block_align = 2048;
ff::av_channel_layout_default(&mut (*ctx).ch_layout, channels as c_int);
let ret = ff::avcodec_open2(ctx, codec, ptr::null_mut());
if ret < 0 {
let mut ctxm = ctx;
ff::avcodec_free_context(&mut ctxm);
return Err(format!("avcodec_open2 failed: {}", av_err(ret)));
}
let frame = ff::av_frame_alloc();
let packet = ff::av_packet_alloc();
if frame.is_null() || packet.is_null() {
let mut ctxm = ctx;
ff::avcodec_free_context(&mut ctxm);
return Err("av_frame_alloc/av_packet_alloc failed".into());
}
// keep our Vec alive as the source of truth for length
extradata.shrink_to_fit();
Ok(Self {
codec,
ctx,
frame,
packet,
extradata,
channels,
})
}
}
pub fn channels(&self) -> u32 {
self.channels
}
/// Feed one raw 2 KB XMA2 packet (header + data) to the decoder. Returns the
/// number of bytes the decoder accepted (0 = buffered, needs no new packet
/// yet / EAGAIN). Decoded frames are pulled via [`receive_frame`].
pub fn send_packet(&mut self, packet: &[u8]) -> Result<(), String> {
unsafe {
// av_packet_from_data takes ownership of an av_malloc buffer; simpler
// to point at our own bytes via a stack packet with a padded copy.
let pad = ff::AV_INPUT_BUFFER_PADDING_SIZE as usize;
let buf = ff::av_malloc(packet.len() + pad) as *mut u8;
if buf.is_null() {
return Err("av_malloc packet failed".into());
}
ptr::copy_nonoverlapping(packet.as_ptr(), buf, packet.len());
ptr::write_bytes(buf.add(packet.len()), 0, pad);
ff::av_packet_unref(self.packet);
// Wrap buf so FFmpeg frees it.
let ret = ff::av_packet_from_data(self.packet, buf, packet.len() as c_int);
if ret < 0 {
ff::av_free(buf as *mut _);
return Err(format!("av_packet_from_data failed: {}", av_err(ret)));
}
let ret = ff::avcodec_send_packet(self.ctx, self.packet);
if ret == ff::AVERROR(ff::EAGAIN) {
// Decoder full — caller should drain frames first then retry.
return Err("EAGAIN".into());
}
if ret < 0 {
return Err(format!("avcodec_send_packet failed: {}", av_err(ret)));
}
Ok(())
}
}
/// Signal end-of-stream so the decoder flushes its internal FIFO.
pub fn send_eof(&mut self) {
unsafe {
let _ = ff::avcodec_send_packet(self.ctx, ptr::null());
}
}
/// Pull one decoded frame as interleaved S16 big-endian PCM, or None if the
/// decoder needs more input (EAGAIN) or is drained (EOF). Returns
/// (samples_per_channel, interleaved_s16be_bytes).
pub fn receive_frame(&mut self) -> Option<(u32, Vec<u8>)> {
unsafe {
let ret = ff::avcodec_receive_frame(self.ctx, self.frame);
if ret < 0 {
return None;
}
let nb = (*self.frame).nb_samples as u32;
if nb == 0 {
return None;
}
let ch = (*self.frame).ch_layout.nb_channels.max(1) as u32;
let out = convert_frame_planar_to_s16be(self.frame, ch, nb);
Some((nb, out))
}
}
}
impl Drop for Xma2Codec {
fn drop(&mut self) {
unsafe {
if !self.frame.is_null() {
ff::av_frame_free(&mut self.frame);
}
if !self.packet.is_null() {
ff::av_packet_free(&mut self.packet);
}
if !self.ctx.is_null() {
ff::avcodec_free_context(&mut self.ctx);
}
let _ = &self.codec;
let _ = &self.extradata;
}
}
}
/// Convert FFmpeg planar-float output to interleaved S16 big-endian PCM
/// (faithful to canary `XmaContext::ConvertFrame`: saturate to [-1,1], scale by
/// 2^15-1, byte-swap each sample). `channels` planes of `nb_samples` floats.
unsafe fn convert_frame_planar_to_s16be(
frame: *mut ff::AVFrame,
channels: u32,
nb_samples: u32,
) -> Vec<u8> {
const SCALE: f32 = ((1i32 << 15) - 1) as f32;
let mut out = Vec::with_capacity((nb_samples * channels * 2) as usize);
unsafe {
// extended_data[ch] points to a plane of f32 (AV_SAMPLE_FMT_FLTP).
let ext = (*frame).extended_data;
for i in 0..nb_samples as isize {
for ch in 0..channels as isize {
let plane = *ext.offset(ch) as *const f32;
let s = if plane.is_null() { 0.0 } else { *plane.offset(i) };
let clamped = s.clamp(-1.0, 1.0) * SCALE;
let v = clamped as i16;
out.extend_from_slice(&v.to_be_bytes());
}
}
}
out
}
fn av_err(code: c_int) -> String {
unsafe {
let mut buf = [0i8; ff::AV_ERROR_MAX_STRING_SIZE as usize];
ff::av_strerror(code, buf.as_mut_ptr(), buf.len());
let cstr = std::ffi::CStr::from_ptr(buf.as_ptr());
cstr.to_string_lossy().into_owned()
}
}