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
- 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
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
- Shader defines bind groups with
@group(N)annotations - ShaderFactory parses bind group names and types from shader source
- WebGPUShaderInfo stores name-to-index mapping (
m_nameToIndex) - BindGroupBinder queries index via
shaderInfo->getBindGroupIndex(layoutInfo->getName()) - 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);- 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+)
| 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 |
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.
// 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
));
}
};// 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
// 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// 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 reboundHow 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(); // TrackChoose 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 |
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
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)processBindGroupProviders() only uploads data when it differs:
// In preRender() - only provide data if it changed
if (animationStateChanged) {
outProviders.push_back(BindGroupDataProvider::create(...));
}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 2The 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- 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.
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"] = 5Different 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
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
- Automatic uniform reflection
- Bind group validation for layout sizes
- Hot reload on shader changes
- Usage metrics and bind group statistics