Skip to main content

cranpose_render_common/
graph.rs

1use std::{
2    mem::size_of,
3    ops::{Deref, DerefMut, Range},
4    rc::Rc,
5};
6
7use cranpose_core::{NodeId, collections::map::HashSet};
8use cranpose_ui::{
9    GraphicsLayer, ModifierNodeSlices, Point, Rect, RenderEffect, RoundedCornerShape,
10    TextLayoutOptions, TextOverflow, TextStyle,
11    text::{AnnotatedString, RenderString, SpanStyle, TextDrawStyle},
12};
13pub use cranpose_ui_graphics::transform::{ProjectiveTransform, quad_bounds};
14use cranpose_ui_graphics::{
15    BlendMode, ColorFilter, CommandRecording, DrawPrimitive, RecordingSummary, ShadowPrimitive,
16};
17
18use crate::{raster_cache::LayerRasterCacheHashes, style_shared::DrawPlacement};
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub struct IsolationReasons {
22    pub explicit_offscreen: bool,
23    pub shape_clip: bool,
24    pub effect: bool,
25    pub backdrop: bool,
26    pub group_opacity: bool,
27    pub blend_mode: bool,
28}
29
30impl IsolationReasons {
31    pub fn has_any(self) -> bool {
32        self.explicit_offscreen
33            || self.shape_clip
34            || self.effect
35            || self.backdrop
36            || self.group_opacity
37            || self.blend_mode
38    }
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub enum CachePolicy {
43    #[default]
44    None,
45    Auto,
46}
47
48#[derive(Clone)]
49pub struct HitTestNode {
50    pub shape: Option<RoundedCornerShape>,
51    /// The node's modifier slices, shared rather than copied: they hold its
52    /// pointer inputs and the pointer icon it asks for while hovered. A node
53    /// that only names an icon is still a hit target, which is how a
54    /// decorative panel carries a cursor without handling clicks.
55    pub handlers: Rc<ModifierNodeSlices>,
56}
57
58#[derive(Clone, Debug, PartialEq)]
59pub struct DrawPrimitiveNode {
60    pub primitive: DrawPrimitive,
61    pub clip: Option<Rect>,
62}
63
64#[derive(Clone, Debug, PartialEq)]
65pub struct TextPrimitiveNode {
66    pub node_id: NodeId,
67    pub rect: Rect,
68    /// Shared so the renderer can hand the same allocation to every draw it
69    /// emits for this node instead of deep-copying the string once per emit.
70    pub text: Rc<AnnotatedString>,
71    /// `text` as the renderer's draws carry it across threads, shared with
72    /// the prepared layout it came from so no frame converts it again.
73    pub render_text: std::sync::Arc<RenderString>,
74    /// Shared so every frame's draws of this text hand over the same style
75    /// instead of copying it.
76    pub text_style: std::sync::Arc<TextStyle>,
77    pub font_size: f32,
78    pub layout_options: TextLayoutOptions,
79    pub clip: Option<Rect>,
80}
81
82impl TextPrimitiveNode {
83    /// Conservative bounds for the emitted glyphs and paint. Returns `None`
84    /// when an unclipped layout or style can draw beyond its layout rectangle.
85    pub fn draw_bounds(&self) -> Option<Rect> {
86        if let Some(clip) = self.clip {
87            return Some(clip);
88        }
89        let style = &self.text_style.span_style;
90        let stroked =
91            |style: &SpanStyle| matches!(style.draw_style, Some(TextDrawStyle::Stroke { .. }));
92        (!matches!(self.layout_options.overflow, TextOverflow::Visible)
93            && style.shadow.is_none()
94            && style
95                .baseline_shift
96                .is_none_or(|shift| !shift.is_specified() || shift.0 == 0.0)
97            && !stroked(style)
98            && self
99                .text
100                .span_styles
101                .iter()
102                .all(|span| !stroked(&span.item)))
103        .then_some(self.rect)
104    }
105
106    fn draws_within(&self, bounds: Rect) -> bool {
107        self.draw_bounds()
108            .is_some_and(|drawn| rect_within(drawn, bounds))
109    }
110}
111
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum PrimitivePhase {
114    BeforeChildren,
115    AfterChildren,
116}
117
118#[derive(Clone, Debug, PartialEq)]
119pub enum PrimitiveNode {
120    Draw(Box<DrawPrimitiveNode>),
121    Text(Box<TextPrimitiveNode>),
122}
123
124#[derive(Clone, Debug, PartialEq)]
125pub struct PrimitiveEntry {
126    pub phase: PrimitivePhase,
127    pub node: PrimitiveNode,
128}
129
130/// Scene properties with implicit defaults and independently mutable clones.
131#[derive(Clone, Debug, Default)]
132pub struct LayerProperties(Option<Box<GraphicsLayer>>);
133
134impl LayerProperties {
135    pub(crate) fn replace(&mut self, value: Option<GraphicsLayer>) {
136        match (&mut self.0, value) {
137            (Some(current), Some(value)) => **current = value,
138            (storage, value) => *storage = value.map(Box::new),
139        }
140    }
141}
142
143impl Deref for LayerProperties {
144    type Target = GraphicsLayer;
145
146    fn deref(&self) -> &Self::Target {
147        self.0.as_deref().unwrap_or(&GraphicsLayer::DEFAULT)
148    }
149}
150
151impl DerefMut for LayerProperties {
152    fn deref_mut(&mut self) -> &mut Self::Target {
153        self.0.get_or_insert_with(Box::default)
154    }
155}
156
157impl From<GraphicsLayer> for LayerProperties {
158    fn from(value: GraphicsLayer) -> Self {
159        Self(Some(Box::new(value)))
160    }
161}
162
163impl From<Option<GraphicsLayer>> for LayerProperties {
164    fn from(value: Option<GraphicsLayer>) -> Self {
165        Self(value.map(Box::new))
166    }
167}
168
169impl PartialEq for LayerProperties {
170    fn eq(&self, other: &Self) -> bool {
171        **self == **other
172    }
173}
174
175#[derive(Clone)]
176pub struct LayerNode {
177    pub node_id: Option<NodeId>,
178    /// Set on the synthetic layer that holds a node's outer draws, those chained
179    /// before its graphics layer, around that node's own layer. Scene updates
180    /// address the node through this id because the node's layer has none of the
181    /// outer draws and the wrapper has no node id of its own.
182    pub wraps: Option<NodeId>,
183    /// The bounds of the node's layer, where its clip cuts, its transforms
184    /// pivot and an offscreen pass draws it.
185    pub local_bounds: Rect,
186    /// The node's own rect, `(0, 0, size)`, where its pointer handlers are,
187    /// when it differs from `local_bounds`: a clip or graphics layer that
188    /// wraps a coordinator a later padding or offset moves bounds the layer
189    /// there instead, as in Compose. `None` when the two agree.
190    pub node_bounds: Option<Rect>,
191    pub transform_to_parent: ProjectiveTransform,
192    pub content_offset: Point,
193    pub motion_context_animated: bool,
194    pub translated_content_context: bool,
195    pub translated_content_offset: Point,
196    /// Where layout placed this layer within its parent's content, before the
197    /// parent's content offset. Scene updates add these up from the root, with
198    /// each layer's content offset and graphics-layer translation, to find the
199    /// window origin of a subtree they rebuild.
200    pub origin_in_parent: Point,
201    pub graphics_layer: LayerProperties,
202    pub clip_to_bounds: bool,
203    pub shadow_clip: Option<Rect>,
204    pub hit_test: Option<HitTestNode>,
205    pub has_hit_targets: bool,
206    /// Whether this subtree publishes live window origins (a text field's
207    /// popup anchor, a scroll container's viewport rect). Those sinks are
208    /// written during a full lowering, so the scroll fast path may translate
209    /// a retained subtree in place only when this is false.
210    pub has_origin_sinks: bool,
211    /// Whether everything this layer and its subtree draw lies within its
212    /// `local_bounds`, give or take [`CONTAINED_DRAW_SLACK`], so a renderer
213    /// may skip the whole subtree where those bounds are clipped away. Scene
214    /// building keeps it current; `false` promises nothing.
215    pub draws_within_bounds: bool,
216    pub isolation: IsolationReasons,
217    pub cache_policy: CachePolicy,
218    pub cache_hashes: LayerRasterCacheHashes,
219    pub cache_hashes_valid: bool,
220    pub children: Vec<RenderNode>,
221}
222
223impl Default for LayerNode {
224    fn default() -> Self {
225        Self {
226            node_id: None,
227            wraps: None,
228            local_bounds: Rect {
229                x: 0.0,
230                y: 0.0,
231                width: 0.0,
232                height: 0.0,
233            },
234            node_bounds: None,
235            transform_to_parent: ProjectiveTransform::identity(),
236            content_offset: Point::default(),
237            motion_context_animated: false,
238            translated_content_context: false,
239            translated_content_offset: Point::default(),
240            origin_in_parent: Point::default(),
241            graphics_layer: LayerProperties::default(),
242            clip_to_bounds: false,
243            shadow_clip: None,
244            hit_test: None,
245            has_hit_targets: false,
246            has_origin_sinks: false,
247            draws_within_bounds: false,
248            isolation: IsolationReasons::default(),
249            cache_policy: CachePolicy::None,
250            cache_hashes: LayerRasterCacheHashes::default(),
251            cache_hashes_valid: false,
252            children: Vec::new(),
253        }
254    }
255}
256
257/// How far past its bounds a layer that draws within them may still put
258/// pixels: glyph and edge antialiasing.
259pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
260
261impl LayerNode {
262    /// Whether this layer's content, as [`LayerNode::draws_within_bounds`]
263    /// describes it, stays within its bounds: it clips to them, or its draws,
264    /// its texts and its children placed where they are all fit inside.
265    pub fn content_draws_within_bounds(&self) -> bool {
266        if self.visual_clip_rect().is_some() {
267            return true;
268        }
269        let bounds = inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK);
270        self.children.iter().all(|child| match child {
271            RenderNode::DrawRun(run) => {
272                !run.summary.has_shadow
273                    && run
274                        .recording
275                        .bounds()
276                        .is_none_or(|drawn| rect_within(drawn, bounds))
277            }
278            RenderNode::Primitive(entry) => match &entry.node {
279                PrimitiveNode::Text(text) => text.draws_within(bounds),
280                PrimitiveNode::Draw(_) => false,
281            },
282            RenderNode::Layer(layer) => layer.draws_within_parent(bounds),
283        })
284    }
285
286    pub(crate) fn refresh_child_facts(&mut self) {
287        self.has_hit_targets = self.hit_test.is_some()
288            || self.children.iter().any(|child| match child {
289                RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
290                RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
291            });
292        self.draws_within_bounds = self.content_draws_within_bounds();
293    }
294
295    /// Whether this layer, drawn as a child, puts nothing outside `bounds`:
296    /// it draws within its own bounds, casts no shadow, applies no effect,
297    /// and its bounds placed in its parent lie inside.
298    fn draws_within_parent(&self, bounds: Rect) -> bool {
299        self.draws_within_bounds
300            && self.graphics_layer.shadow_elevation <= 0.0
301            && self.effect().is_none()
302            && self.backdrop().is_none()
303            && rect_within(
304                quad_bounds(
305                    self.transform_to_parent
306                        .map_rect(inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK)),
307                ),
308                bounds,
309            )
310    }
311
312    /// The node's own rect, where its pointer handlers are.
313    pub fn node_rect(&self) -> Rect {
314        self.node_bounds.unwrap_or(self.local_bounds)
315    }
316
317    pub fn clip_rect(&self) -> Option<Rect> {
318        (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
319    }
320
321    /// Where the layer's drawing is cut: its clip or, when it composites
322    /// through an offscreen buffer for its alpha or by request, its bounds,
323    /// as Compose's layer-sized buffer cuts it. Hit testing takes only
324    /// [`Self::clip_rect`]: alpha hides nothing from a finger.
325    pub fn visual_clip_rect(&self) -> Option<Rect> {
326        self.clip_rect().or_else(|| {
327            (self.isolation.group_opacity || self.isolation.explicit_offscreen)
328                .then_some(self.local_bounds)
329        })
330    }
331
332    pub fn effect(&self) -> Option<&RenderEffect> {
333        self.graphics_layer.render_effect.as_ref()
334    }
335
336    pub fn backdrop(&self) -> Option<&RenderEffect> {
337        self.graphics_layer.backdrop_effect.as_ref()
338    }
339
340    pub fn opacity(&self) -> f32 {
341        self.graphics_layer.alpha
342    }
343
344    pub fn blend_mode(&self) -> BlendMode {
345        self.graphics_layer.blend_mode
346    }
347
348    pub fn color_filter(&self) -> Option<ColorFilter> {
349        self.graphics_layer.color_filter
350    }
351
352    pub fn target_content_hash(&self) -> u64 {
353        if self.cache_hashes_valid {
354            self.cache_hashes.target_content
355        } else {
356            crate::graph_hash::layer_raster_cache_hashes(self).target_content
357        }
358    }
359
360    pub fn motion_source_content_hash(&self) -> u64 {
361        crate::graph_hash::layer_motion_source_content_hash(self)
362    }
363
364    pub fn effect_hash(&self) -> u64 {
365        if self.cache_hashes_valid {
366            self.cache_hashes.effect
367        } else {
368            crate::graph_hash::layer_raster_cache_hashes(self).effect
369        }
370    }
371
372    pub fn recompute_raster_cache_hashes(&mut self) {
373        crate::graph_hash::recompute_layer_raster_cache_hashes(self);
374    }
375}
376
377#[derive(Clone)]
378pub enum RenderNode {
379    Primitive(PrimitiveEntry),
380    /// A whole draw command's primitives as one node. A heavy canvas records
381    /// thousands of primitives per frame; wrapping each in its own
382    /// [`RenderNode`] made the graph rebuild move every one of them twice and
383    /// free seventeen thousand nodes per frame on a stress scene. The run
384    /// keeps the recorded vector intact — semantically it is exactly that
385    /// many consecutive `Primitive` draw entries with no per-primitive clip.
386    DrawRun(DrawRunNode),
387    Layer(Box<LayerNode>),
388}
389
390/// Stable identity of the draw command a run was recorded from: the layout
391/// node owning the command, the command's index in that node's command list,
392/// and which placement pass produced this run (a `WithContent` command emits
393/// one run per placement, so the pair alone is not unique). Rendering does
394/// not read it yet; it is the key under which retained recording state lives
395/// as retention moves up to the draw-command recorder, and it must survive
396/// recording, graph construction, normalized-scene creation, and renderer
397/// cache lookup unchanged.
398#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
399pub struct DrawCommandId {
400    pub node_id: NodeId,
401    pub command_index: u32,
402    pub placement: DrawPlacement,
403}
404
405#[derive(Clone, Debug, PartialEq)]
406pub struct DrawRunNode {
407    pub phase: PrimitivePhase,
408    /// Which draw command recorded these primitives. `None` only for runs
409    /// with no per-command provenance (hand-built tests).
410    pub command: Option<DrawCommandId>,
411    /// Shared, not owned: the recording registry keyed by [`DrawCommandId`]
412    /// keeps a handle to the same recording, so its buffers survive this
413    /// node being dropped on the next rebuild and the command re-records
414    /// into them. Nothing mutates a recording after construction, which is
415    /// what makes sharing sound.
416    pub recording: Rc<CommandRecording>,
417    /// The segments of the recording this run draws: one placement's part
418    /// of a with-content command, or the whole recording.
419    pub segments: Range<u32>,
420    /// Content facts consumers keep asking per frame, answered while
421    /// recording and never by rescanning.
422    pub summary: DrawRunSummary,
423}
424
425/// What a run contains, answered while recording.
426pub type DrawRunSummary = RecordingSummary;
427
428impl DrawRunNode {
429    pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
430        Self::for_command(phase, None, primitives)
431    }
432
433    pub fn for_command(
434        phase: PrimitivePhase,
435        command: Option<DrawCommandId>,
436        primitives: Vec<DrawPrimitive>,
437    ) -> Self {
438        let recording = CommandRecording::from_primitives(primitives);
439        let segments = recording.all_segments();
440        Self::for_command_shared(phase, command, Rc::new(recording), segments)
441    }
442
443    pub fn for_command_shared(
444        phase: PrimitivePhase,
445        command: Option<DrawCommandId>,
446        recording: Rc<CommandRecording>,
447        segments: Range<u32>,
448    ) -> Self {
449        let summary = recording.summary_in(&segments);
450        Self {
451            phase,
452            command,
453            recording,
454            segments,
455            summary,
456        }
457    }
458
459    /// The run's primitives, materialised from the recording in order.
460    pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
461        self.recording.primitives(self.segments.clone())
462    }
463
464    /// The rect every entry of the run can reach.
465    pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
466        self.recording.coverage_rects(self.segments.clone())
467    }
468
469    pub fn len(&self) -> usize {
470        self.recording.len_in(&self.segments)
471    }
472
473    pub fn is_empty(&self) -> bool {
474        self.recording.is_empty_in(&self.segments)
475    }
476}
477
478#[derive(Clone)]
479pub struct RenderGraph {
480    pub root: LayerNode,
481}
482
483impl RenderGraph {
484    pub fn new(mut root: LayerNode) -> Self {
485        root.recompute_raster_cache_hashes();
486        Self { root }
487    }
488
489    pub fn node_count(&self) -> usize {
490        fn count_layer(layer: &LayerNode) -> usize {
491            1 + layer
492                .children
493                .iter()
494                .map(|child| match child {
495                    RenderNode::Primitive(_) => 1,
496                    RenderNode::DrawRun(run) => run.len(),
497                    RenderNode::Layer(child_layer) => count_layer(child_layer),
498                })
499                .sum::<usize>()
500        }
501
502        count_layer(&self.root)
503    }
504
505    pub fn heap_bytes(&self) -> usize {
506        layer_heap_bytes(&self.root)
507    }
508
509    /// Replaces the set with the graph's visual observation owners, retaining its capacity.
510    pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
511        fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
512            if let Some(node_id) = layer.node_id {
513                nodes.insert(node_id);
514            }
515            for child in &layer.children {
516                match child {
517                    RenderNode::DrawRun(run) => {
518                        if let Some(command) = run.command {
519                            nodes.insert(command.node_id);
520                        }
521                    }
522                    RenderNode::Layer(child) => collect(child, nodes),
523                    RenderNode::Primitive(_) => {}
524                }
525            }
526        }
527
528        nodes.clear();
529        collect(&self.root, nodes);
530    }
531}
532
533fn layer_heap_bytes(layer: &LayerNode) -> usize {
534    size_of::<RenderNode>() * layer.children.capacity()
535        + layer
536            .graphics_layer
537            .0
538            .as_ref()
539            .map_or(0, |_| size_of::<GraphicsLayer>())
540        + layer
541            .children
542            .iter()
543            .map(render_node_heap_bytes)
544            .sum::<usize>()
545}
546
547fn render_node_heap_bytes(node: &RenderNode) -> usize {
548    match node {
549        RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
550        RenderNode::DrawRun(run) => {
551            run.recording.pod_heap_bytes()
552                + std::mem::size_of_val(run.recording.others())
553                + run
554                    .recording
555                    .others()
556                    .iter()
557                    .map(draw_primitive_heap_bytes)
558                    .sum::<usize>()
559        }
560        RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
561    }
562}
563
564fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
565    match &entry.node {
566        PrimitiveNode::Draw(draw) => {
567            size_of::<DrawPrimitiveNode>() + draw_primitive_heap_bytes(&draw.primitive)
568        }
569        PrimitiveNode::Text(text) => {
570            size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
571        }
572    }
573}
574
575fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
576    match primitive {
577        DrawPrimitive::Content
578        | DrawPrimitive::Rect { .. }
579        | DrawPrimitive::RoundRect { .. }
580        | DrawPrimitive::Arc { .. }
581        | DrawPrimitive::Line { .. } => 0,
582        DrawPrimitive::Blend { primitive, .. } => {
583            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
584        }
585        DrawPrimitive::Image { .. } => 0,
586        DrawPrimitive::Text(text) => {
587            size_of::<cranpose_ui_graphics::TextPrimitive>()
588                + text.text.len()
589                + text
590                    .style
591                    .font_family
592                    .as_ref()
593                    .map_or(0, std::string::String::capacity)
594        }
595        DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
596    }
597}
598
599fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
600    match shadow {
601        ShadowPrimitive::Drop { shape, .. } => {
602            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
603        }
604        ShadowPrimitive::Inner { fill, cutout, .. } => {
605            size_of::<DrawPrimitive>() * 2
606                + draw_primitive_heap_bytes(fill)
607                + draw_primitive_heap_bytes(cutout)
608        }
609    }
610}
611
612fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
613    text.text.capacity()
614        + text.span_styles.capacity() * size_of::<usize>() * 2
615        + text.paragraph_styles.capacity() * size_of::<usize>() * 2
616        + text.string_annotations.capacity() * size_of::<usize>() * 2
617        + text.link_annotations.capacity() * size_of::<usize>() * 2
618        + text
619            .string_annotations
620            .iter()
621            .map(|annotation| {
622                annotation.item.tag.capacity() + annotation.item.annotation.capacity()
623            })
624            .sum::<usize>()
625        + text
626            .link_annotations
627            .iter()
628            .map(|annotation| match &annotation.item {
629                cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
630                cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
631            })
632            .sum::<usize>()
633}
634
635fn inflate_rect(rect: Rect, by: f32) -> Rect {
636    Rect {
637        x: rect.x - by,
638        y: rect.y - by,
639        width: rect.width + by * 2.0,
640        height: rect.height + by * 2.0,
641    }
642}
643
644fn rect_within(inner: Rect, outer: Rect) -> bool {
645    inner.x >= outer.x
646        && inner.y >= outer.y
647        && inner.x + inner.width <= outer.x + outer.width
648        && inner.y + inner.height <= outer.y + outer.height
649}
650
651#[cfg(test)]
652#[path = "tests/graph_tests.rs"]
653mod tests;