//! 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 { 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, 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, /// 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, 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, } } }