//! 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, } 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 { self.is_initialized() && 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.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`, wrapping at `size_dwords`. pub fn advance_read(&mut self, dwords: u32) { if self.size_dwords == 0 { return; } 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 = (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)); } }