git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitce153370cfc6fe43f1a49956b739df11b56d0eef
parent2787f9b39c
authorLucas Galante <[email protected]>
date2026-06-22 12:44
feat: implement GPU-side rounded corner clipping for 2D UI and 3D Viewport

 src/app.rs         | 95 +++++++++++++++++++++++++++++++++++++++++++++++-------
 src/shader.wgsl    | 47 +++++++++++++++++++++++++++
 src/shader_3d.wgsl | 42 ++++++++++++++++++++++++
 3 files changed, 173 insertions(+), 11 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 9290dce..646f992 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -648,6 +648,15 @@ pub struct ResizeDirection {
     pub bottom: bool,
 }
 
+#[repr(C)]
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct ViewportUniforms {
+    pub mvp: [[f32; 4]; 4],
+    pub window_size: [f32; 2],
+    pub window_radius: f32,
+    pub _padding: f32,
+}
+
 pub struct State {
     pub wgpu_adapter: cce_ui::backend::WgpuAdapter,
     pub render_pipeline: wgpu::RenderPipeline,
@@ -678,6 +687,7 @@ pub struct State {
     pub backdrop_sampler: wgpu::Sampler,
     pub backdrop_bind_group_layout: wgpu::BindGroupLayout,
     pub backdrop_bind_group: wgpu::BindGroup,
+    pub window_info_buffer: wgpu::Buffer,
     pub rotation_y: f32,
     pub rotation_x: f32,
     pub is_rotating_viewport: bool,
@@ -2154,6 +2164,16 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                     count: None,
                 },
+                wgpu::BindGroupLayoutEntry {
+                    binding: 2,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Buffer {
+                        ty: wgpu::BufferBindingType::Uniform,
+                        has_dynamic_offset: false,
+                        min_binding_size: wgpu::BufferSize::new(16),
+                    },
+                    count: None,
+                },
             ],
         });
 
@@ -2180,6 +2200,21 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
         });
         let backdrop_texture_view = backdrop_texture.create_view(&wgpu::TextureViewDescriptor::default());
 
+        let window_info_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+            label: Some("Window Info Buffer"),
+            size: 16,
+            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+            mapped_at_creation: false,
+        });
+
+        let window_info_data = [
+            pw as f32,
+            ph as f32,
+            cce_ui::color::window_corner_radius() * scale as f32,
+            0.0,
+        ];
+        queue.write_buffer(&window_info_buffer, 0, bytemuck::cast_slice(&window_info_data));
+
         let backdrop_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
             label: Some("Backdrop Bind Group"),
             layout: &backdrop_bind_group_layout,
@@ -2192,6 +2227,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     binding: 1,
                     resource: wgpu::BindingResource::Sampler(&backdrop_sampler),
                 },
+                wgpu::BindGroupEntry {
+                    binding: 2,
+                    resource: window_info_buffer.as_entire_binding(),
+                },
             ],
         });
 
@@ -2248,7 +2287,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
 
         let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
             label: Some("Uniform Buffer"),
-            size: 64,
+            size: 80,
             usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
             mapped_at_creation: false,
         });
@@ -2257,11 +2296,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             label: Some("3D Bind Group Layout"),
             entries: &[wgpu::BindGroupLayoutEntry {
                 binding: 0,
-                visibility: wgpu::ShaderStages::VERTEX,
+                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
                 ty: wgpu::BindingType::Buffer {
                     ty: wgpu::BufferBindingType::Uniform,
                     has_dynamic_offset: false,
-                    min_binding_size: wgpu::BufferSize::new(64),
+                    min_binding_size: wgpu::BufferSize::new(80),
                 },
                 count: None,
             }],
@@ -2281,14 +2320,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
                     buffer: &uniform_buffer,
                     offset: 0,
-                    size: wgpu::BufferSize::new(64),
+                    size: wgpu::BufferSize::new(80),
                 }),
             }],
         });
 
         let uniform_buffer_grid = device.create_buffer(&wgpu::BufferDescriptor {
             label: Some("Grid Uniform Buffer"),
-            size: 64,
+            size: 80,
             usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
             mapped_at_creation: false,
         });
@@ -2301,14 +2340,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
                     buffer: &uniform_buffer_grid,
                     offset: 0,
-                    size: wgpu::BufferSize::new(64),
+                    size: wgpu::BufferSize::new(80),
                 }),
             }],
         });
 
         let uniform_buffer_pivot = device.create_buffer(&wgpu::BufferDescriptor {
             label: Some("Pivot Uniform Buffer"),
-            size: 64,
+            size: 80,
             usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
             mapped_at_creation: false,
         });
@@ -2321,7 +2360,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
                     buffer: &uniform_buffer_pivot,
                     offset: 0,
-                    size: wgpu::BufferSize::new(64),
+                    size: wgpu::BufferSize::new(80),
                 }),
             }],
         });
@@ -2680,6 +2719,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             backdrop_sampler,
             backdrop_bind_group_layout,
             backdrop_bind_group,
+            window_info_buffer,
             rotation_y: 0.0,
             rotation_x: 0.0,
             is_rotating_viewport: false,
@@ -3795,6 +3835,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             self.backdrop_texture = b_tex;
             self.backdrop_texture_view = b_view;
 
