vk: timer query refactor (#8459)

- Put QueryManager into its own .h/.cpp
 - Do not assert when endTime <= beginTime.  This could happen if
   they were set on different command buffers.  To be resolved
   later.
 - Better logic for iterating through a bitset.
This commit is contained in:
Powei Feng
2025-02-27 11:08:17 -08:00
committed by GitHub
parent db30a9a50c
commit ff1f2fcac6
10 changed files with 214 additions and 171 deletions

View File

@@ -194,7 +194,6 @@ if (FILAMENT_SUPPORTS_VULKAN)
src/vulkan/utils/Spirv.h
src/vulkan/utils/Spirv.cpp
src/vulkan/utils/StaticVector.h
src/vulkan/VulkanAsyncHandles.cpp
src/vulkan/VulkanAsyncHandles.h
src/vulkan/VulkanBlitter.cpp
src/vulkan/VulkanBlitter.h
@@ -216,6 +215,8 @@ if (FILAMENT_SUPPORTS_VULKAN)
src/vulkan/VulkanMemory.cpp
src/vulkan/VulkanPipelineCache.cpp
src/vulkan/VulkanPipelineCache.h
src/vulkan/VulkanQueryManager.cpp
src/vulkan/VulkanQueryManager.h
src/vulkan/VulkanSamplerCache.cpp
src/vulkan/VulkanSamplerCache.h
src/vulkan/VulkanStagePool.cpp

View File

@@ -1,43 +0,0 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "VulkanAsyncHandles.h"
namespace filament::backend {
VulkanTimerQuery::VulkanTimerQuery(std::tuple<uint32_t, uint32_t> indices)
: mStartingQueryIndex(std::get<0>(indices)),
mStoppingQueryIndex(std::get<1>(indices)) {}
void VulkanTimerQuery::setFence(std::shared_ptr<VulkanCmdFence> fence) noexcept {
std::unique_lock<utils::Mutex> lock(mFenceMutex);
mFence = fence;
}
bool VulkanTimerQuery::isCompleted() noexcept {
std::unique_lock<utils::Mutex> lock(mFenceMutex);
// QueryValue is a synchronous call and might occur before beginTimerQuery has written anything
// into the command buffer, which is an error according to the validation layer that ships in
// the Android NDK. Even when AVAILABILITY_BIT is set, validation seems to require that the
// timestamp has at least been written into a processed command buffer.
// This fence indicates that the corresponding buffer has been completed.
return mFence && mFence->getStatus() == VK_SUCCESS;
}
VulkanTimerQuery::~VulkanTimerQuery() = default;
} // namespace filament::backend

View File

@@ -53,17 +53,28 @@ struct VulkanFence : public HwFence, fvkmemory::ThreadSafeResource {
};
struct VulkanTimerQuery : public HwTimerQuery, fvkmemory::ThreadSafeResource {
explicit VulkanTimerQuery(std::tuple<uint32_t, uint32_t> indices);
~VulkanTimerQuery();
VulkanTimerQuery(uint32_t startingIndex, uint32_t stoppingIndex)
: mStartingQueryIndex(startingIndex),
mStoppingQueryIndex(stoppingIndex) {}
void setFence(std::shared_ptr<VulkanCmdFence> fence) noexcept;
bool isCompleted() noexcept;
uint32_t getStartingQueryIndex() const {
return mStartingQueryIndex;
void setFence(std::shared_ptr<VulkanCmdFence> fence) noexcept {
std::unique_lock<utils::Mutex> lock(mFenceMutex);
mFence = fence;
}
bool isCompleted() noexcept {
std::unique_lock<utils::Mutex> lock(mFenceMutex);
// QueryValue is a synchronous call and might occur before beginTimerQuery has written
// anything into the command buffer, which is an error according to the validation layer
// that ships in the Android NDK. Even when AVAILABILITY_BIT is set, validation seems to
// require that the timestamp has at least been written into a processed command buffer.
// This fence indicates that the corresponding buffer has been completed.
return mFence && mFence->getStatus() == VK_SUCCESS;
}
uint32_t getStartingQueryIndex() const { return mStartingQueryIndex; }
uint32_t getStoppingQueryIndex() const {
return mStoppingQueryIndex;
}

View File

@@ -76,75 +76,5 @@ VkImageSubresourceRange VulkanAttachment::getSubresourceRange() const {
};
}
VulkanTimestamps::VulkanTimestamps(VkDevice device) : mDevice(device) {
// Create a timestamp pool large enough to hold a pair of queries for each timer.
VkQueryPoolCreateInfo tqpCreateInfo = {
.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
.queryType = VK_QUERY_TYPE_TIMESTAMP,
};
std::unique_lock<utils::Mutex> lock(mMutex);
tqpCreateInfo.queryCount = mUsed.size() * 2;
VkResult result = vkCreateQueryPool(mDevice, &tqpCreateInfo, VKALLOC, &mPool);
FILAMENT_CHECK_POSTCONDITION(result == VK_SUCCESS) << "vkCreateQueryPool failed."
<< " error=" << static_cast<int32_t>(result);
mUsed.reset();
}
std::tuple<uint32_t, uint32_t> VulkanTimestamps::getNextQuery() {
std::unique_lock<utils::Mutex> lock(mMutex);
size_t const maxTimers = mUsed.size();
assert_invariant(mUsed.count() < maxTimers);
for (size_t timerIndex = 0; timerIndex < maxTimers; ++timerIndex) {
if (!mUsed.test(timerIndex)) {
mUsed.set(timerIndex);
return std::make_tuple(timerIndex * 2, timerIndex * 2 + 1);
}
}
FVK_LOGE << "More than " << maxTimers << " timers are not supported." << utils::io::endl;
return std::make_tuple(0, 1);
}
void VulkanTimestamps::clearQuery(uint32_t queryIndex) {
mUsed.unset(queryIndex / 2);
}
void VulkanTimestamps::beginQuery(VulkanCommandBuffer const* commands,
fvkmemory::resource_ptr<VulkanTimerQuery> query) {
uint32_t const index = query->getStartingQueryIndex();
auto const cmdbuffer = commands->buffer();
vkCmdResetQueryPool(cmdbuffer, mPool, index, 2);
vkCmdWriteTimestamp(cmdbuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mPool, index);
// We stash this because getResult might come before the query is actually processed.
query->setFence(commands->getFenceStatus());
}
void VulkanTimestamps::endQuery(VulkanCommandBuffer const* commands,
fvkmemory::resource_ptr<VulkanTimerQuery> query) {
uint32_t const index = query->getStoppingQueryIndex();
vkCmdWriteTimestamp(commands->buffer(), VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mPool, index);
}
VulkanTimestamps::QueryResult VulkanTimestamps::getResult(
fvkmemory::resource_ptr<VulkanTimerQuery> query) {
uint32_t const index = query->getStartingQueryIndex();
QueryResult result;
size_t const dataSize = result.size() * sizeof(uint64_t);
VkDeviceSize const stride = sizeof(uint64_t) * 2;
VkResult vkresult =
vkGetQueryPoolResults(mDevice, mPool, index, 2, dataSize, (void*) result.data(), stride,
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
FILAMENT_CHECK_POSTCONDITION(vkresult == VK_SUCCESS || vkresult == VK_NOT_READY)
<< "vkGetQueryPoolResults error=" << static_cast<int32_t>(vkresult);
if (vkresult == VK_NOT_READY) {
return {0, 0, 0, 0};
}
return result;
}
VulkanTimestamps::~VulkanTimestamps() {
vkDestroyQueryPool(mDevice, mPool, VKALLOC);
}
} // namespace filament::backend

View File

@@ -37,12 +37,12 @@ VK_DEFINE_HANDLE(VmaPool)
namespace filament::backend {
struct VulkanCommandBuffer;
struct VulkanRenderTarget;
struct VulkanSwapChain;
struct VulkanTexture;
class VulkanStagePool;
struct VulkanTimerQuery;
struct VulkanCommandBuffer;
struct VulkanAttachment {
fvkmemory::resource_ptr<VulkanTexture> texture;
@@ -60,33 +60,6 @@ struct VulkanAttachment {
VkImageSubresourceRange getSubresourceRange() const;
};
class VulkanTimestamps {
public:
using QueryResult = std::array<uint64_t, 4>;
VulkanTimestamps(VkDevice device);
~VulkanTimestamps();
// Not copy-able.
VulkanTimestamps(VulkanTimestamps const&) = delete;
VulkanTimestamps& operator=(VulkanTimestamps const&) = delete;
std::tuple<uint32_t, uint32_t> getNextQuery();
void clearQuery(uint32_t queryIndex);
void beginQuery(VulkanCommandBuffer const* commands,
fvkmemory::resource_ptr<VulkanTimerQuery> query);
void endQuery(VulkanCommandBuffer const* commands,
fvkmemory::resource_ptr<VulkanTimerQuery> query);
QueryResult getResult(fvkmemory::resource_ptr<VulkanTimerQuery> query);
private:
VkDevice mDevice;
VkQueryPool mPool;
utils::bitset32 mUsed;
utils::Mutex mMutex;
};
struct VulkanRenderPass {
// Between the begin and end command render pass we cache the command buffer
VulkanCommandBuffer* commandBuffer;

View File

@@ -217,6 +217,7 @@ VulkanDriver::VulkanDriver(VulkanPlatform* platform, VulkanContext const& contex
mBlitter(mPlatform->getPhysicalDevice(), &mCommands),
mReadPixels(mPlatform->getDevice()),
mDescriptorSetManager(mPlatform->getDevice(), &mResourceManager),
mQueryManager(mPlatform->getDevice()),
mIsSRGBSwapChainSupported(mPlatform->getCustomization().isSRGBSwapChainSupported),
mStereoscopicType(driverConfig.stereoscopicType) {
@@ -241,8 +242,6 @@ VulkanDriver::VulkanDriver(VulkanPlatform* platform, VulkanContext const& contex
<< " error=" << static_cast<int32_t>(result);
}
#endif
mTimestamps = std::make_unique<VulkanTimestamps>(mPlatform->getDevice());
}
VulkanDriver::~VulkanDriver() noexcept = default;
@@ -313,7 +312,7 @@ void VulkanDriver::terminate() {
mDefaultRenderTarget = {};
mBoundPipeline = {};
mTimestamps.reset();
mQueryManager.terminate();
mBlitter.terminate();
mReadPixels.terminate();
@@ -851,8 +850,7 @@ Handle<HwSwapChain> VulkanDriver::createSwapChainHeadlessS() noexcept {
Handle<HwTimerQuery> VulkanDriver::createTimerQueryS() noexcept {
// The handle must be constructed here, as a synchronous call to getTimerQueryValue might happen
// before createTimerQueryR is executed.
auto query = resource_ptr<VulkanTimerQuery>::construct(&mResourceManager,
mTimestamps->getNextQuery());
auto query = mQueryManager.getNextQuery(&mResourceManager);
query.inc();
return Handle<VulkanTimerQuery>(query.id());
}
@@ -884,6 +882,7 @@ void VulkanDriver::destroyTimerQuery(Handle<HwTimerQuery> tqh) {
return;
}
auto vtq = resource_ptr<VulkanTimerQuery>::cast(&mResourceManager, tqh);
mQueryManager.clearQuery(vtq);
vtq.dec();
}
@@ -1196,26 +1195,25 @@ TimerQueryResult VulkanDriver::getTimerQueryValue(Handle<HwTimerQuery> tqh, uint
return TimerQueryResult::NOT_READY;
}
auto results = mTimestamps->getResult(vtq);
uint64_t timestamp0 = results[0];
uint64_t available0 = results[1];
uint64_t timestamp1 = results[2];
uint64_t available1 = results[3];
if (available0 == 0 || available1 == 0) {
auto results = mQueryManager.getResult(vtq);
if (results.beginAvailable == 0 || results.endAvailable == 0) {
return TimerQueryResult::NOT_READY;
}
FILAMENT_CHECK_POSTCONDITION(timestamp1 >= timestamp0)
<< "Timestamps are not monotonically increasing.";
uint64_t const begin = results.beginTime;
uint64_t const end = results.endTime;
if (begin >= end) {
// TODO: queries might have ran on different command buffers.
FVK_LOGW << "Timestamps are not monotonically increasing. " << utils::io::endl;
*elapsedTime = 0;
return TimerQueryResult::ERROR;
}
// NOTE: MoltenVK currently writes system time so the following delta will always be zero.
// However there are plans for implementing this properly. See the following GitHub ticket.
// https://github.com/KhronosGroup/MoltenVK/issues/773
float const period = mContext.getPhysicalDeviceLimits().timestampPeriod;
uint64_t delta = uint64_t(float(timestamp1 - timestamp0) * period);
*elapsedTime = delta;
*elapsedTime = uint64_t(float(end - begin) * period);
return TimerQueryResult::AVAILABLE;
}
@@ -1856,12 +1854,12 @@ void VulkanDriver::scissor(Viewport scissorBox) {
void VulkanDriver::beginTimerQuery(Handle<HwTimerQuery> tqh) {
auto vtq = resource_ptr<VulkanTimerQuery>::cast(&mResourceManager, tqh);
mTimestamps->beginQuery(&(mCommands.get()), vtq);
mQueryManager.beginQuery(&(mCommands.get()), vtq);
}
void VulkanDriver::endTimerQuery(Handle<HwTimerQuery> tqh) {
auto vtq = resource_ptr<VulkanTimerQuery>::cast(&mResourceManager, tqh);
mTimestamps->endQuery(&(mCommands.get()), vtq);
mQueryManager.endQuery(&(mCommands.get()), vtq);
}
void VulkanDriver::debugCommandBegin(CommandStream* cmds, bool synchronous, const char* methodName) noexcept {

View File

@@ -26,6 +26,7 @@
#include "VulkanReadPixels.h"
#include "VulkanSamplerCache.h"
#include "VulkanStagePool.h"
#include "VulkanQueryManager.h"
#include "vulkan/caching/VulkanDescriptorSetManager.h"
#include "vulkan/caching/VulkanPipelineLayoutCache.h"
#include "vulkan/memory/ResourceManager.h"
@@ -119,7 +120,6 @@ private:
VulkanPlatform* mPlatform = nullptr;
fvkmemory::ResourceManager mResourceManager;
std::unique_ptr<VulkanTimestamps> mTimestamps;
resource_ptr<VulkanSwapChain> mCurrentSwapChain;
resource_ptr<VulkanRenderTarget> mDefaultRenderTarget;
@@ -138,6 +138,7 @@ private:
VulkanBlitter mBlitter;
VulkanReadPixels mReadPixels;
VulkanDescriptorSetManager mDescriptorSetManager;
VulkanQueryManager mQueryManager;
// This is necessary for us to write to push constants after binding a pipeline.
struct {

View File

@@ -55,7 +55,6 @@ inline uint8_t collapsedCount(Bitmask const& mask) {
} // anonymous namespace
class VulkanTimestamps;
struct VulkanBufferObject;
struct VulkanDescriptorSetLayout : public HwDescriptorSetLayout, fvkmemory::Resource {

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "VulkanQueryManager.h"
#include "VulkanAsyncHandles.h"
#include "VulkanCommands.h"
#include "VulkanConstants.h"
#include <bluevk/BlueVK.h>
namespace filament::backend {
using namespace bluevk;
VulkanQueryManager::VulkanQueryManager(VkDevice device) : mDevice(device) {
// Create a timestamp pool large enough to hold a pair of queries for each timer.
VkQueryPoolCreateInfo tqpCreateInfo = {
.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
.queryType = VK_QUERY_TYPE_TIMESTAMP,
.queryCount = (uint32_t) mUsed.size() * 2,
};
VkResult result = vkCreateQueryPool(mDevice, &tqpCreateInfo, VKALLOC, &mPool);
FILAMENT_CHECK_POSTCONDITION(result == VK_SUCCESS) << "vkCreateQueryPool failed."
<< " error=" << static_cast<int32_t>(result);
}
fvkmemory::resource_ptr<VulkanTimerQuery> VulkanQueryManager::getNextQuery(
fvkmemory::ResourceManager* resourceManager) {
auto unused = ~mUsed;
if (unused.empty()) {
FVK_LOGE << "More than " << mUsed.size() << " timers are not supported." << utils::io::endl;
return {};
}
bool found = false;
std::pair<uint32_t, uint32_t> queryIndices;
unused.forEachSetBit([&](size_t index) {
if (found) {
return;
}
std::unique_lock<utils::Mutex> lock(mMutex);
mUsed.set(index);
found = true;
queryIndices = std::make_pair(index * 2, index * 2 + 1);
});
return fvkmemory::resource_ptr<VulkanTimerQuery>::construct(resourceManager, queryIndices.first,
queryIndices.second);
}
void VulkanQueryManager::clearQuery(fvkmemory::resource_ptr<VulkanTimerQuery> query) {
std::unique_lock<utils::Mutex> lock(mMutex);
uint32_t const startingIndex = query->getStartingQueryIndex();
mUsed.unset(startingIndex / 2);
}
void VulkanQueryManager::beginQuery(VulkanCommandBuffer const* commands,
fvkmemory::resource_ptr<VulkanTimerQuery> query) {
uint32_t const index = query->getStartingQueryIndex();
auto const cmdbuffer = commands->buffer();
vkCmdResetQueryPool(cmdbuffer, mPool, index, 2);
vkCmdWriteTimestamp(cmdbuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mPool, index);
// We stash this because getResult might come before the query is actually processed.
query->setFence(commands->getFenceStatus());
}
void VulkanQueryManager::endQuery(VulkanCommandBuffer const* commands,
fvkmemory::resource_ptr<VulkanTimerQuery> query) {
uint32_t const index = query->getStoppingQueryIndex();
vkCmdWriteTimestamp(commands->buffer(), VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mPool, index);
}
VulkanQueryManager::QueryResult VulkanQueryManager::getResult(
fvkmemory::resource_ptr<VulkanTimerQuery> query) {
uint32_t const index = query->getStartingQueryIndex();
QueryResult result;
size_t const dataSize = sizeof(result);
VkDeviceSize const stride = sizeof(uint64_t) * 2;
VkResult vkresult =
vkGetQueryPoolResults(mDevice, mPool, index, 2, dataSize, (void*) &result, stride,
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
FILAMENT_CHECK_POSTCONDITION(vkresult == VK_SUCCESS || vkresult == VK_NOT_READY)
<< "vkGetQueryPoolResults error=" << static_cast<int32_t>(vkresult);
if (vkresult == VK_NOT_READY) {
return {};
}
return result;
}
void VulkanQueryManager::terminate() noexcept {
vkDestroyQueryPool(mDevice, mPool, VKALLOC);
mPool = VK_NULL_HANDLE;
}
} // filament::backend

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_FILAMENT_BACKEND_VULKANQUERYMANAGER_H
#define TNT_FILAMENT_BACKEND_VULKANQUERYMANAGER_H
#include "vulkan/memory/ResourcePointer.h"
namespace filament::backend {
struct VulkanCommandBuffer;
struct VulkanTimerQuery;
class VulkanQueryManager {
public:
struct QueryResult {
uint64_t beginTime = 0;
uint64_t beginAvailable = 0;
uint64_t endTime = 0;
uint64_t endAvailable = 0;
};
VulkanQueryManager(VkDevice device);
~VulkanQueryManager() = default;
// Not copy-able.
VulkanQueryManager(VulkanQueryManager const&) = delete;
VulkanQueryManager& operator=(VulkanQueryManager const&) = delete;
fvkmemory::resource_ptr<VulkanTimerQuery>
getNextQuery(fvkmemory::ResourceManager* mResourceManager);
void clearQuery(fvkmemory::resource_ptr<VulkanTimerQuery> query);
void beginQuery(VulkanCommandBuffer const* commands,
fvkmemory::resource_ptr<VulkanTimerQuery> query);
void endQuery(VulkanCommandBuffer const* commands,
fvkmemory::resource_ptr<VulkanTimerQuery> query);
QueryResult getResult(fvkmemory::resource_ptr<VulkanTimerQuery> query);
void terminate() noexcept;
private:
VkDevice mDevice;
VkQueryPool mPool;
utils::bitset32 mUsed;
utils::Mutex mMutex;
};
} // filament::backend
#endif // TNT_FILAMENT_BACKEND_VULKANQUERYMANAGER_H