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