Skip to main content

cranpose_render_common/
graph.rs

1use std::mem::size_of;
2use std::rc::Rc;
3
4use cranpose_core::NodeId;
5use cranpose_foundation::PointerEvent;
6use cranpose_ui::text::AnnotatedString;
7use cranpose_ui::{
8    GraphicsLayer, Point, Rect, RenderEffect, RoundedCornerShape, TextLayoutOptions, TextStyle,
9};
10use cranpose_ui_graphics::{BlendMode, ColorFilter, DrawPrimitive, ShadowPrimitive};
11
12use crate::raster_cache::LayerRasterCacheHashes;
13
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct ProjectiveTransform {
16    matrix: [[f32; 3]; 3],
17}
18
19impl ProjectiveTransform {
20    pub const fn identity() -> Self {
21        Self {
22            matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
23        }
24    }
25
26    pub fn translation(tx: f32, ty: f32) -> Self {
27        Self {
28            matrix: [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]],
29        }
30    }
31
32    pub fn from_rect_to_quad(rect: Rect, quad: [[f32; 2]; 4]) -> Self {
33        if rect.width.abs() <= f32::EPSILON || rect.height.abs() <= f32::EPSILON {
34            return Self::translation(quad[0][0], quad[0][1]);
35        }
36
37        if let Some(axis_aligned) = axis_aligned_rect_from_quad(quad) {
38            let scale_x = axis_aligned.width / rect.width;
39            let scale_y = axis_aligned.height / rect.height;
40            return Self {
41                matrix: [
42                    [scale_x, 0.0, axis_aligned.x - rect.x * scale_x],
43                    [0.0, scale_y, axis_aligned.y - rect.y * scale_y],
44                    [0.0, 0.0, 1.0],
45                ],
46            };
47        }
48
49        let source = [
50            [rect.x, rect.y],
51            [rect.x + rect.width, rect.y],
52            [rect.x, rect.y + rect.height],
53            [rect.x + rect.width, rect.y + rect.height],
54        ];
55        let Some(coefficients) = solve_homography(source, quad) else {
56            return Self::identity();
57        };
58
59        Self {
60            matrix: [
61                [coefficients[0], coefficients[1], coefficients[2]],
62                [coefficients[3], coefficients[4], coefficients[5]],
63                [coefficients[6], coefficients[7], 1.0],
64            ],
65        }
66    }
67
68    /// Returns the composed transform that applies `self` first and `next` second.
69    pub fn then(self, next: Self) -> Self {
70        Self {
71            matrix: multiply_matrices(next.matrix, self.matrix),
72        }
73    }
74
75    pub fn inverse(self) -> Option<Self> {
76        let m = self.matrix;
77        let a = m[0][0];
78        let b = m[0][1];
79        let c = m[0][2];
80        let d = m[1][0];
81        let e = m[1][1];
82        let f = m[1][2];
83        let g = m[2][0];
84        let h = m[2][1];
85        let i = m[2][2];
86
87        let cofactor00 = e * i - f * h;
88        let cofactor01 = -(d * i - f * g);
89        let cofactor02 = d * h - e * g;
90        let cofactor10 = -(b * i - c * h);
91        let cofactor11 = a * i - c * g;
92        let cofactor12 = -(a * h - b * g);
93        let cofactor20 = b * f - c * e;
94        let cofactor21 = -(a * f - c * d);
95        let cofactor22 = a * e - b * d;
96
97        let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
98        if determinant.abs() <= f32::EPSILON {
99            return None;
100        }
101        let inverse_determinant = 1.0 / determinant;
102
103        Some(Self {
104            matrix: [
105                [
106                    cofactor00 * inverse_determinant,
107                    cofactor10 * inverse_determinant,
108                    cofactor20 * inverse_determinant,
109                ],
110                [
111                    cofactor01 * inverse_determinant,
112                    cofactor11 * inverse_determinant,
113                    cofactor21 * inverse_determinant,
114                ],
115                [
116                    cofactor02 * inverse_determinant,
117                    cofactor12 * inverse_determinant,
118                    cofactor22 * inverse_determinant,
119                ],
120            ],
121        })
122    }
123
124    pub fn matrix(self) -> [[f32; 3]; 3] {
125        self.matrix
126    }
127
128    pub fn map_point(self, point: Point) -> Point {
129        let x = point.x;
130        let y = point.y;
131        let w = self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2];
132        let safe_w = if w.abs() <= f32::EPSILON { 1.0 } else { w };
133
134        Point {
135            x: (self.matrix[0][0] * x + self.matrix[0][1] * y + self.matrix[0][2]) / safe_w,
136            y: (self.matrix[1][0] * x + self.matrix[1][1] * y + self.matrix[1][2]) / safe_w,
137        }
138    }
139
140    pub fn map_rect(self, rect: Rect) -> [[f32; 2]; 4] {
141        [
142            self.map_point(Point {
143                x: rect.x,
144                y: rect.y,
145            }),
146            self.map_point(Point {
147                x: rect.x + rect.width,
148                y: rect.y,
149            }),
150            self.map_point(Point {
151                x: rect.x,
152                y: rect.y + rect.height,
153            }),
154            self.map_point(Point {
155                x: rect.x + rect.width,
156                y: rect.y + rect.height,
157            }),
158        ]
159        .map(|point| [point.x, point.y])
160    }
161
162    pub fn bounds_for_rect(self, rect: Rect) -> Rect {
163        quad_bounds(self.map_rect(rect))
164    }
165}
166
167fn axis_aligned_rect_from_quad(quad: [[f32; 2]; 4]) -> Option<Rect> {
168    let top_left = quad[0];
169    let top_right = quad[1];
170    let bottom_left = quad[2];
171    let bottom_right = quad[3];
172    let x_epsilon = 1e-4;
173    let y_epsilon = 1e-4;
174
175    if (top_left[1] - top_right[1]).abs() > y_epsilon
176        || (bottom_left[1] - bottom_right[1]).abs() > y_epsilon
177        || (top_left[0] - bottom_left[0]).abs() > x_epsilon
178        || (top_right[0] - bottom_right[0]).abs() > x_epsilon
179    {
180        return None;
181    }
182
183    Some(Rect {
184        x: top_left[0],
185        y: top_left[1],
186        width: top_right[0] - top_left[0],
187        height: bottom_left[1] - top_left[1],
188    })
189}
190
191impl Default for ProjectiveTransform {
192    fn default() -> Self {
193        Self::identity()
194    }
195}
196
197#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
198pub struct IsolationReasons {
199    pub explicit_offscreen: bool,
200    pub shape_clip: bool,
201    pub effect: bool,
202    pub backdrop: bool,
203    pub group_opacity: bool,
204    pub blend_mode: bool,
205}
206
207impl IsolationReasons {
208    pub fn has_any(self) -> bool {
209        self.explicit_offscreen
210            || self.shape_clip
211            || self.effect
212            || self.backdrop
213            || self.group_opacity
214            || self.blend_mode
215    }
216}
217
218#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
219pub enum CachePolicy {
220    #[default]
221    None,
222    Auto,
223}
224
225#[derive(Clone)]
226pub struct HitTestNode {
227    pub shape: Option<RoundedCornerShape>,
228    pub click_actions: Vec<Rc<dyn Fn(Point)>>,
229    pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
230    pub clip: Option<Rect>,
231}
232
233#[derive(Clone, Debug, PartialEq)]
234pub struct DrawPrimitiveNode {
235    pub primitive: DrawPrimitive,
236    pub clip: Option<Rect>,
237}
238
239#[derive(Clone, Debug, PartialEq)]
240pub struct TextPrimitiveNode {
241    pub node_id: NodeId,
242    pub rect: Rect,
243    pub text: AnnotatedString,
244    pub text_style: TextStyle,
245    pub font_size: f32,
246    pub layout_options: TextLayoutOptions,
247    pub clip: Option<Rect>,
248}
249
250#[derive(Clone, Copy, Debug, PartialEq, Eq)]
251pub enum PrimitivePhase {
252    BeforeChildren,
253    AfterChildren,
254}
255
256#[derive(Clone, Debug, PartialEq)]
257pub enum PrimitiveNode {
258    Draw(DrawPrimitiveNode),
259    Text(Box<TextPrimitiveNode>),
260}
261
262#[derive(Clone, Debug, PartialEq)]
263pub struct PrimitiveEntry {
264    pub phase: PrimitivePhase,
265    pub node: PrimitiveNode,
266}
267
268#[derive(Clone)]
269pub struct LayerNode {
270    pub node_id: Option<NodeId>,
271    pub local_bounds: Rect,
272    pub transform_to_parent: ProjectiveTransform,
273    pub content_offset: Point,
274    pub motion_context_animated: bool,
275    pub translated_content_context: bool,
276    pub translated_content_offset: Point,
277    pub graphics_layer: GraphicsLayer,
278    pub clip_to_bounds: bool,
279    pub shadow_clip: Option<Rect>,
280    pub hit_test: Option<HitTestNode>,
281    pub has_hit_targets: bool,
282    pub isolation: IsolationReasons,
283    pub cache_policy: CachePolicy,
284    pub cache_hashes: LayerRasterCacheHashes,
285    pub cache_hashes_valid: bool,
286    pub children: Vec<RenderNode>,
287}
288
289impl LayerNode {
290    pub fn clip_rect(&self) -> Option<Rect> {
291        (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
292    }
293
294    pub fn effect(&self) -> Option<&RenderEffect> {
295        self.graphics_layer.render_effect.as_ref()
296    }
297
298    pub fn backdrop(&self) -> Option<&RenderEffect> {
299        self.graphics_layer.backdrop_effect.as_ref()
300    }
301
302    pub fn opacity(&self) -> f32 {
303        self.graphics_layer.alpha
304    }
305
306    pub fn blend_mode(&self) -> BlendMode {
307        self.graphics_layer.blend_mode
308    }
309
310    pub fn color_filter(&self) -> Option<ColorFilter> {
311        self.graphics_layer.color_filter
312    }
313
314    pub fn target_content_hash(&self) -> u64 {
315        if self.cache_hashes_valid {
316            self.cache_hashes.target_content
317        } else {
318            crate::graph_hash::layer_raster_cache_hashes(self).target_content
319        }
320    }
321
322    pub fn motion_source_content_hash(&self) -> u64 {
323        crate::graph_hash::layer_motion_source_content_hash(self)
324    }
325
326    pub fn effect_hash(&self) -> u64 {
327        if self.cache_hashes_valid {
328            self.cache_hashes.effect
329        } else {
330            crate::graph_hash::layer_raster_cache_hashes(self).effect
331        }
332    }
333
334    pub fn recompute_raster_cache_hashes(&mut self) {
335        crate::graph_hash::recompute_layer_raster_cache_hashes(self);
336    }
337}
338
339#[derive(Clone)]
340pub enum RenderNode {
341    Primitive(PrimitiveEntry),
342    Layer(Box<LayerNode>),
343}
344
345#[derive(Clone)]
346pub struct RenderGraph {
347    pub root: LayerNode,
348}
349
350impl RenderGraph {
351    pub fn new(mut root: LayerNode) -> Self {
352        root.recompute_raster_cache_hashes();
353        Self { root }
354    }
355
356    pub fn node_count(&self) -> usize {
357        fn count_layer(layer: &LayerNode) -> usize {
358            1 + layer
359                .children
360                .iter()
361                .map(|child| match child {
362                    RenderNode::Primitive(_) => 1,
363                    RenderNode::Layer(child_layer) => count_layer(child_layer),
364                })
365                .sum::<usize>()
366        }
367
368        count_layer(&self.root)
369    }
370
371    pub fn heap_bytes(&self) -> usize {
372        layer_heap_bytes(&self.root)
373    }
374}
375
376fn layer_heap_bytes(layer: &LayerNode) -> usize {
377    layer.hit_test.as_ref().map_or(0, hit_test_heap_bytes)
378        + size_of::<RenderNode>() * layer.children.capacity()
379        + layer
380            .children
381            .iter()
382            .map(render_node_heap_bytes)
383            .sum::<usize>()
384}
385
386fn render_node_heap_bytes(node: &RenderNode) -> usize {
387    match node {
388        RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
389        RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
390    }
391}
392
393fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
394    match &entry.node {
395        PrimitiveNode::Draw(draw) => draw_primitive_heap_bytes(&draw.primitive),
396        PrimitiveNode::Text(text) => {
397            size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
398        }
399    }
400}
401
402fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
403    match primitive {
404        DrawPrimitive::Content | DrawPrimitive::Rect { .. } | DrawPrimitive::RoundRect { .. } => 0,
405        DrawPrimitive::Blend { primitive, .. } => {
406            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
407        }
408        DrawPrimitive::Image { .. } => 0,
409        DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
410    }
411}
412
413fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
414    match shadow {
415        ShadowPrimitive::Drop { shape, .. } => {
416            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
417        }
418        ShadowPrimitive::Inner { fill, cutout, .. } => {
419            size_of::<DrawPrimitive>() * 2
420                + draw_primitive_heap_bytes(fill)
421                + draw_primitive_heap_bytes(cutout)
422        }
423    }
424}
425
426fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
427    text.text.capacity()
428        + text.span_styles.capacity() * size_of::<usize>() * 2
429        + text.paragraph_styles.capacity() * size_of::<usize>() * 2
430        + text.string_annotations.capacity() * size_of::<usize>() * 2
431        + text.link_annotations.capacity() * size_of::<usize>() * 2
432        + text
433            .string_annotations
434            .iter()
435            .map(|annotation| {
436                annotation.item.tag.capacity() + annotation.item.annotation.capacity()
437            })
438            .sum::<usize>()
439        + text
440            .link_annotations
441            .iter()
442            .map(|annotation| match &annotation.item {
443                cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
444                cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
445            })
446            .sum::<usize>()
447}
448
449fn hit_test_heap_bytes(hit_test: &HitTestNode) -> usize {
450    hit_test.click_actions.capacity() * size_of::<Rc<dyn Fn(Point)>>()
451        + hit_test.pointer_inputs.capacity() * size_of::<Rc<dyn Fn(PointerEvent)>>()
452}
453
454pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
455    let mut min_x = f32::INFINITY;
456    let mut min_y = f32::INFINITY;
457    let mut max_x = f32::NEG_INFINITY;
458    let mut max_y = f32::NEG_INFINITY;
459
460    for [x, y] in quad {
461        min_x = min_x.min(x);
462        min_y = min_y.min(y);
463        max_x = max_x.max(x);
464        max_y = max_y.max(y);
465    }
466
467    Rect {
468        x: min_x,
469        y: min_y,
470        width: (max_x - min_x).max(0.0),
471        height: (max_y - min_y).max(0.0),
472    }
473}
474
475fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
476    let mut out = [[0.0; 3]; 3];
477    for row in 0..3 {
478        for col in 0..3 {
479            out[row][col] =
480                lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col];
481        }
482    }
483    out
484}
485
486fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
487    let mut matrix = [[0.0f32; 9]; 8];
488    for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
489        let row = index * 2;
490        let x = src[0];
491        let y = src[1];
492        let u = dst[0];
493        let v = dst[1];
494
495        matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
496        matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
497    }
498
499    for pivot in 0..8 {
500        let mut pivot_row = pivot;
501        let mut pivot_value = matrix[pivot][pivot].abs();
502        let mut candidate = pivot + 1;
503        while candidate < 8 {
504            let candidate_value = matrix[candidate][pivot].abs();
505            if candidate_value > pivot_value {
506                pivot_row = candidate;
507                pivot_value = candidate_value;
508            }
509            candidate += 1;
510        }
511
512        if pivot_value <= f32::EPSILON {
513            return None;
514        }
515
516        if pivot_row != pivot {
517            matrix.swap(pivot, pivot_row);
518        }
519
520        let divisor = matrix[pivot][pivot];
521        let mut col = pivot;
522        while col < 9 {
523            matrix[pivot][col] /= divisor;
524            col += 1;
525        }
526
527        for row in 0..8 {
528            if row == pivot {
529                continue;
530            }
531            let factor = matrix[row][pivot];
532            if factor.abs() <= f32::EPSILON {
533                continue;
534            }
535            let mut col = pivot;
536            while col < 9 {
537                matrix[row][col] -= factor * matrix[pivot][col];
538                col += 1;
539            }
540        }
541    }
542
543    let mut solution = [0.0f32; 8];
544    for index in 0..8 {
545        solution[index] = matrix[index][8];
546    }
547    Some(solution)
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553    use crate::raster_cache::LayerRasterCacheHashes;
554    use cranpose_ui_graphics::{Brush, Color, DrawPrimitive};
555
556    fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
557        LayerNode {
558            node_id: None,
559            local_bounds,
560            transform_to_parent: ProjectiveTransform::identity(),
561            content_offset: Point::default(),
562            motion_context_animated: false,
563            translated_content_context: false,
564            translated_content_offset: Point::default(),
565            graphics_layer: GraphicsLayer::default(),
566            clip_to_bounds: false,
567            shadow_clip: None,
568            hit_test: None,
569            has_hit_targets: false,
570            isolation: IsolationReasons::default(),
571            cache_policy: CachePolicy::None,
572            cache_hashes: LayerRasterCacheHashes::default(),
573            cache_hashes_valid: false,
574            children,
575        }
576    }
577
578    #[test]
579    fn projective_transform_translation_maps_points() {
580        let transform = ProjectiveTransform::translation(7.0, -3.5);
581        let mapped = transform.map_point(Point { x: 2.0, y: 4.0 });
582        assert!((mapped.x - 9.0).abs() < 1e-6);
583        assert!((mapped.y - 0.5).abs() < 1e-6);
584    }
585
586    #[test]
587    fn projective_transform_then_composes_in_parent_order() {
588        let child = ProjectiveTransform::translation(4.0, 2.0);
589        let parent = ProjectiveTransform::translation(10.0, -1.0);
590        let composed = child.then(parent);
591        let mapped = composed.map_point(Point { x: 1.0, y: 1.0 });
592        assert!((mapped.x - 15.0).abs() < 1e-6);
593        assert!((mapped.y - 2.0).abs() < 1e-6);
594    }
595
596    #[test]
597    fn homography_maps_rect_corners_to_target_quad() {
598        let rect = Rect {
599            x: 0.0,
600            y: 0.0,
601            width: 20.0,
602            height: 10.0,
603        };
604        let quad = [[5.0, 7.0], [25.0, 6.0], [7.0, 20.0], [28.0, 21.0]];
605        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
606        let mapped = transform.map_rect(rect);
607        for (expected, actual) in quad.into_iter().zip(mapped) {
608            assert!((expected[0] - actual[0]).abs() < 1e-4);
609            assert!((expected[1] - actual[1]).abs() < 1e-4);
610        }
611    }
612
613    #[test]
614    fn axis_aligned_rect_to_quad_keeps_exact_affine_matrix() {
615        let rect = Rect {
616            x: 2.0,
617            y: 3.0,
618            width: 20.0,
619            height: 10.0,
620        };
621        let quad = [[12.0, 9.0], [32.0, 9.0], [12.0, 19.0], [32.0, 19.0]];
622        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
623
624        assert_eq!(
625            transform.matrix(),
626            [[1.0, 0.0, 10.0], [0.0, 1.0, 6.0], [0.0, 0.0, 1.0]]
627        );
628    }
629
630    #[test]
631    fn axis_aligned_rect_to_quad_keeps_exact_axis_aligned_scale() {
632        let rect = Rect {
633            x: 4.0,
634            y: 6.0,
635            width: 10.0,
636            height: 8.0,
637        };
638        let quad = [[20.0, 18.0], [50.0, 18.0], [20.0, 42.0], [50.0, 42.0]];
639        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
640
641        assert_eq!(
642            transform.matrix(),
643            [[3.0, 0.0, 8.0], [0.0, 3.0, 0.0], [0.0, 0.0, 1.0]]
644        );
645    }
646
647    #[test]
648    fn render_graph_new_recomputes_manual_layer_hashes() {
649        let primitive = PrimitiveEntry {
650            phase: PrimitivePhase::BeforeChildren,
651            node: PrimitiveNode::Draw(DrawPrimitiveNode {
652                primitive: DrawPrimitive::Rect {
653                    rect: Rect {
654                        x: 1.0,
655                        y: 2.0,
656                        width: 8.0,
657                        height: 6.0,
658                    },
659                    brush: Brush::solid(Color::WHITE),
660                },
661                clip: None,
662            }),
663        };
664        let mut root = test_layer(
665            Rect {
666                x: 0.0,
667                y: 0.0,
668                width: 20.0,
669                height: 20.0,
670            },
671            vec![RenderNode::Primitive(primitive)],
672        );
673        root.graphics_layer.render_effect = Some(RenderEffect::blur(3.0));
674        let mut expected = root.clone();
675        expected.recompute_raster_cache_hashes();
676
677        let graph = RenderGraph::new(root);
678        assert_eq!(
679            graph.root.target_content_hash(),
680            expected.target_content_hash()
681        );
682        assert_eq!(graph.root.effect_hash(), expected.effect_hash());
683    }
684
685    #[test]
686    fn motion_source_content_hash_ignores_translated_content_offset() {
687        let primitive = PrimitiveEntry {
688            phase: PrimitivePhase::BeforeChildren,
689            node: PrimitiveNode::Draw(DrawPrimitiveNode {
690                primitive: DrawPrimitive::Rect {
691                    rect: Rect {
692                        x: 1.0,
693                        y: 2.0,
694                        width: 8.0,
695                        height: 6.0,
696                    },
697                    brush: Brush::solid(Color::WHITE),
698                },
699                clip: None,
700            }),
701        };
702        let mut base = test_layer(
703            Rect {
704                x: 0.0,
705                y: 0.0,
706                width: 20.0,
707                height: 20.0,
708            },
709            vec![RenderNode::Primitive(primitive)],
710        );
711        base.translated_content_context = true;
712        base.translated_content_offset = Point::new(0.0, -24.0);
713        base.recompute_raster_cache_hashes();
714
715        let mut moved = base.clone();
716        moved.translated_content_offset = Point::new(0.0, -72.0);
717        moved.recompute_raster_cache_hashes();
718
719        assert_ne!(base.target_content_hash(), moved.target_content_hash());
720        assert_eq!(
721            base.motion_source_content_hash(),
722            moved.motion_source_content_hash()
723        );
724    }
725}