//! 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`. //! //! Scope: list, strip, fan, quad, and rectangle expansions are all handled //! (rectangles via CPU triangle-list rewrite — see `expand_rectangles`). 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>, /// 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 rectangle; the 4th corner is extrapolated as /// `v3 = v0 + v2 - v1` (parallelogram completion). Canary expands this in a /// host vertex-shader variant (`kRectangleListAsTriangleStrip`, /// `primitive_processor.cc:389-456`): a 4-vertex triangle strip per rect with /// the 4th corner synthesized *in the VS* from the host-vertex index. /// /// We draw the full quad as a 6-vertex triangle list `[v0,v1,v2, v0,v2,v3]` /// per rect (canary's tessellation). The synthesized `v3` (index `base+3`) has no backing vertex in /// the guest window, so the translated `vs_main` computes its attributes as /// `v0 + v2 - v1` when the host-vertex index maps to that corner (see the /// RectangleList handling + `emit_vfetch` corner synthesis in `translator.rs`). /// Emitting only the front triangle `(v0,v1,v2)` — as we used to — fills just /// half the rect and leaves a diagonal seam (the movie's fullscreen YUV rect /// showed exactly that). fn expand_rectangles(indices: Option<&[u32]>, vertex_count: u32) -> ProcessedPrimitive { let rect_count = vertex_count / 3; let mut out = Vec::with_capacity(6 * rect_count as usize); let get = |i: u32| -> u32 { match indices { Some(buf) => buf[i as usize], None => i, } }; for r in 0..rect_count { let base = r * 3; let (v0, v1, v2) = (get(base), get(base + 1), get(base + 2)); // Front triangle (v0,v1,v2), then the completing triangle (v0,v2,v3) // with the synthesized 4th corner (index `base + 3`; the VS // extrapolates its attributes as v0+v2-v1). out.extend_from_slice(&[v0, v1, v2, v0, v2, base + 3]); } let host_vertex_count = out.len() as u32; metrics::counter!("gpu.primitive.expanded", "shape" => "rectangle_list").increment(1); ProcessedPrimitive { topology: HostTopology::TriangleList, rewritten_indices: Some(out), host_vertex_count, rejected: false, } } /// 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 { 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 rectangle_list_expansion() { // 2 rects (6 verts) → full quad per rect: [v0,v1,v2, v2,v1,v3] where // v3 is the synthesized 4th corner (index base+3, extrapolated in the // VS). rect0 → base 0, rect1 → base 3. let p = process(PrimitiveType::RectangleList, 6, None); let idx = p.rewritten_indices.unwrap(); assert_eq!(idx, vec![0, 1, 2, 0, 2, 3, 3, 4, 5, 3, 5, 6]); assert_eq!(p.topology, HostTopology::TriangleList); assert_eq!(p.host_vertex_count, 12); assert!(!p.rejected); } #[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); } }