ailloli_ui_render_wgpu/
offscreen_pool.rs

1//! Reusable offscreen color / stencil textures (isolated compositor).
2
3use std::cell::Cell;
4
5use crate::stencil::StencilTarget;
6
7/// Exact allocation class for a reusable offscreen slot.
8///
9/// # Examples
10///
11/// ```
12/// use ailloli_ui_render_wgpu::offscreen_pool::PoolKey;
13/// let key = PoolKey::color(0, 20, true);
14/// assert_eq!((key.width, key.height, key.needs_stencil, key.ping), (1, 20, true, false));
15/// ```
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct PoolKey {
18    /// Allocation width in physical pixels; constructors clamp it to at least one.
19    pub width: u32,
20    /// Allocation height in physical pixels; constructors clamp it to at least one.
21    pub height: u32,
22    /// Whether the slot owns a depth/stencil attachment.
23    pub needs_stencil: bool,
24    /// `false` = primary offscreen color ; `true` = ping-pong buffer for blur.
25    pub ping: bool,
26}
27
28impl PoolKey {
29    /// Creates a primary color-slot key with optional stencil.
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use ailloli_ui_render_wgpu::offscreen_pool::PoolKey;
35    /// let key = PoolKey::color(64, 32, false);
36    /// assert_eq!((key.width, key.height, key.ping), (64, 32, false));
37    /// ```
38    pub fn color(width: u32, height: u32, needs_stencil: bool) -> Self {
39        let width = width.max(1);
40        let height = height.max(1);
41        Self {
42            width,
43            height,
44            needs_stencil,
45            ping: false,
46        }
47    }
48
49    /// Creates a color-only ping-pong key for effects.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use ailloli_ui_render_wgpu::offscreen_pool::PoolKey;
55    /// let key = PoolKey::ping_pong(8, 0);
56    /// assert_eq!((key.height, key.needs_stencil, key.ping), (1, false, true));
57    /// ```
58    pub fn ping_pong(width: u32, height: u32) -> Self {
59        Self {
60            width: width.max(1),
61            height: height.max(1),
62            needs_stencil: false,
63            ping: true,
64        }
65    }
66}
67
68/// One owned allocation and its frame-local in-use flag.
69struct PoolSlot {
70    /// Exact extent/format/sample key required for reuse.
71    key: PoolKey,
72    #[allow(dead_code)]
73    /// Owned offscreen color texture.
74    color_texture: wgpu::Texture,
75    /// Default view spanning the full color texture.
76    color_view: wgpu::TextureView,
77    /// Optional depth-stencil attachment matching the color extent.
78    stencil: Option<StencilTarget>,
79    /// UI-thread lease flag preventing simultaneous reuse.
80    in_use: Cell<bool>,
81}
82
83/// Frame-transient pool of offscreen render targets.
84///
85/// Slots persist across frames and are matched by exact [`PoolKey`]. Call
86/// [`Self::end_frame`] only after all encoded composites have finished
87/// referencing their leases. The pool has no eviction policy.
88///
89/// # Examples
90///
91/// ```
92/// use ailloli_ui_render_wgpu::offscreen_pool::OffscreenSurfacePool;
93/// let pool = OffscreenSurfacePool::default();
94/// assert_eq!((pool.reuse_hits, pool.allocs, pool.peak_bytes()), (0, 0, 0));
95/// ```
96#[derive(Default)]
97pub struct OffscreenSurfacePool {
98    /// Reusable slots retained across frames.
99    slots: Vec<PoolSlot>,
100    /// Successful exact-key lease reuses across the pool lifetime.
101    pub reuse_hits: u32,
102    /// New slot allocations across the pool lifetime.
103    pub allocs: u32,
104}
105
106/// Lightweight handle to one currently leased pool slot.
107///
108/// A lease is meaningful only with the pool that created it. It does not own or
109/// automatically release the slot, and copying it does not create a new lease.
110///
111/// # Examples
112///
113/// ```
114/// use ailloli_ui_render_wgpu::offscreen_pool::LeasedOffscreen;
115/// let _: usize = std::mem::size_of::<LeasedOffscreen>();
116/// ```
117#[derive(Clone, Copy)]
118pub struct LeasedOffscreen {
119    /// Slot width in physical pixels.
120    pub width: u32,
121    /// Slot height in physical pixels.
122    pub height: u32,
123    /// Index of the leased slot in the owning pool.
124    slot_index: usize,
125}
126
127impl LeasedOffscreen {
128    /// Returns the slot's full color view.
129    ///
130    /// # Panics
131    ///
132    /// Panics if used with a different or structurally corrupted pool.
133    ///
134    /// # Examples
135    ///
136    /// ```no_run
137    /// use ailloli_ui_render_wgpu::offscreen_pool::{LeasedOffscreen, OffscreenSurfacePool};
138    /// fn view<'a>(lease: &LeasedOffscreen, pool: &'a OffscreenSurfacePool)
139    ///     -> &'a wgpu::TextureView { lease.color_view(pool) }
140    /// ```
141    pub fn color_view<'a>(&self, pool: &'a OffscreenSurfacePool) -> &'a wgpu::TextureView {
142        &pool.slots[self.slot_index].color_view
143    }
144
145    /// Returns the slot's owned color texture for copy operations.
146    ///
147    /// # Panics
148    ///
149    /// Panics if used with a different or structurally corrupted pool.
150    ///
151    /// # Examples
152    ///
153    /// ```no_run
154    /// use ailloli_ui_render_wgpu::offscreen_pool::{LeasedOffscreen, OffscreenSurfacePool};
155    /// fn texture<'a>(lease: &LeasedOffscreen, pool: &'a OffscreenSurfacePool)
156    ///     -> &'a wgpu::Texture { lease.color_texture(pool) }
157    /// ```
158    pub fn color_texture<'a>(&self, pool: &'a OffscreenSurfacePool) -> &'a wgpu::Texture {
159        &pool.slots[self.slot_index].color_texture
160    }
161
162    /// Returns the optional stencil view requested by the slot's key.
163    ///
164    /// # Examples
165    ///
166    /// ```no_run
167    /// use ailloli_ui_render_wgpu::offscreen_pool::{LeasedOffscreen, OffscreenSurfacePool};
168    /// fn stencil<'a>(lease: &LeasedOffscreen, pool: &'a OffscreenSurfacePool)
169    ///     -> Option<&'a wgpu::TextureView> { lease.stencil_view(pool) }
170    /// ```
171    pub fn stencil_view<'a>(
172        &self,
173        pool: &'a OffscreenSurfacePool,
174    ) -> Option<&'a wgpu::TextureView> {
175        pool.slots[self.slot_index]
176            .stencil
177            .as_ref()
178            .map(|s| &s.view)
179    }
180}
181
182impl OffscreenSurfacePool {
183    /// Leases an exact-key slot, reusing a free allocation or creating one.
184    ///
185    /// New textures use `format`, but an existing slot is keyed only by
186    /// dimensions, stencil, and ping role. A pool must therefore be scoped to a
187    /// single surface format. Counters use `u32` and may overflow only after an
188    /// impractically large number of operations.
189    ///
190    /// # Examples
191    ///
192    /// ```no_run
193    /// use ailloli_ui_render_wgpu::offscreen_pool::{OffscreenSurfacePool, PoolKey};
194    /// fn lease(pool: &mut OffscreenSurfacePool, device: &wgpu::Device) {
195    ///     let slot = pool.lease(device, PoolKey::color(64, 64, true),
196    ///         wgpu::TextureFormat::Rgba8Unorm);
197    ///     assert_eq!(slot.width, 64);
198    /// }
199    /// ```
200    pub fn lease(
201        &mut self,
202        device: &wgpu::Device,
203        key: PoolKey,
204        format: wgpu::TextureFormat,
205    ) -> LeasedOffscreen {
206        if let Some(idx) = self
207            .slots
208            .iter()
209            .position(|s| !s.in_use.get() && s.key == key)
210        {
211            self.slots[idx].in_use.set(true);
212            self.reuse_hits += 1;
213            let _slot = &self.slots[idx];
214            return LeasedOffscreen {
215                width: key.width,
216                height: key.height,
217                slot_index: idx,
218            };
219        }
220
221        self.allocs += 1;
222        let color_texture = device.create_texture(&wgpu::TextureDescriptor {
223            label: Some("offscreen color"),
224            size: wgpu::Extent3d {
225                width: key.width,
226                height: key.height,
227                depth_or_array_layers: 1,
228            },
229            mip_level_count: 1,
230            sample_count: 1,
231            dimension: wgpu::TextureDimension::D2,
232            format,
233            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
234                | wgpu::TextureUsages::TEXTURE_BINDING
235                | wgpu::TextureUsages::COPY_DST,
236            view_formats: &[],
237        });
238        let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
239        let stencil = key
240            .needs_stencil
241            .then(|| StencilTarget::new(device, key.width, key.height));
242        let idx = self.slots.len();
243        self.slots.push(PoolSlot {
244            key,
245            color_texture,
246            color_view,
247            stencil,
248            in_use: Cell::new(true),
249        });
250        LeasedOffscreen {
251            width: key.width,
252            height: key.height,
253            slot_index: idx,
254        }
255    }
256
257    /// Marks a lease's slot free if its index exists in this pool.
258    ///
259    /// Invalid or foreign indices are silently ignored. Releasing copied leases
260    /// more than once is idempotent at the flag level.
261    ///
262    /// # Examples
263    ///
264    /// ```no_run
265    /// use ailloli_ui_render_wgpu::offscreen_pool::{LeasedOffscreen, OffscreenSurfacePool};
266    /// fn release(pool: &OffscreenSurfacePool, lease: LeasedOffscreen) { pool.release(lease); }
267    /// ```
268    pub fn release(&self, lease: LeasedOffscreen) {
269        if let Some(slot) = self.slots.get(lease.slot_index) {
270            slot.in_use.set(false);
271        }
272    }
273
274    /// Marks every leased slot free after the main pass has sampled offscreen textures.
275    ///
276    /// # Examples
277    ///
278    /// ```
279    /// use ailloli_ui_render_wgpu::offscreen_pool::OffscreenSurfacePool;
280    /// OffscreenSurfacePool::default().end_frame();
281    /// ```
282    pub fn end_frame(&self) {
283        for slot in &self.slots {
284            slot.in_use.set(false);
285        }
286    }
287
288    /// Debug-only: leased slot count must match active isolated passes until [`Self::end_frame`].
289    ///
290    /// Release builds compile this method as a no-op, preserving call-site shape.
291    ///
292    /// # Panics
293    ///
294    /// Debug builds panic when the number of in-use slots differs from
295    /// `expected`.
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// use ailloli_ui_render_wgpu::offscreen_pool::OffscreenSurfacePool;
301    /// OffscreenSurfacePool::default().debug_assert_leased_count(0);
302    /// ```
303    #[cfg(debug_assertions)]
304    pub fn debug_assert_leased_count(&self, expected: usize) {
305        let in_use = self.slots.iter().filter(|s| s.in_use.get()).count();
306        debug_assert_eq!(
307            in_use, expected,
308            "offscreen pool: expected {expected} leased slots before main pass composite"
309        );
310    }
311
312    #[cfg(not(debug_assertions))]
313    /// Release-build no-op counterpart of the debug lease-count assertion.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use ailloli_ui_render_wgpu::offscreen_pool::OffscreenSurfacePool;
319    /// OffscreenSurfacePool::default().debug_assert_leased_count(0);
320    /// ```
321    pub fn debug_assert_leased_count(&self, _expected: usize) {}
322
323    /// Estimates peak bytes retained by all slots, including free ones.
324    ///
325    /// Color and stencil each count as four bytes per pixel. This is a logical
326    /// estimate and excludes row alignment, driver metadata, views, and samplers.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use ailloli_ui_render_wgpu::offscreen_pool::OffscreenSurfacePool;
332    /// assert_eq!(OffscreenSurfacePool::default().peak_bytes(), 0);
333    /// ```
334    pub fn peak_bytes(&self) -> u64 {
335        self.slots
336            .iter()
337            .map(|s| {
338                let color = (s.key.width * s.key.height * 4) as u64;
339                let stencil = if s.key.needs_stencil {
340                    (s.key.width * s.key.height * 4) as u64
341                } else {
342                    0
343                };
344                color + stencil
345            })
346            .sum()
347    }
348}