buffer update opt: refactor UboManager and introduce FenceManager (#9418)

This commit is contained in:
Doris Wu
2025-11-11 16:56:06 +08:00
committed by GitHub
parent 3a503976c8
commit ad27d48fd3
5 changed files with 513 additions and 160 deletions

View File

@@ -20,9 +20,11 @@
#include "details/BufferAllocator.h"
#include <backend/DriverEnums.h>
#include <private/utils/Tracing.h>
#include <utils/Logger.h>
#include <vector>
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<AllocationId>&& allocationIds) {
if (allocationIds.empty()) {
return;
}
mFenceAllocationList.emplace_back(driver.createFence(), std::move(allocationIds));
}
void UboManager::FenceManager::reclaimCompletedResources(DriverApi& driver,
std::function<void(AllocationId)> 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<HwFence>& 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<const FMaterial*, ResourceList<FMaterialInstance>>& 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<const FMaterial*, ResourceList<FMaterialInstance>>&
materialInstances) {
// Collect all MIs that need allocation into two groups.
std::vector<FMaterialInstance*> newInstances;
std::vector<FMaterialInstance*> 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<const FMaterial*, ResourceList<FMaterialInstance>>&
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<HwFence>& 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<const FMaterial*, ResourceList<FMaterialInstance>>&
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<HwBufferObject>& 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,

View File

@@ -25,7 +25,9 @@
#include <backend/Handle.h>
#include <private/backend/DriverApi.h>
#include <functional>
#include <unordered_set>
#include <vector>
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<AllocationId>&& 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<void(AllocationId)> 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<backend::Handle<backend::HwFence>, std::unordered_set<AllocationId>>;
std::vector<FenceAndAllocations> 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<const FMaterial*, ResourceList<FMaterialInstance>>&
materialInstances,
AllocationMode allocationMode);
static AllocationResult tryAllocateMaterialInstanceSlot(FMaterialInstance* mi,
BufferAllocator& allocator, const backend::Handle<backend::HwBufferObject>& ubHandle,
AllocationMode allocationMode);
materialInstances);
void allocateAllInstances(
const std::unordered_map<const FMaterial*, ResourceList<FMaterialInstance>>&
materialInstances);
void reallocate(backend::DriverApi& driver, BufferAllocator::allocation_size_t requiredSize);
BufferAllocator::allocation_size_t calculateRequiredSize(
const std::unordered_map<const FMaterial*, ResourceList<FMaterialInstance>>&
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::pair<backend::Handle<backend::HwFence>,
std::unordered_set<BufferAllocator::AllocationId>>>;
FenceAllocationList mFenceAllocationList;
FenceManager mFenceManager;
BufferAllocator mAllocator;
};

View File

@@ -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

149
filament/test/MockDriver.h Normal file
View File

@@ -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 <gmock/gmock.h>
#include <gtest/gtest.h>
#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<ShaderLanguage> 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<typename T>
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

View File

@@ -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 <gmock/gmock.h>
#include <gtest/gtest.h>
#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<UboManager::FenceManager::AllocationId> 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<HwFence>(1)))
.WillOnce(Return(FenceStatus::TIMEOUT_EXPIRED));
std::unordered_set<UboManager::FenceManager::AllocationId> 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<HwFence>(1)))
.WillOnce(Return(FenceStatus::CONDITION_SATISFIED));
std::unordered_set<UboManager::FenceManager::AllocationId> 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<HwFence>(3)))
.WillOnce(Return(FenceStatus::TIMEOUT_EXPIRED));
EXPECT_CALL(mMockDriver, getFenceStatus(Handle<HwFence>(2)))
.WillOnce(Return(FenceStatus::CONDITION_SATISFIED));
EXPECT_CALL(mMockDriver, getFenceStatus(Handle<HwFence>(1)))
.WillOnce(Return(FenceStatus::CONDITION_SATISFIED));
std::vector<UboManager::FenceManager::AllocationId> 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<UboManager::FenceManager::AllocationId> reclaimedIds;
EXPECT_CALL(mMockDriver, getFenceStatus(_)).Times(0);
mFenceManager.reclaimCompletedResources(mDriverApi,
[&reclaimedIds](BufferAllocator::AllocationId id) { reclaimedIds.push_back(id); });
EXPECT_TRUE(reclaimedIds.empty());
}
} // namespace anonymous