From 90f32e727707d8275167eec5594dd9039725bd43 Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Wed, 12 Oct 2022 22:52:22 -0700 Subject: [PATCH] A basic POT 2D allocator This 2D allocator only supports power-of-two, square allocations. It uses a quadtree to store the allocated regions. The quadtree implementation currently has a fixed depth (templated). The allocator does a best-fit match, but currently doesn't implement a free method. --- filament/CMakeLists.txt | 1 + filament/src/AtlasAllocator.cpp | 161 ++++++++++++++++ filament/src/AtlasAllocator.h | 95 +++++++++ filament/test/CMakeLists.txt | 1 + .../test/filament_AtlasAllocator_test.cpp | 149 +++++++++++++++ libs/utils/CMakeLists.txt | 1 + libs/utils/include/utils/QuadTree.h | 180 ++++++++++++++++++ libs/utils/test/test_QuadTreeArray.cpp | 43 +++++ 8 files changed, 631 insertions(+) create mode 100644 filament/src/AtlasAllocator.cpp create mode 100644 filament/src/AtlasAllocator.h create mode 100644 filament/test/filament_AtlasAllocator_test.cpp create mode 100644 libs/utils/include/utils/QuadTree.h create mode 100644 libs/utils/test/test_QuadTreeArray.cpp diff --git a/filament/CMakeLists.txt b/filament/CMakeLists.txt index 257b3e6d4e..cc2b07059c 100644 --- a/filament/CMakeLists.txt +++ b/filament/CMakeLists.txt @@ -48,6 +48,7 @@ set(PUBLIC_HDRS ) set(SRCS + src/AtlasAllocator.cpp src/Box.cpp src/BufferObject.cpp src/Camera.cpp diff --git a/filament/src/AtlasAllocator.cpp b/filament/src/AtlasAllocator.cpp new file mode 100644 index 0000000000..b1f2951e11 --- /dev/null +++ b/filament/src/AtlasAllocator.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2022 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 "AtlasAllocator.h" + +#include + +namespace filament { + +using namespace utils; + +static inline constexpr std::pair unmorton(uint16_t m) noexcept { + uint32_t r = (m | (uint32_t(m) << 15u)) & 0x55555555u; + r = (r | (r >> 1u)) & 0x33333333u; + r = (r | (r >> 2u)) & 0x0f0f0f0fu; + r = r | (r >> 4u); + return { uint8_t(r), uint8_t(r >> 16u) }; +} + +AtlasAllocator::AtlasAllocator(size_t maxTextureSize) noexcept { + // round to power-of-two immediately inferior or equal to the size specified. + mMaxTextureSizePot = (sizeof(maxTextureSize) * 8 - 1u) - utils::clz(maxTextureSize); +} + +Viewport AtlasAllocator::allocate(size_t textureSize) noexcept { + Viewport result{}; + const size_t powerOfTwo = (sizeof(textureSize) * 8 - 1u) - utils::clz(textureSize); + + // asked for a texture size too large + if (UTILS_UNLIKELY(powerOfTwo > mMaxTextureSizePot)) { + return result; + } + + // asked for a texture size too small + if (UTILS_UNLIKELY(mMaxTextureSizePot - powerOfTwo >= QuadTree::height())) { + return result; + } + + const size_t layer = mMaxTextureSizePot - powerOfTwo; + const NodeId loc = allocateInLayer(layer); + if (loc.l >= 0) { + assert_invariant(loc.l == int8_t(layer)); + size_t dimension = 1u << powerOfTwo; + // find the location of in the texture from the morton code (quadtree position) + auto [x, y] = unmorton(loc.code); + // scale to our maximum allocation size + result.left = int32_t(x) << powerOfTwo; + result.bottom = int32_t(y) << powerOfTwo; + result.width = dimension; + result.height = dimension; + } + return result; +} + +void AtlasAllocator::clear(size_t maxTextureSize) noexcept { + std::fill(mQuadTree.begin(), mQuadTree.end(), Node{}); + mMaxTextureSizePot = (sizeof(maxTextureSize) * 8 - 1u) - utils::clz(maxTextureSize); +} + +AtlasAllocator::NodeId AtlasAllocator::allocateInLayer(size_t maxHeight) noexcept { + using namespace QuadTreeUtils; + + NodeId candidate{ -1, 0 }; + if (UTILS_UNLIKELY(maxHeight > QuadTree::height())) { + return candidate; + } + + const int8_t n = int8_t(maxHeight); + + QuadTree::traverse(0, 0, n, + [this, n, &candidate](NodeId const& curr) -> QuadTree::TraversalResult { + + // we should never reach a level higher than n + assert_invariant(curr.l <= n); + + // get the node being processed + const size_t i = index(curr.l, curr.code); + Node const& node = mQuadTree[i]; + + // if the node is allocated, the whole tree below it is unavailable + if (node.isAllocated()) { + // if a node is allocated it can't have children + assert_invariant(!node.hasChildren()); + return QuadTree::TraversalResult::SKIP_CHILDREN; + } + + // we're looking for the `deepest` unallocated node that has no children, + // up to the depth we're trying to allocate + if (curr.l > candidate.l && !node.hasChildren()) { + candidate = curr; + // Special case where we found a fitting node, just exit. + if (curr.l == n) { + return QuadTree::TraversalResult::EXIT; + } + } + + // We only want to find a fitting node that already has siblings, in order + // to accomplish a "best fit" allocation. So if we're a parent of a 'fitting' + // node and don't have children, we skip the children recursion. + if (curr.l == n - 1 && !node.hasChildren()) { + return QuadTree::TraversalResult::SKIP_CHILDREN; + } + + // continue going down the tree + return QuadTree::TraversalResult::RECURSE; + }); + + if (candidate.l >= 0) { + const size_t i = index(candidate.l, candidate.code); + Node& allocation = mQuadTree[i]; + + if (candidate.l == n) { + allocation.allocated = true; + if (UTILS_LIKELY(n > 0)) { + // the root doesn't have a parent + const size_t p = parent(candidate.l, candidate.code); + Node& parent = mQuadTree[p]; + assert_invariant(!parent.isAllocated()); + assert_invariant(parent.hasChildren()); + assert_invariant(!parent.hasAllChildren()); + parent.children++; + } + } else if (candidate.l < int8_t(QuadTree::height())) { + // we need to create the hierarchy down to the level we need + assert_invariant(!allocation.isAllocated()); + assert_invariant(!allocation.hasChildren()); + + NodeId found{}; + QuadTree::traverse(candidate.l, candidate.code, + [this, n, &found](NodeId const& curr) -> QuadTree::TraversalResult { + size_t i = index(curr.l, curr.code); + Node& node = mQuadTree[i]; + if (curr.l == n) { + found = curr; + node.allocated = true; + return QuadTree::TraversalResult::EXIT; + } + node.children++; + return QuadTree::TraversalResult::RECURSE; + }); + + candidate = found; + } + } + return candidate; +} + +} // namespace filament diff --git a/filament/src/AtlasAllocator.h b/filament/src/AtlasAllocator.h new file mode 100644 index 0000000000..30f4f88e35 --- /dev/null +++ b/filament/src/AtlasAllocator.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 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_ATLASALLOCATOR_H +#define TNT_FILAMENT_ATLASALLOCATOR_H + +#include + +#include + +class AtlasAllocator_AllocateFirstLevel_Test; +class AtlasAllocator_AllocateSecondLevel_Test; +class AtlasAllocator_AllocateMixed0_Test; +class AtlasAllocator_AllocateMixed1_Test; +class AtlasAllocator_AllocateMixed2_Test; + +namespace filament { + +/* + * A 2D allocator. Allocations must be square and have a power-of-two size. + * AtlasAllocator hard codes a depth of 4, that is only 4 allocation sizes are permitted. + * This doesn't actually allocate memory, just manages space within an abstract 2D image. + */ +class AtlasAllocator { + + /* + * A quadtree is used to track the allocated regions. Each node of the quadtree stores a + * `Node` data structure below. + * The `Node` tracks if it is allocated as well as the number of children it has. It doesn't + * track which children though. + */ + struct Node { + // whether this node is allocated. Implies no children. + constexpr bool isAllocated() const noexcept { return allocated; } + // whether this node has children. Implies it's not allocated. + constexpr bool hasChildren() const noexcept { return children != 0; } + // whether this node has all four children. Implies hasChildren(). + constexpr bool hasAllChildren() const noexcept { return children == 4; } + bool allocated : 1; // true / false + uint8_t children : 3; // 0, 1, 2, 3, 4 + }; + + static constexpr size_t QUAD_TREE_DEPTH = 4u; + using QuadTree = utils::QuadTreeArray; + using NodeId = QuadTree::NodeId; + +public: + /* + * Create allocator and specify the maximum texture size. Must be a power of two. + * Allocations size allowed are the four power-of-two smaller or equal to this size. + */ + explicit AtlasAllocator(size_t maxTextureSize = 1024) noexcept; + + /* + * Allocates a square of size `textureSize`. Must be one of the power-of-two allowed + * (see above). + * Returns the location of the allocation within the maxTextureSize^2 square. + */ + Viewport allocate(size_t textureSize) noexcept; + + /* + * Frees all allocations and reset the maximum texture size. + */ + void clear(size_t maxTextureSize = 1024) noexcept; + +private: + friend AtlasAllocator_AllocateFirstLevel_Test; + friend AtlasAllocator_AllocateSecondLevel_Test; + friend AtlasAllocator_AllocateMixed0_Test; + friend AtlasAllocator_AllocateMixed1_Test; + friend AtlasAllocator_AllocateMixed2_Test; + + QuadTree::NodeId allocateInLayer(size_t n) noexcept; + + // quad-tree array to store the allocated list + QuadTree mQuadTree{}; + uint8_t mMaxTextureSizePot = 0; +}; + +} // namespace filament + +#endif // TNT_FILAMENT_ATLASALLOCATOR_H diff --git a/filament/test/CMakeLists.txt b/filament/test/CMakeLists.txt index 49ff22e299..f9aedc5468 100644 --- a/filament/test/CMakeLists.txt +++ b/filament/test/CMakeLists.txt @@ -42,6 +42,7 @@ list(APPEND RESGEN_SOURCE ${DUMMY_SRC}) # away in Release builds if (TNT_DEV) add_executable(test_${TARGET} + filament_AtlasAllocator_test.cpp filament_test_exposure.cpp filament_rendering_test.cpp filament_framegraph_test.cpp diff --git a/filament/test/filament_AtlasAllocator_test.cpp b/filament/test/filament_AtlasAllocator_test.cpp new file mode 100644 index 0000000000..442a8b3ddc --- /dev/null +++ b/filament/test/filament_AtlasAllocator_test.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2022 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 + +#include "AtlasAllocator.h" + +using namespace filament; + +TEST(AtlasAllocator, AllocateFirstLevel) { + + AtlasAllocator allocator; + + auto a = allocator.allocateInLayer(0); + EXPECT_TRUE(a.l == 0 && a.code == 0); + + auto b = allocator.allocateInLayer(1); + EXPECT_TRUE(b.l < 0); + + auto c = allocator.allocateInLayer(2); + EXPECT_TRUE(c.l < 0); + + auto d = allocator.allocateInLayer(5); + EXPECT_TRUE(d.l < 0); +} + +TEST(AtlasAllocator, AllocateSecondLevel) { + + AtlasAllocator allocator; + + auto d0 = allocator.allocateInLayer(1); + EXPECT_TRUE(d0.l == 1 && d0.code == 0); + + auto d1 = allocator.allocateInLayer(1); + EXPECT_TRUE(d1.l == 1 && d1.code == 1); + + auto d2 = allocator.allocateInLayer(1); + EXPECT_TRUE(d2.l == 1 && d2.code == 2); + + auto d3 = allocator.allocateInLayer(1); + EXPECT_TRUE(d3.l == 1 && d3.code == 3); +} + +TEST(AtlasAllocator, AllocateMixed0) { + AtlasAllocator allocator; + + auto e0 = allocator.allocateInLayer(1); + EXPECT_TRUE(e0.l == 1 && e0.code == 0); + + auto e1 = allocator.allocateInLayer(1); + EXPECT_TRUE(e1.l == 1 && e1.code == 1); + + auto e2 = allocator.allocateInLayer(1); + EXPECT_TRUE(e2.l == 1 && e2.code == 2); + + auto e3 = allocator.allocateInLayer(2); + EXPECT_TRUE(e3.l == 2 && e3.code == 12); + + auto e4 = allocator.allocateInLayer(1); + EXPECT_TRUE(e4.l < 0); +} + +TEST(AtlasAllocator, AllocateMixed1) { + AtlasAllocator allocator; + + auto e0 = allocator.allocateInLayer(1); + EXPECT_TRUE(e0.l == 1 && e0.code == 0); + + auto e1 = allocator.allocateInLayer(1); + EXPECT_TRUE(e1.l == 1 && e1.code == 1); + + auto e2 = allocator.allocateInLayer(2); + EXPECT_TRUE(e2.l == 2 && e2.code == 8); + + auto e3 = allocator.allocateInLayer(1); + EXPECT_TRUE(e3.l == 1 && e3.code == 3); +} + +TEST(AtlasAllocator, AllocateMixed2) { + AtlasAllocator allocator; + + auto e0 = allocator.allocateInLayer(1); + EXPECT_TRUE(e0.l == 1 && e0.code == 0); + + auto e1 = allocator.allocateInLayer(1); + EXPECT_TRUE(e1.l == 1 && e1.code == 1); + + auto c0 = allocator.allocateInLayer(2); + EXPECT_TRUE(c0.l == 2 && c0.code == 8); + auto c1 = allocator.allocateInLayer(2); + EXPECT_TRUE(c1.l == 2 && c1.code == 9); + auto c2 = allocator.allocateInLayer(2); + EXPECT_TRUE(c2.l == 2 && c2.code == 10); + auto c3 = allocator.allocateInLayer(2); + EXPECT_TRUE(c3.l == 2 && c3.code == 11); + + auto c4 = allocator.allocateInLayer(2); + EXPECT_TRUE(c4.l == 2 && c4.code == 12); + auto c5 = allocator.allocateInLayer(2); + EXPECT_TRUE(c5.l == 2 && c5.code == 13); + auto c6 = allocator.allocateInLayer(2); + EXPECT_TRUE(c6.l == 2 && c6.code == 14); + auto c7 = allocator.allocateInLayer(2); + EXPECT_TRUE(c7.l == 2 && c7.code == 15); +} + +TEST(AtlasAllocator, AllocateBySize) { + AtlasAllocator allocator(256); + + Viewport vp(0,0,256,256); + auto vp0 = allocator.allocate(256); + EXPECT_EQ(vp0, vp); + + auto vp1 = allocator.allocate(128); + EXPECT_TRUE(vp1.empty()); +} + +TEST(AtlasAllocator, AllocateBySizeOneOfEach) { + AtlasAllocator allocator(256); + + Viewport r0(0,0,128,128); + Viewport r1(128,0,64,64); + Viewport r2(192,0,32,32); + + auto vp0 = allocator.allocate(128); + auto vp1 = allocator.allocate(64); + auto vp2 = allocator.allocate(32); + auto vp3 = allocator.allocate(16); + + EXPECT_EQ(vp0, r0); + EXPECT_EQ(vp1, r1); + EXPECT_EQ(vp2, r2); + EXPECT_TRUE(vp3.empty()); + +} + diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt index bba0607824..906f7441d9 100644 --- a/libs/utils/CMakeLists.txt +++ b/libs/utils/CMakeLists.txt @@ -145,6 +145,7 @@ set(TEST_SRCS test/test_FixedCapacityVector.cpp test/test_Hash.cpp test/test_JobSystem.cpp + test/test_QuadTreeArray.cpp test/test_RangeMap.cpp test/test_StructureOfArrays.cpp test/test_sstream.cpp diff --git a/libs/utils/include/utils/QuadTree.h b/libs/utils/include/utils/QuadTree.h new file mode 100644 index 0000000000..0e139539c6 --- /dev/null +++ b/libs/utils/include/utils/QuadTree.h @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2022 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_UTILS_QUADTREE_H +#define TNT_UTILS_QUADTREE_H + +#include +#include + +#include +#include + +#include +#include +#include + +namespace utils { + +namespace QuadTreeUtils { + +/** + * 16-bits morton encoding + * @param x 8-bits horizontal coordinate + * @param y 8-bits vertical coordinate + * @return morton encoding of (x,y) + */ +static inline constexpr uint16_t morton(uint8_t x, uint8_t y) noexcept { + uint32_t r = x | (uint32_t(y) << 16); + r = (r | (r << 4u)) & 0x0f0f0f0fu; + r = (r | (r << 2u)) & 0x33333333u; + r = (r | (r << 1u)) & 0x55555555u; + return uint16_t(r | (r >> 15u)); +} + +/** + * size of a quad-tree of height `height` + * @param height height of the Quad-tree + * @return the number of elements in the tree + */ +static inline constexpr size_t size(size_t height) noexcept { + return QuadTreeUtils::morton(uint8_t((1u << height) - 1u), 0u); +} + +/** + * Index in the QuadTreeArray of a Quad-tree node referenced by its height and code + * @param l height of the node + * @param code morton code of the node + * @return index in the QuadTreeArray of this node + */ +static inline constexpr size_t index(size_t l, size_t code) noexcept { + return size(l) + code; +} + +/** + * Index in the QuadTreeArray of the parent of the specified node + * @param l height of the node + * @param code morton code of the node + * @return index in the QuadTreeArray of this node's parent + */ +static inline constexpr size_t parent(size_t l, size_t code) noexcept { + assert_invariant(l > 0); + return index(l - 1u, code >> 2u); +} + +// integrated unit-tests! +static_assert(size(0) == 0); +static_assert(size(1) == 1); +static_assert(size(2) == 5); +static_assert(size(3) == 21); +static_assert(size(4) == 85); + +} // namespace QuadTreeUtils + +/** + * A Quad-tree encoded as an array. + * @tparam T Type of the quad-tree nodes + * @tparam HEIGHT Height of the quad-tree, muse be <= 4 + */ +template +class QuadTreeArray : public std::array { + static_assert(HEIGHT <= 4, "QuadTreeArray height must be <= 4"); + + // Simple fixed capacity stack + template::value>::type> + class stack { + TYPE mElements[CAPACITY]; + size_t mSize = 0; + public: + bool empty() const noexcept { return mSize == 0; } + void push(TYPE const& v) noexcept { + assert_invariant(mSize < CAPACITY); + mElements[mSize++] = v; + } + void pop() noexcept { + assert_invariant(mSize > 0); + --mSize; + } + const TYPE& back() const noexcept { + return mElements[mSize - 1]; + } + }; + +public: + using code_t = uint8_t; + + struct NodeId { + int8_t l; // height of the node or -1 if invalid + code_t code; // morton code of the node + }; + + enum class TraversalResult { + EXIT, // end traversal + RECURSE, // proceed with the children + SKIP_CHILDREN // skip children + }; + + static inline constexpr size_t height() noexcept { + return HEIGHT; + } + + /** + * non-recursive depth-first traversal + * + * @tparam Process closure to process each visited node + * @param l height of the node to start with + * @param code code of the node to start with + * @param h maximum height to visit, must be < height() + * @param process closure to process each visited node + */ + template>::type> + static void traverse(int8_t l, code_t code, size_t maxHeight, Process&& process) noexcept { + assert_invariant(maxHeight < height()); + const int8_t h = int8_t(maxHeight); + stack stack; + stack.push({ l, code }); + while (!stack.empty()) { + NodeId curr = stack.back(); + stack.pop(); + TraversalResult r = process(curr); + if (r == TraversalResult::EXIT) { + break; + } + if (r == TraversalResult::RECURSE && curr.l < h) { + for (size_t c = 0; c < 4; c++) { + stack.push({ + int8_t(curr.l + 1u), + code_t((curr.code << 2u) | (3u - c)) + }); + } + } + } + } + + template>::type> + static void traverse(int8_t l, code_t code, Node&& process) noexcept { + traverse(l, code, height() - 1, std::forward(process)); + } +}; + +} // namespace utils + +#endif //TNT_UTILS_QUADTREE_H diff --git a/libs/utils/test/test_QuadTreeArray.cpp b/libs/utils/test/test_QuadTreeArray.cpp new file mode 100644 index 0000000000..15b25cd5e7 --- /dev/null +++ b/libs/utils/test/test_QuadTreeArray.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 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 + +#include + +using namespace utils; + +TEST(QuadTreeArrayTest, TraversalDFS) { + + using QuadTree = QuadTreeArray; + QuadTree qt; + QuadTree::NodeId indices[qt.size()]; + + QuadTree::traverse(0, 0, + [&](auto const& curr) -> QuadTree::TraversalResult { + size_t i = QuadTreeUtils::index(curr.l, curr.code); + indices[i] = curr; + return QuadTree::TraversalResult::RECURSE; + }); + + size_t i = 0; + for (size_t y = 0; y < QuadTree::height(); y++) { + for (size_t x = 0; x < (1 << 2 * y); x++, i++) { + EXPECT_EQ(indices[i].l, y); + EXPECT_EQ(indices[i].code, x); + } + } +}