[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>
This commit is contained in:
MechaCat02
2026-07-02 20:53:37 +02:00
parent 3559c8f6ef
commit deb9292395
2 changed files with 64 additions and 31 deletions

View File

@@ -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);
}

View File

@@ -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<u32, 6>(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<f32>({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<f32>({l0}, {l1}, {l2}, {l3}); \
}} }}",
if (rect_synth) {{ \
var t0 = vec4<f32>(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(())
}