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