| // 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<RenderNode>, |
| /// Dependencies between nodes, defining execution order. |
| pub edges: Vec<RenderEdge>, |
| /// 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<NodeId>, |
| /// The root layer node ID, tracked separately since it's never popped. |
| /// This is appended to the execution order when iterating. |
| root_node: Option<NodeId>, |
| /// 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<Item = NodeId> + '_ { |
| 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<NodeId> { |
| // 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<NodeId, Vec<NodeId>> = 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<NodeId> = (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, |
| } |
| |
| /// 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::new([0, 0, 0, 0]) |
| } |
| } |