1use crate::{
2 Brush, Color, ColorFilter, CornerRadii, DrawPrimitive, ImageBitmap, LayerShape, Point, Rect,
3 RenderEffect, RuntimeShader, ShadowPrimitive,
4};
5use std::collections::hash_map::DefaultHasher;
6use std::hash::{Hash, Hasher};
7
8pub trait RenderHash {
9 fn render_hash(&self) -> u64;
10}
11
12impl RenderHash for Color {
13 fn render_hash(&self) -> u64 {
14 finish_hash(|state| hash_color(*self, state))
15 }
16}
17
18impl RenderHash for Point {
19 fn render_hash(&self) -> u64 {
20 finish_hash(|state| hash_point(*self, state))
21 }
22}
23
24impl RenderHash for Rect {
25 fn render_hash(&self) -> u64 {
26 finish_hash(|state| hash_rect(*self, state))
27 }
28}
29
30impl RenderHash for CornerRadii {
31 fn render_hash(&self) -> u64 {
32 finish_hash(|state| hash_corner_radii(*self, state))
33 }
34}
35
36impl RenderHash for LayerShape {
37 fn render_hash(&self) -> u64 {
38 finish_hash(|state| hash_layer_shape(*self, state))
39 }
40}
41
42impl RenderHash for Brush {
43 fn render_hash(&self) -> u64 {
44 finish_hash(|state| hash_brush(self, state))
45 }
46}
47
48impl RenderHash for ColorFilter {
49 fn render_hash(&self) -> u64 {
50 finish_hash(|state| hash_color_filter(*self, state))
51 }
52}
53
54impl RenderHash for ImageBitmap {
55 fn render_hash(&self) -> u64 {
56 finish_hash(|state| self.id().hash(state))
57 }
58}
59
60impl RenderHash for RuntimeShader {
61 fn render_hash(&self) -> u64 {
62 finish_hash(|state| hash_runtime_shader(self, state))
63 }
64}
65
66impl RenderHash for RenderEffect {
67 fn render_hash(&self) -> u64 {
68 finish_hash(|state| hash_render_effect(self, state))
69 }
70}
71
72impl RenderHash for DrawPrimitive {
73 fn render_hash(&self) -> u64 {
74 finish_hash(|state| hash_draw_primitive(self, state))
75 }
76}
77
78impl RenderHash for ShadowPrimitive {
79 fn render_hash(&self) -> u64 {
80 finish_hash(|state| hash_shadow_primitive(self, state))
81 }
82}
83
84fn finish_hash(write: impl FnOnce(&mut DefaultHasher)) -> u64 {
85 let mut hasher = DefaultHasher::new();
86 write(&mut hasher);
87 hasher.finish()
88}
89
90fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
91 value.to_bits().hash(state);
92}
93
94fn hash_color<H: Hasher>(color: Color, state: &mut H) {
95 hash_f32_bits(color.0, state);
96 hash_f32_bits(color.1, state);
97 hash_f32_bits(color.2, state);
98 hash_f32_bits(color.3, state);
99}
100
101fn hash_point<H: Hasher>(point: Point, state: &mut H) {
102 hash_f32_bits(point.x, state);
103 hash_f32_bits(point.y, state);
104}
105
106fn hash_rect<H: Hasher>(rect: Rect, state: &mut H) {
107 hash_f32_bits(rect.x, state);
108 hash_f32_bits(rect.y, state);
109 hash_f32_bits(rect.width, state);
110 hash_f32_bits(rect.height, state);
111}
112
113fn hash_corner_radii<H: Hasher>(radii: CornerRadii, state: &mut H) {
114 hash_f32_bits(radii.top_left, state);
115 hash_f32_bits(radii.top_right, state);
116 hash_f32_bits(radii.bottom_right, state);
117 hash_f32_bits(radii.bottom_left, state);
118}
119
120fn hash_layer_shape<H: Hasher>(shape: LayerShape, state: &mut H) {
121 match shape {
122 LayerShape::Rectangle => 0u8.hash(state),
123 LayerShape::Rounded(shape) => {
124 1u8.hash(state);
125 hash_corner_radii(shape.radii(), state);
126 }
127 }
128}
129
130fn hash_brush<H: Hasher>(brush: &Brush, state: &mut H) {
131 match brush {
132 Brush::Solid(color) => {
133 0u8.hash(state);
134 hash_color(*color, state);
135 }
136 Brush::LinearGradient {
137 colors,
138 stops,
139 start,
140 end,
141 tile_mode,
142 } => {
143 1u8.hash(state);
144 hash_color_slice(colors, state);
145 hash_optional_stop_list(stops.as_deref(), state);
146 hash_point(*start, state);
147 hash_point(*end, state);
148 tile_mode.hash(state);
149 }
150 Brush::RadialGradient {
151 colors,
152 stops,
153 center,
154 radius,
155 tile_mode,
156 } => {
157 2u8.hash(state);
158 hash_color_slice(colors, state);
159 hash_optional_stop_list(stops.as_deref(), state);
160 hash_point(*center, state);
161 hash_f32_bits(*radius, state);
162 tile_mode.hash(state);
163 }
164 Brush::SweepGradient {
165 colors,
166 stops,
167 center,
168 } => {
169 3u8.hash(state);
170 hash_color_slice(colors, state);
171 hash_optional_stop_list(stops.as_deref(), state);
172 hash_point(*center, state);
173 }
174 }
175}
176
177fn hash_color_slice<H: Hasher>(colors: &[Color], state: &mut H) {
178 colors.len().hash(state);
179 for color in colors {
180 hash_color(*color, state);
181 }
182}
183
184fn hash_optional_stop_list<H: Hasher>(stops: Option<&[f32]>, state: &mut H) {
185 match stops {
186 Some(stops) => {
187 1u8.hash(state);
188 stops.len().hash(state);
189 for stop in stops {
190 hash_f32_bits(*stop, state);
191 }
192 }
193 None => 0u8.hash(state),
194 }
195}
196
197fn hash_color_filter<H: Hasher>(filter: ColorFilter, state: &mut H) {
198 match filter {
199 ColorFilter::Tint(color) => {
200 0u8.hash(state);
201 hash_color(color, state);
202 }
203 ColorFilter::Modulate(color) => {
204 1u8.hash(state);
205 hash_color(color, state);
206 }
207 ColorFilter::Matrix(matrix) => {
208 2u8.hash(state);
209 for value in matrix {
210 hash_f32_bits(value, state);
211 }
212 }
213 }
214}
215
216fn hash_runtime_shader<H: Hasher>(shader: &RuntimeShader, state: &mut H) {
217 shader.source_hash().hash(state);
223 hash_f32_bits(shader.input_padding(), state);
224}
225
226fn hash_render_effect<H: Hasher>(effect: &RenderEffect, state: &mut H) {
227 match effect {
228 RenderEffect::Blur {
229 radius_x,
230 radius_y,
231 edge_treatment,
232 } => {
233 0u8.hash(state);
234 hash_f32_bits(*radius_x, state);
235 hash_f32_bits(*radius_y, state);
236 edge_treatment.hash(state);
237 }
238 RenderEffect::Offset { offset_x, offset_y } => {
239 1u8.hash(state);
240 hash_f32_bits(*offset_x, state);
241 hash_f32_bits(*offset_y, state);
242 }
243 RenderEffect::Shader { shader } => {
244 2u8.hash(state);
245 hash_runtime_shader(shader, state);
246 }
247 RenderEffect::Chain { first, second } => {
248 3u8.hash(state);
249 hash_render_effect(first, state);
250 hash_render_effect(second, state);
251 }
252 }
253}
254
255fn hash_draw_primitive<H: Hasher>(primitive: &DrawPrimitive, state: &mut H) {
256 match primitive {
257 DrawPrimitive::Content => {
258 0u8.hash(state);
259 }
260 DrawPrimitive::Blend {
261 primitive,
262 blend_mode,
263 } => {
264 1u8.hash(state);
265 blend_mode.hash(state);
266 hash_draw_primitive(primitive, state);
267 }
268 DrawPrimitive::Rect { rect, brush } => {
269 2u8.hash(state);
270 hash_rect(*rect, state);
271 hash_brush(brush, state);
272 }
273 DrawPrimitive::RoundRect { rect, brush, radii } => {
274 3u8.hash(state);
275 hash_rect(*rect, state);
276 hash_brush(brush, state);
277 hash_corner_radii(*radii, state);
278 }
279 DrawPrimitive::Image {
280 rect,
281 image,
282 alpha,
283 color_filter,
284 sampling,
285 src_rect,
286 } => {
287 4u8.hash(state);
288 hash_rect(*rect, state);
289 image.id().hash(state);
290 hash_f32_bits(*alpha, state);
291 sampling.hash(state);
292 match color_filter {
293 Some(filter) => {
294 1u8.hash(state);
295 hash_color_filter(*filter, state);
296 }
297 None => 0u8.hash(state),
298 }
299 match src_rect {
300 Some(rect) => {
301 1u8.hash(state);
302 hash_rect(*rect, state);
303 }
304 None => 0u8.hash(state),
305 }
306 }
307 DrawPrimitive::Shadow(shadow) => {
308 5u8.hash(state);
309 hash_shadow_primitive(shadow, state);
310 }
311 }
312}
313
314fn hash_shadow_primitive<H: Hasher>(shadow: &ShadowPrimitive, state: &mut H) {
315 match shadow {
316 ShadowPrimitive::Drop {
317 shape,
318 cutout,
319 blur_radius,
320 blend_mode,
321 } => {
322 0u8.hash(state);
323 hash_draw_primitive(shape, state);
324 match cutout {
325 Some(cutout) => {
326 1u8.hash(state);
327 hash_draw_primitive(cutout, state);
328 }
329 None => 0u8.hash(state),
330 }
331 hash_f32_bits(*blur_radius, state);
332 blend_mode.hash(state);
333 }
334 ShadowPrimitive::Inner {
335 fill,
336 cutout,
337 blur_radius,
338 blend_mode,
339 clip_rect,
340 } => {
341 1u8.hash(state);
342 hash_draw_primitive(fill, state);
343 hash_draw_primitive(cutout, state);
344 hash_f32_bits(*blur_radius, state);
345 blend_mode.hash(state);
346 hash_rect(*clip_rect, state);
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::render_effect::TileMode;
355
356 #[test]
357 fn color_render_hash_changes_with_channels() {
358 assert_ne!(
359 Color(1.0, 0.0, 0.0, 1.0).render_hash(),
360 Color(0.0, 1.0, 0.0, 1.0).render_hash()
361 );
362 }
363
364 #[test]
365 fn point_rect_and_corner_radii_render_hash_changes_with_geometry() {
366 assert_ne!(
367 Point::new(1.0, 2.0).render_hash(),
368 Point::new(2.0, 1.0).render_hash()
369 );
370 assert_ne!(
371 Rect {
372 x: 0.0,
373 y: 0.0,
374 width: 10.0,
375 height: 20.0,
376 }
377 .render_hash(),
378 Rect {
379 x: 0.0,
380 y: 0.0,
381 width: 20.0,
382 height: 10.0,
383 }
384 .render_hash()
385 );
386 assert_ne!(
387 CornerRadii::uniform(4.0).render_hash(),
388 CornerRadii::uniform(6.0).render_hash()
389 );
390 }
391
392 #[test]
393 fn layer_shape_render_hash_tracks_shape_kind_and_radii() {
394 assert_ne!(
395 LayerShape::Rectangle.render_hash(),
396 LayerShape::Rounded(crate::RoundedCornerShape::uniform(8.0)).render_hash()
397 );
398 assert_ne!(
399 LayerShape::Rounded(crate::RoundedCornerShape::uniform(4.0)).render_hash(),
400 LayerShape::Rounded(crate::RoundedCornerShape::uniform(8.0)).render_hash()
401 );
402 }
403
404 #[test]
405 fn brush_render_hash_tracks_gradient_structure() {
406 let base = Brush::linear_gradient_with_tile_mode(
407 vec![Color::RED, Color::BLUE],
408 Point::new(0.0, 0.0),
409 Point::new(10.0, 10.0),
410 TileMode::Clamp,
411 );
412 let shifted = Brush::linear_gradient_with_tile_mode(
413 vec![Color::RED, Color::BLUE],
414 Point::new(1.0, 0.0),
415 Point::new(10.0, 10.0),
416 TileMode::Clamp,
417 );
418
419 assert_ne!(base.render_hash(), shifted.render_hash());
420 }
421
422 #[test]
423 fn color_filter_render_hash_tracks_variant_and_values() {
424 assert_ne!(
425 ColorFilter::Tint(Color::RED).render_hash(),
426 ColorFilter::Modulate(Color::RED).render_hash()
427 );
428 assert_ne!(
429 ColorFilter::Matrix([1.0; 20]).render_hash(),
430 ColorFilter::Matrix([0.0; 20]).render_hash()
431 );
432 }
433
434 #[test]
435 fn render_effect_render_hash_tracks_variant_parameters() {
436 assert_ne!(
437 RenderEffect::blur(4.0).render_hash(),
438 RenderEffect::blur(6.0).render_hash()
439 );
440 assert_ne!(
441 RenderEffect::offset(2.0, 1.0).render_hash(),
442 RenderEffect::offset(1.0, 2.0).render_hash()
443 );
444 }
445
446 #[test]
447 fn runtime_shader_render_hash_ignores_uniforms() {
448 let mut base = RuntimeShader::new("// hash");
449 base.set_float(0, 1.0);
450 let mut changed = base.clone();
451 changed.set_float(0, 2.0);
452 assert_eq!(
453 base.render_hash(),
454 changed.render_hash(),
455 "render_hash must depend only on source, not uniforms — \
456 animated uniforms (time, position) would otherwise produce a \
457 new effect_hash every frame, filling the layer cache with stale textures"
458 );
459 }
460
461 #[test]
462 fn runtime_shader_render_hash_tracks_source() {
463 let a = RuntimeShader::new("// shader A");
464 let b = RuntimeShader::new("// shader B");
465 assert_ne!(a.render_hash(), b.render_hash());
466 }
467
468 #[test]
469 fn draw_primitive_render_hash_tracks_nested_structure() {
470 let base = DrawPrimitive::Blend {
471 primitive: Box::new(DrawPrimitive::Rect {
472 rect: Rect {
473 x: 0.0,
474 y: 0.0,
475 width: 12.0,
476 height: 8.0,
477 },
478 brush: Brush::solid(Color::WHITE),
479 }),
480 blend_mode: crate::BlendMode::SrcOver,
481 };
482 let changed = DrawPrimitive::Blend {
483 primitive: Box::new(DrawPrimitive::Rect {
484 rect: Rect {
485 x: 0.0,
486 y: 0.0,
487 width: 12.0,
488 height: 8.0,
489 },
490 brush: Brush::solid(Color::BLACK),
491 }),
492 blend_mode: crate::BlendMode::SrcOver,
493 };
494 assert_ne!(base.render_hash(), changed.render_hash());
495 }
496}