xenia-gpu: end-to-end Xenos pipeline (PM4, ucode, EDRAM, resolve)
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>
This commit is contained in:
123
crates/xenia-gpu/src/ring_view.rs
Normal file
123
crates/xenia-gpu/src/ring_view.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
//! 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user