Skip to main content

pax_gpu/render_backend/
stencil.rs

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