Skip to content

Latest commit

 

History

History
574 lines (445 loc) · 18.7 KB

File metadata and controls

574 lines (445 loc) · 18.7 KB

Render Graph System

Render Graph System

The Render Graph system in Aphrodite provides a high-level abstraction for GPU resource management and rendering pipeline construction. It automatically handles resource transitions, dependencies, and synchronization while allowing developers to focus on the logical structure of their rendering.

Architecture Overview

The system is built on a directed acyclic graph (DAG) where:

  • Nodes represent rendering operations (passes)
  • Edges represent resource dependencies between passes
graph TD
    %% Frame composer and render graphs
    FC[FrameComposer] --> RG0[RenderGraph 0]
    FC --> RG1[RenderGraph 1] 
    
    %% Passes in RenderGraph 0
    RG0 --> PA0[Pass A]
    RG0 --> PB0[Pass B]
    RG0 --> PC0[Pass C]
    
    %% Passes in RenderGraph 1
    RG1 --> PA1[Pass A]
    RG1 --> PB1[Pass B]
    
    %% Resources
    subgraph Resources["Resources"]
        direction TB
        subgraph SharedRes["Shared Resources"]
            direction LR
            SR1[Texture]
            SR2[Uniform Buffer]
        end
        
        subgraph PerFrameRes["Per-Frame Resources"]
            direction LR
            RT0[RenderTarget 0]
            RT1[RenderTarget 1]
            DB0[DepthBuffer 0]
            DB1[DepthBuffer 1]
        end
    end
    
    %% Dependencies through resources
    PA0 -- "writes" --> RT0
    PA0 -- "writes" --> DB0
    PA0 -- "reads" --> SR1
    PA0 -- "reads" --> SR2
    
    PB0 -- "reads" --> RT0
    PB0 -- "reads" --> DB0
    PB0 -- "writes" --> RT0
    
    PC0 -- "reads" --> SR1
    PC0 -- "reads" --> RT0
    
    PA1 -- "writes" --> RT1
    PA1 -- "writes" --> DB1
    PA1 -- "reads" --> SR1
    PA1 -- "reads" --> SR2
    
    PB1 -- "reads" --> RT1
    PB1 -- "reads" --> DB1
    
    %% Sequential dependencies
    PA0 -. "executes before" .-> PB0
    PB0 -. "executes before" .-> PC0
    PA1 -. "executes before" .-> PB1
    
    %% Styling
    classDef composer fill:#3b82f6,stroke:#1e3a8a,color:#fff,stroke-width:2px
    classDef renderGraph fill:#60a5fa,stroke:#1e3a8a,color:#000,stroke-width:1px
    classDef pass fill:#fcd34d,stroke:#92400e,color:#000,stroke-width:1px
    classDef sharedRes fill:#4ade80,stroke:#166534,color:#000,stroke-width:2px
    classDef frameRes fill:#a5b4fc,stroke:#3730a3,color:#000,stroke-width:1px
    classDef dependency stroke:#888,stroke-width:1px,stroke-dasharray:5 5
    
    class FC composer
    class RG0,RG1 renderGraph
    class PA0,PB0,PC0,PA1,PB1 pass
    class SR1,SR2 sharedRes
    class RT0,RT1,DB0,DB1 frameRes
Loading

Key Components

  1. FrameComposer: Manages multiple render graphs with shared resources across frames
  2. RenderGraph: Orchestrates passes and resources within a single frame
  3. RenderPass: Represents a rendering operation with specific inputs and outputs
  4. PassResource: Abstract base class for resources managed by the graph
    • PassImageResource: Image/texture resources
    • PassBufferResource: Buffer resources
  5. Builder patterns for intuitive configuration:
    • RenderPass::Builder for configuring render pass inputs/outputs

Core Features

  • Automatic resource state transitions and barriers
  • Dependency analysis and topological sorting
  • Command buffer recording and submission
  • Resource sharing between multiple frames
  • Shader loading with resource setup callbacks
  • Powerful debugging capabilities
  • Dry run mode for validation without GPU operations
  • Support for conditional pass execution

Resource Management

The render graph manages two primary resource types:

Resource TypeClassExamples
ImagesPassImageResourceTextures, render targets, depth buffers
BuffersPassBufferResourceVertex, index, uniform, and storage buffers

Resource States

Resources are automatically tracked and transitioned between states:

  • Undefined: Initial state
  • General: General purpose access
  • RenderTarget: Used as a color attachment
  • DepthStencil: Used as a depth/stencil attachment
  • ShaderResource: Used for sampling in shaders
  • ComputeGeneral: Used in compute shaders
  • Present: Ready for presentation

