Skip to main content

cranpose_ui_graphics/
render_hash.rs

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
126/// Every stroke field feeds the hash: layer/scene-range surface caches key off
127/// content hashes, so a width/cap/join change that did not move the hash would
128/// replay a stale cached surface.
129fn 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}