// Copyright 2025 the Vello Authors // SPDX-License-Identifier: Apache-2.0 OR MIT //! Render graph for multi-pass rendering with filter effects. //! //! This module provides a directed acyclic graph (DAG) representation of the rendering pipeline, //! enabling efficient multi-pass rendering when layers have filter effects applied. //! //! # Overview //! //! The render graph tracks dependencies between rendering operations (nodes) and ensures they //! execute in the correct order. Each node represents either: //! - The root (backdrop) layer that forms the base for compositing //! - A filtered layer that renders geometry, applies effects, and blends the result //! //! # Key Features //! //! - **Incremental execution order**: The graph builds its execution order as layers are popped //! during scene encoding, avoiding the need for a full topological sort at render time. //! - **Data dependencies**: Edges represent dependencies where one layer's output is consumed //! as input to another operation (e.g., a filter reading from a previous layer's buffer). //! - **Efficient traversal**: The pre-computed execution order allows O(1) iteration over nodes //! in dependency order, with children always processed before their parents. //! //! # Usage Pattern //! //! 1. During scene encoding, nodes are added via [`RenderGraph::add_node`] as layers are created //! 2. When layers with dependencies are popped, edges are added via [`RenderGraph::add_edge`] //! 3. As each layer completes, it's recorded in execution order via [`RenderGraph::record_node_for_execution`] //! 4. At render time, iterate nodes in order using [`RenderGraph::execution_order`] //! //! # Example Structure //! //! ```text //! RootLayer (id=0) ← backdrop //! │ //! └─→ FilterLayer (id=1, blur filter) //! │ //! └─→ FilterLayer (id=2, drop shadow filter) //! //! Execution order: [2, 1, 0] (deepest children first, then parents) //! ``` //! //! In this example, `FilterLayer 2` (drop shadow) is the most deeply nested child, so it //! executes first. Then `FilterLayer 1` (blur) executes and can reference `FilterLayer 2`'s output //! if needed. Finally, the `RootLayer` composites all filtered results into the final image. //! //! # Future Improvements //! //! TODO: Consider using `render_graph` for `FilterGraph` execution for filters. The render graph //! infrastructure could potentially be extended to handle the internal DAG structure within //! individual filter effects, not just layer-to-layer dependencies. use crate::coarse::WideTilesBbox; use crate::filter_effects::Filter; use crate::kurbo::Affine; use alloc::vec; use alloc::vec::Vec; use hashbrown::HashMap; /// A render graph containing nodes and edges representing the rendering pipeline. /// /// This graph tracks layers with filter effects and their dependencies, enabling /// multi-pass rendering where filter outputs can be used as inputs to subsequent operations. #[derive(Debug, Clone)] pub struct RenderGraph { /// All render operations (layers) in the graph. pub nodes: Vec, /// Dependencies between nodes, defining execution order. pub edges: Vec, /// Counter for generating unique node IDs. next_node_id: NodeId, /// Pre-computed execution order collected during `pop_layer` calls. /// This is built incrementally as layers are popped (children before parents), /// avoiding the need for topological sort at render time. node_execution_order: Vec, /// The root layer node ID, tracked separately since it's never popped. /// This is appended to the execution order when iterating. root_node: Option, /// Flag indicating whether the graph contains any filter layers. /// Set to true when a `FilterLayer` node is added. has_filters: bool, } /// The type of dependency between nodes. #[derive(Debug, Clone)] pub enum DependencyKind { /// Sequential execution: the `from` node must execute before the `to` node. /// /// The `to` node consumes output from the `from` node via the specified layer. /// For example, a parent filter reads from a child layer buffer produced by a previous operation. Sequential { /// The layer that connects the two nodes, whose output flows from `from` to `to`. layer_id: LayerId, }, } impl Default for RenderGraph { fn default() -> Self { Self::new() } } impl RenderGraph { /// Creates a new empty render graph. /// /// The graph starts with no nodes or edges. Nodes are added via [`add_node`](Self::add_node) /// as layers are created during scene encoding. pub fn new() -> Self { Self { nodes: Vec::new(), edges: Vec::new(), next_node_id: 0, node_execution_order: Vec::new(), root_node: None, has_filters: false, } } /// Clears the render graph state while preserving allocated capacity. /// /// This resets the graph to an empty state (equivalent to [`new`](Self::new)) /// but reuses the existing memory allocations, avoiding the need to reallocate /// when building a new scene. /// /// After calling `clear()`, the graph will have no nodes or edges, and counters /// will be reset to their initial state. pub fn clear(&mut self) { self.nodes.clear(); self.edges.clear(); self.next_node_id = 0; self.node_execution_order.clear(); self.root_node = None; self.has_filters = false; } /// Add a node to the graph and return its ID. /// /// This is called during scene encoding when a new layer is created. The returned ID /// should be stored with the layer to establish edges later when dependencies are known. pub fn add_node(&mut self, kind: RenderNodeKind) -> NodeId { let id = self.next_node_id; self.next_node_id += 1; // Track root node separately since it's never popped and executes last if matches!(kind, RenderNodeKind::RootLayer { .. }) { self.root_node = Some(id); } // Track if we have any filters to avoid scanning nodes later if matches!(kind, RenderNodeKind::FilterLayer { .. }) { self.has_filters = true; } self.nodes.push(RenderNode { id, kind }); id } /// Add an edge between two nodes, establishing a dependency relationship. /// /// This is called when a layer with dependencies is popped during scene encoding. /// The edge ensures that `from` node executes before `to` node, typically because /// `to` needs to read data produced by `from` (e.g., a filter reading from a layer buffer). pub fn add_edge(&mut self, from: NodeId, to: NodeId, kind: DependencyKind) { self.edges.push(RenderEdge { from, to, kind }); } /// Record a node as ready for execution in topological order. /// /// This is called from `pop_layer` to incrementally build the execution order /// as layers are popped. Since layers are popped in a depth-first manner (children /// before parents), this naturally builds the correct dependency order without /// requiring a separate topological sort at render time. /// /// # Example /// /// For nested layers A → B → C (where C is the deepest child): /// - C is popped first → recorded first /// - B is popped second → recorded second /// - A is popped last → recorded last /// /// This gives execution order [C, B, A], which respects dependencies. pub fn record_node_for_execution(&mut self, node_id: NodeId) { self.node_execution_order.push(node_id); } /// Check if the graph contains any filter passes. /// /// Returns `true` if any layers have filter effects that need processing. /// This is useful for determining whether to use the multi-pass rendering /// pipeline (with filter support) or a simpler single-pass approach. /// /// This is set when filter nodes are added. pub fn has_filters(&self) -> bool { self.has_filters } /// Get an iterator over nodes in execution order. /// /// Returns filter layers followed by the root layer, in the order they should be executed. /// This order is built incrementally during `pop_layer` calls (children before parents), /// avoiding the need for topological sort at render time. Returns an iterator to avoid allocation. pub fn execution_order(&self) -> impl Iterator + '_ { self.node_execution_order .iter() .copied() .chain(self.root_node) } /// Topologically sort nodes based on their dependencies using Kahn's algorithm. /// /// Returns nodes in execution order, ensuring all dependencies are satisfied before /// each node is processed. This implements a standard topological sort with cycle detection. /// /// # Performance /// /// This is primarily used for validation and debugging. For normal rendering, /// prefer [`execution_order`](Self::execution_order) which is pre-computed during /// scene encoding and avoids the O(V+E) sorting overhead. /// /// # Panics /// /// Panics if the graph contains a cycle, which should never happen in a valid render graph. #[allow( dead_code, reason = "we don't use currently topological sort but will in the future" )] pub fn topological_sort(&self) -> Vec { // Track how many incoming edges each node has (dependencies it's waiting for) let mut in_degree = vec![0; self.nodes.len()]; // Map each node to its dependents (nodes that depend on it) let mut adj_list: HashMap> = HashMap::new(); // Build adjacency list and count incoming edges for each node for edge in &self.edges { adj_list.entry(edge.from).or_default().push(edge.to); in_degree[edge.to] += 1; } // Start with nodes that have no dependencies (in-degree = 0) // These are safe to execute immediately let mut queue: Vec = (0..self.nodes.len()) .filter(|&i| in_degree[i] == 0) .collect(); let mut result = Vec::new(); // Process nodes in dependency order (Kahn's algorithm) while let Some(node_id) = queue.pop() { result.push(node_id); // For each node that depends on this one, mark this dependency as satisfied if let Some(neighbors) = adj_list.get(&node_id) { for &neighbor in neighbors { in_degree[neighbor] -= 1; // If all dependencies are now satisfied, this node is ready to execute if in_degree[neighbor] == 0 { queue.push(neighbor); } } } } // If we didn't process all nodes, there's a cycle (which should never happen) assert_eq!( result.len(), self.nodes.len(), "Cycle detected in render graph" ); result } } /// A node in the render graph representing a single render operation. #[derive(Debug, Clone)] pub struct RenderNode { /// Unique identifier for this node. pub id: NodeId, /// The type of render operation this node performs. pub kind: RenderNodeKind, } impl RenderNode { /// Returns `true` if this node has an empty (zero-area) bounding box. pub fn is_empty(&self) -> bool { match &self.kind { RenderNodeKind::RootLayer { wtile_bbox, .. } => wtile_bbox.is_empty(), RenderNodeKind::FilterLayer { wtile_bbox, .. } => wtile_bbox.is_empty(), } } } /// An edge representing a dependency between two nodes. #[derive(Debug, Clone)] pub struct RenderEdge { /// The source node that must complete first. pub from: NodeId, /// The destination node that depends on the source. pub to: NodeId, /// The type of dependency relationship. pub kind: DependencyKind, } /// The type of operation a render node performs. /// /// Each variant represents a different kind of rendering pass in the pipeline. /// Nodes are executed in dependency order to ensure correct compositing and filtering. #[derive(Debug, Clone)] pub enum RenderNodeKind { /// The root (backdrop) layer that serves as the base for compositing. /// /// This layer is always present and is rendered last, after all filter layers have /// been processed. It contains all non-filtered geometry and serves as the final /// compositing target. RootLayer { /// ID of the root layer. layer_id: LayerId, /// Bounding box in wide tile coordinates covered by this layer. wtile_bbox: WideTilesBbox, }, /// A layer with filter effects applied. /// /// This combines multiple passes: render geometry → apply filter → blend result. /// Layers are added to the render graph only if they have filters. The filter /// output may be consumed by other filter layers or composited into the root layer. FilterLayer { /// ID of this filtered layer. layer_id: LayerId, /// The filter effect to apply. filter: Filter, /// Bounding box in wide tile coordinates containing geometry for this layer. wtile_bbox: WideTilesBbox, /// Transform that was active when the layer was created. /// Used to scale filter parameters based on the current scale/zoom level. transform: Affine, }, } /// Unique identifier for a layer in the rendering system. /// /// Layer IDs are assigned by the encoding system and used to track layer buffers /// across multiple render passes. A layer may or may not have a corresponding node /// in the render graph (only layers with filters create graph nodes). pub type LayerId = u32; /// Unique identifier for a node in the render graph. /// /// Node IDs are assigned sequentially as nodes are added during scene encoding. /// They are used to reference nodes when adding edges and during execution order traversal. pub type NodeId = usize; #[cfg(test)] mod tests { use super::*; use crate::color::AlphaColor; #[test] fn new_creates_empty_graph() { let graph = RenderGraph::new(); assert_eq!(graph.nodes.len(), 0); assert_eq!(graph.edges.len(), 0); assert!(!graph.has_filters()); assert!(graph.root_node.is_none()); } #[test] fn clear_resets_to_empty_state() { let mut graph = RenderGraph::new(); graph.add_node(RenderNodeKind::RootLayer { layer_id: 0, wtile_bbox: dummy_bbox(), }); graph.add_node(RenderNodeKind::FilterLayer { layer_id: 1, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); graph.record_node_for_execution(1); graph.clear(); assert_eq!(graph.nodes.len(), 0); assert_eq!(graph.edges.len(), 0); assert_eq!(graph.next_node_id, 0); assert!(graph.root_node.is_none()); assert!(!graph.has_filters()); assert_eq!(graph.execution_order().count(), 0); } #[test] fn clear_preserves_capacity() { let mut graph = RenderGraph::new(); let from = graph.add_node(RenderNodeKind::RootLayer { layer_id: 0, wtile_bbox: dummy_bbox(), }); let to = graph.add_node(RenderNodeKind::RootLayer { layer_id: 1, wtile_bbox: dummy_bbox(), }); graph.add_edge(from, to, DependencyKind::Sequential { layer_id: 1 }); let nodes_capacity = graph.nodes.capacity(); let edges_capacity = graph.edges.capacity(); graph.clear(); assert!(graph.nodes.capacity() >= nodes_capacity); assert!(graph.edges.capacity() >= edges_capacity); } #[test] fn add_node_returns_sequential_ids() { let mut graph = RenderGraph::new(); let id0 = graph.add_node(RenderNodeKind::RootLayer { layer_id: 0, wtile_bbox: dummy_bbox(), }); let id1 = graph.add_node(RenderNodeKind::FilterLayer { layer_id: 1, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); assert_eq!(id0, 0); assert_eq!(id1, 1); } #[test] fn add_node_tracks_root() { let mut graph = RenderGraph::new(); let root_id = graph.add_node(RenderNodeKind::RootLayer { layer_id: 0, wtile_bbox: dummy_bbox(), }); assert_eq!(graph.root_node, Some(root_id)); } #[test] fn add_node_filter_sets_has_filters() { let mut graph = RenderGraph::new(); graph.add_node(RenderNodeKind::FilterLayer { layer_id: 1, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); assert!(graph.has_filters()); } #[test] fn add_node_root_does_not_set_has_filters() { let mut graph = RenderGraph::new(); graph.add_node(RenderNodeKind::RootLayer { layer_id: 0, wtile_bbox: dummy_bbox(), }); assert!(!graph.has_filters()); } #[test] fn add_edge_creates_dependency() { let mut graph = RenderGraph::new(); let from = graph.add_node(RenderNodeKind::FilterLayer { layer_id: 1, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); let to = graph.add_node(RenderNodeKind::RootLayer { layer_id: 0, wtile_bbox: dummy_bbox(), }); graph.add_edge(from, to, DependencyKind::Sequential { layer_id: 1 }); assert_eq!(graph.edges.len(), 1); assert_eq!(graph.edges[0].from, from); assert_eq!(graph.edges[0].to, to); } #[test] fn add_edge_multiple_from_same_node() { let mut graph = RenderGraph::new(); let from = graph.add_node(RenderNodeKind::FilterLayer { layer_id: 1, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); let to1 = graph.add_node(RenderNodeKind::FilterLayer { layer_id: 2, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); let to2 = graph.add_node(RenderNodeKind::RootLayer { layer_id: 0, wtile_bbox: dummy_bbox(), }); graph.add_edge(from, to1, DependencyKind::Sequential { layer_id: 1 }); graph.add_edge(from, to2, DependencyKind::Sequential { layer_id: 1 }); assert_eq!(graph.edges.len(), 2); } #[test] fn execution_order_includes_recorded_nodes() { let mut graph = RenderGraph::new(); let filter = graph.add_node(RenderNodeKind::FilterLayer { layer_id: 1, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); graph.record_node_for_execution(filter); let order: Vec<_> = graph.execution_order().collect(); assert_eq!(order, vec![filter]); } #[test] fn execution_order_includes_only_root_when_present() { let mut graph = RenderGraph::new(); let root = graph.add_node(RenderNodeKind::RootLayer { layer_id: 0, wtile_bbox: dummy_bbox(), }); let order: Vec<_> = graph.execution_order().collect(); assert_eq!(order, vec![root]); } #[test] fn execution_order_root_comes_last() { let mut graph = RenderGraph::new(); let filter1 = graph.add_node(RenderNodeKind::FilterLayer { layer_id: 1, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); let filter2 = graph.add_node(RenderNodeKind::FilterLayer { layer_id: 2, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); let root = graph.add_node(RenderNodeKind::RootLayer { layer_id: 0, wtile_bbox: dummy_bbox(), }); graph.record_node_for_execution(filter2); graph.record_node_for_execution(filter1); let order: Vec<_> = graph.execution_order().collect(); assert_eq!(order, vec![filter2, filter1, root]); } #[test] fn topological_sort_diamond_dependency() { let mut graph = RenderGraph::new(); let bottom = graph.add_node(RenderNodeKind::FilterLayer { layer_id: 3, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); let left = graph.add_node(RenderNodeKind::FilterLayer { layer_id: 1, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); let right = graph.add_node(RenderNodeKind::FilterLayer { layer_id: 2, filter: dummy_filter(), wtile_bbox: dummy_bbox(), transform: Affine::IDENTITY, }); let top = graph.add_node(RenderNodeKind::RootLayer { layer_id: 0, wtile_bbox: dummy_bbox(), }); // Diamond: bottom -> left -> top, bottom -> right -> top graph.add_edge(bottom, left, DependencyKind::Sequential { layer_id: 3 }); graph.add_edge(bottom, right, DependencyKind::Sequential { layer_id: 3 }); graph.add_edge(left, top, DependencyKind::Sequential { layer_id: 1 }); graph.add_edge(right, top, DependencyKind::Sequential { layer_id: 2 }); let sorted = graph.topological_sort(); assert_eq!(sorted, vec![bottom, right, left, top]); } fn dummy_filter() -> Filter { Filter::from_primitive(crate::filter_effects::FilterPrimitive::Flood { color: AlphaColor::from_rgba8(0, 0, 0, 255), }) } fn dummy_bbox() -> WideTilesBbox { WideTilesBbox::ZERO } }