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,229 @@
//! Primitive processor — normalize Xenos primitives into host-GPU forms.
//!
//! wgpu only exposes `PrimitiveTopology::{PointList, LineList, LineStrip,
//! TriangleList, TriangleStrip}`. For everything else (fans, quads,
//! rectangles) we rewrite indices on the CPU side so the host just sees a
//! triangle list. Ground truth: `xenia-canary/src/xenia/gpu/primitive_processor.h/cc`.
//!
//! P3 scope: only the shapes Sylpheed's UI + early gameplay paths need
//! (list, strip, fan). Rectangle + quad expansions are stubs logged via
//! `tracing::warn!` for later.
use crate::draw_state::{IndexSize, PrimitiveType};
/// Host primitive topology — a subset of wgpu's that we commit to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostTopology {
PointList,
LineList,
LineStrip,
TriangleList,
TriangleStrip,
}
/// Result of primitive processing.
#[derive(Debug, Clone)]
pub struct ProcessedPrimitive {
pub topology: HostTopology,
/// When the Xenos primitive needed client-side rewriting (fans, quads),
/// this buffer holds the rewritten 16-bit or 32-bit index sequence.
/// `None` means the input index buffer is usable as-is.
pub rewritten_indices: Option<Vec<u32>>,
/// Post-processing vertex count — equals the input count when indices
/// pass through unchanged.
pub host_vertex_count: u32,
/// `true` if we rejected the primitive (unsupported shape) and the
/// caller should skip this draw. Logged via `tracing::warn!`.
pub rejected: bool,
}
/// Normalize a draw.
///
/// `indices` is `None` for `AutoIndex` draws; otherwise it's the decoded
/// index stream (already endian-converted / widened to u32 by the caller).
pub fn process(
primitive: PrimitiveType,
vertex_count: u32,
indices: Option<&[u32]>,
) -> ProcessedPrimitive {
match primitive {
PrimitiveType::PointList => pass_through(HostTopology::PointList, vertex_count),
PrimitiveType::LineList => pass_through(HostTopology::LineList, vertex_count),
PrimitiveType::LineStrip => pass_through(HostTopology::LineStrip, vertex_count),
PrimitiveType::TriangleList => pass_through(HostTopology::TriangleList, vertex_count),
PrimitiveType::TriangleStrip => pass_through(HostTopology::TriangleStrip, vertex_count),
PrimitiveType::TriangleFan => expand_fan(indices, vertex_count),
PrimitiveType::RectangleList => expand_rectangles(indices, vertex_count),
PrimitiveType::QuadList => expand_quads(indices, vertex_count),
PrimitiveType::None | PrimitiveType::Unknown(_) => {
tracing::warn!(?primitive, "gpu: rejecting unsupported primitive");
metrics::counter!("gpu.primitive.rejected").increment(1);
ProcessedPrimitive {
topology: HostTopology::TriangleList,
rewritten_indices: None,
host_vertex_count: 0,
rejected: true,
}
}
}
}
fn pass_through(topology: HostTopology, vertex_count: u32) -> ProcessedPrimitive {
ProcessedPrimitive {
topology,
rewritten_indices: None,
host_vertex_count: vertex_count,
rejected: false,
}
}
/// Convert a triangle fan to a triangle list. Fan indices `[0, 1, 2, 3, 4]`
/// expand to triangles `(0,1,2), (0,2,3), (0,3,4)` — 3 × (n-2) host indices.
fn expand_fan(indices: Option<&[u32]>, vertex_count: u32) -> ProcessedPrimitive {
if vertex_count < 3 {
return ProcessedPrimitive {
topology: HostTopology::TriangleList,
rewritten_indices: Some(Vec::new()),
host_vertex_count: 0,
rejected: false,
};
}
let mut out = Vec::with_capacity(3 * (vertex_count as usize - 2));
let get = |i: u32| -> u32 {
match indices {
Some(buf) => buf[i as usize],
None => i,
}
};
let apex = get(0);
for i in 1..vertex_count.saturating_sub(1) {
out.push(apex);
out.push(get(i));
out.push(get(i + 1));
}
let host_vertex_count = out.len() as u32;
ProcessedPrimitive {
topology: HostTopology::TriangleList,
rewritten_indices: Some(out),
host_vertex_count,
rejected: false,
}
}
/// Convert a quad list (groups of 4) to a triangle list (groups of 6).
fn expand_quads(indices: Option<&[u32]>, vertex_count: u32) -> ProcessedPrimitive {
let quad_count = vertex_count / 4;
let mut out = Vec::with_capacity(6 * quad_count as usize);
let get = |i: u32| -> u32 {
match indices {
Some(buf) => buf[i as usize],
None => i,
}
};
for q in 0..quad_count {
let base = q * 4;
let a = get(base);
let b = get(base + 1);
let c = get(base + 2);
let d = get(base + 3);
out.extend_from_slice(&[a, b, c, a, c, d]);
}
let host_vertex_count = out.len() as u32;
ProcessedPrimitive {
topology: HostTopology::TriangleList,
rewritten_indices: Some(out),
host_vertex_count,
rejected: false,
}
}
/// Rectangle lists: a Xenos-specific primitive where each group of 3
/// vertices defines a right-angle rectangle by its three non-repeated
/// corners (the 4th is derived). The uber-shader doesn't support this yet;
/// the ucode translator will emulate it as a geometry-stage fake. For P3
/// we emit an empty draw.
fn expand_rectangles(_indices: Option<&[u32]>, _vertex_count: u32) -> ProcessedPrimitive {
tracing::warn!("gpu: rectangle list primitive not yet implemented (P3 stub)");
metrics::counter!("gpu.primitive.rejected", "reason" => "rectangle_list").increment(1);
ProcessedPrimitive {
topology: HostTopology::TriangleList,
rewritten_indices: Some(Vec::new()),
host_vertex_count: 0,
rejected: true,
}
}
/// Widen a u16 index buffer to u32. The primitive processor normalizes to
/// u32 so downstream wgpu pipeline descriptors stay simple.
pub fn widen_indices(raw: &[u8], size: IndexSize, count: u32) -> Vec<u32> {
let mut out = Vec::with_capacity(count as usize);
match size {
IndexSize::Sixteen => {
for i in 0..count as usize {
let off = i * 2;
if off + 2 > raw.len() {
break;
}
// Xenos indices are big-endian on the wire.
let be = u16::from_be_bytes([raw[off], raw[off + 1]]);
out.push(be as u32);
}
}
IndexSize::ThirtyTwo => {
for i in 0..count as usize {
let off = i * 4;
if off + 4 > raw.len() {
break;
}
let be = u32::from_be_bytes([raw[off], raw[off + 1], raw[off + 2], raw[off + 3]]);
out.push(be);
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn triangle_list_passes_through() {
let p = process(PrimitiveType::TriangleList, 6, None);
assert_eq!(p.topology, HostTopology::TriangleList);
assert!(p.rewritten_indices.is_none());
assert_eq!(p.host_vertex_count, 6);
assert!(!p.rejected);
}
#[test]
fn fan_to_list_expands_correctly() {
// Fan of 5 vertices → triangles (0,1,2), (0,2,3), (0,3,4)
let p = process(PrimitiveType::TriangleFan, 5, None);
let idx = p.rewritten_indices.unwrap();
assert_eq!(idx, vec![0, 1, 2, 0, 2, 3, 0, 3, 4]);
assert_eq!(p.topology, HostTopology::TriangleList);
assert_eq!(p.host_vertex_count, 9);
}
#[test]
fn quad_list_expansion() {
let p = process(PrimitiveType::QuadList, 8, None);
let idx = p.rewritten_indices.unwrap();
assert_eq!(idx, vec![0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7]);
}
#[test]
fn widen_u16_indices_big_endian() {
// 3 indices [1, 2, 0x1234] in BE u16.
let raw = [0, 1, 0, 2, 0x12, 0x34];
let out = widen_indices(&raw, IndexSize::Sixteen, 3);
assert_eq!(out, vec![1, 2, 0x1234]);
}
#[test]
fn rejects_unknown_primitive() {
let p = process(PrimitiveType::Unknown(0x2A), 3, None);
assert!(p.rejected);
}
}