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