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 shader.overrides_hash().hash(state);
244 hash_f32_bits(shader.input_padding(), state);
245 hash_f32_bits(shader.output_padding(), state);
246 for rect in [shader.output_support(), shader.sample_domain()] {
247 match rect {
248 Some(rect) => {
249 1u8.hash(state);
250 hash_f32_bits(rect.x, state);
251 hash_f32_bits(rect.y, state);
252 hash_f32_bits(rect.width, state);
253 hash_f32_bits(rect.height, state);
254 }
255 None => 0u8.hash(state),
256 }
257 }
258 shader.hash_substrates(state);
259 shader.uniforms().len().hash(state);
260 for uniform in shader.uniforms() {
261 hash_f32_bits(*uniform, state);
262 }
263}
264
265fn hash_render_effect<H: Hasher>(effect: &RenderEffect, state: &mut H) {
266 match effect {
267 RenderEffect::Blur {
268 radius_x,
269 radius_y,
270 edge_treatment,
271 } => {
272 0u8.hash(state);
273 hash_f32_bits(*radius_x, state);
274 hash_f32_bits(*radius_y, state);
275 edge_treatment.hash(state);
276 }
277 RenderEffect::Offset { offset_x, offset_y } => {
278 1u8.hash(state);
279 hash_f32_bits(*offset_x, state);
280 hash_f32_bits(*offset_y, state);
281 }
282 RenderEffect::Shader { shader } => {
283 2u8.hash(state);
284 hash_runtime_shader(shader, state);
285 }
286 RenderEffect::Chain { first, second } => {
287 3u8.hash(state);
288 hash_render_effect(first, state);
289 hash_render_effect(second, state);
290 }
291 }
292}
293
294fn hash_draw_primitive<H: Hasher>(primitive: &DrawPrimitive, state: &mut H) {
295 match primitive {
296 DrawPrimitive::Content => {
297 0u8.hash(state);
298 }
299 DrawPrimitive::Blend {
300 primitive,
301 blend_mode,
302 } => {
303 1u8.hash(state);
304 blend_mode.hash(state);
305 hash_draw_primitive(primitive, state);
306 }
307 DrawPrimitive::Rect {
308 rect,
309 brush,
310 stroke,
311 } => {
312 2u8.hash(state);
313 hash_rect(*rect, state);
314 hash_brush(brush, state);
315 hash_optional_stroke(*stroke, state);
316 }
317 DrawPrimitive::RoundRect {
318 rect,
319 brush,
320 radii,
321 stroke,
322 } => {
323 3u8.hash(state);
324 hash_rect(*rect, state);
325 hash_brush(brush, state);
326 hash_corner_radii(*radii, state);
327 hash_optional_stroke(*stroke, state);
328 }
329 DrawPrimitive::Arc {
330 rect,
331 brush,
332 center,
333 radius,
334 start_angle,
335 sweep_angle,
336 stroke,
337 inner_radius,
338 } => {
339 6u8.hash(state);
340 hash_rect(*rect, state);
341 hash_brush(brush, state);
342 hash_point(*center, state);
343 hash_f32_bits(*radius, state);
344 hash_f32_bits(*start_angle, state);
345 hash_f32_bits(*sweep_angle, state);
346 hash_optional_stroke(*stroke, state);
347 hash_f32_bits(*inner_radius, state);
348 }
349 DrawPrimitive::Image {
350 rect,
351 image,
352 alpha,
353 color_filter,
354 sampling,
355 src_rect,
356 } => {
357 4u8.hash(state);
358 hash_rect(*rect, state);
359 image.id().hash(state);
360 hash_f32_bits(*alpha, state);
361 sampling.hash(state);
362 match color_filter {
363 Some(filter) => {
364 1u8.hash(state);
365 hash_color_filter(*filter, state);
366 }
367 None => 0u8.hash(state),
368 }
369 match src_rect {
370 Some(rect) => {
371 1u8.hash(state);
372 hash_rect(*rect, state);
373 }
374 None => 0u8.hash(state),
375 }
376 }
377 DrawPrimitive::Text(text) => {
378 7u8.hash(state);
379 hash_rect(text.rect, state);
380 text.text.hash(state);
381 hash_text_style(&text.style, state);
382 hash_color(text.color, state);
383 }
384 DrawPrimitive::Shadow(shadow) => {
385 5u8.hash(state);
386 hash_shadow_primitive(shadow, state);
387 }
388 }
389}
390
391fn hash_text_style<H: Hasher>(style: &crate::DrawTextStyle, state: &mut H) {
392 style.font_family.hash(state);
393 hash_f32_bits(style.font_size, state);
394 style.font_weight.hash(state);
395 style.font_style.hash(state);
396 hash_f32_bits(style.letter_spacing, state);
397 match style.line_height {
398 Some(line_height) => {
399 1u8.hash(state);
400 hash_f32_bits(line_height, state);
401 }
402 None => 0u8.hash(state),
403 }
404 style.align.hash(state);
405 style.vertical_align.hash(state);
406}
407
408fn hash_shadow_primitive<H: Hasher>(shadow: &ShadowPrimitive, state: &mut H) {
409 match shadow {
410 ShadowPrimitive::Drop {
411 shape,
412 cutout,
413 blur_radius,
414 blend_mode,
415 } => {
416 0u8.hash(state);
417 hash_draw_primitive(shape, state);
418 match cutout {
419 Some(cutout) => {
420 1u8.hash(state);
421 hash_draw_primitive(cutout, state);
422 }
423 None => 0u8.hash(state),
424 }
425 hash_f32_bits(*blur_radius, state);
426 blend_mode.hash(state);
427 }
428 ShadowPrimitive::Inner {
429 fill,
430 cutout,
431 blur_radius,
432 blend_mode,
433 clip_rect,
434 } => {
435 1u8.hash(state);
436 hash_draw_primitive(fill, state);
437 hash_draw_primitive(cutout, state);
438 hash_f32_bits(*blur_radius, state);
439 blend_mode.hash(state);
440 hash_rect(*clip_rect, state);
441 }
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 #[test]
448 fn runtime_shader_overrides_change_the_render_hash() {
449 let plain = crate::RuntimeShader::new("// hash-overrides");
450 let mut raised = plain.clone();
451 raised.set_override("FLAG", 1.0);
452 assert_ne!(
453 crate::RenderEffect::runtime_shader(plain).render_hash(),
454 crate::RenderEffect::runtime_shader(raised).render_hash(),
455 "a specialized pipeline renders through different code, so cached output keyed \
456 without the overrides would serve the wrong program"
457 );
458 }
459
460 use super::*;
461 use crate::render_effect::TileMode;
462
463 #[test]
464 fn color_render_hash_changes_with_channels() {
465 assert_ne!(
466 Color(1.0, 0.0, 0.0, 1.0).render_hash(),
467 Color(0.0, 1.0, 0.0, 1.0).render_hash()
468 );
469 }
470
471 #[test]
472 fn point_rect_and_corner_radii_render_hash_changes_with_geometry() {
473 assert_ne!(
474 Point::new(1.0, 2.0).render_hash(),
475 Point::new(2.0, 1.0).render_hash()
476 );
477 assert_ne!(
478 Rect {
479 x: 0.0,
480 y: 0.0,
481 width: 10.0,
482 height: 20.0,
483 }
484 .render_hash(),
485 Rect {
486 x: 0.0,
487 y: 0.0,
488 width: 20.0,
489 height: 10.0,
490 }
491 .render_hash()
492 );
493 assert_ne!(
494 CornerRadii::uniform(4.0).render_hash(),
495 CornerRadii::uniform(6.0).render_hash()
496 );
497 }
498
499 #[test]
500 fn layer_shape_render_hash_tracks_shape_kind_and_radii() {
501 assert_ne!(
502 LayerShape::Rectangle.render_hash(),
503 LayerShape::Rounded(crate::RoundedCornerShape::uniform(8.0)).render_hash()
504 );
505 assert_ne!(
506 LayerShape::Rounded(crate::RoundedCornerShape::uniform(4.0)).render_hash(),
507 LayerShape::Rounded(crate::RoundedCornerShape::uniform(8.0)).render_hash()
508 );
509 }
510
511 #[test]
512 fn brush_render_hash_tracks_gradient_structure() {
513 let base = Brush::linear_gradient_with_tile_mode(
514 vec![Color::RED, Color::BLUE],
515 Point::new(0.0, 0.0),
516 Point::new(10.0, 10.0),
517 TileMode::Clamp,
518 );
519 let shifted = Brush::linear_gradient_with_tile_mode(
520 vec![Color::RED, Color::BLUE],
521 Point::new(1.0, 0.0),
522 Point::new(10.0, 10.0),
523 TileMode::Clamp,
524 );
525
526 assert_ne!(base.render_hash(), shifted.render_hash());
527 }
528
529 #[test]
530 fn color_filter_render_hash_tracks_variant_and_values() {
531 assert_ne!(
532 ColorFilter::Tint(Color::RED).render_hash(),
533 ColorFilter::Modulate(Color::RED).render_hash()
534 );
535 assert_ne!(
536 ColorFilter::Matrix([1.0; 20]).render_hash(),
537 ColorFilter::Matrix([0.0; 20]).render_hash()
538 );
539 }
540
541 #[test]
542 fn render_effect_render_hash_tracks_variant_parameters() {
543 assert_ne!(
544 RenderEffect::blur(4.0).render_hash(),
545 RenderEffect::blur(6.0).render_hash()
546 );
547 assert_ne!(
548 RenderEffect::offset(2.0, 1.0).render_hash(),
549 RenderEffect::offset(1.0, 2.0).render_hash()
550 );
551 }
552
553 #[test]
554 fn runtime_shader_render_hash_tracks_uniforms() {
555 let mut base = RuntimeShader::new("// hash");
556 base.set_float(0, 1.0);
557 let same = base.clone();
558 let mut changed = base.clone();
559 changed.set_float(0, 2.0);
560 assert_eq!(
561 base.render_hash(),
562 same.render_hash(),
563 "equal source and uniforms hash equally"
564 );
565 assert_ne!(
566 base.render_hash(),
567 changed.render_hash(),
568 "a uniform change is a pixel change: a surface that bakes this shader \
569 must miss the layer cache, and the cache admits repeating keys only, \
570 so per-frame uniforms never fill it with stale textures"
571 );
572 }
573
574 #[test]
575 fn runtime_shader_render_hash_tracks_source() {
576 let a = RuntimeShader::new("// shader A");
577 let b = RuntimeShader::new("// shader B");
578 assert_ne!(a.render_hash(), b.render_hash());
579 }
580
581 #[test]
582 fn draw_primitive_render_hash_tracks_nested_structure() {
583 let base = DrawPrimitive::Blend {
584 primitive: Box::new(DrawPrimitive::Rect {
585 rect: Rect {
586 x: 0.0,
587 y: 0.0,
588 width: 12.0,
589 height: 8.0,
590 },
591 brush: Brush::solid(Color::WHITE),
592 stroke: None,
593 }),
594 blend_mode: crate::BlendMode::SrcOver,
595 };
596 let changed = DrawPrimitive::Blend {
597 primitive: Box::new(DrawPrimitive::Rect {
598 rect: Rect {
599 x: 0.0,
600 y: 0.0,
601 width: 12.0,
602 height: 8.0,
603 },
604 brush: Brush::solid(Color::BLACK),
605 stroke: None,
606 }),
607 blend_mode: crate::BlendMode::SrcOver,
608 };
609 assert_ne!(base.render_hash(), changed.render_hash());
610 }
611
612 use crate::{Stroke, StrokeCap, StrokeJoin};
613
614 fn stroked_rect(stroke: Option<Stroke>) -> DrawPrimitive {
615 DrawPrimitive::Rect {
616 rect: Rect {
617 x: 0.0,
618 y: 0.0,
619 width: 12.0,
620 height: 8.0,
621 },
622 brush: Brush::solid(Color::WHITE),
623 stroke,
624 }
625 }
626
627 fn arc(
628 radius: f32,
629 start_angle: f32,
630 sweep_angle: f32,
631 stroke: Option<Stroke>,
632 inner_radius: f32,
633 ) -> DrawPrimitive {
634 DrawPrimitive::Arc {
635 rect: Rect {
636 x: 0.0,
637 y: 0.0,
638 width: 40.0,
639 height: 40.0,
640 },
641 brush: Brush::solid(Color::WHITE),
642 center: Point::new(20.0, 20.0),
643 radius,
644 start_angle,
645 sweep_angle,
646 stroke,
647 inner_radius,
648 }
649 }
650
651 #[test]
652 fn stroke_render_hash_tracks_width_cap_and_join() {
653 let base = Stroke::new(2.0);
654 assert_ne!(base.render_hash(), Stroke::new(3.0).render_hash());
655 assert_ne!(
656 base.render_hash(),
657 base.with_cap(StrokeCap::Round).render_hash()
658 );
659 assert_ne!(
660 base.render_hash(),
661 base.with_join(StrokeJoin::Bevel).render_hash()
662 );
663 assert_eq!(base.render_hash(), Stroke::new(2.0).render_hash());
664 }
665
666 #[test]
667 fn rect_render_hash_separates_fill_from_stroke() {
668 let fill = stroked_rect(None);
669 let stroked = stroked_rect(Some(Stroke::new(2.0)));
670 assert_ne!(fill.render_hash(), stroked.render_hash());
671 assert_eq!(
672 stroked.render_hash(),
673 stroked_rect(Some(Stroke::new(2.0))).render_hash()
674 );
675 }
676
677 #[test]
678 fn rect_render_hash_tracks_every_stroke_field() {
679 let base = Stroke::new(2.0);
680 let base_hash = stroked_rect(Some(base)).render_hash();
681 assert_ne!(
682 base_hash,
683 stroked_rect(Some(base.with_width(2.5))).render_hash()
684 );
685 assert_ne!(
686 base_hash,
687 stroked_rect(Some(base.with_cap(StrokeCap::Square))).render_hash()
688 );
689 assert_ne!(
690 base_hash,
691 stroked_rect(Some(base.with_join(StrokeJoin::Round))).render_hash()
692 );
693 }
694
695 #[test]
696 fn round_rect_render_hash_tracks_stroke() {
697 let make = |stroke| DrawPrimitive::RoundRect {
698 rect: Rect {
699 x: 0.0,
700 y: 0.0,
701 width: 12.0,
702 height: 8.0,
703 },
704 brush: Brush::solid(Color::WHITE),
705 radii: CornerRadii::uniform(3.0),
706 stroke,
707 };
708 assert_ne!(
709 make(None).render_hash(),
710 make(Some(Stroke::new(2.0))).render_hash()
711 );
712 assert_ne!(
713 make(Some(Stroke::new(2.0))).render_hash(),
714 make(Some(Stroke::new(2.0).with_join(StrokeJoin::Bevel))).render_hash()
715 );
716 }
717
718 #[test]
719 fn arc_render_hash_tracks_angles_radii_and_stroke() {
720 let stroke = Some(Stroke::new(4.0));
721 let base = arc(10.0, 0.0, 1.0, stroke, 0.0);
722 let base_hash = base.render_hash();
723
724 assert_eq!(base_hash, arc(10.0, 0.0, 1.0, stroke, 0.0).render_hash());
725 assert_ne!(base_hash, arc(11.0, 0.0, 1.0, stroke, 0.0).render_hash());
726 assert_ne!(base_hash, arc(10.0, 0.5, 1.0, stroke, 0.0).render_hash());
727 assert_ne!(base_hash, arc(10.0, 0.0, 1.5, stroke, 0.0).render_hash());
728 assert_ne!(base_hash, arc(10.0, 0.0, -1.0, stroke, 0.0).render_hash());
729 assert_ne!(base_hash, arc(10.0, 0.0, 1.0, stroke, 4.0).render_hash());
730 assert_ne!(base_hash, arc(10.0, 0.0, 1.0, None, 0.0).render_hash());
731 assert_ne!(
732 base_hash,
733 arc(
734 10.0,
735 0.0,
736 1.0,
737 Some(Stroke::new(4.0).with_cap(StrokeCap::Round)),
738 0.0
739 )
740 .render_hash()
741 );
742 }
743
744 #[test]
745 fn arc_render_hash_differs_from_rect_with_same_bounds() {
746 assert_ne!(
747 arc(10.0, 0.0, 1.0, None, 4.0).render_hash(),
748 DrawPrimitive::Rect {
749 rect: Rect {
750 x: 0.0,
751 y: 0.0,
752 width: 40.0,
753 height: 40.0,
754 },
755 brush: Brush::solid(Color::WHITE),
756 stroke: None,
757 }
758 .render_hash()
759 );
760 }
761
762 #[test]
763 fn a_shaders_declared_support_and_sample_domain_are_part_of_its_hash() {
764 let plain = crate::RuntimeShader::new("fn effect_fs() {}");
765 let rect = crate::Rect {
766 x: 1.0,
767 y: 2.0,
768 width: 3.0,
769 height: 4.0,
770 };
771 let mut supported = plain.clone();
772 supported.set_output_support(Some(rect));
773 let mut bounded = plain.clone();
774 bounded.set_sample_domain(Some(rect));
775 assert_ne!(plain.render_hash(), supported.render_hash());
776 assert_ne!(plain.render_hash(), bounded.render_hash());
777 assert_ne!(supported.render_hash(), bounded.render_hash());
778 }
779}