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 = 4;
33
34pub 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 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 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 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}