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}