From fd2607452d8804b7ffa959a20b8db61b99e27c6e Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 23 Jul 2026 07:23:10 +0300 Subject: [PATCH] fix(wipeout): frame draws above the mask; mask joins the draw order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wipeout pass ran after the wire pass with no draw-order bias, so the mask erased its own boundary frame (and everything else at its plane regardless of draw order). The mask uniform now carries the entity's draw-order depth (uniform grows 80 → 96 bytes) and biases its clip-z like every other pipeline, and the frame wire rides a +half-rank override — the later mask pass fails the depth test at the frame's pixels, so the boundary stays visible, while entities drawn above the wipeout are no longer erased. Co-Authored-By: Claude Fable 5 --- src/scene/convert/tess.rs | 7 +++++++ src/scene/entity.rs | 8 +++++++- src/scene/pipeline/wipeout_gpu.rs | 8 ++++++-- src/shaders/wipeout.wgsl | 7 +++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index bec162c9d..e12866c3d 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -1341,6 +1341,13 @@ pub(crate) fn tessellate_entity( w.name = host_name.clone(); w.aci = aci; set_wire_aabb(w, aabb); + // A wipeout's frame sits ABOVE its own mask: the mask + // draws at the entity's rank, the frame half a rank + // closer, so the later mask pass fails the depth test at + // the frame's pixels instead of erasing it. + if matches!(e, EntityType::Wipeout(_)) { + w.depth_override = Some(0.5); + } } bases.extend(wires); } diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 2ae94fc7b..74efec0f1 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1032,6 +1032,7 @@ impl Scene { // epoch boundary and never re-evaluate as the user pans. The // pipeline's `wipeout_skip_flags` (compute_wipeout_lod) does // the per-frame skip at draw time instead. + let depth_map = self.draw_depth_map(); let mut models = Vec::new(); for entity in self.document.entities() { let EntityType::Wipeout(wo) = entity else { @@ -1072,7 +1073,12 @@ impl Scene { angle_offset: 0.0, scale: 1.0, world_origin: fill_origin, - draw_depth: 0.0, + // The mask erases what draws below the wipeout and loses + // to what draws above — including its own frame wire, + // which rides a +half-rank override on top of this. + draw_depth: depth_map + .get(&wo.common.handle.value()) + .map_or(0.0, |d| d[0]), }); } } diff --git a/src/scene/pipeline/wipeout_gpu.rs b/src/scene/pipeline/wipeout_gpu.rs index dd114c2c1..4b7472f71 100644 --- a/src/scene/pipeline/wipeout_gpu.rs +++ b/src/scene/pipeline/wipeout_gpu.rs @@ -47,7 +47,7 @@ impl HatchVertex { // ── Uniform structs ─────────────────────────────────────────────────────── -/// Per-hatch parameters (binding 0). Must be 80 bytes (16-byte aligned). +/// Per-hatch parameters (binding 0). Must be 96 bytes (16-byte aligned). /// /// `mode` encoding: /// 0 → Pattern (families in FamilyBatchData) @@ -75,7 +75,9 @@ pub struct HatchUniformData { // // is small (e.g. UTM drawing + sub-mm hatch). pub origin_low: [f32; 2], // 72: anchor low residual (double-single) so the // // vertex clip position stays precise at UTM scale. -} // total 80 bytes + pub draw_depth: f32, // 80: signed (-1,1) draw-order bias + pub _pad: [f32; 3], // 84..96 +} // total 96 bytes /// Boundary polygon (binding 1). Matches WGSL `Boundary`. #[repr(C)] @@ -298,6 +300,8 @@ impl WipeoutGpu { (origin[0] - origin[0] as f32 as f64) as f32, (origin[1] - origin[1] as f32 as f64) as f32, ], + draw_depth: model.draw_depth, + _pad: [0.0; 3], }; // ── BoundaryData (in snapped-local space) ──────────────────────── diff --git a/src/shaders/wipeout.wgsl b/src/shaders/wipeout.wgsl index 67c08aaa8..78c9da889 100644 --- a/src/shaders/wipeout.wgsl +++ b/src/shaders/wipeout.wgsl @@ -52,6 +52,9 @@ struct HatchUniforms { // // catastrophically cancel when world coords // // are large and pattern spacing is small. origin_low: vec2, // 72: anchor low residual (double-single) + draw_depth: f32, // 80: signed (-1,1) draw-order bias; 0 = neutral + _pad0: f32, // 84 + _pad1: vec2, // 88 } @group(1) @binding(0) var h: HatchUniforms; @@ -111,6 +114,10 @@ struct VOut { v.pos.y + h.origin_low.y - u.eye_low.y, v.pos.z - u.eye_low.z); o.clip = u.view_rot * vec4(hi + lo, 1.0); + // Draw-order bias (see wire.wgsl): the mask erases what draws BELOW its + // entity and loses to what draws above — including its own frame wire, + // which rides a +half-rank override. + o.clip.z = o.clip.z - h.draw_depth * 0.001 * o.clip.w; o.xz = vec2(v.pos.x, v.pos.y); return o; }