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}
37
38impl LayerRasterCacheKind {
39 fn identity_kind(self) -> u8 {
40 match self {
41 Self::FullSurface => 0,
42 Self::SourceContent => 1,
43 Self::BackdropEffect => 2,
44 Self::SceneRange => 3,
45 }
46 }
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub struct LayerRasterCacheIdentity {
51 stable_id: NodeId,
52 kind: u8,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56pub struct LayerRasterCacheKey {
57 kind: LayerRasterCacheKind,
58 stable_id: Option<NodeId>,
59 content_hash: u64,
60 effect_hash: u64,
61 local_bounds_bits: [u32; 4],
62 pixel_size: [u32; 2],
63 scale_bucket: ScaleBucket,
64}
65
66impl LayerRasterCacheKey {
67 pub fn new(
68 stable_id: Option<NodeId>,
69 content_hash: u64,
70 effect_hash: u64,
71 local_bounds: Rect,
72 pixel_size: (u32, u32),
73 scale_bucket: ScaleBucket,
74 ) -> Self {
75 Self {
76 kind: LayerRasterCacheKind::FullSurface,
77 stable_id,
78 content_hash,
79 effect_hash,
80 local_bounds_bits: [
81 local_bounds.x.to_bits(),
82 local_bounds.y.to_bits(),
83 local_bounds.width.to_bits(),
84 local_bounds.height.to_bits(),
85 ],
86 pixel_size: [pixel_size.0, pixel_size.1],
87 scale_bucket,
88 }
89 }
90
91 pub fn source_content(
92 stable_id: Option<NodeId>,
93 content_hash: u64,
94 local_bounds: Rect,
95 pixel_size: (u32, u32),
96 scale_bucket: ScaleBucket,
97 ) -> Self {
98 Self {
99 kind: LayerRasterCacheKind::SourceContent,
100 stable_id,
101 content_hash,
102 effect_hash: 0,
103 local_bounds_bits: [
104 local_bounds.x.to_bits(),
105 local_bounds.y.to_bits(),
106 local_bounds.width.to_bits(),
107 local_bounds.height.to_bits(),
108 ],
109 pixel_size: [pixel_size.0, pixel_size.1],
110 scale_bucket,
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: [
128 local_bounds.x.to_bits(),
129 local_bounds.y.to_bits(),
130 local_bounds.width.to_bits(),
131 local_bounds.height.to_bits(),
132 ],
133 pixel_size: [pixel_size.0, pixel_size.1],
134 scale_bucket,
135 }
136 }
137
138 pub fn scene_range(
139 content_hash: u64,
140 local_bounds: Rect,
141 pixel_size: (u32, u32),
142 scale_bucket: ScaleBucket,
143 ) -> Self {
144 Self {
145 kind: LayerRasterCacheKind::SceneRange,
146 stable_id: None,
147 content_hash,
148 effect_hash: 0,
149 local_bounds_bits: [
150 local_bounds.x.to_bits(),
151 local_bounds.y.to_bits(),
152 local_bounds.width.to_bits(),
153 local_bounds.height.to_bits(),
154 ],
155 pixel_size: [pixel_size.0, pixel_size.1],
156 scale_bucket,
157 }
158 }
159
160 pub fn stable_id(self) -> Option<NodeId> {
161 self.stable_id
162 }
163
164 pub fn is_scene_range(self) -> bool {
165 self.kind == LayerRasterCacheKind::SceneRange
166 }
167
168 pub fn identity(self) -> Option<LayerRasterCacheIdentity> {
169 Some(LayerRasterCacheIdentity {
170 stable_id: self.stable_id?,
171 kind: self.kind.identity_kind(),
172 })
173 }
174
175 pub fn pixel_size(self) -> (u32, u32) {
176 (self.pixel_size[0], self.pixel_size[1])
177 }
178
179 pub fn scale_bucket(self) -> ScaleBucket {
180 self.scale_bucket
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn scale_bucket_normalizes_invalid_values() {
190 assert_eq!(
191 ScaleBucket::from_scale(0.0).raw(),
192 ScaleBucket::from_scale(1.0).raw()
193 );
194 assert_eq!(
195 ScaleBucket::from_scale(-3.0).raw(),
196 ScaleBucket::from_scale(1.0).raw()
197 );
198 assert_eq!(
199 ScaleBucket::from_scale(f32::NAN).raw(),
200 ScaleBucket::from_scale(1.0).raw()
201 );
202 }
203
204 #[test]
205 fn scale_bucket_quantizes_small_fractional_changes() {
206 let a = ScaleBucket::from_scale(1.0);
207 let b = ScaleBucket::from_scale(1.001);
208 let c = ScaleBucket::from_scale(1.01);
209 assert_eq!(a, b);
210 assert_ne!(a, c);
211 }
212
213 #[test]
214 fn layer_raster_cache_key_captures_bounds_and_pixel_size() {
215 let rect = Rect {
216 x: 1.0,
217 y: 2.0,
218 width: 30.0,
219 height: 40.0,
220 };
221 let base = LayerRasterCacheKey::new(
222 Some(7),
223 11,
224 13,
225 rect,
226 (30, 40),
227 ScaleBucket::from_scale(1.0),
228 );
229 let moved = LayerRasterCacheKey::new(
230 Some(7),
231 11,
232 13,
233 Rect { x: 2.0, ..rect },
234 (30, 40),
235 ScaleBucket::from_scale(1.0),
236 );
237 let resized = LayerRasterCacheKey::new(
238 Some(7),
239 11,
240 13,
241 rect,
242 (60, 80),
243 ScaleBucket::from_scale(2.0),
244 );
245
246 assert_ne!(base, moved);
247 assert_ne!(base, resized);
248 assert_eq!(base.stable_id(), Some(7));
249 assert_eq!(base.pixel_size(), (30, 40));
250 }
251
252 #[test]
253 fn source_content_keys_do_not_collide_with_full_surface_keys() {
254 let rect = Rect {
255 x: 1.0,
256 y: 2.0,
257 width: 30.0,
258 height: 40.0,
259 };
260 let scale = ScaleBucket::from_scale(1.0);
261 let source = LayerRasterCacheKey::source_content(Some(7), 11, rect, (30, 40), scale);
262 let full = LayerRasterCacheKey::new(Some(7), 11, 0, rect, (30, 40), scale);
263
264 assert_ne!(source, full);
265 assert_ne!(source.identity(), full.identity());
266 }
267
268 #[test]
269 fn backdrop_effect_keys_do_not_collide_with_layer_surface_keys() {
270 let rect = Rect {
271 x: 1.0,
272 y: 2.0,
273 width: 30.0,
274 height: 40.0,
275 };
276 let scale = ScaleBucket::from_scale(1.0);
277 let backdrop = LayerRasterCacheKey::backdrop_effect(Some(7), 11, 13, rect, (30, 40), scale);
278 let source = LayerRasterCacheKey::source_content(Some(7), 11, rect, (30, 40), scale);
279 let full = LayerRasterCacheKey::new(Some(7), 11, 13, rect, (30, 40), scale);
280
281 assert_ne!(backdrop, source);
282 assert_ne!(backdrop, full);
283 assert_ne!(backdrop.identity(), source.identity());
284 assert_ne!(backdrop.identity(), full.identity());
285 }
286
287 #[test]
288 fn scene_range_keys_do_not_collide_with_layer_surface_keys() {
289 let rect = Rect {
290 x: 0.0,
291 y: 0.0,
292 width: 320.0,
293 height: 240.0,
294 };
295 let scale = ScaleBucket::from_scale(1.0);
296 let range = LayerRasterCacheKey::scene_range(11, rect, (320, 240), scale);
297 let source = LayerRasterCacheKey::source_content(None, 11, rect, (320, 240), scale);
298 let full = LayerRasterCacheKey::new(None, 11, 0, rect, (320, 240), scale);
299
300 assert_ne!(range, source);
301 assert_ne!(range, full);
302 assert_eq!(range.identity(), None);
303 }
304}