Render Graph
The render graph is a frame graph that declaratively manages GPU work. Instead of issuing draw calls in an arbitrary order, you define discrete passes and their resource dependencies. The graph then compiles this high-level description into an optimized execution sequence, automatically handling memory aliasing and synchronization hazards.
Concepts
Resources
Resources are the inputs and outputs of the graph - any GPU-side data that a pass reads or writes:
| Type | Description |
|---|---|
Framebuffer | Render target with one or more color/depth attachments |
Texture2D / Texture3D | Sampled image resources |
StorageBuffer | GPU-writable SSBO |
LogicalBuffer | Typed CPU-side data shared between passes |
Resources can be transient (allocated per-frame by the graph) or persistent (externally owned, imported into the graph).
Passes
A pass is a RenderPass subclass that implements:
void setup(Ref<RenderTarget> target,
const std::vector<const RenderGraphResource*>& resources);
void begin(double dt);
void execute(double dt);
void end(double dt);
void cleanup();
std::string name() const;setup() is called once when the graph is compiled. begin() / execute() / end() are called every frame in order.
Building a Render Graph
Use RenderGraphBuilder to declare resources and passes, then call build():
RenderGraphBuilder builder;
// Declare transient resources
uint32_t shadow_map = builder.declare_framebuffer(
"shadow_map", 2048, 2048,
FramebufferTextureFormat::DEPTH_ONLY
);
uint32_t scene_color = builder.declare_framebuffer(
"scene_color", width, height,
FramebufferTextureFormat::RGBA16F
);
// Import the window framebuffer as persistent
uint32_t output = builder.import_persistent_framebuffer(
"output", render_target->framebuffer().get()
);
// Add passes with resource declarations
builder.add_pass(create_scope<ShadowPass>())
.write(shadow_map)
.end();
builder.add_pass(create_scope<GeometryPass>())
.read(shadow_map)
.write(scene_color)
.end();
builder.add_pass(create_scope<SkyboxPass>())
.write(scene_color)
.end();
builder.add_pass(create_scope<PostProcessPass>())
.read(scene_color)
.write(output)
.end();
// Compile
auto graph = builder.build();
graph->compile(render_target);Compilation Steps
When compile() is called the graph runs the following steps in order:
- compute_resource_lifetimes - determines
first_write_passandlast_read_passfor each resource. - infer_dependencies - adds computed dependency edges between passes that share resources.
- topological_sort - produces a valid execution order respecting both declared and inferred dependencies.
- cull_passes - marks passes whose outputs are never read (dead passes) as culled.
- alias_resources - reuses framebuffer/storage-buffer memory for non-overlapping transient resources.
- create_transient_resources - allocates GPU memory for transient framebuffers and storage buffers.
- setup_passes - calls
RenderPass::setup()on each active pass.
Exporting the Graph
The graph can be exported to Mermaid, Graphviz (DOT), or ASCII for debugging:
MermaidExporter exporter;
graph->export_graph(exporter, "render_graph.md");Resource Access Modes
When adding a pass with PassBuilder, declare how each resource is accessed:
| Method | Meaning |
|---|---|
.read(idx) | Pass only reads the resource |
.write(idx) | Pass writes (produces) the resource |
.read_write(idx) | Pass both reads and writes the resource |
These declarations drive dependency inference and lifetime computation.
Logical Buffers
A logical buffer lets passes exchange typed CPU data without going through GPU memory:
struct LightData { glm::vec3 direction; float intensity; };
uint32_t light_buf = builder.declare_logical_buffer<LightData>("light_data");
// In one pass's setup/execute:
auto* data = static_cast<LightData*>(resource->get_logical_buffer());
data->direction = glm::vec3(0, -1, 0);
// In a downstream pass:
auto* data = static_cast<LightData*>(resource->get_logical_buffer());
use(data->direction);