Skip to main content

cranpose_ui_graphics/
render_hash.rs

1use crate::{
2    Brush, Color, ColorFilter, CornerRadii, DrawPrimitive, FxHasher, ImageBitmap, LayerShape,
3    Point, Rect, RenderEffect, RuntimeShader, ShadowPrimitive, Stroke,
4};
5use std::hash::{Hash, Hasher};
6
7pub trait RenderHash {
8    fn render_hash(&self) -> u64;
9}
10
11impl RenderHash for Color {
12    fn render_hash(&self) -> u64 {
13        finish_hash(|state| hash_color(*self, state))
14    }
15}
16
17impl RenderHash for Point {
18    fn render_hash(&self) -> u64 {
19        finish_hash(|state| hash_point(*self, state))
20    }
21}
22
23impl RenderHash for Rect {
24    fn render_hash(&self) -> u64 {
25        finish_hash(|state| hash_rect(*self, state))
26    }
27}
28
29impl RenderHash for CornerRadii {
30    fn render_hash(&self) -> u64 {
31        finish_hash(|state| hash_corner_radii(*self, state))
32    }
33}
34
35impl RenderHash for LayerShape {
36    fn render_hash(&self) -> u64 {
37        finish_hash(|state| hash_layer_shape(*self, state))
38    }
39}
40
41impl RenderHash for Stroke {
42    fn render_hash(&self) -> u64 {
43        finish_hash(|state| hash_stroke(*self, state))
44    }
45}
46
47impl RenderHash for Brush {
48    fn render_hash(&self) -> u64 {
49        finish_hash(|state| hash_brush(self, state))
50    }
51}
52
53impl RenderHash for ColorFilter {
54    fn render_hash(&self) -> u64 {
55        finish_hash(|state| hash_color_filter(*self, state))
56    }
57}
58
59impl RenderHash for ImageBitmap {
60    fn render_hash(&self) -> u64 {
61        finish_hash(|state| self.id().hash(state))
62    }
63}
64
65impl RenderHash for RuntimeShader {
66    fn render_hash(&self) -> u64 {
67        finish_hash(|state| hash_runtime_shader(self, state))
68    }
69}
70
71impl RenderHash for RenderEffect {
72    fn render_hash(&self) -> u64 {
73        finish_hash(|state| hash_render_effect(self, state))
74    }
75}
76
77impl RenderHash for DrawPrimitive {
78    fn render_hash(&self) -> u64 {
79        finish_hash(|state| hash_draw_primitive(self, state))
80    }
81}
82
83impl RenderHash for ShadowPrimitive {
84    fn render_hash(&self) -> u64 {
85        finish_hash(|state| hash_shadow_primitive(self, state))
86    }
87}
88
89fn finish_hash(write: impl FnOnce(&mut FxHasher)) -> u64 {
90    let mut hasher = FxHasher::default();
91    write(&mut hasher);
92    hasher.finish()
93}
94
95fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
96    value.to_bits().hash(state);
97}
98
99fn hash_color<H: Hasher>(color: Color, state: &mut H) {
100    hash_f32_bits(color.0, state);
101    hash_f32_bits(color.1, state);
102    hash_f32_bits(color.2, state);
103    hash_f32_bits(color.3, state);
104}
105
106fn hash_point<H: Hasher>(point: Point, state: &mut H) {
107    hash_f32_bits(point.x, state);
108    hash_f32_bits(point.y, state);
109}
110
111fn hash_rect<H: Hasher>(rect: Rect, state: &mut H) {
112    hash_f32_bits(rect.x, state);
113    hash_f32_bits(rect.y, state);
114    hash_f32_bits(rect.width, state);
115    hash_f32_bits(rect.height, state);
116}
117
118fn hash_corner_radii<H: Hasher>(radii: CornerRadii, state: &mut H) {
119    hash_f32_bits(radii.top_left, state);
120    hash_f32_bits(radii.top_right, state);
121    hash_f32_bits(radii.bottom_right, state);
122    hash_f32_bits(radii.bottom_left, state);
123}
124
125/// Every stroke field feeds the hash: layer/scene-range surface caches key off
126/// content hashes, so a width/cap/join change that did not move the hash would
127/// replay a stale cached surface.
128fn hash_stroke<H: Hasher>(stroke: Stroke, state: &mut H) {
129    hash_f32_bits(stroke.width, state);
130    stroke.cap.hash(state);
131    stroke.join.hash(state);
132}
133
134fn hash_optional_stroke<H: Hasher>(stroke: Option<Stroke>, state: &mut H) {
135    match stroke {
136        Some(stroke) => {
137            1u8.hash(state);
138            hash_stroke(stroke, state);
139        }
140        None => 0u8.hash(state),
141    }
142}
143
144fn hash_layer_shape<H: Hasher>(shape: LayerShape, state: &mut H) {
145    match shape {
146        LayerShape::Rectangle => 0u8.hash(state),
147        LayerShape::Rounded(shape) => {
148            1u8.hash(state);
149            hash_corner_radii(shape.radii(), state);
150        }
151    }
152}
153
154fn hash_brush<H: Hasher>(brush: &Brush, state: &mut H) {
155    match brush {
156        Brush::Solid(color) => {
157            0u8.hash(state);
158            hash_color(*color, state);
159        }
160        Brush::LinearGradient {
161            colors,
162            stops,
163            start,
164            end,
165            tile_mode,
166        } => {
167            1u8.hash(state);
168            hash_color_slice(colors, state);
169            hash_optional_stop_list(stops.as_deref(), state);
170            hash_point(*start, state);
171            hash_point(*end, state);
172            tile_mode.hash(state);
173        }
174        Brush::RadialGradient {
175            colors,
176            stops,
177            center,
178            radius,
179            tile_mode,
180        } => {
181            2u8.hash(state);
182            hash_color_slice(colors, state);
183            hash_optional_stop_list(stops.as_deref(), state);
184            hash_point(*center, state);
185            hash_f32_bits(*radius, state);
186            tile_mode.hash(state);
187        }
188        Brush::SweepGradient {
189            colors,
190            stops,
191            center,
192        } => {
193            3u8.hash(state);
194            hash_color_slice(colors, state);
195            hash_optional_stop_list(stops.as_deref(), state);
196            hash_point(*center, state);
197        }
198    }
199}
200
201fn hash_color_slice<H: Hasher>(colors: &[Color], state: &mut H) {
202    colors.len().hash(state);
203    for color in colors {
204        hash_color(*color, state);
205    }
206}
207
208fn hash_optional_stop_list<H: Hasher>(stops: Option<&[f32]>, state: &mut H) {
209    match stops {
210        Some(stops) => {
211            1u8.hash(state);
212            stops.len().hash(state);
213            for stop in stops {
214                hash_f32_bits(*stop, state);
215            }
216        }
217        None => 0u8.hash(state),
218    }
219}
220
221fn hash_color_filter<H: Hasher>(filter: ColorFilter, state: &mut H) {
222    match filter {
223        ColorFilter::Tint(color) => {
224            0u8.hash(state);
225            hash_color(color, state);
226        }
227        ColorFilter::Modulate(color) => {
228            1u8.hash(state);
229            hash_color(color, state);
230        }
231        ColorFilter::Matrix(matrix) => {
232            2u8.hash(state);
233            for value in matrix {
234                hash_f32_bits(value, state);
235            }
236        }
237    }
238}
239
240fn hash_runtime_shader<H: Hasher>(shader: &RuntimeShader, state: &mut H) {
241    // Only hash the source, not the uniforms. Uniforms change every frame for
242    // animated shaders (time, position, etc.) and including them would produce
243    // a new effect_hash every frame, filling the layer surface cache with
244    // stale entries. The pipeline cache already deduplicates by source hash,
245    // and stable_id in the cache key distinguishes different nodes.
246    shader.source_hash().hash(state);
247    hash_f32_bits(shader.input_padding(), state);
248}
249
250fn hash_render_effect<H: Hasher>(effect: &RenderEffect, state: &mut H) {
251    match effect {
252        RenderEffect::Blur {
253            radius_x,
254            radius_y,
255            edge_treatment,
256        } => {
257            0u8.hash(state);
258            hash_f32_bits(*radius_x, state);
259            hash_f32_bits(*radius_y, state);
260            edge_treatment.hash(state);
261        }
262        RenderEffect::Offset { offset_x, offset_y } => {
263            1u8.hash(state);
264            hash_f32_bits(*offset_x, state);
265            hash_f32_bits(*offset_y, state);
266        }
267        RenderEffect::Shader { shader } => {
268            2u8.hash(state);
269            hash_runtime_shader(shader, state);
270        }
271        RenderEffect::Chain { first, second } => {
272            3u8.hash(state);
273            hash_render_effect(first, state);
274            hash_render_effect(second, state);
275        }
276    }
277}
278
279fn hash_draw_primitive<H: Hasher>(primitive: &DrawPrimitive, state: &mut H) {
280    match primitive {
281        DrawPrimitive::Content => {
282            0u8.hash(state);
283        }
284        DrawPrimitive::Blend {
285            primitive,
286            blend_mode,
287        } => {
288            1u8.hash(state);
289            blend_mode.hash(state);
290            hash_draw_primitive(primitive, state);
291        }
292        DrawPrimitive::Rect {
293            rect,
294            brush,
295            stroke,
296        } => {
297            2u8.hash(state);
298            hash_rect(*rect, state);
299            hash_brush(brush, state);
300            hash_optional_stroke(*stroke, state);
301        }
302        DrawPrimitive::RoundRect {
303            rect,
304            brush,
305            radii,
306            stroke,
307        } => {
308            3u8.hash(state);
309            hash_rect(*rect, state);
310            hash_brush(brush, state);
311            hash_corner_radii(*radii, state);
312            hash_optional_stroke(*stroke, state);
313        }
314        DrawPrimitive::Arc {
315            rect,
316            brush,
317            center,
318            radius,
319            start_angle,
320            sweep_angle,
321            stroke,
322            inner_radius,
323        } => {
324            6u8.hash(state);
325            hash_rect(*rect, state);
326            hash_brush(brush, state);
327            hash_point(*center, state);
328            hash_f32_bits(*radius, state);
329            hash_f32_bits(*start_angle, state);
330            hash_f32_bits(*sweep_angle, state);
331            hash_optional_stroke(*stroke, state);
332            hash_f32_bits(*inner_radius, state);
333        }
334        DrawPrimitive::Image {
335            rect,
336            image,
337            alpha,
338            color_filter,
339            sampling,
340            src_rect,
341        } => {
342            4u8.hash(state);
343            hash_rect(*rect, state);
344            image.id().hash(state);
345            hash_f32_bits(*alpha, state);
346            sampling.hash(state);
347            match color_filter {
348                Some(filter) => {
349                    1u8.hash(state);
350                    hash_color_filter(*filter, state);
351                }
352                None => 0u8.hash(state),
353            }
354            match src_rect {
355                Some(rect) => {
356                    1u8.hash(state);
357                    hash_rect(*rect, state);
358                }
359                None => 0u8.hash(state),
360            }
361        }
362        DrawPrimitive::Text(text) => {
363            7u8.hash(state);
364            hash_rect(text.rect, state);
365            text.text.hash(state);
366            hash_text_style(&text.style, state);
367            hash_color(text.color, state);
368        }
369        DrawPrimitive::Shadow(shadow) => {
370            5u8.hash(state);
371            hash_shadow_primitive(shadow, state);
372        }
373    }
374}
375
376fn hash_text_style<H: Hasher>(style: &crate::TextStyle, state: &mut H) {
377    style.font_family.hash(state);
378    hash_f32_bits(style.font_size, state);
379    style.font_weight.hash(state);
380    style.font_style.hash(state);
381    hash_f32_bits(style.letter_spacing, state);
382    match style.line_height {
383        Some(line_height) => {
384            1u8.hash(state);
385            hash_f32_bits(line_height, state);
386        }
387        None => 0u8.hash(state),
388    }
389    style.align.hash(state);
390    style.vertical_align.hash(state);
391}
392
393fn hash_shadow_primitive<H: Hasher>(shadow: &ShadowPrimitive, state: &mut H) {
394    match shadow {
395        ShadowPrimitive::Drop {
396            shape,
397            cutout,
398            blur_radius,
399            blend_mode,
400        } => {
401            0u8.hash(state);
402            hash_draw_primitive(shape, state);
403            match cutout {
404                Some(cutout) => {
405                    1u8.hash(state);
406                    hash_draw_primitive(cutout, state);
407                }
408                None => 0u8.hash(state),
409            }
410            hash_f32_bits(*blur_radius, state);
411            blend_mode.hash(state);
412        }
413        ShadowPrimitive::Inner {
414            fill,
415            cutout,
416            blur_radius,
417            blend_mode,
418            clip_rect,
419        } => {
420            1u8.hash(state);
421            hash_draw_primitive(fill, state);
422            hash_draw_primitive(cutout, state);
423            hash_f32_bits(*blur_radius, state);
424            blend_mode.hash(state);
425            hash_rect(*clip_rect, state);
426        }
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use crate::render_effect::TileMode;
434
435    #[test]
436    fn color_render_hash_changes_with_channels() {
437        assert_ne!(
438            Color(1.0, 0.0, 0.0, 1.0).render_hash(),
439            Color(0.0, 1.0, 0.0, 1.0).render_hash()
440        );
441    }
442
443    #[test]
444    fn point_rect_and_corner_radii_render_hash_changes_with_geometry() {
445        assert_ne!(
446            Point::new(1.0, 2.0).render_hash(),
447            Point::new(2.0, 1.0).render_hash()
448        );
449        assert_ne!(
450            Rect {
451                x: 0.0,
452                y: 0.0,
453                width: 10.0,
454                height: 20.0,
455            }
456            .render_hash(),
457            Rect {
458                x: 0.0,
459                y: 0.0,
460                width: 20.0,
461                height: 10.0,
462            }
463            .render_hash()
464        );
465        assert_ne!(
466            CornerRadii::uniform(4.0).render_hash(),
467            CornerRadii::uniform(6.0).render_hash()
468        );
469    }
470
471    #[test]
472    fn layer_shape_render_hash_tracks_shape_kind_and_radii() {
473        assert_ne!(
474            LayerShape::Rectangle.render_hash(),
475            LayerShape::Rounded(crate::RoundedCornerShape::uniform(8.0)).render_hash()
476        );
477        assert_ne!(
478            LayerShape::Rounded(crate::RoundedCornerShape::uniform(4.0)).render_hash(),
479            LayerShape::Rounded(crate::RoundedCornerShape::uniform(8.0)).render_hash()
480        );
481    }
482
483    #[test]
484    fn brush_render_hash_tracks_gradient_structure() {
485        let base = Brush::linear_gradient_with_tile_mode(
486            vec![Color::RED, Color::BLUE],
487            Point::new(0.0, 0.0),
488            Point::new(10.0, 10.0),
489            TileMode::Clamp,
490        );
491        let shifted = Brush::linear_gradient_with_tile_mode(
492            vec![Color::RED, Color::BLUE],
493            Point::new(1.0, 0.0),
494            Point::new(10.0, 10.0),
495            TileMode::Clamp,
496        );
497
498        assert_ne!(base.render_hash(), shifted.render_hash());
499    }
500
501    #[test]
502    fn color_filter_render_hash_tracks_variant_and_values() {
503        assert_ne!(
504            ColorFilter::Tint(Color::RED).render_hash(),
505            ColorFilter::Modulate(Color::RED).render_hash()
506        );
507        assert_ne!(
508            ColorFilter::Matrix([1.0; 20]).render_hash(),
509            ColorFilter::Matrix([0.0; 20]).render_hash()
510        );
511    }
512
513    #[test]
514    fn render_effect_render_hash_tracks_variant_parameters() {
515        assert_ne!(
516            RenderEffect::blur(4.0).render_hash(),
517            RenderEffect::blur(6.0).render_hash()
518        );
519        assert_ne!(
520            RenderEffect::offset(2.0, 1.0).render_hash(),
521            RenderEffect::offset(1.0, 2.0).render_hash()
522        );
523    }
524
525    #[test]
526    fn runtime_shader_render_hash_ignores_uniforms() {
527        let mut base = RuntimeShader::new("// hash");
528        base.set_float(0, 1.0);
529        let mut changed = base.clone();
530        changed.set_float(0, 2.0);
531        assert_eq!(
532            base.render_hash(),
533            changed.render_hash(),
534            "render_hash must depend only on source, not uniforms — \
535             animated uniforms (time, position) would otherwise produce a \
536             new effect_hash every frame, filling the layer cache with stale textures"
537        );
538    }
539
540    #[test]
541    fn runtime_shader_render_hash_tracks_source() {
542        let a = RuntimeShader::new("// shader A");
543        let b = RuntimeShader::new("// shader B");
544        assert_ne!(a.render_hash(), b.render_hash());
545    }
546
547    #[test]
548    fn draw_primitive_render_hash_tracks_nested_structure() {
549        let base = DrawPrimitive::Blend {
550            primitive: Box::new(DrawPrimitive::Rect {
551                rect: Rect {
552                    x: 0.0,
553                    y: 0.0,
554                    width: 12.0,
555                    height: 8.0,
556                },
557                brush: Brush::solid(Color::WHITE),
558                stroke: None,
559            }),
560            blend_mode: crate::BlendMode::SrcOver,
561        };
562        let changed = DrawPrimitive::Blend {
563            primitive: Box::new(DrawPrimitive::Rect {
564                rect: Rect {
565                    x: 0.0,
566                    y: 0.0,
567                    width: 12.0,
568                    height: 8.0,
569                },
570                brush: Brush::solid(Color::BLACK),
571                stroke: None,
572            }),
573            blend_mode: crate::BlendMode::SrcOver,
574        };
575        assert_ne!(base.render_hash(), changed.render_hash());
576    }
577
578    // ── Stroke / arc hashing ────────────────────────────────────────────────
579    //
580    // These are load-bearing: `LayerRasterCacheKey` and the scene-range surface
581    // cache key off content hashes. A stroke or arc parameter that does not
582    // reach the hash would silently replay a stale cached surface.
583
584    use crate::{Stroke, StrokeCap, StrokeJoin};
585
586    fn stroked_rect(stroke: Option<Stroke>) -> DrawPrimitive {
587        DrawPrimitive::Rect {
588            rect: Rect {
589                x: 0.0,
590                y: 0.0,
591                width: 12.0,
592                height: 8.0,
593            },
594            brush: Brush::solid(Color::WHITE),
595            stroke,
596        }
597    }
598
599    fn arc(
600        radius: f32,
601        start_angle: f32,
602        sweep_angle: f32,
603        stroke: Option<Stroke>,
604        inner_radius: f32,
605    ) -> DrawPrimitive {
606        DrawPrimitive::Arc {
607            rect: Rect {
608                x: 0.0,
609                y: 0.0,
610                width: 40.0,
611                height: 40.0,
612            },
613            brush: Brush::solid(Color::WHITE),
614            center: Point::new(20.0, 20.0),
615            radius,
616            start_angle,
617            sweep_angle,
618            stroke,
619            inner_radius,
620        }
621    }
622
623    #[test]
624    fn stroke_render_hash_tracks_width_cap_and_join() {
625        let base = Stroke::new(2.0);
626        assert_ne!(base.render_hash(), Stroke::new(3.0).render_hash());
627        assert_ne!(
628            base.render_hash(),
629            base.with_cap(StrokeCap::Round).render_hash()
630        );
631        assert_ne!(
632            base.render_hash(),
633            base.with_join(StrokeJoin::Bevel).render_hash()
634        );
635        assert_eq!(base.render_hash(), Stroke::new(2.0).render_hash());
636    }
637
638    #[test]
639    fn rect_render_hash_separates_fill_from_stroke() {
640        let fill = stroked_rect(None);
641        let stroked = stroked_rect(Some(Stroke::new(2.0)));
642        assert_ne!(fill.render_hash(), stroked.render_hash());
643        assert_eq!(
644            stroked.render_hash(),
645            stroked_rect(Some(Stroke::new(2.0))).render_hash()
646        );
647    }
648
649    #[test]
650    fn rect_render_hash_tracks_every_stroke_field() {
651        let base = Stroke::new(2.0);
652        let base_hash = stroked_rect(Some(base)).render_hash();
653        assert_ne!(
654            base_hash,
655            stroked_rect(Some(base.with_width(2.5))).render_hash()
656        );
657        assert_ne!(
658            base_hash,
659            stroked_rect(Some(base.with_cap(StrokeCap::Square))).render_hash()
660        );
661        assert_ne!(
662            base_hash,
663            stroked_rect(Some(base.with_join(StrokeJoin::Round))).render_hash()
664        );
665    }
666
667    #[test]
668    fn round_rect_render_hash_tracks_stroke() {
669        let make = |stroke| DrawPrimitive::RoundRect {
670            rect: Rect {
671                x: 0.0,
672                y: 0.0,
673                width: 12.0,
674                height: 8.0,
675            },
676            brush: Brush::solid(Color::WHITE),
677            radii: CornerRadii::uniform(3.0),
678            stroke,
679        };
680        assert_ne!(
681            make(None).render_hash(),
682            make(Some(Stroke::new(2.0))).render_hash()
683        );
684        assert_ne!(
685            make(Some(Stroke::new(2.0))).render_hash(),
686            make(Some(Stroke::new(2.0).with_join(StrokeJoin::Bevel))).render_hash()
687        );
688    }
689
690    #[test]
691    fn arc_render_hash_tracks_angles_radii_and_stroke() {
692        let stroke = Some(Stroke::new(4.0));
693        let base = arc(10.0, 0.0, 1.0, stroke, 0.0);
694        let base_hash = base.render_hash();
695
696        assert_eq!(base_hash, arc(10.0, 0.0, 1.0, stroke, 0.0).render_hash());
697        assert_ne!(base_hash, arc(11.0, 0.0, 1.0, stroke, 0.0).render_hash());
698        assert_ne!(base_hash, arc(10.0, 0.5, 1.0, stroke, 0.0).render_hash());
699        assert_ne!(base_hash, arc(10.0, 0.0, 1.5, stroke, 0.0).render_hash());
700        assert_ne!(base_hash, arc(10.0, 0.0, -1.0, stroke, 0.0).render_hash());
701        assert_ne!(base_hash, arc(10.0, 0.0, 1.0, stroke, 4.0).render_hash());
702        assert_ne!(base_hash, arc(10.0, 0.0, 1.0, None, 0.0).render_hash());
703        assert_ne!(
704            base_hash,
705            arc(
706                10.0,
707                0.0,
708                1.0,
709                Some(Stroke::new(4.0).with_cap(StrokeCap::Round)),
710                0.0
711            )
712            .render_hash()
713        );
714    }
715
716    #[test]
717    fn arc_render_hash_differs_from_rect_with_same_bounds() {
718        assert_ne!(
719            arc(10.0, 0.0, 1.0, None, 4.0).render_hash(),
720            DrawPrimitive::Rect {
721                rect: Rect {
722                    x: 0.0,
723                    y: 0.0,
724                    width: 40.0,
725                    height: 40.0,
726                },
727                brush: Brush::solid(Color::WHITE),
728                stroke: None,
729            }
730            .render_hash()
731        );
732    }
733}