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
30pub const LAYER_RASTER_CACHE_KIND_COUNT: usize = 5;
33
34pub 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 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 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 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 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}