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