From ad27d48fd309e1e8940e1a23a95ec992f07c481c Mon Sep 17 00:00:00 2001 From: Doris Wu Date: Tue, 11 Nov 2025 16:56:06 +0800 Subject: [PATCH] buffer update opt: refactor UboManager and introduce FenceManager (#9418) --- filament/src/details/UboManager.cpp | 288 ++++++++++++++-------------- filament/src/details/UboManager.h | 76 ++++++-- filament/test/CMakeLists.txt | 2 + filament/test/MockDriver.h | 149 ++++++++++++++ filament/test/test_FenceManager.cpp | 158 +++++++++++++++ 5 files changed, 513 insertions(+), 160 deletions(-) create mode 100644 filament/test/MockDriver.h create mode 100644 filament/test/test_FenceManager.cpp diff --git a/filament/src/details/UboManager.cpp b/filament/src/details/UboManager.cpp index 4986e61055..31f60ad244 100644 --- a/filament/src/details/UboManager.cpp +++ b/filament/src/details/UboManager.cpp @@ -20,9 +20,11 @@ #include "details/BufferAllocator.h" #include - +#include #include +#include + namespace filament { namespace { @@ -33,6 +35,80 @@ using AllocationId = BufferAllocator::AllocationId; using allocation_size_t = BufferAllocator::allocation_size_t; } // anonymous namespace +// ------------------------------------------------------------------------------------------------ +// FenceManager +// ------------------------------------------------------------------------------------------------ + +void UboManager::FenceManager::track(DriverApi& driver, std::unordered_set&& allocationIds) { + if (allocationIds.empty()) { + return; + } + mFenceAllocationList.emplace_back(driver.createFence(), std::move(allocationIds)); +} + +void UboManager::FenceManager::reclaimCompletedResources(DriverApi& driver, + std::function const& onReclaimed) { + uint32_t signaledCount = 0; + bool seenSignaledFence = false; + + // Iterate from the newest fence to the oldest. + for (auto it = mFenceAllocationList.rbegin(); it != mFenceAllocationList.rend(); ++it) { + const Handle& fence = it->first; + const FenceStatus status = driver.getFenceStatus(fence); + + // If we have already seen a signaled fence, we can assume all older fences + // are also complete, regardless of their reported status (e.g., TIMEOUT_EXPIRED). + // This is guaranteed by the in-order execution of GPU command queues. + if (seenSignaledFence) { + signaledCount++; +#ifndef NDEBUG + if (UTILS_UNLIKELY(status != FenceStatus::CONDITION_SATISFIED)) { + LOG(WARNING) << "A fence is either in an error state or hasn't signaled, but a newer " + "fence has. Will release the resource anyway."; + } +#endif + continue; + } + + if (status == FenceStatus::CONDITION_SATISFIED) { + seenSignaledFence = true; + signaledCount++; + } + } + + if (signaledCount == 0) { + // No fences have completed, nothing to do. + return; + } + + auto firstToKeep = mFenceAllocationList.begin() + signaledCount; + + // Invoke the callback for all resources protected by completed fences. + for (auto it = mFenceAllocationList.begin(); it != firstToKeep; ++it) { + for (const AllocationId& id : it->second) { + onReclaimed(id); + } + // Destroy the fence handle as it's no longer needed. + driver.destroyFence(std::move(it->first)); + } + + mFenceAllocationList.erase(mFenceAllocationList.begin(), firstToKeep); +} + +void UboManager::FenceManager::reset(DriverApi& driver) { + for (auto& [fence, _] : mFenceAllocationList) { + if (fence) { + driver.destroyFence(std::move(fence)); + } + } + mFenceAllocationList.clear(); +} + + +// ------------------------------------------------------------------------------------------------ +// UboManager +// ------------------------------------------------------------------------------------------------ + UboManager::UboManager(DriverApi& driver, allocation_size_t defaultSlotSizeInBytes, allocation_size_t defaultTotalSizeInBytes) : mAllocator(defaultTotalSizeInBytes, defaultSlotSizeInBytes) { @@ -41,17 +117,16 @@ UboManager::UboManager(DriverApi& driver, allocation_size_t defaultSlotSizeInByt void UboManager::beginFrame(DriverApi& driver, const std::unordered_map>& materialInstances) { + FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT); // Check finished frames and decrement GPU count accordingly. - checkFenceAndUnlockSlots(driver); + mFenceManager.reclaimCompletedResources(driver, + [this](AllocationId id) { mAllocator.releaseGpu(id); }); // Actually merge the slots. mAllocator.releaseFreeSlots(); // Traverse all MIs and see which of them need slot allocation. - AllocationResult allocationResult = - updateMaterialInstanceAllocations(materialInstances, ON_DEMAND); - - if (allocationResult == SUCCESS) { + if (allocateOnDemand(materialInstances) == SUCCESS) { // No need to grow the buffer, so we can just map the buffer for writing and return. mMemoryMappedBufferHandle = driver.mapBuffer(mUbHandle, 0, mUboSize, MapBufferAccessFlags::WRITE_BIT, "UboManager"); @@ -64,24 +139,20 @@ void UboManager::beginFrame(DriverApi& driver, reallocate(driver, requiredSize); // Allocate slots for each MI on the new Ubo. - UTILS_UNUSED_IN_RELEASE const AllocationResult needReallocationAgain = - updateMaterialInstanceAllocations(materialInstances, ALWAYS); - assert_invariant(needReallocationAgain != REALLOCATION_REQUIRED); + allocateAllInstances(materialInstances); // Map the buffer so that we can write to it mMemoryMappedBufferHandle = driver.mapBuffer(mUbHandle, 0, mUboSize, MapBufferAccessFlags::WRITE_BIT, "UboManager"); - // Migrate all MI data to the new allocated slots. + // Invalidate the migrated MIs, so that next commit() call must be triggered. for (const auto& materialInstance : materialInstances) { - materialInstance.second.forEach([this, &driver](const FMaterialInstance* mi) { + materialInstance.second.forEach([](const FMaterialInstance* mi) { if (!mi->isUsingUboBatching()) { return; } - const AllocationId allocationId = mi->getAllocationId(); - assert_invariant(BufferAllocator::isValid(allocationId)); - updateSlot(driver, allocationId, mi->getUniformBuffer().toBufferDescriptor(driver)); + mi->getUniformBuffer().invalidate(); }); } } @@ -113,17 +184,11 @@ void UboManager::endFrame(DriverApi& driver, }); } - mFenceAllocationList.emplace_back( driver.createFence(), std::move(allocationIds) ); + mFenceManager.track(driver, std::move(allocationIds)); } void UboManager::terminate(DriverApi& driver) { - for (auto& [fence, _]: mFenceAllocationList) { - if (fence) { - driver.destroyFence(std::move(fence)); - } - } - mFenceAllocationList.clear(); - + mFenceManager.reset(driver); driver.destroyBufferObject(mUbHandle); } @@ -142,6 +207,66 @@ void UboManager::retireSlot(BufferAllocator::AllocationId id) { mAllocator.retire(id); } +UboManager::AllocationResult UboManager::allocateOnDemand( + const std::unordered_map>& + materialInstances) { + // Collect all MIs that need allocation into two groups. + std::vector newInstances; + std::vector existingInstances; + for (const auto& [_, miList] : materialInstances) { + miList.forEach([&](FMaterialInstance* mi) { + if (!mi->isUsingUboBatching()) { + return; + } + if (BufferAllocator::isValid(mi->getAllocationId())) { + existingInstances.push_back(mi); + } else { + newInstances.push_back(mi); + } + }); + } + + bool reallocationNeeded = false; + + // Pass 1: Allocate slots for new material instances (that don't have a slot yet). + for (FMaterialInstance* mi : newInstances) { + auto [newId, newOffset] = mAllocator.allocate(mi->getUniformBuffer().getSize()); + mi->assignUboAllocation(mUbHandle, newId, newOffset); + if (!BufferAllocator::isValid(newId)) { + reallocationNeeded = true; + } + } + + // Pass 2: Allocate slots for existing material instances that need to be orphaned. + for (FMaterialInstance* mi : existingInstances) { + if (mi->getUniformBuffer().isDirty() && mAllocator.isLockedByGpu(mi->getAllocationId())) { + mAllocator.retire(mi->getAllocationId()); + auto [newId, newOffset] = mAllocator.allocate(mi->getUniformBuffer().getSize()); + mi->assignUboAllocation(mUbHandle, newId, newOffset); + if (!BufferAllocator::isValid(newId)) { + reallocationNeeded = true; + } + } + } + + return reallocationNeeded ? REALLOCATION_REQUIRED : SUCCESS; +} + +void UboManager::allocateAllInstances( + const std::unordered_map>& + materialInstances) { + for (const auto& [_, miList] : materialInstances) { + miList.forEach([this](FMaterialInstance* mi) { + if (!mi->isUsingUboBatching()) { + return; + } + auto [newId, newOffset] = mAllocator.allocate(mi->getUniformBuffer().getSize()); + assert_invariant(BufferAllocator::isValid(newId)); + mi->assignUboAllocation(mUbHandle, newId, newOffset); + }); + } +} + allocation_size_t UboManager::getTotalSize() const noexcept { return mUboSize; } @@ -150,128 +275,13 @@ allocation_size_t UboManager::getAllocationOffset(AllocationId id) const { return mAllocator.getAllocationOffset(id); } -void UboManager::checkFenceAndUnlockSlots(DriverApi& driver) { - uint32_t signaledCount = 0; - bool seenSignaledFence = false; - - // Iterate from the newest fence to the oldest. - for (auto it = mFenceAllocationList.rbegin(); it != mFenceAllocationList.rend(); ++it) { - const Handle& fence = it->first; - const FenceStatus status = driver.getFenceStatus(fence); - - // If we have already seen a signaled fence, we can assume all older fences - // are also complete, regardless of their reported status (e.g., TIMEOUT_EXPIRED). - // This is guaranteed by the in-order execution of GPU command queues. - if (seenSignaledFence) { - signaledCount++; -#ifndef NDEBUG - if (UTILS_UNLIKELY(status != FenceStatus::CONDITION_SATISFIED)) { - LOG(WARNING) << "A fence is either an error or hasn't signaled, but the new fence has " - "been " - "signaled. Will release the resource anyways." - << io::endl; - } -#endif - continue; - } - - if (status == FenceStatus::CONDITION_SATISFIED) { - seenSignaledFence = true; - signaledCount++; - } - } - - if (signaledCount == 0) { - // No fences have completed, nothing to do. - return; - } - - auto firstToKeep = mFenceAllocationList.begin() + signaledCount; - - // Release GPU locks for all completed fences. - for (auto it = mFenceAllocationList.begin(); it != firstToKeep; ++it) { - for (const AllocationId& id: it->second) { - mAllocator.releaseGpu(id); - } - // Destroy the fence handle as it's no longer needed. - driver.destroyFence(std::move(it->first)); - } - - mFenceAllocationList.erase(mFenceAllocationList.begin(), firstToKeep); -} - -UboManager::AllocationResult UboManager::updateMaterialInstanceAllocations( - const std::unordered_map>& - materialInstances, - AllocationMode allocationMode) { - AllocationResult result = SUCCESS; - for (const auto& materialInstance: materialInstances) { - materialInstance.second.forEach([this, &result, allocationMode](FMaterialInstance* mi) { - if (tryAllocateMaterialInstanceSlot(mi, mAllocator, mUbHandle, allocationMode) == - REALLOCATION_REQUIRED) { - result = REALLOCATION_REQUIRED; - } - }); - } - - return result; -} - -UboManager::AllocationResult UboManager::tryAllocateMaterialInstanceSlot(FMaterialInstance* mi, - BufferAllocator& allocator, const Handle& ubHandle, - AllocationMode allocationMode) { - if (!mi->isUsingUboBatching()) { - return SUCCESS; - } - - const AllocationId id = mi->getAllocationId(); - auto allocateAndAssign = [&](AllocationId originalId) -> AllocationResult { - auto [newId, newOffset] = allocator.allocate(mi->getUniformBuffer().getSize()); - // Special handling for instances that were previously UNALLOCATED: - // If the new allocation also fails, keep it UNALLOCATED to signal initial failure. - if (originalId == BufferAllocator::UNALLOCATED && - newId == BufferAllocator::REALLOCATION_REQUIRED) { - newId = BufferAllocator::UNALLOCATED; - } - - // Even if the allocation is not valid, we need to set it to let the following - // process knows. - mi->assignUboAllocation(ubHandle, newId, newOffset); - - if (!BufferAllocator::isValid(newId)) { - return REALLOCATION_REQUIRED; - } - return SUCCESS; - }; - - if (!BufferAllocator::isValid(id) || allocationMode == ALWAYS) { - // The material instance is first time being allocated (if !isValid) - // or is being forcibly re-allocated (if ALWAYS). - return allocateAndAssign(id); - } - - if (mi->getUniformBuffer().isDirty() && allocator.isLockedByGpu(id)) { - // If the uniform buffer is updated and the slot is still being locked by GPU, - // we will need to allocate a new slot to write the updated content. - // This is known as "orphaning". - allocator.retire(id); - return allocateAndAssign(id); - } - // Else, the slot is valid and no need to allocate a new slot. - - return SUCCESS; -} - void UboManager::reallocate(DriverApi& driver, allocation_size_t requiredSize) { + FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT); if (mUbHandle) { driver.destroyBufferObject(mUbHandle); } - for (auto& [fence, allocations]: mFenceAllocationList) { - driver.destroyFence(std::move(fence)); - } - mFenceAllocationList.clear(); - + mFenceManager.reset(driver); mAllocator.reset(requiredSize); mUboSize = requiredSize; mUbHandle = driver.createBufferObject(requiredSize, BufferObjectBinding::UNIFORM, diff --git a/filament/src/details/UboManager.h b/filament/src/details/UboManager.h index db1b14a4a5..2845bc46ef 100644 --- a/filament/src/details/UboManager.h +++ b/filament/src/details/UboManager.h @@ -25,7 +25,9 @@ #include #include +#include #include +#include namespace filament { @@ -40,6 +42,47 @@ class FMaterialInstance; // undefined behavior. class UboManager { public: + // This utility tracks resources that are in-use by the GPU across multiple frames. + // It uses backend fences to determine when the GPU has finished with a set of resources, + // allowing them to be safely reclaimed or reused. + // + // The typical usage is to `track()` a set of resources at the end of a frame and + // call `reclaimCompletedResources()` at the beginning of a future frame to free up + // resources from completed GPU work. + // + // This class is designed for single-threaded access. + class FenceManager { + public: + using AllocationId = BufferAllocator::AllocationId; + + FenceManager() = default; + ~FenceManager() = default; + + FenceManager(FenceManager const&) = delete; + FenceManager(FenceManager&&) = delete; + + + // Creates a new fence to track a set of allocation IDs for the current frame. + // This marks the beginning of GPU's usage of these resources. + void track(backend::DriverApi& driver, std::unordered_set&& allocationIds); + + + // Checks all tracked fences and invokes a callback for resources associated with + // completed fences. This should be called once per frame. + void reclaimCompletedResources(backend::DriverApi& driver, + std::function const& onReclaimed); + + // Destroys all tracked fences and clears the tracking list. + // This is used for cleanup during termination or major reallocations. + void reset(backend::DriverApi& driver); + + private: + // Not ideal, but we need to know which slots to decrement gpuUseCount for each frame. + using FenceAndAllocations = + std::pair, std::unordered_set>; + std::vector mFenceAllocationList; + }; + explicit UboManager(backend::DriverApi& driver, BufferAllocator::allocation_size_t defaultSlotSizeInBytes, BufferAllocator::allocation_size_t defaultTotalSizeInBytes); @@ -82,30 +125,25 @@ public: private: constexpr static float BUFFER_SIZE_GROWTH_MULTIPLIER = 1.5f; - // Query the offset by the allocation id. - [[nodiscard]] BufferAllocator::allocation_size_t getAllocationOffset( - BufferAllocator::AllocationId id) const; - - void checkFenceAndUnlockSlots(backend::DriverApi& driver); - enum AllocationResult { SUCCESS, REALLOCATION_REQUIRED }; - enum AllocationMode { - ON_DEMAND, - ALWAYS - }; + // Query the offset by the allocation id. + [[nodiscard]] BufferAllocator::allocation_size_t getAllocationOffset( + BufferAllocator::AllocationId id) const; - AllocationResult updateMaterialInstanceAllocations( + AllocationResult allocateOnDemand( const std::unordered_map>& - materialInstances, - AllocationMode allocationMode); - static AllocationResult tryAllocateMaterialInstanceSlot(FMaterialInstance* mi, - BufferAllocator& allocator, const backend::Handle& ubHandle, - AllocationMode allocationMode); + materialInstances); + + void allocateAllInstances( + const std::unordered_map>& + materialInstances); + void reallocate(backend::DriverApi& driver, BufferAllocator::allocation_size_t requiredSize); + BufferAllocator::allocation_size_t calculateRequiredSize( const std::unordered_map>& materialInstances); @@ -114,11 +152,7 @@ private: backend::MemoryMappedBufferHandle mMemoryMappedBufferHandle; BufferAllocator::allocation_size_t mUboSize{}; - // Not ideal, but we need to know which slots to decrement gpuUseCount for each frame. - using FenceAllocationList = std::vector, - std::unordered_set>>; - FenceAllocationList mFenceAllocationList; - + FenceManager mFenceManager; BufferAllocator mAllocator; }; diff --git a/filament/test/CMakeLists.txt b/filament/test/CMakeLists.txt index e735fae713..cf01040942 100644 --- a/filament/test/CMakeLists.txt +++ b/filament/test/CMakeLists.txt @@ -44,10 +44,12 @@ list(APPEND RESGEN_SOURCE ${DUMMY_SRC}) # away in Release builds if (TNT_DEV) add_executable(test_${TARGET} + MockDriver.h filament_AtlasAllocator_test.cpp test_BufferAllocator.cpp test_BufferAllocatorStress.cpp test_CircularQueue.cpp + test_FenceManager.cpp filament_test_exposure.cpp filament_rendering_test.cpp filament_bimap_test.cpp diff --git a/filament/test/MockDriver.h b/filament/test/MockDriver.h new file mode 100644 index 0000000000..d24058f929 --- /dev/null +++ b/filament/test/MockDriver.h @@ -0,0 +1,149 @@ +/* + * 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_TEST_MOCK_DRIVER_H +#define TNT_FILAMENT_TEST_MOCK_DRIVER_H + +#include +#include + +#include "private/backend/CommandStream.h" +#include "private/backend/Driver.h" + +using namespace filament; +using namespace filament::backend; + +using ::testing::_; +using ::testing::InSequence; +using ::testing::NiceMock; +using ::testing::Return; + +// A mock implementation of the backend::Driver for unit testing. +// +// This mock is primarily designed for testing synchronous APIs and functions that +// return a value immediately (those with the "S" suffix, like `createBufferObjectS`). +// It does not fully support asynchronous command stream behavior, so its use for +// testing complex rendering logic is limited. +class MockDriver : public Driver { +public: + MockDriver() = default; + ~MockDriver() override = default; + + uint64_t nextFakeHandle = 1; + + MOCK_METHOD(void, terminate, (), (override)); + MOCK_METHOD(backend::StreamHandle, createStreamNative, + (void* stream, utils::ImmutableCString tag), (override)); + MOCK_METHOD(backend::StreamHandle, createStreamAcquired, (utils::ImmutableCString tag), + (override)); + MOCK_METHOD(void, setAcquiredImage, + (backend::StreamHandle stream, void* image, const math::mat3f& transform, + backend::CallbackHandler* handler, backend::StreamCallback cb, void* userData), + (override)); + MOCK_METHOD(void, setStreamDimensions, + (backend::StreamHandle stream, uint32_t width, uint32_t height), (override)); + MOCK_METHOD(int64_t, getStreamTimestamp, (backend::StreamHandle stream), (override)); + MOCK_METHOD(void, updateStreams, (backend::DriverApi * driver), (override)); + MOCK_METHOD(backend::FenceStatus, getFenceStatus, (backend::FenceHandle fh), (override)); + MOCK_METHOD(backend::FenceStatus, fenceWait, (backend::FenceHandle fh, uint64_t timeout), + (override)); + MOCK_METHOD(void, getPlatformSync, + (backend::SyncHandle sh, backend::CallbackHandler* handler, + backend::Platform::SyncCallback cb, void* userData), + (override)); + MOCK_METHOD(bool, isTextureFormatSupported, (backend::TextureFormat format), (override)); + MOCK_METHOD(bool, isTextureSwizzleSupported, (), (override)); + MOCK_METHOD(bool, isTextureFormatMipmappable, (backend::TextureFormat format), (override)); + MOCK_METHOD(bool, isRenderTargetFormatSupported, (backend::TextureFormat format), (override)); + MOCK_METHOD(bool, isFrameBufferFetchSupported, (), (override)); + MOCK_METHOD(bool, isFrameBufferFetchMultiSampleSupported, (), (override)); + MOCK_METHOD(bool, isFrameTimeSupported, (), (override)); + MOCK_METHOD(bool, isAutoDepthResolveSupported, (), (override)); + MOCK_METHOD(bool, isSRGBSwapChainSupported, (), (override)); + MOCK_METHOD(bool, isMSAASwapChainSupported, (uint32_t samples), (override)); + MOCK_METHOD(bool, isProtectedContentSupported, (), (override)); + MOCK_METHOD(bool, isStereoSupported, (), (override)); + MOCK_METHOD(bool, isParallelShaderCompileSupported, (), (override)); + MOCK_METHOD(bool, isDepthStencilResolveSupported, (), (override)); + MOCK_METHOD(bool, isDepthStencilBlitSupported, (backend::TextureFormat format), (override)); + MOCK_METHOD(bool, isProtectedTexturesSupported, (), (override)); + MOCK_METHOD(bool, isDepthClampSupported, (), (override)); + MOCK_METHOD(uint8_t, getMaxDrawBuffers, (), (override)); + MOCK_METHOD(size_t, getMaxUniformBufferSize, (), (override)); + MOCK_METHOD(size_t, getMaxTextureSize, (backend::SamplerType target), (override)); + MOCK_METHOD(size_t, getMaxArrayTextureLayers, (), (override)); + MOCK_METHOD(math::float2, getClipSpaceParams, (), (override)); + MOCK_METHOD(void, setupExternalImage2, (backend::Platform::ExternalImageHandleRef image), + (override)); + MOCK_METHOD(void, setupExternalImage, (void* image), (override)); + MOCK_METHOD(backend::TimerQueryResult, getTimerQueryValue, + (backend::TimerQueryHandle tqh, uint64_t* elapsedTime), (override)); + MOCK_METHOD(bool, isWorkaroundNeeded, (backend::Workaround workaround), (override)); + MOCK_METHOD(backend::FeatureLevel, getFeatureLevel, (), (override)); + MOCK_METHOD(size_t, getUniformBufferOffsetAlignment, (), (override)); + MOCK_METHOD(bool, isCompositorTimingSupported, (), (override)); + MOCK_METHOD(bool, queryFrameTimestamps, (SwapChainHandle, uint64_t, FrameTimestamps*), (override)); + MOCK_METHOD(bool, queryCompositorTiming, (SwapChainHandle, CompositorTiming*), (override)); + MOCK_METHOD(void, fenceCancel, (backend::FenceHandle), (override)); + + + ShaderModel getShaderModel() const noexcept final { return ShaderModel::DESKTOP; } + utils::FixedCapacityVector getShaderLanguages( + ShaderLanguage preferredLanguage) const noexcept final { + return { + ShaderLanguage::ESSL3, + ShaderLanguage::ESSL1, + ShaderLanguage::SPIRV, + ShaderLanguage::MSL, + ShaderLanguage::METAL_LIBRARY, + ShaderLanguage::WGSL, + }; + } + void debugCommandBegin(CommandStream* cmds, bool synchronous, + const char* methodName) noexcept override {} + void debugCommandEnd(CommandStream* cmds, bool synchronous, + const char* methodName) noexcept override {} + void purge() noexcept override {} + + template + friend class ConcreteDispatcher; + +#define DECL_DRIVER_API(methodName, paramsDecl, params) \ + UTILS_ALWAYS_INLINE inline void methodName(paramsDecl) {} +#define DECL_DRIVER_API_SYNCHRONOUS(RetType, methodName, paramsDecl, params) +#define DECL_DRIVER_API_RETURN(RetType, methodName, paramsDecl, params) \ + RetType methodName##S() noexcept override { \ + return RetType((RetType::HandleId) nextFakeHandle++); \ + } \ + UTILS_ALWAYS_INLINE inline void methodName##R(RetType, paramsDecl) {} + +#include "private/backend/DriverAPI.inc" + + Dispatcher getDispatcher() const noexcept override { + Dispatcher dispatcher; + // Currently we don't have a good way to mock the functions for asynchronous backend API. + // We set all API to nullptr to let the users know it doesn't work. +#define DECL_DRIVER_API(methodName, paramsDecl, params) dispatcher.methodName##_ = nullptr; +#define DECL_DRIVER_API_RETURN(RetType, methodName, paramsDecl, params) \ + dispatcher.methodName##_ = nullptr; +#define DECL_DRIVER_API_SYNCHRONOUS(RetType, methodName, paramsDecl, params) +#include "private/backend/DriverAPI.inc" + + return dispatcher; + } +}; + +#endif // TNT_FILAMENT_TEST_MOCK_DRIVER_H diff --git a/filament/test/test_FenceManager.cpp b/filament/test/test_FenceManager.cpp new file mode 100644 index 0000000000..b5065dacd3 --- /dev/null +++ b/filament/test/test_FenceManager.cpp @@ -0,0 +1,158 @@ +/* + * 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 "MockDriver.h" + +#include +#include + +#include "details/UboManager.h" +#include "private/backend/CommandBufferQueue.h" + +#include "private/backend/CommandStream.h" +#include "private/backend/Driver.h" + +namespace { + +using namespace filament; +using namespace backend; + +using ::testing::_; +using ::testing::Return; + +static constexpr size_t CONFIG_MIN_COMMAND_BUFFERS_SIZE = 1 * 1024 * 1024; +static constexpr size_t CONFIG_COMMAND_BUFFERS_SIZE = 3 * CONFIG_MIN_COMMAND_BUFFERS_SIZE; + +class FenceManagerTest : public ::testing::Test { +protected: + FenceManagerTest() + : mCommandBufferQueue(CONFIG_MIN_COMMAND_BUFFERS_SIZE, CONFIG_COMMAND_BUFFERS_SIZE, + /*mPaused=*/false), + mCommandStream(mMockDriver, mCommandBufferQueue.getCircularBuffer()), + mDriverApi(mCommandStream) {} + + MockDriver mMockDriver; + CommandBufferQueue mCommandBufferQueue; + CommandStream mCommandStream; + DriverApi& mDriverApi; + UboManager::FenceManager mFenceManager; +}; + +TEST_F(FenceManagerTest, TrackEmptySet) { + mFenceManager.track(mDriverApi, {}); + EXPECT_EQ(mMockDriver.nextFakeHandle, 1); // No fence allocation +} + +TEST_F(FenceManagerTest, TrackNonEmptySet) { + mFenceManager.track(mDriverApi, { 1, 2, 3 }); + EXPECT_EQ(mMockDriver.nextFakeHandle, 2); +} + +TEST_F(FenceManagerTest, ReclaimWithNoFences) { + std::vector reclaimedIds; + mFenceManager.reclaimCompletedResources(mDriverApi, + [&reclaimedIds](BufferAllocator::AllocationId id) { reclaimedIds.push_back(id); }); + + EXPECT_TRUE(reclaimedIds.empty()); +} + +TEST_F(FenceManagerTest, ReclaimWhenFenceNotSignaled) { + mFenceManager.track(mDriverApi, { 10 }); + + EXPECT_CALL(mMockDriver, getFenceStatus(Handle(1))) + .WillOnce(Return(FenceStatus::TIMEOUT_EXPIRED)); + + std::unordered_set reclaimedIds; + mFenceManager.reclaimCompletedResources(mDriverApi, + [&reclaimedIds](BufferAllocator::AllocationId id) { reclaimedIds.insert(id); }); + + EXPECT_TRUE(reclaimedIds.empty()); +} + +TEST_F(FenceManagerTest, ReclaimWhenFenceSignaled) { + mFenceManager.track(mDriverApi, { 10, 20 }); + EXPECT_EQ(mMockDriver.nextFakeHandle, 2); + + EXPECT_CALL(mMockDriver, getFenceStatus(Handle(1))) + .WillOnce(Return(FenceStatus::CONDITION_SATISFIED)); + + std::unordered_set reclaimedIds; + mFenceManager.reclaimCompletedResources(mDriverApi, + [&reclaimedIds](BufferAllocator::AllocationId id) { reclaimedIds.insert(id); }); + + // Verify that the correct IDs were reclaimed. + ASSERT_EQ(reclaimedIds.size(), 2); + EXPECT_TRUE(reclaimedIds.contains(10)); + EXPECT_TRUE(reclaimedIds.contains(20)); + + // Verify that the fence is no longer tracked by calling reclaim again. + reclaimedIds.clear(); + EXPECT_CALL(mMockDriver, getFenceStatus(_)).Times(0); // No fences left to check. + mFenceManager.reclaimCompletedResources(mDriverApi, + [&reclaimedIds](BufferAllocator::AllocationId id) { reclaimedIds.insert(id); }); + EXPECT_TRUE(reclaimedIds.empty()); +} + +TEST_F(FenceManagerTest, ReclaimMultipleFencesPartial) { + // Frame 1 + mFenceManager.track(mDriverApi, { 1 }); + EXPECT_EQ(mMockDriver.nextFakeHandle, 2); + + // Frame 2 + mFenceManager.track(mDriverApi, { 2 }); + EXPECT_EQ(mMockDriver.nextFakeHandle, 3); + + // Frame 3 + mFenceManager.track(mDriverApi, { 3 }); + EXPECT_EQ(mMockDriver.nextFakeHandle, 4); + + // Now, reclaim. Assume fence 1 and 2 are done, but 3 is not. + // The implementation iterates from newest to oldest. + EXPECT_CALL(mMockDriver, getFenceStatus(Handle(3))) + .WillOnce(Return(FenceStatus::TIMEOUT_EXPIRED)); + EXPECT_CALL(mMockDriver, getFenceStatus(Handle(2))) + .WillOnce(Return(FenceStatus::CONDITION_SATISFIED)); + EXPECT_CALL(mMockDriver, getFenceStatus(Handle(1))) + .WillOnce(Return(FenceStatus::CONDITION_SATISFIED)); + + std::vector reclaimedIds; + mFenceManager.reclaimCompletedResources(mDriverApi, + [&reclaimedIds](BufferAllocator::AllocationId id) { reclaimedIds.push_back(id); }); + + // Verify that resources from the first two frames were reclaimed. + ASSERT_EQ(reclaimedIds.size(), 2); + EXPECT_EQ(reclaimedIds[0], 1); + EXPECT_EQ(reclaimedIds[1], 2); +} + +TEST_F(FenceManagerTest, Reset) { + mFenceManager.track(mDriverApi, { 10 }); + EXPECT_EQ(mMockDriver.nextFakeHandle, 2); + + mFenceManager.track(mDriverApi, { 20 }); + EXPECT_EQ(mMockDriver.nextFakeHandle, 3); + + mFenceManager.reset(mDriverApi); + + // After reset, reclaiming should do nothing. + std::vector reclaimedIds; + EXPECT_CALL(mMockDriver, getFenceStatus(_)).Times(0); + mFenceManager.reclaimCompletedResources(mDriverApi, + [&reclaimedIds](BufferAllocator::AllocationId id) { reclaimedIds.push_back(id); }); + EXPECT_TRUE(reclaimedIds.empty()); +} + +} // namespace anonymous