Resource Lifetimes

The render graph analyzes resource lifetimes to optimize memory usage:

  1. Resources are created when first needed
  2. Transient resources (only used within the graph) can be optimized
  3. Shared resources exist across multiple frames
  4. Resources are automatically cleaned up

Pass System

Render passes define operations that read and write resources:

Pass Types

Passes can be created for different queue types:

  • Graphics: Rendering operations
  • Compute: Compute shader operations
  • Transfer: Memory transfer operations

Pass Resources

Passes declare their resource usage through clear interfaces:

  • Inputs: Resources read by the pass
    • Textures
    • Uniform buffers
    • Storage buffers (read)
  • Outputs: Resources written by the pass
    • Color attachments
    • Depth/stencil attachments
    • Storage textures
    • Storage buffers (write)

Pass Groups

Passes can be organized into logical groups for better organization.

Using the Render Graph

FrameComposer and Multi-Frame Management

// Create a frame composer with frame count for buffering
aph::FrameComposerCreateInfo composerInfo{
    .pDevice = m_pDevice,
    .pResourceLoader = m_pResourceLoader,
    .frameCount = 3  // Triple buffering
};
auto* composer = aph::FrameComposer::Create(composerInfo).value();

// Iterate through frames for setup
for (auto frameResource : composer->frames())
{
    auto* graph = frameResource.pGraph;
    // Configure graph for this frame...
}

// Build all graphs with shared resources
composer->buildAllGraphs(m_pSwapChain);

// In render loop - get current frame's graph and advance
auto frameResource = composer->nextFrame();
auto* graph = frameResource.pGraph;
// Execute the current frame
graph->execute();

Creating Render Passes

// Create a simple render pass
auto* renderPass = pRenderGraph->createPass("main render", aph::QueueType::Graphics);

// Create a pass in a logical group
auto renderGroup = pRenderGraph->createPassGroup("Deferred");
auto* geometryPass = renderGroup.addPass("geometry pass", aph::QueueType::Graphics);
auto* lightingPass = renderGroup.addPass("lighting pass", aph::QueueType::Graphics);

Configuring Resources with Shared Resources

// Configure a pass using the builder pattern
drawPass->configure()
    .colorOutput("render output", {.createInfo = renderTargetColorInfo})
    .depthOutput("depth buffer", {.createInfo = renderTargetDepthInfo})
    // Shared resources across frames
    .sharedTextureInput("albedo", {.debugName = "albedo texture",
                                  .data = "texture://textures/albedo.png"})
    .sharedBufferInput("matrix ubo", {.debugName = "matrix data",
                                     .data = &m_mvp,
                                     .dataSize = sizeof(m_mvp),
                                     .createInfo = {
                                         .size = sizeof(m_mvp),
                                         .usage = aph::BufferUsage::Uniform,
                                         .domain = aph::MemoryDomain::Host,
                                     }})
    // Add shader with resource setup callback
    .shader("main_program", 
            aph::ShaderLoadInfo{.debugName = "main shader",
                              .data = {"shader://main.slang"},
                              .stageInfo = {{aph::ShaderStage::VS, "vertMain"},
                                           {aph::ShaderStage::FS, "fragMain"}}},
            [this]() {
                // Resource setup after loading but before execution
                // Set up bindless resources, etc.
            })
    .build();

Accessing Shared Resources

// Access shared resources from composer
auto* textureAsset = composer->getSharedResource<aph::vk::Image>("albedo");
auto* bufferAsset = composer->getSharedResource<aph::vk::Buffer>("matrix ubo");

// Update shared resource
auto mvpBuffer = composer->getSharedResource<aph::vk::Buffer>("matrix ubo");
m_pResourceLoader->update({.data = &m_mvp, .range = {0, sizeof(m_mvp)}}, mvpBuffer);

Recording Commands

// Record commands for a pass
renderPass->recordExecute([this](auto* pCmd) {
    // Set rendering state
    pCmd->setDepthState({
        .enable = true,
        .write = true,
        .compareOp = aph::CompareOp::Less,
    });
    
    // Add debug labels
    pCmd->beginDebugLabel({
        .name = "main rendering",
        .color = {0.5f, 0.3f, 0.2f, 1.0f},
    });
    
    // Draw with specific shader
    pCmd->setProgram(m_pProgram->getProgram());
    pCmd->draw(aph::DispatchArguments{1, 1, 1});
    
    pCmd->endDebugLabel();
});

