//! Primary ring buffer view. //! //! Games allocate a ring buffer in physical memory (via //! `MmAllocatePhysicalMemoryEx` with WRITE_COMBINE), then hand the base //! address + log2(size) to `VdInitializeRingBuffer`. They subsequently push //! PM4 packets into it, advancing the write-pointer by writing to a GPU //! register (`CP_RB_WPTR`) or via kernel-call shims. //! //! The GPU consumes packets from `read_offset_dwords` up to (but not past) //! the write pointer. After consuming enough bytes it writes `read_offset` //! into the guest-memory address registered by `VdEnableRingBufferRPtrWriteBack` //! so the game can know how much of the ring has been consumed. /// Tracks the primary ring buffer as set up by the guest. #[derive(Debug, Clone, Copy, Default)] pub struct RingBufferView { /// Guest physical/virtual base address. `0` means uninitialized. pub base: u32, /// Size of the ring in dwords. `0` means uninitialized. pub size_dwords: u32, /// Dword offset the GPU has consumed up to (relative to `base`). pub read_offset_dwords: u32, /// Dword offset the guest has last written into (relative to `base`). /// Updated either by an MMIO write to `CP_RB_WPTR` or by the kernel /// (`VdSwap` is a hint — the game reserves a 64-dword slot in the ring /// for it). pub write_offset_dwords: u32, /// Guest address where we mirror `read_offset_dwords` each time we make /// progress. `0` if the game never called `VdEnableRingBufferRPtrWriteBack`. pub rptr_writeback_addr: u32, /// Write-back block granularity in dwords (from the `log2` arg to /// `VdEnableRingBufferRPtrWriteBack`). We always write back eagerly, so /// we don't actually use this for scheduling — kept for observability. pub rptr_writeback_block_dwords: u32, /// True for an indirect-buffer (`INDIRECT_BUFFER`) view. An IB is a fixed /// *linear* sub-stream, not a circular ring: it is fully written when the /// GPU jumps to it, so the read pointer advances monotonically from `0` to /// `size_dwords` and then the buffer is exhausted (the caller ring is /// popped). It must NOT wrap, and the primary `CP_RB_WPTR` must not be /// applied to it. Mirrors canary `ExecuteIndirectBuffer`, which executes /// the IB through a separate `RingBuffer reader_` and restores the primary /// reader afterward (command_processor.cc). Circular (primary-ring) /// semantics are used when this is `false`. pub indirect: bool, } impl RingBufferView { pub fn new() -> Self { Self::default() } /// True if the guest has provided a base + size. pub fn is_initialized(&self) -> bool { self.base != 0 && self.size_dwords != 0 } /// True if there is pending unread data to consume. pub fn has_pending(&self) -> bool { if !self.is_initialized() { return false; } if self.indirect { // Linear sub-stream: exhausted once the read pointer reaches the // (fixed) write pointer. Never wraps. self.read_offset_dwords < self.write_offset_dwords } else { self.read_offset_dwords != self.write_offset_dwords } } /// Number of dwords we can consume without wrapping past the write ptr. pub fn pending_dwords(&self) -> u32 { if !self.is_initialized() { return 0; } if self.indirect { self.write_offset_dwords .saturating_sub(self.read_offset_dwords) } else if self.write_offset_dwords >= self.read_offset_dwords { self.write_offset_dwords - self.read_offset_dwords } else { // write has wrapped — we can read up to the end of the ring. self.size_dwords - self.read_offset_dwords } } /// Advance the read pointer by `dwords`. Circular rings wrap at /// `size_dwords`; an indirect buffer advances linearly (no wrap) so it /// terminates exactly at its fixed write pointer. pub fn advance_read(&mut self, dwords: u32) { if self.size_dwords == 0 { return; } if self.indirect { self.read_offset_dwords = self.read_offset_dwords.saturating_add(dwords); } else { self.read_offset_dwords = (self.read_offset_dwords + dwords) % self.size_dwords; } } /// Guest address for the dword at relative offset `i` from the current /// read pointer. `None` if uninitialized. pub fn addr_at_offset(&self, offset_dwords: u32) -> Option { if !self.is_initialized() { return None; } let off = if self.indirect { self.read_offset_dwords.saturating_add(offset_dwords) } else { (self.read_offset_dwords + offset_dwords) % self.size_dwords }; Some(self.base.wrapping_add(off.wrapping_mul(4))) } } #[cfg(test)] mod tests { use super::*; #[test] fn uninitialized_view_reports_empty() { let v = RingBufferView::new(); assert!(!v.is_initialized()); assert!(!v.has_pending()); assert_eq!(v.pending_dwords(), 0); } #[test] fn wrap_around_arithmetic() { let mut v = RingBufferView::new(); v.base = 0x4000_0000; v.size_dwords = 16; v.read_offset_dwords = 14; v.write_offset_dwords = 2; // wrapped // We can only read to end-of-ring in one chunk. assert_eq!(v.pending_dwords(), 2); v.advance_read(2); assert_eq!(v.read_offset_dwords, 0); // Now unwrapped, 2 more to go. assert_eq!(v.pending_dwords(), 2); } #[test] fn addr_at_offset_wraps() { let mut v = RingBufferView::new(); v.base = 0x4000_0000; v.size_dwords = 4; v.read_offset_dwords = 3; assert_eq!(v.addr_at_offset(0), Some(0x4000_000C)); assert_eq!(v.addr_at_offset(1), Some(0x4000_0000)); assert_eq!(v.addr_at_offset(2), Some(0x4000_0004)); } #[test] fn indirect_buffer_drains_linearly_and_terminates() { // An indirect buffer is a fixed linear sub-stream: read advances from // 0 to `size_dwords` and then is exhausted — it must NOT wrap back to // 0 (which previously caused an infinite re-read of a system command // buffer; iterate-2O). write_offset == size, exactly as the // INDIRECT_BUFFER handler sets it. let mut ib = RingBufferView { base: 0x4adf_5080, size_dwords: 11, read_offset_dwords: 0, write_offset_dwords: 11, rptr_writeback_addr: 0, rptr_writeback_block_dwords: 0, indirect: true, }; assert!(ib.has_pending()); // Drain the exact packet layout observed for Sylpheed's init IB: // 2 + 3 + 6 dwords = 11. ib.advance_read(2); assert!(ib.has_pending()); ib.advance_read(3); assert!(ib.has_pending()); ib.advance_read(6); // reaches 11 == write assert_eq!(ib.read_offset_dwords, 11); assert!( !ib.has_pending(), "indirect buffer must terminate at write ptr, not wrap to 0" ); // addr_at_offset must not modulo-wrap for an indirect buffer. ib.read_offset_dwords = 9; assert_eq!(ib.addr_at_offset(1), Some(0x4adf_5080 + 10 * 4)); } #[test] fn indirect_flag_does_not_affect_circular_ring() { // Sanity: a circular (primary) ring still wraps as before. let mut v = RingBufferView::new(); v.base = 0x4adc_c000; v.size_dwords = 8192; v.read_offset_dwords = 8190; v.write_offset_dwords = 2; assert!(v.has_pending()); v.advance_read(4); // (8190 + 4) % 8192 = 2 assert_eq!(v.read_offset_dwords, 2); assert!(!v.has_pending()); } }