1use oxiui_core::UiError;
27
28use crate::gpu::device::TARGET_FORMAT;
29
30pub const DEPTH_STENCIL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24PlusStencil8;
32
33pub struct StencilTarget {
40 pub texture: wgpu::Texture,
42 pub view: wgpu::TextureView,
44 pub width: u32,
46 pub height: u32,
48 pub sample_count: u32,
50}
51
52impl StencilTarget {
53 pub fn new(
60 device: &wgpu::Device,
61 width: u32,
62 height: u32,
63 sample_count: u32,
64 ) -> Result<Self, UiError> {
65 if width == 0 || height == 0 {
66 return Err(UiError::Unsupported(
67 "StencilTarget dimensions must be non-zero".to_string(),
68 ));
69 }
70 let sc = sample_count.max(1);
71 let texture = device.create_texture(&wgpu::TextureDescriptor {
72 label: Some("oxiui-render-wgpu stencil target"),
73 size: wgpu::Extent3d {
74 width,
75 height,
76 depth_or_array_layers: 1,
77 },
78 mip_level_count: 1,
79 sample_count: sc,
80 dimension: wgpu::TextureDimension::D2,
81 format: DEPTH_STENCIL_FORMAT,
82 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
83 view_formats: &[],
84 });
85 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
86 Ok(Self {
87 texture,
88 view,
89 width,
90 height,
91 sample_count: sc,
92 })
93 }
94
95 pub fn resize(
99 &mut self,
100 device: &wgpu::Device,
101 new_width: u32,
102 new_height: u32,
103 ) -> Result<(), UiError> {
104 *self = Self::new(device, new_width, new_height, self.sample_count)?;
105 Ok(())
106 }
107}
108
109pub struct StencilWritePipeline {
121 pub write: wgpu::RenderPipeline,
123 pub clear: wgpu::RenderPipeline,
125 pub globals_layout: wgpu::BindGroupLayout,
127}
128
129impl StencilWritePipeline {
130 pub fn new(device: &wgpu::Device, sample_count: u32) -> Self {
134 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
136 label: Some("oxiui-render-wgpu stencil solid shader"),
137 source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/solid.wgsl").into()),
138 });
139
140 let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
141 label: Some("oxiui-render-wgpu stencil globals layout"),
142 entries: &[wgpu::BindGroupLayoutEntry {
143 binding: 0,
144 visibility: wgpu::ShaderStages::VERTEX,
145 ty: wgpu::BindingType::Buffer {
146 ty: wgpu::BufferBindingType::Uniform,
147 has_dynamic_offset: false,
148 min_binding_size: None,
149 },
150 count: None,
151 }],
152 });
153
154 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
155 label: Some("oxiui-render-wgpu stencil pipeline layout"),
156 bind_group_layouts: &[Some(&globals_layout)],
157 immediate_size: 0,
158 });
159
160 let write_stencil_face = wgpu::StencilFaceState {
162 compare: wgpu::CompareFunction::Always,
163 fail_op: wgpu::StencilOperation::Keep,
164 depth_fail_op: wgpu::StencilOperation::Keep,
165 pass_op: wgpu::StencilOperation::Replace,
166 };
167
168 let attrs = vertex_attrs_56();
170 let vertex_layout = wgpu::VertexBufferLayout {
171 array_stride: 56,
172 step_mode: wgpu::VertexStepMode::Vertex,
173 attributes: &attrs,
174 };
175
176 let write = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
177 label: Some("oxiui-render-wgpu stencil write pipeline"),
178 layout: Some(&pipeline_layout),
179 vertex: wgpu::VertexState {
180 module: &shader,
181 entry_point: Some("vs_main"),
182 buffers: std::slice::from_ref(&vertex_layout),
183 compilation_options: wgpu::PipelineCompilationOptions::default(),
184 },
185 fragment: Some(wgpu::FragmentState {
186 module: &shader,
187 entry_point: Some("fs_main"),
188 targets: &[Some(wgpu::ColorTargetState {
189 format: TARGET_FORMAT,
190 blend: None,
191 write_mask: wgpu::ColorWrites::empty(), })],
193 compilation_options: wgpu::PipelineCompilationOptions::default(),
194 }),
195 primitive: wgpu::PrimitiveState {
196 topology: wgpu::PrimitiveTopology::TriangleList,
197 strip_index_format: None,
198 front_face: wgpu::FrontFace::Ccw,
199 cull_mode: None,
200 unclipped_depth: false,
201 polygon_mode: wgpu::PolygonMode::Fill,
202 conservative: false,
203 },
204 depth_stencil: Some(wgpu::DepthStencilState {
205 format: DEPTH_STENCIL_FORMAT,
206 depth_write_enabled: Some(false),
207 depth_compare: Some(wgpu::CompareFunction::Always),
208 stencil: wgpu::StencilState {
209 front: write_stencil_face,
210 back: write_stencil_face,
211 read_mask: 0xFF,
212 write_mask: 0xFF,
213 },
214 bias: wgpu::DepthBiasState::default(),
215 }),
216 multisample: wgpu::MultisampleState {
217 count: sample_count,
218 mask: !0,
219 alpha_to_coverage_enabled: false,
220 },
221 multiview_mask: None,
222 cache: None,
223 });
224
225 let clear_stencil_face = wgpu::StencilFaceState {
227 compare: wgpu::CompareFunction::Always,
228 fail_op: wgpu::StencilOperation::Zero,
229 depth_fail_op: wgpu::StencilOperation::Zero,
230 pass_op: wgpu::StencilOperation::Zero,
231 };
232
233 let clear = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
234 label: Some("oxiui-render-wgpu stencil clear pipeline"),
235 layout: Some(&pipeline_layout),
236 vertex: wgpu::VertexState {
237 module: &shader,
238 entry_point: Some("vs_main"),
239 buffers: &[vertex_layout],
240 compilation_options: wgpu::PipelineCompilationOptions::default(),
241 },
242 fragment: Some(wgpu::FragmentState {
243 module: &shader,
244 entry_point: Some("fs_main"),
245 targets: &[Some(wgpu::ColorTargetState {
246 format: TARGET_FORMAT,
247 blend: None,
248 write_mask: wgpu::ColorWrites::empty(),
249 })],
250 compilation_options: wgpu::PipelineCompilationOptions::default(),
251 }),
252 primitive: wgpu::PrimitiveState {
253 topology: wgpu::PrimitiveTopology::TriangleList,
254 strip_index_format: None,
255 front_face: wgpu::FrontFace::Ccw,
256 cull_mode: None,
257 unclipped_depth: false,
258 polygon_mode: wgpu::PolygonMode::Fill,
259 conservative: false,
260 },
261 depth_stencil: Some(wgpu::DepthStencilState {
262 format: DEPTH_STENCIL_FORMAT,
263 depth_write_enabled: Some(false),
264 depth_compare: Some(wgpu::CompareFunction::Always),
265 stencil: wgpu::StencilState {
266 front: clear_stencil_face,
267 back: clear_stencil_face,
268 read_mask: 0xFF,
269 write_mask: 0xFF,
270 },
271 bias: wgpu::DepthBiasState::default(),
272 }),
273 multisample: wgpu::MultisampleState {
274 count: sample_count,
275 mask: !0,
276 alpha_to_coverage_enabled: false,
277 },
278 multiview_mask: None,
279 cache: None,
280 });
281
282 Self {
283 write,
284 clear,
285 globals_layout,
286 }
287 }
288}
289
290fn vertex_attrs_56() -> [wgpu::VertexAttribute; 7] {
292 [
293 wgpu::VertexAttribute {
294 format: wgpu::VertexFormat::Float32x2,
295 offset: 0,
296 shader_location: 0,
297 },
298 wgpu::VertexAttribute {
299 format: wgpu::VertexFormat::Float32x4,
300 offset: 8,
301 shader_location: 1,
302 },
303 wgpu::VertexAttribute {
304 format: wgpu::VertexFormat::Float32x2,
305 offset: 24,
306 shader_location: 2,
307 },
308 wgpu::VertexAttribute {
309 format: wgpu::VertexFormat::Float32x2,
310 offset: 32,
311 shader_location: 3,
312 },
313 wgpu::VertexAttribute {
314 format: wgpu::VertexFormat::Float32,
315 offset: 40,
316 shader_location: 4,
317 },
318 wgpu::VertexAttribute {
319 format: wgpu::VertexFormat::Float32,
320 offset: 44,
321 shader_location: 5,
322 },
323 wgpu::VertexAttribute {
324 format: wgpu::VertexFormat::Float32x2,
325 offset: 48,
326 shader_location: 6,
327 },
328 ]
329}
330
331#[derive(Clone, Debug, Default)]
338pub struct StencilClipState {
339 depth: u8,
340}
341
342impl StencilClipState {
343 pub fn reference(&self) -> u32 {
346 u32::from(self.depth)
347 }
348
349 pub fn push(&mut self) -> Option<u32> {
353 if self.depth >= 254 {
354 return None;
355 }
356 self.depth += 1;
357 Some(u32::from(self.depth))
358 }
359
360 pub fn pop(&mut self) {
362 self.depth = self.depth.saturating_sub(1);
363 }
364
365 pub fn reset(&mut self) {
367 self.depth = 0;
368 }
369}
370
371#[cfg(test)]
374mod tests {
375 use super::*;
376 use oxiui_core::UiError;
377
378 fn try_device() -> Option<(wgpu::Device, wgpu::Queue)> {
379 let instance = wgpu::Instance::default();
380 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
381 power_preference: wgpu::PowerPreference::default(),
382 force_fallback_adapter: false,
383 compatible_surface: None,
384 }))
385 .ok()?;
386 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
387 label: Some("stencil test device"),
388 required_features: wgpu::Features::empty(),
389 required_limits: wgpu::Limits::downlevel_defaults(),
390 memory_hints: wgpu::MemoryHints::Performance,
391 experimental_features: wgpu::ExperimentalFeatures::disabled(),
392 trace: wgpu::Trace::Off,
393 }))
394 .ok()
395 }
396
397 #[test]
398 fn stencil_clip_state_push_pop() {
399 let mut s = StencilClipState::default();
400 assert_eq!(s.reference(), 0);
401 let r = s.push().expect("first push");
402 assert_eq!(r, 1);
403 assert_eq!(s.reference(), 1);
404 s.pop();
405 assert_eq!(s.reference(), 0);
406 }
407
408 #[test]
409 fn stencil_clip_state_max_depth() {
410 let mut s = StencilClipState::default();
411 for _ in 0..254 {
412 assert!(s.push().is_some());
413 }
414 assert_eq!(s.reference(), 254);
415 assert!(s.push().is_none(), "should not exceed 254");
416 }
417
418 #[test]
419 fn stencil_clip_state_pop_at_zero_is_safe() {
420 let mut s = StencilClipState::default();
421 s.pop(); assert_eq!(s.reference(), 0);
423 }
424
425 #[test]
426 fn stencil_target_zero_dimensions_fail() {
427 let Some((device, _)) = try_device() else {
428 return;
429 };
430 assert!(matches!(
431 StencilTarget::new(&device, 0, 64, 1),
432 Err(UiError::Unsupported(_))
433 ));
434 }
435
436 #[test]
437 fn stencil_target_creates_ok() {
438 let Some((device, _)) = try_device() else {
439 return;
440 };
441 let st = StencilTarget::new(&device, 64, 64, 1).expect("create stencil target");
442 assert_eq!(st.width, 64);
443 assert_eq!(st.height, 64);
444 assert_eq!(st.sample_count, 1);
445 }
446
447 #[test]
448 fn stencil_write_pipeline_compiles() {
449 let Some((device, _)) = try_device() else {
450 return;
451 };
452 let _pipeline = StencilWritePipeline::new(&device, 1);
454 }
455
456 #[test]
457 fn stencil_format_is_depth24plus_stencil8() {
458 assert_eq!(
459 DEPTH_STENCIL_FORMAT,
460 wgpu::TextureFormat::Depth24PlusStencil8
461 );
462 }
463}