From f34cce56149473be4d006dcc04a92773dd2c9294 Mon Sep 17 00:00:00 2001 From: Magnus Norddahl Date: Sun, 16 Mar 2025 18:42:49 +0100 Subject: [PATCH] Replace RectPacker with a version that can do dynamic allocations --- libraries/dp_rect_pack/dp_rect_pack.h | 672 ------------------ src/CMakeLists.txt | 2 +- .../hwrenderer/data/hw_levelmesh.cpp | 22 +- .../rendering/hwrenderer/data/hw_levelmesh.h | 4 +- .../hwrenderer/data/hw_rectpacker.cpp | 280 ++++++++ .../rendering/hwrenderer/data/hw_rectpacker.h | 93 +++ .../rendering/vulkan/vk_lightmapper.cpp | 14 +- src/common/rendering/vulkan/vk_lightmapper.h | 3 - 8 files changed, 393 insertions(+), 697 deletions(-) delete mode 100644 libraries/dp_rect_pack/dp_rect_pack.h create mode 100644 src/common/rendering/hwrenderer/data/hw_rectpacker.cpp create mode 100644 src/common/rendering/hwrenderer/data/hw_rectpacker.h diff --git a/libraries/dp_rect_pack/dp_rect_pack.h b/libraries/dp_rect_pack/dp_rect_pack.h deleted file mode 100644 index 48c07162e..000000000 --- a/libraries/dp_rect_pack/dp_rect_pack.h +++ /dev/null @@ -1,672 +0,0 @@ -/* - * Rectangle packing library. - * v1.1.3 - * - * Copyright (c) 2017-2021 Daniel Plakhotich - * - * This software is provided 'as-is', without any express or implied - * warranty. In no event will the authors be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, subject to the following restrictions: - * - * 1. The origin of this software must not be misrepresented; you must not - * claim that you wrote the original software. If you use this software - * in a product, an acknowledgement in the product documentation would be - * appreciated but is not required. - * 2. Altered source versions must be plainly marked as such, and must not be - * misrepresented as being the original software. - * 3. This notice may not be removed or altered from any source distribution. - */ - - -#ifndef DP_RECT_PACK_H -#define DP_RECT_PACK_H - -#include -#include -#include - - -#define DP_RECT_PACK_VERSION_MAJOR 1 -#define DP_RECT_PACK_VERSION_MINOR 1 -#define DP_RECT_PACK_VERSION_PATCH 3 - - -namespace dp { -namespace rect_pack { - - -/** - * Status of the RectPacker::InsertResult. - * - * Only InsertStatus::ok indicates a successful insertion; - * all other values are kinds of errors. - */ -struct InsertStatus { - enum Type { - ok, ///< Successful insertion - negativeSize, ///< Width and/or height is negative - zeroSize, ///< Width and/or height is zero - - /** - * Rectangle is too big to fit in a single page. - * - * Width and/or height of the rectangle exceeds the maximum - * size a single page can hold, which is the maximum page size - * minus the padding. - * - * \sa RectPacker::RectPacker() - */ - rectTooBig - }; -}; - - -// A note on the implementation. -// The current algorithm is absolutely the same as in version 1.0.0, -// except that we only keep the leaf nodes of the binary tree. This -// dramatically improves performance and reduces memory usage, but -// growDown() and growRight() methods are harder to understand -// because the leafs insertion order depends on several layers of -// parent branches that don't physically exist. I added comments to -// help you visualize what happens, but it will probably be easier -// to just look at the code of the version 1.0.0. - - -/** - * Rectangle packer. - * - * GeomT is not required to hold negative numbers, and thus can be - * an unsigned integer. It's also possible to use a floating-point - * or a custom numeric type. - * - * A custom type for GeomT should support: - * * Implicit construction from an integer >= 0 - * * Addition and subtraction (including compound assignment) - * * Comparison - * - * \tparam GeomT numeric type to use for geometry - */ -template -class RectPacker { -public: - struct Spacing { - GeomT x; ///< Horizontal spacing - GeomT y; ///< Vertical spacing - - /** - * Construct Spacing with the same spacing for both dimensions. - */ - explicit Spacing(GeomT spacing) - : x(spacing) - , y(spacing) - {} - - Spacing(GeomT x, GeomT y) - : x(x) - , y(y) - {} - }; - - struct Padding { - GeomT top; - GeomT bottom; - GeomT left; - GeomT right; - - /** - * Construct Padding with the same padding for all sides. - */ - explicit Padding(GeomT padding) - : top(padding) - , bottom(padding) - , left(padding) - , right(padding) - {} - - Padding(GeomT top, GeomT bottom, GeomT left, GeomT right) - : top(top) - , bottom(bottom) - , left(left) - , right(right) - {} - }; - - struct Position { - GeomT x; - GeomT y; - - Position() - : x() - , y() - {} - - Position(GeomT x, GeomT y) - : x(x) - , y(y) - {} - }; - - /** - * Result returned by RectPacker::insert(). - */ - struct InsertResult { - /** - * Status of the insertion. - * - * \warning If InsertResult.status is not InsertStatus::ok, - * values of all other fields of InsertResult are undefined. - */ - InsertStatus::Type status; - - /** - * Position of the inserted rectangle within the page. - */ - Position pos; - - /** - * Index of the page in which the rectangle was inserted. - * - * \sa getPageSize() - */ - std::size_t pageIndex; - }; - - /** - * RectPacker constructor. - * - * maxPageWidth and maxPageHeight define the maximum size of - * a single page, including the padding. Depending on this limit - * and the features of GeomT, a RectPacker can work in multipage - * or infinite single-page mode. - * - * To enable infinite single-page mode, you have two choices, - * depending on the properties of GeomT: - * * If GeomT has a physical limit (like any standard integer), - * you can set the maximum size to the maximum positive - * value GeomT can hold. - * * Otherwise, if GeomT is a floating-point type or a custom - * unbounded type, you can set the maximum size to a huge - * value or, if supported by the type, a magic value that - * always bigger than any finite number (like a positive - * infinity for floating-point types). - * - * If GeomT can hold negative values, the maximum page size, spacing, - * and padding will be clamped to 0. Keep in mind that if the - * maximum page size is 0, or if the total padding greater or equal - * to the maximum page size, pages will have no free space for - * rectangles, and all calls to insert() will result in - * InsertStatus::rectTooBig. - * - * \param maxPageWidth maximum width of a page, including - * the horizontal padding - * \param maxPageHeight maximum height of a page, including - * the vertical padding - * \param rectsSpacing space between rectangles - * \param pagePadding space between rectangles and edges of a page - */ - RectPacker( - GeomT maxPageWidth, GeomT maxPageHeight, - const Spacing& rectsSpacing = Spacing(0), - const Padding& pagePadding = Padding(0)) - : ctx(maxPageWidth, maxPageHeight, rectsSpacing, pagePadding) - , pages(1) - {} - - /** - * Return the current number of pages. - * - * \returns number of pages (always > 0) - */ - std::size_t getNumPages() const - { - return pages.size(); - } - - /** - * Return the current size of the page. - * - * \param pageIndex index of the page in range [0..getNumPages()) - * \param[out] width width of the page - * \param[out] height height of the page - * - * \sa getNumPages(), InsertResult::pageIndex - */ - void getPageSize(std::size_t pageIndex, GeomT& width, GeomT& height) const - { - const Size size = pages[pageIndex].getSize(ctx); - width = size.w; - height = size.h; - } - - /** - * Insert a rectangle. - * - * The rectangles you'll feed to insert() should be sorted in - * descending order by comparing first by height, then by width. - * A comparison function for std::sort may look like the following: - * \code - * bool compare(const T& a, const T& b) - * { - * if (a.height != b.height) - * return a.height > b.height; - * else - * return a.width > b.width; - * } - * \endcode - * - * \param width width of the rectangle - * \param height height of the rectangle - * \returns InsertResult - */ - InsertResult insert(GeomT width, GeomT height); -private: - struct Size { - GeomT w; - GeomT h; - - Size(GeomT w, GeomT h) - : w(w) - , h(h) - {} - }; - - struct Context; - class Page { - public: - Page() - : nodes() - , rootSize(0, 0) - , growDownRootBottomIdx(0) - {} - - Size getSize(const Context& ctx) const - { - return Size( - ctx.padding.left + rootSize.w + ctx.padding.right, - ctx.padding.top + rootSize.h + ctx.padding.bottom); - } - - bool insert(Context& ctx, const Size& rect, Position& pos); - private: - struct Node { - Position pos; - Size size; - - Node(GeomT x, GeomT y, GeomT w, GeomT h) - : pos(x, y) - , size(w, h) - {} - }; - - // Leaf nodes of the binary tree in depth-first order - std::vector nodes; - Size rootSize; - // The index of the first leaf bottom node of the new root - // created in growDown(). See the method for more details. - std::size_t growDownRootBottomIdx; - - bool tryInsert(Context& ctx, const Size& rect, Position& pos); - bool findNode( - const Size& rect, - std::size_t& nodeIdx, Position& pos) const; - void subdivideNode( - Context& ctx, std::size_t nodeIdx, const Size& rect); - bool tryGrow(Context& ctx, const Size& rect, Position& pos); - void growDown(Context& ctx, const Size& rect, Position& pos); - void growRight(Context& ctx, const Size& rect, Position& pos); - }; - - struct Context { - Size maxSize; - Spacing spacing; - Padding padding; - - Context( - GeomT maxPageWidth, GeomT maxPageHeight, - const Spacing& rectsSpacing, const Padding& pagePadding); - - static void subtractPadding(GeomT& padding, GeomT& size); - }; - - Context ctx; - std::vector pages; -}; - - -template -typename RectPacker::InsertResult -RectPacker::insert(GeomT width, GeomT height) -{ - InsertResult result; - - if (width < 0 || height < 0) { - result.status = InsertStatus::negativeSize; - return result; - } - - if (width == 0 || height == 0) { - result.status = InsertStatus::zeroSize; - return result; - } - - if (width > ctx.maxSize.w || height > ctx.maxSize.h) { - result.status = InsertStatus::rectTooBig; - return result; - } - - const Size rect(width, height); - - for (std::size_t i = 0; i < pages.size(); ++i) - if (pages[i].insert(ctx, rect, result.pos)) { - result.status = InsertStatus::ok; - result.pageIndex = i; - return result; - } - - pages.push_back(Page()); - Page& page = pages.back(); - page.insert(ctx, rect, result.pos); - result.status = InsertStatus::ok; - result.pageIndex = pages.size() - 1; - - return result; -} - - -template -bool RectPacker::Page::insert( - Context& ctx, const Size& rect, Position& pos) -{ - assert(rect.w > 0); - assert(rect.w <= ctx.maxSize.w); - assert(rect.h > 0); - assert(rect.h <= ctx.maxSize.h); - - // The first insertion should be handled especially since - // growRight() and growDown() add spacing between the root - // and the inserted rectangle. - if (rootSize.w == 0) { - rootSize = rect; - pos.x = ctx.padding.left; - pos.y = ctx.padding.top; - - return true; - } - - return tryInsert(ctx, rect, pos) || tryGrow(ctx, rect, pos); -} - - -template -bool RectPacker::Page::tryInsert( - Context& ctx, const Size& rect, Position& pos) -{ - std::size_t nodeIdx; - if (findNode(rect, nodeIdx, pos)) { - subdivideNode(ctx, nodeIdx, rect); - return true; - } - - return false; -} - - -template -bool RectPacker::Page::findNode( - const Size& rect, std::size_t& nodeIdx, Position& pos) const -{ - for (nodeIdx = 0; nodeIdx < nodes.size(); ++nodeIdx) { - const Node& node = nodes[nodeIdx]; - if (rect.w <= node.size.w && rect.h <= node.size.h) { - pos = node.pos; - return true; - } - } - - return false; -} - - -/** - * Called after a rectangle was inserted in the top left corner of - * a free node to create child nodes from free space, if any. - * - * The node is first cut horizontally along the rect's bottom, - * then vertically along the right edge of the rect. Splitting - * that way is crucial for the algorithm to work correctly. - * - * +---+ - * | | - * +---+---+ - * | | - * +-------+ - */ -template -void RectPacker::Page::subdivideNode( - Context& ctx, std::size_t nodeIdx, const Size& rect) -{ - assert(nodeIdx < nodes.size()); - - Node& node = nodes[nodeIdx]; - - assert(node.size.w >= rect.w); - const GeomT rightW = node.size.w - rect.w; - const bool hasSpaceRight = rightW > ctx.spacing.x; - - assert(node.size.h >= rect.h); - const GeomT bottomH = node.size.h - rect.h; - const bool hasSpaceBelow = bottomH > ctx.spacing.y; - - if (hasSpaceRight) { - // Right node replaces the current - - const GeomT bottomX = node.pos.x; - const GeomT bottomW = node.size.w; - - node.pos.x += rect.w + ctx.spacing.x; - node.size.w = rightW - ctx.spacing.x; - node.size.h = rect.h; - - if (hasSpaceBelow) { - nodes.insert( - nodes.begin() + nodeIdx + 1, - Node( - bottomX, - node.pos.y + rect.h + ctx.spacing.y, - bottomW, - bottomH - ctx.spacing.y)); - - if (nodeIdx <= growDownRootBottomIdx) - ++growDownRootBottomIdx; - } - } else if (hasSpaceBelow) { - // Bottom node replaces the current - node.pos.y += rect.h + ctx.spacing.y; - node.size.h = bottomH - ctx.spacing.y; - } else { - nodes.erase(nodes.begin() + nodeIdx); - if (nodeIdx < growDownRootBottomIdx) - --growDownRootBottomIdx; - } -} - - -template -bool RectPacker::Page::tryGrow( - Context& ctx, const Size& rect, Position& pos) -{ - assert(ctx.maxSize.w >= rootSize.w); - const GeomT freeW = ctx.maxSize.w - rootSize.w; - assert(ctx.maxSize.h >= rootSize.h); - const GeomT freeH = ctx.maxSize.h - rootSize.h; - - const bool canGrowDown = ( - freeH >= rect.h && freeH - rect.h >= ctx.spacing.y); - const bool mustGrowDown = ( - canGrowDown - && freeW >= ctx.spacing.x - && (rootSize.w + ctx.spacing.x - >= rootSize.h + rect.h + ctx.spacing.y)); - if (mustGrowDown) { - growDown(ctx, rect, pos); - return true; - } - - const bool canGrowRight = ( - freeW >= rect.w && freeW - rect.w >= ctx.spacing.x); - if (canGrowRight) { - growRight(ctx, rect, pos); - return true; - } - - if (canGrowDown) { - growDown(ctx, rect, pos); - return true; - } - - return false; -} - - -template -void RectPacker::Page::growDown( - Context& ctx, const Size& rect, Position& pos) -{ - assert(ctx.maxSize.h > rootSize.h); - assert(ctx.maxSize.h - rootSize.h >= rect.h); - assert(ctx.maxSize.h - rootSize.h - rect.h >= ctx.spacing.y); - - pos.x = ctx.padding.left; - pos.y = ctx.padding.top + rootSize.h + ctx.spacing.y; - - if (rootSize.w < rect.w) { - if (rect.w - rootSize.w > ctx.spacing.x) { - // The auxiliary node becomes the right child of the new - // root. It contains the current root (bottom child) and - // free space at the current root's right (right child). - nodes.insert( - nodes.begin(), - Node( - ctx.padding.left + rootSize.w + ctx.spacing.x, - ctx.padding.top, - rect.w - rootSize.w - ctx.spacing.x, - rootSize.h)); - ++growDownRootBottomIdx; - } - - rootSize.w = rect.w; - } else if (rootSize.w - rect.w > ctx.spacing.x) { - // Free space at the right of the inserted rect becomes the - // right child of the rect's node, which in turn is the - // bottom child of the new root. - nodes.insert( - nodes.begin() + growDownRootBottomIdx, - Node( - pos.x + rect.w + ctx.spacing.x, - pos.y, - rootSize.w - rect.w - ctx.spacing.x, - rect.h)); - - // The inserted node is visited before the node from the next - // growDown() since the current new root will be the right - // child of the next root. - ++growDownRootBottomIdx; - } - - rootSize.h += ctx.spacing.y + rect.h; -} - - -template -void RectPacker::Page::growRight( - Context& ctx, const Size& rect, Position& pos) -{ - assert(ctx.maxSize.w > rootSize.w); - assert(ctx.maxSize.w - rootSize.w >= rect.w); - assert(ctx.maxSize.w - rootSize.w - rect.w >= ctx.spacing.x); - - pos.x = ctx.padding.left + rootSize.w + ctx.spacing.x; - pos.y = ctx.padding.top; - - if (rootSize.h < rect.h) { - if (rect.h - rootSize.h > ctx.spacing.y) - // The auxiliary node becomes the bottom child of the - // new root. It contains the current root (right child) - // and free space at the current root's bottom, if any - // (bottom child). - nodes.insert( - nodes.end(), - Node( - ctx.padding.left, - ctx.padding.top + rootSize.h + ctx.spacing.y, - rootSize.w, - rect.h - rootSize.h - ctx.spacing.y)); - - rootSize.h = rect.h; - } else if (rootSize.h - rect.h > ctx.spacing.y) { - // Free space at the bottom of the inserted rect becomes the - // bottom child of the rect's node, which in turn is the - // right child of the new root node. - nodes.insert( - nodes.begin(), - Node( - pos.x, - pos.y + rect.h + ctx.spacing.y, - rect.w, - rootSize.h - rect.h - ctx.spacing.y)); - ++growDownRootBottomIdx; - } - - rootSize.w += ctx.spacing.x + rect.w; -} - - -template -RectPacker::Context::Context( - GeomT maxPageWidth, GeomT maxPageHeight, - const Spacing& rectsSpacing, const Padding& pagePadding) - : maxSize(maxPageWidth, maxPageHeight) - , spacing(rectsSpacing) - , padding(pagePadding) -{ - if (maxSize.w < 0) - maxSize.w = 0; - if (maxSize.h < 0) - maxSize.h = 0; - - if (spacing.x < 0) - spacing.x = 0; - if (spacing.y < 0) - spacing.y = 0; - - subtractPadding(padding.top, maxSize.h); - subtractPadding(padding.bottom, maxSize.h); - subtractPadding(padding.left, maxSize.w); - subtractPadding(padding.right, maxSize.w); -} - - -template -void RectPacker::Context::subtractPadding( - GeomT& padding, GeomT& size) -{ - if (padding < 0) - padding = 0; - else if (padding < size) - size -= padding; - else { - padding = size; - size = 0; - } -} - - -} // namespace rect_pack -} // namespace dp - - -#endif // DP_RECT_PACK_H \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1b2ac3af4..806e92289 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1124,6 +1124,7 @@ set (PCH_SOURCES common/rendering/hwrenderer/data/hw_levelmesh.cpp common/rendering/hwrenderer/data/hw_meshbuilder.cpp common/rendering/hwrenderer/data/hw_lightprobe.cpp + common/rendering/hwrenderer/data/hw_rectpacker.cpp common/rendering/hwrenderer/postprocessing/hw_postprocessshader.cpp common/rendering/hwrenderer/postprocessing/hw_postprocess.cpp common/rendering/hwrenderer/postprocessing/hw_postprocess_cvars.cpp @@ -1311,7 +1312,6 @@ include_directories( ../libraries/ZVulkan/include ../libraries/ZWidget/include ../libraries/webp/include - ../libraries/dp_rect_pack ${SYSTEM_SOURCES_DIR} ) diff --git a/src/common/rendering/hwrenderer/data/hw_levelmesh.cpp b/src/common/rendering/hwrenderer/data/hw_levelmesh.cpp index 1f2fc6b7a..0a5219cff 100644 --- a/src/common/rendering/hwrenderer/data/hw_levelmesh.cpp +++ b/src/common/rendering/hwrenderer/data/hw_levelmesh.cpp @@ -170,17 +170,17 @@ void LevelMesh::PackStaticLightmapAtlas() std::sort(sortedTiles.begin(), sortedTiles.end(), [](LightmapTile* a, LightmapTile* b) { return a->AtlasLocation.Height != b->AtlasLocation.Height ? a->AtlasLocation.Height > b->AtlasLocation.Height : a->AtlasLocation.Width > b->AtlasLocation.Width; }); // We do not need to add spacing here as this is already built into the tile size itself. - RectPacker packer(Lightmap.TextureSize, Lightmap.TextureSize, RectPacker::Spacing(0), RectPacker::Padding(0)); + RectPacker packer(Lightmap.TextureSize, Lightmap.TextureSize, 0); for (LightmapTile* tile : sortedTiles) { - auto result = packer.insert(tile->AtlasLocation.Width, tile->AtlasLocation.Height); - tile->AtlasLocation.X = result.pos.x; - tile->AtlasLocation.Y = result.pos.y; - tile->AtlasLocation.ArrayIndex = (int)result.pageIndex; + auto result = packer.Alloc(tile->AtlasLocation.Width, tile->AtlasLocation.Height); + tile->AtlasLocation.X = result->X; + tile->AtlasLocation.Y = result->Y; + tile->AtlasLocation.ArrayIndex = (int)result->PageIndex; } - Lightmap.TextureCount = (int)packer.getNumPages(); + Lightmap.TextureCount = (int)packer.GetNumPages(); // Calculate final texture coordinates for (int i = 0, count = Mesh.Surfaces.Size(); i < count; i++) @@ -225,14 +225,14 @@ void LevelMesh::PackDynamicLightmapAtlas() std::sort(sortedTiles.begin(), sortedTiles.end(), [](LightmapTile* a, LightmapTile* b) { return a->AtlasLocation.Height != b->AtlasLocation.Height ? a->AtlasLocation.Height > b->AtlasLocation.Height : a->AtlasLocation.Width > b->AtlasLocation.Width; }); // We do not need to add spacing here as this is already built into the tile size itself. - RectPacker packer(Lightmap.TextureSize, Lightmap.TextureSize, RectPacker::Spacing(0), RectPacker::Padding(0)); + RectPacker packer(Lightmap.TextureSize, Lightmap.TextureSize, 0); for (LightmapTile* tile : sortedTiles) { - auto result = packer.insert(tile->AtlasLocation.Width, tile->AtlasLocation.Height); - tile->AtlasLocation.X = result.pos.x; - tile->AtlasLocation.Y = result.pos.y; - tile->AtlasLocation.ArrayIndex = Lightmap.TextureCount; // (int)result.pageIndex; + auto result = packer.Alloc(tile->AtlasLocation.Width, tile->AtlasLocation.Height); + tile->AtlasLocation.X = result->X; + tile->AtlasLocation.Y = result->Y; + tile->AtlasLocation.ArrayIndex = Lightmap.TextureCount; // (int)result->PageIndex; } for (int surfIndex : Lightmap.DynamicSurfaces) diff --git a/src/common/rendering/hwrenderer/data/hw_levelmesh.h b/src/common/rendering/hwrenderer/data/hw_levelmesh.h index 842010dfa..33a903ecc 100644 --- a/src/common/rendering/hwrenderer/data/hw_levelmesh.h +++ b/src/common/rendering/hwrenderer/data/hw_levelmesh.h @@ -11,13 +11,11 @@ #include "hw_levelmeshsurface.h" #include "hw_materialstate.h" #include "hw_surfaceuniforms.h" +#include "hw_rectpacker.h" #include "engineerrors.h" #include #include -#include -typedef dp::rect_pack::RectPacker RectPacker; - struct LevelMeshTileStats; struct GeometryAllocInfo diff --git a/src/common/rendering/hwrenderer/data/hw_rectpacker.cpp b/src/common/rendering/hwrenderer/data/hw_rectpacker.cpp new file mode 100644 index 000000000..ae7c1d003 --- /dev/null +++ b/src/common/rendering/hwrenderer/data/hw_rectpacker.cpp @@ -0,0 +1,280 @@ + +#include "hw_rectpacker.h" +#include "engineerrors.h" + +// This is a dynamic atlas texture packer +// It is used by the level mesh to dynamically allocate and free room in the lightmap atlas textures +// +// See https://mozillagfx.wordpress.com/2021/02/04/improving-texture-atlas-allocation-in-webrender/ +// +// This code doesn't use entirely the same algorithm, but it uses the same shelf terminology. +// (look at the pictures) +// +// Plus it gives you a general idea about what dynamic atlas texture packing is about. + +RectPacker::RectPacker(int width, int height, int padding) : PageWidth(width), PageHeight(height), Padding(padding) +{ +} + +RectPackerItem* RectPacker::Alloc(int width, int height) +{ + width += Padding * 2; + height += Padding * 2; + + if (width < 0 || height < 0 || width > PageWidth || height >= PageHeight) + I_FatalError("Tile too large for RectPacker!"); + + // How much wasted space we want to allow per shelf + int threshold = width * 2; + + // Search pages for room + for (RectPackerPage& page : Pages) + { + // Look for space on an existing shelf + for (auto& shelf : page.Shelves) + { + if (shelf->Width >= width && shelf->Width <= threshold) + { + // We found a shelf with an acceptable width + + for (RectPackerItem* item = shelf->AvailableList; item; item = item->NextAvailable) + { + if (height <= item->Height) + { + // We found a free slot with room! + return AllocateRoom(item, width, height); + } + } + } + } + + // No shelf found. Do we have room for a new shelf on the page? + int nextX = (page.Shelves.size() != 0) ? page.Shelves.back()->X + page.Shelves.back()->Width : 0; + int availableShelfSpace = PageWidth - nextX; + if (width <= availableShelfSpace) + { + // We have room. Create a shelf and allocate room in it + return CreateShelf(&page, nextX, width, height); + } + } + + // No space on any pages. Create a new one + Pages.Push(RectPackerPage((int)Pages.size())); + return CreateShelf(&Pages.back(), 0, width, height); +} + +RectPackerItem* RectPacker::AllocateRoom(RectPackerItem* item, int width, int height) +{ + if (item->Height == height) + { + // Perfect fit. Just remove it from the available space list. + RemoveFromAvailableList(item); + item->Width = width; + + AddPadding(item); + return item; + } + else + { + // We still have space left. Insert a new item. + RectPackerItem* newitem = AllocItem(item->Shelf); + AddToItemList(newitem, item); + newitem->X = item->X; + newitem->Y = item->Y; + newitem->Width = width; + newitem->Height = height; + newitem->PageIndex = item->PageIndex; + + // Update the available space item to only contain what is left + item->Y += height; + item->Height -= height; + + AddPadding(newitem); + return newitem; + } +} + +RectPackerItem* RectPacker::CreateShelf(RectPackerPage* page, int x, int width, int height) +{ + // Create shelf object + page->Shelves.Push(std::make_unique()); + RectPackerShelf* shelf = page->Shelves.back().get(); + shelf->X = x; + shelf->Width = width; + + // Fill it with empty space. + RectPackerItem* item = AllocItem(shelf); + AddToItemList(item); + item->X = x; + item->Y = 0; + item->Width = 0; + item->Height = PageHeight; + item->PageIndex = page->PageIndex; + AddToAvailableList(item); + + // Allocate room for our rect + return AllocateRoom(item, width, height); +} + +void RectPacker::AddPadding(RectPackerItem* item) +{ + item->X += Padding; + item->Y += Padding; + item->Width -= Padding * 2; + item->Height -= Padding * 2; + + if (item->X < 0 || item->Y < 0 || item->Width <= 0 || item->Height <= 0 || item->X + item->Width > PageWidth || item->Y + item->Height > PageHeight || item->PageIndex >= (int)Pages.size()) + I_FatalError("RectPackerItem is out of bounds!"); + +#ifdef VALIDATE_RECTPACKER + ValidateAllocation(item, false); +#endif +} + +void RectPacker::RemovePadding(RectPackerItem* item) +{ +#ifdef VALIDATE_RECTPACKER + ValidateAllocation(item, true); +#endif + + item->X -= Padding; + item->Y -= Padding; + item->Width += Padding * 2; + item->Height += Padding * 2; +} + +void RectPacker::Free(RectPackerItem* item) +{ + // Already freed or no item? + if (!item || item->IsAvailable) + return; + + RemovePadding(item); + AddToAvailableList(item); + + // If next item is available space we can merge them + if (item->NextItem && item->NextItem->IsAvailable) + { + item->Height += item->NextItem->Height; + FreeItem(item->NextItem); + } + + // If previous item is available space we can merge them + if (item->PrevItem && item->PrevItem->IsAvailable) + { + item->PrevItem->Height += item->Height; + FreeItem(item); + } +} + +RectPackerItem* RectPacker::AllocItem(RectPackerShelf* shelf) +{ + if (ItemFreeList.size() != 0) + { + RectPackerItem* item = ItemFreeList.back(); + ItemFreeList.Pop(); + item->Shelf = shelf; + return item; + } + + Items.Push(std::make_unique()); + Items.back()->Shelf = shelf; + return Items.back().get(); +} + +void RectPacker::FreeItem(RectPackerItem* item) +{ + RemoveFromItemList(item); + RemoveFromAvailableList(item); + item->Shelf = nullptr; + ItemFreeList.Push(item); +} + +void RectPacker::AddToItemList(RectPackerItem* item, RectPackerItem* insertAt) +{ + item->PrevItem = insertAt ? insertAt->PrevItem : nullptr; + item->NextItem = insertAt; + if (insertAt && insertAt->PrevItem) + insertAt->PrevItem->NextItem = item; + if (insertAt) + insertAt->PrevItem = item; + if (item->Shelf->ItemList == insertAt) + item->Shelf->ItemList = item; +} + +void RectPacker::RemoveFromItemList(RectPackerItem* item) +{ + if (!item->Shelf) + return; + + if (item->Shelf->ItemList == item) + item->Shelf->ItemList = item->NextItem; + + if (item->PrevItem) + item->PrevItem->NextItem = item->NextItem; + + if (item->NextItem) + item->NextItem->PrevItem = item->PrevItem; + + item->PrevItem = nullptr; + item->NextItem = nullptr; +} + +void RectPacker::AddToAvailableList(RectPackerItem* item) +{ + item->IsAvailable = true; + + item->NextAvailable = item->Shelf->AvailableList; + if (item->NextAvailable) + item->NextAvailable->PrevAvailable = item; + item->Shelf->AvailableList = item; +} + +void RectPacker::RemoveFromAvailableList(RectPackerItem* item) +{ + if (!item->Shelf || !item->IsAvailable) + return; + + if (item->Shelf->AvailableList == item) + item->Shelf->AvailableList = item->NextAvailable; + + if (item->PrevAvailable) + item->PrevAvailable->NextAvailable = item->NextAvailable; + + if (item->NextAvailable) + item->NextAvailable->PrevAvailable = item->PrevAvailable; + + item->IsAvailable = false; + item->PrevAvailable = nullptr; + item->NextAvailable = nullptr; +} + +#ifdef VALIDATE_RECTPACKER +void RectPacker::ValidateAllocation(RectPackerItem* item, bool dealloc) +{ + // This crudely checks if an allocation is already in use by an existing allocation + + size_t bufsize = (item->PageIndex + 1) * PageWidth * PageHeight; + while (ValidationBuffer.size() < bufsize) + ValidationBuffer.push_back(0); + + uint8_t oldvalue = dealloc ? 1 : 0; + uint8_t newvalue = dealloc ? 0 : 1; + + uint8_t* page = ValidationBuffer.Data() + item->PageIndex * PageWidth * PageHeight; + int pitch = PageWidth; + int x = item->X; + int y = item->Y; + int w = item->Width; + int h = item->Height; + for (int yy = y; yy < y + h; yy++) + { + for (int xx = x; xx < x + w; xx++) + { + if (page[xx + yy * pitch] != oldvalue) + I_FatalError("RectPacker items are corrupted"); + page[xx + yy * pitch] = newvalue; + } + } +} +#endif diff --git a/src/common/rendering/hwrenderer/data/hw_rectpacker.h b/src/common/rendering/hwrenderer/data/hw_rectpacker.h new file mode 100644 index 000000000..d8819108e --- /dev/null +++ b/src/common/rendering/hwrenderer/data/hw_rectpacker.h @@ -0,0 +1,93 @@ +#pragma once + +#include "templates.h" +#include + +// Enable this if you want to test if atlas building is broken internally in the RectPacker +// #define VALIDATE_RECTPACKER + +class RectPacker; +class RectPackerPage; +class RectPackerShelf; +class RectPackerItem; + +class RectPackerItem +{ +public: + int X = 0; + int Y = 0; + int Width = 0; + int Height = 0; + int PageIndex = 0; + +private: + RectPackerShelf* Shelf = nullptr; + + RectPackerItem* PrevItem = nullptr; + RectPackerItem* NextItem = nullptr; + + bool IsAvailable = false; + RectPackerItem* PrevAvailable = nullptr; + RectPackerItem* NextAvailable = nullptr; + + friend class RectPacker; +}; + +class RectPackerShelf +{ +public: + int X = 0; + int Width = 0; + RectPackerItem* ItemList = nullptr; + RectPackerItem* AvailableList = nullptr; +}; + +class RectPackerPage +{ +public: + RectPackerPage(int pageIndex) : PageIndex(pageIndex) { } + + int PageIndex = 0; + TArray> Shelves; +}; + +class RectPacker +{ +public: + RectPacker(int width, int height, int padding); + + RectPackerItem* Alloc(int width, int height); + void Free(RectPackerItem* item); + + int GetNumPages() { return (int)Pages.size(); } + +private: + void AddPadding(RectPackerItem* item); + void RemovePadding(RectPackerItem* item); + + RectPackerItem* AllocateRoom(RectPackerItem* item, int width, int height); + RectPackerItem* CreateShelf(RectPackerPage* page, int x, int width, int height); + + RectPackerItem* AllocItem(RectPackerShelf* shelf); + void FreeItem(RectPackerItem* item); + + void AddToItemList(RectPackerItem* item, RectPackerItem* insertAt = nullptr); + void RemoveFromItemList(RectPackerItem* item); + + void AddToAvailableList(RectPackerItem* item); + void RemoveFromAvailableList(RectPackerItem* item); + + int PageWidth = 0; + int PageHeight = 0; + int Padding = 0; + TArray Pages; + TArray> Items; + TArray ItemFreeList; + +#ifdef VALIDATE_RECTPACKER + void ValidateAllocation(RectPackerItem* item, bool dealloc); + TArray ValidationBuffer; +#endif + + friend class RectPackerPage; +}; diff --git a/src/common/rendering/vulkan/vk_lightmapper.cpp b/src/common/rendering/vulkan/vk_lightmapper.cpp index 3ad559667..faf14a489 100644 --- a/src/common/rendering/vulkan/vk_lightmapper.cpp +++ b/src/common/rendering/vulkan/vk_lightmapper.cpp @@ -121,7 +121,7 @@ void VkLightmapper::SelectTiles(const TArray& tiles) selectedTiles.Clear(); // We use a 3 texel spacing between rectangles so that the blur pass will not pick up anything from a neighbour tile. - RectPacker packer(bakeImageSize, bakeImageSize, RectPacker::Spacing(3), RectPacker::Padding(3)); + RectPacker packer(bakeImageSize, bakeImageSize, 3); for (int i = 0, count = tiles.Size(); i < count; i++) { @@ -131,17 +131,17 @@ void VkLightmapper::SelectTiles(const TArray& tiles) continue; // Only grab surfaces until our bake texture is full - auto result = packer.insert(tile->AtlasLocation.Width, tile->AtlasLocation.Height); - if (result.pageIndex == 0) + auto result = packer.Alloc(tile->AtlasLocation.Width, tile->AtlasLocation.Height); + if (result->PageIndex == 0) { SelectedTile selected; selected.Tile = tile; - selected.X = result.pos.x; - selected.Y = result.pos.y; + selected.X = result->X; + selected.Y = result->Y; selectedTiles.Push(selected); - bakeImage.maxX = std::max(bakeImage.maxX, uint16_t(result.pos.x + tile->AtlasLocation.Width)); - bakeImage.maxY = std::max(bakeImage.maxY, uint16_t(result.pos.y + tile->AtlasLocation.Height)); + bakeImage.maxX = std::max(bakeImage.maxX, uint16_t(result->X + tile->AtlasLocation.Width)); + bakeImage.maxY = std::max(bakeImage.maxY, uint16_t(result->Y + tile->AtlasLocation.Height)); tile->NeedsUpdate = false; } diff --git a/src/common/rendering/vulkan/vk_lightmapper.h b/src/common/rendering/vulkan/vk_lightmapper.h index 3c9675825..8060b7398 100644 --- a/src/common/rendering/vulkan/vk_lightmapper.h +++ b/src/common/rendering/vulkan/vk_lightmapper.h @@ -2,9 +2,6 @@ #include "common/rendering/hwrenderer/data/hw_levelmesh.h" #include "zvulkan/vulkanobjects.h" -#include - -typedef dp::rect_pack::RectPacker RectPacker; class VulkanRenderDevice; class FString;