Add WebGPU bind groups caching

This commit is contained in:
Wojtek Figat
2026-03-03 09:47:59 +01:00
parent a925c7410a
commit c4f4983f6d
6 changed files with 167 additions and 28 deletions

View File

@@ -14,6 +14,7 @@
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Viewport.h"
#include "Engine/Core/Math/Rectangle.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/RenderStats.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
@@ -504,10 +505,6 @@ void GPUContextWebGPU::Flush()
wgpuQueueSubmit(_device->Queue, 1, &commandBuffer);
wgpuCommandBufferRelease(commandBuffer);
}
for (auto e : _unusedBindGroups)
wgpuBindGroupRelease(e);
_unusedBindGroups.Clear();
}
void GPUContextWebGPU::UpdateBuffer(GPUBuffer* buffer, const void* data, uint32 size, uint32 offset)
@@ -962,20 +959,24 @@ void GPUContextWebGPU::FlushBindGroup()
_bindGroupDirty = false;
// Each shader stage (Vertex, Pixel) uses a separate bind group
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
GPUPipelineStateWebGPU::BindGroupKey key;
for (int32 groupIndex = 0; groupIndex < GPUBindGroupsWebGPU::GraphicsMax; groupIndex++)
{
auto descriptors = _pipelineState->BindGroupDescriptors[groupIndex];
bindGroupDesc.layout = _pipelineState->BindGroupLayouts[groupIndex];
if (!descriptors || !bindGroupDesc.layout)
key.Layout = _pipelineState->BindGroupLayouts[groupIndex];
if (!descriptors || !key.Layout)
continue;
// Build descriptors for the bind group
auto entriesCount = descriptors->DescriptorTypesCount;
_dynamicOffsets.Clear();
_bindGroupEntries.Resize(entriesCount);
auto entriesPtr = _bindGroupEntries.Get();
static_assert(ARRAY_COUNT(key.Entries) == SpirvShaderDescriptorInfo::MaxDescriptors, "Invalid size of bind group entries array.");
static_assert(ARRAY_COUNT(key.Versions) == SpirvShaderDescriptorInfo::MaxDescriptors, "Invalid size of bind group versions array.");
key.EntriesCount = entriesCount;
auto entriesPtr = key.Entries;
auto versionsPtr = key.Versions;
Platform::MemoryClear(entriesPtr, entriesCount * sizeof(WGPUBindGroupEntry));
Platform::MemoryClear(versionsPtr, ((entriesCount + 3) & ~0x3) * sizeof(uint8));
for (int32 index = 0; index < entriesCount; index++)
{
auto& descriptor = descriptors->DescriptorTypes[index];
@@ -999,7 +1000,10 @@ void GPUContextWebGPU::FlushBindGroup()
auto view = _shaderResources[descriptor.Slot];
auto ptr = view ? (GPUResourceViewPtrWebGPU*)view->GetNativePtr() : nullptr;
if (ptr && ptr->TextureView)
{
entry.textureView = ptr->TextureView->View;
versionsPtr[index] = ptr->Version;
}
if (!entry.textureView)
{
// Fallback
@@ -1036,7 +1040,11 @@ void GPUContextWebGPU::FlushBindGroup()
GPUResourceView* view = _resourceTables[(int32)descriptor.BindingType][descriptor.Slot];
auto ptr = view ? (GPUResourceViewPtrWebGPU*)view->GetNativePtr() : nullptr;
if (ptr && ptr->BufferView)
{
entry.buffer = ptr->BufferView->Buffer;
entry.size = ((GPUBufferWebGPU*)view->GetParent())->GetSize();
versionsPtr[index] = (uint64)ptr->Version;
}
if (!entry.buffer)
entry.buffer = _device->DefaultBuffer; // Fallback
break;
@@ -1068,13 +1076,11 @@ void GPUContextWebGPU::FlushBindGroup()
}
}
// Create a bind group
bindGroupDesc.entryCount = _bindGroupEntries.Count();
bindGroupDesc.entries = entriesPtr;
#if BUILD_DEBUG
for (int32 i = 0; i < bindGroupDesc.entryCount; i++)
// Validate
for (int32 i = 0; i < entriesCount; i++)
{
auto& e = bindGroupDesc.entries[i];
auto& e = entriesPtr[i];
if ((e.buffer != nullptr) + (e.sampler != nullptr) + (e.textureView != nullptr) != 1)
{
LOG(Error, "Invalid binding in group {} at index {} ({})", groupIndex, i, _pipelineState->GetName());
@@ -1084,10 +1090,9 @@ void GPUContextWebGPU::FlushBindGroup()
}
}
#endif
WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(_device->Device, &bindGroupDesc);
_unusedBindGroups.Add(bindGroup);
// Bind group
WGPUBindGroup bindGroup = _pipelineState->GetBindGroup(key);
wgpuRenderPassEncoderSetBindGroup(_renderPass, groupIndex, bindGroup, _dynamicOffsets.Count(), _dynamicOffsets.Get());
}
}

