Skip to main content

oxiui_render_wgpu/gpu/
layer_cache.rs

1//! Render-layer cache for GPU-backed subtree caching.
2//!
3//! [`LayerCache`] manages a pool of [`RenderTarget`]s keyed by a caller-provided
4//! `u64` `layer_id`.  When a widget subtree is rendered, its output is saved to
5//! a `RenderTarget`; subsequent frames can composite the cached layer texture
6//! directly without replaying the full draw list, as long as the layer hasn't
7//! been invalidated.
8//!
9//! # Invalidation model
10//!
11//! - Each layer has a generation counter.  The caller bumps the generation (via
12//!   [`LayerCache::invalidate`]) when the subtree's content changes.
13//! - Layers that haven't been accessed for `max_idle_frames` frames are evicted
14//!   to reclaim GPU memory.
15//!
16//! # Usage
17//!
18//! ```rust,ignore
19//! let mut cache = LayerCache::new(16);    // keep up to 16 layers
20//! cache.begin_frame();
21//!
22//! let layer_id = widget.stable_id();
23//! if let Some(target) = cache.get(layer_id) {
24//!     if !target.is_dirty() {
25//!         // composite cached layer texture into the parent pass
26//!         composite_layer(target.texture_view(), ...);
27//!         return;
28//!     }
29//! }
30//! // Layer is dirty or absent — render the subtree into it.
31//! let target = cache.get_or_create(device, layer_id, width, height, 1)?;
32//! render_subtree_into(target, ...);
33//! target.mark_clean();
34//! ```
35
36use oxiui_core::UiError;
37
38use crate::gpu::render_target::RenderTarget;
39
40// ── LayerEntry ────────────────────────────────────────────────────────────────
41
42struct LayerEntry {
43    target: RenderTarget,
44    /// Frame number of the last access (used for LRU eviction).
45    last_frame: u64,
46    /// Content generation — bumped by `invalidate()`.
47    generation: u64,
48}
49
50// ── LayerCache ────────────────────────────────────────────────────────────────
51
52/// A pool of off-screen render targets, keyed by `layer_id: u64`.
53pub struct LayerCache {
54    entries: std::collections::HashMap<u64, LayerEntry>,
55    /// Maximum number of idle frames before a layer is evicted.
56    max_idle_frames: u64,
57    /// Current frame counter, advanced by `begin_frame()`.
58    current_frame: u64,
59    /// Maximum number of live layers before the oldest is evicted.
60    max_layers: usize,
61}
62
63impl LayerCache {
64    /// Create a new cache that holds at most `max_layers` layers simultaneously.
65    pub fn new(max_layers: usize) -> Self {
66        Self {
67            entries: std::collections::HashMap::new(),
68            max_idle_frames: 4,
69            current_frame: 0,
70            max_layers: max_layers.max(1),
71        }
72    }
73
74    /// Set the number of idle frames after which a layer is evicted.
75    ///
76    /// A larger value trades GPU memory for reduced re-render cost when a layer
77    /// goes temporarily off-screen.
78    pub fn set_max_idle_frames(&mut self, frames: u64) {
79        self.max_idle_frames = frames.max(1);
80    }
81
82    /// Advance the internal frame counter and evict any layers that have not
83    /// been accessed for `max_idle_frames` frames.
84    ///
85    /// **Must be called once per frame**, before any `get`/`get_or_create` calls
86    /// for that frame.
87    pub fn begin_frame(&mut self) {
88        self.current_frame += 1;
89        // Evict idle layers.
90        let max_idle = self.max_idle_frames;
91        let current = self.current_frame;
92        self.entries
93            .retain(|_, entry| current - entry.last_frame <= max_idle);
94    }
95
96    /// Look up an existing layer by `layer_id`.
97    ///
98    /// Updates the last-access frame counter.  Returns `None` if no layer with
99    /// this id is currently cached.
100    pub fn get(&mut self, layer_id: u64) -> Option<&mut RenderTarget> {
101        let current = self.current_frame;
102        let entry = self.entries.get_mut(&layer_id)?;
103        entry.last_frame = current;
104        Some(&mut entry.target)
105    }
106
107    /// Return the [`RenderTarget`] for `layer_id`, creating a new one if absent.
108    ///
109    /// If a new target is created it is `width × height` pixels with the given
110    /// `sample_count`.  If the layer already exists but has different dimensions
111    /// it is **not** resized — the existing target is returned as-is.  Call
112    /// [`LayerCache::invalidate`] + [`LayerCache::remove`] then re-create if
113    /// you need a different size.
114    ///
115    /// When the cache is at capacity (`max_layers`), the LRU layer is evicted
116    /// before creating the new entry.
117    ///
118    /// # Errors
119    ///
120    /// Propagates [`RenderTarget::new`] errors.
121    pub fn get_or_create(
122        &mut self,
123        device: &wgpu::Device,
124        layer_id: u64,
125        width: u32,
126        height: u32,
127        sample_count: u32,
128    ) -> Result<&mut RenderTarget, UiError> {
129        let current = self.current_frame;
130
131        if !self.entries.contains_key(&layer_id) {
132            // Evict LRU entry if at capacity.
133            if self.entries.len() >= self.max_layers {
134                self.evict_lru();
135            }
136            let target = RenderTarget::new(device, width, height, sample_count)?;
137            self.entries.insert(
138                layer_id,
139                LayerEntry {
140                    target,
141                    last_frame: current,
142                    generation: 0,
143                },
144            );
145        }
146
147        let entry = self.entries.get_mut(&layer_id).expect("just inserted");
148        entry.last_frame = current;
149        Ok(&mut entry.target)
150    }
151
152    /// Invalidate a layer by `layer_id`, marking it dirty and bumping its
153    /// generation counter.  If the layer is not cached, this is a no-op.
154    pub fn invalidate(&mut self, layer_id: u64) {
155        if let Some(entry) = self.entries.get_mut(&layer_id) {
156            entry.target.mark_dirty();
157            entry.generation += 1;
158        }
159    }
160
161    /// Invalidate all cached layers.
162    pub fn invalidate_all(&mut self) {
163        for entry in self.entries.values_mut() {
164            entry.target.mark_dirty();
165            entry.generation += 1;
166        }
167    }
168
169    /// Remove a layer by `layer_id`, freeing the associated GPU texture.
170    pub fn remove(&mut self, layer_id: u64) {
171        self.entries.remove(&layer_id);
172    }
173
174    /// Remove all cached layers, freeing all associated GPU textures.
175    pub fn clear(&mut self) {
176        self.entries.clear();
177    }
178
179    /// Return the number of live layers currently in the cache.
180    pub fn len(&self) -> usize {
181        self.entries.len()
182    }
183
184    /// Return `true` if the cache is empty.
185    pub fn is_empty(&self) -> bool {
186        self.entries.is_empty()
187    }
188
189    /// Return the current frame counter.
190    pub fn current_frame(&self) -> u64 {
191        self.current_frame
192    }
193
194    // ── Private helpers ──────────────────────────────────────────────────────
195
196    /// Evict the layer that was accessed least recently.
197    fn evict_lru(&mut self) {
198        // Find the key with the smallest `last_frame`.
199        let lru_key = self
200            .entries
201            .iter()
202            .min_by_key(|(_, e)| e.last_frame)
203            .map(|(&k, _)| k);
204        if let Some(k) = lru_key {
205            self.entries.remove(&k);
206        }
207    }
208}
209
210// ── Tests ─────────────────────────────────────────────────────────────────────
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    fn try_device() -> Option<(wgpu::Device, wgpu::Queue)> {
217        let instance = wgpu::Instance::default();
218        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
219            power_preference: wgpu::PowerPreference::default(),
220            force_fallback_adapter: false,
221            compatible_surface: None,
222        }))
223        .ok()?;
224        pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
225            label: Some("layer-cache test device"),
226            required_features: wgpu::Features::empty(),
227            required_limits: wgpu::Limits::downlevel_defaults(),
228            memory_hints: wgpu::MemoryHints::Performance,
229            experimental_features: wgpu::ExperimentalFeatures::disabled(),
230            trace: wgpu::Trace::Off,
231        }))
232        .ok()
233    }
234
235    #[test]
236    fn layer_cache_empty_on_creation() {
237        let cache = LayerCache::new(8);
238        assert_eq!(cache.len(), 0);
239        assert!(cache.is_empty());
240        assert_eq!(cache.current_frame(), 0);
241    }
242
243    #[test]
244    fn layer_cache_get_or_create_and_get() {
245        let Some((device, _queue)) = try_device() else {
246            return;
247        };
248        let mut cache = LayerCache::new(4);
249        cache.begin_frame();
250
251        let target = cache
252            .get_or_create(&device, 42, 32, 32, 1)
253            .expect("create layer 42");
254        assert!(target.is_dirty(), "fresh layer must be dirty");
255        target.mark_clean();
256
257        assert_eq!(cache.len(), 1);
258        let t = cache.get(42).expect("layer 42 must exist");
259        assert!(!t.is_dirty(), "layer must be clean after mark_clean");
260    }
261
262    #[test]
263    fn layer_cache_invalidate_marks_dirty() {
264        let Some((device, _queue)) = try_device() else {
265            return;
266        };
267        let mut cache = LayerCache::new(4);
268        cache.begin_frame();
269
270        let target = cache.get_or_create(&device, 1, 16, 16, 1).expect("create");
271        target.mark_clean();
272        cache.invalidate(1);
273        let t = cache.get(1).expect("layer 1 must exist");
274        assert!(t.is_dirty(), "invalidated layer must be dirty");
275    }
276
277    #[test]
278    fn layer_cache_evicts_idle_layers() {
279        let Some((device, _queue)) = try_device() else {
280            return;
281        };
282        let mut cache = LayerCache::new(8);
283        cache.set_max_idle_frames(2);
284
285        cache.begin_frame(); // frame 1
286        cache.get_or_create(&device, 10, 16, 16, 1).expect("create");
287
288        // Advance 3 frames without accessing layer 10.
289        cache.begin_frame(); // frame 2
290        cache.begin_frame(); // frame 3
291        cache.begin_frame(); // frame 4 — idle >= 3 > max_idle_frames(2) → evicted
292
293        assert!(
294            cache.get(10).is_none(),
295            "layer 10 must be evicted after {n} idle frames",
296            n = 3
297        );
298    }
299
300    #[test]
301    fn layer_cache_lru_eviction_at_capacity() {
302        let Some((device, _queue)) = try_device() else {
303            return;
304        };
305        let mut cache = LayerCache::new(2); // capacity = 2 layers
306
307        cache.begin_frame();
308        cache.get_or_create(&device, 1, 8, 8, 1).expect("layer 1");
309        cache.get_or_create(&device, 2, 8, 8, 1).expect("layer 2");
310        assert_eq!(cache.len(), 2);
311
312        // Accessing layer 2 makes layer 1 the LRU.
313        cache.begin_frame();
314        let _ = cache.get(2); // access layer 2 again
315
316        // Creating layer 3 must evict LRU = layer 1.
317        cache.get_or_create(&device, 3, 8, 8, 1).expect("layer 3");
318        assert_eq!(cache.len(), 2, "cache should still hold 2 entries");
319        assert!(
320            cache.get(1).is_none(),
321            "layer 1 (LRU) must have been evicted"
322        );
323        assert!(cache.get(2).is_some(), "layer 2 must survive");
324        assert!(cache.get(3).is_some(), "layer 3 must be present");
325    }
326
327    #[test]
328    fn layer_cache_clear_removes_all() {
329        let Some((device, _queue)) = try_device() else {
330            return;
331        };
332        let mut cache = LayerCache::new(8);
333        cache.begin_frame();
334        cache.get_or_create(&device, 1, 8, 8, 1).expect("layer 1");
335        cache.get_or_create(&device, 2, 8, 8, 1).expect("layer 2");
336        assert_eq!(cache.len(), 2);
337        cache.clear();
338        assert!(cache.is_empty());
339    }
340
341    #[test]
342    fn layer_cache_invalid_layer_id_returns_none() {
343        let mut cache = LayerCache::new(4);
344        cache.begin_frame();
345        assert!(cache.get(9999).is_none());
346    }
347
348    #[test]
349    fn layer_cache_invalidate_all() {
350        let Some((device, _queue)) = try_device() else {
351            return;
352        };
353        let mut cache = LayerCache::new(4);
354        cache.begin_frame();
355        for id in 1u64..=3 {
356            let t = cache.get_or_create(&device, id, 8, 8, 1).expect("create");
357            t.mark_clean();
358        }
359        cache.invalidate_all();
360        for id in 1u64..=3 {
361            let t = cache.get(id).expect("layer must exist");
362            assert!(
363                t.is_dirty(),
364                "all layers must be dirty after invalidate_all"
365            );
366        }
367    }
368}