Skip to main content

sequoia_view/
cache.rs

1//! CPU-side layer cache.
2//!
3//! [`LayerCache`] stores `RenderList` snapshots for static subtrees. The
4//! runtime fills it for elements that opt in through
5//! [`View::cache_layer`](crate::View::cache_layer) (for example [`Cached`]).
6//!
7//! GPU resources are owned by the renderer and are not mirrored here: image
8//! textures, embedded surfaces, group layers, and the offscreen frame target
9//! all have renderer-specific keys and lifetimes.
10//!
11//! [`Cached`]: crate::Cached
12
13use std::collections::HashMap;
14use std::sync::Arc;
15
16use sequoia_core::{Rect, RenderList, RoundedClip, Transform, WindowId};
17
18use crate::paint_region::CullCtx;
19
20/// Identifies a cached layer by element and content revision.
21///
22/// `Runtime` bumps `revision` on the element and all ancestors whenever a
23/// repaint-relevant dirty flag or direct damage mark reaches the subtree.
24#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
25pub struct LayerKey {
26    pub element_id: u64,
27    pub revision: u64,
28}
29
30/// Absolute paint context that a cached layer was captured in.
31///
32/// Cached item rects/clips/transforms are absolute, so a hit is only valid
33/// when the parent offset, transform, clip, opacity, DPI, text system/atlas,
34/// window, interaction targets inside the subtree, and culling configuration
35/// are unchanged.
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub(crate) struct LayerStamp {
38    pub rect: Rect,
39    pub scale: f32,
40    pub transform: Transform,
41    pub clip: Option<Rect>,
42    pub rounded_clip: Option<RoundedClip>,
43    pub opacity: f32,
44    pub text_atlas: u64,
45    pub text_epoch: u64,
46    pub window: WindowId,
47    pub hovered: Option<u64>,
48    pub pressed: Option<u64>,
49    pub focused: Option<u64>,
50    pub captured: Option<u64>,
51    pub cull: CullCtx,
52}
53
54struct LayerEntry {
55    list: Arc<RenderList>,
56    stamp: Option<LayerStamp>,
57}
58
59/// CPU-side cache of rendered subtrees.
60#[derive(Default)]
61pub struct LayerCache {
62    map: HashMap<LayerKey, LayerEntry>,
63    capacity: usize,
64}
65
66impl LayerCache {
67    pub fn new(capacity: usize) -> Self {
68        Self {
69            map: HashMap::new(),
70            capacity: capacity.max(1),
71        }
72    }
73
74    pub fn get(&self, key: LayerKey) -> Option<Arc<RenderList>> {
75        self.map.get(&key).map(|entry| Arc::clone(&entry.list))
76    }
77
78    pub(crate) fn get_stamped(&self, key: LayerKey, stamp: &LayerStamp) -> Option<Arc<RenderList>> {
79        self.map
80            .get(&key)
81            .filter(|entry| entry.stamp.as_ref() == Some(stamp))
82            .map(|entry| Arc::clone(&entry.list))
83    }
84
85    pub fn insert(&mut self, key: LayerKey, list: RenderList) -> Arc<RenderList> {
86        let list = Arc::new(list);
87        self.store(key, Arc::clone(&list), None);
88        list
89    }
90
91    pub(crate) fn insert_stamped(
92        &mut self,
93        key: LayerKey,
94        list: RenderList,
95        stamp: LayerStamp,
96    ) -> Arc<RenderList> {
97        let list = Arc::new(list);
98        self.store(key, Arc::clone(&list), Some(stamp));
99        list
100    }
101
102    fn store(&mut self, key: LayerKey, list: Arc<RenderList>, stamp: Option<LayerStamp>) {
103        if !self.map.contains_key(&key)
104            && self.map.len() >= self.capacity
105            && let Some(victim) = self.map.keys().next().copied()
106        {
107            self.map.remove(&victim);
108        }
109        self.map.insert(key, LayerEntry { list, stamp });
110    }
111
112    /// Drops every cached revision of `element_id`.
113    pub fn invalidate_element(&mut self, element_id: u64) {
114        self.map.retain(|key, _| key.element_id != element_id);
115    }
116
117    pub fn invalidate_all(&mut self) {
118        self.map.clear();
119    }
120
121    pub fn len(&self) -> usize {
122        self.map.len()
123    }
124
125    pub fn is_empty(&self) -> bool {
126        self.map.is_empty()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use sequoia_core::{Color, Rect};
134
135    fn layer(element_id: u64, revision: u64) -> LayerKey {
136        LayerKey {
137            element_id,
138            revision,
139        }
140    }
141
142    #[test]
143    fn layer_cache_hit_returns_same_list() {
144        let mut cache = LayerCache::new(4);
145        let mut list = RenderList::new();
146        list.push_quad(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::RED, 0.0);
147        let first = cache.insert(layer(1, 0), list);
148        let second = cache.get(layer(1, 0)).unwrap();
149        assert!(Arc::ptr_eq(&first, &second));
150    }
151
152    #[test]
153    fn layer_cache_new_revision_is_miss() {
154        let mut cache = LayerCache::new(4);
155        let _ = cache.insert(layer(1, 0), RenderList::new());
156        assert!(cache.get(layer(1, 0)).is_some());
157        assert!(cache.get(layer(1, 1)).is_none());
158    }
159
160    #[test]
161    fn layer_cache_invalidate_element() {
162        let mut cache = LayerCache::new(8);
163        let _ = cache.insert(layer(1, 0), RenderList::new());
164        let _ = cache.insert(layer(2, 0), RenderList::new());
165        cache.invalidate_element(1);
166        assert!(cache.get(layer(1, 0)).is_none());
167        assert!(cache.get(layer(2, 0)).is_some(), "other element untouched");
168    }
169
170    #[test]
171    fn layer_cache_evicts_when_full() {
172        let mut cache = LayerCache::new(2);
173        let _ = cache.insert(layer(1, 0), RenderList::new());
174        let _ = cache.insert(layer(2, 0), RenderList::new());
175        let _ = cache.insert(layer(3, 0), RenderList::new());
176        assert_eq!(cache.len(), 2);
177    }
178
179    #[test]
180    fn layer_cache_replacing_key_does_not_evict_another() {
181        let mut cache = LayerCache::new(2);
182        let _ = cache.insert(layer(1, 0), RenderList::new());
183        let _ = cache.insert(layer(2, 0), RenderList::new());
184        let _ = cache.insert(layer(1, 0), RenderList::new());
185        assert_eq!(cache.len(), 2);
186        assert!(cache.get(layer(2, 0)).is_some());
187    }
188
189    fn stamp(rect: Rect) -> LayerStamp {
190        LayerStamp {
191            rect,
192            scale: 1.0,
193            transform: Transform::IDENTITY,
194            clip: Some(rect),
195            rounded_clip: None,
196            opacity: 1.0,
197            text_atlas: 0,
198            text_epoch: 0,
199            window: WindowId::PRIMARY,
200            hovered: None,
201            pressed: None,
202            focused: None,
203            captured: None,
204            cull: CullCtx::ENABLED,
205        }
206    }
207
208    #[test]
209    fn layer_cache_stamp_must_match() {
210        let mut cache = LayerCache::new(4);
211        let rect = Rect::from_xywh(0.0, 0.0, 10.0, 10.0);
212        let _ = cache.insert_stamped(layer(1, 0), RenderList::new(), stamp(rect));
213        assert!(
214            cache.get_stamped(layer(1, 0), &stamp(rect)).is_some(),
215            "same context hits"
216        );
217        assert!(
218            cache
219                .get_stamped(layer(1, 0), &stamp(Rect::from_xywh(1.0, 0.0, 10.0, 10.0)))
220                .is_none(),
221            "moved layer misses"
222        );
223        let mut hovered = stamp(rect);
224        hovered.hovered = Some(7);
225        assert!(
226            cache.get_stamped(layer(1, 0), &hovered).is_none(),
227            "hover target change misses"
228        );
229    }
230}