View File

@@ -43,9 +43,7 @@ private:
GPUDeviceWebGPU* _device;
uint32 _minUniformBufferOffsetAlignment;
Array<WGPUBindGroupEntry> _bindGroupEntries;
Array<uint32> _dynamicOffsets;
Array<WGPUBindGroup> _unusedBindGroups;
// State tracking
int32 _renderPassDirty : 1;
@@ -70,7 +68,7 @@ private:
BufferBind _indexBuffer;
BufferBind _vertexBuffers[GPU_MAX_VB_BINDED];
GPUPipelineStateWebGPU* _pipelineState;
GPUPipelineStateWebGPU::Key _pipelineKey;
GPUPipelineStateWebGPU::PipelineKey _pipelineKey;
Array<PendingClear, FixedAllocation<16>> _pendingClears;
GPUResourceView* _shaderResources[GPU_MAX_SR_BINDED];
GPUResourceView* _storageResources[GPU_MAX_SR_BINDED];

View File

@@ -132,6 +132,7 @@ struct GPUResourceViewPtrWebGPU
{
class GPUBufferViewWebGPU* BufferView;
class GPUTextureViewWebGPU* TextureView;
uint8 Version;
};
extern GPUDevice* CreateGPUDeviceWebGPU();

View File

@@ -7,11 +7,14 @@
#include "GPUVertexLayoutWebGPU.h"
#include "RenderToolsWebGPU.h"
#include "Engine/Core/Log.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Utilities/Crc.h"
#define WEBGPU_LOG_PSO 0
#define WEBGPU_LOG_BIND_GROUPS 0
WGPUCompareFunction ToCompareFunction(ComparisonFunc value)
{
@@ -139,6 +142,9 @@ void GPUPipelineStateWebGPU::OnReleaseGPU()
{
VS = nullptr;
PS = nullptr;
for (auto& e : _bindGroups)
wgpuBindGroupRelease(e.Value);
_bindGroups.Clear();
for (auto& e : _pipelines)
wgpuRenderPipelineRelease(e.Value);
_pipelines.Clear();
@@ -158,15 +164,29 @@ void GPUPipelineStateWebGPU::OnReleaseGPU()
Platform::MemoryClear(&BindGroupDescriptors, sizeof(BindGroupDescriptors));
}
uint32 GetHash(const GPUPipelineStateWebGPU::Key& key)
uint32 GetHash(const GPUPipelineStateWebGPU::PipelineKey& key)
{
static_assert(sizeof(GPUPipelineStateWebGPU::Key) == sizeof(uint64) * 2, "Invalid PSO key size.");
static_assert(sizeof(GPUPipelineStateWebGPU::PipelineKey) == sizeof(uint64) * 2, "Invalid PSO key size.");
uint32 hash = GetHash(key.Packed[0]);
CombineHash(hash, GetHash(key.Packed[1]));
return hash;
}
WGPURenderPipeline GPUPipelineStateWebGPU::GetPipeline(const Key& key, GPUResourceView* shaderResources[GPU_MAX_SR_BINDED])
uint32 GetHash(const GPUPipelineStateWebGPU::BindGroupKey& key)
{
return key.Hash;
}
bool GPUPipelineStateWebGPU::BindGroupKey::operator==(const BindGroupKey& other) const
{
return Hash == other.Hash
&& Layout == other.Layout
&& EntriesCount == other.EntriesCount
&& Platform::MemoryCompare(&Entries, &other.Entries, EntriesCount * sizeof(WGPUBindGroupEntry)) == 0
&& Platform::MemoryCompare(&Versions, &other.Versions, EntriesCount * sizeof(uint8)) == 0;
}
WGPURenderPipeline GPUPipelineStateWebGPU::GetPipeline(const PipelineKey& key, GPUResourceView* shaderResources[GPU_MAX_SR_BINDED])
{
WGPURenderPipeline pipeline;
if (_pipelines.TryGet(key, pipeline))
@@ -255,10 +275,105 @@ WGPURenderPipeline GPUPipelineStateWebGPU::GetPipeline(const Key& key, GPUResour
// Cache it
_pipelines.Add(key, pipeline);
return pipeline;
}
WGPUBindGroup GPUPipelineStateWebGPU::GetBindGroup(BindGroupKey& key)
{
// Generate a hash for the key
key.LastFrameUsed = Engine::FrameCount;
key.Hash = Crc::MemCrc32(&key.Entries, key.EntriesCount * sizeof(WGPUBindGroupEntry));
CombineHash(key.Hash, GetHash(key.EntriesCount));
CombineHash(key.Hash, GetHash(key.Layout));
CombineHash(key.Hash, Crc::MemCrc32(&key.Versions, key.EntriesCount * sizeof(uint8)));
// Lookup for existing bind group
WGPUBindGroup bindGroup;
auto found = _bindGroups.Find(key);
if (found.IsNotEnd())
{
// Get cached bind group and update the last usage frame
bindGroup = found->Value;
found->Key.LastFrameUsed = key.LastFrameUsed;
// Periodically remove old bind groups (unused for some time)
if (key.LastFrameUsed - _lastFrameBindGroupsGC > 100)
{
_lastFrameBindGroupsGC = key.LastFrameUsed;
int32 freed = 0;
for (auto it = _bindGroups.Begin(); it.IsNotEnd(); ++it)
{
if (key.LastFrameUsed - it->Key.LastFrameUsed > 50)
{
freed++;
wgpuBindGroupRelease(it->Value);
_bindGroups.Remove(it);
}
}
#if WEBGPU_LOG_BIND_GROUPS
if (freed > 0)
{
LOG(Info, "[WebGPU] Removed {} old entries from '{}'", freed, String(_debugName.Get(), _debugName.Count() - 1));
}
#endif
}
return bindGroup;
}
PROFILE_CPU();
PROFILE_MEM(GraphicsCommands);
#if GPU_ENABLE_RESOURCE_NAMING
ZoneText(_debugName.Get(), _debugName.Count() - 1);
#endif
#if WEBGPU_LOG_BIND_GROUPS
LOG(Info, "[WebGPU] GetBindGroup: '{}', hash: {}", String(_debugName.Get(), _debugName.Count() - 1), key.Hash);
#endif
// Build description
WGPUBindGroupDescriptor desc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
#if GPU_ENABLE_RESOURCE_NAMING
desc.label = PipelineDesc.label;
#endif
desc.layout = key.Layout;
desc.entryCount = key.EntriesCount;
desc.entries = key.Entries;
// Create object
bindGroup = wgpuDeviceCreateBindGroup(_device->Device, &desc);
if (!bindGroup)
{
#if GPU_ENABLE_RESOURCE_NAMING
LOG(Error, "wgpuDeviceCreateBindGroup failed for {}", String(_debugName.Get(), _debugName.Count() - 1));
#endif
return nullptr;
}
#if WEBGPU_LOG_BIND_GROUPS
// Debug detection of hash collisions
int32 collisions = 0, equalLayout = 0, equalEntries = 0, equalVersions = 0;
for (auto& e : _bindGroups)
{
auto& other = e.Key;
if (key.Hash == other.Hash)
{
collisions++;
if (key.Layout == other.Layout)
equalLayout++;
if (key.EntriesCount == other.EntriesCount && Platform::MemoryCompare(&key.Entries, &other.Entries, key.EntriesCount * sizeof(WGPUBindGroupEntry)) == 0)
equalEntries++;
if (key.EntriesCount == other.EntriesCount && Platform::MemoryCompare(&key.Versions, &other.Versions, key.EntriesCount * sizeof(uint8)) == 0)
equalVersions++;
}
}
if (collisions > 1)
LOG(Error, "> Hash colllision! {}/{} (capacity: {}), equalLayout: {}, equalEntries: {}, equalVersions: {}", collisions, _bindGroups.Count(), _bindGroups.Capacity(), equalLayout, equalEntries, equalVersions);
#endif
// Cache it
_bindGroups.Add(key, bindGroup);
return bindGroup;
}
void GPUPipelineStateWebGPU::InitLayout(GPUResourceView* shaderResources[GPU_MAX_SR_BINDED])
{
#if WEBGPU_LOG_PSO

View File

@@ -16,7 +16,7 @@ class GPUPipelineStateWebGPU : public GPUResourceWebGPU<GPUPipelineState>
{
public:
// Batches render context state for the pipeline state. Used as a key for caching created pipelines.
struct Key
struct PipelineKey
{
union
{
@@ -31,12 +31,25 @@ public:
uint64 Packed[2];
};
FORCE_INLINE bool operator==(const Key& other) const
FORCE_INLINE bool operator==(const PipelineKey& other) const
{
return Platform::MemoryCompare(&Packed, &other.Packed, sizeof(Packed)) == 0;
}
};
// Batches bind group description for the pipeline state. Used as a key for caching created bind groups.
struct BindGroupKey
{
uint32 Hash;
WGPUBindGroupLayout Layout;
mutable uint64 LastFrameUsed;
WGPUBindGroupEntry Entries[64];
uint8 EntriesCount;
uint8 Versions[64]; // Versions of descriptors used to differentiate when texture residency gets changed
bool operator==(const BindGroupKey& other) const;
};
private:
#if GPU_ENABLE_RESOURCE_NAMING
DebugName _debugName;
@@ -46,7 +59,9 @@ private:
WGPUBlendState _blendState;
WGPUColorTargetState _colorTargets[GPU_MAX_RT_BINDED];
WGPUVertexBufferLayout _vertexBuffers[GPU_MAX_VB_BINDED];
Dictionary<Key, WGPURenderPipeline> _pipelines;
Dictionary<PipelineKey, WGPURenderPipeline> _pipelines;
Dictionary<BindGroupKey, WGPUBindGroup> _bindGroups;
uint64 _lastFrameBindGroupsGC = 0;
public:
GPUShaderProgramVSWebGPU* VS = nullptr;
@@ -63,7 +78,10 @@ public:
public:
// Gets the pipeline for the given rendering state. Pipelines are cached and reused for the same key.
WGPURenderPipeline GetPipeline(const Key& key, GPUResourceView* shaderResources[GPU_MAX_SR_BINDED]);
WGPURenderPipeline GetPipeline(const PipelineKey& key, GPUResourceView* shaderResources[GPU_MAX_SR_BINDED]);
// Gets the bind group for the given key (unhashed). Bind groups are cached and reused for the same key.
WGPUBindGroup GetBindGroup(BindGroupKey& desc);
private:
void InitLayout(GPUResourceView* shaderResources[GPU_MAX_SR_BINDED]);
@@ -78,6 +96,7 @@ protected:
void OnReleaseGPU() final override;
};
uint32 GetHash(const GPUPipelineStateWebGPU::Key& key);
uint32 GetHash(const GPUPipelineStateWebGPU::PipelineKey& key);
uint32 GetHash(const GPUPipelineStateWebGPU::BindGroupKey& key);
#endif

View File

@@ -39,6 +39,7 @@ void SetWebGPUTextureViewSampler(GPUTextureView* view, uint32 samplerType)
void GPUTextureViewWebGPU::Create(WGPUTexture texture, const WGPUTextureViewDescriptor& desc)
{
Ptr.Version++;
if (View)
wgpuTextureViewRelease(View);
Texture = texture;