// Push commands for a specific shader
renderPass->pushCommands("main_program", [](auto* pCmd) {
    // Shader-specific commands
    pCmd->draw(aph::DispatchArguments{1, 1, 1});
});

Conditional Execution

// Set a condition for pass execution
debugPass->setExecutionCondition([this]() {
    return m_showDebugOverlay;
});

// Explicitly cull a pass
occlusionPass->setCulled(m_disableOcclusion);

Building and Executing

// Build the graph and resolve dependencies
pRenderGraph->build(pSwapChain);

// Execute the graph
pRenderGraph->execute();

// Get a fence for CPU/GPU synchronization
vk::Fence* pFrameFence = nullptr;
pRenderGraph->execute(&pFrameFence);

Cleanup

// Clean up when done
aph::FrameComposer::Destroy(pComposer);

Advanced Features

Import External Resources

// Import an existing buffer
vk::Buffer* pExternalBuffer = /* existing buffer */;
renderPass->addBufferIn("external buffer", pExternalBuffer, aph::BufferUsage::Uniform);

// Import an existing image
vk::Image* pExternalImage = /* existing image */;
renderPass->addTextureIn("external texture", pExternalImage);

Back Buffer Management

// Set the back buffer for presentation
pRenderGraph->setBackBuffer("final color");

Debug Visualization

// Export the graph structure to GraphViz format
std::string dotGraph = pRenderGraph->exportToGraphviz();
std::ofstream outFile("render_graph.dot");
outFile << dotGraph;

Dry Run Mode

The dry run mode allows you to validate, analyze, and visualize your render graph without executing any GPU operations. This is invaluable for debugging, architecture planning, and documentation.

Creating a Dry Run Graph

// Create a graph in dry run mode for validation
auto dryRunResult = aph::RenderGraph::CreateDryRun();
APH_VERIFY_RESULT(dryRunResult);
aph::RenderGraph* pDryRunGraph = dryRunResult.value();

// Enable detailed logging of graph operations
pDryRunGraph->enableDebugOutput(true);

Use Cases for Dry Run Mode

  1. Pipeline Design: Plan and validate the structure of complex rendering pipelines
  2. Dependency Analysis: Identify and understand the dependencies between rendering passes
  3. Documentation: Generate visualizations of your rendering architecture
  4. Testing: Validate graph structure changes without GPU overhead
  5. Education: Demonstrate rendering concepts without requiring GPU access

Example: Forward Rendering with Shared Resources

void setupRenderGraph()
{
    // Set up the render graph for each frame resource
    for (auto frameResource : m_pFrameComposer->frames())
    {
        auto* graph = frameResource.pGraph;
        
        // Create descriptions for color and depth attachments
        aph::vk::ImageCreateInfo renderTargetColorInfo{
            .extent = {m_pSwapChain->getWidth(), m_pSwapChain->getHeight(), 1},
            .format = m_pSwapChain->getFormat(),
        };

        aph::vk::ImageCreateInfo renderTargetDepthInfo{
            .extent = {m_pSwapChain->getWidth(), m_pSwapChain->getHeight(), 1},
            .format = aph::Format::D32,
        };

        // Create a render pass group for main rendering
        auto renderGroup = graph->createPassGroup("MainRender");

        // Create and configure drawing pass using the builder pattern
        auto* drawPass = renderGroup.addPass("drawing cube", aph::QueueType::Graphics);
        drawPass->configure()
            .colorOutput("render output", {.createInfo = renderTargetColorInfo})
            .depthOutput("depth buffer", {.createInfo = renderTargetDepthInfo})
            .sharedTextureInput("container texture", {.debugName = "container texture",
                                                     .data = "texture://container2.png"})
            .sharedBufferInput("matrix ubo", {.debugName = "matrix data",
                                             .data = &m_mvp,
                                             .dataSize = sizeof(m_mvp)})
            .shader("bindless_mesh_program", loadInfo, [this]() {
                // Register resources with the bindless system after loading
                auto bindless = m_pDevice->getBindlessResource();
                bindless->updateResource(textureAsset->getImage(), "texture_container");
                bindless->updateResource(mvpBufferAsset->getBuffer(), "transform_cube");
            })
            .build();

        // Set the output buffer for display
        graph->setBackBuffer("render output");
    }
    
    // Build all graphs with shared resources
    m_pFrameComposer->buildAllGraphs(m_pSwapChain);
}

void buildGraph(aph::RenderGraph* pGraph)
{
    auto drawPass = pGraph->getPass("drawing cube");
    drawPass->pushCommands("bindless_mesh_program",
        [](auto* pCmd) {
            pCmd->setDepthState({.enable = true, .write = true});
            pCmd->draw(aph::DispatchArguments{1, 1, 1});
        });

    pGraph->build(m_pSwapChain);
}

