DAGs and ImGui node graphs research
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,212 @@
|
||||
# Node Graph UI with DearImGui — Research
|
||||
|
||||
## Overview
|
||||
|
||||
Three main approaches exist for building a node graph UI with DearImGui:
|
||||
1. Use a standalone library like **ImNodes** or **imgui-node-editor**
|
||||
2. Build from scratch using ImGui's draw list API
|
||||
3. Use a hybrid — custom canvas with ImGui widgets
|
||||
|
||||
---
|
||||
|
||||
## 1. Existing Libraries
|
||||
|
||||
### 1.1 imnodes (Nelarius)
|
||||
|
||||
- **Stars**: ~2.4k | **License**: MIT | **Status**: Active (last push 2024)
|
||||
- **Files**: `imnodes.h`, `imnodes_internal.h`, `imnodes.cpp` — drop-in, no deps beyond ImGui
|
||||
- **API style**: Immediate-mode, mirrors ImGui idioms
|
||||
|
||||
```cpp
|
||||
imnodes::BeginNodeEditor();
|
||||
imnodes::BeginNode(node_id);
|
||||
imnodes::BeginNodeTitleBar();
|
||||
ImGui::Text("Node Name");
|
||||
imnodes::EndNodeTitleBar();
|
||||
imnodes::BeginInputAttribute(pin_id);
|
||||
ImGui::Text("input");
|
||||
imnodes::EndAttribute();
|
||||
imnodes::BeginOutputAttribute(pin_id);
|
||||
ImGui::Text("output");
|
||||
imnodes::EndAttribute();
|
||||
imnodes::EndNode();
|
||||
imnodes::EndNodeEditor();
|
||||
```
|
||||
|
||||
**Strengths:**
|
||||
- Minimal, dependency-free, easy to vendor
|
||||
- True immediate-mode — user owns all state
|
||||
- Pins auto-align with embedded ImGui widgets
|
||||
- Simple `ImNodes::Link(id, from, to)` API
|
||||
|
||||
**Weaknesses:**
|
||||
- Less feature-rich (no built-in minimap, no grouping, limited theming)
|
||||
- No built-in serialization of layout
|
||||
- Slower development velocity
|
||||
|
||||
**Under the hood:**
|
||||
- Uses `ImDrawList::ChannelsSplit()` to layer node backgrounds behind UI
|
||||
- Pins are detected via `ImGui::BeginGroup` bounding box capture
|
||||
- Link picking uses hierarchical bezier subdivision
|
||||
|
||||
### 1.2 imgui-node-editor (thedmd / Michal Cichon)
|
||||
|
||||
- **Stars**: ~4.4k | **License**: MIT | **Status**: Active
|
||||
- **Files**: `imgui_node_editor.h/.cpp` + `imgui_canvas.h/.cpp` — also drop-in
|
||||
- **API style**: retained-state editor context, user draws content
|
||||
|
||||
```cpp
|
||||
ax::NodeEditor::Begin("Editor");
|
||||
ax::NodeEditor::BeginNode(node_id);
|
||||
ax::NodeEditor::BeginPin(pin_id, ax::NodeEditor::PinKind::Input);
|
||||
ImGui::Text("input");
|
||||
ax::NodeEditor::EndPin();
|
||||
ax::NodeEditor::EndNode();
|
||||
ax::NodeEditor::End();
|
||||
```
|
||||
|
||||
**Strengths:**
|
||||
- Rich feature set: zoom/pan, minimap, selection, context menus, copy/paste
|
||||
- Blueprint-UE4-inspired default theme
|
||||
- Bézier curve links with flow animation
|
||||
- Configurable zoom levels, drag/navigate/select button mapping
|
||||
- `ImGuiEx::Canvas` can be used independently for custom infinite-workspace UIs
|
||||
- Built-in serialization callbacks (`SaveSettings`/`LoadSettings`)
|
||||
|
||||
**Weaknesses:**
|
||||
- Heavier than imnodes — more code, larger API surface
|
||||
- Editor context is a retained object (less "pure" immediate mode)
|
||||
- Can conflict with ImGui's own ID stack during complex widget embedding
|
||||
|
||||
**Key API patterns:**
|
||||
|
||||
| Concern | API |
|
||||
|---|---|
|
||||
| Create link | `BeginCreate()` / `QueryNewLink()` / `AcceptNewItem()` / `EndCreate()` |
|
||||
| Delete | `BeginDelete()` / `QueryDeletedLink()` / `AcceptDeletedItem()` / `EndDelete()` |
|
||||
| Suspend for popups | `Suspend()` / `Resume()` — pops out of canvas coordinate space |
|
||||
| Styling | `PushStyleColor()` / `PushStyleVar()` — 20+ style variables |
|
||||
|
||||
### 1.3 ImNodeFlow (Fattorino)
|
||||
|
||||
- **Stars**: Newer (2024–2025) | **License**: MIT
|
||||
- Even more feature-packed: node categories, commenting, layout algorithms
|
||||
- Still maturing; less battle-tested than the two above
|
||||
|
||||
> **Recommendation for Prism**: start with **imgui-node-editor** if we want a polished editor quickly, or **imnodes** if we want minimal deps and full state control. The custom approach (next section) is best if we have very specific rendering needs.
|
||||
|
||||
---
|
||||
|
||||
## 2. Custom Node Graph from Scratch
|
||||
|
||||
Building a node graph manually using `ImDrawList` gives maximum control but requires handling:
|
||||
|
||||
### 2.1 Canvas / Coordinate System
|
||||
|
||||
An infinite-zoom canvas requires:
|
||||
- An offset (`ImVec2`) and scale (`float`) transform
|
||||
- Conversion functions between screen ↔ canvas space
|
||||
- Clipping to the parent ImGui window
|
||||
|
||||
The `imgui-node-editor` library includes an `ImGuiEx::Canvas` utility that handles this standalone — it can be extracted and reused.
|
||||
|
||||
### 2.2 Rendering Nodes (DrawList)
|
||||
|
||||
Nodes are typically rendered in layers:
|
||||
1. **Background layer**: grid dots/lines, selection rectangle
|
||||
2. **Node bodies**: rounded rectangles (`AddRectFilled`)
|
||||
3. **Node borders**: rect strokes, optionally thicker on hover/select
|
||||
4. **Pins**: small circles or squares on left/right edges
|
||||
5. **Links**: cubic Bézier curves between pin centers
|
||||
6. **UI overlay**: selection handles, context menus
|
||||
|
||||
Use `ImDrawList::ChannelsSplit()` for correct z-ordering when mixing drawn shapes with ImGui widgets.
|
||||
|
||||
### 2.3 Interaction Handling
|
||||
|
||||
| Interaction | Implementation |
|
||||
|---|---|
|
||||
| Pan | Track middle-mouse drag → modify canvas offset |
|
||||
| Zoom | Mouse wheel → modify scale (clamp to range, center on cursor) |
|
||||
| Drag node | Hit-test node bodies (invis buttons or rect test), track delta → update node position |
|
||||
| Select | Rectangular marquee — track shift+drag → compute selection rect → test intersection with node rects |
|
||||
| Create link | Detect drag from pin, draw preview bezier, test against other pins on release |
|
||||
| Delete | Keyboard shortcut, query selection, or context menu |
|
||||
|
||||
### 2.4 Link-Picking (Bezier Hit Test)
|
||||
|
||||
Cubic Bézier curves require a hierarchical hit test:
|
||||
1. Subdivide curve into N segments
|
||||
2. Find segment closest to mouse cursor
|
||||
3. Recursively subdivide that segment
|
||||
4. Return hit if distance < threshold
|
||||
|
||||
## 3. Architecture Patterns
|
||||
|
||||
### 3.1 Data Model vs. View Separation
|
||||
|
||||
From Guillaume Boissé's RogueEngine post:
|
||||
- Define a **data model** independent of UI: `PrNode`, `PrGraph`, `PrPin`
|
||||
- The data model is used both by the runtime (graph evaluation) and the editor (UI rendering)
|
||||
- This enables easy serialization, undo/redo, and multi-context editing
|
||||
|
||||
### 3.2 Immediate-Mode Node Rendering Loop
|
||||
|
||||
```
|
||||
For each node in graph:
|
||||
BeginNode(node.id)
|
||||
Render node title bar (colored rect + text)
|
||||
For each input pin:
|
||||
BeginInputPin(pin.id)
|
||||
Render ImGui widget (e.g. DragFloat, ColorEdit)
|
||||
EndInputPin()
|
||||
For each output pin:
|
||||
BeginOutputPin(pin.id)
|
||||
Render ImGui widget
|
||||
EndOutputPin()
|
||||
EndNode()
|
||||
|
||||
For each link in graph:
|
||||
DrawBezierLink(from_pos, to_pos, color, thickness)
|
||||
```
|
||||
|
||||
Positions are stored per-node in user state and updated on drag.
|
||||
|
||||
### 3.3 DrawList Channels (Z-Order)
|
||||
|
||||
```cpp
|
||||
draw_list->ChannelsSplit(3);
|
||||
draw_list->ChannelsSetCurrent(0); // Background: grid, selection rect
|
||||
// ... draw nodes, pins, links
|
||||
draw_list->ChannelsSetCurrent(1); // UI: ImGui widgets inside nodes
|
||||
// ... BeginNode/EndNode calls
|
||||
draw_list->ChannelsSetCurrent(2); // Foreground: tooltips, drag previews
|
||||
draw_list->ChannelsMerge();
|
||||
```
|
||||
|
||||
### 3.4 Undo/Redo
|
||||
|
||||
Simplest viable approach (from RogueEngine / @voxagonlabs): serialize the entire project state on every change. Store snapshots in an undo stack. Works well for small-to-medium projects (node graphs are typically small data).
|
||||
|
||||
## 4. Summary Comparison
|
||||
|
||||
| Criteria | imnodes | imgui-node-editor | Custom |
|
||||
|---|---|---|---|
|
||||
| Integration effort | Copy 3 files | Copy 6-8 files | Full implementation |
|
||||
| Feature depth | Basic | Rich (minimap, flow, groups, copy/paste) | Whatever you build |
|
||||
| Immediate mode | Yes | Partial (retained context) | Yes |
|
||||
| Performance | High | High | Depends on impl |
|
||||
| Styling control | Minimal | Extensive | Full control |
|
||||
| Serialization | None | Built-in callbacks | Build your own |
|
||||
| Community / maturity | Mature, stable | Mature, active | N/A |
|
||||
|
||||
## 5. References
|
||||
|
||||
- **imnodes**: https://github.com/Nelarius/imnodes
|
||||
- **imgui-node-editor**: https://github.com/thedmd/imgui-node-editor
|
||||
- **ImNodeFlow**: https://github.com/Fattorino/ImNodeFlow
|
||||
- **Blog post — Visual node graph with ImGui**: https://gboisse.github.io/posts/node-graph/
|
||||
- **Blog post — Writing imnodes**: https://nelari.us/post/imnodes
|
||||
- **ImGui issue #306** (node editor discussion): https://github.com/ocornut/imgui/issues/306
|
||||
- **ImGui useful extensions wiki**: https://github.com/ocornut/imgui/wiki/Useful-Extensions
|
||||
- **Voxagon undo/redo**: https://blog.voxagon.se/2018/07/10/undo-for-lazy-programmers.html
|
||||
Reference in New Issue
Block a user