vello_common/render_graph.rs
1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Render graph for multi-pass rendering with filter effects.
5//!
6//! This module provides a directed acyclic graph (DAG) representation of the rendering pipeline,
7//! enabling efficient multi-pass rendering when layers have filter effects applied.
8//!
9//! # Overview
10//!
11//! The render graph tracks dependencies between rendering operations (nodes) and ensures they
12//! execute in the correct order. Each node represents either:
13//! - The root (backdrop) layer that forms the base for compositing
14//! - A filtered layer that renders geometry, applies effects, and blends the result
15//!
16//! # Key Features
17//!
18//! - **Incremental execution order**: The graph builds its execution order as layers are popped
19//! during scene encoding, avoiding the need for a full topological sort at render time.
20//! - **Data dependencies**: Edges represent dependencies where one layer's output is consumed
21//! as input to another operation (e.g., a filter reading from a previous layer's buffer).
22//! - **Efficient traversal**: The pre-computed execution order allows O(1) iteration over nodes
23//! in dependency order, with children always processed before their parents.
24//!
25//! # Usage Pattern
26//!
27//! 1. During scene encoding, nodes are added via [`RenderGraph::add_node`] as layers are created
28//! 2. When layers with dependencies are popped, edges are added via [`RenderGraph::add_edge`]
29//! 3. As each layer completes, it's recorded in execution order via [`RenderGraph::record_node_for_execution`]
30//! 4. At render time, iterate nodes in order using [`RenderGraph::execution_order`]
31//!
32//! # Example Structure
33//!
34//! ```text
35//! RootLayer (id=0) ← backdrop
36//! │
37//! └─→ FilterLayer (id=1, blur filter)
38//! │
39//! └─→ FilterLayer (id=2, drop shadow filter)
40//!
41//! Execution order: [2, 1, 0] (deepest children first, then parents)
42//! ```
43//!
44//! In this example, `FilterLayer 2` (drop shadow) is the most deeply nested child, so it
45//! executes first. Then `FilterLayer 1` (blur) executes and can reference `FilterLayer 2`'s output
46//! if needed. Finally, the `RootLayer` composites all filtered results into the final image.
47//!
48//! # Future Improvements
49//!
50//! TODO: Consider using `render_graph` for `FilterGraph` execution for filters. The render graph
51//! infrastructure could potentially be extended to handle the internal DAG structure within
52//! individual filter effects, not just layer-to-layer dependencies.
53
54use crate::coarse::WideTilesBbox;
55use crate::filter_effects::Filter;
56use crate::kurbo::Affine;
57use alloc::vec;
58use alloc::vec::Vec;
59use hashbrown::HashMap;
60
61/// A render graph containing nodes and edges representing the rendering pipeline.
62///
63/// This graph tracks layers with filter effects and their dependencies, enabling
64/// multi-pass rendering where filter outputs can be used as inputs to subsequent operations.
65#[derive(Debug, Clone)]
66pub struct RenderGraph {
67 /// All render operations (layers) in the graph.
68 pub nodes: Vec<RenderNode>,
69 /// Dependencies between nodes, defining execution order.
70 pub edges: Vec<RenderEdge>,
71 /// Counter for generating unique node IDs.
72 next_node_id: NodeId,
73 /// Pre-computed execution order collected during `pop_layer` calls.
74 /// This is built incrementally as layers are popped (children before parents),
75 /// avoiding the need for topological sort at render time.
76 node_execution_order: Vec<NodeId>,
77 /// The root layer node ID, tracked separately since it's never popped.
78 /// This is appended to the execution order when iterating.
79 root_node: Option<NodeId>,
80 /// Flag indicating whether the graph contains any filter layers.
81 /// Set to true when a `FilterLayer` node is added.
82 has_filters: bool,
83}
84
85/// The type of dependency between nodes.
86#[derive(Debug, Clone)]
87pub enum DependencyKind {
88 /// Sequential execution: the `from` node must execute before the `to` node.
89 ///
90 /// The `to` node consumes output from the `from` node via the specified layer.
91 /// For example, a parent filter reads from a child layer buffer produced by a previous operation.
92 Sequential {
93 /// The layer that connects the two nodes, whose output flows from `from` to `to`.
94 layer_id: LayerId,
95 },
96}
97
98impl Default for RenderGraph {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl RenderGraph {
105 /// Creates a new empty render graph.
106 ///
107 /// The graph starts with no nodes or edges. Nodes are added via [`add_node`](Self::add_node)
108 /// as layers are created during scene encoding.
109 pub fn new() -> Self {
110 Self {
111 nodes: Vec::new(),
112 edges: Vec::new(),
113 next_node_id: 0,
114 node_execution_order: Vec::new(),
115 root_node: None,
116 has_filters: false,
117 }
118 }
119
120 /// Clears the render graph state while preserving allocated capacity.
121 ///
122 /// This resets the graph to an empty state (equivalent to [`new`](Self::new))
123 /// but reuses the existing memory allocations, avoiding the need to reallocate
124 /// when building a new scene.
125 ///
126 /// After calling `clear()`, the graph will have no nodes or edges, and counters
127 /// will be reset to their initial state.
128 pub fn clear(&mut self) {
129 self.nodes.clear();
130 self.edges.clear();
131 self.next_node_id = 0;
132 self.node_execution_order.clear();
133 self.root_node = None;
134 self.has_filters = false;
135 }
136
137 /// Add a node to the graph and return its ID.
138 ///
139 /// This is called during scene encoding when a new layer is created. The returned ID
140 /// should be stored with the layer to establish edges later when dependencies are known.
141 pub fn add_node(&mut self, kind: RenderNodeKind) -> NodeId {
142 let id = self.next_node_id;
143 self.next_node_id += 1;
144
145 // Track root node separately since it's never popped and executes last
146 if matches!(kind, RenderNodeKind::RootLayer { .. }) {
147 self.root_node = Some(id);
148 }
149
150 // Track if we have any filters to avoid scanning nodes later
151 if matches!(kind, RenderNodeKind::FilterLayer { .. }) {
152 self.has_filters = true;
153 }
154
155 self.nodes.push(RenderNode { id, kind });
156 id
157 }
158
159 /// Add an edge between two nodes, establishing a dependency relationship.
160 ///
161 /// This is called when a layer with dependencies is popped during scene encoding.
162 /// The edge ensures that `from` node executes before `to` node, typically because
163 /// `to` needs to read data produced by `from` (e.g., a filter reading from a layer buffer).
164 pub fn add_edge(&mut self, from: NodeId, to: NodeId, kind: DependencyKind) {
165 self.edges.push(RenderEdge { from, to, kind });
166 }
167
168 /// Record a node as ready for execution in topological order.
169 ///
170 /// This is called from `pop_layer` to incrementally build the execution order
171 /// as layers are popped. Since layers are popped in a depth-first manner (children
172 /// before parents), this naturally builds the correct dependency order without
173 /// requiring a separate topological sort at render time.
174 ///
175 /// # Example
176 ///
177 /// For nested layers A → B → C (where C is the deepest child):
178 /// - C is popped first → recorded first
179 /// - B is popped second → recorded second
180 /// - A is popped last → recorded last
181 ///
182 /// This gives execution order [C, B, A], which respects dependencies.
183 pub fn record_node_for_execution(&mut self, node_id: NodeId) {
184 self.node_execution_order.push(node_id);
185 }
186
187 /// Check if the graph contains any filter passes.
188 ///
189 /// Returns `true` if any layers have filter effects that need processing.
190 /// This is useful for determining whether to use the multi-pass rendering
191 /// pipeline (with filter support) or a simpler single-pass approach.
192 ///
193 /// This is set when filter nodes are added.
194 pub fn has_filters(&self) -> bool {
195 self.has_filters
196 }
197
198 /// Get an iterator over nodes in execution order.
199 ///
200 /// Returns filter layers followed by the root layer, in the order they should be executed.
201 /// This order is built incrementally during `pop_layer` calls (children before parents),
202 /// avoiding the need for topological sort at render time. Returns an iterator to avoid allocation.
203 pub fn execution_order(&self) -> impl Iterator<Item = NodeId> + '_ {
204 self.node_execution_order
205 .iter()
206 .copied()
207 .chain(self.root_node)
208 }
209
210 /// Topologically sort nodes based on their dependencies using Kahn's algorithm.
211 ///
212 /// Returns nodes in execution order, ensuring all dependencies are satisfied before
213 /// each node is processed. This implements a standard topological sort with cycle detection.
214 ///
215 /// # Performance
216 ///
217 /// This is primarily used for validation and debugging. For normal rendering,
218 /// prefer [`execution_order`](Self::execution_order) which is pre-computed during
219 /// scene encoding and avoids the O(V+E) sorting overhead.
220 ///
221 /// # Panics
222 ///
223 /// Panics if the graph contains a cycle, which should never happen in a valid render graph.
224 #[allow(
225 dead_code,
226 reason = "we don't use currently topological sort but will in the future"
227 )]
228 pub fn topological_sort(&self) -> Vec<NodeId> {
229 // Track how many incoming edges each node has (dependencies it's waiting for)
230 let mut in_degree = vec![0; self.nodes.len()];
231
232 // Map each node to its dependents (nodes that depend on it)
233 let mut adj_list: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
234
235 // Build adjacency list and count incoming edges for each node
236 for edge in &self.edges {
237 adj_list.entry(edge.from).or_default().push(edge.to);
238 in_degree[edge.to] += 1;
239 }
240
241 // Start with nodes that have no dependencies (in-degree = 0)
242 // These are safe to execute immediately
243 let mut queue: Vec<NodeId> = (0..self.nodes.len())
244 .filter(|&i| in_degree[i] == 0)
245 .collect();
246
247 let mut result = Vec::new();
248
249 // Process nodes in dependency order (Kahn's algorithm)
250 while let Some(node_id) = queue.pop() {
251 result.push(node_id);
252
253 // For each node that depends on this one, mark this dependency as satisfied
254 if let Some(neighbors) = adj_list.get(&node_id) {
255 for &neighbor in neighbors {
256 in_degree[neighbor] -= 1;
257 // If all dependencies are now satisfied, this node is ready to execute
258 if in_degree[neighbor] == 0 {
259 queue.push(neighbor);
260 }
261 }
262 }
263 }
264
265 // If we didn't process all nodes, there's a cycle (which should never happen)
266 assert_eq!(
267 result.len(),
268 self.nodes.len(),
269 "Cycle detected in render graph"
270 );
271 result
272 }
273}
274
275/// A node in the render graph representing a single render operation.
276#[derive(Debug, Clone)]
277pub struct RenderNode {
278 /// Unique identifier for this node.
279 pub id: NodeId,
280 /// The type of render operation this node performs.
281 pub kind: RenderNodeKind,
282}
283
284/// An edge representing a dependency between two nodes.
285#[derive(Debug, Clone)]
286pub struct RenderEdge {
287 /// The source node that must complete first.
288 pub from: NodeId,
289 /// The destination node that depends on the source.
290 pub to: NodeId,
291 /// The type of dependency relationship.
292 pub kind: DependencyKind,
293}
294
295/// The type of operation a render node performs.
296///
297/// Each variant represents a different kind of rendering pass in the pipeline.
298/// Nodes are executed in dependency order to ensure correct compositing and filtering.
299#[derive(Debug, Clone)]
300pub enum RenderNodeKind {
301 /// The root (backdrop) layer that serves as the base for compositing.
302 ///
303 /// This layer is always present and is rendered last, after all filter layers have
304 /// been processed. It contains all non-filtered geometry and serves as the final
305 /// compositing target.
306 RootLayer {
307 /// ID of the root layer.
308 layer_id: LayerId,
309 /// Bounding box in wide tile coordinates covered by this layer.
310 wtile_bbox: WideTilesBbox,
311 },
312 /// A layer with filter effects applied.
313 ///
314 /// This combines multiple passes: render geometry → apply filter → blend result.
315 /// Layers are added to the render graph only if they have filters. The filter
316 /// output may be consumed by other filter layers or composited into the root layer.
317 FilterLayer {
318 /// ID of this filtered layer.
319 layer_id: LayerId,
320 /// The filter effect to apply.
321 filter: Filter,
322 /// Bounding box in wide tile coordinates containing geometry for this layer.
323 wtile_bbox: WideTilesBbox,
324 /// Transform that was active when the layer was created.
325 /// Used to scale filter parameters based on the current scale/zoom level.
326 transform: Affine,
327 },
328}
329
330/// Unique identifier for a layer in the rendering system.
331///
332/// Layer IDs are assigned by the encoding system and used to track layer buffers
333/// across multiple render passes. A layer may or may not have a corresponding node
334/// in the render graph (only layers with filters create graph nodes).
335pub type LayerId = u32;
336
337/// Unique identifier for a node in the render graph.
338///
339/// Node IDs are assigned sequentially as nodes are added during scene encoding.
340/// They are used to reference nodes when adding edges and during execution order traversal.
341pub type NodeId = usize;
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use crate::color::AlphaColor;
347
348 #[test]
349 fn new_creates_empty_graph() {
350 let graph = RenderGraph::new();
351 assert_eq!(graph.nodes.len(), 0);
352 assert_eq!(graph.edges.len(), 0);
353 assert!(!graph.has_filters());
354 assert!(graph.root_node.is_none());
355 }
356
357 #[test]
358 fn clear_resets_to_empty_state() {
359 let mut graph = RenderGraph::new();
360 graph.add_node(RenderNodeKind::RootLayer {
361 layer_id: 0,
362 wtile_bbox: dummy_bbox(),
363 });
364 graph.add_node(RenderNodeKind::FilterLayer {
365 layer_id: 1,
366 filter: dummy_filter(),
367 wtile_bbox: dummy_bbox(),
368 transform: Affine::IDENTITY,
369 });
370 graph.record_node_for_execution(1);
371
372 graph.clear();
373
374 assert_eq!(graph.nodes.len(), 0);
375 assert_eq!(graph.edges.len(), 0);
376 assert_eq!(graph.next_node_id, 0);
377 assert!(graph.root_node.is_none());
378 assert!(!graph.has_filters());
379 assert_eq!(graph.execution_order().count(), 0);
380 }
381
382 #[test]
383 fn clear_preserves_capacity() {
384 let mut graph = RenderGraph::new();
385 let from = graph.add_node(RenderNodeKind::RootLayer {
386 layer_id: 0,
387 wtile_bbox: dummy_bbox(),
388 });
389 let to = graph.add_node(RenderNodeKind::RootLayer {
390 layer_id: 1,
391 wtile_bbox: dummy_bbox(),
392 });
393 graph.add_edge(from, to, DependencyKind::Sequential { layer_id: 1 });
394
395 let nodes_capacity = graph.nodes.capacity();
396 let edges_capacity = graph.edges.capacity();
397
398 graph.clear();
399
400 assert!(graph.nodes.capacity() >= nodes_capacity);
401 assert!(graph.edges.capacity() >= edges_capacity);
402 }
403
404 #[test]
405 fn add_node_returns_sequential_ids() {
406 let mut graph = RenderGraph::new();
407 let id0 = graph.add_node(RenderNodeKind::RootLayer {
408 layer_id: 0,
409 wtile_bbox: dummy_bbox(),
410 });
411 let id1 = graph.add_node(RenderNodeKind::FilterLayer {
412 layer_id: 1,
413 filter: dummy_filter(),
414 wtile_bbox: dummy_bbox(),
415 transform: Affine::IDENTITY,
416 });
417
418 assert_eq!(id0, 0);
419 assert_eq!(id1, 1);
420 }
421
422 #[test]
423 fn add_node_tracks_root() {
424 let mut graph = RenderGraph::new();
425 let root_id = graph.add_node(RenderNodeKind::RootLayer {
426 layer_id: 0,
427 wtile_bbox: dummy_bbox(),
428 });
429
430 assert_eq!(graph.root_node, Some(root_id));
431 }
432
433 #[test]
434 fn add_node_filter_sets_has_filters() {
435 let mut graph = RenderGraph::new();
436
437 graph.add_node(RenderNodeKind::FilterLayer {
438 layer_id: 1,
439 filter: dummy_filter(),
440 wtile_bbox: dummy_bbox(),
441 transform: Affine::IDENTITY,
442 });
443
444 assert!(graph.has_filters());
445 }
446
447 #[test]
448 fn add_node_root_does_not_set_has_filters() {
449 let mut graph = RenderGraph::new();
450
451 graph.add_node(RenderNodeKind::RootLayer {
452 layer_id: 0,
453 wtile_bbox: dummy_bbox(),
454 });
455
456 assert!(!graph.has_filters());
457 }
458
459 #[test]
460 fn add_edge_creates_dependency() {
461 let mut graph = RenderGraph::new();
462 let from = graph.add_node(RenderNodeKind::FilterLayer {
463 layer_id: 1,
464 filter: dummy_filter(),
465 wtile_bbox: dummy_bbox(),
466 transform: Affine::IDENTITY,
467 });
468 let to = graph.add_node(RenderNodeKind::RootLayer {
469 layer_id: 0,
470 wtile_bbox: dummy_bbox(),
471 });
472
473 graph.add_edge(from, to, DependencyKind::Sequential { layer_id: 1 });
474
475 assert_eq!(graph.edges.len(), 1);
476 assert_eq!(graph.edges[0].from, from);
477 assert_eq!(graph.edges[0].to, to);
478 }
479
480 #[test]
481 fn add_edge_multiple_from_same_node() {
482 let mut graph = RenderGraph::new();
483 let from = graph.add_node(RenderNodeKind::FilterLayer {
484 layer_id: 1,
485 filter: dummy_filter(),
486 wtile_bbox: dummy_bbox(),
487 transform: Affine::IDENTITY,
488 });
489 let to1 = graph.add_node(RenderNodeKind::FilterLayer {
490 layer_id: 2,
491 filter: dummy_filter(),
492 wtile_bbox: dummy_bbox(),
493 transform: Affine::IDENTITY,
494 });
495 let to2 = graph.add_node(RenderNodeKind::RootLayer {
496 layer_id: 0,
497 wtile_bbox: dummy_bbox(),
498 });
499
500 graph.add_edge(from, to1, DependencyKind::Sequential { layer_id: 1 });
501 graph.add_edge(from, to2, DependencyKind::Sequential { layer_id: 1 });
502
503 assert_eq!(graph.edges.len(), 2);
504 }
505
506 #[test]
507 fn execution_order_includes_recorded_nodes() {
508 let mut graph = RenderGraph::new();
509 let filter = graph.add_node(RenderNodeKind::FilterLayer {
510 layer_id: 1,
511 filter: dummy_filter(),
512 wtile_bbox: dummy_bbox(),
513 transform: Affine::IDENTITY,
514 });
515
516 graph.record_node_for_execution(filter);
517
518 let order: Vec<_> = graph.execution_order().collect();
519 assert_eq!(order, vec![filter]);
520 }
521
522 #[test]
523 fn execution_order_includes_only_root_when_present() {
524 let mut graph = RenderGraph::new();
525 let root = graph.add_node(RenderNodeKind::RootLayer {
526 layer_id: 0,
527 wtile_bbox: dummy_bbox(),
528 });
529
530 let order: Vec<_> = graph.execution_order().collect();
531 assert_eq!(order, vec![root]);
532 }
533
534 #[test]
535 fn execution_order_root_comes_last() {
536 let mut graph = RenderGraph::new();
537 let filter1 = graph.add_node(RenderNodeKind::FilterLayer {
538 layer_id: 1,
539 filter: dummy_filter(),
540 wtile_bbox: dummy_bbox(),
541 transform: Affine::IDENTITY,
542 });
543 let filter2 = graph.add_node(RenderNodeKind::FilterLayer {
544 layer_id: 2,
545 filter: dummy_filter(),
546 wtile_bbox: dummy_bbox(),
547 transform: Affine::IDENTITY,
548 });
549 let root = graph.add_node(RenderNodeKind::RootLayer {
550 layer_id: 0,
551 wtile_bbox: dummy_bbox(),
552 });
553
554 graph.record_node_for_execution(filter2);
555 graph.record_node_for_execution(filter1);
556
557 let order: Vec<_> = graph.execution_order().collect();
558 assert_eq!(order, vec![filter2, filter1, root]);
559 }
560
561 #[test]
562 fn topological_sort_diamond_dependency() {
563 let mut graph = RenderGraph::new();
564 let bottom = graph.add_node(RenderNodeKind::FilterLayer {
565 layer_id: 3,
566 filter: dummy_filter(),
567 wtile_bbox: dummy_bbox(),
568 transform: Affine::IDENTITY,
569 });
570 let left = graph.add_node(RenderNodeKind::FilterLayer {
571 layer_id: 1,
572 filter: dummy_filter(),
573 wtile_bbox: dummy_bbox(),
574 transform: Affine::IDENTITY,
575 });
576 let right = graph.add_node(RenderNodeKind::FilterLayer {
577 layer_id: 2,
578 filter: dummy_filter(),
579 wtile_bbox: dummy_bbox(),
580 transform: Affine::IDENTITY,
581 });
582 let top = graph.add_node(RenderNodeKind::RootLayer {
583 layer_id: 0,
584 wtile_bbox: dummy_bbox(),
585 });
586
587 // Diamond: bottom -> left -> top, bottom -> right -> top
588 graph.add_edge(bottom, left, DependencyKind::Sequential { layer_id: 3 });
589 graph.add_edge(bottom, right, DependencyKind::Sequential { layer_id: 3 });
590 graph.add_edge(left, top, DependencyKind::Sequential { layer_id: 1 });
591 graph.add_edge(right, top, DependencyKind::Sequential { layer_id: 2 });
592
593 let sorted = graph.topological_sort();
594 assert_eq!(sorted, vec![bottom, right, left, top]);
595 }
596
597 fn dummy_filter() -> Filter {
598 Filter::from_primitive(crate::filter_effects::FilterPrimitive::Flood {
599 color: AlphaColor::from_rgba8(0, 0, 0, 255),
600 })
601 }
602
603 fn dummy_bbox() -> WideTilesBbox {
604 WideTilesBbox::new([0, 0, 0, 0])
605 }
606}