Fix query leaks when GetQueryResult call is missed

This commit is contained in:
2026-08-25 12:50:37 +02:00
parent 3fa73c2d69
commit 70155516c3
11 changed files with 94 additions and 29 deletions
@@ -627,6 +627,7 @@ uint64 GPUContextDX11::BeginQuery(GPUQueryType type)
ASSERT_LOW_LAYER(query.State == GPUQueryDataDX11::Ready);
ASSERT_LOW_LAYER(query.Type == type);
query.State = GPUQueryDataDX11::Begin;
query.TTL = 50; // Auto-destroy after some frames when user fails to call GetQueryResult
auto context = _device->GetIM();
if (type == GPUQueryType::Timer)
{
@@ -2,6 +2,7 @@
#pragma once
#include "Engine/Core/Math/Vector4.h"
#include "Engine/Graphics/GPUContext.h"
#include "GPUDeviceDX11.h"
#include "GPUPipelineStateDX11.h"
@@ -998,13 +998,13 @@ void GPUDeviceDX11::DrawEnd()
}
#endif
// Auto-return finished queries back to the pool
// Auto-return finished queries back to the pool (or leaked queries)
auto* queries = _queries.Get();
int32 queriesCount = _queries.Count();
for (int32 i = 0; i < queriesCount; i++)
{
auto& query = queries[i];
if (query.State == GPUQueryDataDX11::Finished)
if (query.State == GPUQueryDataDX11::Finished || (query.State == GPUQueryDataDX11::End && --query.TTL == 0))
{
query.State = GPUQueryDataDX11::Ready;
query.Result = 0;
@@ -1027,7 +1027,8 @@ bool GPUDeviceDX11::GetQueryResult(uint64 queryID, uint64& result, bool wait)
result = query.Result;
return true;
}
ASSERT_LOW_LAYER(query.State == GPUQueryDataDX11::End);
if (query.State != GPUQueryDataDX11::End)
return false;
auto context = GetIM();
RETRY:
@@ -1062,13 +1063,17 @@ RETRY:
}
else if (q.Type == (uint16)GPUQueryType::Occlusion)
{
hasData = context->GetData(query.Query, &result, sizeof(UINT64), 0) == S_OK;
UINT64 data;
hasData = context->GetData(query.Query, &data, sizeof(data), 0) == S_OK;
if (hasData)
result = data;
}
else
{
BOOL resultBool;
hasData = context->GetData(query.Query, &resultBool, sizeof(BOOL), 0) == S_OK;
result = resultBool ? 1 : 0;
BOOL data;
hasData = context->GetData(query.Query, &data, sizeof(data), 0) == S_OK;
if (hasData)
result = data ? 1 : 0;
}
if (!hasData && wait)
@@ -41,7 +41,8 @@ struct GPUQueryDataDX11
ID3D11Query* TimerBeginQuery = nullptr;
ID3D11Query* DisjointQuery = nullptr;
uint64 Result = 0;
enum States { Ready, Begin, End, Finished } State = Ready;
uint16 TTL = 0;
enum States : uint16 { Ready, Begin, End, Finished } State = Ready;
GPUQueryType Type = GPUQueryType::MAX;
void Release();
@@ -1003,7 +1003,7 @@ void GPUDeviceDX12::RenderEnd()
// Resolve the queries
for (auto heap : QueryHeaps)
heap->EndQueryBatchAndResolveQueryData(_mainContext);
heap->EndFrame(_mainContext);
}
void GPUDeviceDX12::OnCrash()
@@ -79,13 +79,20 @@ void QueryHeapDX12::Destroy()
_resultData.SetCapacity(0);
}
void QueryHeapDX12::EndQueryBatchAndResolveQueryData(GPUContextDX12* context)
void QueryHeapDX12::EndFrame(GPUContextDX12* context)
{
ASSERT(_currentBatch.Open);
if (_currentBatch.Count == 0)
return;
// Update existing batches to avoid leaks
for (int32 i = 0; i < _batches.Count(); i++)
{
if (--_batches[i].TTL == 0)
{
_batches.RemoveAt(i--);
}
}
// Close the current batch
// Close the current batch (if not empty)
if (!_currentBatch.Open || _currentBatch.Count == 0)
return;
_currentBatch.Open = false;
// Resolve the batch
@@ -203,6 +210,7 @@ void QueryHeapDX12::StartQueryBatch()
// Start a new batch
_currentBatch.Start = _currentIndex;
_currentBatch.Open = true;
_currentBatch.TTL = 50; // Auto-destroy after some frames when user fails to call GetQueryResult
}
#endif
@@ -75,6 +75,8 @@ private:
/// </summary>
bool Open = false;
uint8 TTL = 0;
/// <summary>
/// Checks if this query batch contains a given element contains the element.
/// </summary>
@@ -146,7 +148,7 @@ public:
/// Stops tracking the current batch of begin/end query calls that will be resolved together. This implicitly starts a new batch.
/// </summary>
/// <param name="context">The context.</param>
void EndQueryBatchAndResolveQueryData(GPUContextDX12* context);
void EndFrame(GPUContextDX12* context);
/// <summary>
/// Checks if can alloc a new query (without rolling the existing batch).
@@ -677,7 +677,6 @@ void QueryPoolVulkan::Reset(CmdBufferVulkan* cmdBuffer)
BufferedQueryPoolVulkan::BufferedQueryPoolVulkan(GPUDeviceVulkan* device, int32 capacity, GPUQueryType type)
: QueryPoolVulkan(device, capacity, type)
, _lastBeginIndex(0)
{
_queryOutput.Resize(capacity);
_usedQueryBits.AddZeroed((capacity + 63) / 64);
@@ -702,10 +701,12 @@ bool BufferedQueryPoolVulkan::AcquireQuery(CmdBufferVulkan* cmdBuffer, uint32& r
resultIndex += wordIndex * 64;
const uint64 bit = (uint64)1 << (uint64)(resultIndex % 64);
_usedQueryBits[wordIndex] = _usedQueryBits[wordIndex] | bit;
_startedQueryBits[wordIndex] &= ~bit;
_readResultsBits[wordIndex] &= ~bit;
_lastBeginIndex = resultIndex + 1;
if (ResetBeforeUse)
Reset(cmdBuffer);
_lastUsedFrame = Engine::FrameCount;
return true;
}
}
@@ -728,6 +729,7 @@ void BufferedQueryPoolVulkan::ReleaseQuery(uint32 queryIndex)
_lastBeginIndex = (uint32)queryIndex;
}
}
_lastUsedFrame = Engine::FrameCount;
if (_usedQueryBits[word] == 0)
{
// Check if pool got empty and reset the pointer back to start
@@ -749,23 +751,21 @@ void BufferedQueryPoolVulkan::MarkQueryAsStarted(uint32 queryIndex)
bool BufferedQueryPoolVulkan::GetResults(uint32 index, uint64& result)
{
const uint64 bit = (uint64)(index % 64);
const uint64 bitMask = (uint64)1 << bit;
const uint64 bit = (uint64)1 << (index % 64);
const uint32 word = index / 64;
if ((_startedQueryBits[word] & bitMask) == 0)
if ((_startedQueryBits[word] & bit) == 0)
{
// Query never started/ended
result = 0;
return true;
}
if ((_readResultsBits[word] & bitMask) == 0)
if ((_readResultsBits[word] & bit) == 0)
{
const VkResult vkResult = vkGetQueryPoolResults(_device->Device, _handle, index, 1, sizeof(uint64), &_queryOutput[index], sizeof(uint64), VK_QUERY_RESULT_64_BIT);
if (vkResult == VK_SUCCESS)
{
_readResultsBits[word] = _readResultsBits[word] | bitMask;
_readResultsBits[word] = _readResultsBits[word] | bit;
#if VULKAN_RESET_QUERY_POOLS
// Add to reset
@@ -777,7 +777,6 @@ bool BufferedQueryPoolVulkan::GetResults(uint32 index, uint64& result)
else if (vkResult == VK_NOT_READY)
{
// No results yet
result = 0;
return false;
}
else
@@ -786,13 +785,15 @@ bool BufferedQueryPoolVulkan::GetResults(uint32 index, uint64& result)
}
}
// Get result
result = _queryOutput[index];
_lastUsedFrame = Engine::FrameCount;
return true;
}
bool BufferedQueryPoolVulkan::HasRoom() const
{
const uint64 allUsedMask = MAX_uint64;
constexpr uint64 allUsedMask = MAX_uint64;
if (_lastBeginIndex < _usedQueryBits.Count() * 64)
{
ASSERT((_usedQueryBits[_lastBeginIndex / 64] & allUsedMask) != allUsedMask);
@@ -801,6 +802,49 @@ bool BufferedQueryPoolVulkan::HasRoom() const
return false;
}
bool BufferedQueryPoolVulkan::TryExpire()
{
// Auto-destroy pools some frames when user fails to call GetQueryResult
if (Engine::FrameCount - _lastUsedFrame < 50)
return false;
// Reset back to clean state all unfinished queries
for (uint32 index = 0; index < _queryOutput.Count(); index++)
{
const uint64 bit = (uint64)1 << (index % 64);
const uint32 word = index / 64;
if ((_usedQueryBits[word] & bit) == 0)
continue;
if ((_readResultsBits[word] & bit) == 0)
{
const VkResult vkResult = vkGetQueryPoolResults(_device->Device, _handle, index, 1, sizeof(uint64), &_queryOutput[index], sizeof(uint64), VK_QUERY_RESULT_64_BIT);
if (vkResult == VK_SUCCESS)
{
_readResultsBits[word] = _readResultsBits[word] | bit;
#if VULKAN_RESET_QUERY_POOLS
// Add to reset
if (!_device->QueriesToReset.Contains(this))
_device->QueriesToReset.Add(this);
_resetRanges.Add(Range{ index, 1 });
#endif
}
}
_usedQueryBits[word] = _usedQueryBits[word] & ~bit;
}
constexpr uint64 allUsedMask = MAX_uint64;
for (int32 wordIndex = 0; wordIndex < _usedQueryBits.Count(); wordIndex++)
{
if (_usedQueryBits[wordIndex] != allUsedMask)
{
_lastBeginIndex = wordIndex * 64;
break;
}
}
return HasRoom();
}
HelperResourcesVulkan::HelperResourcesVulkan(GPUDeviceVulkan* device)
: _device(device)
{
@@ -1276,7 +1320,7 @@ int32 GPUDeviceVulkan::GetOrCreateQueryPool(GPUQueryType type)
for (int32 i = 0; i < QueryPools.Count(); i++)
{
auto pool = pools[i];
if (pool->Type == type && pool->HasRoom())
if (pool->Type == type && (pool->HasRoom() || pool->TryExpire()))
return i;
}
@@ -312,7 +312,9 @@ private:
Array<uint64> _readResultsBits;
// Last potentially free index in the pool
int32 _lastBeginIndex;
int32 _lastBeginIndex = 0;
// Last frame when pool was used
uint64 _lastUsedFrame = 0;
public:
BufferedQueryPoolVulkan(GPUDeviceVulkan* device, int32 capacity, GPUQueryType type);
@@ -321,6 +323,7 @@ public:
void MarkQueryAsStarted(uint32 queryIndex);
bool GetResults(uint32 index, uint64& result);
bool HasRoom() const;
bool TryExpire();
};
/// <summary>
@@ -642,7 +642,7 @@ void GPUSwapChainVulkan::Present(bool vsync)
// Cache a command buffer to wait on its fence before drawing to this backbuffer again
auto& acquiredBackBuffer = _backBuffers[_acquiredImageIndex];
ASSERT(acquiredBackBuffer.SubmitCmdBuffer == nullptr);
ASSERT(acquiredBackBuffer.SubmitCmdBuffer == nullptr || !acquiredBackBuffer.SubmitCmdBuffer->IsSubmitted());
acquiredBackBuffer.SubmitCmdBuffer = context->GetCmdBufferManager()->GetActiveCmdBuffer();
context->GetCmdBufferManager()->SubmitActiveCmdBuffer(_backBuffers[_acquiredImageIndex].RenderingDoneSemaphore);
@@ -17,10 +17,10 @@
#include <webgpu/webgpu.h>
// Utiltiy macro to convert WGPUStringView into UTF-16 string (on stack)
// Utility macro to convert WGPUStringView into UTF-16 string (on stack)
#define WEBGPU_TO_STR(strView) StringAsUTF16<>(strView.data, strView.data ? strView.length : 0).Get()
// Utiltiy macro to get WGPUStringView for a text constant
// Utility macro to get WGPUStringView for a text constant
#define WEBGPU_STR(str) { str, ARRAY_COUNT(str) - 1 }
#define WEBGPU_MAX_QUERY_SETS 8