# DAGs for Node-Based Compositing

## What is a DAG?

A **Directed Acyclic Graph (DAG)** is a graph where:

- **Directed** — every edge has a direction (A → B means "A feeds into B")
- **Acyclic** — no path forms a cycle; you cannot loop back to a node you've already visited

In compositing, nodes are the vertices and image/data flow defines the edges. A **Read** node feeds into a **Blur** node, which feeds into a **Merge** node, etc.

## Why DAGs for Compositing

| Property | Benefit |
|---|---|
| Non-linear | Any node can be tweaked without rebuilding the whole comp |
| Dependency-driven | Only re-evaluate nodes whose inputs changed ("dirty propagation") |
| Parallelism | Independent branches can run concurrently |
| Modular | Nodes are self-contained; easy to add new types |

### Real-world examples

- **Nuke** (Foundry) — the industry standard; everything is a DAG
- **Fusion** (Blackmagic) — same concept, node graph as the primary interface
- **Blender Compositor** — uses a dependency graph internally
- **Shadertoy / MaterialX** — similar DAG concepts for shader/node graphs

## Core Algorithms

**BFS** (Breadth-First Search) explores a graph level by level — you visit all neighbours of a node before moving to their neighbours. Useful for shortest paths and spreading outward.

**DFS** (Depth-First Search) goes deep first — you follow one path as far as it goes, then backtrack. Useful for cycle detection, pathfinding, and topological sort.

### Topological Sort

A **topological ordering** of a DAG is a linear sequence of all vertices such that for every edge u → v, u appears before v. This is the evaluation order for a node graph — you must process a node's inputs before processing the node itself.

There are two standard approaches, one based on each traversal strategy:

### 1. Kahn's Algorithm (BFS-based)

Uses **in-degree** (number of incoming edges) to determine which nodes are ready to execute.

```
1. Compute in-degree for every node
2. Queue all nodes with in-degree == 0 (no dependencies)
3. While queue is not empty:
   a. Dequeue node n, add to result order
   b. For each downstream node m of n:
      - Decrement m's in-degree
      - If m's in-degree reaches 0, enqueue m
4. If result count != node count → there is a cycle
```

**Why Kahn's for compositing:**
- Naturally detects cycles (a graph editor must prevent the user from creating cycles)
- Gives you ready-to-evaluate layers (all in-degree-0 nodes at a given step can run in parallel)
- O(V + E) time, O(V) space
- Easy to implement with arrays

### 2. DFS-based (Post-order)

```
1. For each unvisited node, run DFS
2. After visiting all descendants of a node, prepend it to the result
```

Simpler to code but less practical for incremental / parallel evaluation. Used more for DAG verification in build systems.

## Data Structures for a DAG

### Adjacency List (recommended for Prism)

Store the graph as two flat arrays:

```c
// Node i's outgoing edges are adjacency[i] .. adjacency[i + 1]
u32 *adjacency;       // flat list of edge destinations
u32 *adjacency_begin; // start index into adjacency for each node
u32  node_count;
u32  edge_count;
```

Or simpler: each node stores its outputs:

```c
typedef struct PrNode PrNode;
struct PrNode {
    u32          id;
    PrNodeType   type;
    u32          input_count;     // number of inputs (incoming edges)
    u32          input_nodes[4];  // fixed-size or pointer to array
    u32          output_count;    // number of downstream nodes
    u32         *output_nodes;    // allocated with arena
    // ... data for this node type
};
```

For a **data-oriented** approach in hot paths (graph evaluation), pack fields into parallel arrays:

```c
// SoA layout for evaluation
u8          *node_types;     // PrNodeType for each node
u32         *in_degrees;     // current in-degree (Kahn's state)
u32         *topo_order;     // result of topological sort
```

### Struct-of-Arrays (SoA) Layout

For the graph evaluation hot path, you can use parallel arrays instead of an array of structs:

```c
struct PrGraphEvalState {
    u32   node_count;
    u32  *topo_order;        // [0..node_count-1] in eval order
    u32  *in_degrees;        // temp space for Kahn's
    u8   *dirty_flags;       // per-node dirty bit
    u32  *output_counts;
    u32 **output_lists;      // adjacency
};
```

This keeps only the data needed for traversal in cache-friendly contiguous memory.

## Incremental / Dirty Evaluation

For interactive use, re-running the full topological sort every frame is wasteful. Instead:

1. When a node's parameter changes, mark it **dirty**
2. Propagate the dirty flag downstream (BFS along edges)
3. Only re-evaluate dirty nodes in topological order

Alternatively, skip dirty propagation and just always eval in topo order — each node checks if its inputs are dirty or if its own parameters changed. Simpler, but does more work.

## Cycles in a Graph Editor

A graph editor must **prevent** the user from creating cycles in real time. Approaches:

1. **Check on each edge creation:** before adding edge A → B, check if there's already a path from B to A (DFS from B). O(V + E) per edge add.
2. **Incremental cycle detection:** more sophisticated data structures for dynamic graphs. Probably overkill for V1.
3. **Kahn's validation:** Run Kahn's after every edit; if it doesn't produce a full ordering, reject the edit.

Approach 1 (DFS reachability test) is the simplest for V1.

## Evaluation Pipeline

```
User edits graph
        │
        ▼
Validate no cycles  ← (reject edit if cycle detected)
        │
        ▼
Topological sort    ← (Kahn's algorithm → list of node IDs)
        │
        ▼
Mark dirty nodes    ← (only nodes downstream of changes)
        │
        ▼
For each node in topo order:
    If node is dirty:
        Gather input images (from upstream node outputs)
        Execute node (CPU or dispatch GPU shader)
        Store output image
        │
        ▼
Render final output → display
```

## Key Takeaways for Prism

1. **Kahn's algorithm** is the right choice — simple, O(V+E), built-in cycle detection, parallel-layer grouping
2. **Adjacency list** with flat arrays for the graph structure
3. **Evaluate in topological order**; skip clean nodes for efficiency
4. **Cycle check on edge creation** via DFS from the target node
5. **SoA layout** for evaluation state if profiling shows cache misses on the hot path
6. Node types (Read, Blur, Merge, etc.) can use a `PrNodeType` enum with a function dispatch table, or a union of type-specific data in the node struct

## References

- Kahn, A. B. (1962). "Topological sorting of large networks." *Communications of the ACM*
- Cormen et al., *Introduction to Algorithms*, 3rd ed., Ch. 22.4 (Topological Sort)
- Foundry Nuke documentation: [https://learn.foundry.com/nuke](https://learn.foundry.com/nuke)
- Taskflow C++ library: [https://taskflow.github.io](https://taskflow.github.io)
