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}