Skip to main content

wisp/render/
clip.rs

1//! Clip pipeline — apply a [`MaskShape`] to a foreground
2//! `RenderTexture` and write the masked result.
3//!
4//! Used by the auto-dispatch path in
5//! [`Renderer::render_stage`](crate::render::Renderer::render_stage)
6//! when a container has a
7//! [`Container::clip`](crate::scene::container::Container::clip) set:
8//! the subtree is rendered into a foreground RT, this pipeline samples
9//! the foreground and multiplies in the SDF-based mask alpha, and the
10//! result is composited back onto the parent's destination.
11//!
12//! Today: only [`MaskShape::RoundedRect`]. Later issues add more shape
13//! variants; the same pipeline (uniform-driven SDF) handles them by
14//! switching the SDF function in the WGSL.
15
16use bytemuck::{Pod, Zeroable};
17use wgpu::util::DeviceExt;
18
19use crate::application::Application;
20use crate::scene::clip::MaskShape;
21use crate::texture::render_texture::RenderTexture;
22
23#[repr(C)]
24#[derive(Clone, Copy, Pod, Zeroable)]
25struct ClipUniforms {
26    center: [f32; 2],
27    half_extents: [f32; 2],
28    radius: f32,
29    aa: f32,
30    invert: f32,
31    shape_kind: f32,
32}
33
34pub(crate) struct ClipPipeline {
35    pipeline: wgpu::RenderPipeline,
36    bind_group_layout: wgpu::BindGroupLayout,
37    sampler: wgpu::Sampler,
38}
39
40impl ClipPipeline {
41    pub(crate) fn new(app: &Application, output_format: wgpu::TextureFormat) -> Self {
42        let device = app.device();
43
44        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
45            label: Some("wisp::clip bg layout"),
46            entries: &[
47                wgpu::BindGroupLayoutEntry {
48                    binding: 0,
49                    visibility: wgpu::ShaderStages::FRAGMENT,
50                    ty: wgpu::BindingType::Texture {
51                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
52                        view_dimension: wgpu::TextureViewDimension::D2,
53                        multisampled: false,
54                    },
55                    count: None,
56                },
57                wgpu::BindGroupLayoutEntry {
58                    binding: 1,
59                    visibility: wgpu::ShaderStages::FRAGMENT,
60                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
61                    count: None,
62                },
63                wgpu::BindGroupLayoutEntry {
64                    binding: 2,
65                    visibility: wgpu::ShaderStages::FRAGMENT,
66                    ty: wgpu::BindingType::Buffer {
67                        ty: wgpu::BufferBindingType::Uniform,
68                        has_dynamic_offset: false,
69                        min_binding_size: None,
70                    },
71                    count: None,
72                },
73            ],
74        });
75
76        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
77            label: Some("wisp::clip pipeline layout"),
78            bind_group_layouts: &[&bind_group_layout],
79            push_constant_ranges: &[],
80        });
81
82        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
83            label: Some("wisp::clip shader"),
84            source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/clip.wgsl").into()),
85        });
86
87        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
88            label: Some("wisp::clip pipeline"),
89            layout: Some(&pipeline_layout),
90            vertex: wgpu::VertexState {
91                module: &shader,
92                entry_point: Some("main_vs"),
93                buffers: &[],
94                compilation_options: wgpu::PipelineCompilationOptions::default(),
95            },
96            fragment: Some(wgpu::FragmentState {
97                module: &shader,
98                entry_point: Some("main_fs"),
99                targets: &[Some(wgpu::ColorTargetState {
100                    format: output_format,
101                    blend: Some(wgpu::BlendState::REPLACE),
102                    write_mask: wgpu::ColorWrites::ALL,
103                })],
104                compilation_options: wgpu::PipelineCompilationOptions::default(),
105            }),
106            primitive: wgpu::PrimitiveState::default(),
107            depth_stencil: None,
108            multisample: wgpu::MultisampleState::default(),
109            multiview: None,
110            cache: None,
111        });
112
113        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
114            label: Some("wisp::clip sampler"),
115            address_mode_u: wgpu::AddressMode::ClampToEdge,
116            address_mode_v: wgpu::AddressMode::ClampToEdge,
117            address_mode_w: wgpu::AddressMode::ClampToEdge,
118            mag_filter: wgpu::FilterMode::Linear,
119            min_filter: wgpu::FilterMode::Linear,
120            mipmap_filter: wgpu::FilterMode::Nearest,
121            ..Default::default()
122        });
123
124        Self {
125            pipeline,
126            bind_group_layout,
127            sampler,
128        }
129    }
130
131    /// Sample `foreground` and write the masked result into `output`.
132    /// `output_dims` lets us compute a 1-pixel anti-alias band in the
133    /// shader (`aa = 2/min(w, h)` in NDC units).
134    pub(crate) fn apply(
135        &self,
136        app: &Application,
137        shape: MaskShape,
138        foreground: &RenderTexture,
139        output: &RenderTexture,
140    ) {
141        self.apply_with_invert(app, shape, foreground, output, false);
142    }
143
144    fn apply_with_invert(
145        &self,
146        app: &Application,
147        shape: MaskShape,
148        foreground: &RenderTexture,
149        output: &RenderTexture,
150        invert: bool,
151    ) {
152        let (cx, cy, hx, hy, radius, shape_kind) = match shape {
153            MaskShape::Rect { rect } => {
154                let cx = rect.min.x + rect.size.x * 0.5;
155                let cy = rect.min.y + rect.size.y * 0.5;
156                let hx = (rect.size.x * 0.5).max(0.0);
157                let hy = (rect.size.y * 0.5).max(0.0);
158                (cx, cy, hx, hy, 0.0, 0.0)
159            }
160            MaskShape::RoundedRect { rect, radius } => {
161                let cx = rect.min.x + rect.size.x * 0.5;
162                let cy = rect.min.y + rect.size.y * 0.5;
163                let hx = (rect.size.x * 0.5).max(0.0);
164                let hy = (rect.size.y * 0.5).max(0.0);
165                let r = radius.clamp(0.0, hx.min(hy));
166                (cx, cy, hx, hy, r, 0.0)
167            }
168            MaskShape::Circle { center, radius } => {
169                // Rounded-rect SDF degenerates to circle when
170                // half_extents == radius == r. (The shader formula
171                // becomes length(max(|p|, 0)) - r = length(p) - r.)
172                let r = radius.max(0.0);
173                (center.x, center.y, r, r, r, 0.0)
174            }
175            MaskShape::Ellipse {
176                center,
177                half_extents,
178            } => {
179                let hx = half_extents.x.max(0.0);
180                let hy = half_extents.y.max(0.0);
181                // Radius is unused by the ellipse SDF branch; pass 0.
182                (center.x, center.y, hx, hy, 0.0, 1.0)
183            }
184        };
185
186        let w_f = f32::from(u16::try_from(output.width().min(u32::from(u16::MAX))).unwrap_or(1));
187        let h_f = f32::from(u16::try_from(output.height().min(u32::from(u16::MAX))).unwrap_or(1));
188        let aa = 2.0 / w_f.min(h_f).max(1.0);
189
190        let uniforms = ClipUniforms {
191            center: [cx, cy],
192            half_extents: [hx, hy],
193            radius,
194            aa,
195            invert: if invert { 1.0 } else { 0.0 },
196            shape_kind,
197        };
198        let buffer = app
199            .device()
200            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
201                label: Some("wisp::clip uniforms"),
202                contents: bytemuck::bytes_of(&uniforms),
203                usage: wgpu::BufferUsages::UNIFORM,
204            });
205
206        let bg = app.device().create_bind_group(&wgpu::BindGroupDescriptor {
207            label: Some("wisp::clip bg"),
208            layout: &self.bind_group_layout,
209            entries: &[
210                wgpu::BindGroupEntry {
211                    binding: 0,
212                    resource: wgpu::BindingResource::TextureView(foreground.view()),
213                },
214                wgpu::BindGroupEntry {
215                    binding: 1,
216                    resource: wgpu::BindingResource::Sampler(&self.sampler),
217                },
218                wgpu::BindGroupEntry {
219                    binding: 2,
220                    resource: buffer.as_entire_binding(),
221                },
222            ],
223        });
224
225        let mut encoder = app
226            .device()
227            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
228                label: Some("wisp::clip encoder"),
229            });
230        {
231            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
232                label: Some("wisp::clip pass"),
233                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
234                    view: output.view(),
235                    resolve_target: None,
236                    ops: wgpu::Operations {
237                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
238                        store: wgpu::StoreOp::Store,
239                    },
240                })],
241                depth_stencil_attachment: None,
242                timestamp_writes: None,
243                occlusion_query_set: None,
244            });
245            pass.set_pipeline(&self.pipeline);
246            pass.set_bind_group(0, &bg, &[]);
247            pass.draw(0..3, 0..1);
248        }
249        app.queue().submit(std::iter::once(encoder.finish()));
250    }
251}