Skip to main content

valo_renderer/
pool.rs

1use std::collections::HashMap;
2
3use crate::pipelines::{DEPTH_FORMAT, SAMPLE_COUNT};
4
5/// `TargetPool` reuses offscreen textures across frames.
6///
7/// Layer, snapshot, filter, and raster-attachment targets are taken during
8/// planning, stay alive through GPU submission, and return at [`Self::end_frame`].
9/// Entries unused for several frames are dropped. Main-target MSAA color and
10/// depth scratch is keyed by size and kept.
11///
12/// Views returned by `take_*` are cloned wgpu handles. Do not keep them past
13/// [`Self::end_frame`]: the pool may reuse or drop the underlying textures.
14pub struct TargetPool {
15    device: wgpu::Device,
16    frame: u64,
17    /// Available pooled entries, exact-size matched.
18    layers: Vec<Pooled<LayerTarget>>,
19    snapshots: Vec<Pooled<Snapshot>>,
20    filters: Vec<Pooled<FilterTarget>>,
21    raster_attachments: Vec<Pooled<RasterAttachments>>,
22    /// Taken this frame; reclaimed by `end_frame`.
23    taken_layers: Vec<Pooled<LayerTarget>>,
24    taken_snapshots: Vec<Pooled<Snapshot>>,
25    taken_filters: Vec<Pooled<FilterTarget>>,
26    taken_raster_attachments: Vec<Pooled<RasterAttachments>>,
27    main_scratch: HashMap<(u32, u32, wgpu::TextureFormat, bool), MainScratch>,
28}
29
30/// `FILTER_SIZE_BUCKET` is the size quantum, in pixels, for pooled filter targets.
31///
32/// Blur-chain sizes snap up to this so consecutive frames and the horizontal
33/// and vertical passes of one blur share textures.
34pub const FILTER_SIZE_BUCKET: u32 = 32;
35
36const EVICT_AFTER_FRAMES: u64 = 3;
37
38struct Pooled<T> {
39    size: [u32; 2],
40    format: wgpu::TextureFormat,
41    /// Attachments carry [`wgpu::TextureUsages::TRANSIENT`] — tile-only on
42    /// hardware that supports it, so they cost no system memory.
43    transient: bool,
44    last_used: u64,
45    value: T,
46}
47
48/// `LayerTarget` is one offscreen layer's attachments.
49///
50/// Content renders into `msaa` (4 samples) and resolves to `resolve`.
51/// `resolve_texture` is also the copy source when a snapshot is taken inside
52/// the layer.
53#[derive(Clone)]
54pub struct LayerTarget {
55    pub msaa: wgpu::TextureView,
56    pub resolve_texture: wgpu::Texture,
57    pub resolve: wgpu::TextureView,
58    pub depth: wgpu::TextureView,
59}
60
61/// `Snapshot` is a copy of a destination region for advanced blends.
62///
63/// `view` is sampleable; `texture` is the copy destination.
64#[derive(Clone)]
65pub struct Snapshot {
66    pub texture: wgpu::Texture,
67    pub view: wgpu::TextureView,
68}
69
70/// `RasterAttachments` are the transient MSAA color and depth attachments
71/// for a raster-cache fill.
72///
73/// The resolve target is the cache's own persistent texture, so it is not
74/// pooled here. Both attachments are tile-only on hardware that supports it.
75#[derive(Clone)]
76pub struct RasterAttachments {
77    pub msaa: wgpu::TextureView,
78    pub depth: wgpu::TextureView,
79}
80
81/// `FilterTarget` is a single-sample color target for a gaussian filter pass.
82///
83/// It has no depth buffer. After the pass it is sampled by the next pass or
84/// the composite.
85#[derive(Clone)]
86pub struct FilterTarget {
87    pub view: wgpu::TextureView,
88}
89
90/// `MainScratch` is the MSAA color and depth scratch for the frame's main target.
91///
92/// The caller owns the resolve texture; this pool only provides the 4-sample
93/// attachments around it.
94#[derive(Clone)]
95pub struct MainScratch {
96    pub msaa: wgpu::TextureView,
97    pub depth: wgpu::TextureView,
98}
99
100impl TargetPool {
101    /// `new` creates an empty pool for `device`.
102    pub fn new(device: &wgpu::Device) -> Self {
103        Self {
104            device: device.clone(),
105            frame: 0,
106            layers: Vec::new(),
107            snapshots: Vec::new(),
108            filters: Vec::new(),
109            raster_attachments: Vec::new(),
110            taken_layers: Vec::new(),
111            taken_snapshots: Vec::new(),
112            taken_filters: Vec::new(),
113            taken_raster_attachments: Vec::new(),
114            main_scratch: HashMap::new(),
115        }
116    }
117
118    /// `take_layer` returns a pooled offscreen layer of `size` and `format`.
119    ///
120    /// `transient` is true when every segment of the target discards at pass
121    /// end. The returned views must not be used after [`Self::end_frame`].
122    pub fn take_layer(
123        &mut self,
124        size: [u32; 2],
125        format: wgpu::TextureFormat,
126        transient: bool,
127    ) -> LayerTarget {
128        let entry = take_matching(&mut self.layers, size, format, transient)
129            .unwrap_or_else(|| self.create_layer(size, format, transient));
130        let value = entry.value.clone();
131        self.taken_layers.push(refreshed(entry, self.frame));
132        value
133    }
134
135    /// `take_raster_attachments` returns pooled MSAA color and depth for a
136    /// raster-cache fill of `size` and `format`.
137    ///
138    /// The resolve target is the cache's own persistent texture, so only the
139    /// transient attachments pool here. Exact-size match. The returned views
140    /// must not be used after [`Self::end_frame`].
141    pub fn take_raster_attachments(
142        &mut self,
143        size: [u32; 2],
144        format: wgpu::TextureFormat,
145    ) -> RasterAttachments {
146        let entry = take_matching(&mut self.raster_attachments, size, format, true)
147            .unwrap_or_else(|| self.create_raster_attachments(size, format));
148        let value = entry.value.clone();
149        self.taken_raster_attachments
150            .push(refreshed(entry, self.frame));
151        value
152    }
153
154    /// `take_snapshot` returns a pooled destination copy of `size` and `format`.
155    ///
156    /// The returned views must not be used after [`Self::end_frame`].
157    pub fn take_snapshot(&mut self, size: [u32; 2], format: wgpu::TextureFormat) -> Snapshot {
158        let entry = take_matching(&mut self.snapshots, size, format, false)
159            .unwrap_or_else(|| self.create_snapshot(size, format));
160        let value = entry.value.clone();
161        self.taken_snapshots.push(refreshed(entry, self.frame));
162        value
163    }
164
165    /// `take_filter` returns a pooled single-sample filter target of `size` and `format`.
166    ///
167    /// `size` should already be snapped to `FILTER_SIZE_BUCKET`. The returned
168    /// view must not be used after [`Self::end_frame`].
169    pub fn take_filter(&mut self, size: [u32; 2], format: wgpu::TextureFormat) -> FilterTarget {
170        let entry = take_matching(&mut self.filters, size, format, false)
171            .unwrap_or_else(|| self.create_filter(size, format));
172        let value = entry.value.clone();
173        self.taken_filters.push(refreshed(entry, self.frame));
174        value
175    }
176
177    /// `main_scratch` returns MSAA color and depth for the frame's main target.
178    ///
179    /// `transient` is true when every segment discards at pass end (a
180    /// single-segment frame). The swap to a persistent pair on the first
181    /// resume also comes through here. Scratch is keyed by size and kept.
182    pub fn main_scratch(
183        &mut self,
184        size: [u32; 2],
185        format: wgpu::TextureFormat,
186        transient: bool,
187    ) -> MainScratch {
188        if self.main_scratch.len() > 8 {
189            self.main_scratch.clear(); // a handful of live sizes; reset is fine
190        }
191        let device = self.device.clone();
192        self.main_scratch
193            .entry((size[0], size[1], format, transient))
194            .or_insert_with(|| MainScratch {
195                msaa: attachment_texture(&device, size, format, SAMPLE_COUNT, false, transient)
196                    .create_view(&Default::default()),
197                depth: attachment_texture(
198                    &device,
199                    size,
200                    DEPTH_FORMAT,
201                    SAMPLE_COUNT,
202                    false,
203                    transient,
204                )
205                .create_view(&Default::default()),
206            })
207            .clone()
208    }
209
210    /// `end_frame` returns this frame's takes to the pool and drops idle entries.
211    pub fn end_frame(&mut self) {
212        self.frame += 1;
213        let cutoff = self.frame.saturating_sub(EVICT_AFTER_FRAMES);
214        self.layers.append(&mut self.taken_layers);
215        self.snapshots.append(&mut self.taken_snapshots);
216        self.filters.append(&mut self.taken_filters);
217        self.raster_attachments
218            .append(&mut self.taken_raster_attachments);
219        self.layers.retain(|e| e.last_used >= cutoff);
220        self.snapshots.retain(|e| e.last_used >= cutoff);
221        self.filters.retain(|e| e.last_used >= cutoff);
222        self.raster_attachments.retain(|e| e.last_used >= cutoff);
223    }
224
225    fn create_layer(
226        &self,
227        size: [u32; 2],
228        format: wgpu::TextureFormat,
229        transient: bool,
230    ) -> Pooled<LayerTarget> {
231        let msaa = attachment_texture(&self.device, size, format, SAMPLE_COUNT, false, transient);
232        let resolve = attachment_texture(&self.device, size, format, 1, true, false);
233        let depth = attachment_texture(
234            &self.device,
235            size,
236            DEPTH_FORMAT,
237            SAMPLE_COUNT,
238            false,
239            transient,
240        );
241        Pooled {
242            size,
243            format,
244            transient,
245            last_used: self.frame,
246            value: LayerTarget {
247                msaa: msaa.create_view(&Default::default()),
248                resolve: resolve.create_view(&Default::default()),
249                resolve_texture: resolve,
250                depth: depth.create_view(&Default::default()),
251            },
252        }
253    }
254
255    fn create_raster_attachments(
256        &self,
257        size: [u32; 2],
258        format: wgpu::TextureFormat,
259    ) -> Pooled<RasterAttachments> {
260        let msaa = attachment_texture(&self.device, size, format, SAMPLE_COUNT, false, true);
261        let depth = attachment_texture(&self.device, size, DEPTH_FORMAT, SAMPLE_COUNT, false, true);
262        Pooled {
263            size,
264            format,
265            transient: true,
266            last_used: self.frame,
267            value: RasterAttachments {
268                msaa: msaa.create_view(&Default::default()),
269                depth: depth.create_view(&Default::default()),
270            },
271        }
272    }
273
274    fn create_filter(&self, size: [u32; 2], format: wgpu::TextureFormat) -> Pooled<FilterTarget> {
275        let texture = attachment_texture(&self.device, size, format, 1, true, false);
276        Pooled {
277            size,
278            format,
279            transient: false,
280            last_used: self.frame,
281            value: FilterTarget {
282                view: texture.create_view(&Default::default()),
283            },
284        }
285    }
286
287    fn create_snapshot(&self, size: [u32; 2], format: wgpu::TextureFormat) -> Pooled<Snapshot> {
288        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
289            label: Some("valo.snapshot"),
290            size: wgpu::Extent3d {
291                width: size[0],
292                height: size[1],
293                depth_or_array_layers: 1,
294            },
295            mip_level_count: 1,
296            sample_count: 1,
297            dimension: wgpu::TextureDimension::D2,
298            format,
299            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
300            view_formats: &[],
301        });
302        Pooled {
303            size,
304            format,
305            transient: false,
306            last_used: self.frame,
307            value: Snapshot {
308                view: texture.create_view(&Default::default()),
309                texture,
310            },
311        }
312    }
313}
314
315fn take_matching<T>(
316    pool: &mut Vec<Pooled<T>>,
317    size: [u32; 2],
318    format: wgpu::TextureFormat,
319    transient: bool,
320) -> Option<Pooled<T>> {
321    let idx = pool
322        .iter()
323        .position(|e| e.size == size && e.format == format && e.transient == transient)?;
324    Some(pool.swap_remove(idx))
325}
326
327fn refreshed<T>(mut entry: Pooled<T>, frame: u64) -> Pooled<T> {
328    entry.last_used = frame;
329    entry
330}
331
332/// A render-attachment texture; `sampleable + copyable` adds the usages a
333/// layer's resolve target needs (composited from, snapshotted from).
334fn attachment_texture(
335    device: &wgpu::Device,
336    size: [u32; 2],
337    format: wgpu::TextureFormat,
338    samples: u32,
339    sampleable: bool,
340    transient: bool,
341) -> wgpu::Texture {
342    let mut usage = wgpu::TextureUsages::RENDER_ATTACHMENT;
343    if sampleable {
344        usage |= wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC;
345    }
346    if transient {
347        // Tile-only where hardware supports it (Apple: MTLStorageMode
348        // Memoryless — zero bytes of system memory); the web backend
349        // strips the bit, other backends treat it as a hint. Requires
350        // StoreOp::Discard, which single-segment targets already use.
351        debug_assert!(!sampleable, "transient attachments cannot be sampled");
352        usage |= wgpu::TextureUsages::TRANSIENT_ATTACHMENT;
353    }
354    device.create_texture(&wgpu::TextureDescriptor {
355        label: Some("valo.pooled"),
356        size: wgpu::Extent3d {
357            width: size[0],
358            height: size[1],
359            depth_or_array_layers: 1,
360        },
361        mip_level_count: 1,
362        sample_count: samples,
363        dimension: wgpu::TextureDimension::D2,
364        format,
365        usage,
366        view_formats: &[],
367    })
368}
369
370impl TargetPool {
371    /// Pooled + taken targets and the persistent main scratch. Bytes are
372    /// descriptor estimates: MSAA attachments cost samples × bpp.
373    pub(crate) fn report(&self) -> crate::PoolReport {
374        // 4-sample color + depth (16 + 16) plus a 1-sample resolve;
375        // transient attachments are tile-only, so only the resolve counts.
376        const LAYER_BPP: u64 = 36;
377        const LAYER_TRANSIENT_BPP: u64 = 4;
378        const SCRATCH_BPP: u64 = 32; // caller owns the resolve
379        const FLAT_BPP: u64 = 4; // snapshots + filter targets
380        let mut count = 0u32;
381        let mut bytes = 0u64;
382        let mut add = |size: [u32; 2], bpp: u64| {
383            count += 1;
384            bytes += size[0] as u64 * size[1] as u64 * bpp;
385        };
386        for t in self.layers.iter().chain(&self.taken_layers) {
387            add(
388                t.size,
389                if t.transient {
390                    LAYER_TRANSIENT_BPP
391                } else {
392                    LAYER_BPP
393                },
394            );
395        }
396        for t in self.snapshots.iter().chain(&self.taken_snapshots) {
397            add(t.size, FLAT_BPP);
398        }
399        for t in self.filters.iter().chain(&self.taken_filters) {
400            add(t.size, FLAT_BPP);
401        }
402        for &(w, h, _, transient) in self.main_scratch.keys() {
403            // Transient pairs exist as objects but occupy no memory.
404            add([w, h], if transient { 0 } else { SCRATCH_BPP });
405        }
406        crate::PoolReport { count, bytes }
407    }
408}