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

commit7e95a557524c7b2a1ec9de7be2f055479ba3fae1
parent70be241451
authorLucas Galante <[email protected]>
date2026-07-20 14:55
fix: window-corner cut follows the squircle family too, with AA feather

The window silhouette discards in shader2d and scene3d were still circular
hard cuts, so the (squircle) context border and plate corners misaligned
wherever a pane runs flush to the window edge — the highlight border's
corner arc lay outside the circular cut and got shaved mid-arc. Both
passes now share the superellipse SDF (corner_shape rides the spare
uniform padding slot) and feather ~1px into fragment alpha; straight
edges keep their exact hard cut. Adds a scene3d compile test.

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

 src/vk/renderer.rs   |  7 +++++-
 src/vk/scene.rs      |  5 ++--
 src/vk/scene3d.wgsl  | 61 ++++++++++++++++++++++++------------------------
 src/vk/shader2d.wgsl | 65 +++++++++++++++++++++++++---------------------------
 4 files changed, 70 insertions(+), 68 deletions(-)

diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index f28f2fb..7056ec6 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -799,7 +799,7 @@ impl VkRenderer {
             self.extent.width as f32,
             self.extent.height as f32,
             self.corner_radius_px,
-            0.0f32,
+            crate::layout::corner_shape(),
         ];
         if let Some(allocation) = self.window_info.allocation.as_mut() {
             allocation.mapped_slice_mut().unwrap()[..16]
@@ -1653,4 +1653,9 @@ mod tests {
     fn shader2d_compiles() {
         assert!(!super::shader2d_spirv().is_empty());
     }
+
+    #[test]
+    fn scene3d_compiles() {
+        assert!(!super::scene3d_spirv().is_empty());
+    }
 }
diff --git a/src/vk/scene.rs b/src/vk/scene.rs
index 115c884..0d7bf4b 100644
--- a/src/vk/scene.rs
+++ b/src/vk/scene.rs
@@ -41,7 +41,7 @@ struct SceneUniforms {
     mvp: [[f32; 4]; 4],
     window_size: [f32; 2],
     window_radius: f32,
-    _padding: f32,
+    corner_shape: f32,
 }
 
 const UNIFORM_SIZE: vk::DeviceSize = std::mem::size_of::<SceneUniforms>() as vk::DeviceSize;
@@ -607,13 +607,14 @@ impl SceneStage {
             Self::write_descriptor(device, frame);
         }
         let window_size = [self.extent.width as f32, self.extent.height as f32];
+        let corner_shape = crate::layout::corner_shape();
         let mapped = frame.uniforms.allocation.as_mut().unwrap().mapped_slice_mut().unwrap();
         for (i, draw) in staged.draws.iter().enumerate() {
             let uniforms = SceneUniforms {
                 mvp: draw.mvp,
                 window_size,
                 window_radius: corner_radius_px,
-                _padding: 0.0,
+                corner_shape,
             };
             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 193dcda..8bd7e14 100644
--- a/src/vk/scene3d.wgsl
+++ b/src/vk/scene3d.wgsl
@@ -2,45 +2,40 @@ struct Uniforms {
     mvp: mat4x4<f32>,
     window_size: vec2<f32>,
     window_radius: f32,
-    padding: f32,
+    // Corner-shape exponent shared with shader2d: circular arc at 2,
+    // superellipse squircle above.
+    corner_shape: f32,
 }
 
 @group(0) @binding(0) var<uniform> uniforms: Uniforms;
 
-fn is_outside_window_corners(pos: vec2<f32>) -> bool {
+// Signed distance to the window's rounded silhouette — shader2d's
+// window_corner_distance, kept in lockstep so the 3D scene fill cuts along the
+// exact curve the 2D pass (and the plates' tessellated corners) use: positive
+// outside the corner arcs and past the window bounds, large-negative on the
+// straight edges (those keep their hard cut).
+fn window_corner_distance(pos: vec2<f32>) -> f32 {
     let w = uniforms.window_size.x;
     let h = uniforms.window_size.y;
     let r = uniforms.window_radius;
-    
-    // Top-left
-    if (pos.x < r && pos.y < r) {
-        let dx = pos.x - r;
-        let dy = pos.y - r;
-        return (dx * dx + dy * dy) > r * r;
-    }
-    // Top-right
-    if (pos.x > w - r && pos.y < r) {
-        let dx = pos.x - (w - r);
-        let dy = pos.y - r;
-        return (dx * dx + dy * dy) > r * r;
-    }
-    // Bottom-left
-    if (pos.x < r && pos.y > h - r) {
-        let dx = pos.x - r;
-        let dy = pos.y - (h - r);
-        return (dx * dx + dy * dy) > r * r;
+
+    if (pos.x < 0.0 || pos.x > w || pos.y < 0.0 || pos.y > h) {
+        return 1e5;
     }
-    // Bottom-right
-    if (pos.x > w - r && pos.y > h - r) {
-        let dx = pos.x - (w - r);
-        let dy = pos.y - (h - r);
-        return (dx * dx + dy * dy) > r * r;
+    if (r <= 0.0) {
+        return -1e5;
     }
-    // Boundary check
-    if (pos.x < 0.0 || pos.x > w || pos.y < 0.0 || pos.y > h) {
-        return true;
+    let q = abs(pos - vec2f(w * 0.5, h * 0.5)) - vec2f(w * 0.5 - r, h * 0.5 - r);
+    if (q.x > 0.0 && q.y > 0.0) {
+        let shape = uniforms.corner_shape;
+        if (shape > 2.001) {
+            let lp = max(pow(pow(q.x, shape) + pow(q.y, shape), 1.0 / shape), 1e-4);
+            let g = vec2f(pow(q.x / lp, shape - 1.0), pow(q.y / lp, shape - 1.0));
+            return (lp - r) / max(length(g), 1e-4);
+        }
+        return length(q) - r;
     }
-    return false;
+    return -1e5;
 }
 
 struct VertexOutput {
@@ -65,8 +60,12 @@ fn vs_main(
 
 @fragment
 fn fs_main(in: VertexOutput) -> @location(0) vec4f {
-    if (is_outside_window_corners(in.position.xy)) {
+    // ~1px feather along the squircle window corner (the pass clears to
+    // transparent and blends with straight alpha, so partial coverage fades
+    // the scene out exactly at the silhouette).
+    let cov = 1.0 - smoothstep(-0.5, 0.5, window_corner_distance(in.position.xy));
+    if (cov <= 0.0) {
         discard;
     }
-    return vec4f(in.color, 1.0);
+    return vec4f(in.color, cov);
 }
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 4b997c4..ed9855b 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -11,7 +11,9 @@
 struct WindowInfo {
     window_size: vec2<f32>,
     corner_radius: f32,
-    padding: f32,
+    // Corner-shape exponent shared with the plates and the rounded-rect clip:
+    // circular arc at 2, superellipse squircle above.
+    corner_shape: f32,
 }
 
 @group(0) @binding(2) var<uniform> window_info: WindowInfo;
@@ -33,43 +35,35 @@ struct PlateFeatures {
 }
 @group(0) @binding(3) var<uniform> plate_features: PlateFeatures;
 
-fn is_outside_window_corners(pos: vec2<f32>) -> bool {
+// Signed distance to the window's rounded silhouette at pos: positive outside
+// the corner arcs (and past the window bounds), large-negative elsewhere so the
+// straight edges keep their exact hard cut at the buffer boundary. The corner
+// family follows window_info.corner_shape — circular arc at 2, superellipse
+// squircle above, with the Lp branch's first-order |∇| correction so a feather
+// built on this distance keeps ~uniform width around the arc (the same
+// construction as rr_sdf_grad and the tessellated plate corners).
+fn window_corner_distance(pos: vec2<f32>) -> f32 {
     let w = window_info.window_size.x;
     let h = window_info.window_size.y;
     let r = window_info.corner_radius;
 
-    if (r <= 0.0) {
-        return false;
-    }
-    // Top-left
-    if (pos.x < r && pos.y < r) {
-        let dx = pos.x - r;
-        let dy = pos.y - r;
-        return (dx * dx + dy * dy) > r * r;
-    }
-    // Top-right
-    if (pos.x > w - r && pos.y < r) {
-        let dx = pos.x - (w - r);
-        let dy = pos.y - r;
-        return (dx * dx + dy * dy) > r * r;
-    }
-    // Bottom-left
-    if (pos.x < r && pos.y > h - r) {
-        let dx = pos.x - r;
-        let dy = pos.y - (h - r);
-        return (dx * dx + dy * dy) > r * r;
+    if (pos.x < 0.0 || pos.x > w || pos.y < 0.0 || pos.y > h) {
+        return 1e5;
     }
-    // Bottom-right
-    if (pos.x > w - r && pos.y > h - r) {
-        let dx = pos.x - (w - r);
-        let dy = pos.y - (h - r);
-        return (dx * dx + dy * dy) > r * r;
+    if (r <= 0.0) {
+        return -1e5;
     }
-    // Boundary check
-    if (pos.x < 0.0 || pos.x > w || pos.y < 0.0 || pos.y > h) {
-        return true;
+    let q = abs(pos - vec2f(w * 0.5, h * 0.5)) - vec2f(w * 0.5 - r, h * 0.5 - r);
+    if (q.x > 0.0 && q.y > 0.0) {
+        let shape = window_info.corner_shape;
+        if (shape > 2.001) {
+            let lp = max(pow(pow(q.x, shape) + pow(q.y, shape), 1.0 / shape), 1e-4);
+            let g = vec2f(pow(q.x / lp, shape - 1.0), pow(q.y / lp, shape - 1.0));
+            return (lp - r) / max(length(g), 1e-4);
+        }
+        return length(q) - r;
     }
-    return false;
+    return -1e5;
 }
 
 // Per-batch push constants (112 bytes). The first two vec4s are the rounded-rect
@@ -400,7 +394,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
         return vec4f(gray, final_alpha);
     }
 
-    if (is_outside_window_corners(in.clip_position.xy)) {
+    // Window-corner coverage: ~1px feather along the squircle silhouette in
+    // place of the old hard circular discard, so the window edge, the 3D scene
+    // fill, and the plates' tessellated corners all sit on the same curve.
+    var clip_cov = 1.0 - smoothstep(-0.5, 0.5, window_corner_distance(in.clip_position.xy));
+    if (clip_cov <= 0.0) {
         discard;
     }
     if (in.clip_circle.z > 0.0) {
@@ -416,12 +414,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
     // corners around it, with a ~1px feather folded into the fragment alpha in
     // place of the old hard discard — a clipped edge and a drawn plate corner
     // share both curve and AA. Fully-outside fragments still discard.
-    var clip_cov = 1.0;
     if (rrect_clip.rect1.y > 0.5) {
         let r = rrect_clip.rect1.x;
         let prect = vec4f(rrect_clip.rect0.xy, rrect_clip.rect0.zw + vec2f(r, r));
         let d = rr_sdf_grad(in.clip_position.xy, prect, vec4f(r)).z;
-        clip_cov = 1.0 - smoothstep(-0.5, 0.5, d);
+        clip_cov *= 1.0 - smoothstep(-0.5, 0.5, d);
         if (clip_cov <= 0.0) {
             discard;
         }