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