Skip to main content

cranpose_render_common/
graph.rs

1use std::{mem::size_of, ops::Range, rc::Rc};
2
3use cranpose_core::{NodeId, collections::map::HashSet};
4use cranpose_foundation::PointerEvent;
5use cranpose_ui::{
6    GraphicsLayer, Point, Rect, RenderEffect, RoundedCornerShape, TextLayoutOptions, TextStyle,
7    text::AnnotatedString,
8};
9use cranpose_ui_graphics::{
10    BlendMode, ColorFilter, CommandRecording, DrawPrimitive, RecordingSummary, ShadowPrimitive,
11};
12
13use crate::{raster_cache::LayerRasterCacheHashes, style_shared::DrawPlacement};
14
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct ProjectiveTransform {
17    matrix: [[f32; 3]; 3],
18}
19
20impl ProjectiveTransform {
21    pub const fn identity() -> Self {
22        Self {
23            matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
24        }
25    }
26
27    pub fn translation(tx: f32, ty: f32) -> Self {
28        Self {
29            matrix: [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]],
30        }
31    }
32
33    /// Uniform scale about the origin (device-scale root transform: render
34    /// graphs stay in logical dp; density applies at execution).
35    pub fn uniform_scale(scale: f32) -> Self {
36        Self {
37            matrix: [[scale, 0.0, 0.0], [0.0, scale, 0.0], [0.0, 0.0, 1.0]],
38        }
39    }
40
41    pub fn from_rect_to_quad(rect: Rect, quad: [[f32; 2]; 4]) -> Self {
42        if rect.width.abs() <= f32::EPSILON || rect.height.abs() <= f32::EPSILON {
43            return Self::translation(quad[0][0], quad[0][1]);
44        }
45
46        if let Some(axis_aligned) = axis_aligned_rect_from_quad(quad) {
47            let scale_x = axis_aligned.width / rect.width;
48            let scale_y = axis_aligned.height / rect.height;
49            return Self {
50                matrix: [
51                    [scale_x, 0.0, axis_aligned.x - rect.x * scale_x],
52                    [0.0, scale_y, axis_aligned.y - rect.y * scale_y],
53                    [0.0, 0.0, 1.0],
54                ],
55            };
56        }
57
58        let source = [
59            [rect.x, rect.y],
60            [rect.x + rect.width, rect.y],
61            [rect.x, rect.y + rect.height],
62            [rect.x + rect.width, rect.y + rect.height],
63        ];
64        let Some(coefficients) = solve_homography(source, quad) else {
65            return Self::identity();
66        };
67
68        Self {
69            matrix: [
70                [coefficients[0], coefficients[1], coefficients[2]],
71                [coefficients[3], coefficients[4], coefficients[5]],
72                [coefficients[6], coefficients[7], 1.0],
73            ],
74        }
75    }
76
77    /// Returns the composed transform that applies `self` first and `next` second.
78    pub fn then(self, next: Self) -> Self {
79        Self {
80            matrix: multiply_matrices(next.matrix, self.matrix),
81        }
82    }
83
84    pub fn inverse(self) -> Option<Self> {
85        let m = self.matrix;
86        let a = m[0][0];
87        let b = m[0][1];
88        let c = m[0][2];
89        let d = m[1][0];
90        let e = m[1][1];
91        let f = m[1][2];
92        let g = m[2][0];
93        let h = m[2][1];
94        let i = m[2][2];
95
96        let cofactor00 = e * i - f * h;
97        let cofactor01 = -(d * i - f * g);
98        let cofactor02 = d * h - e * g;
99        let cofactor10 = -(b * i - c * h);
100        let cofactor11 = a * i - c * g;
101        let cofactor12 = -(a * h - b * g);
102        let cofactor20 = b * f - c * e;
103        let cofactor21 = -(a * f - c * d);
104        let cofactor22 = a * e - b * d;
105
106        let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
107        if determinant.abs() <= f32::EPSILON {
108            return None;
109        }
110        let inverse_determinant = 1.0 / determinant;
111
112        Some(Self {
113            matrix: [
114                [
115                    cofactor00 * inverse_determinant,
116                    cofactor10 * inverse_determinant,
117                    cofactor20 * inverse_determinant,
118                ],
119                [
120                    cofactor01 * inverse_determinant,
121                    cofactor11 * inverse_determinant,
122                    cofactor21 * inverse_determinant,
123                ],
124                [
125                    cofactor02 * inverse_determinant,
126                    cofactor12 * inverse_determinant,
127                    cofactor22 * inverse_determinant,
128                ],
129            ],
130        })
131    }
132
133    pub fn matrix(self) -> [[f32; 3]; 3] {
134        self.matrix
135    }
136
137    pub fn map_point(self, point: Point) -> Point {
138        let x = point.x;
139        let y = point.y;
140        let w = self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2];
141        let safe_w = if w.abs() <= f32::EPSILON { 1.0 } else { w };
142
143        Point {
144            x: (self.matrix[0][0] * x + self.matrix[0][1] * y + self.matrix[0][2]) / safe_w,
145            y: (self.matrix[1][0] * x + self.matrix[1][1] * y + self.matrix[1][2]) / safe_w,
146        }
147    }
148
149    pub fn map_rect(self, rect: Rect) -> [[f32; 2]; 4] {
150        [
151            self.map_point(Point {
152                x: rect.x,
153                y: rect.y,
154            }),
155            self.map_point(Point {
156                x: rect.x + rect.width,
157                y: rect.y,
158            }),
159            self.map_point(Point {
160                x: rect.x,
161                y: rect.y + rect.height,
162            }),
163            self.map_point(Point {
164                x: rect.x + rect.width,
165                y: rect.y + rect.height,
166            }),
167        ]
168        .map(|point| [point.x, point.y])
169    }
170
171    pub fn bounds_for_rect(self, rect: Rect) -> Rect {
172        quad_bounds(self.map_rect(rect))
173    }
174}
175
176fn axis_aligned_rect_from_quad(quad: [[f32; 2]; 4]) -> Option<Rect> {
177    let top_left = quad[0];
178    let top_right = quad[1];
179    let bottom_left = quad[2];
180    let bottom_right = quad[3];
181    let x_epsilon = 1e-4;
182    let y_epsilon = 1e-4;
183
184    if (top_left[1] - top_right[1]).abs() > y_epsilon
185        || (bottom_left[1] - bottom_right[1]).abs() > y_epsilon
186        || (top_left[0] - bottom_left[0]).abs() > x_epsilon
187        || (top_right[0] - bottom_right[0]).abs() > x_epsilon
188    {
189        return None;
190    }
191
192    Some(Rect {
193        x: top_left[0],
194        y: top_left[1],
195        width: top_right[0] - top_left[0],
196        height: bottom_left[1] - top_left[1],
197    })
198}
199
200impl Default for ProjectiveTransform {
201    fn default() -> Self {
202        Self::identity()
203    }
204}
205
206#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
207pub struct IsolationReasons {
208    pub explicit_offscreen: bool,
209    pub shape_clip: bool,
210    pub effect: bool,
211    pub backdrop: bool,
212    pub group_opacity: bool,
213    pub blend_mode: bool,
214}
215
216impl IsolationReasons {
217    pub fn has_any(self) -> bool {
218        self.explicit_offscreen
219            || self.shape_clip
220            || self.effect
221            || self.backdrop
222            || self.group_opacity
223            || self.blend_mode
224    }
225}
226
227#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
228pub enum CachePolicy {
229    #[default]
230    None,
231    Auto,
232}
233
234#[derive(Clone)]
235pub struct HitTestNode {
236    pub shape: Option<RoundedCornerShape>,
237    pub click_actions: Vec<Rc<dyn Fn(Point)>>,
238    pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
239    pub clip: Option<Rect>,
240}
241
242#[derive(Clone, Debug, PartialEq)]
243pub struct DrawPrimitiveNode {
244    pub primitive: DrawPrimitive,
245    pub clip: Option<Rect>,
246}
247
248#[derive(Clone, Debug, PartialEq)]
249pub struct TextPrimitiveNode {
250    pub node_id: NodeId,
251    pub rect: Rect,
252    /// Shared so the renderer can hand the same allocation to every draw it
253    /// emits for this node instead of deep-copying the string once per emit.
254    pub text: Rc<AnnotatedString>,
255    pub text_style: TextStyle,
256    pub font_size: f32,
257    pub layout_options: TextLayoutOptions,
258    pub clip: Option<Rect>,
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub enum PrimitivePhase {
263    BeforeChildren,
264    AfterChildren,
265}
266
267#[derive(Clone, Debug, PartialEq)]
268pub enum PrimitiveNode {
269    Draw(DrawPrimitiveNode),
270    Text(Box<TextPrimitiveNode>),
271}
272
273#[derive(Clone, Debug, PartialEq)]
274pub struct PrimitiveEntry {
275    pub phase: PrimitivePhase,
276    pub node: PrimitiveNode,
277}
278
279#[derive(Clone)]
280pub struct LayerNode {
281    pub node_id: Option<NodeId>,
282    /// Set on the synthetic layer that holds a node's outer draws, those chained
283    /// before its graphics layer, around that node's own layer. Scene updates
284    /// address the node through this id because the node's layer has none of the
285    /// outer draws and the wrapper has no node id of its own.
286    pub wraps: Option<NodeId>,
287    pub local_bounds: Rect,
288    pub transform_to_parent: ProjectiveTransform,
289    pub content_offset: Point,
290    pub motion_context_animated: bool,
291    pub translated_content_context: bool,
292    pub translated_content_offset: Point,
293    /// Window-space origin this layer's children are placed from, and the
294    /// accumulated ancestor graphics-layer translation, captured during the
295    /// full per-frame scene build. Read back when a dirty subtree is rebuilt in
296    /// isolation (`update_scene_from_applier`) so a scrolling field's window
297    /// origin stays live during a fling even though the ancestor chain is not
298    /// re-walked from the root. Defaults to the identity origin.
299    pub scene_children_origin: Point,
300    pub scene_children_layer_translation: Point,
301    pub graphics_layer: GraphicsLayer,
302    pub clip_to_bounds: bool,
303    pub shadow_clip: Option<Rect>,
304    pub hit_test: Option<HitTestNode>,
305    pub has_hit_targets: bool,
306    /// Whether this subtree publishes live window origins (a text field's
307    /// popup anchor, a scroll container's viewport rect). Those sinks are
308    /// written during a full lowering, so the scroll fast path may translate
309    /// a retained subtree in place only when this is false.
310    pub has_origin_sinks: bool,
311    pub isolation: IsolationReasons,
312    pub cache_policy: CachePolicy,
313    pub cache_hashes: LayerRasterCacheHashes,
314    pub cache_hashes_valid: bool,
315    pub children: Vec<RenderNode>,
316}
317
318impl Default for LayerNode {
319    fn default() -> Self {
320        Self {
321            node_id: None,
322            wraps: None,
323            local_bounds: Rect {
324                x: 0.0,
325                y: 0.0,
326                width: 0.0,
327                height: 0.0,
328            },
329            transform_to_parent: ProjectiveTransform::identity(),
330            content_offset: Point::default(),
331            motion_context_animated: false,
332            translated_content_context: false,
333            translated_content_offset: Point::default(),
334            scene_children_origin: Point::default(),
335            scene_children_layer_translation: Point::default(),
336            graphics_layer: GraphicsLayer::default(),
337            clip_to_bounds: false,
338            shadow_clip: None,
339            hit_test: None,
340            has_hit_targets: false,
341            has_origin_sinks: false,
342            isolation: IsolationReasons::default(),
343            cache_policy: CachePolicy::None,
344            cache_hashes: LayerRasterCacheHashes::default(),
345            cache_hashes_valid: false,
346            children: Vec::new(),
347        }
348    }
349}
350
351impl LayerNode {
352    pub fn clip_rect(&self) -> Option<Rect> {
353        (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
354    }
355
356    pub fn effect(&self) -> Option<&RenderEffect> {
357        self.graphics_layer.render_effect.as_ref()
358    }
359
360    pub fn backdrop(&self) -> Option<&RenderEffect> {
361        self.graphics_layer.backdrop_effect.as_ref()
362    }
363
364    pub fn opacity(&self) -> f32 {
365        self.graphics_layer.alpha
366    }
367
368    pub fn blend_mode(&self) -> BlendMode {
369        self.graphics_layer.blend_mode
370    }
371
372    pub fn color_filter(&self) -> Option<ColorFilter> {
373        self.graphics_layer.color_filter
374    }
375
376    pub fn target_content_hash(&self) -> u64 {
377        if self.cache_hashes_valid {
378            self.cache_hashes.target_content
379        } else {
380            crate::graph_hash::layer_raster_cache_hashes(self).target_content
381        }
382    }
383
384    pub fn motion_source_content_hash(&self) -> u64 {
385        crate::graph_hash::layer_motion_source_content_hash(self)
386    }
387
388    pub fn effect_hash(&self) -> u64 {
389        if self.cache_hashes_valid {
390            self.cache_hashes.effect
391        } else {
392            crate::graph_hash::layer_raster_cache_hashes(self).effect
393        }
394    }
395
396    pub fn recompute_raster_cache_hashes(&mut self) {
397        crate::graph_hash::recompute_layer_raster_cache_hashes(self);
398    }
399}
400
401#[derive(Clone)]
402pub enum RenderNode {
403    Primitive(PrimitiveEntry),
404    /// A whole draw command's primitives as one node. A heavy canvas records
405    /// thousands of primitives per frame; wrapping each in its own
406    /// [`RenderNode`] made the graph rebuild move every one of them twice and
407    /// free seventeen thousand nodes per frame on a stress scene. The run
408    /// keeps the recorded vector intact — semantically it is exactly that
409    /// many consecutive `Primitive` draw entries with no per-primitive clip.
410    DrawRun(DrawRunNode),
411    Layer(Box<LayerNode>),
412}
413
414/// Stable identity of the draw command a run was recorded from: the layout
415/// node owning the command, the command's index in that node's command list,
416/// and which placement pass produced this run (a `WithContent` command emits
417/// one run per placement, so the pair alone is not unique). Rendering does
418/// not read it yet; it is the key under which retained recording state lives
419/// as retention moves up to the draw-command recorder, and it must survive
420/// recording, graph construction, normalized-scene creation, and renderer
421/// cache lookup unchanged.
422#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
423pub struct DrawCommandId {
424    pub node_id: NodeId,
425    pub command_index: u32,
426    pub placement: DrawPlacement,
427}
428
429#[derive(Clone, Debug, PartialEq)]
430pub struct DrawRunNode {
431    pub phase: PrimitivePhase,
432    /// Which draw command recorded these primitives. `None` only for runs
433    /// with no per-command provenance (hand-built tests).
434    pub command: Option<DrawCommandId>,
435    /// Shared, not owned: the recording registry keyed by [`DrawCommandId`]
436    /// keeps a handle to the same recording, so its buffers survive this
437    /// node being dropped on the next rebuild and the command re-records
438    /// into them. Nothing mutates a recording after construction, which is
439    /// what makes sharing sound.
440    pub recording: Rc<CommandRecording>,
441    /// The segments of the recording this run draws: one placement's part
442    /// of a with-content command, or the whole recording.
443    pub segments: Range<u32>,
444    /// Content facts consumers keep asking per frame, answered while
445    /// recording and never by rescanning.
446    pub summary: DrawRunSummary,
447}
448
449/// What a run contains, answered while recording.
450pub type DrawRunSummary = RecordingSummary;
451
452impl DrawRunNode {
453    pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
454        Self::for_command(phase, None, primitives)
455    }
456
457    pub fn for_command(
458        phase: PrimitivePhase,
459        command: Option<DrawCommandId>,
460        primitives: Vec<DrawPrimitive>,
461    ) -> Self {
462        let recording = CommandRecording::from_primitives(primitives);
463        let segments = recording.all_segments();
464        Self::for_command_shared(phase, command, Rc::new(recording), segments)
465    }
466
467    pub fn for_command_shared(
468        phase: PrimitivePhase,
469        command: Option<DrawCommandId>,
470        recording: Rc<CommandRecording>,
471        segments: Range<u32>,
472    ) -> Self {
473        let summary = recording.summary_in(&segments);
474        Self {
475            phase,
476            command,
477            recording,
478            segments,
479            summary,
480        }
481    }
482
483    /// The run's primitives, materialised from the recording in order.
484    pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
485        self.recording.primitives(self.segments.clone())
486    }
487
488    /// The rect every entry of the run can reach.
489    pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
490        self.recording.coverage_rects(self.segments.clone())
491    }
492
493    pub fn len(&self) -> usize {
494        self.recording.len_in(&self.segments)
495    }
496
497    pub fn is_empty(&self) -> bool {
498        self.recording.is_empty_in(&self.segments)
499    }
500}
501
502#[derive(Clone)]
503pub struct RenderGraph {
504    pub root: LayerNode,
505}
506
507impl RenderGraph {
508    pub fn new(mut root: LayerNode) -> Self {
509        root.recompute_raster_cache_hashes();
510        Self { root }
511    }
512
513    pub fn node_count(&self) -> usize {
514        fn count_layer(layer: &LayerNode) -> usize {
515            1 + layer
516                .children
517                .iter()
518                .map(|child| match child {
519                    RenderNode::Primitive(_) => 1,
520                    RenderNode::DrawRun(run) => run.len(),
521                    RenderNode::Layer(child_layer) => count_layer(child_layer),
522                })
523                .sum::<usize>()
524        }
525
526        count_layer(&self.root)
527    }
528
529    pub fn heap_bytes(&self) -> usize {
530        layer_heap_bytes(&self.root)
531    }
532
533    /// Replaces the set with the graph's visual observation owners, retaining its capacity.
534    pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
535        fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
536            if let Some(node_id) = layer.node_id {
537                nodes.insert(node_id);
538            }
539            for child in &layer.children {
540                match child {
541                    RenderNode::DrawRun(run) => {
542                        if let Some(command) = run.command {
543                            nodes.insert(command.node_id);
544                        }
545                    }
546                    RenderNode::Layer(child) => collect(child, nodes),
547                    RenderNode::Primitive(_) => {}
548                }
549            }
550        }
551
552        nodes.clear();
553        collect(&self.root, nodes);
554    }
555}
556
557fn layer_heap_bytes(layer: &LayerNode) -> usize {
558    layer.hit_test.as_ref().map_or(0, hit_test_heap_bytes)
559        + size_of::<RenderNode>() * layer.children.capacity()
560        + layer
561            .children
562            .iter()
563            .map(render_node_heap_bytes)
564            .sum::<usize>()
565}
566
567fn render_node_heap_bytes(node: &RenderNode) -> usize {
568    match node {
569        RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
570        RenderNode::DrawRun(run) => {
571            run.recording.pod_heap_bytes()
572                + std::mem::size_of_val(run.recording.others())
573                + run
574                    .recording
575                    .others()
576                    .iter()
577                    .map(draw_primitive_heap_bytes)
578                    .sum::<usize>()
579        }
580        RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
581    }
582}
583
584fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
585    match &entry.node {
586        PrimitiveNode::Draw(draw) => draw_primitive_heap_bytes(&draw.primitive),
587        PrimitiveNode::Text(text) => {
588            size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
589        }
590    }
591}
592
593fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
594    match primitive {
595        DrawPrimitive::Content
596        | DrawPrimitive::Rect { .. }
597        | DrawPrimitive::RoundRect { .. }
598        | DrawPrimitive::Arc { .. } => 0,
599        DrawPrimitive::Blend { primitive, .. } => {
600            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
601        }
602        DrawPrimitive::Image { .. } => 0,
603        DrawPrimitive::Text(text) => {
604            size_of::<cranpose_ui_graphics::TextPrimitive>()
605                + text.text.len()
606                + text
607                    .style
608                    .font_family
609                    .as_ref()
610                    .map_or(0, |family| family.capacity())
611        }
612        DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
613    }
614}
615
616fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
617    match shadow {
618        ShadowPrimitive::Drop { shape, .. } => {
619            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
620        }
621        ShadowPrimitive::Inner { fill, cutout, .. } => {
622            size_of::<DrawPrimitive>() * 2
623                + draw_primitive_heap_bytes(fill)
624                + draw_primitive_heap_bytes(cutout)
625        }
626    }
627}
628
629fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
630    text.text.capacity()
631        + text.span_styles.capacity() * size_of::<usize>() * 2
632        + text.paragraph_styles.capacity() * size_of::<usize>() * 2
633        + text.string_annotations.capacity() * size_of::<usize>() * 2
634        + text.link_annotations.capacity() * size_of::<usize>() * 2
635        + text
636            .string_annotations
637            .iter()
638            .map(|annotation| {
639                annotation.item.tag.capacity() + annotation.item.annotation.capacity()
640            })
641            .sum::<usize>()
642        + text
643            .link_annotations
644            .iter()
645            .map(|annotation| match &annotation.item {
646                cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
647                cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
648            })
649            .sum::<usize>()
650}
651
652fn hit_test_heap_bytes(hit_test: &HitTestNode) -> usize {
653    hit_test.click_actions.capacity() * size_of::<Rc<dyn Fn(Point)>>()
654        + hit_test.pointer_inputs.capacity() * size_of::<Rc<dyn Fn(PointerEvent)>>()
655}
656
657pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
658    let mut min_x = f32::INFINITY;
659    let mut min_y = f32::INFINITY;
660    let mut max_x = f32::NEG_INFINITY;
661    let mut max_y = f32::NEG_INFINITY;
662
663    for [x, y] in quad {
664        min_x = min_x.min(x);
665        min_y = min_y.min(y);
666        max_x = max_x.max(x);
667        max_y = max_y.max(y);
668    }
669
670    Rect {
671        x: min_x,
672        y: min_y,
673        width: (max_x - min_x).max(0.0),
674        height: (max_y - min_y).max(0.0),
675    }
676}
677
678fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
679    let mut out = [[0.0; 3]; 3];
680    for row in 0..3 {
681        for col in 0..3 {
682            out[row][col] =
683                lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col];
684        }
685    }
686    out
687}
688
689fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
690    let mut matrix = [[0.0f32; 9]; 8];
691    for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
692        let row = index * 2;
693        let x = src[0];
694        let y = src[1];
695        let u = dst[0];
696        let v = dst[1];
697
698        matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
699        matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
700    }
701
702    for pivot in 0..8 {
703        let mut pivot_row = pivot;
704        let mut pivot_value = matrix[pivot][pivot].abs();
705        let mut candidate = pivot + 1;
706        while candidate < 8 {
707            let candidate_value = matrix[candidate][pivot].abs();
708            if candidate_value > pivot_value {
709                pivot_row = candidate;
710                pivot_value = candidate_value;
711            }
712            candidate += 1;
713        }
714
715        if pivot_value <= f32::EPSILON {
716            return None;
717        }
718
719        if pivot_row != pivot {
720            matrix.swap(pivot, pivot_row);
721        }
722
723        let divisor = matrix[pivot][pivot];
724        let mut col = pivot;
725        while col < 9 {
726            matrix[pivot][col] /= divisor;
727            col += 1;
728        }
729
730        for row in 0..8 {
731            if row == pivot {
732                continue;
733            }
734            let factor = matrix[row][pivot];
735            if factor.abs() <= f32::EPSILON {
736                continue;
737            }
738            let mut col = pivot;
739            while col < 9 {
740                matrix[row][col] -= factor * matrix[pivot][col];
741                col += 1;
742            }
743        }
744    }
745
746    let mut solution = [0.0f32; 8];
747    for index in 0..8 {
748        solution[index] = matrix[index][8];
749    }
750    Some(solution)
751}
752
753#[cfg(test)]
754mod tests {
755    use cranpose_ui_graphics::{Brush, Color, DrawPrimitive};
756
757    use super::*;
758
759    fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
760        LayerNode {
761            local_bounds,
762            children,
763            ..Default::default()
764        }
765    }
766
767    #[test]
768    fn projective_transform_translation_maps_points() {
769        let transform = ProjectiveTransform::translation(7.0, -3.5);
770        let mapped = transform.map_point(Point { x: 2.0, y: 4.0 });
771        assert!((mapped.x - 9.0).abs() < 1e-6);
772        assert!((mapped.y - 0.5).abs() < 1e-6);
773    }
774
775    #[test]
776    fn projective_transform_then_composes_in_parent_order() {
777        let child = ProjectiveTransform::translation(4.0, 2.0);
778        let parent = ProjectiveTransform::translation(10.0, -1.0);
779        let composed = child.then(parent);
780        let mapped = composed.map_point(Point { x: 1.0, y: 1.0 });
781        assert!((mapped.x - 15.0).abs() < 1e-6);
782        assert!((mapped.y - 2.0).abs() < 1e-6);
783    }
784
785    #[test]
786    fn homography_maps_rect_corners_to_target_quad() {
787        let rect = Rect {
788            x: 0.0,
789            y: 0.0,
790            width: 20.0,
791            height: 10.0,
792        };
793        let quad = [[5.0, 7.0], [25.0, 6.0], [7.0, 20.0], [28.0, 21.0]];
794        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
795        let mapped = transform.map_rect(rect);
796        for (expected, actual) in quad.into_iter().zip(mapped) {
797            assert!((expected[0] - actual[0]).abs() < 1e-4);
798            assert!((expected[1] - actual[1]).abs() < 1e-4);
799        }
800    }
801
802    #[test]
803    fn axis_aligned_rect_to_quad_keeps_exact_affine_matrix() {
804        let rect = Rect {
805            x: 2.0,
806            y: 3.0,
807            width: 20.0,
808            height: 10.0,
809        };
810        let quad = [[12.0, 9.0], [32.0, 9.0], [12.0, 19.0], [32.0, 19.0]];
811        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
812
813        assert_eq!(
814            transform.matrix(),
815            [[1.0, 0.0, 10.0], [0.0, 1.0, 6.0], [0.0, 0.0, 1.0]]
816        );
817    }
818
819    #[test]
820    fn axis_aligned_rect_to_quad_keeps_exact_axis_aligned_scale() {
821        let rect = Rect {
822            x: 4.0,
823            y: 6.0,
824            width: 10.0,
825            height: 8.0,
826        };
827        let quad = [[20.0, 18.0], [50.0, 18.0], [20.0, 42.0], [50.0, 42.0]];
828        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
829
830        assert_eq!(
831            transform.matrix(),
832            [[3.0, 0.0, 8.0], [0.0, 3.0, 0.0], [0.0, 0.0, 1.0]]
833        );
834    }
835
836    #[test]
837    fn retained_visual_observation_nodes_collect_layers_and_command_owners() {
838        let bounds = Rect {
839            x: 0.0,
840            y: 0.0,
841            width: 20.0,
842            height: 20.0,
843        };
844        let command = |node_id| DrawCommandId {
845            node_id,
846            command_index: 0,
847            placement: DrawPlacement::Behind,
848        };
849        let mut child = test_layer(
850            bounds,
851            vec![RenderNode::DrawRun(DrawRunNode::for_command(
852                PrimitivePhase::BeforeChildren,
853                Some(command(17)),
854                Vec::new(),
855            ))],
856        );
857        child.node_id = Some(13);
858        let mut root = test_layer(
859            bounds,
860            vec![
861                RenderNode::DrawRun(DrawRunNode::for_command(
862                    PrimitivePhase::BeforeChildren,
863                    Some(command(9)),
864                    Vec::new(),
865                )),
866                RenderNode::DrawRun(DrawRunNode::new(PrimitivePhase::BeforeChildren, Vec::new())),
867                RenderNode::Layer(Box::new(child)),
868            ],
869        );
870
871        root.node_id = Some(5);
872
873        let mut graph = RenderGraph::new(root);
874        let mut nodes = HashSet::from([999]);
875        graph.collect_retained_visual_observation_nodes(&mut nodes);
876        assert_eq!(nodes, HashSet::from([5, 9, 13, 17]));
877        let capacity = nodes.capacity();
878        graph.root.children.clear();
879        graph.root.node_id = Some(23);
880        graph.collect_retained_visual_observation_nodes(&mut nodes);
881        assert_eq!(nodes, HashSet::from([23]));
882        assert_eq!(nodes.capacity(), capacity);
883    }
884
885    #[test]
886    fn render_graph_new_recomputes_manual_layer_hashes() {
887        let primitive = PrimitiveEntry {
888            phase: PrimitivePhase::BeforeChildren,
889            node: PrimitiveNode::Draw(DrawPrimitiveNode {
890                primitive: DrawPrimitive::Rect {
891                    rect: Rect {
892                        x: 1.0,
893                        y: 2.0,
894                        width: 8.0,
895                        height: 6.0,
896                    },
897                    brush: Brush::solid(Color::WHITE),
898                    stroke: None,
899                },
900                clip: None,
901            }),
902        };
903        let mut root = test_layer(
904            Rect {
905                x: 0.0,
906                y: 0.0,
907                width: 20.0,
908                height: 20.0,
909            },
910            vec![RenderNode::Primitive(primitive)],
911        );
912        root.graphics_layer.render_effect = Some(RenderEffect::blur(3.0));
913        let mut expected = root.clone();
914        expected.recompute_raster_cache_hashes();
915
916        let graph = RenderGraph::new(root);
917        assert_eq!(
918            graph.root.target_content_hash(),
919            expected.target_content_hash()
920        );
921        assert_eq!(graph.root.effect_hash(), expected.effect_hash());
922    }
923
924    #[test]
925    fn motion_source_content_hash_ignores_translated_content_offset() {
926        let primitive = PrimitiveEntry {
927            phase: PrimitivePhase::BeforeChildren,
928            node: PrimitiveNode::Draw(DrawPrimitiveNode {
929                primitive: DrawPrimitive::Rect {
930                    rect: Rect {
931                        x: 1.0,
932                        y: 2.0,
933                        width: 8.0,
934                        height: 6.0,
935                    },
936                    brush: Brush::solid(Color::WHITE),
937                    stroke: None,
938                },
939                clip: None,
940            }),
941        };
942        let mut base = test_layer(
943            Rect {
944                x: 0.0,
945                y: 0.0,
946                width: 20.0,
947                height: 20.0,
948            },
949            vec![RenderNode::Primitive(primitive)],
950        );
951        base.translated_content_context = true;
952        base.translated_content_offset = Point::new(0.0, -24.0);
953        base.recompute_raster_cache_hashes();
954
955        let mut moved = base.clone();
956        moved.translated_content_offset = Point::new(0.0, -72.0);
957        moved.recompute_raster_cache_hashes();
958
959        assert_ne!(base.target_content_hash(), moved.target_content_hash());
960        assert_eq!(
961            base.motion_source_content_hash(),
962            moved.motion_source_content_hash()
963        );
964    }
965}