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