Skip to content

Latest commit

 

History

History
257 lines (191 loc) · 6.86 KB

File metadata and controls

257 lines (191 loc) · 6.86 KB

vrendergraph Runtime Integration Example

This document shows how any renderer or engine can integrate the vrendergraph runtime.

The runtime layer has no dependency on ImGui or the editor.
It simply converts a JSON graph description into a FrameGraph execution pipeline.

Typical workflow:

Editor (optional) → JSON → vrendergraph runtime → FrameGraph → Renderer


1. Include vrendergraph

#include <vrendergraph/vrendergraph.hpp>

vrendergraph depends only on:

  • C++23
  • nlohmann::json
  • the FrameGraph library used internally

2. Register render passes

Each application registers the passes that its renderer supports.

#include <vrendergraph/vrendergraph.hpp>

using namespace vultra::rg;

static void registerPasses(RenderGraphRegistry& registry)
{
    registry.registerPass({
        .type = "gbuffer",
        .setup = [](FrameGraph& fg,
                    FrameGraphBlackboard& bb,
                    const ParamBlock& params)
        {
            // Add GBuffer pass to FrameGraph
        }
    });

    registry.registerPass({
        .type = "lighting",
        .setup = [](FrameGraph& fg,
                    FrameGraphBlackboard& bb,
                    const ParamBlock& params)
        {
            // Add lighting pass
        }
    });

    registry.registerPass({
        .type = "present",
        .setup = [](FrameGraph& fg,
                    FrameGraphBlackboard& bb,
                    const ParamBlock& params)
        {
            // Present to swapchain
        }
    });
}

Each setup function translates the abstract graph node into a FrameGraph pass.


3. Load a render graph

Render graphs are stored as JSON files.

#include <fstream>
#include <nlohmann/json.hpp>

RenderGraphDesc loadGraph(const std::string& path)
{
    std::ifstream file(path);

    nlohmann::json j;
    file >> j;

    return loadRenderGraph(j);
}

4. Build the runtime graph

To execute the render graph, construct the runtime object and build a FrameGraph.

RenderGraphRegistry registry;
registerPasses(registry);

RenderGraph runtime(registry, importer);

FrameGraph fg;
FrameGraphBlackboard blackboard;

RenderGraphDesc desc = loadGraph("pipeline.json");

runtime.build(fg, blackboard, desc);

At this stage the FrameGraph contains all passes required by the pipeline.


5. Execute the FrameGraph

The renderer executes the resulting FrameGraph normally.

fg.compile();
fg.execute();

Actual execution may involve:

  • command buffers
  • GPU resources
  • synchronization
  • swapchain presentation

These details are handled by the renderer implementation.


6. Example JSON pipeline

{
  "passes": [
    { "id": "gbuffer", "type": "gbuffer" },
    {
      "id": "lighting",
      "type": "lighting",
      "inputs": {
        "albedo": "gbuffer.albedo",
        "normal": "gbuffer.normal"
      }
    },
    {
      "id": "present",
      "type": "present",
      "inputs": {
        "color": "lighting.output"
      }
    }
  ]
}

The runtime only reads the passes array.
Any meta data (such as editor layout) is ignored.


6.1 Logic nodes (schema v4): one graph, many platforms

Schema v4 replaces the old per-pass when string with first-class logic nodes, so branching is expressed as explicit, visibly-wired nodes and a single graph can target every backend / platform / feature tier. Logic nodes are passes whose type is a reserved $-prefixed identifier; they are never looked up in the registry.

  • Value node $value — emits a scalar on its value pin. params.key is a predicate the host resolves (see ValueResolverFn); params.default is used when the resolver returns null.
  • Boolean combinators $and / $or / $not — value-in → value-out; nest arbitrarily, e.g. $if( $and(featureBindless, platformDesktop) ).
  • Routers $select (alias $if) and $switch — pick one of their resource inputs based on a value selector and republish it on the out pin. The unselected branch's passes are culled (not added to the FrameGraph).

The host supplies values via the resolver overload of build:

runtime.build(fg, blackboard, desc, [&](std::string_view key) -> nlohmann::json {
    if (key == "feature_bindless") return device.supportsBindless();
    if (key == "platform_desktop") return !onMobileOrWeb();
    return nullptr; // -> $value falls back to its params.default
});

Example: route sceneColor to the deferred branch when bindless is available on desktop, else the compat branch — without any when strings:

{
  "version": 4,
  "resources": [ { "name": "backbuffer" }, { "name": "sceneColor" } ],
  "passes": [
    { "id": "featBindless", "type": "$value", "params": { "key": "feature_bindless", "default": false } },
    { "id": "platDesktop",  "type": "$value", "params": { "key": "platform_desktop", "default": false } },
    { "id": "useDeferred",  "type": "$and",
      "inputs": { "a": "featBindless.value", "b": "platDesktop.value" } },

    { "id": "DeferredChain", "type": "...", "outputs": { "color": "DeferredChain.color" } },
    { "id": "CompatChain",   "type": "...", "outputs": { "color": "CompatChain.color" } },

    { "id": "sceneColorSelect", "type": "$if",
      "inputs": { "selector": "useDeferred.value",
                  "whenTrue":  "DeferredChain.color",
                  "whenFalse": "CompatChain.color" },
      "outputs": { "out": "sceneColor" } },

    { "id": "UiOverlay", "type": "...", "inputs": { "source": "sceneColor" } }
  ]
}

Build semantics: value/logic DAG is evaluated, each router's chosen input is resolved, then liveness is computed by backward reachability from roots (passes whose outputs nothing reads) — following only the chosen input at each router — and only live passes are built. Routers do no GPU work; they alias the chosen resource handle onto their out (and any mapped resource name).

Gating side-effect passes — the $enable input (schema v4.1)

Liveness keeps any pass whose output nothing consumes (a "root"), so a side-effect pass — e.g. a motion-vector pass read only outside the graph — would run on every branch. To branch-gate such a pass, wire the reserved $enable input to a value pin: when it resolves falsy the pass is culled (like enabled:false), along with anything that was only kept alive through it.

{ "id": "MotionVectors", "type": "MotionVectors",
  "inputs": { "depth": "DirectGBuffer.depth", "$enable": "useDeferred.value" } }

$enable is a value pin (not a resource input); it is skipped when gathering a pass's resources.


7. Typical runtime architecture

A typical renderer integration looks like:

Renderer
   │
   ├─ registerPasses()
   │
   ├─ load JSON render graph
   │
   ├─ vrendergraph.build()
   │
   └─ execute FrameGraph

This allows render pipelines to be fully data-driven.