void loop()
{
    for (auto frameResource : m_pEngine->loop())
    {
        // Update shared resources
        auto mvpBuffer = m_pFrameComposer->getSharedResource<aph::vk::Buffer>("matrix ubo");
        m_pResourceLoader->update({.data = &m_mvp}, mvpBuffer);

        // Build the render graph for this frame
        buildGraph(frameResource.pGraph);
    }
}

Best Practices

  1. Use FrameComposer: For multi-frame applications, use FrameComposer to manage resources across frames
  2. Share Stable Resources: Mark resources that don’t change per-frame as shared to reduce memory use
  3. Logical Organization: Group related passes together using PassGroups
  4. Resource Naming: Use clear, descriptive names for resources to aid debugging
  5. Minimize Dependencies: Design passes to minimize cross-dependencies
  6. Conditional Execution: Use setExecutionCondition for optimal performance
  7. Validation: Use dry run mode to validate graph structure before GPU execution
  8. Debug Labels: Add debug labels to command buffers for better profiling

Implementation Details

Topological Sorting

The render graph performs topological sorting to determine execution order:

  1. Analyze resource dependencies between passes
  2. Build a directed graph where edges represent dependencies
  3. Perform Kahn’s algorithm for topological sorting
  4. Detect and report cycles in the dependency graph

Resource Barriers

The system automatically inserts appropriate barriers:

  1. Track current resource states
  2. Generate barriers when state transitions are needed
  3. Combine barriers when possible for better performance

Memory Management

Resources are managed efficiently:

  1. External resources are imported without ownership
  2. Internal resources are created and owned by the graph
  3. Transient resources are optimized for better memory usage
  4. Shared resources are maintained across multiple frames

Resource Sharing Mechanism

The FrameComposer handles resource sharing with these steps:

  1. Resources are marked as shared via sharedTextureInput/sharedBufferInput methods
  2. During build, shared resources are loaded once
  3. Resource references are distributed to all per-frame render graphs
  4. Updates to shared resources affect all frames

## RenderGraphComposer

The `RenderGraphComposer` is a utility for managing multiple `RenderGraph` instances with shared resources between frames. It provides a simplified interface for multi-frame rendering and automatic resource sharing.

### Usage Example

“`cpp // Create a composer with the device and resource loader aph::RenderGraphComposerCreateInfo composerInfo{ .pDevice = m_pDevice, .pResourceLoader = m_pResourceLoader, .frameCount = 3 // Triple buffering };

auto* composer = aph::RenderGraphComposer::Create(composerInfo).value();

// Setup all graphs with shared/non-shared resources for (auto frameResource : composer->frames()) { auto* graph = frameResource.pGraph;

// Create common render passes auto* drawPass = graph->createPass(“drawPass”, aph::QueueType::Graphics);

// Configure with shared texture - this will be loaded once and shared across frames drawPass->configure() .sharedTextureInput(“albedo”, {.debugName = “albedo texture”, .data = “texture://textures/albedo.png”}) .sharedTextureInput(“normal”, {.debugName = “normal texture”, .data = “texture://textures/normal.png”}) // Per-frame resources (default behavior) .colorOutput(“render output”, {.createInfo = colorInfo}) .build();

// Set back buffer graph->setBackBuffer(“render output”); }

// Build all graphs composer->buildAllGraphs(m_pSwapChain);

// Access shared resources directly // when using sharedTextureInput/sharedBufferInput auto* albedoTexture = composer->getSharedResourceaph::vk::Image(“albedo”); if (composer->hasSharedResourceaph::vk::Image(“normal”)) { auto* normalTexture = composer->getSharedResourceaph::vk::Image(“normal”); // Use shared textures… }

// In render loop while (running) { // Get the current frame’s graph and advance to next frame uint32_t frameIndex = composer->nextFrame(); auto* graph = composer->getCurrentGraph();

// Execute the current frame graph->execute(); }

// Cleanup aph::RenderGraphComposer::Destroy(composer); “`

### Benefits

  1. **Resource Deduplication** - Automatically shares resources across frames when marked as shared
  2. **Frame Management** - Handles creation and management of per-frame RenderGraph instances
  3. **Simplified API** - Makes multi-frame rendering easier through a centralized API
  4. **Clear Ownership** - Resources are properly managed with explicit sharing semantics
  5. **Compatibility** - Works with existing RenderGraph API

## RenderGraph