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    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}