Skip to main content

wisp/render/
mask_cache.rs

1//! Cache of generated mask textures (M-DYN.2 / AUT-44).
2//!
3//! Keyed on the `MaskShape` data + output dimensions + invert flag.
4//! `f32` fields are bit-cast to `u32` so equality is exact-bit and
5//! `Hash` works (avoids the NaN-aware floating-point quirks of
6//! [`std::cmp::Eq`]). Same canonical NaN bits hash identically — fine
7//! for our use case where callers re-pass the same shape value across
8//! frames.
9//!
10//! Eviction is FIFO, capped at [`MAX_ENTRIES`]. The cap is a balance
11//! between memory (each mask texture is `w × h × 4` bytes of GPU
12//! memory) and reuse efficiency. 64 entries × 256² × 4 bytes ≈ 16 MB
13//! upper bound — small even on integrated GPUs.
14//!
15//! Path masks are intentionally **not** cached in V1: hashing a
16//! `Vec<glam::Vec2>` is non-trivial and most freehand-mask use cases
17//! mutate the polygon between frames anyway. Callers needing path
18//! caching should use [`Renderer::generate_path_mask_texture`][gpmt]
19//! and manage the cache externally.
20//!
21//! [gpmt]: crate::render::Renderer::generate_path_mask_texture
22
23use std::cell::RefCell;
24use std::collections::{HashMap, VecDeque};
25use std::sync::Arc;
26
27use crate::scene::clip::MaskShape;
28use crate::texture::render_texture::RenderTexture;
29
30/// Maximum number of cached mask textures before FIFO eviction.
31pub(crate) const MAX_ENTRIES: usize = 64;
32
33#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)]
34enum ShapeKey {
35    Rect {
36        rect_bits: [u32; 4],
37    },
38    RoundedRect {
39        rect_bits: [u32; 4],
40        radius_bits: u32,
41    },
42    Circle {
43        center_bits: [u32; 2],
44        radius_bits: u32,
45    },
46    Ellipse {
47        center_bits: [u32; 2],
48        half_bits: [u32; 2],
49    },
50}
51
52#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)]
53pub(crate) struct MaskKey {
54    shape: ShapeKey,
55    w: u32,
56    h: u32,
57    invert: bool,
58}
59
60impl MaskKey {
61    pub(crate) fn new(shape: MaskShape, w: u32, h: u32, invert: bool) -> Self {
62        let shape = match shape {
63            MaskShape::Rect { rect } => ShapeKey::Rect {
64                rect_bits: [
65                    rect.min.x.to_bits(),
66                    rect.min.y.to_bits(),
67                    rect.size.x.to_bits(),
68                    rect.size.y.to_bits(),
69                ],
70            },
71            MaskShape::RoundedRect { rect, radius } => ShapeKey::RoundedRect {
72                rect_bits: [
73                    rect.min.x.to_bits(),
74                    rect.min.y.to_bits(),
75                    rect.size.x.to_bits(),
76                    rect.size.y.to_bits(),
77                ],
78                radius_bits: radius.to_bits(),
79            },
80            MaskShape::Circle { center, radius } => ShapeKey::Circle {
81                center_bits: [center.x.to_bits(), center.y.to_bits()],
82                radius_bits: radius.to_bits(),
83            },
84            MaskShape::Ellipse {
85                center,
86                half_extents,
87            } => ShapeKey::Ellipse {
88                center_bits: [center.x.to_bits(), center.y.to_bits()],
89                half_bits: [half_extents.x.to_bits(), half_extents.y.to_bits()],
90            },
91        };
92        Self {
93            shape,
94            w,
95            h,
96            invert,
97        }
98    }
99}
100
101/// FIFO-bounded cache of generated mask textures.
102pub(crate) struct MaskCache {
103    map: HashMap<MaskKey, Arc<RenderTexture>>,
104    order: VecDeque<MaskKey>,
105    hits: u64,
106    misses: u64,
107}
108
109impl MaskCache {
110    pub(crate) fn new() -> Self {
111        Self {
112            map: HashMap::new(),
113            order: VecDeque::new(),
114            hits: 0,
115            misses: 0,
116        }
117    }
118
119    /// Get the cached texture for `key`, generating it via `generate`
120    /// (which is only called on a miss).
121    pub(crate) fn get_or_insert<F>(&mut self, key: MaskKey, generate: F) -> Arc<RenderTexture>
122    where
123        F: FnOnce() -> RenderTexture,
124    {
125        if let Some(existing) = self.map.get(&key) {
126            self.hits += 1;
127            return Arc::clone(existing);
128        }
129        self.misses += 1;
130
131        let rt = Arc::new(generate());
132        if self.map.len() >= MAX_ENTRIES
133            && let Some(oldest) = self.order.pop_front()
134        {
135            self.map.remove(&oldest);
136        }
137        self.map.insert(key, Arc::clone(&rt));
138        self.order.push_back(key);
139        rt
140    }
141
142    pub(crate) fn stats(&self) -> (u64, u64) {
143        (self.hits, self.misses)
144    }
145
146    pub(crate) fn clear(&mut self) {
147        self.map.clear();
148        self.order.clear();
149    }
150}
151
152/// Type alias for the `RefCell`-wrapped cache as stored on
153/// [`crate::render::Renderer`].
154pub(crate) type MaskCacheCell = RefCell<MaskCache>;