Skip to content

Latest commit

 

History

History
360 lines (282 loc) · 11.5 KB

File metadata and controls

360 lines (282 loc) · 11.5 KB

Bind Group System

Overview

The bind group system provides automatic bind group management with name-based resolution and smart state tracking. All bind groups are resolved by shader-defined names, not fixed indices.

Key Features:

  • Name-based resolution: Bind groups found by name, not index
  • Automatic state tracking: Only rebinds when state changes (camera, object, material)
  • Reuse policies: Global, PerFrame, PerObject, PerMaterial
  • Unified handling: Custom and built-in bind groups use the same logic

Architecture

Components

  • BindGroupBinder: Centralized binding with automatic state tracking
  • FrameCache: Frame-wide bind group cache (frame, object, custom)
  • WebGPUShaderInfo: Stores bind group name → index mappings
  • BindGroupDataProvider: Custom uniform data provider for scene nodes

Flow

1. Scene collects BindGroupDataProviders from nodes
2. FrameCache processes providers → creates/caches bind groups
3. RenderPass calls binder.bind(renderPass, pipeline, cameraId, bindGroups, objectId, materialId)
4. Binder:
   - Iterates shader's bind groups (by name)
   - Resolves index via shaderInfo->getBindGroupIndex(name)
   - Fetches from cache/parameter based on type and reuse policy
   - Only binds if state changed or not yet bound
5. GPU rendering

Bind Group Index Resolution

How It Works

  1. Shader defines bind groups with @group(N) annotations
  2. ShaderFactory parses bind group names and types from shader source
  3. WebGPUShaderInfo stores name-to-index mapping (m_nameToIndex)
  4. BindGroupBinder queries index via shaderInfo->getBindGroupIndex(layoutInfo->getName())
  5. setBindGroup() called with resolved index

Example Flow:

// Shader: @group(2) @binding(0) var<uniform> u_object: ObjectUniforms;
// ShaderInfo maps: "ObjectUniforms" -> 2

auto indexOpt = shaderInfo->getBindGroupIndex("ObjectUniforms");
// Returns: 2
renderPass.setBindGroup(2, objectBindGroup, 0, nullptr);

Index Requirements

  • Group 0: Reserved for FrameUniforms (if used by shader)
  • All other groups: Defined by the shader @group(N) annotations
  • Custom bind groups: Can use any available index (typically 5+)

Bind Group Types

Built-in Bind Groups

Type Reuse Policy Source Description
Frame PerFrame Cache (cameraId) Camera view/projection
Light PerFrame Parameter Scene lights
Object PerObject Parameter or cache (objectId) Model matrices
Material PerObject Parameter Material properties/textures
Shadow PerFrame Parameter Shadow maps
Debug Global Parameter Debug primitives

Custom Bind Groups

User-defined via BindGroupDataProvider:

  • Global: Never changes (app settings)
  • PerFrame: Changes per camera (time, frame data)
  • PerObject: Unique per object (animation state)
  • PerMaterial: Unique per material (variants)

All bind groups (built-in and custom) use the same unified logic based on reuse policy.

Usage

Creating Custom Bind Groups

// In your custom RenderNode class
class MyCustomNode : public UpdateNode
{
public:
    void preRender(std::vector<BindGroupDataProvider>& outProviders) override
    {
        // Define your uniform struct
        struct MyUniforms {
            float time;
            glm::vec3 color;
            float intensity;
        };

        MyUniforms uniforms{
            currentTime,
            myColor,
            intensity
        };

        // Shared bind group (Global/PerFrame)
        outProviders.push_back(BindGroupDataProvider::create(
            "MyShader",              // Shader name
            "MyCustomData",          // Bind group name in shader
            uniforms,                // Uniform data
            BindGroupReuse::PerFrame // Reuse policy
        ));

        // Per-object bind group (PerObject/PerMaterial)
        uint64_t objectId = reinterpret_cast<uint64_t>(this);
        outProviders.push_back(BindGroupDataProvider::create(
            "MyShader",
            "PerObjectData",
            uniforms,
            BindGroupReuse::PerObject,
            objectId                 // Instance ID for per-object caching
        ));
    }
};

