1use std::{any::Any, rc::Rc};
2
3use cranpose_core::{MemoryApplier, NodeId, collections::map::HashMap};
4use cranpose_ui::{LayoutBox, ModifierNodeSlices, Point, Rect, Size, SubcomposeLayoutNode};
5use cranpose_ui_graphics::GraphicsLayer;
6
7use crate::{
8 NodeUpdate, SceneUpdates,
9 graph::{LayerNode, ProjectiveTransform, RenderGraph, RenderNode},
10 layer_transform::layer_transform_to_parent,
11};
12
13mod dirty_walk;
14pub(crate) use dirty_walk::{DirtyAncestry, RetainedPath};
15use dirty_walk::{update_along_retained_path, walk_dirty_layers};
16mod draw_recording;
17use draw_recording::{
18 DrawPass, DrawTarget, append_draw_nodes, bump_recording_generation, draw_nodes,
19 layer_node_capacity,
20};
21pub(crate) use draw_recording::{RecorderKey, RecorderSlot};
22pub use draw_recording::{clear_command_recordings_for_tests, draw_command_nodes_for_tests};
23mod hit_graph;
24pub(crate) use hit_graph::HitGraphState;
25use hit_graph::rebuild_tracking_hits;
26mod inline_leaf;
27use inline_leaf::{lower_child, update_dirty_inline};
28mod layer_shape;
29use layer_shape::shaped_graphics_layer;
30mod text_node;
31mod translate_geometry;
32use text_node::{TextNodeParts, text_node_from_parts, translate_unwrapped};
33use translate_geometry::{KeptChild, TranslateGeometry, move_kept_child, translate_retained_child};
34mod node_layer;
35use node_layer::{
36 AbsOrigin, LayerHead, LowerContext, PlacedGeometry, assign_layer, children_have_origin_sinks,
37 hit_test_from_slices, layer_and_node_bounds, layer_identity, layout_box_to_snapshot,
38 lower_root_into, modifier_slices_have_origin_sinks, outer_draws, placed_child_geometry,
39 placed_transform, prepare_node_layer, read_node_data, read_placed_node_data,
40 slices_hit_something, take_wrapped_layer, write_node_content, write_node_layer,
41 write_snapshot_layer, write_wrapper,
42};
43mod translate_layer;
44pub(crate) use translate_layer::TranslateScratch;
45use translate_layer::{
46 TranslatedContainer, translate_tracking_hits, translated_container,
47 write_own_content_around_children,
48};
49
50#[derive(Clone, Default)]
51struct BuildNodeSnapshot {
52 node_id: NodeId,
53 placement: Point,
54 size: Size,
55 content_offset: Point,
56 slices: Rc<ModifierNodeSlices>,
59 graphics_layer: Option<GraphicsLayer>,
60 children: Vec<Self>,
61}
62
63struct SnapshotNodeData<'a> {
64 layout_state: cranpose_ui::widgets::LayoutState,
65 modifier_slices: Rc<ModifierNodeSlices>,
66 children: &'a [NodeId],
67 window_root: bool,
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum GraphRebuildReason {
80 RootLayerUnavailable,
82 DirtyLayerUnavailable,
84 UnmatchedDirtyNodes(usize),
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum GraphUpdate {
94 Patched,
96 NeedsRebuild(GraphRebuildReason),
98}
99
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub struct GraphUpdateReport {
103 pub update: GraphUpdate,
105 pub hit_graph_dirty: bool,
108}
109
110impl GraphUpdateReport {
111 pub fn applied(self) -> bool {
113 matches!(self.update, GraphUpdate::Patched)
114 }
115
116 pub fn rebuild_reason(self) -> Option<GraphRebuildReason> {
118 match self.update {
119 GraphUpdate::Patched => None,
120 GraphUpdate::NeedsRebuild(reason) => Some(reason),
121 }
122 }
123}
124
125fn note_layer_lowered() {
126 #[cfg(test)]
127 crate::thread_state::with(|state| {
128 let count = &state.lowered_layer_count;
129 count.set(count.get() + 1);
130 });
131}
132
133#[cfg(test)]
134fn reset_lowered_layer_count() {
135 crate::thread_state::with(|state| state.lowered_layer_count.set(0));
136}
137
138#[cfg(test)]
139fn lowered_layer_count() -> usize {
140 crate::thread_state::with(|state| state.lowered_layer_count.get())
141}
142
143pub fn build_graph_from_layout_tree(root: &LayoutBox, _scale: f32) -> RenderGraph {
145 bump_recording_generation();
146 let root_snapshot = layout_box_to_snapshot(root, None);
147 let mut graph = RenderGraph::new(LayerNode::default());
148 write_snapshot_layer(root_snapshot, LowerContext::ROOT, &mut graph.root);
149 graph
150}
151
152pub fn build_graph_from_applier(
155 applier: &MemoryApplier,
156 root: NodeId,
157 _scale: f32,
158) -> Option<RenderGraph> {
159 bump_recording_generation();
160 let mut graph = RenderGraph::new(LayerNode::default());
161 lower_root_into(applier, root, &mut graph.root).then_some(graph)
162}
163
164pub fn rebuild_graph_from_applier(
167 applier: &MemoryApplier,
168 root: NodeId,
169 scale: f32,
170 previous: Option<RenderGraph>,
171) -> Option<RenderGraph> {
172 let graph = match previous {
173 Some(mut graph) => {
174 release_for_rebuild(&mut graph.root);
175 bump_recording_generation();
176 graph.retained_path = None;
177 graph.update += 1;
178 graph.root.note_content_change(graph.update);
179 lower_root_into(applier, root, &mut graph.root).then_some(graph)
180 }
181 None => build_graph_from_applier(applier, root, scale),
182 };
183 crate::layer_recycling::release();
184 graph
185}
186
187pub fn update_graph_from_applier(
190 applier: &MemoryApplier,
191 graph: &mut RenderGraph,
192 updates: SceneUpdates<'_>,
193 scale: f32,
194) -> bool {
195 update_graph_from_applier_report(applier, graph, updates, scale).applied()
196}
197
198#[expect(clippy::print_stderr, reason = "CRANPOSE_SCENE_UPDATE_DIAG asks")]
201pub fn update_graph_from_applier_report(
202 applier: &MemoryApplier,
203 graph: &mut RenderGraph,
204 updates: SceneUpdates<'_>,
205 _scale: f32,
206) -> GraphUpdateReport {
207 let report = update_graph_from_applier_report_inner(applier, graph, updates);
208 crate::layer_recycling::release();
209 if let GraphUpdate::NeedsRebuild(reason) = report.update
210 && cranpose_core::DIAGNOSTICS.scene_update_diag.is_set()
211 {
212 eprintln!(
213 "[scene-update-diag] scoped update abandoned, whole scene rebuilt: {reason:?} dirty={}",
214 updates.len()
215 );
216 }
217 report
218}
219
220fn dirty_node_updates(updates: SceneUpdates<'_>, dirty: &mut HashMap<NodeId, NodeUpdate>) {
223 dirty.clear();
224 dirty.reserve(updates.len());
225 for &(id, update) in updates {
226 dirty
227 .entry(id)
228 .and_modify(|kind| *kind = kind.merge(update))
229 .or_insert(update);
230 }
231}
232
233#[expect(clippy::print_stderr, reason = "CRANPOSE_SCENE_UPDATE_DIAG asks")]
234fn update_graph_from_applier_report_inner(
235 applier: &MemoryApplier,
236 graph: &mut RenderGraph,
237 updates: SceneUpdates<'_>,
238) -> GraphUpdateReport {
239 if updates.is_empty() {
240 return GraphUpdateReport {
241 update: GraphUpdate::Patched,
242 hit_graph_dirty: false,
243 };
244 }
245 bump_recording_generation();
246 graph.update += 1;
247 let update = graph.update;
248
249 if cranpose_core::DIAGNOSTICS.scene_update_diag.is_set() {
250 eprintln!("[scene-update-diag] dirty={updates:?}");
251 }
252
253 let mut remaining_dirty_nodes =
255 crate::thread_state::with(|state| state.dirty_node_scratch.take()).unwrap_or_default();
256 dirty_node_updates(updates, &mut remaining_dirty_nodes);
257 let report = update_dirty_nodes(applier, graph, &mut remaining_dirty_nodes, update);
258 remaining_dirty_nodes.clear();
259 crate::thread_state::with(|state| state.dirty_node_scratch.set(Some(remaining_dirty_nodes)));
260 report
261}
262
263fn update_dirty_nodes(
266 applier: &MemoryApplier,
267 graph: &mut RenderGraph,
268 remaining_dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
269 update: u64,
270) -> GraphUpdateReport {
271 if let Some(root_id) = layer_identity(&graph.root)
272 && let Some(kind) = remaining_dirty_nodes.remove(&root_id)
273 {
274 if let Some(hit_graph_dirty) = try_update_retained_layer(
275 applier,
276 &mut graph.root,
277 remaining_dirty_nodes,
278 kind,
279 Level::Root,
280 TranslateAncestorContext {
281 inherited_motion_context_animated: false,
282 update,
283 inherited_translated_content_context: false,
284 parent_content_offset: Point::default(),
285 parent_abs: AbsOrigin::ROOT,
286 },
287 ) {
288 if remaining_dirty_nodes.is_empty() {
289 return GraphUpdateReport {
290 update: GraphUpdate::Patched,
291 hit_graph_dirty,
292 };
293 }
294 let walked = walk_dirty_layers(applier, graph, remaining_dirty_nodes, update)
295 .map(|(report, _)| report);
296 return GraphUpdateReport {
297 update: classify_walk(applier, walked.is_some(), remaining_dirty_nodes),
298 hit_graph_dirty: hit_graph_dirty || walked.is_none_or(|r| r.hit_graph_dirty),
299 };
300 }
301
302 let Some(hit_graph_dirty) = rebuild_tracking_hits(&mut graph.root, |root| {
303 release_for_rebuild(root);
304 lower_root_into(applier, root_id, root)
305 }) else {
306 return GraphUpdateReport {
307 update: GraphUpdate::NeedsRebuild(GraphRebuildReason::RootLayerUnavailable),
308 hit_graph_dirty: true,
309 };
310 };
311 graph.root.note_content_change(update);
312
313 return GraphUpdateReport {
314 update: GraphUpdate::Patched,
315 hit_graph_dirty,
316 };
317 }
318
319 if let Some(path) = graph.retained_path.take()
322 && let Some(report) = update_along_retained_path(
323 applier,
324 &mut graph.root,
325 &path,
326 remaining_dirty_nodes,
327 update,
328 )
329 {
330 if report.is_some() {
331 graph.retained_path = Some(RetainedPath { update, ..path });
332 }
333 return match report {
334 Some(report) => GraphUpdateReport {
335 update: GraphUpdate::Patched,
336 hit_graph_dirty: report.hit_graph_dirty,
337 },
338 None => GraphUpdateReport {
339 update: GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable),
340 hit_graph_dirty: true,
341 },
342 };
343 }
344
345 let Some((report, path)) = walk_dirty_layers(applier, graph, remaining_dirty_nodes, update)
346 else {
347 return GraphUpdateReport {
348 update: GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable),
349 hit_graph_dirty: true,
350 };
351 };
352 graph.retained_path = path;
353
354 match classify_walk(applier, true, remaining_dirty_nodes) {
355 GraphUpdate::Patched => GraphUpdateReport {
356 update: GraphUpdate::Patched,
357 hit_graph_dirty: report.hit_graph_dirty,
358 },
359 update => GraphUpdateReport {
360 update,
361 hit_graph_dirty: true,
362 },
363 }
364}
365
366fn classify_walk(
369 applier: &MemoryApplier,
370 walked: bool,
371 remaining_dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
372) -> GraphUpdate {
373 if !walked {
374 return GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable);
375 }
376 remaining_dirty_nodes.retain(|&node, _| is_drawn(applier, node));
377 if !remaining_dirty_nodes.is_empty() {
378 GraphUpdate::NeedsRebuild(GraphRebuildReason::UnmatchedDirtyNodes(
379 remaining_dirty_nodes.len(),
380 ))
381 } else {
382 GraphUpdate::Patched
383 }
384}
385
386fn is_drawn(applier: &MemoryApplier, node: NodeId) -> bool {
391 let mut child = node;
392 let mut current = node;
393 while let Some(parent) = applier
394 .get_ref(current)
395 .ok()
396 .and_then(cranpose_core::Node::parent)
397 {
398 let Ok(parent_node) = applier.get_ref(parent) else {
399 return true;
400 };
401 current = parent;
402 if parent_node.is_virtual() {
403 continue;
404 }
405 let parent_node: &dyn Any = parent_node;
406 if let Some(subcompose) = parent_node.downcast_ref::<SubcomposeLayoutNode>()
407 && !subcompose.with_active_children(|children| children.contains(&child))
408 {
409 return false;
410 }
411 child = parent;
412 }
413 true
414}
415
416#[expect(clippy::print_stderr, reason = "CRANPOSE_SCENE_UPDATE_DIAG asks")]
417fn translate_bail(reason: &str) -> bool {
418 if cranpose_core::DIAGNOSTICS.scene_update_diag.is_set() {
419 eprintln!("[scene-update-diag] translate bail: {reason}");
420 }
421 false
422}
423
424#[derive(Clone, Copy)]
425struct TranslateAncestorContext {
426 inherited_motion_context_animated: bool,
427 update: u64,
429 inherited_translated_content_context: bool,
430 parent_content_offset: Point,
431 parent_abs: AbsOrigin,
432}
433
434#[derive(Clone, Copy, PartialEq, Eq)]
437enum Level {
438 Root,
439 Child,
440}
441
442#[derive(Clone, Copy, PartialEq, Eq)]
444enum Position {
445 Kept,
446 Moved,
447}
448
449#[derive(Clone, Copy, PartialEq, Eq)]
452enum Wrapping {
453 Wrapped,
454 Bare,
455}
456
457fn try_update_retained_layer(
458 applier: &MemoryApplier,
459 layer: &mut LayerNode,
460 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
461 kind: NodeUpdate,
462 level: Level,
463 ancestors: TranslateAncestorContext,
464) -> Option<bool> {
465 match kind {
466 NodeUpdate::Moved if level == Level::Child => {
467 move_retained_layer(applier, layer, ancestors)
468 .or_else(|| translate_tracking_hits(applier, layer, dirty_nodes, ancestors))
469 }
470 NodeUpdate::Draws if redraw_retained_layer(applier, layer, level, ancestors) => Some(false),
471 NodeUpdate::Content | NodeUpdate::Draws | NodeUpdate::Moved => {
472 translate_tracking_hits(applier, layer, dirty_nodes, ancestors)
473 }
474 NodeUpdate::Layer => {
475 update_layer_properties(applier, layer, level, Position::Kept, ancestors)
476 }
477 NodeUpdate::MovedLayer => {
478 update_layer_properties(applier, layer, level, Position::Moved, ancestors)
479 }
480 }
481}
482
483fn redraw_retained_layer(
488 applier: &MemoryApplier,
489 container: &mut LayerNode,
490 level: Level,
491 ancestors: TranslateAncestorContext,
492) -> bool {
493 if container.wraps.is_some() {
495 return false;
496 }
497 let Some(node_id) = container.node_id else {
498 return false;
499 };
500 read_placed_node_data(applier, node_id, level, |data| {
501 let placement = container.origin_in_parent;
502 let kept = Position::Kept;
503 if !retained_layer_matches_layout(container, &data, placement, Wrapping::Bare, kept) {
504 return false;
505 }
506 let Ok(plan) = translated_container(
507 container,
508 &data.layout_state,
509 &data.modifier_slices,
510 ancestors,
511 Wrapping::Bare,
512 ) else {
513 return false;
514 };
515 container.node_bounds = plan.node_bounds;
516 crate::layer_recycling::recycle_primitives(container);
517 write_own_content_around_children(
518 container,
519 node_id,
520 &data.modifier_slices,
521 data.layout_state.size(),
522 );
523 container.refresh_child_facts();
524 container.note_content_change(ancestors.update);
525 true
526 })
527 .unwrap_or(false)
528}
529
530fn move_retained_layer(
535 applier: &MemoryApplier,
536 layer: &mut LayerNode,
537 ancestors: TranslateAncestorContext,
538) -> Option<bool> {
539 let placed = placed_child_geometry(applier, layer_identity(layer)?)?;
540 if Rect::from_size(placed.size) != layer.node_rect() {
541 return None;
542 }
543 let previous = HitGraphState::of(layer);
544 translate_retained_child(layer, placed.position, ancestors.parent_content_offset);
545 if layer.has_origin_sinks {
546 publish_retained_window_geometry(applier, layer, ancestors.parent_abs);
547 }
548 Some(previous.dirty_against(&HitGraphState::of(layer)))
549}
550
551fn content_layer_mut(container: &mut LayerNode) -> Option<&mut LayerNode> {
552 let Some(node_id) = container.wraps else {
553 return Some(container);
554 };
555 container.children.iter_mut().find_map(|child| match child {
556 RenderNode::Layer(layer) if layer.node_id == Some(node_id) => Some(&mut **layer),
557 _ => None,
558 })
559}
560
561fn retained_layer_matches_layout(
564 layer: &LayerNode,
565 data: &SnapshotNodeData<'_>,
566 placement: Point,
567 wrapping: Wrapping,
568 position: Position,
569) -> bool {
570 let state = &data.layout_state;
571 let slices = &data.modifier_slices;
572 (position == Position::Moved || state.position() == placement)
573 && Rect::from_size(state.size()) == layer.node_rect()
574 && slices.layer_bounds(state.size()) == layer.local_bounds
575 && state.content_offset() == layer.content_offset
576 && (slices.outer_draw_command_count() > 0) == (wrapping == Wrapping::Wrapped)
577}
578
579fn update_layer_properties(
580 applier: &MemoryApplier,
581 container: &mut LayerNode,
582 level: Level,
583 position: Position,
584 ancestors: TranslateAncestorContext,
585) -> Option<bool> {
586 let node_id = layer_identity(container)?;
587 let wrapping = if container.wraps.is_some() {
588 Wrapping::Wrapped
589 } else {
590 Wrapping::Bare
591 };
592 let wrapped = wrapping == Wrapping::Wrapped;
593 if position == Position::Moved && wrapped {
596 return None;
597 }
598 let placement = container.origin_in_parent;
599 let hit_graph_dirty = read_placed_node_data(applier, node_id, level, |data| {
600 let target = content_layer_mut(container)?;
601 let slices = &data.modifier_slices;
602 let state = &data.layout_state;
603 if !retained_layer_matches_layout(target, &data, placement, wrapping, position) {
604 return None;
605 }
606 let context = LowerContext::beneath(ancestors);
607 let previous = HitGraphState::of(target);
608 let (head, child_context) = prepare_node_layer(node_id, slices, state, context);
609 assign_layer(target, head);
610 if wrapped {
611 target.transform_to_parent = layer_transform_to_parent(
612 target.local_bounds,
613 Point::default(),
614 &target.graphics_layer,
615 );
616 target.origin_in_parent = Point::default();
617 }
618 if target.has_origin_sinks
619 && let Some(child_abs) = child_context.parent_abs
620 {
621 for child in &target.children {
622 if let RenderNode::Layer(child) = child {
623 publish_retained_window_geometry(applier, child, child_abs);
624 }
625 }
626 }
627 Some(previous.dirty_against(&HitGraphState::of(target)))
628 })??;
629 if wrapped {
630 container.refresh_child_facts();
631 container.has_origin_sinks = children_have_origin_sinks(&container.children);
632 container.forget_own_raster_cache_hashes();
633 }
634 Some(hit_graph_dirty)
635}
636
637pub(crate) fn publish_space(applier: &MemoryApplier, graph: &RenderGraph) {
642 if !crate::thread_state::with(|state| state.space_changed.replace(false)) {
643 return;
644 }
645 publish_space_now(applier, graph);
646}
647
648#[cold]
649#[inline(never)]
650fn publish_space_now(applier: &MemoryApplier, graph: &RenderGraph) {
651 let root = ProjectiveTransform::identity();
652 crate::space::for_each_window_eye(&graph.root, root, &mut |tree, view, to_window| {
653 if let Some(eye) = &tree.cells[0].scene.eye {
654 eye.world.rebuild(|world| view.build_world(tree, world));
655 }
656 for seen in view
657 .surfaces
658 .iter()
659 .filter(|seen| seen.layer.has_origin_sinks)
660 {
661 if let Some(content) = view.content_to_window(seen, to_window) {
662 publish_content_geometry(applier, seen.layer, content);
663 }
664 }
665 });
666}
667
668fn publish_content_geometry(
672 applier: &MemoryApplier,
673 layer: &LayerNode,
674 to_window: ProjectiveTransform,
675) {
676 if let Some(node_id) = layer.node_id {
677 read_node_data(applier, node_id, |data| {
678 data.modifier_slices.publish_window_geometry(
679 Point::default(),
680 to_window,
681 data.layout_state.size(),
682 );
683 });
684 }
685 for child in &layer.children {
686 if let RenderNode::Layer(child) = child
687 && child.has_origin_sinks
688 && child.graphics_layer.space.is_none()
689 {
690 publish_content_geometry(applier, child, child.transform_to_parent.then(to_window));
691 }
692 }
693}
694
695fn publish_retained_window_geometry(
696 applier: &MemoryApplier,
697 layer: &LayerNode,
698 parent_abs: AbsOrigin,
699) {
700 if !layer.has_origin_sinks {
701 return;
702 }
703 let child_abs = parent_abs.children_of(layer);
704 if let Some(node_id) = layer.node_id {
705 read_node_data(applier, node_id, |data| {
706 data.modifier_slices.publish_window_geometry(
707 Point {
708 x: parent_abs.content_origin.x + layer.origin_in_parent.x,
709 y: parent_abs.content_origin.y + layer.origin_in_parent.y,
710 },
711 child_abs.window_transform,
712 data.layout_state.size(),
713 );
714 });
715 }
716 for child in &layer.children {
717 if let RenderNode::Layer(child) = child {
718 publish_retained_window_geometry(applier, child, child_abs);
719 }
720 }
721}
722
723#[cold]
727#[inline(never)]
728fn note_space_change() {
729 crate::thread_state::with(|state| state.space_changed.set(true));
730}
731
732fn release_for_rebuild(layer: &mut LayerNode) {
733 layer.hit_test = None;
734 crate::layer_recycling::recycle_children(layer);
735}
736
737fn forget_dirty_subtree(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
739 if let Some(node_id) = layer_identity(layer) {
740 dirty_nodes.remove(&node_id);
741 }
742 remove_dirty_descendants(layer, dirty_nodes);
743}
744
745fn forget_dirty_node(node: &RenderNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
748 match node {
749 RenderNode::Layer(layer) => forget_dirty_subtree(layer, dirty_nodes),
750 RenderNode::Inline(inline) => {
751 dirty_nodes.remove(&inline.node_id);
752 }
753 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
754 }
755}
756
757fn remove_dirty_descendants(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
758 for child in &layer.children {
759 match child {
760 RenderNode::Layer(child_layer) => {
761 if let Some(node_id) = child_layer.node_id {
762 dirty_nodes.remove(&node_id);
763 }
764 remove_dirty_descendants(child_layer, dirty_nodes);
765 }
766 RenderNode::Inline(inline) => {
767 dirty_nodes.remove(&inline.node_id);
768 }
769 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
770 }
771 }
772}
773
774fn child_identity(node: &RenderNode) -> Option<NodeId> {
777 match node {
778 RenderNode::Layer(layer) => layer_identity(layer),
779 RenderNode::Inline(inline) => Some(inline.node_id),
780 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => None,
781 }
782}
783
784#[cfg(test)]
785#[path = "tests/scene_builder_tests.rs"]
786mod tests;