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:
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
@group(N) annotationsm_nameToIndex)shaderInfo->getBindGroupIndex(layoutInfo->getName())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);
FrameUniforms (if used by shader)@group(N) annotations| 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:
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:
shaderInfo->getBindGroupIndex("MyCustomData")@group(10) or @group(99) - the system resolves it dynamically// 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 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
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 |
Per-object bind groups create separate GPU resources for each instance. Prefer:
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 2
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
preRender() supplies data each frame and the struct size matches the shader.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"] = 5
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:
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