Shader Definition

// Frame uniforms MUST be at group 0 if used
@group(0) @binding(0) var<uniform> u_frame: FrameUniforms;

// Other engine bind groups (indices resolved by name)
@group(1) @binding(0) var<storage, read> u_lights: LightsBuffer;
@group(2) @binding(0) var<uniform> u_object: ObjectUniforms;

// Custom bind groups can use any available index (typically 5+)
// Index doesn't matter - resolved by name "MyCustomData"
@group(5) @binding(0) var<uniform> myCustomData: MyUniforms;

// Another custom bind group - index doesn't matter
// Resolved by name "PerObjectData"
@group(6) @binding(0) var<uniform> perObjectData: MyUniforms;

struct MyUniforms {
    time: f32,
    color: vec3<f32>,
    intensity: f32,
}

Key Points:

  • Group indices in shader can be any value (except 0 is reserved for Frame)
  • BindGroupBinder uses name to find the correct index: shaderInfo->getBindGroupIndex("MyCustomData")
  • You could use @group(10) or @group(99) - the system resolves it dynamically
  • Bind group names must be unique within a shader

Shader Registration

// In ShaderRegistry or shader setup code
auto shader = shaderBuilder
    .setShaderPath("path/to/shader.wgsl")
    .addCustomBindGroup(
        "MyCustomData",           // Bind group name (must match shader)
        BindGroupReuse::PerFrame, // Reuse policy
        {
            // Define bindings
            {0, "myUniforms", BindingType::UniformBuffer, sizeof(MyUniforms)}
        }
    )
    .addCustomBindGroup(
        "PerObjectData",           // Bind group name (must match shader)
        BindGroupReuse::PerObject,
        {
            {0, "perObjectUniforms", BindingType::UniformBuffer, sizeof(MyUniforms)}
        }
    )
    .build();

// The system will:
// 1. Parse shader to find @group(N) indices
// 2. Store mapping: "MyCustomData" -> 5, "PerObjectData" -> 6
// 3. Use these mappings during rendering

Automatic State Tracking

How It Works

// First object - all groups bound
binder.bind(renderPass, pipeline, cameraId, bindGroups, objectId1, materialId1);

// Second object - only Object group rebound (same camera, different object)
binder.bind(renderPass, pipeline, cameraId, bindGroups, objectId2, materialId1);

// Third object - Object + Material rebound
binder.bind(renderPass, pipeline, cameraId, bindGroups, objectId2, materialId2);

// New render pass - all groups rebound automatically 
auto newPass = encoder.beginRenderPass(&desc);
binder.bind(newPass, shaderInfo, cameraId2, bindGroups, objectId2);  // All rebound

How Tracking Works:

// BindGroupBinder::bindGroupAtIndex()
auto it = m_boundBindGroups.find(groupIndex);
if (it != m_boundBindGroups.end() && it->second == bindGroup.get()) {
    // Already bound at this index, skip
    return true;
}
renderPass.setBindGroup(groupIndex, bindGroup->getBindGroup(), 0, nullptr);
m_boundBindGroups[groupIndex] = bindGroup.get();  // Track

Reuse Policy Selection

Choose the appropriate reuse policy based on your data:

Use Case Policy Instance ID
Global settings, time Global None
Per-frame data (same for all objects) PerFrame None
Animation state, instance data PerObject Object ID
Material variant properties PerMaterial Material ID

Best Practices

1. Minimize Per-Object Bind Groups

Per-object bind groups create separate GPU resources for each instance. Prefer:

  • Good: One PerFrame bind group shared by 1000 objects
  • Avoid: 1000 PerObject bind groups with identical data

2. Group Related Data

Combine related uniforms into a single bind group:

