Files
xenia-rs/crates/xenia-gpu/src/primitive.rs
MechaCat02 79eb52c378 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>
2026-05-01 16:29:38 +02:00

230 lines
8.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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);
}
}