Compare commits

...

1 Commits

Author SHA1 Message Date
Powei Feng
910c70f21b vk: record timestmps on command buffers
Timer queries are inserted into the beginning and the end of each
command buffer. Refactor vk's implementation of Filament's timer
queries so that they are now computed as a sum of the time taken
by one or more command buffers (and not direct insertion of queries
into the command stream).

This is to address a potential issue where the timestamps returned
between command buffers are not consistent.
2025-11-12 12:18:30 -08:00
12 changed files with 287 additions and 331 deletions

View File

@@ -216,8 +216,6 @@ if (FILAMENT_SUPPORTS_VULKAN)
src/vulkan/VulkanPipelineCache.h
src/vulkan/VulkanPipelineLayoutCache.cpp
src/vulkan/VulkanPipelineLayoutCache.h
src/vulkan/VulkanQueryManager.cpp
src/vulkan/VulkanQueryManager.h
src/vulkan/VulkanReadPixels.cpp
src/vulkan/VulkanReadPixels.h
src/vulkan/VulkanSamplerCache.cpp

View File

@@ -48,7 +48,7 @@ struct VulkanPlatformPrivate;
// Forward declare the fence status that will be maintained by the command
// buffer manager.
struct VulkanCmdFence;
struct VulkanCmdBufferState;
/**
* A Platform interface that creates a Vulkan backend.
@@ -242,7 +242,7 @@ public:
* @return A Platform::Sync object tracking the provided fence.
*/
virtual Platform::Sync* createSync(VkFence fence,
std::shared_ptr<VulkanCmdFence> fenceStatus) noexcept;
std::shared_ptr<VulkanCmdBufferState> fenceStatus) noexcept;
/**
* Destroys a sync. If called with a sync not created by this platform
@@ -434,7 +434,7 @@ public:
protected:
struct VulkanSync : public Platform::Sync {
VkFence fence;
std::shared_ptr<VulkanCmdFence> fenceStatus;
std::shared_ptr<VulkanCmdBufferState> fenceStatus;
};
using SurfaceBundle = std::tuple<VkSurfaceKHR, VkExtent2D>;

View File

@@ -29,9 +29,8 @@ using namespace bluevk;
namespace filament::backend {
FenceStatus VulkanCmdFence::wait(VkDevice device, uint64_t const timeout,
std::chrono::steady_clock::time_point const until) {
FenceStatus VulkanCmdBufferState::waitOnFence(VkDevice device, uint64_t const timeout,
std::chrono::steady_clock::time_point const until) {
// this lock MUST be held for READ when calling vkWaitForFences()
std::shared_lock rl(mLock);
@@ -81,7 +80,7 @@ FenceStatus VulkanCmdFence::wait(VkDevice device, uint64_t const timeout,
return FenceStatus::ERROR; // not supported
}
void VulkanCmdFence::resetFence(VkDevice device) {
void VulkanCmdBufferState::resetFence(VkDevice device) {
// This lock prevents vkResetFences() from being called simultaneously with vkWaitForFences(),
// but by construction, when we're here, we know that the fence has signaled and
// vkWaitForFences() will return shortly.
@@ -90,4 +89,35 @@ void VulkanCmdFence::resetFence(VkDevice device) {
vkResetFences(device, 1, &mFence);
}
uint64_t VulkanTimerQuery::getResult() {
{
std::lock_guard<std::mutex> lock(mDurationLock);
if (mDuration != UNKNOWN_QUERY_RESULT) {
return mDuration;
}
}
VulkanCmdBufferState* startState = nullptr;
VulkanCmdBufferState* endState = nullptr;
{
std::shared_lock const l(mMutex);
startState = mBeginState.get();
endState = mEndState.get();
}
// Here we sum up the time taken by each command buffer that were marked by this timer query.
uint64_t duration = 0;
do {
uint64_t const stDuration = startState->getBufferDuration();
assert_invariant(stDuration != VulkanCmdBufferState::UNKNOWN_BUFFER_DURATION);
duration += stDuration;
startState = startState->getNextState().get();
} while (startState != endState);
{
std::lock_guard<std::mutex> lock(mDurationLock);
mDuration = duration;
}
return duration;
}
} // namespace filament::backend

View File

@@ -27,6 +27,7 @@
#include <chrono>
#include <cstdint>
#include <limits>
#include <memory>
#include <mutex>
#include <shared_mutex>
@@ -35,25 +36,36 @@
namespace filament::backend {
// Wrapper to enable use of shared_ptr for implementing shared ownership of low-level Vulkan fences.
struct VulkanCmdFence {
explicit VulkanCmdFence(VkFence fence) : mFence(fence) { }
~VulkanCmdFence() = default;
// Wrapper to enable use of shared_ptr for implementing shared ownership of Vulkan fences
// and timer query results.
struct VulkanCmdBufferState {
static uint64_t const UNKNOWN_BUFFER_DURATION = std::numeric_limits<uint64_t>::max();
void setStatus(VkResult const value) {
explicit VulkanCmdBufferState(VkFence fence)
: mFence(fence) {
}
~VulkanCmdBufferState() = default;
void setStatus(VkResult const value, uint64_t duration) {
std::lock_guard const l(mLock);
mStatus = value;
mDuration = duration;
mCond.notify_all();
}
VkResult getStatus() {
uint64_t getBufferDuration() {
std::shared_lock const l(mLock);
return mDuration;
}
VkResult getFenceStatus() {
std::shared_lock const l(mLock);
return mStatus;
}
void resetFence(VkDevice device);
FenceStatus wait(VkDevice device, uint64_t timeout,
FenceStatus waitOnFence(VkDevice device, uint64_t timeout,
std::chrono::steady_clock::time_point until);
void cancel() {
@@ -62,6 +74,18 @@ struct VulkanCmdFence {
mCond.notify_all();
}
void setNextState(std::shared_ptr<VulkanCmdBufferState> next) {
mNextState = std::move(next);
}
std::shared_ptr<VulkanCmdBufferState>& getNextState() {
return mNextState;
}
VkFence getVkFence() const {
return mFence;
}
private:
std::shared_mutex mLock; // NOLINT(*-include-cleaner)
std::condition_variable_any mCond;
@@ -70,38 +94,42 @@ private:
// gets submitted, its status changes to VK_NOT_READY. Finally, when the GPU actually
// finishes executing the command buffer, the status changes to VK_SUCCESS.
VkResult mStatus{ VK_INCOMPLETE };
VkFence mFence;
VkFence const mFence;
uint64_t mDuration = UNKNOWN_BUFFER_DURATION;
// We assume that command buffers are serialized. This will point to the state of the next
// buffer. This is necessary so that we can return total duration of multiple command buffers
// when a TimerQuery is requested by the front-end.
std::shared_ptr<VulkanCmdBufferState> mNextState;
};
struct VulkanFence : public HwFence, fvkmemory::ThreadSafeResource {
VulkanFence() {}
void setFence(std::shared_ptr<VulkanCmdFence> fence) {
std::lock_guard const l(lock);
sharedFence = std::move(fence);
void setCmdBufferState(std::shared_ptr<VulkanCmdBufferState> state) {
std::lock_guard l(lock);
cmdbufState = std::move(state);
cond.notify_all();
}
std::shared_ptr<VulkanCmdFence>& getSharedFence() {
std::lock_guard const l(lock);
return sharedFence;
std::shared_ptr<VulkanCmdBufferState>& getCmdBufferState() {
std::lock_guard l(lock);
return cmdbufState;
}
std::pair<std::shared_ptr<VulkanCmdFence>, bool>
wait(std::chrono::steady_clock::time_point const until) {
// hold a reference so that our state doesn't disappear while we wait
std::pair<std::shared_ptr<VulkanCmdBufferState>, bool> wait(
std::chrono::steady_clock::time_point const until) {
std::unique_lock l(lock);
cond.wait_until(l, until, [this] {
return bool(sharedFence) || canceled;
});
// here mSharedFence will be null if we timed out
return { sharedFence, canceled };
cond.wait_until(l, until, [&] { return bool(cmdbufState) || canceled; });
// here cmdbufState will be null if we timed out
return { cmdbufState, canceled };
}
void cancel() const {
std::lock_guard const l(lock);
if (sharedFence) {
sharedFence->cancel();
if (cmdbufState) {
cmdbufState->cancel();
}
canceled = true;
cond.notify_all();
@@ -111,10 +139,10 @@ private:
mutable std::mutex lock;
mutable std::condition_variable cond;
mutable bool canceled = false;
std::shared_ptr<VulkanCmdFence> sharedFence;
std::shared_ptr<VulkanCmdBufferState> cmdbufState;
};
struct VulkanSync : fvkmemory::ThreadSafeResource, public HwSync {
struct VulkanSync : public HwSync, fvkmemory::ThreadSafeResource {
struct CallbackData {
CallbackHandler* handler;
Platform::SyncCallback cb;
@@ -128,38 +156,50 @@ struct VulkanSync : fvkmemory::ThreadSafeResource, public HwSync {
};
struct VulkanTimerQuery : public HwTimerQuery, fvkmemory::ThreadSafeResource {
VulkanTimerQuery(uint32_t startingIndex, uint32_t stoppingIndex)
: mStartingQueryIndex(startingIndex),
mStoppingQueryIndex(stoppingIndex) {}
void setFence(std::shared_ptr<VulkanCmdFence> fence) noexcept {
std::lock_guard const lock(mFenceMutex);
mFence = std::move(fence);
static decltype(VulkanCmdBufferState::UNKNOWN_BUFFER_DURATION) const
UNKNOWN_QUERY_RESULT = VulkanCmdBufferState::UNKNOWN_BUFFER_DURATION;
VulkanTimerQuery() {}
void setBeginState(std::shared_ptr<VulkanCmdBufferState> state) {
{
std::lock_guard const lock(mMutex);
mBeginState = std::move(state);
}
{
std::lock_guard<std::mutex> lock(mDurationLock);
mDuration = UNKNOWN_QUERY_RESULT;
}
}
bool isCompleted() noexcept {
std::lock_guard const 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;
void setEndState(std::shared_ptr<VulkanCmdBufferState> state) {
{
std::lock_guard const lock(mMutex);
mEndState = std::move(state);
}
{
std::lock_guard<std::mutex> lock(mDurationLock);
mDuration = UNKNOWN_QUERY_RESULT;
}
}
uint32_t getStartingQueryIndex() const { return mStartingQueryIndex; }
uint32_t getStoppingQueryIndex() const {
return mStoppingQueryIndex;
bool isComplete() {
std::shared_lock const lock(mMutex);
return mBeginState && mEndState &&
mBeginState->getBufferDuration() != UNKNOWN_QUERY_RESULT &&
mEndState->getBufferDuration() != UNKNOWN_QUERY_RESULT;
}
uint64_t getResult();
private:
uint32_t mStartingQueryIndex;
uint32_t mStoppingQueryIndex;
std::shared_ptr<VulkanCmdFence> mFence;
utils::Mutex mFenceMutex;
std::shared_mutex mMutex; // NOLINT(*-include-cleaner)
std::mutex mDurationLock;
std::shared_ptr<VulkanCmdBufferState> mBeginState;
std::shared_ptr<VulkanCmdBufferState> mEndState;
uint64_t mDuration = UNKNOWN_QUERY_RESULT;
};
} // namespace filament::backend

View File

@@ -55,6 +55,22 @@ VkCommandBuffer createCommandBuffer(VkDevice device, VkCommandPool pool) {
return cmdbuffer;
}
VkFence createFence(VkDevice device, VulkanContext const& context, VkCommandPool pool) {
VkFenceCreateInfo fenceCreateInfo{ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
VkExportFenceCreateInfo exportFenceCreateInfo{
.sType = VK_STRUCTURE_TYPE_EXPORT_FENCE_CREATE_INFO,
};
// Necessary to guard this. Otherwise, swiftshader would throw an error.
if (context.getFenceExportFlags()) {
exportFenceCreateInfo.handleTypes = context.getFenceExportFlags(),
fenceCreateInfo.pNext = &exportFenceCreateInfo;
}
VkFence fence;
vkCreateFence(device, &fenceCreateInfo, VKALLOC, &fence);
return fence;
}
} // anonymous namespace
#if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS)
@@ -91,30 +107,21 @@ uint32_t VulkanCommandBuffer::sAgeCounter = 0;
VulkanCommandBuffer::VulkanCommandBuffer(VulkanContext const& context, VkDevice device,
VkQueue queue, VkCommandPool pool, VulkanSemaphoreManager* semaphoreManager,
bool isProtected)
: mContext(context),
mMarkerCount(0),
isProtected(isProtected),
mDevice(device),
mQueue(queue),
mSemaphoreManager(semaphoreManager),
mBuffer(createCommandBuffer(device, pool)),
mSubmission(semaphoreManager->acquire()),
mAge(++sAgeCounter) {
VkFenceCreateInfo fenceCreateInfo{.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
VkExportFenceCreateInfo exportFenceCreateInfo{
.sType = VK_STRUCTURE_TYPE_EXPORT_FENCE_CREATE_INFO,
.handleTypes = context.getFenceExportFlags()
};
// Necessary to guard this. Otherwise, swiftshader would throw an error.
if (context.getFenceExportFlags()) {
fenceCreateInfo.pNext = &exportFenceCreateInfo;
}
vkCreateFence(device, &fenceCreateInfo, VKALLOC, &mFence);
mFenceStatus = std::make_shared<VulkanCmdFence>(mFence);
}
bool isProtected, uint32_t timerQueryIndex, VkQueryPool queryPool)
: mContext(context),
mMarkerCount(0),
isProtected(isProtected),
mDevice(device),
mQueue(queue),
mSemaphoreManager(semaphoreManager),
mBuffer(createCommandBuffer(device, pool)),
mSubmission(semaphoreManager->acquire()),
mFence(createFence(device, context, pool)),
mCmdbufState(std::make_shared<VulkanCmdBufferState>(mFence)),
mAge(++sAgeCounter),
mQueryBeginIndex(timerQueryIndex),
mQueryEndIndex(timerQueryIndex + 1),
mQueryPool(queryPool) {}
VulkanCommandBuffer::~VulkanCommandBuffer() {
vkDestroyFence(mDevice, mFence, VKALLOC);
@@ -129,12 +136,12 @@ void VulkanCommandBuffer::reset() noexcept {
mSubmission = mSemaphoreManager->acquire();
// reset the fence with proper host synchronization
mFenceStatus->resetFence(mDevice);
mCmdbufState->resetFence(mDevice);
// Internally we use the VK_INCOMPLETE status to mean "not yet submitted". When this fence
// gets, gets submitted, its status changes to VK_NOT_READY. Finally, when the GPU actually
// finishes executing the command buffer, the status changes to VK_SUCCESS.
mFenceStatus = std::make_shared<VulkanCmdFence>(mFence);
mCmdbufState = std::make_shared<VulkanCmdBufferState>(mFence);
}
void VulkanCommandBuffer::pushMarker(char const* marker) noexcept {
@@ -191,6 +198,12 @@ void VulkanCommandBuffer::begin() noexcept {
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
};
vkBeginCommandBuffer(mBuffer, &binfo);
// We need to reset the queries before writing to it. (This must happen within a command buffer,
// but not before the queries have occurred).
vkCmdResetQueryPool(mBuffer, mQueryPool, mQueryBeginIndex, 2);
vkCmdWriteTimestamp(mBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, mQueryPool,
mQueryBeginIndex);
}
fvkmemory::resource_ptr<VulkanSemaphore> VulkanCommandBuffer::submit() {
@@ -198,6 +211,9 @@ fvkmemory::resource_ptr<VulkanSemaphore> VulkanCommandBuffer::submit() {
popMarker();
}
vkCmdWriteTimestamp(mBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, mQueryPool,
mQueryEndIndex);
vkEndCommandBuffer(mBuffer);
VkSemaphore submissionSemaphore = mSubmission->getVkSemaphore();
@@ -236,7 +252,7 @@ fvkmemory::resource_ptr<VulkanSemaphore> VulkanCommandBuffer::submit() {
UTILS_UNUSED_IN_RELEASE VkResult result =
vkQueueSubmit(mQueue, 1, &submitInfo, mFence);
mFenceStatus->setStatus(VK_NOT_READY);
mCmdbufState->setStatus(VK_NOT_READY, VulkanCmdBufferState::UNKNOWN_BUFFER_DURATION);
#if FVK_ENABLED(FVK_DEBUG_COMMAND_BUFFER)
if (result != VK_SUCCESS) {
@@ -248,6 +264,45 @@ fvkmemory::resource_ptr<VulkanSemaphore> VulkanCommandBuffer::submit() {
return mSubmission;
}
void VulkanCommandBuffer::setComplete() {
if (mCmdbufState->getBufferDuration() != VulkanCmdBufferState::UNKNOWN_BUFFER_DURATION) {
return;
}
// We always query the duration of this buffer on the GPU. This is then used to inform timer
// queries from the Filament-side. The reason for doing it this way is because the timestamp is
// consistent within a command buffer, but not guarranteed to be consistent outside of a command
// buffer. So we can only infer duration by looking at execution duration with respect to a
// buffer.
struct QueryResult {
uint64_t beginTime = 0;
uint64_t beginAvailable = 0;
uint64_t endTime = 0;
uint64_t endAvailable = 0;
} result;
constexpr size_t dataSize = sizeof(result);
constexpr VkDeviceSize stride = sizeof(uint64_t) * 2;
VkResult const vkresult =
vkGetQueryPoolResults(mDevice, mQueryPool, mQueryBeginIndex, 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);
uint64_t const begin = result.beginTime;
uint64_t const end = result.endTime;
uint64_t duration = end - begin;
assert_invariant(result.beginAvailable != 0 && result.endAvailable != 0);
if (begin > end) {
FVK_LOGW << "Timestamps are not monotonically increasing. begin=" << begin <<
" end=" << end;
duration = begin - end;
}
mCmdbufState->setStatus(VK_SUCCESS, duration);
}
CommandBufferPool::CommandBufferPool(VulkanContext const& context, VkDevice device, VkQueue queue,
uint8_t queueFamilyIndex, VulkanSemaphoreManager* semaphoreManager, bool isProtected)
: mDevice(device),
@@ -259,11 +314,24 @@ CommandBufferPool::CommandBufferPool(VulkanContext const& context, VkDevice devi
(isProtected ? VK_COMMAND_POOL_CREATE_PROTECTED_BIT : 0u),
.queueFamilyIndex = queueFamilyIndex,
};
vkCreateCommandPool(device, &createInfo, VKALLOC, &mPool);
VkResult result = vkCreateCommandPool(device, &createInfo, VKALLOC, &mPool);
FILAMENT_CHECK_POSTCONDITION(result == VK_SUCCESS) << "vkCreateCommandPool failed."
<< " error=" << static_cast<int32_t>(result);
// Create a timestamp pool large enough to hold a pair of queries for each command buffer.
VkQueryPoolCreateInfo tqpCreateInfo = {
.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
.queryType = VK_QUERY_TYPE_TIMESTAMP,
.queryCount = CAPACITY *2,
};
result = vkCreateQueryPool(mDevice, &tqpCreateInfo, VKALLOC, &mQueryPool);
FILAMENT_CHECK_POSTCONDITION(result == VK_SUCCESS) << "vkCreateQueryPool failed."
<< " error=" << static_cast<int32_t>(result);
for (size_t i = 0; i < CAPACITY; ++i) {
mBuffers.emplace_back(std::make_unique<VulkanCommandBuffer>(
context, device, queue, mPool, semaphoreManager, isProtected));
mBuffers.emplace_back(std::make_unique<VulkanCommandBuffer>(context, device, queue, mPool,
semaphoreManager, isProtected, i * 2, mQueryPool));
}
}
@@ -271,6 +339,7 @@ CommandBufferPool::~CommandBufferPool() {
wait();
gc();
vkDestroyCommandPool(mDevice, mPool, VKALLOC);
vkDestroyQueryPool(mDevice, mQueryPool, VKALLOC);
}
VulkanCommandBuffer& CommandBufferPool::getRecording() {
@@ -414,7 +483,7 @@ void VulkanCommands::terminate() {
mPool.reset();
mProtectedPool.reset();
mLastSubmit = {};
mLastFenceStatus = {};
mLastBufferState = {};
}
VulkanCommandBuffer& VulkanCommands::get() {
@@ -443,8 +512,7 @@ bool VulkanCommands::flush() {
fvkmemory::resource_ptr<VulkanSemaphore> dependency;
bool hasFlushed = false;
VkFence flushedFence = VK_NULL_HANDLE;
std::shared_ptr<VulkanCmdFence> flushedFenceStatus;
std::shared_ptr<VulkanCmdBufferState> flushedBufferState;
// Note that we've ordered it so that the non-protected commands are followed by the protected
// commands. This assumes that the protected commands will be that one doing the rendering into
@@ -467,8 +535,7 @@ bool VulkanCommands::flush() {
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT);
mLastSubmit = {};
}
flushedFence = pool->getRecording().getVkFence();
flushedFenceStatus = pool->getRecording().getFenceStatus();
flushedBufferState = pool->getRecording().getState();
dependency = pool->flush();
hasFlushed = true;
}
@@ -476,8 +543,11 @@ bool VulkanCommands::flush() {
if (hasFlushed) {
mInjectedDependency = VK_NULL_HANDLE;
mLastSubmit = dependency;
mLastFence = flushedFence;
mLastFenceStatus = flushedFenceStatus;
if (mLastBufferState) {
mLastBufferState->setNextState(flushedBufferState);
}
mLastBufferState = flushedBufferState;
}
return true;

View File

@@ -42,6 +42,7 @@
namespace filament::backend {
using namespace fvkmemory;
struct CommandBufferPool;
#if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS)
class VulkanGroupMarkers {
@@ -65,7 +66,8 @@ private:
// we're done waiting on the most recent submission of the given command buffer.
struct VulkanCommandBuffer {
VulkanCommandBuffer(VulkanContext const& mContext, VkDevice device, VkQueue queue,
VkCommandPool pool, VulkanSemaphoreManager* semaphoreManager, bool isProtected);
VkCommandPool pool, VulkanSemaphoreManager* semaphoreManager, bool isProtected,
uint32_t timerQueryIndex, VkQueryPool queryPool);
VulkanCommandBuffer(VulkanCommandBuffer const&) = delete;
VulkanCommandBuffer& operator=(VulkanCommandBuffer const&) = delete;
@@ -90,20 +92,14 @@ struct VulkanCommandBuffer {
void begin() noexcept;
fvkmemory::resource_ptr<VulkanSemaphore> submit();
inline void setComplete() {
mFenceStatus->setStatus(VK_SUCCESS);
}
void setComplete();
VkResult getStatus() {
return mFenceStatus->getStatus();
return mCmdbufState->getFenceStatus();
}
std::shared_ptr<VulkanCmdFence> getFenceStatus() const {
return mFenceStatus;
}
VkFence getVkFence() const {
return mFence;
std::shared_ptr<VulkanCmdBufferState> getState() const {
return mCmdbufState;
}
VkCommandBuffer buffer() const {
@@ -115,22 +111,31 @@ struct VulkanCommandBuffer {
}
private:
VkFence getVkFence() const {
return mFence;
}
static uint32_t sAgeCounter;
VulkanContext const& mContext;
uint8_t mMarkerCount;
bool const isProtected;
VkDevice mDevice;
VkQueue mQueue;
VkDevice const mDevice;
VkQueue const mQueue;
VulkanSemaphoreManager* mSemaphoreManager;
fvkutils::StaticVector<VkSemaphore, 2> mWaitSemaphores;
fvkutils::StaticVector<VkPipelineStageFlags, 2> mWaitSemaphoreStages;
VkCommandBuffer mBuffer;
VkCommandBuffer const mBuffer;
fvkmemory::resource_ptr<VulkanSemaphore> mSubmission;
VkFence mFence;
std::shared_ptr<VulkanCmdFence> mFenceStatus;
VkFence const mFence;
std::shared_ptr<VulkanCmdBufferState> mCmdbufState;
std::vector<fvkmemory::resource_ptr<Resource>> mResources;
uint32_t mAge;
uint32_t const mQueryBeginIndex;
uint32_t const mQueryEndIndex;
VkQueryPool const mQueryPool;
friend struct CommandBufferPool;
};
struct CommandBufferPool {
@@ -174,6 +179,8 @@ private:
std::vector<std::unique_ptr<VulkanCommandBuffer>> mBuffers;
int8_t mRecording;
VkQueryPool mQueryPool;
#if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS)
std::unique_ptr<VulkanGroupMarkers> mGroupMarkers;
#endif
@@ -199,7 +206,7 @@ private:
//
// - Allows off-thread queries of command buffer status.
// - Exposes an "updateFences" method that transfers current fence status into atomics.
// - Users can examine these atomic variables (see VulkanCmdFence) to determine status.
// - Users can examine these atomic variables (see VulkanCmdBufferState) to determine status.
// - We do this because vkGetFenceStatus must be called from the rendering thread.
//
class VulkanCommands {
@@ -230,12 +237,8 @@ public:
return sem;
}
VkFence getMostRecentFence() {
return mLastFence;
}
std::shared_ptr<VulkanCmdFence> getMostRecentFenceStatus() {
return mLastFenceStatus;
std::shared_ptr<VulkanCmdBufferState> getMostRecentBufferState() {
return mLastBufferState;
}
// Takes a semaphore that signals when the next flush can occur. Only one injected
@@ -276,8 +279,7 @@ private:
VkSemaphore mInjectedDependency = VK_NULL_HANDLE;
fvkmemory::resource_ptr<VulkanSemaphore> mLastSubmit;
VkFence mLastFence = VK_NULL_HANDLE;
std::shared_ptr<VulkanCmdFence> mLastFenceStatus;
std::shared_ptr<VulkanCmdBufferState> mLastBufferState;
VkPipelineStageFlags mInjectedDependencyWaitStage = 0;
};

View File

@@ -241,7 +241,6 @@ VulkanDriver::VulkanDriver(VulkanPlatform* platform, VulkanContext& context,
mReadPixels(mPlatform->getDevice()),
mDescriptorSetLayoutCache(mPlatform->getDevice(), &mResourceManager),
mDescriptorSetCache(mPlatform->getDevice(), &mResourceManager),
mQueryManager(mPlatform->getDevice()),
mExternalImageManager(platform, &mSamplerCache, &mYcbcrConversionCache, &mDescriptorSetCache,
&mDescriptorSetLayoutCache),
mIsSRGBSwapChainSupported(mPlatform->getCustomization().isSRGBSwapChainSupported),
@@ -338,8 +337,6 @@ void VulkanDriver::terminate() {
mDefaultRenderTarget = {};
mPipelineState = {};
mQueryManager.terminate();
mBlitter.terminate();
mReadPixels.terminate();
@@ -860,32 +857,33 @@ void VulkanDriver::createFenceR(Handle<HwFence> fh, utils::ImmutableCString&& ta
cmdbuf = &mCommands.get();
}
// Note at this point, the fence has already been constructed via createFenceS, so we just tag
// it with appropriate VulkanCmdFence, which is associated with the current, recording command
// it with appropriate VulkanCmdBufferState, which is associated with the current, recording command
// buffer.
auto fence = resource_ptr<VulkanFence>::cast(&mResourceManager, fh);
fence->setFence(cmdbuf->getFenceStatus());
fence->setCmdBufferState(cmdbuf->getState());
mResourceManager.associateHandle(fh.getId(), std::move(tag));
}
void VulkanDriver::createSyncR(Handle<HwSync> sh, utils::ImmutableCString&& tag) {
auto sync = resource_ptr<VulkanSync>::cast(&mResourceManager, sh);
VkFence fence = VK_NULL_HANDLE;
std::shared_ptr<VulkanCmdFence> fenceStatus;
std::shared_ptr<VulkanCmdBufferState> cmdbufState;
if (mCurrentRenderPass.commandBuffer) {
VulkanCommandBuffer* cmdBuff = mCurrentRenderPass.commandBuffer;
fence = cmdBuff->getVkFence();
fenceStatus = cmdBuff->getFenceStatus();
cmdbufState = cmdBuff->getState();
fence = cmdbufState->getVkFence();
// If we're currently recording, flush so that the fence only applies
// to commands already issued.
mCommands.flush();
} else {
fence = mCommands.getMostRecentFence();
fenceStatus = mCommands.getMostRecentFenceStatus();
cmdbufState = mCommands.getMostRecentBufferState();
fence = cmdbufState->getVkFence();
}
{
std::lock_guard<std::mutex> guard(sync->lock);
sync->sync = mPlatform->createSync(fence, fenceStatus);
sync->sync = mPlatform->createSync(fence, cmdbufState);
}
for (auto& cbData : sync->conversionCallbacks) {
@@ -1075,9 +1073,10 @@ 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 = mQueryManager.getNextQuery(&mResourceManager);
auto handle = mResourceManager.allocHandle<VulkanTimerQuery>();
auto query = resource_ptr<VulkanTimerQuery>::make(&mResourceManager, handle);
query.inc();
return Handle<VulkanTimerQuery>(query.id());
return handle;
}
Handle<HwDescriptorSetLayout> VulkanDriver::createDescriptorSetLayoutS() noexcept {
@@ -1134,7 +1133,6 @@ void VulkanDriver::destroyTimerQuery(Handle<HwTimerQuery> tqh) {
return;
}
auto vtq = resource_ptr<VulkanTimerQuery>::cast(&mResourceManager, tqh);
mQueryManager.clearQuery(vtq);
vtq.dec();
}
@@ -1197,7 +1195,6 @@ FenceStatus VulkanDriver::getFenceStatus(Handle<HwFence> const fh) {
FenceStatus VulkanDriver::fenceWait(FenceHandle const fh, uint64_t const timeout) {
// Even though this is a synchronous call, the fence handle must be (and stay) valid
assert_invariant(fh);
auto fence = resource_ptr<VulkanFence>::cast(&mResourceManager, fh);
// we have to take into account that the STL's wait_for() actually works with
@@ -1214,14 +1211,14 @@ FenceStatus VulkanDriver::fenceWait(FenceHandle const fh, uint64_t const timeout
until = now + nanoseconds(timeout);
}
auto const [cmdfence, canceled] = fence->wait(until);
if (!cmdfence || canceled) {
auto const [cmdbufState, canceled] = fence->wait(until);
if (!cmdbufState || canceled) {
return canceled ? FenceStatus::ERROR : FenceStatus::TIMEOUT_EXPIRED;
}
// now we are holding a reference to our VulkanCmdFence, so we know it can't
// now we are holding a reference to our VulkanCmdBufferState, so we know it can't
// disappear while we wait
return cmdfence->wait(mPlatform->getDevice(), timeout, until);
return cmdbufState->waitOnFence(mPlatform->getDevice(), timeout, until);
}
void VulkanDriver::getPlatformSync(Handle<HwSync> sh, CallbackHandler* handler,
@@ -1518,29 +1515,17 @@ void VulkanDriver::setupExternalImage(void* image) {
TimerQueryResult VulkanDriver::getTimerQueryValue(Handle<HwTimerQuery> tqh, uint64_t* elapsedTime) {
auto vtq = resource_ptr<VulkanTimerQuery>::cast(&mResourceManager, tqh);
if (!vtq->isCompleted()) {
if (!vtq->isComplete()) {
return TimerQueryResult::NOT_READY;
}
auto results = mQueryManager.getResult(vtq);
if (results.beginAvailable == 0 || results.endAvailable == 0) {
return TimerQueryResult::NOT_READY;
}
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. ";
*elapsedTime = 0;
return TimerQueryResult::ERROR;
}
uint64_t result = vtq->getResult();
assert_invariant(result != VulkanTimerQuery::UNKNOWN_QUERY_RESULT);
// 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;
*elapsedTime = uint64_t(float(end - begin) * period);
*elapsedTime = uint64_t(float(result) * period);
return TimerQueryResult::AVAILABLE;
}
@@ -2299,12 +2284,14 @@ void VulkanDriver::scissor(Viewport scissorBox) {
void VulkanDriver::beginTimerQuery(Handle<HwTimerQuery> tqh) {
auto vtq = resource_ptr<VulkanTimerQuery>::cast(&mResourceManager, tqh);
mQueryManager.beginQuery(&(mCommands.get()), vtq);
auto cmdbuf = &mCommands.get();
vtq->setBeginState(cmdbuf->getState());
}
void VulkanDriver::endTimerQuery(Handle<HwTimerQuery> tqh) {
auto vtq = resource_ptr<VulkanTimerQuery>::cast(&mResourceManager, tqh);
mQueryManager.endQuery(&(mCommands.get()), vtq);
auto cmdbuf = &mCommands.get();
vtq->setEndState(cmdbuf->getState());
}
void VulkanDriver::debugCommandBegin(CommandStream* cmds, bool synchronous, const char* methodName) noexcept {

View File

@@ -25,7 +25,6 @@
#include "VulkanHandles.h"
#include "VulkanMemory.h"
#include "VulkanPipelineCache.h"
#include "VulkanQueryManager.h"
#include "VulkanReadPixels.h"
#include "VulkanSamplerCache.h"
#include "VulkanSemaphoreManager.h"
@@ -159,7 +158,6 @@ private:
VulkanReadPixels mReadPixels;
VulkanDescriptorSetLayoutCache mDescriptorSetLayoutCache;
VulkanDescriptorSetCache mDescriptorSetCache;
VulkanQueryManager mQueryManager;
VulkanExternalImageManager mExternalImageManager;
// This maps a VulkanSwapchain to a native swapchain. VulkanSwapchain should have a copy of the

View File

@@ -1,106 +0,0 @@
/*
* 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.";
return {};
}
std::pair<uint32_t, uint32_t> queryIndices;
size_t const firstUnused = unused.firstSetBit();
{
std::unique_lock<utils::Mutex> lock(mMutex);
mUsed.set(firstUnused);
queryIndices = { firstUnused * 2, firstUnused * 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

@@ -1,63 +0,0 @@
/*
* 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

View File

@@ -971,7 +971,7 @@ SwapChainPtr VulkanPlatform::createSwapChain(void* nativeWindow, uint64_t flags,
}
Platform::Sync* VulkanPlatform::createSync(VkFence fence,
std::shared_ptr<VulkanCmdFence> fenceStatus) noexcept {
std::shared_ptr<VulkanCmdBufferState> fenceStatus) noexcept {
return new VulkanSync{.fence = fence, .fenceStatus = fenceStatus};
}

View File

@@ -407,7 +407,7 @@ bool VulkanPlatformAndroid::convertSyncToFd(Platform::Sync* sync, int* fd) const
VulkanSync& vulkanSync = static_cast<VulkanSync&>(*sync);
assert_invariant(vulkanSync.fenceStatus);
if (vulkanSync.fenceStatus->getStatus() == VK_SUCCESS) {
if (vulkanSync.fenceStatus->getFenceStatus() == VK_SUCCESS) {
// We've already signaled; return -1 so that operations will proceed
// immediately. Also, signal that fence conversion was successful.
*fd = -1;