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
47pub struct ClipDraw<'a> {
49 pub clip_id: u32,
50 pub geometry_signature: u64,
51 pub geometry: &'a VertexBuffers<Vertex, u16>,
52}
53
54pub 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)]
83pub struct Vertex {
85 pub position: [f32; 2],
86}
87
88impl StencilRenderer {
89 #[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 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 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}