1use crate::{
2 Brush, Color, ColorFilter, CornerRadii, DrawPrimitive, FxHasher, ImageBitmap, LayerShape,
3 Point, Rect, RenderEffect, RuntimeShader, ShadowPrimitive, Stroke,
4};
5use std::hash::{Hash, Hasher};
6
7pub trait RenderHash {
8 fn render_hash(&self) -> u64;
9}
10
11impl RenderHash for Color {
12 fn render_hash(&self) -> u64 {
13 finish_hash(|state| hash_color(*self, state))
14 }
15}
16
17impl RenderHash for Point {
18 fn render_hash(&self) -> u64 {
19 finish_hash(|state| hash_point(*self, state))
20 }
21}
22
23impl RenderHash for Rect {
24 fn render_hash(&self) -> u64 {
25 finish_hash(|state| hash_rect(*self, state))
26 }
27}
28
29impl RenderHash for CornerRadii {
30 fn render_hash(&self) -> u64 {
31 finish_hash(|state| hash_corner_radii(*self, state))
32 }
33}
34
35impl RenderHash for LayerShape {
36 fn render_hash(&self) -> u64 {
37 finish_hash(|state| hash_layer_shape(*self, state))
38 }
39}
40
41impl RenderHash for Stroke {
42 fn render_hash(&self) -> u64 {
43 finish_hash(|state| hash_stroke(*self, state))
44 }
45}
46
47impl RenderHash for Brush {
48 fn render_hash(&self) -> u64 {
49 finish_hash(|state| hash_brush(self, state))
50 }
51}
52
53impl RenderHash for ColorFilter {
54 fn render_hash(&self) -> u64 {
55 finish_hash(|state| hash_color_filter(*self, state))
56 }
57}
58
59impl RenderHash for ImageBitmap {
60 fn render_hash(&self) -> u64 {
61 finish_hash(|state| self.id().hash(state))
62 }
63}
64
65impl RenderHash for RuntimeShader {
66 fn render_hash(&self) -> u64 {
67 finish_hash(|state| hash_runtime_shader(self, state))
68 }
69}
70
71impl RenderHash for RenderEffect {
72 fn render_hash(&self) -> u64 {
73 finish_hash(|state| hash_render_effect(self, state))
74 }
75}
76
77impl RenderHash for DrawPrimitive {
78 fn render_hash(&self) -> u64 {
79 finish_hash(|state| hash_draw_primitive(self, state))
80 }
81}
82
83impl RenderHash for ShadowPrimitive {
84 fn render_hash(&self) -> u64 {
85 finish_hash(|state| hash_shadow_primitive(self, state))
86 }
87}
88
89fn finish_hash(write: impl FnOnce(&mut FxHasher)) -> u64 {
90 let mut hasher = FxHasher::default();
91 write(&mut hasher);
92 hasher.finish()
93}
94
95fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
96 value.to_bits().hash(state);
97}
98
99fn hash_color<H: Hasher>(color: Color, state: &mut H) {
100 hash_f32_bits(color.0, state);
101 hash_f32_bits(color.1, state);
102 hash_f32_bits(color.2, state);
103 hash_f32_bits(color.3, state);
104}
105
106fn hash_point<H: Hasher>(point: Point, state: &mut H) {
107 hash_f32_bits(point.x, state);
108 hash_f32_bits(point.y, state);
109}
110
111fn hash_rect<H: Hasher>(rect: Rect, state: &mut H) {
112 hash_f32_bits(rect.x, state);
113 hash_f32_bits(rect.y, state);
114 hash_f32_bits(rect.width, state);
115 hash_f32_bits(rect.height, state);
116}
117
118fn hash_corner_radii<H: Hasher>(radii: CornerRadii, state: &mut H) {
119 hash_f32_bits(radii.top_left, state);
120 hash_f32_bits(radii.top_right, state);
121 hash_f32_bits(radii.bottom_right, state);
122 hash_f32_bits(radii.bottom_left, state);
123}
124
125fn hash_stroke<H: Hasher>(stroke: Stroke, state: &mut H) {
129 hash_f32_bits(stroke.width, state);
130 stroke.cap.hash(state);
131 stroke.join.hash(state);
132}
133
134fn hash_optional_stroke<H: Hasher>(stroke: Option<Stroke>, state: &mut H) {
135 match stroke {
136 Some(stroke) => {
137 1u8.hash(state);
138 hash_stroke(stroke, state);
139 }
140 None => 0u8.hash(state),
141 }
142}
143
144fn hash_layer_shape<H: Hasher>(shape: LayerShape, state: &mut H) {
145 match shape {
146 LayerShape::Rectangle => 0u8.hash(state),
147 LayerShape::Rounded(shape) => {
148 1u8.hash(state);
149 hash_corner_radii(shape.radii(), state);
150 }
151 }
152}
153
154fn hash_brush<H: Hasher>(brush: &Brush, state: &mut H) {
155 match brush {
156 Brush::Solid(color) => {
157 0u8.hash(state);
158 hash_color(*color, state);
159 }
160 Brush::LinearGradient {
161 colors,
162 stops,
163 start,
164 end,
165 tile_mode,
166 } => {
167 1u8.hash(state);
168 hash_color_slice(colors, state);
169 hash_optional_stop_list(stops.as_deref(), state);
170 hash_point(*start, state);
171 hash_point(*end, state);
172 tile_mode.hash(state);
173 }
174 Brush::RadialGradient {
175 colors,
176 stops,
177 center,
178 radius,
179 tile_mode,
180 } => {
181 2u8.hash(state);
182 hash_color_slice(colors, state);
183 hash_optional_stop_list(stops.as_deref(), state);
184 hash_point(*center, state);
185 hash_f32_bits(*radius, state);
186 tile_mode.hash(state);
187 }
188 Brush::SweepGradient {
189 colors,
190 stops,
191 center,
192 } => {
193 3u8.hash(state);
194 hash_color_slice(colors, state);
195 hash_optional_stop_list(stops.as_deref(), state);
196 hash_point(*center, state);
197 }
198 }
199}
200
201fn hash_color_slice<H: Hasher>(colors: &[Color], state: &mut H) {
202 colors.len().hash(state);
203 for color in colors {
204 hash_color(*color, state);
205 }
206}
207
208fn hash_optional_stop_list<H: Hasher>(stops: Option<&[f32]>, state: &mut H) {
209 match stops {
210 Some(stops) => {
211 1u8.hash(state);
212 stops.len().hash(state);
213 for stop in stops {
214 hash_f32_bits(*stop, state);
215 }
216 }
217 None => 0u8.hash(state),
218 }
219}
220
221fn hash_color_filter<H: Hasher>(filter: ColorFilter, state: &mut H) {
222 match filter {
223 ColorFilter::Tint(color) => {
224 0u8.hash(state);
225 hash_color(color, state);
226 }
227 ColorFilter::Modulate(color) => {
228 1u8.hash(state);
229 hash_color(color, state);
230 }
231 ColorFilter::Matrix(matrix) => {
232 2u8.hash(state);
233 for value in matrix {
234 hash_f32_bits(value, state);
235 }
236 }
237 }
238}
239
240fn hash_runtime_shader<H: Hasher>(shader: &RuntimeShader, state: &mut H) {
241 shader.source_hash().hash(state);
247 hash_f32_bits(shader.input_padding(), state);
248}
249
250fn hash_render_effect<H: Hasher>(effect: &RenderEffect, state: &mut H) {
251 match effect {
252 RenderEffect::Blur {
253 radius_x,
254 radius_y,
255 edge_treatment,
256 } => {
257 0u8.hash(state);
258 hash_f32_bits(*radius_x, state);
259 hash_f32_bits(*radius_y, state);
260 edge_treatment.hash(state);
261 }
262 RenderEffect::Offset { offset_x, offset_y } => {
263 1u8.hash(state);
264 hash_f32_bits(*offset_x, state);
265 hash_f32_bits(*offset_y, state);
266 }
267 RenderEffect::Shader { shader } => {
268 2u8.hash(state);
269 hash_runtime_shader(shader, state);
270 }
271 RenderEffect::Chain { first, second } => {
272 3u8.hash(state);
273 hash_render_effect(first, state);
274 hash_render_effect(second, state);
275 }
276 }
277}
278
279fn hash_draw_primitive<H: Hasher>(primitive: &DrawPrimitive, state: &mut H) {
280 match primitive {
281 DrawPrimitive::Content => {
282 0u8.hash(state);
283 }
284 DrawPrimitive::Blend {
285 primitive,
286 blend_mode,
287 } => {
288 1u8.hash(state);
289 blend_mode.hash(state);
290 hash_draw_primitive(primitive, state);
291 }
292 DrawPrimitive::Rect {
293 rect,
294 brush,
295 stroke,
296 } => {
297 2u8.hash(state);
298 hash_rect(*rect, state);
299 hash_brush(brush, state);
300 hash_optional_stroke(*stroke, state);
301 }
302 DrawPrimitive::RoundRect {
303 rect,
304 brush,
305 radii,
306 stroke,
307 } => {
308 3u8.hash(state);
309 hash_rect(*rect, state);
310 hash_brush(brush, state);
311 hash_corner_radii(*radii, state);
312 hash_optional_stroke(*stroke, state);
313 }
314 DrawPrimitive::Arc {
315 rect,
316 brush,
317 center,
318 radius,
319 start_angle,
320 sweep_angle,
321 stroke,
322 inner_radius,
323 } => {
324 6u8.hash(state);
325 hash_rect(*rect, state);
326 hash_brush(brush, state);
327 hash_point(*center, state);
328 hash_f32_bits(*radius, state);
329 hash_f32_bits(*start_angle, state);
330 hash_f32_bits(*sweep_angle, state);
331 hash_optional_stroke(*stroke, state);
332 hash_f32_bits(*inner_radius, state);
333 }
334 DrawPrimitive::Image {
335 rect,
336 image,
337 alpha,
338 color_filter,
339 sampling,
340 src_rect,
341 } => {
342 4u8.hash(state);
343 hash_rect(*rect, state);
344 image.id().hash(state);
345 hash_f32_bits(*alpha, state);
346 sampling.hash(state);
347 match color_filter {
348 Some(filter) => {
349 1u8.hash(state);
350 hash_color_filter(*filter, state);
351 }
352 None => 0u8.hash(state),
353 }
354 match src_rect {
355 Some(rect) => {
356 1u8.hash(state);
357 hash_rect(*rect, state);
358 }
359 None => 0u8.hash(state),
360 }
361 }
362 DrawPrimitive::Text(text) => {
363 7u8.hash(state);
364 hash_rect(text.rect, state);
365 text.text.hash(state);
366 hash_text_style(&text.style, state);
367 hash_color(text.color, state);
368 }
369 DrawPrimitive::Shadow(shadow) => {
370 5u8.hash(state);
371 hash_shadow_primitive(shadow, state);
372 }
373 }
374}
375
376fn hash_text_style<H: Hasher>(style: &crate::TextStyle, state: &mut H) {
377 style.font_family.hash(state);
378 hash_f32_bits(style.font_size, state);
379 style.font_weight.hash(state);
380 style.font_style.hash(state);
381 hash_f32_bits(style.letter_spacing, state);
382 match style.line_height {
383 Some(line_height) => {
384 1u8.hash(state);
385 hash_f32_bits(line_height, state);
386 }
387 None => 0u8.hash(state),
388 }
389 style.align.hash(state);
390 style.vertical_align.hash(state);
391}
392
393fn hash_shadow_primitive<H: Hasher>(shadow: &ShadowPrimitive, state: &mut H) {
394 match shadow {
395 ShadowPrimitive::Drop {
396 shape,
397 cutout,
398 blur_radius,
399 blend_mode,
400 } => {
401 0u8.hash(state);
402 hash_draw_primitive(shape, state);
403 match cutout {
404 Some(cutout) => {
405 1u8.hash(state);
406 hash_draw_primitive(cutout, state);
407 }
408 None => 0u8.hash(state),
409 }
410 hash_f32_bits(*blur_radius, state);
411 blend_mode.hash(state);
412 }
413 ShadowPrimitive::Inner {
414 fill,
415 cutout,
416 blur_radius,
417 blend_mode,
418 clip_rect,
419 } => {
420 1u8.hash(state);
421 hash_draw_primitive(fill, state);
422 hash_draw_primitive(cutout, state);
423 hash_f32_bits(*blur_radius, state);
424 blend_mode.hash(state);
425 hash_rect(*clip_rect, state);
426 }
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use crate::render_effect::TileMode;
434
435 #[test]
436 fn color_render_hash_changes_with_channels() {
437 assert_ne!(
438 Color(1.0, 0.0, 0.0, 1.0).render_hash(),
439 Color(0.0, 1.0, 0.0, 1.0).render_hash()
440 );
441 }
442
443 #[test]
444 fn point_rect_and_corner_radii_render_hash_changes_with_geometry() {
445 assert_ne!(
446 Point::new(1.0, 2.0).render_hash(),
447 Point::new(2.0, 1.0).render_hash()
448 );
449 assert_ne!(
450 Rect {
451 x: 0.0,
452 y: 0.0,
453 width: 10.0,
454 height: 20.0,
455 }
456 .render_hash(),
457 Rect {
458 x: 0.0,
459 y: 0.0,
460 width: 20.0,
461 height: 10.0,
462 }
463 .render_hash()
464 );
465 assert_ne!(
466 CornerRadii::uniform(4.0).render_hash(),
467 CornerRadii::uniform(6.0).render_hash()
468 );
469 }
470
471 #[test]
472 fn layer_shape_render_hash_tracks_shape_kind_and_radii() {
473 assert_ne!(
474 LayerShape::Rectangle.render_hash(),
475 LayerShape::Rounded(crate::RoundedCornerShape::uniform(8.0)).render_hash()
476 );
477 assert_ne!(
478 LayerShape::Rounded(crate::RoundedCornerShape::uniform(4.0)).render_hash(),
479 LayerShape::Rounded(crate::RoundedCornerShape::uniform(8.0)).render_hash()
480 );
481 }
482
483 #[test]
484 fn brush_render_hash_tracks_gradient_structure() {
485 let base = Brush::linear_gradient_with_tile_mode(
486 vec![Color::RED, Color::BLUE],
487 Point::new(0.0, 0.0),
488 Point::new(10.0, 10.0),
489 TileMode::Clamp,
490 );
491 let shifted = Brush::linear_gradient_with_tile_mode(
492 vec![Color::RED, Color::BLUE],
493 Point::new(1.0, 0.0),
494 Point::new(10.0, 10.0),
495 TileMode::Clamp,
496 );
497
498 assert_ne!(base.render_hash(), shifted.render_hash());
499 }
500
501 #[test]
502 fn color_filter_render_hash_tracks_variant_and_values() {
503 assert_ne!(
504 ColorFilter::Tint(Color::RED).render_hash(),
505 ColorFilter::Modulate(Color::RED).render_hash()
506 );
507 assert_ne!(
508 ColorFilter::Matrix([1.0; 20]).render_hash(),
509 ColorFilter::Matrix([0.0; 20]).render_hash()
510 );
511 }
512
513 #[test]
514 fn render_effect_render_hash_tracks_variant_parameters() {
515 assert_ne!(
516 RenderEffect::blur(4.0).render_hash(),
517 RenderEffect::blur(6.0).render_hash()
518 );
519 assert_ne!(
520 RenderEffect::offset(2.0, 1.0).render_hash(),
521 RenderEffect::offset(1.0, 2.0).render_hash()
522 );
523 }
524
525 #[test]
526 fn runtime_shader_render_hash_ignores_uniforms() {
527 let mut base = RuntimeShader::new("// hash");
528 base.set_float(0, 1.0);
529 let mut changed = base.clone();
530 changed.set_float(0, 2.0);
531 assert_eq!(
532 base.render_hash(),
533 changed.render_hash(),
534 "render_hash must depend only on source, not uniforms — \
535 animated uniforms (time, position) would otherwise produce a \
536 new effect_hash every frame, filling the layer cache with stale textures"
537 );
538 }
539
540 #[test]
541 fn runtime_shader_render_hash_tracks_source() {
542 let a = RuntimeShader::new("// shader A");
543 let b = RuntimeShader::new("// shader B");
544 assert_ne!(a.render_hash(), b.render_hash());
545 }
546
547 #[test]
548 fn draw_primitive_render_hash_tracks_nested_structure() {
549 let base = DrawPrimitive::Blend {
550 primitive: Box::new(DrawPrimitive::Rect {
551 rect: Rect {
552 x: 0.0,
553 y: 0.0,
554 width: 12.0,
555 height: 8.0,
556 },
557 brush: Brush::solid(Color::WHITE),
558 stroke: None,
559 }),
560 blend_mode: crate::BlendMode::SrcOver,
561 };
562 let changed = DrawPrimitive::Blend {
563 primitive: Box::new(DrawPrimitive::Rect {
564 rect: Rect {
565 x: 0.0,
566 y: 0.0,
567 width: 12.0,
568 height: 8.0,
569 },
570 brush: Brush::solid(Color::BLACK),
571 stroke: None,
572 }),
573 blend_mode: crate::BlendMode::SrcOver,
574 };
575 assert_ne!(base.render_hash(), changed.render_hash());
576 }
577
578 use crate::{Stroke, StrokeCap, StrokeJoin};
585
586 fn stroked_rect(stroke: Option<Stroke>) -> DrawPrimitive {
587 DrawPrimitive::Rect {
588 rect: Rect {
589 x: 0.0,
590 y: 0.0,
591 width: 12.0,
592 height: 8.0,
593 },
594 brush: Brush::solid(Color::WHITE),
595 stroke,
596 }
597 }
598
599 fn arc(
600 radius: f32,
601 start_angle: f32,
602 sweep_angle: f32,
603 stroke: Option<Stroke>,
604 inner_radius: f32,
605 ) -> DrawPrimitive {
606 DrawPrimitive::Arc {
607 rect: Rect {
608 x: 0.0,
609 y: 0.0,
610 width: 40.0,
611 height: 40.0,
612 },
613 brush: Brush::solid(Color::WHITE),
614 center: Point::new(20.0, 20.0),
615 radius,
616 start_angle,
617 sweep_angle,
618 stroke,
619 inner_radius,
620 }
621 }
622
623 #[test]
624 fn stroke_render_hash_tracks_width_cap_and_join() {
625 let base = Stroke::new(2.0);
626 assert_ne!(base.render_hash(), Stroke::new(3.0).render_hash());
627 assert_ne!(
628 base.render_hash(),
629 base.with_cap(StrokeCap::Round).render_hash()
630 );
631 assert_ne!(
632 base.render_hash(),
633 base.with_join(StrokeJoin::Bevel).render_hash()
634 );
635 assert_eq!(base.render_hash(), Stroke::new(2.0).render_hash());
636 }
637
638 #[test]
639 fn rect_render_hash_separates_fill_from_stroke() {
640 let fill = stroked_rect(None);
641 let stroked = stroked_rect(Some(Stroke::new(2.0)));
642 assert_ne!(fill.render_hash(), stroked.render_hash());
643 assert_eq!(
644 stroked.render_hash(),
645 stroked_rect(Some(Stroke::new(2.0))).render_hash()
646 );
647 }
648
649 #[test]
650 fn rect_render_hash_tracks_every_stroke_field() {
651 let base = Stroke::new(2.0);
652 let base_hash = stroked_rect(Some(base)).render_hash();
653 assert_ne!(
654 base_hash,
655 stroked_rect(Some(base.with_width(2.5))).render_hash()
656 );
657 assert_ne!(
658 base_hash,
659 stroked_rect(Some(base.with_cap(StrokeCap::Square))).render_hash()
660 );
661 assert_ne!(
662 base_hash,
663 stroked_rect(Some(base.with_join(StrokeJoin::Round))).render_hash()
664 );
665 }
666
667 #[test]
668 fn round_rect_render_hash_tracks_stroke() {
669 let make = |stroke| DrawPrimitive::RoundRect {
670 rect: Rect {
671 x: 0.0,
672 y: 0.0,
673 width: 12.0,
674 height: 8.0,
675 },
676 brush: Brush::solid(Color::WHITE),
677 radii: CornerRadii::uniform(3.0),
678 stroke,
679 };
680 assert_ne!(
681 make(None).render_hash(),
682 make(Some(Stroke::new(2.0))).render_hash()
683 );
684 assert_ne!(
685 make(Some(Stroke::new(2.0))).render_hash(),
686 make(Some(Stroke::new(2.0).with_join(StrokeJoin::Bevel))).render_hash()
687 );
688 }
689
690 #[test]
691 fn arc_render_hash_tracks_angles_radii_and_stroke() {
692 let stroke = Some(Stroke::new(4.0));
693 let base = arc(10.0, 0.0, 1.0, stroke, 0.0);
694 let base_hash = base.render_hash();
695
696 assert_eq!(base_hash, arc(10.0, 0.0, 1.0, stroke, 0.0).render_hash());
697 assert_ne!(base_hash, arc(11.0, 0.0, 1.0, stroke, 0.0).render_hash());
698 assert_ne!(base_hash, arc(10.0, 0.5, 1.0, stroke, 0.0).render_hash());
699 assert_ne!(base_hash, arc(10.0, 0.0, 1.5, stroke, 0.0).render_hash());
700 assert_ne!(base_hash, arc(10.0, 0.0, -1.0, stroke, 0.0).render_hash());
701 assert_ne!(base_hash, arc(10.0, 0.0, 1.0, stroke, 4.0).render_hash());
702 assert_ne!(base_hash, arc(10.0, 0.0, 1.0, None, 0.0).render_hash());
703 assert_ne!(
704 base_hash,
705 arc(
706 10.0,
707 0.0,
708 1.0,
709 Some(Stroke::new(4.0).with_cap(StrokeCap::Round)),
710 0.0
711 )
712 .render_hash()
713 );
714 }
715
716 #[test]
717 fn arc_render_hash_differs_from_rect_with_same_bounds() {
718 assert_ne!(
719 arc(10.0, 0.0, 1.0, None, 4.0).render_hash(),
720 DrawPrimitive::Rect {
721 rect: Rect {
722 x: 0.0,
723 y: 0.0,
724 width: 40.0,
725 height: 40.0,
726 },
727 brush: Brush::solid(Color::WHITE),
728 stroke: None,
729 }
730 .render_hash()
731 );
732 }
733}