ailloli_ui_render_wgpu/stencil.rs
1//! Shared depth/stencil texture for [`crate::clip::ClipRenderMode::Stencil`] clips.
2
3use crate::render_target::PhysicalExtent;
4
5/// GPU depth/stencil attachment sized to the window.
6///
7/// # Examples
8///
9/// ```
10/// use ailloli_ui_render_wgpu::stencil::StencilTarget;
11/// assert_eq!(StencilTarget::FORMAT, wgpu::TextureFormat::Depth24PlusStencil8);
12/// ```
13pub struct StencilTarget {
14 /// Owned attachment texture.
15 pub texture: wgpu::Texture,
16 /// Full view used by render passes.
17 pub view: wgpu::TextureView,
18 /// Actual allocation extent in physical pixels, with each axis at least one.
19 pub size: PhysicalExtent,
20 /// Attachment format, always [`Self::FORMAT`].
21 pub format: wgpu::TextureFormat,
22}
23
24impl StencilTarget {
25 /// Depth/stencil format expected by stencil-enabled pipelines.
26 ///
27 /// # Examples
28 ///
29 /// ```
30 /// use ailloli_ui_render_wgpu::stencil::StencilTarget;
31 /// assert_eq!(StencilTarget::FORMAT, wgpu::TextureFormat::Depth24PlusStencil8);
32 /// ```
33 pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24PlusStencil8;
34
35 /// Allocates a render attachment, clamping each axis to at least one pixel.
36 ///
37 /// # Examples
38 ///
39 /// ```no_run
40 /// use ailloli_ui_render_wgpu::stencil::StencilTarget;
41 /// fn create(device: &wgpu::Device) -> StencilTarget { StencilTarget::new(device, 0, 64) }
42 /// ```
43 pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
44 let width = width.max(1);
45 let height = height.max(1);
46 let texture = device.create_texture(&wgpu::TextureDescriptor {
47 label: Some("stencil target"),
48 size: wgpu::Extent3d {
49 width,
50 height,
51 depth_or_array_layers: 1,
52 },
53 mip_level_count: 1,
54 sample_count: 1,
55 dimension: wgpu::TextureDimension::D2,
56 format: Self::FORMAT,
57 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
58 view_formats: &[],
59 });
60 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
61 Self {
62 texture,
63 view,
64 size: PhysicalExtent::new(width, height),
65 format: Self::FORMAT,
66 }
67 }
68
69 /// Reallocates only when the clamped physical extent changes.
70 ///
71 /// # Examples
72 ///
73 /// ```no_run
74 /// use ailloli_ui_render_wgpu::stencil::StencilTarget;
75 /// fn resize(target: &mut StencilTarget, device: &wgpu::Device) {
76 /// target.recreate(device, 128, 96);
77 /// }
78 /// ```
79 pub fn recreate(&mut self, device: &wgpu::Device, width: u32, height: u32) {
80 if self.size.width == width.max(1) && self.size.height == height.max(1) {
81 return;
82 }
83 *self = Self::new(device, width, height);
84 }
85}
86
87/// Per-frame stencil state (global clear + reference value per layer).
88///
89/// # Examples
90///
91/// ```
92/// use ailloli_ui_render_wgpu::stencil::StencilFrameState;
93/// let state = StencilFrameState::default();
94/// assert_eq!((state.cleared, state.next_ref), (false, 0));
95/// ```
96#[derive(Debug, Default)]
97pub struct StencilFrameState {
98 /// Whether the attachment has been cleared during this reference cycle.
99 pub cleared: bool,
100 /// Last stencil reference allocated, in the range `0..=255`.
101 pub next_ref: u32,
102}
103
104impl StencilFrameState {
105 /// Allocates the next nonzero eight-bit reference and required load op.
106 ///
107 /// The first layer clears to zero and returns reference 1. Subsequent layers
108 /// load and increment through 255. The next request clears again and wraps
109 /// to reference 1.
110 ///
111 /// # Examples
112 ///
113 /// ```
114 /// use ailloli_ui_render_wgpu::stencil::StencilFrameState;
115 /// let mut state = StencilFrameState::default();
116 /// let (load, reference) = state.begin_layer();
117 /// assert!(matches!(load, wgpu::LoadOp::Clear(0)));
118 /// assert_eq!(reference, 1);
119 /// ```
120 pub fn begin_layer(&mut self) -> (wgpu::LoadOp<u32>, u32) {
121 let load = if self.cleared {
122 wgpu::LoadOp::Load
123 } else {
124 self.cleared = true;
125 wgpu::LoadOp::Clear(0)
126 };
127 let ref_val = self.next_ref.saturating_add(1);
128 if ref_val > 255 {
129 self.cleared = false;
130 self.next_ref = 1;
131 return (wgpu::LoadOp::Clear(0), 1);
132 }
133 self.next_ref = ref_val;
134 (load, ref_val)
135 }
136}
137
138#[cfg(test)]
139/// Verifies stencil target reuse and recreation across physical resizes.
140mod tests {
141 use super::*;
142
143 #[test]
144 fn stencil_target_recreate_on_size_change() {
145 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::default());
146 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
147 power_preference: wgpu::PowerPreference::LowPower,
148 compatible_surface: None,
149 force_fallback_adapter: false,
150 }))
151 .expect("adapter");
152 let (device, _queue) = pollster::block_on(adapter.request_device(
153 &wgpu::DeviceDescriptor {
154 label: None,
155 required_features: wgpu::Features::empty(),
156 required_limits: wgpu::Limits::default(),
157 },
158 None,
159 ))
160 .expect("device");
161
162 let mut target = StencilTarget::new(&device, 64, 64);
163 assert_eq!(target.size.width, 64);
164 target.recreate(&device, 128, 96);
165 assert_eq!(target.size.width, 128);
166 assert_eq!(target.size.height, 96);
167 }
168}