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:
MechaCat02
2026-05-01 16:29:38 +02:00
parent 5f0d6487ea
commit 79eb52c378
24 changed files with 10984 additions and 18 deletions

View 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));
}
}