Skip to main content

cranpose_render_common/
raster_cache.rs

1use cranpose_core::NodeId;
2use cranpose_ui_graphics::Rect;
3
4const SCALE_BUCKET_STEPS: f32 = 256.0;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub struct ScaleBucket(u32);
8
9impl ScaleBucket {
10    pub fn from_scale(scale: f32) -> Self {
11        let normalized = if scale.is_finite() && scale > 0.0 {
12            scale
13        } else {
14            1.0
15        };
16        Self((normalized * SCALE_BUCKET_STEPS).round().max(1.0) as u32)
17    }
18
19    pub fn raw(self) -> u32 {
20        self.0
21    }
22}
23
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
25pub struct LayerRasterCacheHashes {
26    pub target_content: u64,
27    pub effect: u64,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31enum LayerRasterCacheKind {
32    FullSurface,
33    SourceContent,
34    BackdropEffect,
35    SceneRange,
36    PrefixSnapshot,
37}
38
39impl LayerRasterCacheKind {
40    fn identity_kind(self) -> u8 {
41        match self {
42            Self::FullSurface => 0,
43            Self::SourceContent => 1,
44            Self::BackdropEffect => 2,
45            Self::SceneRange => 3,
46            Self::PrefixSnapshot => 4,
47        }
48    }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
52pub struct LayerRasterCacheIdentity {
53    stable_id: NodeId,
54    kind: u8,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
58pub struct LayerRasterCacheKey {
59    kind: LayerRasterCacheKind,
60    stable_id: Option<NodeId>,
61    content_hash: u64,
62    effect_hash: u64,
63    local_bounds_bits: [u32; 4],
64    pixel_size: [u32; 2],
65    scale_bucket: ScaleBucket,
66}
67
68impl LayerRasterCacheKey {
69    pub fn new(
70        stable_id: Option<NodeId>,
71        content_hash: u64,
72        effect_hash: u64,
73        local_bounds: Rect,
74        pixel_size: (u32, u32),
75        scale_bucket: ScaleBucket,
76    ) -> Self {
77        Self {
78            kind: LayerRasterCacheKind::FullSurface,
79            stable_id,
80            content_hash,
81            effect_hash,
82            local_bounds_bits: [
83                local_bounds.x.to_bits(),
84                local_bounds.y.to_bits(),
85                local_bounds.width.to_bits(),
86                local_bounds.height.to_bits(),
87            ],
88            pixel_size: [pixel_size.0, pixel_size.1],
89            scale_bucket,
90        }
91    }
92
93    pub fn source_content(
94        stable_id: Option<NodeId>,
95        content_hash: u64,
96        local_bounds: Rect,
97        pixel_size: (u32, u32),
98        scale_bucket: ScaleBucket,
99    ) -> Self {
100        Self {
101            kind: LayerRasterCacheKind::SourceContent,
102            stable_id,
103            content_hash,
104            effect_hash: 0,
105            local_bounds_bits: [
106                local_bounds.x.to_bits(),
107                local_bounds.y.to_bits(),
108                local_bounds.width.to_bits(),
109                local_bounds.height.to_bits(),
110            ],
111            pixel_size: [pixel_size.0, pixel_size.1],
112            scale_bucket,
113        }
114    }
115
116    pub fn backdrop_effect(
117        stable_id: Option<NodeId>,
118        input_hash: u64,
119        effect_hash: u64,
120        local_bounds: Rect,
121        pixel_size: (u32, u32),
122        scale_bucket: ScaleBucket,
123    ) -> Self {
124        Self {
125            kind: LayerRasterCacheKind::BackdropEffect,
126            stable_id,
127            content_hash: input_hash,
128            effect_hash,
129            local_bounds_bits: [
130                local_bounds.x.to_bits(),
131                local_bounds.y.to_bits(),
132                local_bounds.width.to_bits(),
133                local_bounds.height.to_bits(),
134            ],
135            pixel_size: [pixel_size.0, pixel_size.1],
136            scale_bucket,
137        }
138    }
139
140    pub fn scene_range(
141        content_hash: u64,
142        local_bounds: Rect,
143        pixel_size: (u32, u32),
144        scale_bucket: ScaleBucket,
145    ) -> Self {
146        Self {
147            kind: LayerRasterCacheKind::SceneRange,
148            stable_id: None,
149            content_hash,
150            effect_hash: 0,
151            local_bounds_bits: [
152                local_bounds.x.to_bits(),
153                local_bounds.y.to_bits(),
154                local_bounds.width.to_bits(),
155                local_bounds.height.to_bits(),
156            ],
157            pixel_size: [pixel_size.0, pixel_size.1],
158            scale_bucket,
159        }
160    }
161
162    /// A snapshot of the scene's rendered prefix: the bytes the target held
163    /// after drawing ops `[0, prefix_len)` over the pass's clear color. A
164    /// replay of captured bytes is identical to direct rendering by
165    /// construction — no flattening, so none of the chained-rounding
166    /// divergence flatten entries carry.
167    pub fn prefix_snapshot(
168        content_hash: u64,
169        prefix_len: u64,
170        local_bounds: Rect,
171        pixel_size: (u32, u32),
172        scale_bucket: ScaleBucket,
173    ) -> Self {
174        Self {
175            kind: LayerRasterCacheKind::PrefixSnapshot,
176            stable_id: None,
177            content_hash,
178            effect_hash: prefix_len,
179            local_bounds_bits: [
180                local_bounds.x.to_bits(),
181                local_bounds.y.to_bits(),
182                local_bounds.width.to_bits(),
183                local_bounds.height.to_bits(),
184            ],
185            pixel_size: [pixel_size.0, pixel_size.1],
186            scale_bucket,
187        }
188    }
189
190    pub fn stable_id(self) -> Option<NodeId> {
191        self.stable_id
192    }
193
194    pub fn is_scene_range(self) -> bool {
195        matches!(
196            self.kind,
197            LayerRasterCacheKind::SceneRange | LayerRasterCacheKind::PrefixSnapshot
198        )
199    }
200
201    pub fn is_source_content(self) -> bool {
202        self.kind == LayerRasterCacheKind::SourceContent
203    }
204
205    pub fn identity(self) -> Option<LayerRasterCacheIdentity> {
206        Some(LayerRasterCacheIdentity {
207            stable_id: self.stable_id?,
208            kind: self.kind.identity_kind(),
209        })
210    }
211
212    pub fn pixel_size(self) -> (u32, u32) {
213        (self.pixel_size[0], self.pixel_size[1])
214    }
215
216    pub fn scale_bucket(self) -> ScaleBucket {
217        self.scale_bucket
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn scale_bucket_normalizes_invalid_values() {
227        assert_eq!(
228            ScaleBucket::from_scale(0.0).raw(),
229            ScaleBucket::from_scale(1.0).raw()
230        );
231        assert_eq!(
232            ScaleBucket::from_scale(-3.0).raw(),
233            ScaleBucket::from_scale(1.0).raw()
234        );
235        assert_eq!(
236            ScaleBucket::from_scale(f32::NAN).raw(),
237            ScaleBucket::from_scale(1.0).raw()
238        );
239    }
240
241    #[test]
242    fn scale_bucket_quantizes_small_fractional_changes() {
243        let a = ScaleBucket::from_scale(1.0);
244        let b = ScaleBucket::from_scale(1.001);
245        let c = ScaleBucket::from_scale(1.01);
246        assert_eq!(a, b);
247        assert_ne!(a, c);
248    }
249
250    #[test]
251    fn layer_raster_cache_key_captures_bounds_and_pixel_size() {
252        let rect = Rect {
253            x: 1.0,
254            y: 2.0,
255            width: 30.0,
256            height: 40.0,
257        };
258        let base = LayerRasterCacheKey::new(
259            Some(7),
260            11,
261            13,
262            rect,
263            (30, 40),
264            ScaleBucket::from_scale(1.0),
265        );
266        let moved = LayerRasterCacheKey::new(
267            Some(7),
268            11,
269            13,
270            Rect { x: 2.0, ..rect },
271            (30, 40),
272            ScaleBucket::from_scale(1.0),
273        );
274        let resized = LayerRasterCacheKey::new(
275            Some(7),
276            11,
277            13,
278            rect,
279            (60, 80),
280            ScaleBucket::from_scale(2.0),
281        );
282
283        assert_ne!(base, moved);
284        assert_ne!(base, resized);
285        assert_eq!(base.stable_id(), Some(7));
286        assert_eq!(base.pixel_size(), (30, 40));
287    }
288
289    #[test]
290    fn source_content_keys_do_not_collide_with_full_surface_keys() {
291        let rect = Rect {
292            x: 1.0,
293            y: 2.0,
294            width: 30.0,
295            height: 40.0,
296        };
297        let scale = ScaleBucket::from_scale(1.0);
298        let source = LayerRasterCacheKey::source_content(Some(7), 11, rect, (30, 40), scale);
299        let full = LayerRasterCacheKey::new(Some(7), 11, 0, rect, (30, 40), scale);
300
301        assert_ne!(source, full);
302        assert_ne!(source.identity(), full.identity());
303    }
304
305    #[test]
306    fn backdrop_effect_keys_do_not_collide_with_layer_surface_keys() {
307        let rect = Rect {
308            x: 1.0,
309            y: 2.0,
310            width: 30.0,
311            height: 40.0,
312        };
313        let scale = ScaleBucket::from_scale(1.0);
314        let backdrop = LayerRasterCacheKey::backdrop_effect(Some(7), 11, 13, rect, (30, 40), scale);
315        let source = LayerRasterCacheKey::source_content(Some(7), 11, rect, (30, 40), scale);
316        let full = LayerRasterCacheKey::new(Some(7), 11, 13, rect, (30, 40), scale);
317
318        assert_ne!(backdrop, source);
319        assert_ne!(backdrop, full);
320        assert_ne!(backdrop.identity(), source.identity());
321        assert_ne!(backdrop.identity(), full.identity());
322    }
323
324    #[test]
325    fn prefix_snapshot_keys_share_the_scene_range_partition_but_never_a_key() {
326        let rect = Rect {
327            x: 0.0,
328            y: 0.0,
329            width: 320.0,
330            height: 240.0,
331        };
332        let scale = ScaleBucket::from_scale(1.0);
333        let prefix = LayerRasterCacheKey::prefix_snapshot(11, 7, rect, (320, 240), scale);
334        let range = LayerRasterCacheKey::scene_range(11, rect, (320, 240), scale);
335        let longer = LayerRasterCacheKey::prefix_snapshot(11, 8, rect, (320, 240), scale);
336
337        assert!(prefix.is_scene_range());
338        assert_ne!(prefix, range);
339        assert_ne!(prefix, longer);
340        assert_eq!(prefix.identity(), None);
341        assert_eq!(prefix.pixel_size(), (320, 240));
342    }
343
344    #[test]
345    fn scene_range_keys_do_not_collide_with_layer_surface_keys() {
346        let rect = Rect {
347            x: 0.0,
348            y: 0.0,
349            width: 320.0,
350            height: 240.0,
351        };
352        let scale = ScaleBucket::from_scale(1.0);
353        let range = LayerRasterCacheKey::scene_range(11, rect, (320, 240), scale);
354        let source = LayerRasterCacheKey::source_content(None, 11, rect, (320, 240), scale);
355        let full = LayerRasterCacheKey::new(None, 11, 0, rect, (320, 240), scale);
356
357        assert_ne!(range, source);
358        assert_ne!(range, full);
359        assert_eq!(range.identity(), None);
360    }
361}