use std::{any::Any, rc::Rc};
use cranpose_core::{MemoryApplier, NodeId, collections::map::HashMap};
use cranpose_ui::{LayoutBox, ModifierNodeSlices, Point, Rect, Size, SubcomposeLayoutNode};
use cranpose_ui_graphics::GraphicsLayer;
use crate::{
NodeUpdate, SceneUpdates,
graph::{LayerNode, ProjectiveTransform, RenderGraph, RenderNode},
layer_transform::layer_transform_to_parent,
};
mod dirty_walk;
pub(crate) use dirty_walk::{DirtyAncestry, RetainedPath};
use dirty_walk::{update_along_retained_path, walk_dirty_layers};
mod draw_recording;
use draw_recording::{
DrawPass, DrawTarget, append_draw_nodes, bump_recording_generation, draw_nodes,
layer_node_capacity,
};
pub(crate) use draw_recording::{RecorderKey, RecorderSlot};
pub use draw_recording::{clear_command_recordings_for_tests, draw_command_nodes_for_tests};
mod hit_graph;
pub(crate) use hit_graph::HitGraphState;
use hit_graph::rebuild_tracking_hits;
mod inline_leaf;
use inline_leaf::{lower_child, update_dirty_inline};
mod layer_shape;
use layer_shape::shaped_graphics_layer;
mod text_node;
mod translate_geometry;
use text_node::{TextNodeParts, text_node_from_parts, translate_unwrapped};
use translate_geometry::{KeptChild, TranslateGeometry, move_kept_child, translate_retained_child};
mod node_layer;
use node_layer::{
AbsOrigin, LayerHead, LowerContext, PlacedGeometry, assign_layer, children_have_origin_sinks,
hit_test_from_slices, layer_and_node_bounds, layer_identity, layout_box_to_snapshot,
lower_root_into, modifier_slices_have_origin_sinks, outer_draws, placed_child_geometry,
placed_transform, prepare_node_layer, read_node_data, read_placed_node_data,
slices_hit_something, take_wrapped_layer, write_node_content, write_node_layer,
write_snapshot_layer, write_wrapper,
};
mod translate_layer;
pub(crate) use translate_layer::TranslateScratch;
use translate_layer::{
TranslatedContainer, translate_tracking_hits, translated_container,
write_own_content_around_children,
};
#[derive(Clone, Default)]
struct BuildNodeSnapshot {
node_id: NodeId,
placement: Point,
size: Size,
content_offset: Point,
slices: Rc<ModifierNodeSlices>,
graphics_layer: Option<GraphicsLayer>,
children: Vec<Self>,
}
struct SnapshotNodeData<'a> {
layout_state: cranpose_ui::widgets::LayoutState,
modifier_slices: Rc<ModifierNodeSlices>,
children: &'a [NodeId],
window_root: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GraphRebuildReason {
RootLayerUnavailable,
DirtyLayerUnavailable,
UnmatchedDirtyNodes(usize),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GraphUpdate {
Patched,
NeedsRebuild(GraphRebuildReason),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GraphUpdateReport {
pub update: GraphUpdate,
pub hit_graph_dirty: bool,
}
impl GraphUpdateReport {
pub fn applied(self) -> bool {
matches!(self.update, GraphUpdate::Patched)
}
pub fn rebuild_reason(self) -> Option<GraphRebuildReason> {
match self.update {
GraphUpdate::Patched => None,
GraphUpdate::NeedsRebuild(reason) => Some(reason),
}
}
}
fn note_layer_lowered() {
#[cfg(test)]
crate::thread_state::with(|state| {
let count = &state.lowered_layer_count;
count.set(count.get() + 1);
});
}
#[cfg(test)]
fn reset_lowered_layer_count() {
crate::thread_state::with(|state| state.lowered_layer_count.set(0));
}
#[cfg(test)]
fn lowered_layer_count() -> usize {
crate::thread_state::with(|state| state.lowered_layer_count.get())
}
pub fn build_graph_from_layout_tree(root: &LayoutBox, _scale: f32) -> RenderGraph {
bump_recording_generation();
let root_snapshot = layout_box_to_snapshot(root, None);
let mut graph = RenderGraph::new(LayerNode::default());
write_snapshot_layer(root_snapshot, LowerContext::ROOT, &mut graph.root);
graph
}
pub fn build_graph_from_applier(
applier: &MemoryApplier,
root: NodeId,
_scale: f32,
) -> Option<RenderGraph> {
bump_recording_generation();
let mut graph = RenderGraph::new(LayerNode::default());
lower_root_into(applier, root, &mut graph.root).then_some(graph)
}
pub fn rebuild_graph_from_applier(
applier: &MemoryApplier,
root: NodeId,
scale: f32,
previous: Option<RenderGraph>,
) -> Option<RenderGraph> {
let graph = match previous {
Some(mut graph) => {
release_for_rebuild(&mut graph.root);
bump_recording_generation();
graph.retained_path = None;
graph.update += 1;
graph.root.note_content_change(graph.update);
lower_root_into(applier, root, &mut graph.root).then_some(graph)
}
None => build_graph_from_applier(applier, root, scale),
};
crate::layer_recycling::release();
graph
}
pub fn update_graph_from_applier(
applier: &MemoryApplier,
graph: &mut RenderGraph,
updates: SceneUpdates<'_>,
scale: f32,
) -> bool {
update_graph_from_applier_report(applier, graph, updates, scale).applied()
}
#[expect(clippy::print_stderr, reason = "CRANPOSE_SCENE_UPDATE_DIAG asks")]
pub fn update_graph_from_applier_report(
applier: &MemoryApplier,
graph: &mut RenderGraph,
updates: SceneUpdates<'_>,
_scale: f32,
) -> GraphUpdateReport {
let report = update_graph_from_applier_report_inner(applier, graph, updates);
crate::layer_recycling::release();
if let GraphUpdate::NeedsRebuild(reason) = report.update
&& cranpose_core::DIAGNOSTICS.scene_update_diag.is_set()
{
eprintln!(
"[scene-update-diag] scoped update abandoned, whole scene rebuilt: {reason:?} dirty={}",
updates.len()
);
}
report
}
fn dirty_node_updates(updates: SceneUpdates<'_>, dirty: &mut HashMap<NodeId, NodeUpdate>) {
dirty.clear();
dirty.reserve(updates.len());
for &(id, update) in updates {
dirty
.entry(id)
.and_modify(|kind| *kind = kind.merge(update))
.or_insert(update);
}
}
#[expect(clippy::print_stderr, reason = "CRANPOSE_SCENE_UPDATE_DIAG asks")]
fn update_graph_from_applier_report_inner(
applier: &MemoryApplier,
graph: &mut RenderGraph,
updates: SceneUpdates<'_>,
) -> GraphUpdateReport {
if updates.is_empty() {
return GraphUpdateReport {
update: GraphUpdate::Patched,
hit_graph_dirty: false,
};
}
bump_recording_generation();
graph.update += 1;
let update = graph.update;
if cranpose_core::DIAGNOSTICS.scene_update_diag.is_set() {
eprintln!("[scene-update-diag] dirty={updates:?}");
}
let mut remaining_dirty_nodes =
crate::thread_state::with(|state| state.dirty_node_scratch.take()).unwrap_or_default();
dirty_node_updates(updates, &mut remaining_dirty_nodes);
let report = update_dirty_nodes(applier, graph, &mut remaining_dirty_nodes, update);
remaining_dirty_nodes.clear();
crate::thread_state::with(|state| state.dirty_node_scratch.set(Some(remaining_dirty_nodes)));
report
}
fn update_dirty_nodes(
applier: &MemoryApplier,
graph: &mut RenderGraph,
remaining_dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
update: u64,
) -> GraphUpdateReport {
if let Some(root_id) = layer_identity(&graph.root)
&& let Some(kind) = remaining_dirty_nodes.remove(&root_id)
{
if let Some(hit_graph_dirty) = try_update_retained_layer(
applier,
&mut graph.root,
remaining_dirty_nodes,
kind,
Level::Root,
TranslateAncestorContext {
inherited_motion_context_animated: false,
update,
inherited_translated_content_context: false,
parent_content_offset: Point::default(),
parent_abs: AbsOrigin::ROOT,
},
) {
if remaining_dirty_nodes.is_empty() {
return GraphUpdateReport {
update: GraphUpdate::Patched,
hit_graph_dirty,
};
}
let walked = walk_dirty_layers(applier, graph, remaining_dirty_nodes, update)
.map(|(report, _)| report);
return GraphUpdateReport {
update: classify_walk(applier, walked.is_some(), remaining_dirty_nodes),
hit_graph_dirty: hit_graph_dirty || walked.is_none_or(|r| r.hit_graph_dirty),
};
}
let Some(hit_graph_dirty) = rebuild_tracking_hits(&mut graph.root, |root| {
release_for_rebuild(root);
lower_root_into(applier, root_id, root)
}) else {
return GraphUpdateReport {
update: GraphUpdate::NeedsRebuild(GraphRebuildReason::RootLayerUnavailable),
hit_graph_dirty: true,
};
};
graph.root.note_content_change(update);
return GraphUpdateReport {
update: GraphUpdate::Patched,
hit_graph_dirty,
};
}
if let Some(path) = graph.retained_path.take()
&& let Some(report) = update_along_retained_path(
applier,
&mut graph.root,
&path,
remaining_dirty_nodes,
update,
)
{
if report.is_some() {
graph.retained_path = Some(RetainedPath { update, ..path });
}
return match report {
Some(report) => GraphUpdateReport {
update: GraphUpdate::Patched,
hit_graph_dirty: report.hit_graph_dirty,
},
None => GraphUpdateReport {
update: GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable),
hit_graph_dirty: true,
},
};
}
let Some((report, path)) = walk_dirty_layers(applier, graph, remaining_dirty_nodes, update)
else {
return GraphUpdateReport {
update: GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable),
hit_graph_dirty: true,
};
};
graph.retained_path = path;
match classify_walk(applier, true, remaining_dirty_nodes) {
GraphUpdate::Patched => GraphUpdateReport {
update: GraphUpdate::Patched,
hit_graph_dirty: report.hit_graph_dirty,
},
update => GraphUpdateReport {
update,
hit_graph_dirty: true,
},
}
}
fn classify_walk(
applier: &MemoryApplier,
walked: bool,
remaining_dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
) -> GraphUpdate {
if !walked {
return GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable);
}
remaining_dirty_nodes.retain(|&node, _| is_drawn(applier, node));
if !remaining_dirty_nodes.is_empty() {
GraphUpdate::NeedsRebuild(GraphRebuildReason::UnmatchedDirtyNodes(
remaining_dirty_nodes.len(),
))
} else {
GraphUpdate::Patched
}
}
fn is_drawn(applier: &MemoryApplier, node: NodeId) -> bool {
let mut child = node;
let mut current = node;
while let Some(parent) = applier
.get_ref(current)
.ok()
.and_then(cranpose_core::Node::parent)
{
let Ok(parent_node) = applier.get_ref(parent) else {
return true;
};
current = parent;
if parent_node.is_virtual() {
continue;
}
let parent_node: &dyn Any = parent_node;
if let Some(subcompose) = parent_node.downcast_ref::<SubcomposeLayoutNode>()
&& !subcompose.with_active_children(|children| children.contains(&child))
{
return false;
}
child = parent;
}
true
}
#[expect(clippy::print_stderr, reason = "CRANPOSE_SCENE_UPDATE_DIAG asks")]
fn translate_bail(reason: &str) -> bool {
if cranpose_core::DIAGNOSTICS.scene_update_diag.is_set() {
eprintln!("[scene-update-diag] translate bail: {reason}");
}
false
}
#[derive(Clone, Copy)]
struct TranslateAncestorContext {
inherited_motion_context_animated: bool,
update: u64,
inherited_translated_content_context: bool,
parent_content_offset: Point,
parent_abs: AbsOrigin,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Level {
Root,
Child,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Position {
Kept,
Moved,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Wrapping {
Wrapped,
Bare,
}
fn try_update_retained_layer(
applier: &MemoryApplier,
layer: &mut LayerNode,
dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
kind: NodeUpdate,
level: Level,
ancestors: TranslateAncestorContext,
) -> Option<bool> {
match kind {
NodeUpdate::Moved if level == Level::Child => {
move_retained_layer(applier, layer, ancestors)
.or_else(|| translate_tracking_hits(applier, layer, dirty_nodes, ancestors))
}
NodeUpdate::Draws if redraw_retained_layer(applier, layer, level, ancestors) => Some(false),
NodeUpdate::Content | NodeUpdate::Draws | NodeUpdate::Moved => {
translate_tracking_hits(applier, layer, dirty_nodes, ancestors)
}
NodeUpdate::Layer => {
update_layer_properties(applier, layer, level, Position::Kept, ancestors)
}
NodeUpdate::MovedLayer => {
update_layer_properties(applier, layer, level, Position::Moved, ancestors)
}
}
}
fn redraw_retained_layer(
applier: &MemoryApplier,
container: &mut LayerNode,
level: Level,
ancestors: TranslateAncestorContext,
) -> bool {
if container.wraps.is_some() {
return false;
}
let Some(node_id) = container.node_id else {
return false;
};
read_placed_node_data(applier, node_id, level, |data| {
let placement = container.origin_in_parent;
let kept = Position::Kept;
if !retained_layer_matches_layout(container, &data, placement, Wrapping::Bare, kept) {
return false;
}
let Ok(plan) = translated_container(
container,
&data.layout_state,
&data.modifier_slices,
ancestors,
Wrapping::Bare,
) else {
return false;
};
container.node_bounds = plan.node_bounds;
crate::layer_recycling::recycle_primitives(container);
write_own_content_around_children(
container,
node_id,
&data.modifier_slices,
data.layout_state.size(),
);
container.refresh_child_facts();
container.note_content_change(ancestors.update);
true
})
.unwrap_or(false)
}
fn move_retained_layer(
applier: &MemoryApplier,
layer: &mut LayerNode,
ancestors: TranslateAncestorContext,
) -> Option<bool> {
let placed = placed_child_geometry(applier, layer_identity(layer)?)?;
if Rect::from_size(placed.size) != layer.node_rect() {
return None;
}
let previous = HitGraphState::of(layer);
translate_retained_child(layer, placed.position, ancestors.parent_content_offset);
if layer.has_origin_sinks {
publish_retained_window_geometry(applier, layer, ancestors.parent_abs);
}
Some(previous.dirty_against(&HitGraphState::of(layer)))
}
fn content_layer_mut(container: &mut LayerNode) -> Option<&mut LayerNode> {
let Some(node_id) = container.wraps else {
return Some(container);
};
container.children.iter_mut().find_map(|child| match child {
RenderNode::Layer(layer) if layer.node_id == Some(node_id) => Some(&mut **layer),
_ => None,
})
}
fn retained_layer_matches_layout(
layer: &LayerNode,
data: &SnapshotNodeData<'_>,
placement: Point,
wrapping: Wrapping,
position: Position,
) -> bool {
let state = &data.layout_state;
let slices = &data.modifier_slices;
(position == Position::Moved || state.position() == placement)
&& Rect::from_size(state.size()) == layer.node_rect()
&& slices.layer_bounds(state.size()) == layer.local_bounds
&& state.content_offset() == layer.content_offset
&& (slices.outer_draw_command_count() > 0) == (wrapping == Wrapping::Wrapped)
}
fn update_layer_properties(
applier: &MemoryApplier,
container: &mut LayerNode,
level: Level,
position: Position,
ancestors: TranslateAncestorContext,
) -> Option<bool> {
let node_id = layer_identity(container)?;
let wrapping = if container.wraps.is_some() {
Wrapping::Wrapped
} else {
Wrapping::Bare
};
let wrapped = wrapping == Wrapping::Wrapped;
if position == Position::Moved && wrapped {
return None;
}
let placement = container.origin_in_parent;
let hit_graph_dirty = read_placed_node_data(applier, node_id, level, |data| {
let target = content_layer_mut(container)?;
let slices = &data.modifier_slices;
let state = &data.layout_state;
if !retained_layer_matches_layout(target, &data, placement, wrapping, position) {
return None;
}
let context = LowerContext::beneath(ancestors);
let previous = HitGraphState::of(target);
let (head, child_context) = prepare_node_layer(node_id, slices, state, context);
assign_layer(target, head);
if wrapped {
target.transform_to_parent = layer_transform_to_parent(
target.local_bounds,
Point::default(),
&target.graphics_layer,
);
target.origin_in_parent = Point::default();
}
if target.has_origin_sinks
&& let Some(child_abs) = child_context.parent_abs
{
for child in &target.children {
if let RenderNode::Layer(child) = child {
publish_retained_window_geometry(applier, child, child_abs);
}
}
}
Some(previous.dirty_against(&HitGraphState::of(target)))
})??;
if wrapped {
container.refresh_child_facts();
container.has_origin_sinks = children_have_origin_sinks(&container.children);
container.forget_own_raster_cache_hashes();
}
Some(hit_graph_dirty)
}
pub(crate) fn publish_space(applier: &MemoryApplier, graph: &RenderGraph) {
if !crate::thread_state::with(|state| state.space_changed.replace(false)) {
return;
}
publish_space_now(applier, graph);
}
#[cold]
#[inline(never)]
fn publish_space_now(applier: &MemoryApplier, graph: &RenderGraph) {
let root = ProjectiveTransform::identity();
crate::space::for_each_window_eye(&graph.root, root, &mut |tree, view, to_window| {
if let Some(eye) = &tree.cells[0].scene.eye {
eye.world.rebuild(|world| view.build_world(tree, world));
}
for seen in view
.surfaces
.iter()
.filter(|seen| seen.layer.has_origin_sinks)
{
if let Some(content) = view.content_to_window(seen, to_window) {
publish_content_geometry(applier, seen.layer, content);
}
}
});
}
fn publish_content_geometry(
applier: &MemoryApplier,
layer: &LayerNode,
to_window: ProjectiveTransform,
) {
if let Some(node_id) = layer.node_id {
read_node_data(applier, node_id, |data| {
data.modifier_slices.publish_window_geometry(
Point::default(),
to_window,
data.layout_state.size(),
);
});
}
for child in &layer.children {
if let RenderNode::Layer(child) = child
&& child.has_origin_sinks
&& child.graphics_layer.space.is_none()
{
publish_content_geometry(applier, child, child.transform_to_parent.then(to_window));
}
}
}
fn publish_retained_window_geometry(
applier: &MemoryApplier,
layer: &LayerNode,
parent_abs: AbsOrigin,
) {
if !layer.has_origin_sinks {
return;
}
let child_abs = parent_abs.children_of(layer);
if let Some(node_id) = layer.node_id {
read_node_data(applier, node_id, |data| {
data.modifier_slices.publish_window_geometry(
Point {
x: parent_abs.content_origin.x + layer.origin_in_parent.x,
y: parent_abs.content_origin.y + layer.origin_in_parent.y,
},
child_abs.window_transform,
data.layout_state.size(),
);
});
}
for child in &layer.children {
if let RenderNode::Layer(child) = child {
publish_retained_window_geometry(applier, child, child_abs);
}
}
}
#[cold]
#[inline(never)]
fn note_space_change() {
crate::thread_state::with(|state| state.space_changed.set(true));
}
fn release_for_rebuild(layer: &mut LayerNode) {
layer.hit_test = None;
crate::layer_recycling::recycle_children(layer);
}
fn forget_dirty_subtree(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
if let Some(node_id) = layer_identity(layer) {
dirty_nodes.remove(&node_id);
}
remove_dirty_descendants(layer, dirty_nodes);
}
fn forget_dirty_node(node: &RenderNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
match node {
RenderNode::Layer(layer) => forget_dirty_subtree(layer, dirty_nodes),
RenderNode::Inline(inline) => {
dirty_nodes.remove(&inline.node_id);
}
RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
}
}
fn remove_dirty_descendants(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
for child in &layer.children {
match child {
RenderNode::Layer(child_layer) => {
if let Some(node_id) = child_layer.node_id {
dirty_nodes.remove(&node_id);
}
remove_dirty_descendants(child_layer, dirty_nodes);
}
RenderNode::Inline(inline) => {
dirty_nodes.remove(&inline.node_id);
}
RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
}
}
}
fn child_identity(node: &RenderNode) -> Option<NodeId> {
match node {
RenderNode::Layer(layer) => layer_identity(layer),
RenderNode::Inline(inline) => Some(inline.node_id),
RenderNode::Primitive(_) | RenderNode::DrawRun(_) => None,
}
}
#[cfg(test)]
#[path = "tests/scene_builder_tests.rs"]
mod tests;