From deb92923957c159d8a8a5407b5c05b91c8b5f62e Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 2 Jul 2026 20:53:37 +0200 Subject: [PATCH] [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 --- crates/xenia-gpu/src/primitive.rs | 35 +++++++++-------- crates/xenia-gpu/src/translator.rs | 60 ++++++++++++++++++++++-------- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/crates/xenia-gpu/src/primitive.rs b/crates/xenia-gpu/src/primitive.rs index caffb99..53cf339 100644 --- a/crates/xenia-gpu/src/primitive.rs +++ b/crates/xenia-gpu/src/primitive.rs @@ -143,18 +143,17 @@ fn expand_quads(indices: Option<&[u32]>, vertex_count: u32) -> ProcessedPrimitiv /// `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. /// -/// Our replay pipeline has no host-VS corner synthesis (and the procedural -/// `vs_main` does not consume `rewritten_indices` yet), so we mirror the -/// `expand_quads`/`expand_fan` CPU idiom and emit the 3 real vertices of each -/// rect as one triangle list `(v0,v1,v2)` — the visible lower half of the -/// rect. This un-rejects the draw and gives a faithful `host_vertex_count`. -/// -/// TODO: once `vs_main` does real vertex fetch + interpolation, upgrade to the -/// full quad — 6 indices `[v0,v1,v2, v2,v1,v3]` with a synthesized `v3` corner -/// — mirroring canary's `kRectangleListAsTriangleStrip`. +/// 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(3 * rect_count as usize); + let mut out = Vec::with_capacity(6 * rect_count as usize); let get = |i: u32| -> u32 { match indices { Some(buf) => buf[i as usize], @@ -163,9 +162,11 @@ fn expand_rectangles(indices: Option<&[u32]>, vertex_count: u32) -> ProcessedPri }; for r in 0..rect_count { let base = r * 3; - out.push(get(base)); - out.push(get(base + 1)); - out.push(get(base + 2)); + 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); @@ -239,12 +240,14 @@ mod tests { #[test] fn rectangle_list_expansion() { - // 2 rects (6 verts) → one triangle (v0,v1,v2) per rect, not rejected. + // 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, 3, 4, 5]); + 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, 6); + assert_eq!(p.host_vertex_count, 12); assert!(!p.rejected); } diff --git a/crates/xenia-gpu/src/translator.rs b/crates/xenia-gpu/src/translator.rs index 0cf9fc2..5befe1d 100644 --- a/crates/xenia-gpu/src/translator.rs +++ b/crates/xenia-gpu/src/translator.rs @@ -235,11 +235,17 @@ impl EmitCtx { // vertices here (mirrors `primitive.rs` index rewrite, but in the // VS since the replay path is non-indexed): // QuadList(13): 6 host verts → guest [0,1,2, 0,2,3] - // RectangleList(8): drawn as one triangle [0,1,2] (the 4th - // corner needs cross-vertex synthesis — TODO), so host - // indices >=3 fold onto the existing triangle. + // RectangleList(8): 6 host verts → [0,1,2, 2,1,3]; corner 3 + // has no backing vertex, so `rect_synth` flags it and + // `emit_vfetch` extrapolates its attributes as v0+v2-v1 + // (parallelogram completion, matching Xenos rect semantics). + // Drawing only the front triangle left a diagonal seam. // Other prims pass through unchanged. self.push("var gvidx: u32 = vidx;"); + // Rectangle-list 4th-corner synthesis state, consumed by + // `emit_vfetch`. `rect_synth` is false for every other prim. + self.push("var rect_base: u32 = 0u;"); + self.push("var rect_synth: bool = false;"); self.push("if (draw_ctx.prim_kind == 13u) {"); self.indent += 1; self.push("let q = vidx % 6u; let qbase = (vidx / 6u) * 4u;"); @@ -248,8 +254,13 @@ impl EmitCtx { self.indent -= 1; self.push("} else if (draw_ctx.prim_kind == 8u) {"); self.indent += 1; - self.push("let t = vidx % 3u; let rbase = (vidx / 3u) * 3u;"); - self.push("gvidx = rbase + t;"); + self.push("let local = vidx % 6u; rect_base = (vidx / 6u) * 3u;"); + // Triangles (v0,v1,v2) + (v0,v2,v3) — canary's rect tessellation + // (an explicit list, not a strip). v3 (corner 3) is synthesized. + self.push("var rlut = array(0u, 1u, 2u, 0u, 2u, 3u);"); + self.push("let corner = rlut[local];"); + self.push("gvidx = rect_base + corner;"); + self.push("rect_synth = corner == 3u;"); self.indent -= 1; self.push("}"); // Seed r0 with vertex index for simple shaders that read it. @@ -681,24 +692,43 @@ impl EmitCtx { } else { "" }; + let l0 = lane(0); + let l1 = lane(1); + let l2 = lane(2); + let l3 = lane(3); + // One decode of `addr` → the attribute vec4. Reused verbatim for the + // normal single-vertex read and for each of the three source vertices + // of a synthesized rectangle corner (the lane exprs reference `addr` + // and `w16` by name, so each `{ let addr = …; }` sub-scope re-decodes). + let read_into = |dst: &str, idx_expr: &str| -> String { + format!( + "{{ let addr = base + ({idx_expr}) * {stride}u + {attr_off}u; \ + if (addr + {read_bound}u < n) {{ {w16_decl}{dst} = vec4({l0}, {l1}, {l2}, {l3}); }} }}" + ) + }; + // RectangleList 4th corner: no backing vertex, so extrapolate the + // attribute as v0 + v2 - v1 (parallelogram completion, matching canary). + // With the (v0,v1,v2)+(v0,v2,v3) tessellation the synthesized corner is + // diagonal to v1 (e.g. TL,TR,BR given → BL = TL+BR-TR). Applies + // uniformly to position and every interpolator. `rect_synth`/`rect_base` + // come from the VS preamble; false/0 for every non-rect draw. + let t0 = read_into("t0", "rect_base + 0u"); + let t1 = read_into("t1", "rect_base + 1u"); + let t2 = read_into("t2", "rect_base + 2u"); + let normal = read_into(&format!("r[{dst_reg}u]"), &format!("u32(r[{src_reg}u].x)")); self.push(&format!( "{{ let endian = {endian_term}; \ - let vidx = u32(r[{src_reg}u].x); \ var base = 0u; \ if (draw_ctx.vertex_base_dwords == 0u) {{ \ base = (xenos_consts.fetch[{fc0_idx}u] & 0xFFFFFFFCu) >> 2u; \ }} \ - let addr = base + vidx * {stride}u + {attr_off}u; \ let n = arrayLength(&vertex_buffer); \ - if (addr + {read_bound}u < n) {{ \ - {w16_decl}\ - r[{dst_reg}u] = vec4({l0}, {l1}, {l2}, {l3}); \ - }} }}", + if (rect_synth) {{ \ + var t0 = vec4(0.0, 0.0, 0.0, 1.0); var t1 = t0; var t2 = t0; \ + {t0} {t1} {t2} \ + r[{dst_reg}u] = t0 + t2 - t1; \ + }} else {{ {normal} }} }}", fc0_idx = const_off, - l0 = lane(0), - l1 = lane(1), - l2 = lane(2), - l3 = lane(3), )); Ok(()) }