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>
691 lines
26 KiB
Rust
691 lines
26 KiB
Rust
//! Stage 3 — the real XMA2→PCM decoder.
|
|
//!
|
|
//! A faithful port of xenia-canary's `apu/xma_context_new.cc` decode pipeline
|
|
//! (`Work`/`Decode`/`Consume`/`StoreContextMerged`), adapted to the *mainline*
|
|
//! distro FFmpeg `AV_CODEC_ID_XMA2` decoder rather than canary's vendored
|
|
//! `AV_CODEC_ID_XMAFRAMES`.
|
|
//!
|
|
//! ## Determinism
|
|
//! There is no host decoder thread. [`super::xma::XmaDecoder::decode_pending`]
|
|
//! is invoked from the CPU scheduler's per-round coordinator
|
|
//! (`coord_post_round` in xenia-app). FFmpeg decode is itself deterministic
|
|
//! (same input bytes → same PCM), so the lockstep golden stays reproducible.
|
|
//!
|
|
//! ## FFmpeg framing — why this differs from canary
|
|
//! Canary feeds FFmpeg one *frame* at a time (it bit-extracts a single 512-
|
|
//! sample frame from the guest packet stream and hands it to the vendored
|
|
//! `XMAFRAMES` codec with a custom 1-byte padding header). The mainline
|
|
//! `xma2` decoder does NOT have `XMAFRAMES`; instead it consumes whole 2 KB
|
|
//! XMA2 *packets* (`block_align == 2048`), needs `extradata` declaring the
|
|
//! stream/channel layout, and manages frame splitting + a per-stream sample
|
|
//! FIFO internally. So this module keeps canary's *guest-facing* contract
|
|
//! (the `XMA_CONTEXT_DATA` packet/frame bookkeeping, the 256-byte-block output
|
|
//! ring buffer, the field writeback) but replaces canary's per-frame
|
|
//! `Decode()` body with: feed the current 2 KB packet to the xma2 decoder,
|
|
//! pull any 512-sample PCM frames it emits, convert them to interleaved S16BE,
|
|
//! and stage them as the "raw frame" that `Consume()` drains into the output
|
|
//! ring.
|
|
//!
|
|
//! See `xma2_codec.rs` for the unsafe FFmpeg wrapper.
|
|
|
|
use std::collections::VecDeque;
|
|
|
|
use xenia_memory::access::MemoryAccess;
|
|
use xenia_memory::GuestMemory;
|
|
|
|
use crate::xma2_codec::Xma2Codec;
|
|
|
|
// ---- Constants (canary `XmaContext` / `XmaContextNew`).
|
|
|
|
pub const BYTES_PER_PACKET: u32 = 2048;
|
|
pub const BYTES_PER_PACKET_HEADER: u32 = 4;
|
|
pub const BYTES_PER_PACKET_DATA: u32 = BYTES_PER_PACKET - BYTES_PER_PACKET_HEADER;
|
|
pub const BITS_PER_PACKET: u32 = BYTES_PER_PACKET * 8;
|
|
/// Canary `kBitsPerPacketHeader` (in the *new* context) is 32.
|
|
pub const BITS_PER_PACKET_HEADER: u32 = 32;
|
|
pub const BITS_PER_FRAME_HEADER: u32 = 15;
|
|
|
|
pub const SAMPLES_PER_FRAME: u32 = 512;
|
|
pub const BYTES_PER_SAMPLE: u32 = 2;
|
|
pub const BYTES_PER_FRAME_CHANNEL: u32 = SAMPLES_PER_FRAME * BYTES_PER_SAMPLE; // 1024
|
|
pub const OUTPUT_BYTES_PER_BLOCK: u32 = 256;
|
|
pub const OUTPUT_MAX_SIZE_BYTES: u32 = 31 * OUTPUT_BYTES_PER_BLOCK;
|
|
|
|
pub const MAX_FRAME_LENGTH: u32 = 0x7FFF;
|
|
pub const MAX_FRAME_SIZE_IN_BITS: u32 = 0x4000 - BITS_PER_PACKET_HEADER;
|
|
|
|
const ID_TO_SAMPLE_RATE: [u32; 4] = [24000, 32000, 44100, 48000];
|
|
|
|
/// Project a bare-physical XMA buffer pointer (`0x0xxxxxxx`) to the host-backed
|
|
/// guest VA used by the rest of the emulator. Identical formula to
|
|
/// `xenia_gpu::physical_to_backing` for the physical window; the input/output
|
|
/// buffer pointers in the context are always in the low physical window.
|
|
#[inline]
|
|
pub fn xma_phys_to_backing(p: u32) -> u32 {
|
|
0x4000_0000 | (p & 0x1FFF_FFFF)
|
|
}
|
|
|
|
// ---- XMA_CONTEXT_DATA (canary `xma_context.h`, 64 bytes, 16 dwords).
|
|
//
|
|
// Stored big-endian in guest memory. We load all 16 dwords (BE) and unpack the
|
|
// bitfields exactly per the canary layout (bitfields pack LSB-first within each
|
|
// host-order dword). All fields below are kept as plain integers.
|
|
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct XmaContextData {
|
|
// DWORD 0
|
|
pub input_buffer_0_packet_count: u32, // :12
|
|
pub loop_count: u32, // :8
|
|
pub input_buffer_0_valid: u32, // :1
|
|
pub input_buffer_1_valid: u32, // :1
|
|
pub output_buffer_block_count: u32, // :5
|
|
pub output_buffer_write_offset: u32, // :5
|
|
// DWORD 1
|
|
pub input_buffer_1_packet_count: u32, // :12
|
|
pub loop_subframe_start: u32, // :2
|
|
pub loop_subframe_end: u32, // :3
|
|
pub loop_subframe_skip: u32, // :3
|
|
pub subframe_decode_count: u32, // :4
|
|
pub output_buffer_padding: u32, // :3
|
|
pub sample_rate: u32, // :2
|
|
pub is_stereo: u32, // :1
|
|
pub unk_dword_1_c: u32, // :1
|
|
pub output_buffer_valid: u32, // :1
|
|
// DWORD 2
|
|
pub input_buffer_read_offset: u32, // :26
|
|
pub error_status: u32, // :5
|
|
pub error_set: u32, // :1
|
|
// DWORD 3
|
|
pub loop_start: u32, // :26
|
|
pub parser_error_status: u32, // :5
|
|
pub parser_error_set: u32, // :1
|
|
// DWORD 4
|
|
pub loop_end: u32, // :26
|
|
pub packet_metadata: u32, // :5
|
|
pub current_buffer: u32, // :1
|
|
// DWORD 5..8
|
|
pub input_buffer_0_ptr: u32,
|
|
pub input_buffer_1_ptr: u32,
|
|
pub output_buffer_ptr: u32,
|
|
pub work_buffer_ptr: u32,
|
|
// DWORD 9
|
|
pub output_buffer_read_offset: u32, // :5
|
|
pub stop_when_done: u32, // :1 (bit 30)
|
|
pub interrupt_when_done: u32, // :1 (bit 31)
|
|
}
|
|
|
|
#[inline]
|
|
fn bits(v: u32, shift: u32, width: u32) -> u32 {
|
|
(v >> shift) & ((1u32 << width) - 1)
|
|
}
|
|
|
|
impl XmaContextData {
|
|
/// Read the 64-byte context struct from guest VA `ctx_va` (already a VA,
|
|
/// not a physical ptr). Each dword is read big-endian via `read_u32`.
|
|
pub fn read(mem: &GuestMemory, ctx_va: u32) -> Self {
|
|
let mut d = [0u32; 16];
|
|
for (i, w) in d.iter_mut().enumerate() {
|
|
*w = mem.read_u32(ctx_va + (i as u32) * 4);
|
|
}
|
|
let mut c = Self::default();
|
|
// DWORD 0
|
|
c.input_buffer_0_packet_count = bits(d[0], 0, 12);
|
|
c.loop_count = bits(d[0], 12, 8);
|
|
c.input_buffer_0_valid = bits(d[0], 20, 1);
|
|
c.input_buffer_1_valid = bits(d[0], 21, 1);
|
|
c.output_buffer_block_count = bits(d[0], 22, 5);
|
|
c.output_buffer_write_offset = bits(d[0], 27, 5);
|
|
// DWORD 1
|
|
c.input_buffer_1_packet_count = bits(d[1], 0, 12);
|
|
c.loop_subframe_start = bits(d[1], 12, 2);
|
|
c.loop_subframe_end = bits(d[1], 14, 3);
|
|
c.loop_subframe_skip = bits(d[1], 17, 3);
|
|
c.subframe_decode_count = bits(d[1], 20, 4);
|
|
c.output_buffer_padding = bits(d[1], 24, 3);
|
|
c.sample_rate = bits(d[1], 27, 2);
|
|
c.is_stereo = bits(d[1], 29, 1);
|
|
c.unk_dword_1_c = bits(d[1], 30, 1);
|
|
c.output_buffer_valid = bits(d[1], 31, 1);
|
|
// DWORD 2
|
|
c.input_buffer_read_offset = bits(d[2], 0, 26);
|
|
c.error_status = bits(d[2], 26, 5);
|
|
c.error_set = bits(d[2], 31, 1);
|
|
// DWORD 3
|
|
c.loop_start = bits(d[3], 0, 26);
|
|
c.parser_error_status = bits(d[3], 26, 5);
|
|
c.parser_error_set = bits(d[3], 31, 1);
|
|
// DWORD 4
|
|
c.loop_end = bits(d[4], 0, 26);
|
|
c.packet_metadata = bits(d[4], 26, 5);
|
|
c.current_buffer = bits(d[4], 31, 1);
|
|
// DWORD 5..8
|
|
c.input_buffer_0_ptr = d[5];
|
|
c.input_buffer_1_ptr = d[6];
|
|
c.output_buffer_ptr = d[7];
|
|
c.work_buffer_ptr = d[8];
|
|
// DWORD 9
|
|
c.output_buffer_read_offset = bits(d[9], 0, 5);
|
|
c.stop_when_done = bits(d[9], 30, 1);
|
|
c.interrupt_when_done = bits(d[9], 31, 1);
|
|
c
|
|
}
|
|
|
|
/// Repack the bitfields back into the 16 dwords (host order). Only the
|
|
/// decoder-owned fields differ from what was read; callers use
|
|
/// [`store_merged`] to write back without clobbering game-owned fields.
|
|
fn pack(&self) -> [u32; 16] {
|
|
let mut d = [0u32; 16];
|
|
d[0] = (self.input_buffer_0_packet_count & 0xFFF)
|
|
| ((self.loop_count & 0xFF) << 12)
|
|
| ((self.input_buffer_0_valid & 1) << 20)
|
|
| ((self.input_buffer_1_valid & 1) << 21)
|
|
| ((self.output_buffer_block_count & 0x1F) << 22)
|
|
| ((self.output_buffer_write_offset & 0x1F) << 27);
|
|
d[1] = (self.input_buffer_1_packet_count & 0xFFF)
|
|
| ((self.loop_subframe_start & 0x3) << 12)
|
|
| ((self.loop_subframe_end & 0x7) << 14)
|
|
| ((self.loop_subframe_skip & 0x7) << 17)
|
|
| ((self.subframe_decode_count & 0xF) << 20)
|
|
| ((self.output_buffer_padding & 0x7) << 24)
|
|
| ((self.sample_rate & 0x3) << 27)
|
|
| ((self.is_stereo & 1) << 29)
|
|
| ((self.unk_dword_1_c & 1) << 30)
|
|
| ((self.output_buffer_valid & 1) << 31);
|
|
d[2] = (self.input_buffer_read_offset & 0x3FF_FFFF)
|
|
| ((self.error_status & 0x1F) << 26)
|
|
| ((self.error_set & 1) << 31);
|
|
d[3] = (self.loop_start & 0x3FF_FFFF)
|
|
| ((self.parser_error_status & 0x1F) << 26)
|
|
| ((self.parser_error_set & 1) << 31);
|
|
d[4] = (self.loop_end & 0x3FF_FFFF)
|
|
| ((self.packet_metadata & 0x1F) << 26)
|
|
| ((self.current_buffer & 1) << 31);
|
|
d[5] = self.input_buffer_0_ptr;
|
|
d[6] = self.input_buffer_1_ptr;
|
|
d[7] = self.output_buffer_ptr;
|
|
d[8] = self.work_buffer_ptr;
|
|
d[9] = (self.output_buffer_read_offset & 0x1F)
|
|
| ((self.stop_when_done & 1) << 30)
|
|
| ((self.interrupt_when_done & 1) << 31);
|
|
d
|
|
}
|
|
|
|
pub fn is_input_buffer_valid(&self, idx: u32) -> bool {
|
|
if idx == 0 {
|
|
self.input_buffer_0_valid != 0
|
|
} else {
|
|
self.input_buffer_1_valid != 0
|
|
}
|
|
}
|
|
pub fn is_current_input_buffer_valid(&self) -> bool {
|
|
self.is_input_buffer_valid(self.current_buffer)
|
|
}
|
|
pub fn is_any_input_buffer_valid(&self) -> bool {
|
|
self.input_buffer_0_valid != 0 || self.input_buffer_1_valid != 0
|
|
}
|
|
pub fn input_buffer_address(&self, idx: u32) -> u32 {
|
|
if idx == 0 {
|
|
self.input_buffer_0_ptr
|
|
} else {
|
|
self.input_buffer_1_ptr
|
|
}
|
|
}
|
|
pub fn current_input_buffer_address(&self) -> u32 {
|
|
self.input_buffer_address(self.current_buffer)
|
|
}
|
|
pub fn input_buffer_packet_count(&self, idx: u32) -> u32 {
|
|
if idx == 0 {
|
|
self.input_buffer_0_packet_count
|
|
} else {
|
|
self.input_buffer_1_packet_count
|
|
}
|
|
}
|
|
pub fn current_input_buffer_packet_count(&self) -> u32 {
|
|
self.input_buffer_packet_count(self.current_buffer)
|
|
}
|
|
}
|
|
|
|
/// Merge decoder-owned fields back into guest memory (canary `StoreContextMerged`).
|
|
/// Re-reads the current context (game may have raced an update), overwrites only
|
|
/// the fields the decoder owns, and writes all 16 dwords back BE.
|
|
fn store_merged(
|
|
mem: &GuestMemory,
|
|
ctx_va: u32,
|
|
data: &XmaContextData,
|
|
initial: &XmaContextData,
|
|
) {
|
|
let mut fresh = XmaContextData::read(mem, ctx_va);
|
|
// DWORD 0
|
|
fresh.loop_count = data.loop_count;
|
|
fresh.output_buffer_write_offset = data.output_buffer_write_offset;
|
|
if initial.input_buffer_0_valid != 0 && data.input_buffer_0_valid == 0 {
|
|
fresh.input_buffer_0_valid = 0;
|
|
}
|
|
if initial.input_buffer_1_valid != 0 && data.input_buffer_1_valid == 0 {
|
|
fresh.input_buffer_1_valid = 0;
|
|
}
|
|
// DWORD 1
|
|
if initial.output_buffer_valid != 0 && data.output_buffer_valid == 0 {
|
|
fresh.output_buffer_valid = 0;
|
|
}
|
|
// DWORD 2
|
|
fresh.input_buffer_read_offset = data.input_buffer_read_offset;
|
|
fresh.error_status = data.error_status;
|
|
// DWORD 4
|
|
fresh.current_buffer = data.current_buffer;
|
|
// DWORD 9
|
|
fresh.output_buffer_read_offset = data.output_buffer_read_offset;
|
|
|
|
let d = fresh.pack();
|
|
for (i, w) in d.iter().enumerate() {
|
|
mem.write_u32(ctx_va + (i as u32) * 4, *w);
|
|
}
|
|
}
|
|
|
|
/// Public wrapper for [`store_merged`] (called from the orchestrator in xma.rs).
|
|
pub fn store_merged_pub(
|
|
mem: &GuestMemory,
|
|
ctx_va: u32,
|
|
data: &XmaContextData,
|
|
initial: &XmaContextData,
|
|
) {
|
|
store_merged(mem, ctx_va, data, initial);
|
|
}
|
|
|
|
/// Free byte count in a ring buffer from `write_off` to `read_off`
|
|
/// (canary `RingBuffer::write_count`).
|
|
pub fn ring_write_count(read_off: u32, write_off: u32, capacity: u32) -> u32 {
|
|
if read_off == write_off {
|
|
capacity
|
|
} else if write_off < read_off {
|
|
read_off - write_off
|
|
} else {
|
|
(capacity - write_off) + read_off
|
|
}
|
|
}
|
|
|
|
/// Write `bytes` into the guest ring buffer at `backing + write_off`, wrapping
|
|
/// at `capacity`. Returns the new write offset (canary `RingBuffer::Write`).
|
|
pub fn ring_write(
|
|
mem: &GuestMemory,
|
|
backing: u32,
|
|
capacity: u32,
|
|
write_off: u32,
|
|
bytes: &[u8],
|
|
) -> u32 {
|
|
let count = (bytes.len() as u32).min(capacity);
|
|
if count == 0 {
|
|
return write_off;
|
|
}
|
|
if write_off + count < capacity {
|
|
mem.write_bytes(backing + write_off, &bytes[..count as usize]);
|
|
write_off + count
|
|
} else {
|
|
let left = capacity - write_off;
|
|
mem.write_bytes(backing + write_off, &bytes[..left as usize]);
|
|
let right = count - left;
|
|
mem.write_bytes(backing, &bytes[left as usize..(left + right) as usize]);
|
|
right
|
|
}
|
|
}
|
|
|
|
// ---- BitStream (port of canary `base/bit_stream.cc`). Big-endian source.
|
|
|
|
pub struct BitStream<'a> {
|
|
buf: &'a [u8],
|
|
offset_bits: usize,
|
|
size_bits: usize,
|
|
}
|
|
|
|
impl<'a> BitStream<'a> {
|
|
pub fn new(buf: &'a [u8], size_bits: usize) -> Self {
|
|
Self { buf, offset_bits: 0, size_bits }
|
|
}
|
|
pub fn offset_bits(&self) -> usize {
|
|
self.offset_bits
|
|
}
|
|
pub fn set_offset(&mut self, off: usize) {
|
|
self.offset_bits = off.min(self.size_bits);
|
|
}
|
|
pub fn advance(&mut self, n: usize) {
|
|
self.set_offset(self.offset_bits + n);
|
|
}
|
|
pub fn bits_remaining(&self) -> usize {
|
|
self.size_bits - self.offset_bits
|
|
}
|
|
/// Peek up to 57 bits (canary contract). Reads 8 bytes BE then shifts.
|
|
pub fn peek(&self, num_bits: usize) -> u64 {
|
|
debug_assert!(num_bits <= 57);
|
|
// offset_bytes = min(offset>>3, (size-64)>>3), matching canary so an
|
|
// 8-byte load near the buffer end stays in range.
|
|
let max_byte = if self.size_bits >= 64 {
|
|
(self.size_bits - 64) >> 3
|
|
} else {
|
|
0
|
|
};
|
|
let offset_bytes = (self.offset_bits >> 3).min(max_byte);
|
|
let rel = self.offset_bits - (offset_bytes << 3);
|
|
let mut tmp = [0u8; 8];
|
|
let avail = self.buf.len().saturating_sub(offset_bytes).min(8);
|
|
tmp[..avail].copy_from_slice(&self.buf[offset_bytes..offset_bytes + avail]);
|
|
let mut value = u64::from_be_bytes(tmp);
|
|
value >>= 64 - (rel + num_bits);
|
|
value &= (1u64 << num_bits) - 1;
|
|
value
|
|
}
|
|
pub fn read(&mut self, num_bits: usize) -> u64 {
|
|
let v = self.peek(num_bits);
|
|
self.advance(num_bits);
|
|
v
|
|
}
|
|
/// Copy `num_bits` from the stream into `dest` (bit-packed, MSB-first within
|
|
/// each byte). Returns the starting bit offset within the first byte
|
|
/// (canary returns `rel_offset_bits` — the frame's intra-byte alignment).
|
|
pub fn copy(&mut self, dest: &mut [u8], num_bits: usize) -> usize {
|
|
let offset_bytes = self.offset_bits >> 3;
|
|
let rel = self.offset_bits - (offset_bytes << 3);
|
|
let mut bits_left = num_bits;
|
|
let mut out = 0usize;
|
|
|
|
if rel != 0 {
|
|
let bits = self.peek(8 - rel) as u8;
|
|
let clear_mask = !(((1u8 << rel) - 1)) as u8;
|
|
dest[out] &= clear_mask;
|
|
dest[out] |= bits;
|
|
bits_left -= 8 - rel;
|
|
self.advance(8 - rel);
|
|
out += 1;
|
|
}
|
|
if bits_left >= 8 {
|
|
let nbytes = bits_left / 8;
|
|
let src_off = (self.offset_bits >> 3).min(self.buf.len());
|
|
let copy = nbytes.min(self.buf.len().saturating_sub(src_off));
|
|
dest[out..out + copy]
|
|
.copy_from_slice(&self.buf[src_off..src_off + copy]);
|
|
out += nbytes;
|
|
self.advance(nbytes * 8);
|
|
bits_left -= nbytes * 8;
|
|
}
|
|
if bits_left != 0 {
|
|
let mut b = self.peek(bits_left) as u8;
|
|
b <<= 8 - bits_left;
|
|
let clear_mask = ((1u16 << bits_left) - 1) as u8;
|
|
dest[out] &= clear_mask;
|
|
dest[out] |= b;
|
|
self.advance(bits_left);
|
|
}
|
|
rel
|
|
}
|
|
}
|
|
|
|
// ---- XMA packet header helpers (canary `xma_helpers.h`).
|
|
|
|
#[inline]
|
|
pub fn packet_frame_count(packet: &[u8]) -> u8 {
|
|
packet[0] >> 2
|
|
}
|
|
#[inline]
|
|
pub fn packet_metadata(packet: &[u8]) -> u8 {
|
|
packet[2] & 0x7
|
|
}
|
|
#[inline]
|
|
pub fn is_packet_xma2(packet: &[u8]) -> bool {
|
|
packet_metadata(packet) == 1
|
|
}
|
|
#[inline]
|
|
pub fn packet_skip_count(packet: &[u8]) -> u8 {
|
|
packet[3]
|
|
}
|
|
/// First frame offset in bits (canary `GetPacketFrameOffset`): a 15-bit value
|
|
/// across bytes 0..2, plus the 32-bit header.
|
|
#[inline]
|
|
pub fn packet_frame_offset(packet: &[u8]) -> u32 {
|
|
let val = (((packet[0] as u32 & 0x3) << 13)
|
|
| ((packet[1] as u32) << 5)
|
|
| ((packet[2] as u32) >> 3))
|
|
& 0xFFFF;
|
|
val + 32
|
|
}
|
|
|
|
/// Sample-rate id → Hz.
|
|
pub fn sample_rate_hz(id: u32) -> u32 {
|
|
ID_TO_SAMPLE_RATE[id.min(3) as usize]
|
|
}
|
|
|
|
// ---- Packet-walk for faithful input-offset advance (canary `GetPacketInfo`,
|
|
// `GetNextPacketReadOffset`, and the offset arithmetic at the tail of
|
|
// `XmaContextNew::Decode`). These let us advance `input_buffer_read_offset` one
|
|
// *frame* at a time at canary's exact cadence — independent of the mainline
|
|
// xma2 decoder's whole-packet/burst framing — so the offset crosses packet and
|
|
// buffer boundaries (and triggers SwapInputBuffer) at the true input-drain
|
|
// rate the guest's WMV demuxer polls.
|
|
|
|
/// Info about the frame at a given bit offset within a packet (canary
|
|
/// `kPacketInfo` / `GetPacketInfo`). `frame_count_` is the number of frames
|
|
/// that begin in the packet; `current_frame_size_` is the compressed bit size
|
|
/// of the frame at `frame_offset` (0 if it can't be resolved within this
|
|
/// packet — a split header).
|
|
#[derive(Default, Clone, Copy)]
|
|
pub struct PacketInfo {
|
|
pub frame_count: u32,
|
|
pub current_frame: u32,
|
|
pub current_frame_size: u32,
|
|
}
|
|
|
|
impl PacketInfo {
|
|
pub fn is_last_frame_in_packet(&self) -> bool {
|
|
self.current_frame + 1 == self.frame_count
|
|
}
|
|
}
|
|
|
|
/// Faithful port of canary `XmaContextNew::GetPacketInfo`.
|
|
pub fn get_packet_info(packet: &[u8], frame_offset: u32) -> PacketInfo {
|
|
let mut info = PacketInfo::default();
|
|
let first_frame_offset = packet_frame_offset(packet);
|
|
let mut stream = BitStream::new(packet, BITS_PER_PACKET as usize);
|
|
stream.set_offset(first_frame_offset as usize);
|
|
|
|
// Split frame from previous packet.
|
|
if frame_offset < first_frame_offset {
|
|
info.current_frame = 0;
|
|
info.current_frame_size = first_frame_offset - frame_offset;
|
|
}
|
|
|
|
loop {
|
|
if stream.bits_remaining() < BITS_PER_FRAME_HEADER as usize {
|
|
break;
|
|
}
|
|
let frame_size = stream.peek(BITS_PER_FRAME_HEADER as usize) as u32;
|
|
if frame_size == 0 || frame_size == MAX_FRAME_LENGTH {
|
|
break;
|
|
}
|
|
if stream.offset_bits() == frame_offset as usize {
|
|
info.current_frame = info.frame_count;
|
|
info.current_frame_size = frame_size;
|
|
}
|
|
info.frame_count += 1;
|
|
if frame_size as usize > stream.bits_remaining() {
|
|
// Last frame.
|
|
break;
|
|
}
|
|
stream.advance((frame_size - 1) as usize);
|
|
// Trailing continuation bit.
|
|
if stream.read(1) == 0 {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if is_packet_xma2(packet) {
|
|
let xma2_frame_count = packet_frame_count(packet) as u32;
|
|
if xma2_frame_count > info.frame_count {
|
|
if info.current_frame_size == 0 {
|
|
info.current_frame = info.frame_count;
|
|
}
|
|
info.frame_count = xma2_frame_count;
|
|
}
|
|
}
|
|
info
|
|
}
|
|
|
|
/// Packet number for a bit offset (canary `GetPacketNumber`). Returns None when
|
|
/// the offset is in the header or past the buffer.
|
|
pub fn packet_number(size_bytes: u32, bit_offset: u32) -> Option<u32> {
|
|
if bit_offset < BITS_PER_PACKET_HEADER {
|
|
return None;
|
|
}
|
|
if bit_offset >= size_bytes * 8 {
|
|
return None;
|
|
}
|
|
Some((bit_offset >> 3) / BYTES_PER_PACKET)
|
|
}
|
|
|
|
/// min(remaining_stream_bits, frame_size) (canary `GetAmountOfBitsToRead`).
|
|
pub fn amount_of_bits_to_read(remaining_stream_bits: u32, frame_size: u32) -> u32 {
|
|
remaining_stream_bits.min(frame_size)
|
|
}
|
|
|
|
// ---- Per-context decode state (lives in the XmaDecoder, one per ctx).
|
|
|
|
#[derive(Default)]
|
|
pub struct ContextDecodeState {
|
|
/// FFmpeg xma2 codec for this context (lazily created / reconfigured).
|
|
pub codec: Option<Xma2Codec>,
|
|
pub codec_rate: u32,
|
|
pub codec_channels: u32,
|
|
/// Staged interleaved S16BE PCM for the current decoded frame
|
|
/// (`raw_frame_`), drained by Consume in 256-byte blocks.
|
|
pub raw_frame: Vec<u8>,
|
|
/// Decoded interleaved S16BE PCM not yet split into per-frame `raw_frame`s.
|
|
/// The mainline xma2 decoder emits bursts of many 512-sample frames at once
|
|
/// (internal FIFO + 4096-sample lookahead); we queue the bytes here and
|
|
/// hand the guest exactly one 512-sample frame per `produce_frame`.
|
|
pub pcm_queue: VecDeque<u8>,
|
|
pub current_frame_remaining_subframes: u8,
|
|
pub remaining_subframe_blocks_in_output: i32,
|
|
/// Total 512-sample frames decoded for this context (diagnostic).
|
|
pub frames_decoded: u64,
|
|
/// Whether a "first frame" diagnostic has been emitted.
|
|
pub first_frame_logged: bool,
|
|
/// FFmpeg feed cursor: the next packet index (within the *current* input
|
|
/// buffer at feed time) we will hand to FFmpeg. This is the decoder's
|
|
/// internal intake position and is intentionally decoupled from the
|
|
/// guest-visible `input_buffer_read_offset` (which advances per *emitted*
|
|
/// frame via the faithful packet-walk). We feed ahead so FFmpeg always has
|
|
/// enough buffered input to satisfy the guest's drain, while the guest sees
|
|
/// the read offset move at canary's true per-frame cadence.
|
|
pub feed_packet_index: u32,
|
|
/// `current_buffer` the feed cursor is reading from; reset on swap so the
|
|
/// feed follows the same ping-pong as the guest-visible buffer.
|
|
pub feed_buffer: u32,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// The bitfield unpack/pack must round-trip every decoder-relevant field at
|
|
/// the exact canary offsets (regression against a shifted bit).
|
|
#[test]
|
|
fn context_bitfields_round_trip() {
|
|
let mut c = XmaContextData::default();
|
|
c.input_buffer_0_packet_count = 632;
|
|
c.loop_count = 0;
|
|
c.input_buffer_0_valid = 1;
|
|
c.input_buffer_1_valid = 0;
|
|
c.output_buffer_block_count = 30;
|
|
c.output_buffer_write_offset = 5;
|
|
c.subframe_decode_count = 8;
|
|
c.output_buffer_padding = 1;
|
|
c.sample_rate = 3;
|
|
c.is_stereo = 1;
|
|
c.output_buffer_valid = 1;
|
|
c.input_buffer_read_offset = 16416;
|
|
c.error_status = 4;
|
|
c.current_buffer = 1;
|
|
c.input_buffer_0_ptr = 0x0b9f_d000;
|
|
c.output_buffer_ptr = 0x01f6_6e00;
|
|
c.output_buffer_read_offset = 7;
|
|
c.interrupt_when_done = 1;
|
|
|
|
// pack → words → re-read via the same word layout.
|
|
let d = c.pack();
|
|
// Simulate read() decode from the packed words.
|
|
let mut c2 = XmaContextData::default();
|
|
c2.input_buffer_0_packet_count = bits(d[0], 0, 12);
|
|
c2.input_buffer_0_valid = bits(d[0], 20, 1);
|
|
c2.output_buffer_block_count = bits(d[0], 22, 5);
|
|
c2.output_buffer_write_offset = bits(d[0], 27, 5);
|
|
c2.subframe_decode_count = bits(d[1], 20, 4);
|
|
c2.output_buffer_padding = bits(d[1], 24, 3);
|
|
c2.sample_rate = bits(d[1], 27, 2);
|
|
c2.is_stereo = bits(d[1], 29, 1);
|
|
c2.output_buffer_valid = bits(d[1], 31, 1);
|
|
c2.input_buffer_read_offset = bits(d[2], 0, 26);
|
|
c2.error_status = bits(d[2], 26, 5);
|
|
c2.current_buffer = bits(d[4], 31, 1);
|
|
c2.output_buffer_read_offset = bits(d[9], 0, 5);
|
|
c2.interrupt_when_done = bits(d[9], 31, 1);
|
|
|
|
assert_eq!(c2.input_buffer_0_packet_count, 632);
|
|
assert_eq!(c2.input_buffer_0_valid, 1);
|
|
assert_eq!(c2.output_buffer_block_count, 30);
|
|
assert_eq!(c2.output_buffer_write_offset, 5);
|
|
assert_eq!(c2.subframe_decode_count, 8);
|
|
assert_eq!(c2.output_buffer_padding, 1);
|
|
assert_eq!(c2.sample_rate, 3);
|
|
assert_eq!(c2.is_stereo, 1);
|
|
assert_eq!(c2.output_buffer_valid, 1);
|
|
assert_eq!(c2.input_buffer_read_offset, 16416);
|
|
assert_eq!(c2.error_status, 4);
|
|
assert_eq!(c2.current_buffer, 1);
|
|
assert_eq!(c2.output_buffer_read_offset, 7);
|
|
assert_eq!(c2.interrupt_when_done, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn phys_to_backing_projects_physical_window() {
|
|
assert_eq!(xma_phys_to_backing(0x0b9f_d000), 0x4b9f_d000);
|
|
assert_eq!(xma_phys_to_backing(0x01f6_6e00), 0x41f6_6e00);
|
|
}
|
|
|
|
#[test]
|
|
fn ring_write_count_matches_canary() {
|
|
// empty (read==write) → full capacity.
|
|
assert_eq!(ring_write_count(0, 0, 7680), 7680);
|
|
// write ahead of read.
|
|
assert_eq!(ring_write_count(0, 256, 7680), 7680 - 256);
|
|
// write wrapped behind read.
|
|
assert_eq!(ring_write_count(512, 256, 7680), 256);
|
|
}
|
|
|
|
#[test]
|
|
fn packet_header_helpers() {
|
|
// Matches the observed first packet word 0x08000000: byte0=0x08.
|
|
let pkt = [0x08u8, 0x00, 0x00, 0x00];
|
|
assert_eq!(packet_frame_count(&pkt), 2); // 0x08>>2 = 2
|
|
// frame offset: ((0x08&3)<<13 | 0<<5 | 0x00>>3) + 32 = 32.
|
|
assert_eq!(packet_frame_offset(&pkt), 32);
|
|
// A non-zero byte2 shifts the offset: 0x08>>3 = 1 → +1.
|
|
let pkt2 = [0x08u8, 0x00, 0x08, 0x00];
|
|
assert_eq!(packet_frame_offset(&pkt2), 33);
|
|
}
|
|
}
|
|
|
|
impl ContextDecodeState {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
codec: None,
|
|
codec_rate: 0,
|
|
codec_channels: 0,
|
|
raw_frame: vec![0u8; (BYTES_PER_FRAME_CHANNEL * 2) as usize],
|
|
pcm_queue: VecDeque::new(),
|
|
current_frame_remaining_subframes: 0,
|
|
remaining_subframe_blocks_in_output: 0,
|
|
frames_decoded: 0,
|
|
first_frame_logged: false,
|
|
feed_packet_index: 0,
|
|
feed_buffer: 0,
|
|
}
|
|
}
|
|
}
|