buffer update opt: refactor UboManager and introduce FenceManager (#9418)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
149
filament/test/MockDriver.h
Normal 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
|
||||
158
filament/test/test_FenceManager.cpp
Normal file
158
filament/test/test_FenceManager.cpp
Normal 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
|
||||
Reference in New Issue
Block a user