// Good - single bind group
struct AnimationData {
    float animationTime;
    glm::mat4 boneMatrices[64];
    float blendWeights[4];
};

// Avoid - multiple small bind groups
// (overhead of multiple bind group bindings)

3. Update Only When Changed

processBindGroupProviders() only uploads data when it differs:

// In preRender() - only provide data if it changed
if (animationStateChanged) {
    outProviders.push_back(BindGroupDataProvider::create(...));
}

4. Understand Name-Based Resolution

The system uses bind group names, not fixed indices:

// WRONG - Assuming fixed indices
renderPass.setBindGroup(0, frameBindGroup, ...);   // Frame
renderPass.setBindGroup(1, lightBindGroup, ...);   // Light
renderPass.setBindGroup(2, objectBindGroup, ...);  // Object

// CORRECT - Name-based resolution via BindGroupBinder
binder.bind(renderPass, shaderInfo, cameraId, {
    {BindGroupType::Light, lightBindGroup},
    {BindGroupType::Object, objectBindGroup}
});
// System resolves: "LightBuffer" -> index 1, "ObjectUniforms" -> index 2

5. Understand State Tracking

The binder automatically optimizes bind group bindings:

// Automatic detection of changes
binder.bind(renderPass, shaderInfo, cameraId1, bindGroups, objectId1);
binder.bind(renderPass, shaderInfo, cameraId1, bindGroups, objectId2);  // Only Object rebound
binder.bind(renderPass, shaderInfo, cameraId2, bindGroups, objectId2);  // Only Frame rebound

// New render pass automatically detected
auto newPass = encoder.beginRenderPass(&desc);
binder.bind(newPass, shaderInfo, cameraId2, bindGroups, objectId2);  // All rebound

Troubleshooting

  • Bind group not found: Shader name and bind group name must match exactly (case-sensitive).
  • Data not updating: Ensure preRender() supplies data each frame and the struct size matches the shader.
  • Wrong bind group bound: Check instance ID for PerObject/PerMaterial reuse policies.
  • Index resolution fails: Verify shaderInfo->getBindGroupIndex(name) returns a value.

Advanced Topics

Bind Group Index Assignment

The system automatically assigns indices during shader parsing:

// WebGPUShaderFactory parses shader
// Finds: @group(5) @binding(0) var<uniform> myData: MyUniforms;

// Creates WebGPUBindGroupLayoutInfo
auto layoutInfo = std::make_shared<WebGPUBindGroupLayoutInfo>(
    "MyCustomData",              // Name for lookup
    BindGroupType::Custom,       // Type
    BindGroupReuse::PerFrame,    // Reuse policy
    bindings                     // Binding definitions
);

// Adds to shader info with actual shader index
shaderInfo->addBindGroupLayout(5, layoutInfo);
// This stores: m_nameToIndex["MyCustomData"] = 5

Multi-Shader Compatibility

Different shaders can use different indices for the same logical bind group:

// ShaderA.wgsl
@group(0) @binding(0) var<uniform> u_frame: FrameUniforms;
@group(3) @binding(0) var<uniform> u_object: ObjectUniforms;

// ShaderB.wgsl
@group(0) @binding(0) var<uniform> u_frame: FrameUniforms;
@group(7) @binding(0) var<uniform> u_object: ObjectUniforms;

Both work correctly! The system resolves by name:

  • ShaderA: "ObjectUniforms" -> 3
  • ShaderB: "ObjectUniforms" -> 7

Name Resolution Flow

1. RenderPass calls: binder.bind(renderPass, shaderInfo, ...)
2. Binder iterates: shaderInfo->getBindGroupLayoutVector()
3. For each layout:
   - Resolve index via shaderInfo->getBindGroupIndex(layoutName)
   - Fetch bind group based on type and reuse policy
   - Bind only if changed

Future Extensions

  • Automatic uniform reflection
  • Bind group validation for layout sizes
  • Hot reload on shader changes
  • Usage metrics and bind group statistics