Skip to main content

cranpose_ui_graphics/
render_hash.rs

1use crate::{
2    Brush, Color, ColorFilter, CornerRadii, DrawPrimitive, ImageBitmap, LayerShape, Point, Rect,
3    RenderEffect, RuntimeShader, ShadowPrimitive,
4};
5use std::collections::hash_map::DefaultHasher;
6use std::hash::{Hash, Hasher};
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 Brush {
43    fn render_hash(&self) -> u64 {
44        finish_hash(|state| hash_brush(self, state))
45    }
46}
47
48impl RenderHash for ColorFilter {
49    fn render_hash(&self) -> u64 {
50        finish_hash(|state| hash_color_filter(*self, state))
51    }
52}
53
54impl RenderHash for ImageBitmap {
55    fn render_hash(&self) -> u64 {
56        finish_hash(|state| self.id().hash(state))
57    }
58}
59
60impl RenderHash for RuntimeShader {
61    fn render_hash(&self) -> u64 {
62        finish_hash(|state| hash_runtime_shader(self, state))
63    }
64}
65
66impl RenderHash for RenderEffect {
67    fn render_hash(&self) -> u64 {
68        finish_hash(|state| hash_render_effect(self, state))
69    }
70}
71
72impl RenderHash for DrawPrimitive {
73    fn render_hash(&self) -> u64 {
74        finish_hash(|state| hash_draw_primitive(self, state))
75    }
76}
77
78impl RenderHash for ShadowPrimitive {
79    fn render_hash(&self) -> u64 {
80        finish_hash(|state| hash_shadow_primitive(self, state))
81    }
82}
83
84fn finish_hash(write: impl FnOnce(&mut DefaultHasher)) -> u64 {
85    let mut hasher = DefaultHasher::new();
86    write(&mut hasher);
87    hasher.finish()
88}
89
90fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
91    value.to_bits().hash(state);
92}
93
94fn hash_color<H: Hasher>(color: Color, state: &mut H) {
95    hash_f32_bits(color.0, state);
96    hash_f32_bits(color.1, state);
97    hash_f32_bits(color.2, state);
98    hash_f32_bits(color.3, state);
99}
100
101fn hash_point<H: Hasher>(point: Point, state: &mut H) {
102    hash_f32_bits(point.x, state);
103    hash_f32_bits(point.y, state);
104}
105
106fn hash_rect<H: Hasher>(rect: Rect, state: &mut H) {
107    hash_f32_bits(rect.x, state);
108    hash_f32_bits(rect.y, state);
109    hash_f32_bits(rect.width, state);
110    hash_f32_bits(rect.height, state);
111}
112
113fn hash_corner_radii<H: Hasher>(radii: CornerRadii, state: &mut H) {
114    hash_f32_bits(radii.top_left, state);
115    hash_f32_bits(radii.top_right, state);
116    hash_f32_bits(radii.bottom_right, state);
117    hash_f32_bits(radii.bottom_left, state);
118}
119
120fn hash_layer_shape<H: Hasher>(shape: LayerShape, state: &mut H) {
121    match shape {
122        LayerShape::Rectangle => 0u8.hash(state),
123        LayerShape::Rounded(shape) => {
124            1u8.hash(state);
125            hash_corner_radii(shape.radii(), state);
126        }
127    }
128}
129
130fn hash_brush<H: Hasher>(brush: &Brush, state: &mut H) {
131    match brush {
132        Brush::Solid(color) => {
133            0u8.hash(state);
134            hash_color(*color, state);
135        }
136        Brush::LinearGradient {
137            colors,
138            stops,
139            start,
140            end,
141            tile_mode,
142        } => {
143            1u8.hash(state);
144            hash_color_slice(colors, state);
145            hash_optional_stop_list(stops.as_deref(), state);
146            hash_point(*start, state);
147            hash_point(*end, state);
148            tile_mode.hash(state);
149        }
150        Brush::RadialGradient {
151            colors,
152            stops,
153            center,
154            radius,
155            tile_mode,
156        } => {
157            2u8.hash(state);
158            hash_color_slice(colors, state);
159            hash_optional_stop_list(stops.as_deref(), state);
160            hash_point(*center, state);
161            hash_f32_bits(*radius, state);
162            tile_mode.hash(state);
163        }
164        Brush::SweepGradient {
165            colors,
166            stops,
167            center,
168        } => {
169            3u8.hash(state);
170            hash_color_slice(colors, state);
171            hash_optional_stop_list(stops.as_deref(), state);
172            hash_point(*center, state);
173        }
174    }
175}
176
177fn hash_color_slice<H: Hasher>(colors: &[Color], state: &mut H) {
178    colors.len().hash(state);
179    for color in colors {
180        hash_color(*color, state);
181    }
182}
183
184fn hash_optional_stop_list<H: Hasher>(stops: Option<&[f32]>, state: &mut H) {
185    match stops {
186        Some(stops) => {
187            1u8.hash(state);
188            stops.len().hash(state);
189            for stop in stops {
190                hash_f32_bits(*stop, state);
191            }
192        }
193        None => 0u8.hash(state),
194    }
195}
196
197fn hash_color_filter<H: Hasher>(filter: ColorFilter, state: &mut H) {
198    match filter {
199        ColorFilter::Tint(color) => {
200            0u8.hash(state);
201            hash_color(color, state);
202        }
203        ColorFilter::Modulate(color) => {
204            1u8.hash(state);
205            hash_color(color, state);
206        }
207        ColorFilter::Matrix(matrix) => {
208            2u8.hash(state);
209            for value in matrix {
210                hash_f32_bits(value, state);
211            }
212        }
213    }
214}
215
216fn hash_runtime_shader<H: Hasher>(shader: &RuntimeShader, state: &mut H) {
217    // Only hash the source, not the uniforms. Uniforms change every frame for
218    // animated shaders (time, position, etc.) and including them would produce
219    // a new effect_hash every frame, filling the layer surface cache with
220    // stale entries. The pipeline cache already deduplicates by source hash,
221    // and stable_id in the cache key distinguishes different nodes.
222    shader.source_hash().hash(state);
223    hash_f32_bits(shader.input_padding(), state);
224}
225
226fn hash_render_effect<H: Hasher>(effect: &RenderEffect, state: &mut H) {
227    match effect {
228        RenderEffect::Blur {
229            radius_x,
230            radius_y,
231            edge_treatment,
232        } => {
233            0u8.hash(state);
234            hash_f32_bits(*radius_x, state);
235            hash_f32_bits(*radius_y, state);
236            edge_treatment.hash(state);
237        }
238        RenderEffect::Offset { offset_x, offset_y } => {
239            1u8.hash(state);
240            hash_f32_bits(*offset_x, state);
241            hash_f32_bits(*offset_y, state);
242        }
243        RenderEffect::Shader { shader } => {
244            2u8.hash(state);
245            hash_runtime_shader(shader, state);
246        }
247        RenderEffect::Chain { first, second } => {
248            3u8.hash(state);
249            hash_render_effect(first, state);
250            hash_render_effect(second, state);
251        }
252    }
253}
254
255fn hash_draw_primitive<H: Hasher>(primitive: &DrawPrimitive, state: &mut H) {
256    match primitive {
257        DrawPrimitive::Content => {
258            0u8.hash(state);
259        }
260        DrawPrimitive::Blend {
261            primitive,
262            blend_mode,
263        } => {
264            1u8.hash(state);
265            blend_mode.hash(state);
266            hash_draw_primitive(primitive, state);
267        }
268        DrawPrimitive::Rect { rect, brush } => {
269            2u8.hash(state);
270            hash_rect(*rect, state);
271            hash_brush(brush, state);
272        }
273        DrawPrimitive::RoundRect { rect, brush, radii } => {
274            3u8.hash(state);
275            hash_rect(*rect, state);
276            hash_brush(brush, state);
277            hash_corner_radii(*radii, state);
278        }
279        DrawPrimitive::Image {
280            rect,
281            image,
282            alpha,
283            color_filter,
284            sampling,
285            src_rect,
286        } => {
287            4u8.hash(state);
288            hash_rect(*rect, state);
289            image.id().hash(state);
290            hash_f32_bits(*alpha, state);
291            sampling.hash(state);
292            match color_filter {
293                Some(filter) => {
294                    1u8.hash(state);
295                    hash_color_filter(*filter, state);
296                }
297                None => 0u8.hash(state),
298            }
299            match src_rect {
300                Some(rect) => {
301                    1u8.hash(state);
302                    hash_rect(*rect, state);
303                }
304                None => 0u8.hash(state),
305            }
306        }
307        DrawPrimitive::Shadow(shadow) => {
308            5u8.hash(state);
309            hash_shadow_primitive(shadow, state);
310        }
311    }
312}
313
314fn hash_shadow_primitive<H: Hasher>(shadow: &ShadowPrimitive, state: &mut H) {
315    match shadow {
316        ShadowPrimitive::Drop {
317            shape,
318            cutout,
319            blur_radius,
320            blend_mode,
321        } => {
322            0u8.hash(state);
323            hash_draw_primitive(shape, state);
324            match cutout {
325                Some(cutout) => {
326                    1u8.hash(state);
327                    hash_draw_primitive(cutout, state);
328                }
329                None => 0u8.hash(state),
330            }
331            hash_f32_bits(*blur_radius, state);
332            blend_mode.hash(state);
333        }
334        ShadowPrimitive::Inner {
335            fill,
336            cutout,
337            blur_radius,
338            blend_mode,
339            clip_rect,
340        } => {
341            1u8.hash(state);
342            hash_draw_primitive(fill, state);
343            hash_draw_primitive(cutout, state);
344            hash_f32_bits(*blur_radius, state);
345            blend_mode.hash(state);
346            hash_rect(*clip_rect, state);
347        }
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use crate::render_effect::TileMode;
355
356    #[test]
357    fn color_render_hash_changes_with_channels() {
358        assert_ne!(
359            Color(1.0, 0.0, 0.0, 1.0).render_hash(),
360            Color(0.0, 1.0, 0.0, 1.0).render_hash()
361        );
362    }
363
364    #[test]
365    fn point_rect_and_corner_radii_render_hash_changes_with_geometry() {
366        assert_ne!(
367            Point::new(1.0, 2.0).render_hash(),
368            Point::new(2.0, 1.0).render_hash()
369        );
370        assert_ne!(
371            Rect {
372                x: 0.0,
373                y: 0.0,
374                width: 10.0,
375                height: 20.0,
376            }
377            .render_hash(),
378            Rect {
379                x: 0.0,
380                y: 0.0,
381                width: 20.0,
382                height: 10.0,
383            }
384            .render_hash()
385        );
386        assert_ne!(
387            CornerRadii::uniform(4.0).render_hash(),
388            CornerRadii::uniform(6.0).render_hash()
389        );
390    }
391
392    #[test]
393    fn layer_shape_render_hash_tracks_shape_kind_and_radii() {
394        assert_ne!(
395            LayerShape::Rectangle.render_hash(),
396            LayerShape::Rounded(crate::RoundedCornerShape::uniform(8.0)).render_hash()
397        );
398        assert_ne!(
399            LayerShape::Rounded(crate::RoundedCornerShape::uniform(4.0)).render_hash(),
400            LayerShape::Rounded(crate::RoundedCornerShape::uniform(8.0)).render_hash()
401        );
402    }
403
404    #[test]
405    fn brush_render_hash_tracks_gradient_structure() {
406        let base = Brush::linear_gradient_with_tile_mode(
407            vec![Color::RED, Color::BLUE],
408            Point::new(0.0, 0.0),
409            Point::new(10.0, 10.0),
410            TileMode::Clamp,
411        );
412        let shifted = Brush::linear_gradient_with_tile_mode(
413            vec![Color::RED, Color::BLUE],
414            Point::new(1.0, 0.0),
415            Point::new(10.0, 10.0),
416            TileMode::Clamp,
417        );
418
419        assert_ne!(base.render_hash(), shifted.render_hash());
420    }
421
422    #[test]
423    fn color_filter_render_hash_tracks_variant_and_values() {
424        assert_ne!(
425            ColorFilter::Tint(Color::RED).render_hash(),
426            ColorFilter::Modulate(Color::RED).render_hash()
427        );
428        assert_ne!(
429            ColorFilter::Matrix([1.0; 20]).render_hash(),
430            ColorFilter::Matrix([0.0; 20]).render_hash()
431        );
432    }
433
434    #[test]
435    fn render_effect_render_hash_tracks_variant_parameters() {
436        assert_ne!(
437            RenderEffect::blur(4.0).render_hash(),
438            RenderEffect::blur(6.0).render_hash()
439        );
440        assert_ne!(
441            RenderEffect::offset(2.0, 1.0).render_hash(),
442            RenderEffect::offset(1.0, 2.0).render_hash()
443        );
444    }
445
446    #[test]
447    fn runtime_shader_render_hash_ignores_uniforms() {
448        let mut base = RuntimeShader::new("// hash");
449        base.set_float(0, 1.0);
450        let mut changed = base.clone();
451        changed.set_float(0, 2.0);
452        assert_eq!(
453            base.render_hash(),
454            changed.render_hash(),
455            "render_hash must depend only on source, not uniforms — \
456             animated uniforms (time, position) would otherwise produce a \
457             new effect_hash every frame, filling the layer cache with stale textures"
458        );
459    }
460
461    #[test]
462    fn runtime_shader_render_hash_tracks_source() {
463        let a = RuntimeShader::new("// shader A");
464        let b = RuntimeShader::new("// shader B");
465        assert_ne!(a.render_hash(), b.render_hash());
466    }
467
468    #[test]
469    fn draw_primitive_render_hash_tracks_nested_structure() {
470        let base = DrawPrimitive::Blend {
471            primitive: Box::new(DrawPrimitive::Rect {
472                rect: Rect {
473                    x: 0.0,
474                    y: 0.0,
475                    width: 12.0,
476                    height: 8.0,
477                },
478                brush: Brush::solid(Color::WHITE),
479            }),
480            blend_mode: crate::BlendMode::SrcOver,
481        };
482        let changed = DrawPrimitive::Blend {
483            primitive: Box::new(DrawPrimitive::Rect {
484                rect: Rect {
485                    x: 0.0,
486                    y: 0.0,
487                    width: 12.0,
488                    height: 8.0,
489                },
490                brush: Brush::solid(Color::BLACK),
491            }),
492            blend_mode: crate::BlendMode::SrcOver,
493        };
494        assert_ne!(base.render_hash(), changed.render_hash());
495    }
496}