TurboGraph is a GPU-accelerated Proof-Of-Concept node graph viewer built for AI model architectures at extreme scale, with interactive pan, zoom, and fluid physics that animate subgraphs as they expand and collapse. Graph size is bounded only by available RAM rather than any fixed limit — tested at 500,000+ nodes and 2.5 million edges on a 32 GB RAM, 10 GB VRAM desktop, and it sizes itself automatically to whatever memory the machine has.
- Built on Dear ImGui, SDL2 and OpenGL.
- Not tested on Linux
- Forked from Intel GVK as plans exist to integrate this library with Pipeline Explorer once ported to Vulkan.
- Originally ImGui node code based off of ChemistAion/nodes.cpp.
Demo: https://www.youtube.com/watch?v=rYEJ_euHGYw
For a version without fluid-sim subgraph animations with less overall vibe coding, see the branch noSubgraphAnimation
The world is divided into a sparse grid of 8192×8192 tiles, each an offscreen framebuffer. Nodes are baked into a tile once, then the tile is composited to screen every frame with a camera transform. Two consequences matter:
- Pan and zoom cost nothing to re-render. Camera changes don't dirty tiles, so navigation is just texture blits of already-baked content.
- There is no maximum world size. Nothing is ever allocated at world resolution, so the usual
GL_MAX_TEXTURE_SIZEceiling (typically 16384) doesn't apply.
Tiles are allocated lazily (only where nodes exist), and Least Recently Used tiles are evicted when over budget with currently visible tiles protected from eviction. Each tile stores RGB565 (2 bytes/pixel), so one tile costs 128 MB.
Nodes, edges, text (SDF), and port circles each have their own draw pipeline, with compute shaders building edge geometry. Text uses a pre-generated signed-distance-field atlas so labels stay crisp at any zoom. A CPU fallback path exists for machines without the required compute shader support.
Expand and collapse are animated by an SPH (smoothed-particle hydrodynamics) simulation running in compute shaders: children flow outward from their parent, neighbors are displaced and spring back. The graph settles organically instead of snapping between layouts.
Available RAM is normally the binding limiter. So RAM usage is predicted, budgeted, and enforced to avoid getting too close to the RAM limits at any moment. The enforced ceiling is the minimum of Physical RAM and Commit (pagefile) limits.
If a layout needs more tiles than fit, it degrades gracefully: navigation still works and un-baked tiles simply don't display. It does not crash or thrash.
TurboGraph would be more efficient than other model graph visualizers when ran on a high VRAM/RAM machine with:
- Very large models (ex. training graphs, 100k+ nodes) whose hierarchy data is unrecoverable/nonexistent
- Graphs with high edge density, or otherwise complex graphs
Google's Model Explorer frontend is a great library that works as an excellent comparison. It redraws all geometry every frame using a geometric instancing optimization.
TurboGraph is limited by memory (capacity, then bandwidth), whereas Model Explorer is limited by GPU compute. If benchmarked, as node/edge count increases then whichever resource is scarcer on a machine slows down first. If you add enough of both (memory + fast compute) they become indistinguishable.
Clone with submodules — Dear ImGui is a submodule, and the build patches its imconfig.h, so it must
be present before configuring:
git clone --recursive https://github.com/eversolea/turboGraph.gitAlready cloned without --recursive? Run:
git submodule update --init --recursiveConfigure and build (Windows / Visual Studio 2022):
cmake -G "Visual Studio 17 2022" -A x64 -B build
cmake --build build --config ReleaseRun:
./build/Release/turboGraph.exe| Option | Description |
|---|---|
--data <name> |
Dataset: test, large, wide, longedge, grid (default large) |
--nodes N |
Node count for --data grid. Omit to auto-size to available memory |
--gpu on|off |
GPU rendering (default on) |
--curve 0..3 |
Edge shape: 0 = bezier direct, 1 = polyline, 2 = Catmull-Rom, 3 = bezier chain |
--commit-overrun GB |
Let the tile budget exceed free RAM by GB, backed by the pagefile (default 0) |
--debug |
Verbose memory/profiling output |
--help |
Show usage |
Start here to see it working at scale — this auto-sizes the graph to your machine's avaliable RAM.
./build/Release/turboGraph.exe --data gridBy default the tile cache is sized to fit in physical RAM, so no tile ever needs to be paged out.
--commit-overrun trades that guarantee for a higher tile ceiling, which keeps more of a large graph
rendered instead of leaving distant tiles blank.
Warning: this can hang your whole machine for some time, not just this app. Be careful using it with 500k+ nodes. The extra tiles this enables are pagefile-backed, so Windows has to grow the pagefile mid-render — a synchronous disk operation that saturates the disk and > leaves the entire system unresponsive, mouse included, for as long as it takes. Program recovery is not guaranteed: When commit-overrun is set at 4 on a high-RAM machine with a large graph (600k+ nodes) the usual outcome is a hang followed by the app being killed.
./build/Release/turboGraph.exe --data grid --commit-overrun 4Controls: right-drag to pan, mouse wheel to zoom, left-drag to move nodes, shift left-click to expand/collapse nodes
Your graph lives in GraphData::DataModel (DataModel.h) — pure structure, no positions and no rendering
state. You build a model, hand it to a layout engine, and point the view at both. Positions are computed
for you.
A node needs an id, a name, and its input/output ports. Ports are what edges attach to, by index.
#include "DataModel.h"
using namespace GraphData;
DataModel* model = new DataModel();
Node conv(0, "Conv2D"); // id, display name
conv.inputs.push_back(Port("input", PortType::IMAGE));
conv.outputs.push_back(Port("output", PortType::IMAGE));
model->AddNode(conv);
Node relu(1, "ReLU");
relu.inputs.push_back(Port("input", PortType::IMAGE));
relu.outputs.push_back(Port("output", PortType::IMAGE));
model->AddNode(relu);
// Conv2D output[0] -> ReLU input[0]
model->AddConnection(Connection(0, 0, 1, 0));Pass id = -1 (or use the Node() default) to have AddNode() assign the next free id; it returns the
id it used. Port types are INT, FLOAT, BOOL, STRING, VECTOR, IMAGE, TEXT, GENERIC and
affect port appearance.
This is what the expand/collapse physics animates. Mark a parent expandable, list its children, and set
each child's parentId:
Node block(2, "ResidualBlock");
block.isExpandable = true;
block.childNodeIds = { 0, 1 }; // Conv2D and ReLU live inside
model->AddNode(block);
model->GetNodeMutable(0)->parentId = 2;
model->GetNodeMutable(1)->parentId = 2;To forward a child's port to the parent's boundary, use parentInputPortMap / parentOutputPortMap:
entry i gives the parent port index that child port i represents, or -1 for internal-only ports.
Collapsed parents then keep their external edges connected.
LayoutEngine* layout = new LayoutEngine();
layout->SetGridParameters(150.0f, 50.0f, 50.0f, 50.0f); // nodeW, nodeH, hSpacing, vSpacing
layout->ComputeLayoutLayered(*model); // hierarchical; follows edge direction, reduces crossings
layout->SetEdgeRenderMode(2); // Catmull-Rom
graphView.SetDataModel(model);
graphView.SetLayoutengine(layout);Use ComputeLayoutLayered() for real model topology. The alternative, ComputeGridLayout() (paired with
SetGridNodesPerRow()), exists for the synthetic stress-test grids and ignores edge direction.
For the pattern in full — layout, world bounds, tile budget, camera fit — see the node setup block in
main.cpp, which does exactly the above for the built-in datasets.
When your data changes, dirty only the affected region and let the event loop pick it up:
ImRect changed = /* world-space bounds of what moved */;
tileCache.MarkDirty(changed);
nodesDirty = true;Call layout->ToggleExpand(nodeId) (or ExpandNode / CollapseNode) to drive hierarchy changes; the
physics simulation animates the transition. MarkAllDirty() re-bakes everything, which is expensive on a
large graph — prefer a region.
Biggest one is graphs with ~1,000,000 nodes stalls the program with no observable bottlenecks (on 64GB RAM, RTX 3060). Some kind of limit seems to have been reached. Another bug listed in todo.txt. Undiscovered bugs likely exists as this is a Proof-of-Concept.
main.cpp Entry point: memory budgeting, tile bake loop, compositing, camera, event loop
TileCache.h/cpp Tile allocation, LRU eviction, visibility
DataModel.h/cpp Graph structure: nodes, ports, connections, hierarchy
LayoutEngine.h/cpp Layered + grid layout, expand/collapse state, world bounds
PhysicsLayoutSimulation SPH fluid simulation for subgraph animation (compute shaders)
GPURenderer.h/cpp Draw pipelines: nodes, edges, text, circles, particles
BMFont.h/cpp SDF font atlas loading
nodes.h/cpp Node drawing + culling (pure logic, no GL)
shaders/ GLSL: render + compute
imgui/ Dear ImGui (submodule; ImDrawIdx patched to 32-bit for >65k vertices)
sdl2/ SDL2 (Windows x64)
OpenGL use is concentrated in GPURenderer.cpp (draw pipelines and compute passes),
PhysicsLayoutSimulation.cpp (SPH compute + SSBOs), TileCache.cpp (FBO/texture allocation) and
main.cpp (context, per-tile FBO binds, frame loop). nodes.cpp, LayoutEngine and DataModel contain
no GL calls.