+            let window_info_data = [
+                width as f32,
+                height as f32,
+                cce_ui::color::window_corner_radius() * self.scale as f32,
+                0.0,
+            ];
+            self.wgpu_adapter.queue.write_buffer(&self.window_info_buffer, 0, bytemuck::cast_slice(&window_info_data));
+
             self.backdrop_bind_group = self.wgpu_adapter.device.create_bind_group(&wgpu::BindGroupDescriptor {
                 label: Some("Backdrop Bind Group"),
                 layout: &self.backdrop_bind_group_layout,
@@ -3807,6 +3855,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                         binding: 1,
                         resource: wgpu::BindingResource::Sampler(&self.backdrop_sampler),
                     },
+                    wgpu::BindGroupEntry {
+                        binding: 2,
+                        resource: self.window_info_buffer.as_entire_binding(),
+                    },
                 ],
             });
 
@@ -5430,10 +5482,25 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     let view_mat = Mat4::from_rotation_z(rz.to_radians()) * Mat4::look_at_rh(camera_world_pos, pivot, camera_up);
                     let model = Mat4::from_rotation_y(self.rotation_y) * Mat4::from_rotation_x(self.rotation_x);
                     let mvp = proj * view_mat * model;
-                    self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[mvp.to_cols_array_2d()]));
+                    let window_size = [self.physical_width as f32, self.physical_height as f32];
+                    let window_radius = cce_ui::color::window_corner_radius() * self.scale as f32;
+
+                    let uniforms = ViewportUniforms {
+                        mvp: mvp.to_cols_array_2d(),
+                        window_size,
+                        window_radius,
+                        _padding: 0.0,
+                    };
+                    self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
 
                     let mvp_grid = proj * view_mat * model;
-                    self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer_grid, 0, bytemuck::cast_slice(&[mvp_grid.to_cols_array_2d()]));
+                    let uniforms_grid = ViewportUniforms {
+                        mvp: mvp_grid.to_cols_array_2d(),
+                        window_size,
+                        window_radius,
+                        _padding: 0.0,
+                    };
+                    self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer_grid, 0, bytemuck::cast_slice(&[uniforms_grid]));
 
                     let cam_angle_y = camera_pos.x.atan2(camera_pos.z);
                     let rot_angle = if self.active_camera != "Default Camera" {
@@ -5443,7 +5510,13 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     };
                     let model_pivot = Mat4::from_translation(pivot) * Mat4::from_rotation_y(rot_angle);
                     let mvp_pivot = proj * view_mat * model_pivot;
-                    self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer_pivot, 0, bytemuck::cast_slice(&[mvp_pivot.to_cols_array_2d()]));
+                    let uniforms_pivot = ViewportUniforms {
+                        mvp: mvp_pivot.to_cols_array_2d(),
+                        window_size,
+                        window_radius,
+                        _padding: 0.0,
+                    };
+                    self.wgpu_adapter.queue.write_buffer(&self.uniform_buffer_pivot, 0, bytemuck::cast_slice(&[uniforms_pivot]));
 
                     let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                         label: Some("3D Render Pass"),
diff --git a/src/shader.wgsl b/src/shader.wgsl
index 9d132f2..2050430 100644
--- a/src/shader.wgsl
+++ b/src/shader.wgsl
@@ -1,6 +1,50 @@
 @group(0) @binding(0) var t_backdrop: texture_2d<f32>;
 @group(0) @binding(1) var s_backdrop: sampler;
 
+struct WindowInfo {
+    window_size: vec2<f32>,
+    corner_radius: f32,
+    padding: f32,
+}
+
+@group(0) @binding(2) var<uniform> window_info: WindowInfo;
+
+fn is_outside_window_corners(pos: vec2<f32>) -> bool {
+    let w = window_info.window_size.x;
+    let h = window_info.window_size.y;
+    let r = window_info.corner_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;
+    }
+    // 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;
+    }
+    // Boundary check
+    if (pos.x < 0.0 || pos.x > w || pos.y < 0.0 || pos.y > h) {
+        return true;
+    }
+    return false;
+}
+
 struct VertexOutput {
     @builtin(position) clip_position: vec4f,
     @location(0) color: vec4f,
@@ -22,6 +66,9 @@ fn vs_main(
 
 @fragment
 fn fs_main(in: VertexOutput) -> @location(0) vec4f {
+    if (is_outside_window_corners(in.clip_position.xy)) {
+        discard;
+    }
     if (in.clip_circle.z > 0.0) {
         let dx = in.clip_position.x - in.clip_circle.x;
         let dy = in.clip_position.y - in.clip_circle.y;
diff --git a/src/shader_3d.wgsl b/src/shader_3d.wgsl
index bdf8e05..6ac7527 100644
--- a/src/shader_3d.wgsl
+++ b/src/shader_3d.wgsl
@@ -1,9 +1,48 @@
 struct Uniforms {
     mvp: mat4x4<f32>,
+    window_size: vec2<f32>,
+    window_radius: f32,
+    padding: f32,
 }
 
 @group(0) @binding(0) var<uniform> uniforms: Uniforms;
 
+fn is_outside_window_corners(pos: vec2<f32>) -> bool {
+    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;
+    }
+    // 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;
+    }
+    // Boundary check
+    if (pos.x < 0.0 || pos.x > w || pos.y < 0.0 || pos.y > h) {
+        return true;
+    }
+    return false;
+}
+
 struct VertexOutput {
     @builtin(position) position: vec4f,
     @location(0) color: vec3f,
@@ -22,5 +61,8 @@ fn vs_main(
 
 @fragment
 fn fs_main(in: VertexOutput) -> @location(0) vec4f {
+    if (is_outside_window_corners(in.position.xy)) {
+        discard;
+    }
     return vec4f(in.color, 1.0);
 }