Skip to main content

pax_gpu/render_backend/
stencil.rs

1use bytemuck::{Pod, Zeroable};
2use lyon::tessellation::VertexBuffers;
3use std::collections::{HashMap, HashSet};
4use std::rc::Rc;
5use wgpu::util::DeviceExt;
6use wgpu::{BufferUsages, Device, Queue, RenderPipeline};
7
8fn padded_u16_buffer(data: &[u16]) -> Vec<u16> {
9    if data.len() % 2 == 0 {
10        return data.to_vec();
11    }
12
13    let mut padded = Vec::with_capacity(data.len() + 1);
14    padded.extend_from_slice(data);
15    padded.push(0);
16    padded
17}
18
19fn has_drawable_stencil_geometry(index_count: u32) -> bool {
20    index_count > 0
21}
22
23struct CachedStencilGeometry {
24    vertices_buffer: wgpu::Buffer,
25    indices_buffer: wgpu::Buffer,
26    index_count: u32,
27}
28
29#[derive(Clone, Copy, Debug)]
30struct StencilStackEntry {
31    clip_id: u32,
32    geometry_signature: u64,
33}
34
35#[derive(Default)]
36pub(crate) struct PreparedStencilSync {
37    popped: Vec<StencilStackEntry>,
38    pushed: Vec<StencilStackEntry>,
39}
40
41impl PreparedStencilSync {
42    pub(crate) fn is_empty(&self) -> bool {
43        self.popped.is_empty() && self.pushed.is_empty()
44    }
45}
46
47/// One clip geometry instance to draw into the stencil buffer.
48pub struct ClipDraw<'a> {
49    pub clip_id: u32,
50    pub geometry_signature: u64,
51    pub geometry: &'a VertexBuffers<Vertex, u16>,
52}
53
54/// Maintains the stencil stack used to render nested vector clips.
55pub struct StencilRenderer {
56    stencil_pipeline: Rc<RenderPipeline>,
57    decrement_pipeline: Rc<RenderPipeline>,
58    color_stencil_pipeline: Rc<RenderPipeline>,
59    color_decrement_pipeline: Rc<RenderPipeline>,
60    stencil_texture: wgpu::Texture,
61    stencil_view: wgpu::TextureView,
62    stencil_layer: u32,
63    stencil_geometry_stack: Vec<StencilStackEntry>,
64    cached_geometry: HashMap<u64, CachedStencilGeometry>,
65    cached_clip_instances: HashMap<u32, wgpu::Buffer>,
66    width: u32,
67    height: u32,
68    sample_count: u32,
69    stencil_bind_group: wgpu::BindGroup,
70}
71
72#[derive(Clone)]
73pub(crate) struct StencilPipelineResources {
74    stencil_pipeline: Rc<RenderPipeline>,
75    decrement_pipeline: Rc<RenderPipeline>,
76    color_stencil_pipeline: Rc<RenderPipeline>,
77    color_decrement_pipeline: Rc<RenderPipeline>,
78    stencil_bind_group_layout: Rc<wgpu::BindGroupLayout>,
79}
80
81#[repr(C)]
82#[derive(Copy, Clone, Debug, Pod, Zeroable)]
83/// Tessellated stencil vertex.
84pub struct Vertex {
85    pub position: [f32; 2],
86}
87
88impl StencilRenderer {
89    /// Create the stencil pipelines and backing texture.
90    #[allow(dead_code)]
91    pub fn new(
92        device: &Device,
93        width: u32,
94        height: u32,
95        sample_count: u32,
96        color_format: wgpu::TextureFormat,
97        globals: &wgpu::Buffer,
98        clip_transforms: &wgpu::Buffer,
99    ) -> Self {
100        let resources = Self::create_pipeline_resources(device, sample_count, color_format);
101        Self::with_pipeline_resources(
102            device,
103            width,
104            height,
105            sample_count,
106            globals,
107            clip_transforms,
108            resources,
109        )
110    }
111
112    pub(crate) fn with_pipeline_resources(
113        device: &Device,
114        width: u32,
115        height: u32,
116        sample_count: u32,
117        globals: &wgpu::Buffer,
118        clip_transforms: &wgpu::Buffer,
119        resources: StencilPipelineResources,
120    ) -> Self {
121        let stencil_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
122            layout: resources.stencil_bind_group_layout.as_ref(),
123            entries: &[
124                wgpu::BindGroupEntry {
125                    binding: 0,
126                    resource: globals.as_entire_binding(),
127                },
128                wgpu::BindGroupEntry {
129                    binding: 1,
130                    resource: clip_transforms.as_entire_binding(),
131                },
132            ],
133            label: Some("stencil_bind_group"),
134        });
135        let (stencil_texture, stencil_view) =
136            Self::create_stencil_texture(device, width, height, sample_count);
137
138        Self {
139            stencil_pipeline: resources.stencil_pipeline,
140            decrement_pipeline: resources.decrement_pipeline,
141            color_stencil_pipeline: resources.color_stencil_pipeline,
142            color_decrement_pipeline: resources.color_decrement_pipeline,
143            stencil_texture,
144            stencil_view,
145            width,
146            height,
147            sample_count,
148            stencil_layer: 0,
149            stencil_geometry_stack: vec![],
150            cached_geometry: HashMap::new(),
151            cached_clip_instances: HashMap::new(),
152            stencil_bind_group,
153        }
154    }
155
156    pub(crate) fn create_pipeline_resources(
157        device: &Device,
158        sample_count: u32,
159        color_format: wgpu::TextureFormat,
160    ) -> StencilPipelineResources {
161        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
162            label: Some("Stencil Shader"),
163            source: wgpu::ShaderSource::Wgsl(include_str!("stencil.wgsl").into()),
164        });
165
166        let stencil_bind_group_layout =
167            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
168                entries: &[
169                    wgpu::BindGroupLayoutEntry {
170                        binding: 0,
171                        visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
172                        ty: wgpu::BindingType::Buffer {
173                            ty: wgpu::BufferBindingType::Uniform,
174                            has_dynamic_offset: false,
175                            min_binding_size: None,
176                        },
177                        count: None,
178                    },
179                    wgpu::BindGroupLayoutEntry {
180                        binding: 1,
181                        visibility: wgpu::ShaderStages::VERTEX,
182                        ty: wgpu::BindingType::Buffer {
183                            ty: wgpu::BufferBindingType::Uniform,
184                            has_dynamic_offset: false,
185                            min_binding_size: None,
186                        },
187                        count: None,
188                    },
189                ],
190                label: Some("stencil_bind_group_layout"),
191            });
192
193        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
194            label: Some("Stencil Pipeline Layout"),
195            bind_group_layouts: &[&stencil_bind_group_layout],
196            immediate_size: 0,
197        });
198
199        fn create_pipeline(
200            device: &Device,
201            shader: &wgpu::ShaderModule,
202            pipeline_layout: &wgpu::PipelineLayout,
203            label: &'static str,
204            sample_count: u32,
205            color_target: Option<wgpu::TextureFormat>,
206            stencil_op: wgpu::StencilOperation,
207        ) -> RenderPipeline {
208            let color_targets: Vec<Option<wgpu::ColorTargetState>> = match color_target {
209                Some(format) => vec![Some(wgpu::ColorTargetState {
210                    format,
211                    blend: None,
212                    write_mask: wgpu::ColorWrites::empty(),
213                })],
214                None => vec![None],
215            };
216
217            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
218                label: Some(label),
219                layout: Some(pipeline_layout),
220                vertex: wgpu::VertexState {
221                    module: shader,
222                    entry_point: Some("vs_main"),
223                    buffers: &[
224                        wgpu::VertexBufferLayout {
225                            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
226                            step_mode: wgpu::VertexStepMode::Vertex,
227                            attributes: &wgpu::vertex_attr_array![0 => Float32x2],
228                        },
229                        wgpu::VertexBufferLayout {
230                            array_stride: std::mem::size_of::<u32>() as wgpu::BufferAddress,
231                            step_mode: wgpu::VertexStepMode::Instance,
232                            attributes: &wgpu::vertex_attr_array![1 => Uint32],
233                        },
234                    ],
235                    compilation_options: Default::default(),
236                },
237                fragment: Some(wgpu::FragmentState {
238                    module: shader,
239                    entry_point: Some("fs_main"),
240                    targets: &color_targets,
241                    compilation_options: Default::default(),
242                }),
243                primitive: wgpu::PrimitiveState {
244                    topology: wgpu::PrimitiveTopology::TriangleList,
245                    strip_index_format: None,
246                    front_face: wgpu::FrontFace::Ccw,
247                    cull_mode: None,
248                    polygon_mode: wgpu::PolygonMode::Fill,
249                    unclipped_depth: false,
250                    conservative: false,
251                },
252                depth_stencil: Some(wgpu::DepthStencilState {
253                    format: wgpu::TextureFormat::Stencil8,
254                    depth_write_enabled: false,
255                    depth_compare: wgpu::CompareFunction::Always,
256                    stencil: wgpu::StencilState {
257                        front: wgpu::StencilFaceState {
258                            compare: wgpu::CompareFunction::Always,
259                            fail_op: wgpu::StencilOperation::Keep,
260                            depth_fail_op: wgpu::StencilOperation::Keep,
261                            pass_op: stencil_op,
262                        },
263                        back: wgpu::StencilFaceState::IGNORE,
264                        read_mask: !0,
265                        write_mask: !0,
266                    },
267                    bias: Default::default(),
268                }),
269                multisample: wgpu::MultisampleState {
270                    count: sample_count,
271                    ..Default::default()
272                },
273                multiview_mask: None,
274                cache: None,
275            })
276        }
277
278        // Main stencil pipeline for incrementing
279        let stencil_pipeline = create_pipeline(
280            device,
281            &shader,
282            &pipeline_layout,
283            "Stencil Pipeline",
284            sample_count,
285            None,
286            wgpu::StencilOperation::IncrementClamp,
287        );
288        let color_stencil_pipeline = create_pipeline(
289            device,
290            &shader,
291            &pipeline_layout,
292            "Color Stencil Pipeline",
293            sample_count,
294            Some(color_format),
295            wgpu::StencilOperation::IncrementClamp,
296        );
297        let decrement_pipeline = create_pipeline(
298            device,
299            &shader,
300            &pipeline_layout,
301            "Decrement Pipeline",
302            sample_count,
303            None,
304            wgpu::StencilOperation::DecrementClamp,
305        );
306        let color_decrement_pipeline = create_pipeline(
307            device,
308            &shader,
309            &pipeline_layout,
310            "Color Decrement Pipeline",
311            sample_count,
312            Some(color_format),
313            wgpu::StencilOperation::DecrementClamp,
314        );
315
316        StencilPipelineResources {
317            stencil_pipeline: Rc::new(stencil_pipeline),
318            decrement_pipeline: Rc::new(decrement_pipeline),
319            color_stencil_pipeline: Rc::new(color_stencil_pipeline),
320            color_decrement_pipeline: Rc::new(color_decrement_pipeline),
321            stencil_bind_group_layout: Rc::new(stencil_bind_group_layout),
322        }
323    }
324
325    fn create_stencil_texture(
326        device: &Device,
327        width: u32,
328        height: u32,
329        sample_count: u32,
330    ) -> (wgpu::Texture, wgpu::TextureView) {
331        let texture = device.create_texture(&wgpu::TextureDescriptor {
332            label: Some("Stencil Texture"),
333            size: wgpu::Extent3d {
334                width,
335                height,
336                depth_or_array_layers: 1,
337            },
338            mip_level_count: 1,
339            sample_count,
340            dimension: wgpu::TextureDimension::D2,
341            format: wgpu::TextureFormat::Stencil8,
342            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
343            view_formats: &[],
344        });
345
346        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
347        (texture, view)
348    }
349
350    pub fn resize(&mut self, device: &Device, width: u32, height: u32) {
351        if width == self.width && height == self.height {
352            return;
353        }
354
355        (self.stencil_texture, self.stencil_view) =
356            Self::create_stencil_texture(device, width, height, self.sample_count);
357        self.width = width;
358        self.height = height;
359    }
360
361    fn create_cached_geometry(
362        device: &Device,
363        geometry: &VertexBuffers<Vertex, u16>,
364    ) -> CachedStencilGeometry {
365        let vertices_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
366            label: Some("Stencil Vertices"),
367            contents: bytemuck::cast_slice(&geometry.vertices),
368            usage: BufferUsages::VERTEX,
369        });
370        let padded_indices = padded_u16_buffer(&geometry.indices);
371        let indices_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
372            label: Some("Stencil Indices"),
373            contents: bytemuck::cast_slice(&padded_indices),
374            usage: BufferUsages::INDEX,
375        });
376
377        CachedStencilGeometry {
378            vertices_buffer,
379            indices_buffer,
380            index_count: geometry.indices.len() as u32,
381        }
382    }
383
384    fn ensure_cached_geometry(
385        &mut self,
386        device: &Device,
387        signature: u64,
388        geometry: &VertexBuffers<Vertex, u16>,
389    ) {
390        self.cached_geometry
391            .entry(signature)
392            .or_insert_with(|| Self::create_cached_geometry(device, geometry));
393    }
394
395    fn ensure_cached_clip_instance(&mut self, device: &Device, clip_id: u32) {
396        self.cached_clip_instances
397            .entry(clip_id)
398            .or_insert_with(|| {
399                device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
400                    label: Some("Stencil Clip Instance Buffer"),
401                    contents: bytemuck::bytes_of(&clip_id),
402                    usage: BufferUsages::VERTEX,
403                })
404            });
405    }
406
407    pub fn sync_stencil_stack(
408        &mut self,
409        device: &Device,
410        queue: &Queue,
411        depth: u32,
412        clips: &[ClipDraw<'_>],
413    ) {
414        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
415            label: Some("Stencil Sync Encoder"),
416        });
417        self.encode_stencil_stack_sync(device, &mut encoder, depth, clips);
418        queue.submit(std::iter::once(encoder.finish()));
419    }
420
421    pub fn encode_stencil_stack_sync(
422        &mut self,
423        device: &Device,
424        encoder: &mut wgpu::CommandEncoder,
425        depth: u32,
426        clips: &[ClipDraw<'_>],
427    ) {
428        let prepared = self.prepare_stencil_stack_sync(device, depth, clips);
429        if prepared.is_empty() {
430            return;
431        }
432
433        {
434            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
435                label: Some("Stencil Sync Pass"),
436                color_attachments: &[None],
437                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
438                    view: &self.stencil_view,
439                    depth_ops: None,
440                    stencil_ops: Some(wgpu::Operations {
441                        load: wgpu::LoadOp::Load,
442                        store: wgpu::StoreOp::Store,
443                    }),
444                }),
445                timestamp_writes: None,
446                occlusion_query_set: None,
447                multiview_mask: None,
448            });
449            self.encode_prepared_stencil_sync(&mut render_pass, &prepared, false);
450        }
451    }
452
453    pub(crate) fn prepare_stencil_stack_sync(
454        &mut self,
455        device: &Device,
456        depth: u32,
457        clips: &[ClipDraw<'_>],
458    ) -> PreparedStencilSync {
459        let mut prepared = PreparedStencilSync::default();
460        while self.stencil_layer > depth {
461            let Some(entry) = self.stencil_geometry_stack.pop() else {
462                log::error!("geometry stack shouldn't be embty when stencil layer > 0");
463                break;
464            };
465            prepared.popped.push(entry);
466            self.stencil_layer = self.stencil_layer.saturating_sub(1);
467        }
468
469        for clip in clips {
470            self.ensure_cached_geometry(device, clip.geometry_signature, clip.geometry);
471            self.ensure_cached_clip_instance(device, clip.clip_id);
472        }
473
474        for clip in clips {
475            let entry = StencilStackEntry {
476                clip_id: clip.clip_id,
477                geometry_signature: clip.geometry_signature,
478            };
479            self.stencil_geometry_stack.push(entry);
480            self.stencil_layer += 1;
481            prepared.pushed.push(entry);
482        }
483
484        prepared
485    }
486
487    pub(crate) fn encode_prepared_stencil_sync<'pass>(
488        &'pass self,
489        render_pass: &mut wgpu::RenderPass<'pass>,
490        prepared: &'pass PreparedStencilSync,
491        has_color_attachment: bool,
492    ) {
493        if prepared.is_empty() {
494            return;
495        }
496
497        render_pass.set_bind_group(0, &self.stencil_bind_group, &[]);
498
499        if !prepared.popped.is_empty() {
500            let pipeline = if has_color_attachment {
501                self.color_decrement_pipeline.as_ref()
502            } else {
503                self.decrement_pipeline.as_ref()
504            };
505            render_pass.set_pipeline(pipeline);
506            for entry in &prepared.popped {
507                self.draw_stencil_entry(render_pass, entry);
508            }
509        }
510
511        if !prepared.pushed.is_empty() {
512            let pipeline = if has_color_attachment {
513                self.color_stencil_pipeline.as_ref()
514            } else {
515                self.stencil_pipeline.as_ref()
516            };
517            render_pass.set_pipeline(pipeline);
518            for entry in &prepared.pushed {
519                self.draw_stencil_entry(render_pass, entry);
520            }
521        }
522    }
523
524    fn draw_stencil_entry<'pass>(
525        &'pass self,
526        render_pass: &mut wgpu::RenderPass<'pass>,
527        entry: &StencilStackEntry,
528    ) {
529        let Some(cached_geometry) = self.cached_geometry.get(&entry.geometry_signature) else {
530            log::error!(
531                "missing cached stencil geometry for signature {}",
532                entry.geometry_signature
533            );
534            return;
535        };
536        // A reactive mask can legitimately pass through an empty/tiny path
537        // while its geometry is being inserted or animated. WGPU rejects
538        // slices of the zero-length buffers cached for that geometry, and a
539        // zero-index stencil draw has no effect anyway.
540        if !has_drawable_stencil_geometry(cached_geometry.index_count) {
541            return;
542        }
543        let Some(instance_buffer) = self.cached_clip_instances.get(&entry.clip_id) else {
544            log::error!(
545                "missing cached stencil clip instance for clip {}",
546                entry.clip_id
547            );
548            return;
549        };
550        render_pass.set_vertex_buffer(0, cached_geometry.vertices_buffer.slice(..));
551        render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
552        render_pass.set_index_buffer(
553            cached_geometry.indices_buffer.slice(..),
554            wgpu::IndexFormat::Uint16,
555        );
556        render_pass.draw_indexed(0..cached_geometry.index_count, 0, 0..1);
557    }
558
559    pub fn needs_stack_sync(&self, depth: u32, clips: &[ClipDraw<'_>]) -> bool {
560        self.stencil_layer > depth || !clips.is_empty()
561    }
562
563    pub fn retain_cached_resources(
564        &mut self,
565        active_signatures: &HashSet<u64>,
566        active_clip_ids: &HashSet<u32>,
567    ) {
568        let stack_signatures: HashSet<u64> = self
569            .stencil_geometry_stack
570            .iter()
571            .map(|entry| entry.geometry_signature)
572            .collect();
573        self.cached_geometry.retain(|signature, _| {
574            active_signatures.contains(signature) || stack_signatures.contains(signature)
575        });
576        let stack_clip_ids: HashSet<u32> = self
577            .stencil_geometry_stack
578            .iter()
579            .map(|entry| entry.clip_id)
580            .collect();
581        self.cached_clip_instances.retain(|clip_id, _| {
582            active_clip_ids.contains(clip_id) || stack_clip_ids.contains(clip_id)
583        });
584    }
585
586    pub fn get_stencil(&self) -> (&wgpu::TextureView, u32) {
587        (&self.stencil_view, self.stencil_layer)
588    }
589
590    pub(crate) fn stencil_view_clone(&self) -> wgpu::TextureView {
591        self.stencil_view.clone()
592    }
593
594    pub fn clear(&mut self, device: &Device, queue: &Queue) {
595        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
596            label: Some("Stencil Clear Encoder"),
597        });
598        self.encode_clear(&mut encoder);
599        queue.submit(std::iter::once(encoder.finish()));
600    }
601
602    pub fn encode_clear(&mut self, encoder: &mut wgpu::CommandEncoder) {
603        {
604            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
605                label: Some("Stencil Clear Pass"),
606                color_attachments: &[None],
607                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
608                    view: &self.stencil_view,
609                    depth_ops: None,
610                    stencil_ops: Some(wgpu::Operations {
611                        load: wgpu::LoadOp::Clear(0),
612                        store: wgpu::StoreOp::Store,
613                    }),
614                }),
615                timestamp_writes: None,
616                occlusion_query_set: None,
617                multiview_mask: None,
618            });
619
620            render_pass.set_pipeline(self.stencil_pipeline.as_ref());
621        }
622
623        self.stencil_layer = 0;
624        self.stencil_geometry_stack.clear();
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    #[test]
633    fn empty_stencil_geometry_is_not_submitted_to_wgpu() {
634        assert!(!has_drawable_stencil_geometry(0));
635        assert!(has_drawable_stencil_geometry(3));
636    }
637}