Skip to main content

cranpose_render_common/
raster_cache.rs

1use cranpose_core::NodeId;
2use cranpose_ui_graphics::{Point, 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/// Number of distinct [`LayerRasterCacheKey`] kinds; see
31/// [`LayerRasterCacheKey::kind_slot`] and [`LAYER_RASTER_CACHE_KIND_LABELS`].
32pub const LAYER_RASTER_CACHE_KIND_COUNT: usize = 4;
33
34/// Short labels per kind slot, in [`LayerRasterCacheKey::kind_slot`] order.
35pub const LAYER_RASTER_CACHE_KIND_LABELS: [&str; LAYER_RASTER_CACHE_KIND_COUNT] =
36    ["src", "backdrop", "range", "prefix"];
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39enum LayerRasterCacheKind {
40    SourceContent,
41    BackdropEffect,
42    SceneRange,
43    PrefixSnapshot,
44}
45
46impl LayerRasterCacheKind {
47    fn identity_kind(self) -> u8 {
48        match self {
49            Self::SourceContent => 0,
50            Self::BackdropEffect => 1,
51            Self::SceneRange => 2,
52            Self::PrefixSnapshot => 3,
53        }
54    }
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
58pub struct LayerRasterCacheIdentity {
59    stable_id: NodeId,
60    kind: u8,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64pub struct LayerRasterCacheKey {
65    kind: LayerRasterCacheKind,
66    stable_id: Option<NodeId>,
67    content_hash: u64,
68    effect_hash: u64,
69    local_bounds_bits: [u32; 4],
70    pixel_size: [u32; 2],
71    scale_bucket: ScaleBucket,
72    device_phase_steps: [u32; 2],
73}
74
75const DEVICE_PHASE_STEPS: f32 = 16.0;
76
77fn device_phase_steps(phase: Point) -> [u32; 2] {
78    let steps = |value: f32| {
79        ((value.rem_euclid(1.0) * DEVICE_PHASE_STEPS).round() as u32) % DEVICE_PHASE_STEPS as u32
80    };
81    [steps(phase.x), steps(phase.y)]
82}
83
84fn local_bounds_bits(local_bounds: Rect) -> [u32; 4] {
85    [
86        local_bounds.x.to_bits(),
87        local_bounds.y.to_bits(),
88        local_bounds.width.to_bits(),
89        local_bounds.height.to_bits(),
90    ]
91}
92
93impl LayerRasterCacheKey {
94    pub fn source_content(
95        stable_id: Option<NodeId>,
96        content_hash: u64,
97        local_bounds: Rect,
98        pixel_size: (u32, u32),
99        scale_bucket: ScaleBucket,
100        device_phase: Point,
101    ) -> Self {
102        Self {
103            kind: LayerRasterCacheKind::SourceContent,
104            stable_id,
105            content_hash,
106            effect_hash: 0,
107            local_bounds_bits: local_bounds_bits(local_bounds),
108            pixel_size: [pixel_size.0, pixel_size.1],
109            scale_bucket,
110            device_phase_steps: device_phase_steps(device_phase),
111        }
112    }
113
114    pub fn backdrop_effect(
115        stable_id: Option<NodeId>,
116        input_hash: u64,
117        effect_hash: u64,
118        local_bounds: Rect,
119        pixel_size: (u32, u32),
120        scale_bucket: ScaleBucket,
121    ) -> Self {
122        Self {
123            kind: LayerRasterCacheKind::BackdropEffect,
124            stable_id,
125            content_hash: input_hash,
126            effect_hash,
127            local_bounds_bits: local_bounds_bits(local_bounds),
128            pixel_size: [pixel_size.0, pixel_size.1],
129            scale_bucket,
130            device_phase_steps: [0; 2],
131        }
132    }
133
134    pub fn scene_range(
135        content_hash: u64,
136        local_bounds: Rect,
137        pixel_size: (u32, u32),
138        scale_bucket: ScaleBucket,
139    ) -> Self {
140        Self {
141            kind: LayerRasterCacheKind::SceneRange,
142            stable_id: None,
143            content_hash,
144            effect_hash: 0,
145            local_bounds_bits: local_bounds_bits(local_bounds),
146            pixel_size: [pixel_size.0, pixel_size.1],
147            scale_bucket,
148            device_phase_steps: [0; 2],
149        }
150    }
151
152    /// A snapshot of the scene's rendered prefix: the bytes the target held
153    /// after drawing ops `[0, prefix_len)` over the pass's clear color. A
154    /// replay of captured bytes is identical to direct rendering by
155    /// construction — no flattening, so none of the chained-rounding
156    /// divergence flatten entries carry.
157    pub fn prefix_snapshot(
158        content_hash: u64,
159        prefix_len: u64,
160        local_bounds: Rect,
161        pixel_size: (u32, u32),
162        scale_bucket: ScaleBucket,
163    ) -> Self {
164        Self {
165            kind: LayerRasterCacheKind::PrefixSnapshot,
166            stable_id: None,
167            content_hash,
168            effect_hash: prefix_len,
169            local_bounds_bits: local_bounds_bits(local_bounds),
170            pixel_size: [pixel_size.0, pixel_size.1],
171            scale_bucket,
172            device_phase_steps: [0; 2],
173        }
174    }
175
176    /// Index of this key's kind in `0..LAYER_RASTER_CACHE_KIND_COUNT`, for
177    /// per-kind accounting.
178    pub fn kind_slot(self) -> usize {
179        self.kind.identity_kind() as usize
180    }
181
182    pub fn stable_id(self) -> Option<NodeId> {
183        self.stable_id
184    }
185
186    pub fn is_scene_range(self) -> bool {
187        matches!(
188            self.kind,
189            LayerRasterCacheKind::SceneRange | LayerRasterCacheKind::PrefixSnapshot
190        )
191    }
192
193    pub fn is_source_content(self) -> bool {
194        self.kind == LayerRasterCacheKind::SourceContent
195    }
196
197    pub fn identity(self) -> Option<LayerRasterCacheIdentity> {
198        Some(LayerRasterCacheIdentity {
199            stable_id: self.stable_id?,
200            kind: self.kind.identity_kind(),
201        })
202    }
203
204    pub fn pixel_size(self) -> (u32, u32) {
205        (self.pixel_size[0], self.pixel_size[1])
206    }
207
208    /// The bit pattern of the entry's local bounds: the place a keyless entry
209    /// occupies, stable while its content changes.
210    pub fn local_bounds_bits(self) -> [u32; 4] {
211        self.local_bounds_bits
212    }
213
214    pub fn scale_bucket(self) -> ScaleBucket {
215        self.scale_bucket
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn scale_bucket_normalizes_invalid_values() {
225        assert_eq!(
226            ScaleBucket::from_scale(0.0).raw(),
227            ScaleBucket::from_scale(1.0).raw()
228        );
229        assert_eq!(
230            ScaleBucket::from_scale(-3.0).raw(),
231            ScaleBucket::from_scale(1.0).raw()
232        );
233        assert_eq!(
234            ScaleBucket::from_scale(f32::NAN).raw(),
235            ScaleBucket::from_scale(1.0).raw()
236        );
237    }
238
239    #[test]
240    fn scale_bucket_quantizes_small_fractional_changes() {
241        let a = ScaleBucket::from_scale(1.0);
242        let b = ScaleBucket::from_scale(1.001);
243        let c = ScaleBucket::from_scale(1.01);
244        assert_eq!(a, b);
245        assert_ne!(a, c);
246    }
247
248    #[test]
249    fn layer_raster_cache_key_captures_bounds_and_pixel_size() {
250        let rect = Rect {
251            x: 1.0,
252            y: 2.0,
253            width: 30.0,
254            height: 40.0,
255        };
256        let base = LayerRasterCacheKey::source_content(
257            Some(7),
258            11,
259            rect,
260            (30, 40),
261            ScaleBucket::from_scale(1.0),
262            Point::default(),
263        );
264        let moved = LayerRasterCacheKey::source_content(
265            Some(7),
266            11,
267            Rect { x: 2.0, ..rect },
268            (30, 40),
269            ScaleBucket::from_scale(1.0),
270            Point::default(),
271        );
272        let resized = LayerRasterCacheKey::source_content(
273            Some(7),
274            11,
275            rect,
276            (60, 80),
277            ScaleBucket::from_scale(2.0),
278            Point::default(),
279        );
280
281        assert_ne!(base, moved);
282        assert_ne!(base, resized);
283        assert_eq!(base.stable_id(), Some(7));
284        assert_eq!(base.pixel_size(), (30, 40));
285    }
286
287    #[test]
288    fn layer_raster_cache_key_captures_the_device_phase() {
289        let rect = Rect {
290            x: 1.0,
291            y: 2.0,
292            width: 30.0,
293            height: 40.0,
294        };
295        let key = |phase: Point| {
296            LayerRasterCacheKey::source_content(
297                Some(7),
298                11,
299                rect,
300                (30, 40),
301                ScaleBucket::from_scale(1.0),
302                phase,
303            )
304        };
305        assert_ne!(key(Point::default()), key(Point::new(0.5, 0.0)));
306        assert_eq!(key(Point::new(0.5, 0.25)), key(Point::new(1.5, -0.75)));
307        assert_eq!(key(Point::new(0.01, 0.0)), key(Point::default()));
308    }
309
310    #[test]
311    fn source_content_keys_separate_by_content_hash() {
312        let rect = Rect {
313            x: 1.0,
314            y: 2.0,
315            width: 30.0,
316            height: 40.0,
317        };
318        let scale = ScaleBucket::from_scale(1.0);
319        let source = LayerRasterCacheKey::source_content(
320            Some(7),
321            11,
322            rect,
323            (30, 40),
324            scale,
325            Point::default(),
326        );
327        let other = LayerRasterCacheKey::source_content(
328            Some(7),
329            12,
330            rect,
331            (30, 40),
332            scale,
333            Point::default(),
334        );
335
336        assert_ne!(source, other);
337        assert_eq!(source.identity(), other.identity());
338    }
339
340    #[test]
341    fn backdrop_effect_keys_do_not_collide_with_layer_surface_keys() {
342        let rect = Rect {
343            x: 1.0,
344            y: 2.0,
345            width: 30.0,
346            height: 40.0,
347        };
348        let scale = ScaleBucket::from_scale(1.0);
349        let backdrop = LayerRasterCacheKey::backdrop_effect(Some(7), 11, 13, rect, (30, 40), scale);
350        let source = LayerRasterCacheKey::source_content(
351            Some(7),
352            11,
353            rect,
354            (30, 40),
355            scale,
356            Point::default(),
357        );
358
359        assert_ne!(backdrop, source);
360        assert_ne!(backdrop.identity(), source.identity());
361    }
362
363    #[test]
364    fn prefix_snapshot_keys_share_the_scene_range_partition_but_never_a_key() {
365        let rect = Rect {
366            x: 0.0,
367            y: 0.0,
368            width: 320.0,
369            height: 240.0,
370        };
371        let scale = ScaleBucket::from_scale(1.0);
372        let prefix = LayerRasterCacheKey::prefix_snapshot(11, 7, rect, (320, 240), scale);
373        let range = LayerRasterCacheKey::scene_range(11, rect, (320, 240), scale);
374        let longer = LayerRasterCacheKey::prefix_snapshot(11, 8, rect, (320, 240), scale);
375
376        assert!(prefix.is_scene_range());
377        assert_ne!(prefix, range);
378        assert_ne!(prefix, longer);
379        assert_eq!(prefix.identity(), None);
380        assert_eq!(prefix.pixel_size(), (320, 240));
381    }
382
383    #[test]
384    fn scene_range_keys_do_not_collide_with_layer_surface_keys() {
385        let rect = Rect {
386            x: 0.0,
387            y: 0.0,
388            width: 320.0,
389            height: 240.0,
390        };
391        let scale = ScaleBucket::from_scale(1.0);
392        let range = LayerRasterCacheKey::scene_range(11, rect, (320, 240), scale);
393        let source = LayerRasterCacheKey::source_content(
394            None,
395            11,
396            rect,
397            (320, 240),
398            scale,
399            Point::default(),
400        );
401
402        assert_ne!(range, source);
403        assert_eq!(range.identity(), None);
404    }
405}