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 is_source_content(self) -> bool {
169 self.kind == LayerRasterCacheKind::SourceContent
170 }
171
172 pub fn identity(self) -> Option<LayerRasterCacheIdentity> {
173 Some(LayerRasterCacheIdentity {
174 stable_id: self.stable_id?,
175 kind: self.kind.identity_kind(),
176 })
177 }
178
179 pub fn pixel_size(self) -> (u32, u32) {
180 (self.pixel_size[0], self.pixel_size[1])
181 }
182
183 pub fn scale_bucket(self) -> ScaleBucket {
184 self.scale_bucket
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn scale_bucket_normalizes_invalid_values() {
194 assert_eq!(
195 ScaleBucket::from_scale(0.0).raw(),
196 ScaleBucket::from_scale(1.0).raw()
197 );
198 assert_eq!(
199 ScaleBucket::from_scale(-3.0).raw(),
200 ScaleBucket::from_scale(1.0).raw()
201 );
202 assert_eq!(
203 ScaleBucket::from_scale(f32::NAN).raw(),
204 ScaleBucket::from_scale(1.0).raw()
205 );
206 }
207
208 #[test]
209 fn scale_bucket_quantizes_small_fractional_changes() {
210 let a = ScaleBucket::from_scale(1.0);
211 let b = ScaleBucket::from_scale(1.001);
212 let c = ScaleBucket::from_scale(1.01);
213 assert_eq!(a, b);
214 assert_ne!(a, c);
215 }
216
217 #[test]
218 fn layer_raster_cache_key_captures_bounds_and_pixel_size() {
219 let rect = Rect {
220 x: 1.0,
221 y: 2.0,
222 width: 30.0,
223 height: 40.0,
224 };
225 let base = LayerRasterCacheKey::new(
226 Some(7),
227 11,
228 13,
229 rect,
230 (30, 40),
231 ScaleBucket::from_scale(1.0),
232 );
233 let moved = LayerRasterCacheKey::new(
234 Some(7),
235 11,
236 13,
237 Rect { x: 2.0, ..rect },
238 (30, 40),
239 ScaleBucket::from_scale(1.0),
240 );
241 let resized = LayerRasterCacheKey::new(
242 Some(7),
243 11,
244 13,
245 rect,
246 (60, 80),
247 ScaleBucket::from_scale(2.0),
248 );
249
250 assert_ne!(base, moved);
251 assert_ne!(base, resized);
252 assert_eq!(base.stable_id(), Some(7));
253 assert_eq!(base.pixel_size(), (30, 40));
254 }
255
256 #[test]
257 fn source_content_keys_do_not_collide_with_full_surface_keys() {
258 let rect = Rect {
259 x: 1.0,
260 y: 2.0,
261 width: 30.0,
262 height: 40.0,
263 };
264 let scale = ScaleBucket::from_scale(1.0);
265 let source = LayerRasterCacheKey::source_content(Some(7), 11, rect, (30, 40), scale);
266 let full = LayerRasterCacheKey::new(Some(7), 11, 0, rect, (30, 40), scale);
267
268 assert_ne!(source, full);
269 assert_ne!(source.identity(), full.identity());
270 }
271
272 #[test]
273 fn backdrop_effect_keys_do_not_collide_with_layer_surface_keys() {
274 let rect = Rect {
275 x: 1.0,
276 y: 2.0,
277 width: 30.0,
278 height: 40.0,
279 };
280 let scale = ScaleBucket::from_scale(1.0);
281 let backdrop = LayerRasterCacheKey::backdrop_effect(Some(7), 11, 13, rect, (30, 40), scale);
282 let source = LayerRasterCacheKey::source_content(Some(7), 11, rect, (30, 40), scale);
283 let full = LayerRasterCacheKey::new(Some(7), 11, 13, rect, (30, 40), scale);
284
285 assert_ne!(backdrop, source);
286 assert_ne!(backdrop, full);
287 assert_ne!(backdrop.identity(), source.identity());
288 assert_ne!(backdrop.identity(), full.identity());
289 }
290
291 #[test]
292 fn scene_range_keys_do_not_collide_with_layer_surface_keys() {
293 let rect = Rect {
294 x: 0.0,
295 y: 0.0,
296 width: 320.0,
297 height: 240.0,
298 };
299 let scale = ScaleBucket::from_scale(1.0);
300 let range = LayerRasterCacheKey::scene_range(11, rect, (320, 240), scale);
301 let source = LayerRasterCacheKey::source_content(None, 11, rect, (320, 240), scale);
302 let full = LayerRasterCacheKey::new(None, 11, 0, rect, (320, 240), scale);
303
304 assert_ne!(range, source);
305 assert_ne!(range, full);
306 assert_eq!(range.identity(), None);
307 }
308}