Compare commits
6 Commits
v1.66.1
...
revert-932
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd073119d2 | ||
|
|
06c4ed4e6b | ||
|
|
49c4a5d62c | ||
|
|
c757cc3629 | ||
|
|
dbdf8f672b | ||
|
|
54bd888374 |
@@ -181,7 +181,7 @@ important for <code>matc</code> (material compiler).</p>
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.filament:filament-android:1.65.4'
|
||||
implementation 'com.google.android.filament:filament-android:1.66.0'
|
||||
}
|
||||
</code></pre>
|
||||
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
|
||||
@@ -196,7 +196,7 @@ dependencies {
|
||||
</div>
|
||||
<h3 id="ios"><a class="header" href="#ios">iOS</a></h3>
|
||||
<p>iOS projects can use CocoaPods to install the latest release:</p>
|
||||
<pre><code class="language-shell">pod 'Filament', '~> 1.65.4'
|
||||
<pre><code class="language-shell">pod 'Filament', '~> 1.66.0'
|
||||
</code></pre>
|
||||
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
|
||||
<ul>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -117,6 +117,7 @@ set(SRCS
|
||||
src/components/LightManager.cpp
|
||||
src/components/RenderableManager.cpp
|
||||
src/components/TransformManager.cpp
|
||||
src/details/BufferAllocator.cpp
|
||||
src/details/BufferObject.cpp
|
||||
src/details/Camera.cpp
|
||||
src/details/ColorGrading.cpp
|
||||
@@ -196,6 +197,7 @@ set(PRIVATE_HDRS
|
||||
src/components/LightManager.h
|
||||
src/components/RenderableManager.h
|
||||
src/components/TransformManager.h
|
||||
src/details/BufferAllocator.h
|
||||
src/details/BufferObject.h
|
||||
src/details/Camera.h
|
||||
src/details/ColorGrading.h
|
||||
|
||||
@@ -760,6 +760,7 @@ void OpenGLContext::initExtensionsGLES(Extensions* ext, GLint major, GLint minor
|
||||
ext->EXT_texture_compression_rgtc = exts.has("GL_EXT_texture_compression_rgtc"sv);
|
||||
ext->EXT_texture_compression_bptc = exts.has("GL_EXT_texture_compression_bptc"sv);
|
||||
ext->EXT_texture_cube_map_array = exts.has("GL_EXT_texture_cube_map_array"sv) || exts.has("GL_OES_texture_cube_map_array"sv);
|
||||
ext->EXT_texture_filter_anisotropic = exts.has("GL_EXT_texture_filter_anisotropic"sv);
|
||||
ext->GOOGLE_cpp_style_line_directive = exts.has("GL_GOOGLE_cpp_style_line_directive"sv);
|
||||
ext->KHR_debug = exts.has("GL_KHR_debug"sv);
|
||||
ext->KHR_parallel_shader_compile = exts.has("GL_KHR_parallel_shader_compile"sv);
|
||||
|
||||
234
filament/src/details/BufferAllocator.cpp
Normal file
234
filament/src/details/BufferAllocator.cpp
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* 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 "details/BufferAllocator.h"
|
||||
|
||||
#include <utils/Panic.h>
|
||||
#include <utils/debug.h>
|
||||
|
||||
namespace filament {
|
||||
namespace {
|
||||
|
||||
static bool isValidId(BufferAllocator::AllocationId id) {
|
||||
return id != BufferAllocator::UNALLOCATED && id != BufferAllocator::REALLOCATION_REQUIRED;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
constexpr static bool isPowerOfTwo(uint32_t n) {
|
||||
return (n > 0) && ((n & (n - 1)) == 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
BufferAllocator::BufferAllocator(allocation_size_t totalSize, allocation_size_t slotSize)
|
||||
: mTotalSize(totalSize),
|
||||
mSlotSize(slotSize) {
|
||||
assert_invariant(mSlotSize > 0);
|
||||
assert_invariant(isPowerOfTwo(mSlotSize));
|
||||
|
||||
reset(mTotalSize);
|
||||
}
|
||||
|
||||
void BufferAllocator::reset(allocation_size_t newTotalSize) {
|
||||
assert_invariant(newTotalSize % mSlotSize == 0);
|
||||
|
||||
mTotalSize = newTotalSize;
|
||||
|
||||
mSlotPool.clear();
|
||||
mFreeList.clear();
|
||||
mOffsetMap.clear();
|
||||
|
||||
// Initialize the pool with a single large free slot.
|
||||
mSlotPool.emplace_back(InternalSlotNode{
|
||||
.mSlot = {
|
||||
.offset = 0,
|
||||
.slotSize = mTotalSize,
|
||||
.isAllocated = false,
|
||||
.gpuUseCount = 0
|
||||
}
|
||||
});
|
||||
|
||||
InternalSlotNode* firstNode = &mSlotPool.front();
|
||||
|
||||
// Add this initial free slot to the free list and offset map.
|
||||
auto freeListIter = mFreeList.emplace(newTotalSize, firstNode);
|
||||
auto offsetMapIter = mOffsetMap.emplace(0, firstNode);
|
||||
|
||||
firstNode->mSlotPoolIterator = mSlotPool.begin();
|
||||
firstNode->mFreeListIterator = freeListIter;
|
||||
firstNode->mOffsetMapIterator = offsetMapIter.first;
|
||||
}
|
||||
|
||||
std::pair<BufferAllocator::AllocationId, BufferAllocator::allocation_size_t>
|
||||
BufferAllocator::allocate(allocation_size_t size) noexcept {
|
||||
if (size == 0) {
|
||||
return { UNALLOCATED, 0 };
|
||||
}
|
||||
|
||||
const allocation_size_t alignedSize = alignUp(size);
|
||||
auto bestFitIter = mFreeList.lower_bound(alignedSize);
|
||||
|
||||
if (bestFitIter == mFreeList.end()) {
|
||||
return { REALLOCATION_REQUIRED, 0 };
|
||||
}
|
||||
|
||||
InternalSlotNode* targetNode = bestFitIter->second;
|
||||
const allocation_size_t originalSlotSize = targetNode->mSlot.slotSize;
|
||||
|
||||
mFreeList.erase(bestFitIter);
|
||||
targetNode->mFreeListIterator = mFreeList.end();
|
||||
targetNode->mSlot.isAllocated = true;
|
||||
|
||||
// Split the slot if it is larger than what we need.
|
||||
if (originalSlotSize > alignedSize) {
|
||||
targetNode->mSlot.slotSize = alignedSize;
|
||||
|
||||
allocation_size_t remainingSize = originalSlotSize - alignedSize;
|
||||
allocation_size_t newSlotOffset = targetNode->mSlot.offset + alignedSize;
|
||||
assert_invariant(remainingSize % mSlotSize == 0);
|
||||
assert_invariant(newSlotOffset % mSlotSize == 0);
|
||||
|
||||
// Create a new node for the remaining free space.
|
||||
auto insertPos = std::next(targetNode->mSlotPoolIterator);
|
||||
auto newNodeIter = mSlotPool.emplace(insertPos, InternalSlotNode{
|
||||
.mSlot = {
|
||||
.offset = newSlotOffset,
|
||||
.slotSize = remainingSize,
|
||||
.isAllocated = false,
|
||||
.gpuUseCount = 0
|
||||
}
|
||||
});
|
||||
InternalSlotNode* newNode = &(*newNodeIter);
|
||||
|
||||
// Add the new free slot to our tracking maps.
|
||||
auto freeListIter = mFreeList.emplace(remainingSize, newNode);
|
||||
auto offsetMapIter = mOffsetMap.emplace(newSlotOffset, newNode);
|
||||
newNode->mSlotPoolIterator = newNodeIter;
|
||||
newNode->mFreeListIterator = freeListIter;
|
||||
newNode->mOffsetMapIterator = offsetMapIter.first;
|
||||
}
|
||||
|
||||
auto allocationId = calculateIdByOffset(targetNode->mSlot.offset);
|
||||
return { allocationId, targetNode->mSlot.offset };
|
||||
}
|
||||
|
||||
BufferAllocator::InternalSlotNode* BufferAllocator::getNodeById(
|
||||
AllocationId id) const noexcept {
|
||||
if (!isValidId(id)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto offset = getAllocationOffset(id);
|
||||
auto iter = mOffsetMap.find(offset);
|
||||
|
||||
// We cannot find the corresponding node in the map.
|
||||
if (iter == mOffsetMap.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
void BufferAllocator::retire(AllocationId id) {
|
||||
auto targetNode = getNodeById(id);
|
||||
assert_invariant(targetNode != nullptr);
|
||||
|
||||
targetNode->mSlot.isAllocated = false;
|
||||
}
|
||||
|
||||
void BufferAllocator::acquireGpu(AllocationId id) {
|
||||
auto targetNode = getNodeById(id);
|
||||
assert_invariant(targetNode != nullptr);
|
||||
|
||||
targetNode->mSlot.gpuUseCount++;
|
||||
}
|
||||
|
||||
void BufferAllocator::releaseGpu(AllocationId id) {
|
||||
auto targetNode = getNodeById(id);
|
||||
assert_invariant(targetNode != nullptr);
|
||||
assert_invariant(targetNode->mSlot.gpuUseCount > 0);
|
||||
|
||||
targetNode->mSlot.gpuUseCount--;
|
||||
}
|
||||
|
||||
void BufferAllocator::releaseFreeSlots() {
|
||||
auto curr = mSlotPool.begin();
|
||||
while (curr != mSlotPool.end()) {
|
||||
if (!curr->mSlot.isFree()) {
|
||||
++curr;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto next = std::next(curr);
|
||||
bool merged = false;
|
||||
while (next != mSlotPool.end() && next->mSlot.isFree()) {
|
||||
merged = true;
|
||||
// Combine the size of free slots
|
||||
curr->mSlot.slotSize += next->mSlot.slotSize;
|
||||
assert_invariant(curr->mSlot.slotSize % mSlotSize == 0);
|
||||
|
||||
// Erase the merged slot from all maps
|
||||
if (next->mFreeListIterator != mFreeList.end()) {
|
||||
mFreeList.erase(next->mFreeListIterator);
|
||||
}
|
||||
mOffsetMap.erase(next->mOffsetMapIterator);
|
||||
next = mSlotPool.erase(next);
|
||||
}
|
||||
|
||||
// If we performed any merge, the current block's size has changed.
|
||||
// We need to update its position in the mFreeList.
|
||||
if (curr->mFreeListIterator != mFreeList.end()) {
|
||||
// If it's already in the free list and we merged, we need to update it.
|
||||
if (merged) {
|
||||
mFreeList.erase(curr->mFreeListIterator);
|
||||
curr->mFreeListIterator = mFreeList.emplace(curr->mSlot.slotSize, &(*curr));
|
||||
}
|
||||
} else {
|
||||
// If it's not in the free list, it must be a newly freed block. Add it.
|
||||
curr->mFreeListIterator = mFreeList.emplace(curr->mSlot.slotSize, &(*curr));
|
||||
}
|
||||
|
||||
curr = next;
|
||||
}
|
||||
}
|
||||
|
||||
BufferAllocator::allocation_size_t BufferAllocator::getTotalSize() const noexcept {
|
||||
return mTotalSize;
|
||||
}
|
||||
|
||||
BufferAllocator::allocation_size_t
|
||||
BufferAllocator::getAllocationOffset(AllocationId id) const {
|
||||
assert_invariant(isValidId(id));
|
||||
|
||||
return (id - 1) * mSlotSize;
|
||||
}
|
||||
|
||||
BufferAllocator::AllocationId BufferAllocator::calculateIdByOffset(
|
||||
allocation_size_t offset) const {
|
||||
assert_invariant(offset % mSlotSize == 0);
|
||||
|
||||
// The ID is 1-based since we use 0 for UNALLOCATED.
|
||||
return (offset / mSlotSize) + 1;
|
||||
}
|
||||
|
||||
BufferAllocator::allocation_size_t BufferAllocator::alignUp(
|
||||
allocation_size_t size) const noexcept {
|
||||
if (size == 0) return 0;
|
||||
|
||||
return (size + mSlotSize - 1) & ~(mSlotSize - 1);
|
||||
}
|
||||
|
||||
} // namespace filament
|
||||
118
filament/src/details/BufferAllocator.h
Normal file
118
filament/src/details/BufferAllocator.h
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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_DETAILS_BUFFERALLOCATOR_H
|
||||
#define TNT_FILAMENT_DETAILS_BUFFERALLOCATOR_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace filament {
|
||||
|
||||
// This class is NOT thread-safe.
|
||||
//
|
||||
// It internally manages shared state (e.g., mSlotPool, mFreeList, mOffsetMap) without any
|
||||
// synchronization primitives. Concurrent access from multiple threads to the same
|
||||
// BufferAllocator instance will result in data races and undefined behavior.
|
||||
//
|
||||
// If an instance of this class is to be shared between threads, all calls to its member
|
||||
// functions MUST be protected by external synchronization (e.g., a std::mutex).
|
||||
class BufferAllocator {
|
||||
public:
|
||||
using allocation_size_t = uint32_t;
|
||||
using AllocationId = uint32_t;
|
||||
|
||||
static constexpr AllocationId UNALLOCATED = 0;
|
||||
static constexpr AllocationId REALLOCATION_REQUIRED = ~0u;
|
||||
|
||||
struct Slot {
|
||||
const allocation_size_t offset; // 4 bytes
|
||||
allocation_size_t slotSize; // 4 bytes
|
||||
bool isAllocated; // 1 byte
|
||||
char padding[3]; // 3 bytes
|
||||
uint32_t gpuUseCount; // 4 bytes
|
||||
|
||||
[[nodiscard]] bool isFree() const noexcept {
|
||||
return !isAllocated && gpuUseCount == 0;
|
||||
}
|
||||
};
|
||||
|
||||
// `slotSize` is derived from the GPU's uniform buffer offset alignment requirement,
|
||||
// which can be up to 256 bytes.
|
||||
explicit BufferAllocator(allocation_size_t totalSize,
|
||||
allocation_size_t slotSize);
|
||||
|
||||
BufferAllocator(BufferAllocator const&) = delete;
|
||||
BufferAllocator(BufferAllocator&&) = delete;
|
||||
|
||||
// Allocate a new slot and return its id and slot offset in the UBO.
|
||||
// If the returned id is not valid, that means there's no large enough slot for allocation.
|
||||
[[nodiscard]] std::pair<AllocationId, allocation_size_t> allocate(
|
||||
allocation_size_t size) noexcept;
|
||||
|
||||
// Call it when MaterialInstance gives up the ownership of the allocation.
|
||||
// We don't release the slot immediately in this function even if it is not being used,
|
||||
// the release is centralized in releaseFreeSlots().
|
||||
void retire(AllocationId id);
|
||||
|
||||
// Increments the GPU read-lock.
|
||||
void acquireGpu(AllocationId id);
|
||||
|
||||
// Decrements the GPU read-lock.
|
||||
// We don't release the slot immediately in this function even if it is not being used,
|
||||
// the release is centralized in releaseFreeSlots().
|
||||
void releaseGpu(AllocationId id);
|
||||
|
||||
// Traverse all slots and free all slots that are not being used by both CPU and GPU.
|
||||
// Perform the merge at the same time.
|
||||
void releaseFreeSlots();
|
||||
|
||||
// Resets the allocator to its initial state with a new total size.
|
||||
// All existing allocations are cleared.
|
||||
void reset(allocation_size_t newTotalSize);
|
||||
|
||||
// Size of the UBO in bytes.
|
||||
[[nodiscard]] allocation_size_t getTotalSize() const noexcept;
|
||||
|
||||
// Query the allocation offset by AllocationId.
|
||||
allocation_size_t getAllocationOffset(AllocationId id) const;
|
||||
|
||||
private:
|
||||
[[nodiscard]] AllocationId calculateIdByOffset(allocation_size_t offset) const;
|
||||
[[nodiscard]] allocation_size_t alignUp(allocation_size_t size) const noexcept;
|
||||
|
||||
// Having an internal node type holding the base slot node and additional information.
|
||||
struct InternalSlotNode {
|
||||
Slot mSlot;
|
||||
std::list<InternalSlotNode>::iterator mSlotPoolIterator;
|
||||
std::multimap<allocation_size_t, InternalSlotNode*>::iterator mFreeListIterator;
|
||||
std::unordered_map<allocation_size_t, InternalSlotNode*>::iterator mOffsetMapIterator;
|
||||
};
|
||||
|
||||
[[nodiscard]] InternalSlotNode* getNodeById(AllocationId id) const noexcept;
|
||||
|
||||
allocation_size_t mTotalSize;
|
||||
const allocation_size_t mSlotSize; // Size of a single slot in bytes.
|
||||
std::list<InternalSlotNode> mSlotPool; // All slots, including both allocated and freed
|
||||
std::multimap</*slot size*/allocation_size_t, InternalSlotNode*> mFreeList;
|
||||
std::unordered_map</*slot offset*/allocation_size_t, InternalSlotNode*> mOffsetMap;
|
||||
};
|
||||
|
||||
} // namespace filament
|
||||
|
||||
#endif // TNT_FILAMENT_DETAILS_BUFFERALLOCATOR_H
|
||||
@@ -43,6 +43,8 @@ list(APPEND RESGEN_SOURCE ${DUMMY_SRC})
|
||||
if (TNT_DEV)
|
||||
add_executable(test_${TARGET}
|
||||
filament_AtlasAllocator_test.cpp
|
||||
test_BufferAllocator.cpp
|
||||
test_BufferAllocatorStress.cpp
|
||||
test_CircularQueue.cpp
|
||||
filament_test_exposure.cpp
|
||||
filament_rendering_test.cpp
|
||||
|
||||
415
filament/test/test_BufferAllocator.cpp
Normal file
415
filament/test/test_BufferAllocator.cpp
Normal file
@@ -0,0 +1,415 @@
|
||||
/*
|
||||
* 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 <gtest/gtest.h>
|
||||
|
||||
#include "../src/details/BufferAllocator.h"
|
||||
#include "utils/Panic.h"
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace filament;
|
||||
|
||||
namespace {
|
||||
|
||||
class BufferAllocatorTest : public ::testing::Test {
|
||||
protected:
|
||||
// We use a total size of 1024 and a slot size (alignment) of 64.
|
||||
// This gives us 1024 / 64 = 16 total possible slots if aligned.
|
||||
static constexpr BufferAllocator::allocation_size_t TOTAL_SIZE = 1024;
|
||||
static constexpr BufferAllocator::allocation_size_t SLOT_SIZE = 64;
|
||||
|
||||
BufferAllocatorTest() : mAllocator(TOTAL_SIZE, SLOT_SIZE) {}
|
||||
|
||||
BufferAllocator mAllocator;
|
||||
};
|
||||
|
||||
TEST_F(BufferAllocatorTest, ConstructorFailure) {
|
||||
// The constructor requires slotSize to be a power of two.
|
||||
constexpr BufferAllocator::allocation_size_t NON_POT_SLOT_SIZE = 60;
|
||||
EXPECT_DEATH(BufferAllocator(TOTAL_SIZE, NON_POT_SLOT_SIZE), "failed assertion");
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, InitialState) {
|
||||
EXPECT_EQ(mAllocator.getTotalSize(), TOTAL_SIZE);
|
||||
// Initially, there should be one large free block of the total size.
|
||||
// Let's try to allocate the whole thing.
|
||||
auto [id, offset] = mAllocator.allocate(TOTAL_SIZE);
|
||||
EXPECT_EQ(id, 1); // The first ID should be 1.
|
||||
EXPECT_EQ(offset, 0);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, SimpleAllocation) {
|
||||
// Allocate 100 bytes, which should be aligned up to 128 (2 * 64).
|
||||
auto [id, offset] = mAllocator.allocate(100);
|
||||
EXPECT_EQ(id, 1);
|
||||
EXPECT_EQ(offset, 0);
|
||||
EXPECT_EQ(mAllocator.getAllocationOffset(id), offset);
|
||||
|
||||
// Try to allocate again. The next allocation should start after the first one.
|
||||
auto [id2, offset2] = mAllocator.allocate(50); // Aligns to 64
|
||||
EXPECT_EQ(id2, 3); // ID is (128 / 64) + 1 = 3
|
||||
EXPECT_EQ(offset2, 128);
|
||||
EXPECT_EQ(mAllocator.getAllocationOffset(id2), offset2);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, AllocateZeroSize) {
|
||||
// Allocating zero bytes should return an unallocated ID and not affect the state.
|
||||
auto [id, offset] = mAllocator.allocate(0);
|
||||
EXPECT_EQ(id, BufferAllocator::UNALLOCATED);
|
||||
EXPECT_EQ(offset, 0);
|
||||
|
||||
// The allocator should still be in its initial state, able to allocate the full size.
|
||||
auto [fullId, fullOffset] = mAllocator.allocate(TOTAL_SIZE);
|
||||
EXPECT_EQ(fullId, 1);
|
||||
EXPECT_EQ(fullOffset, 0);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, AllocateAll) {
|
||||
auto [id1, offset1] = mAllocator.allocate(512);
|
||||
EXPECT_EQ(id1, 1);
|
||||
EXPECT_EQ(offset1, 0);
|
||||
|
||||
auto [id2, offset2] = mAllocator.allocate(512);
|
||||
EXPECT_EQ(id2, 9); // ID is (512 / 64) + 1 = 9
|
||||
EXPECT_EQ(offset2, 512);
|
||||
|
||||
// The buffer is now full. The next allocation should fail.
|
||||
auto [id3, offset3] = mAllocator.allocate(1);
|
||||
EXPECT_EQ(id3, BufferAllocator::REALLOCATION_REQUIRED);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, AllocationFailure) {
|
||||
auto [id1, offset1] = mAllocator.allocate(TOTAL_SIZE - 1); // Allocate most of the buffer.
|
||||
EXPECT_EQ(id1, 1);
|
||||
EXPECT_EQ(offset1, 0);
|
||||
|
||||
// Try to allocate more than the remaining space.
|
||||
auto [id2, offset2] = mAllocator.allocate(100);
|
||||
EXPECT_EQ(id2, BufferAllocator::REALLOCATION_REQUIRED);
|
||||
EXPECT_EQ(offset2, 0);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, AllocationLifecycle) {
|
||||
// 1. Allocate
|
||||
auto [id, offset] = mAllocator.allocate(128);
|
||||
EXPECT_EQ(id, 1);
|
||||
|
||||
// 2. Retire from CPU side
|
||||
mAllocator.retire(id);
|
||||
|
||||
// 3. The slot is not free yet because the GPU might still be using it.
|
||||
// Let's simulate the GPU acquiring and releasing it.
|
||||
mAllocator.acquireGpu(id);
|
||||
mAllocator.releaseGpu(id);
|
||||
|
||||
// Now the slot should be considered free, but it's not merged yet.
|
||||
// Let's try to allocate something else to see where it goes.
|
||||
auto [id2, offset2] = mAllocator.allocate(200); // Aligns to 256
|
||||
EXPECT_EQ(id2, 3); // It should use the space after the first retired slot.
|
||||
EXPECT_EQ(offset2, 128);
|
||||
|
||||
// Now, let's release the free slots.
|
||||
mAllocator.releaseFreeSlots();
|
||||
|
||||
// The first slot (128 bytes) should now be available again.
|
||||
// Let's try to allocate something that fits in it.
|
||||
auto [id3, offset3] = mAllocator.allocate(100); // Aligns to 128
|
||||
EXPECT_EQ(id3, 1); // It should reuse the first slot.
|
||||
EXPECT_EQ(offset3, 0);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, RetireThenReleaseGpu) {
|
||||
// 1. Allocate a block and a dummy block next to it.
|
||||
auto [id1, offset1] = mAllocator.allocate(128);
|
||||
auto [id2, offset2] = mAllocator.allocate(128);
|
||||
EXPECT_EQ(id1, 1);
|
||||
EXPECT_EQ(id2, 3);
|
||||
EXPECT_EQ(offset1, 0);
|
||||
EXPECT_EQ(offset2, 128);
|
||||
|
||||
// 2. Retire the first block (CPU is done), then acquire it for the GPU.
|
||||
mAllocator.retire(id1);
|
||||
mAllocator.acquireGpu(id1); // gpuUseCount = 1
|
||||
|
||||
// 3. The slot is now free from CPU but locked by GPU. releaseFreeSlots should do nothing to it.
|
||||
mAllocator.releaseFreeSlots();
|
||||
|
||||
// 4. Try to allocate the same space. It should fail, and the allocation should go
|
||||
// to the next available free space.
|
||||
auto [id3, offset3] = mAllocator.allocate(64);
|
||||
EXPECT_EQ(id3, 5);
|
||||
EXPECT_EQ(offset3, 256); // It should be allocated after id2.
|
||||
|
||||
// 5. Now, release the GPU lock.
|
||||
mAllocator.releaseGpu(id1); // gpuUseCount = 0
|
||||
|
||||
// 6. Call releaseFreeSlots again. This time it should be freed.
|
||||
mAllocator.releaseFreeSlots();
|
||||
|
||||
// 7. The original slot should now be available for allocation.
|
||||
auto [id4, offset4] = mAllocator.allocate(64);
|
||||
EXPECT_EQ(id4, 1);
|
||||
EXPECT_EQ(offset4, 0); // Success! It reuses the first slot.
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, MultipleGpuAcquires) {
|
||||
// 1. Allocate a block.
|
||||
auto [id, offset] = mAllocator.allocate(128);
|
||||
EXPECT_EQ(id, 1);
|
||||
EXPECT_EQ(offset, 0);
|
||||
|
||||
// 2. Retire from CPU, then acquire multiple times for GPU (e.g., used in 3 command buffers).
|
||||
mAllocator.retire(id);
|
||||
mAllocator.acquireGpu(id); // gpuUseCount = 1
|
||||
mAllocator.acquireGpu(id); // gpuUseCount = 2
|
||||
mAllocator.acquireGpu(id); // gpuUseCount = 3
|
||||
|
||||
// 3. Release GPU lock once. The slot should still be locked.
|
||||
mAllocator.releaseGpu(id); // gpuUseCount = 2
|
||||
mAllocator.releaseFreeSlots();
|
||||
auto [failId1, failOffset1] = mAllocator.allocate(64);
|
||||
EXPECT_NE(failOffset1, 0); // Should not be able to allocate at offset 0.
|
||||
EXPECT_NE(failId1, 1);
|
||||
|
||||
// 4. Release GPU lock again. The slot should still be locked.
|
||||
mAllocator.releaseGpu(id); // gpuUseCount = 1
|
||||
mAllocator.releaseFreeSlots();
|
||||
auto [failId2, failOffset2] = mAllocator.allocate(64);
|
||||
EXPECT_NE(failOffset2, 0); // Still cannot allocate at offset 0.
|
||||
EXPECT_NE(failId2, 1);
|
||||
|
||||
// 5. Final release. The lock count is now 0.
|
||||
mAllocator.releaseGpu(id); // gpuUseCount = 0
|
||||
|
||||
// 6. Now it should be freed and available.
|
||||
mAllocator.releaseFreeSlots();
|
||||
auto [successId, successOffset] = mAllocator.allocate(64);
|
||||
EXPECT_EQ(successOffset, 0);
|
||||
EXPECT_EQ(successId, 1);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, GpuPanicOnUnderflow) {
|
||||
// 1. Allocate a block and acquire it.
|
||||
auto [id, _] = mAllocator.allocate(128);
|
||||
mAllocator.acquireGpu(id);
|
||||
|
||||
// 2. Release it once, which is fine.
|
||||
mAllocator.releaseGpu(id);
|
||||
|
||||
// 3. Releasing it again when the count is 0 should trigger a failed assertion.
|
||||
EXPECT_DEATH(mAllocator.releaseGpu(id), "failed assertion");
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, MergeFreeSlots) {
|
||||
// Allocate three blocks
|
||||
auto [id1, offset1] = mAllocator.allocate(128); // Slot 0-127
|
||||
auto [id2, offset2] = mAllocator.allocate(128); // Slot 128-255
|
||||
auto [id3, offset3] = mAllocator.allocate(128); // Slot 256-383
|
||||
|
||||
EXPECT_EQ(id1, 1);
|
||||
EXPECT_EQ(id2, 3);
|
||||
EXPECT_EQ(id3, 5);
|
||||
|
||||
EXPECT_EQ(offset1, 0);
|
||||
EXPECT_EQ(offset2, 128);
|
||||
EXPECT_EQ(offset3, 256);
|
||||
|
||||
// Retire the first and third blocks
|
||||
mAllocator.retire(id1);
|
||||
mAllocator.retire(id3);
|
||||
|
||||
// At this point, we have: [Free, Allocated, Free, Free (remaining)]
|
||||
// releaseFreeSlots should not merge slot 1 and 3 because they are not adjacent.
|
||||
// It should merge slot 3 and slot 4 instead.
|
||||
mAllocator.releaseFreeSlots();
|
||||
|
||||
// At this point, we have: [Free, Allocated, Free (remaining)]
|
||||
// Let's verify by trying to allocate 200. It should go into the merged slot.
|
||||
auto [id4, offset4] = mAllocator.allocate(200); // Aligns to 256
|
||||
EXPECT_EQ(id4, 5);
|
||||
EXPECT_EQ(offset4, 256);
|
||||
|
||||
// Now, retire the middle block
|
||||
mAllocator.retire(id2);
|
||||
|
||||
// Now we have: [Free, Free, Allocated, Free (remaining)]
|
||||
// The first two blocks are now adjacent and free.
|
||||
mAllocator.releaseFreeSlots();
|
||||
|
||||
// After merging, the first 256 bytes should be one large free block.
|
||||
// Let's try to allocate something that requires this merged space.
|
||||
auto [id5, offset5] = mAllocator.allocate(200); // Aligns to 256
|
||||
EXPECT_EQ(id5, 1);
|
||||
EXPECT_EQ(offset5, 0);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, MergeAllSlots) {
|
||||
// Allocate the entire buffer in small chunks.
|
||||
constexpr BufferAllocator::allocation_size_t CHUNK_SIZE = 128;
|
||||
constexpr uint32_t NUM_CHUNKS = TOTAL_SIZE / CHUNK_SIZE;
|
||||
std::vector<BufferAllocator::AllocationId> ids;
|
||||
for (uint32_t i = 0; i < NUM_CHUNKS; ++i) {
|
||||
auto [id, offset] = mAllocator.allocate(CHUNK_SIZE);
|
||||
ASSERT_NE(id, BufferAllocator::REALLOCATION_REQUIRED);
|
||||
ids.push_back(id);
|
||||
}
|
||||
|
||||
// The buffer should be full.
|
||||
auto [failId, failOffset] = mAllocator.allocate(1);
|
||||
EXPECT_EQ(failId, BufferAllocator::REALLOCATION_REQUIRED);
|
||||
EXPECT_EQ(failOffset, 0);
|
||||
|
||||
// Retire all chunks.
|
||||
for (auto id : ids) {
|
||||
mAllocator.retire(id);
|
||||
}
|
||||
|
||||
// Release and merge.
|
||||
mAllocator.releaseFreeSlots();
|
||||
|
||||
// Now, the allocator should be back to its initial state with one large free block.
|
||||
// We should be able to allocate the entire buffer again.
|
||||
auto [fullId, fullOffset] = mAllocator.allocate(TOTAL_SIZE);
|
||||
EXPECT_EQ(fullId, 1);
|
||||
EXPECT_EQ(fullOffset, 0);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, NoMergePossible) {
|
||||
// Allocate blocks in an alternating pattern.
|
||||
auto [id1, offset1] = mAllocator.allocate(128);
|
||||
auto [id2, offset2] = mAllocator.allocate(128);
|
||||
auto [id3, offset3] = mAllocator.allocate(128);
|
||||
auto [id4, offset4] = mAllocator.allocate(128);
|
||||
EXPECT_EQ(id1, 1);
|
||||
EXPECT_EQ(id2, 3);
|
||||
EXPECT_EQ(id3, 5);
|
||||
EXPECT_EQ(id4, 7);
|
||||
EXPECT_EQ(offset1, 0);
|
||||
EXPECT_EQ(offset2, 128);
|
||||
EXPECT_EQ(offset3, 256);
|
||||
EXPECT_EQ(offset4, 384);
|
||||
|
||||
// Retire the first and third blocks, leaving allocated blocks in between.
|
||||
mAllocator.retire(id1);
|
||||
mAllocator.retire(id3);
|
||||
|
||||
// At this point, we have: [Free, Allocated, Free, Allocated, Free (remaining)]
|
||||
mAllocator.releaseFreeSlots();
|
||||
|
||||
// Now, let's try to re-allocate the two 128-byte slots.
|
||||
auto [id5, offset5] = mAllocator.allocate(128);
|
||||
auto [id6, offset6] = mAllocator.allocate(128);
|
||||
EXPECT_EQ(offset5, 0);
|
||||
EXPECT_EQ(offset6, 256);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, Reset) {
|
||||
auto [id1, offset1] = mAllocator.allocate(100);
|
||||
auto [id2, offset2] = mAllocator.allocate(200);
|
||||
EXPECT_EQ(id1, 1);
|
||||
EXPECT_EQ(id2, 3);
|
||||
EXPECT_EQ(offset1, 0);
|
||||
EXPECT_EQ(offset2, 128);
|
||||
|
||||
// Reset the allocator to a new size.
|
||||
mAllocator.reset(2048);
|
||||
EXPECT_EQ(mAllocator.getTotalSize(), 2048);
|
||||
|
||||
// After reset, all previous allocations should be gone,
|
||||
// and we should be able to allocate the entire new size.
|
||||
auto [id3, offset3] = mAllocator.allocate(2048);
|
||||
EXPECT_EQ(id3, 1);
|
||||
EXPECT_EQ(offset3, 0);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, ResetWithInvalidSize) {
|
||||
// Reset to a size which is not a power of two.
|
||||
EXPECT_DEATH(mAllocator.reset(123), "failed assertion");
|
||||
}
|
||||
|
||||
|
||||
TEST_F(BufferAllocatorTest, ResetWithGpuLock) {
|
||||
// 1. Allocate a block and acquire a GPU lock on it.
|
||||
auto [id1, offset1] = mAllocator.allocate(128);
|
||||
EXPECT_EQ(id1, 1);
|
||||
mAllocator.acquireGpu(id1); // gpuUseCount = 1
|
||||
|
||||
// 2. Call reset. This should disregard the GPU lock and clear everything.
|
||||
constexpr BufferAllocator::allocation_size_t NEW_TOTAL_SIZE = 4096;
|
||||
mAllocator.reset(NEW_TOTAL_SIZE);
|
||||
|
||||
// 3. Verify the allocator is in a pristine state with the new size.
|
||||
EXPECT_EQ(mAllocator.getTotalSize(), NEW_TOTAL_SIZE);
|
||||
|
||||
// 4. The strongest verification is to allocate the entire new size, which should
|
||||
// succeed, proving that the old GPU-locked block is gone.
|
||||
auto [id2, offset2] = mAllocator.allocate(NEW_TOTAL_SIZE);
|
||||
EXPECT_EQ(id2, 1);
|
||||
EXPECT_EQ(offset2, 0);
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, InvalidOperations) {
|
||||
// These operations on invalid IDs should not crash and should be handled gracefully.
|
||||
EXPECT_DEATH(mAllocator.retire(BufferAllocator::UNALLOCATED), "failed assertion");
|
||||
EXPECT_DEATH(mAllocator.retire(999), "failed assertion"); // Non-existent ID
|
||||
|
||||
EXPECT_DEATH(mAllocator.acquireGpu(BufferAllocator::UNALLOCATED), "failed assertion");
|
||||
EXPECT_DEATH(mAllocator.acquireGpu(999), "failed assertion");
|
||||
|
||||
EXPECT_DEATH(mAllocator.releaseGpu(BufferAllocator::UNALLOCATED), "failed assertion");
|
||||
EXPECT_DEATH(mAllocator.releaseGpu(999), "failed assertion");
|
||||
|
||||
// Check that an invalid offset query panics in debug/testing builds.
|
||||
EXPECT_DEATH(mAllocator.getAllocationOffset(BufferAllocator::UNALLOCATED), "failed assertion");
|
||||
EXPECT_DEATH(mAllocator.getAllocationOffset(BufferAllocator::REALLOCATION_REQUIRED),
|
||||
"failed assertion");
|
||||
}
|
||||
|
||||
TEST_F(BufferAllocatorTest, ComplexScenario) {
|
||||
std::vector<BufferAllocator::AllocationId> ids;
|
||||
// 1. Allocate 4 blocks of 256 bytes
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
auto [id, offset] = mAllocator.allocate(256);
|
||||
EXPECT_EQ(offset, i * 256);
|
||||
ids.push_back(id);
|
||||
}
|
||||
|
||||
// Buffer should be full
|
||||
auto [failId1, failOffset1] = mAllocator.allocate(1);
|
||||
EXPECT_EQ(failId1, BufferAllocator::REALLOCATION_REQUIRED);
|
||||
|
||||
// 2. Retire the 2nd and 3rd blocks (ids[1] and ids[2])
|
||||
mAllocator.retire(ids[1]);
|
||||
mAllocator.retire(ids[2]);
|
||||
|
||||
// 3. Release free slots. This should merge the two retired blocks.
|
||||
mAllocator.releaseFreeSlots();
|
||||
|
||||
// We now have a free block of 512 bytes at offset 256.
|
||||
// Let's allocate 512 bytes. It should fit perfectly.
|
||||
auto [id, offset] = mAllocator.allocate(512);
|
||||
EXPECT_EQ(id, 5); // (256 / 64) + 1 = 5
|
||||
EXPECT_EQ(offset, 256);
|
||||
|
||||
// 4. The buffer should be full again.
|
||||
auto [failId2,failOffset2] = mAllocator.allocate(1);
|
||||
EXPECT_EQ(failId2, BufferAllocator::REALLOCATION_REQUIRED);
|
||||
EXPECT_EQ(failOffset2,0);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
122
filament/test/test_BufferAllocatorStress.cpp
Normal file
122
filament/test/test_BufferAllocatorStress.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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 <gtest/gtest.h>
|
||||
|
||||
#include "../src/details/BufferAllocator.h"
|
||||
#include "utils/Panic.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
using namespace filament;
|
||||
|
||||
namespace {
|
||||
|
||||
class BufferAllocatorStressTest : public ::testing::Test {
|
||||
protected:
|
||||
// We use a total size of 1024 * 64 and a slot size (alignment) of 64.
|
||||
// This gives us 1024 total possible slots if aligned.
|
||||
static constexpr BufferAllocator::allocation_size_t SLOT_SIZE = 64;
|
||||
static constexpr BufferAllocator::allocation_size_t SLOT_COUNT = 4096;
|
||||
static constexpr BufferAllocator::allocation_size_t TOTAL_SIZE = SLOT_COUNT * SLOT_SIZE;
|
||||
|
||||
BufferAllocatorStressTest() : mAllocator(TOTAL_SIZE, SLOT_SIZE) {
|
||||
}
|
||||
|
||||
BufferAllocator mAllocator;
|
||||
};
|
||||
|
||||
TEST_F(BufferAllocatorStressTest, StressTest) {
|
||||
// Many operations.
|
||||
constexpr int operationCount = 5000;
|
||||
|
||||
// 1. Prepare the random number distributions.
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> slotCountDistrib(1, SLOT_COUNT);
|
||||
// Random the slot offset we allocate within a slot.
|
||||
std::uniform_int_distribution<> slotOffsetDistrib(0, SLOT_SIZE - 1);
|
||||
// Random the operation we perform: 0,1,2 -> allocate, 3 -> retire, 4 -> release slots
|
||||
// We bias the operations to make more allocations than releases.
|
||||
std::uniform_int_distribution operationDistrib(0, 4);
|
||||
|
||||
// 2. Randomly do some actions and expect to have no crash
|
||||
std::vector<BufferAllocator::AllocationId> ids;
|
||||
for (int i = 0; i < operationCount; i++) {
|
||||
switch (operationDistrib(gen)) {
|
||||
case 0: // Allocate
|
||||
case 1: // Allocate
|
||||
case 2: // Allocate
|
||||
{
|
||||
// Random the slot count + its offset.
|
||||
const BufferAllocator::allocation_size_t size =
|
||||
slotCountDistrib(gen) * SLOT_SIZE - slotOffsetDistrib(gen);
|
||||
|
||||
if (auto [id, _] = mAllocator.allocate(size);
|
||||
id != BufferAllocator::REALLOCATION_REQUIRED && id !=
|
||||
BufferAllocator::UNALLOCATED) {
|
||||
ids.push_back(id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3: // Allocate
|
||||
{
|
||||
if (ids.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Retire a random slot.
|
||||
std::uniform_int_distribution<uint32_t> idDistrib(0, ids.size() - 1);
|
||||
uint32_t indexToRetire = idDistrib(gen);
|
||||
|
||||
BufferAllocator::AllocationId idToRetire = ids[indexToRetire];
|
||||
mAllocator.retire(idToRetire);
|
||||
|
||||
// Remove the retired id from the list.
|
||||
std::swap(ids[indexToRetire], ids.back());
|
||||
ids.pop_back();
|
||||
break;
|
||||
}
|
||||
case 4: // Allocate
|
||||
{
|
||||
mAllocator.releaseFreeSlots();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Retire all remaining allocations.
|
||||
for (const auto& id: ids) {
|
||||
mAllocator.retire(id);
|
||||
}
|
||||
ids.clear();
|
||||
|
||||
// 4. Release and merge everything.
|
||||
mAllocator.releaseFreeSlots();
|
||||
|
||||
// 5. The allocator should now be in a pristine state.
|
||||
// A final allocation of the total size should succeed.
|
||||
auto [finalId, finalOffset] = mAllocator.allocate(TOTAL_SIZE);
|
||||
EXPECT_NE(finalId, BufferAllocator::REALLOCATION_REQUIRED);
|
||||
EXPECT_NE(finalId, BufferAllocator::UNALLOCATED);
|
||||
EXPECT_EQ(finalOffset, 0);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
@@ -191,6 +191,8 @@ static int handleCommandLineArgments(int argc, char* argv[], Config* config) {
|
||||
}
|
||||
|
||||
static void cleanup(Engine* engine, View*, Scene*) {
|
||||
g_meshSet.reset(nullptr);
|
||||
|
||||
for (const auto& material : g_meshMaterialInstances) {
|
||||
engine->destroy(material.second);
|
||||
}
|
||||
@@ -203,8 +205,6 @@ static void cleanup(Engine* engine, View*, Scene*) {
|
||||
engine->destroy(i);
|
||||
}
|
||||
|
||||
g_meshSet.reset(nullptr);
|
||||
|
||||
engine->destroy(g_params.light);
|
||||
engine->destroy(g_params.spotLight);
|
||||
engine->destroy(g_colorGrading);
|
||||
|
||||
Reference in New Issue
Block a user