Files
xenia-rs/crates/xenia-gpu/src/primitive.rs
MechaCat02 deb9292395 [iterate-4A] intro-video: complete RectangleList quad (fix diagonal seam)
A Xenos RectangleList gives 3 corners of a rectangle; the GPU synthesizes
the 4th (v3 = v0 + v2 - v1) and tessellates the quad as two triangles
(v0,v1,v2) + (v0,v2,v3). Our expansion emitted only the front triangle, so
the movie's fullscreen YUV rect filled half the screen and left a diagonal
seam down the frame (absent in canary).

expand_rectangles now emits the full 6-index quad. Since v3 has no backing
vertex in the guest window, the translated VS synthesizes its attributes:
the RectangleList arm maps 6 host verts through [0,1,2, 0,2,3], flags the
4th corner, and emit_vfetch computes that corner's attributes (position and
every interpolator) as v0 + v2 - v1 by reading the three real source
vertices. Non-rect draws are unaffected (the synth flag is false).

Verified: the movie background now fills the whole frame uniformly, no
diagonal, matching canary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:53:37 +02:00

268 lines
9.9 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`.
//!
//! 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<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 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<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 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);
}
}