git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit3a978d8715b6a3b34dfffa70c4e482a658405ca7
parent5c474aaf9c
authorLucas Galante <[email protected]>
date2026-07-22 15:23
feat: per-draw wire tint + depth-biased wireframe pipeline

SceneDraw gains wire_tint (rgb + mix, zero = untouched), carried
through the per-draw uniform block into scene3d's fragment mix — a
wireframe pass overlaid on its own filled mesh needs it, since the
lines inherit the mesh colors and would vanish into the identical
fill. The wireframe pipeline gets a small negative depth bias so those
lines win the LESS depth test against the coplanar fill instead of
z-fighting it.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/vk/scene.rs     | 15 +++++++++++++++
 src/vk/scene3d.wgsl |  7 ++++++-
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/vk/scene.rs b/src/vk/scene.rs
index c0a60e0..078246b 100644
--- a/src/vk/scene.rs
+++ b/src/vk/scene.rs
@@ -35,6 +35,12 @@ pub struct SceneDraw {
     /// Rasterize as lines (PolygonMode::LINE) instead of filled triangles.
     /// Falls back to filled when the device lacks fillModeNonSolid.
     pub wireframe: bool,
+    /// rgb + mix: the fragment color is mixed toward `wire_tint.rgb` by
+    /// `wire_tint[3]`. Zero = vertex colors untouched (the default draw).
+    /// A wireframe pass overlaid on its own filled mesh needs this — the
+    /// lines inherit the mesh's colors and would otherwise vanish into the
+    /// identical fill beneath.
+    pub wire_tint: [f32; 4],
 }
 
 /// shader_3d.wgsl's uniform block.
@@ -45,6 +51,7 @@ struct SceneUniforms {
     window_size: [f32; 2],
     window_radius: f32,
     corner_shape: f32,
+    wire_tint: [f32; 4],
 }
 
 const UNIFORM_SIZE: vk::DeviceSize = std::mem::size_of::<SceneUniforms>() as vk::DeviceSize;
@@ -282,11 +289,18 @@ impl SceneStage {
 
             // The wireframe twin: identical but rasterized as lines. Culling
             // stays on so the wire view matches the fill's visible surface.
+            // A small negative depth bias pulls the lines toward the viewer,
+            // so a wire pass drawn over its own filled mesh (the overlay
+            // mode) wins the LESS depth test instead of z-fighting the
+            // coplanar fill.
             let wireframe_pipeline = wireframe_supported.then(|| {
                 let rasterization_lines = vk::PipelineRasterizationStateCreateInfo::default()
                     .polygon_mode(vk::PolygonMode::LINE)
                     .cull_mode(vk::CullModeFlags::BACK)
                     .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
+                    .depth_bias_enable(true)
+                    .depth_bias_constant_factor(-2.0)
+                    .depth_bias_slope_factor(-1.0)
                     .line_width(1.0);
                 device
                     .create_graphics_pipelines(
@@ -652,6 +666,7 @@ impl SceneStage {
                 window_size,
                 window_radius: corner_radius_px,
                 corner_shape,
+                wire_tint: draw.wire_tint,
             };
             let offset = (self.uniform_stride as usize) * i;
             mapped[offset..offset + UNIFORM_SIZE as usize]
diff --git a/src/vk/scene3d.wgsl b/src/vk/scene3d.wgsl
index 8bd7e14..d06179b 100644
--- a/src/vk/scene3d.wgsl
+++ b/src/vk/scene3d.wgsl
@@ -5,6 +5,10 @@ struct Uniforms {
     // Corner-shape exponent shared with shader2d: circular arc at 2,
     // superellipse squircle above.
     corner_shape: f32,
+    // rgb + mix: fragment color mixed toward .rgb by .a. Zero = vertex
+    // colors untouched; a wireframe pass overlaid on its own filled mesh
+    // sets it so the lines separate from the identical fill beneath.
+    wire_tint: vec4<f32>,
 }
 
 @group(0) @binding(0) var<uniform> uniforms: Uniforms;
@@ -67,5 +71,6 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
     if (cov <= 0.0) {
         discard;
     }
-    return vec4f(in.color, cov);
+    let rgb = mix(in.color, uniforms.wire_tint.rgb, uniforms.wire_tint.a);
+    return vec4f(rgb, cov);
 }