First real GPU implementation. Ring/PM4 frontend (ring_view,
ring_drain, pm4) drains the command processor; gpu_system owns the
threaded backend (DrainFence RPC + parker/fence helpers from M1) and
the MMIO-mapped register block (mmio_region).
Xenos shader frontend: ucode/{alu,control_flow,fetch,mod}.rs decode
the Xbox 360 microcode, translator.rs lowers it onto the WGSL
xenos_interp interpreter shader (shaders/xenos_interp.wgsl).
shader_metrics.rs counts decode/translate work.
Render state: draw_state, primitive, render_target_cache,
texture_cache, tiled_address (Xenos's swizzled tiled-memory layout),
xenos_constants (register field constants), edram (the 10 MiB EDRAM
model with MSAA), and resolve.rs (TILE_FLUSH copy-out — clear-resolve
plus bitwise-equivalent 32 bpp + 64 bpp paths landed). handle.rs
owns the typed GPU-resource handles the kernel hands out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
4.4 KiB
Rust
124 lines
4.4 KiB
Rust
//! 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<u32> {
|
|
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));
|
|
}
|
|
}
|