diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8503fabcf..90748f257 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -995,6 +995,8 @@ add_executable( zdoom WIN32 MACOSX_BUNDLE wi_stuff.cpp zstrformat.cpp zstring.cpp + GuillotineBinPack.cpp + SkylineBinPack.cpp g_doom/a_doommisc.cpp g_heretic/a_hereticmisc.cpp g_hexen/a_hexenmisc.cpp diff --git a/src/GuillotineBinPack.cpp b/src/GuillotineBinPack.cpp new file mode 100644 index 000000000..61ac7a4ce --- /dev/null +++ b/src/GuillotineBinPack.cpp @@ -0,0 +1,633 @@ +/** @file GuillotineBinPack.cpp + @author Jukka Jylänki + + @brief Implements different bin packer algorithms that use the GUILLOTINE data structure. + + This work is released to Public Domain, do whatever you want with it. +*/ + +#include + +#include "GuillotineBinPack.h" + +using namespace std; + +GuillotineBinPack::GuillotineBinPack() +:binWidth(0), +binHeight(0) +{ +} + +GuillotineBinPack::GuillotineBinPack(int width, int height) +{ + Init(width, height); +} + +void GuillotineBinPack::Init(int width, int height) +{ + binWidth = width; + binHeight = height; + +#ifdef _DEBUG + disjointRects.Clear(); +#endif + + // Clear any memory of previously packed rectangles. + usedRectangles.Clear(); + + // We start with a single big free rectangle that spans the whole bin. + Rect n; + n.x = 0; + n.y = 0; + n.width = width; + n.height = height; + + freeRectangles.Clear(); + freeRectangles.Push(n); +} + +void GuillotineBinPack::Insert(TArray &rects, TArray &dst, bool merge, + FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod) +{ + dst.Clear(); + + // Remember variables about the best packing choice we have made so far during the iteration process. + int bestFreeRect = 0; + int bestRect = 0; + bool bestFlipped = false; + + // Pack rectangles one at a time until we have cleared the rects array of all rectangles. + // rects will get destroyed in the process. + while(rects.Size() > 0) + { + // Stores the penalty score of the best rectangle placement - bigger=worse, smaller=better. + int bestScore = INT_MAX; + + for(unsigned i = 0; i < freeRectangles.Size(); ++i) + { + for(unsigned j = 0; j < rects.Size(); ++j) + { + // If this rectangle is a perfect match, we pick it instantly. + if (rects[j].width == freeRectangles[i].width && rects[j].height == freeRectangles[i].height) + { + bestFreeRect = i; + bestRect = j; + bestFlipped = false; + bestScore = INT_MIN; + i = freeRectangles.Size(); // Force a jump out of the outer loop as well - we got an instant fit. + break; + } + // If flipping this rectangle is a perfect match, pick that then. + else if (rects[j].height == freeRectangles[i].width && rects[j].width == freeRectangles[i].height) + { + bestFreeRect = i; + bestRect = j; + bestFlipped = true; + bestScore = INT_MIN; + i = freeRectangles.Size(); // Force a jump out of the outer loop as well - we got an instant fit. + break; + } + // Try if we can fit the rectangle upright. + else if (rects[j].width <= freeRectangles[i].width && rects[j].height <= freeRectangles[i].height) + { + int score = ScoreByHeuristic(rects[j].width, rects[j].height, freeRectangles[i], rectChoice); + if (score < bestScore) + { + bestFreeRect = i; + bestRect = j; + bestFlipped = false; + bestScore = score; + } + } + // If not, then perhaps flipping sideways will make it fit? + else if (rects[j].height <= freeRectangles[i].width && rects[j].width <= freeRectangles[i].height) + { + int score = ScoreByHeuristic(rects[j].height, rects[j].width, freeRectangles[i], rectChoice); + if (score < bestScore) + { + bestFreeRect = i; + bestRect = j; + bestFlipped = true; + bestScore = score; + } + } + } + } + + // If we didn't manage to find any rectangle to pack, abort. + if (bestScore == INT_MAX) + return; + + // Otherwise, we're good to go and do the actual packing. + Rect newNode; + newNode.x = freeRectangles[bestFreeRect].x; + newNode.y = freeRectangles[bestFreeRect].y; + newNode.width = rects[bestRect].width; + newNode.height = rects[bestRect].height; + + if (bestFlipped) + std::swap(newNode.width, newNode.height); + + // Remove the free space we lost in the bin. + SplitFreeRectByHeuristic(freeRectangles[bestFreeRect], newNode, splitMethod); + freeRectangles.Delete(bestFreeRect); + + // Remove the rectangle we just packed from the input list. + rects.Delete(bestRect); + + // Perform a Rectangle Merge step if desired. + if (merge) + MergeFreeList(); + + // Remember the new used rectangle. + usedRectangles.Push(newNode); + + // Check that we're really producing correct packings here. + assert(disjointRects.Add(newNode) == true); + } +} + +/// @return True if r fits inside freeRect (possibly rotated). +bool Fits(const RectSize &r, const Rect &freeRect) +{ + return (r.width <= freeRect.width && r.height <= freeRect.height) || + (r.height <= freeRect.width && r.width <= freeRect.height); +} + +/// @return True if r fits perfectly inside freeRect, i.e. the leftover area is 0. +bool FitsPerfectly(const RectSize &r, const Rect &freeRect) +{ + return (r.width == freeRect.width && r.height == freeRect.height) || + (r.height == freeRect.width && r.width == freeRect.height); +} + +/* +// A helper function for GUILLOTINE-MAXFITTING. Counts how many rectangles fit into the given rectangle +// after it has been split. +void CountNumFitting(const Rect &freeRect, int width, int height, const TArray &rects, + int usedRectIndex, bool splitHorizontal, int &score1, int &score2) +{ + const int w = freeRect.width - width; + const int h = freeRect.height - height; + + Rect bottom; + bottom.x = freeRect.x; + bottom.y = freeRect.y + height; + bottom.height = h; + + Rect right; + right.x = freeRect.x + width; + right.y = freeRect.y; + right.width = w; + + if (splitHorizontal) + { + bottom.width = freeRect.width; + right.height = height; + } + else // Split vertically + { + bottom.width = width; + right.height = freeRect.height; + } + + int fitBottom = 0; + int fitRight = 0; + for(size_t i = 0; i < rects.size(); ++i) + if (i != usedRectIndex) + { + if (FitsPerfectly(rects[i], bottom)) + fitBottom |= 0x10000000; + if (FitsPerfectly(rects[i], right)) + fitRight |= 0x10000000; + + if (Fits(rects[i], bottom)) + ++fitBottom; + if (Fits(rects[i], right)) + ++fitRight; + } + + score1 = min(fitBottom, fitRight); + score2 = max(fitBottom, fitRight); +} +*/ +/* +// Implements GUILLOTINE-MAXFITTING, an experimental heuristic that's really cool but didn't quite work in practice. +void GuillotineBinPack::InsertMaxFitting(TArray &rects, TArray &dst, bool merge, + FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod) +{ + dst.clear(); + int bestRect = 0; + bool bestFlipped = false; + bool bestSplitHorizontal = false; + + // Pick rectangles one at a time and pack the one that leaves the most choices still open. + while(rects.size() > 0 && freeRectangles.size() > 0) + { + int bestScore1 = -1; + int bestScore2 = -1; + + ///\todo Different sort predicates. + clb::sort::QuickSort(&freeRectangles[0], freeRectangles.size(), CompareRectShortSide); + + Rect &freeRect = freeRectangles[0]; + + for(size_t j = 0; j < rects.size(); ++j) + { + int score1; + int score2; + + if (rects[j].width == freeRect.width && rects[j].height == freeRect.height) + { + bestRect = j; + bestFlipped = false; + bestScore1 = bestScore2 = std::numeric_limits::max(); + break; + } + else if (rects[j].width <= freeRect.width && rects[j].height <= freeRect.height) + { + CountNumFitting(freeRect, rects[j].width, rects[j].height, rects, j, false, score1, score2); + + if (score1 > bestScore1 || (score1 == bestScore1 && score2 > bestScore2)) + { + bestRect = j; + bestScore1 = score1; + bestScore2 = score2; + bestFlipped = false; + bestSplitHorizontal = false; + } + + CountNumFitting(freeRect, rects[j].width, rects[j].height, rects, j, true, score1, score2); + + if (score1 > bestScore1 || (score1 == bestScore1 && score2 > bestScore2)) + { + bestRect = j; + bestScore1 = score1; + bestScore2 = score2; + bestFlipped = false; + bestSplitHorizontal = true; + } + } + + if (rects[j].height == freeRect.width && rects[j].width == freeRect.height) + { + bestRect = j; + bestFlipped = true; + bestScore1 = bestScore2 = std::numeric_limits::max(); + break; + } + else if (rects[j].height <= freeRect.width && rects[j].width <= freeRect.height) + { + CountNumFitting(freeRect, rects[j].height, rects[j].width, rects, j, false, score1, score2); + + if (score1 > bestScore1 || (score1 == bestScore1 && score2 > bestScore2)) + { + bestRect = j; + bestScore1 = score1; + bestScore2 = score2; + bestFlipped = true; + bestSplitHorizontal = false; + } + + CountNumFitting(freeRect, rects[j].height, rects[j].width, rects, j, true, score1, score2); + + if (score1 > bestScore1 || (score1 == bestScore1 && score2 > bestScore2)) + { + bestRect = j; + bestScore1 = score1; + bestScore2 = score2; + bestFlipped = true; + bestSplitHorizontal = true; + } + } + } + + if (bestScore1 >= 0) + { + Rect newNode; + newNode.x = freeRect.x; + newNode.y = freeRect.y; + newNode.width = rects[bestRect].width; + newNode.height = rects[bestRect].height; + if (bestFlipped) + std::swap(newNode.width, newNode.height); + + assert(disjointRects.Disjoint(newNode)); + SplitFreeRectAlongAxis(freeRect, newNode, bestSplitHorizontal); + + rects.erase(rects.begin() + bestRect); + + if (merge) + MergeFreeList(); + + usedRectangles.push_back(newNode); +#ifdef _DEBUG + disjointRects.Add(newNode); +#endif + } + + freeRectangles.erase(freeRectangles.begin()); + } +} +*/ + +Rect GuillotineBinPack::Insert(int width, int height, bool merge, FreeRectChoiceHeuristic rectChoice, + GuillotineSplitHeuristic splitMethod) +{ + // Find where to put the new rectangle. + int freeNodeIndex = 0; + Rect newRect = FindPositionForNewNode(width, height, rectChoice, &freeNodeIndex); + + // Abort if we didn't have enough space in the bin. + if (newRect.height == 0) + return newRect; + + // Remove the space that was just consumed by the new rectangle. + SplitFreeRectByHeuristic(freeRectangles[freeNodeIndex], newRect, splitMethod); + freeRectangles.Delete(freeNodeIndex); + + // Perform a Rectangle Merge step if desired. + if (merge) + MergeFreeList(); + + // Remember the new used rectangle. + usedRectangles.Push(newRect); + + // Check that we're really producing correct packings here. + assert(disjointRects.Add(newRect) == true); + + return newRect; +} + +/// Computes the ratio of used surface area to the total bin area. +float GuillotineBinPack::Occupancy() const +{ + ///\todo The occupancy rate could be cached/tracked incrementally instead + /// of looping through the list of packed rectangles here. + unsigned long usedSurfaceArea = 0; + for(unsigned i = 0; i < usedRectangles.Size(); ++i) + usedSurfaceArea += usedRectangles[i].width * usedRectangles[i].height; + + return (float)usedSurfaceArea / (binWidth * binHeight); +} + +/// Returns the heuristic score value for placing a rectangle of size width*height into freeRect. Does not try to rotate. +int GuillotineBinPack::ScoreByHeuristic(int width, int height, const Rect &freeRect, FreeRectChoiceHeuristic rectChoice) +{ + switch(rectChoice) + { + case RectBestAreaFit: return ScoreBestAreaFit(width, height, freeRect); + case RectBestShortSideFit: return ScoreBestShortSideFit(width, height, freeRect); + case RectBestLongSideFit: return ScoreBestLongSideFit(width, height, freeRect); + case RectWorstAreaFit: return ScoreWorstAreaFit(width, height, freeRect); + case RectWorstShortSideFit: return ScoreWorstShortSideFit(width, height, freeRect); + case RectWorstLongSideFit: return ScoreWorstLongSideFit(width, height, freeRect); + default: assert(false); return INT_MAX; + } +} + +int GuillotineBinPack::ScoreBestAreaFit(int width, int height, const Rect &freeRect) +{ + return freeRect.width * freeRect.height - width * height; +} + +int GuillotineBinPack::ScoreBestShortSideFit(int width, int height, const Rect &freeRect) +{ + int leftoverHoriz = abs(freeRect.width - width); + int leftoverVert = abs(freeRect.height - height); + int leftover = min(leftoverHoriz, leftoverVert); + return leftover; +} + +int GuillotineBinPack::ScoreBestLongSideFit(int width, int height, const Rect &freeRect) +{ + int leftoverHoriz = abs(freeRect.width - width); + int leftoverVert = abs(freeRect.height - height); + int leftover = max(leftoverHoriz, leftoverVert); + return leftover; +} + +int GuillotineBinPack::ScoreWorstAreaFit(int width, int height, const Rect &freeRect) +{ + return -ScoreBestAreaFit(width, height, freeRect); +} + +int GuillotineBinPack::ScoreWorstShortSideFit(int width, int height, const Rect &freeRect) +{ + return -ScoreBestShortSideFit(width, height, freeRect); +} + +int GuillotineBinPack::ScoreWorstLongSideFit(int width, int height, const Rect &freeRect) +{ + return -ScoreBestLongSideFit(width, height, freeRect); +} + +Rect GuillotineBinPack::FindPositionForNewNode(int width, int height, FreeRectChoiceHeuristic rectChoice, int *nodeIndex) +{ + Rect bestNode; + memset(&bestNode, 0, sizeof(Rect)); + + int bestScore = INT_MAX; + + /// Try each free rectangle to find the best one for placement. + for(unsigned i = 0; i < freeRectangles.Size(); ++i) + { + // If this is a perfect fit upright, choose it immediately. + if (width == freeRectangles[i].width && height == freeRectangles[i].height) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = width; + bestNode.height = height; + bestScore = INT_MIN; + *nodeIndex = i; + assert(disjointRects.Disjoint(bestNode)); + break; + } + // If this is a perfect fit sideways, choose it. +/* else if (height == freeRectangles[i].width && width == freeRectangles[i].height) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = height; + bestNode.height = width; + bestScore = INT_MIN; + *nodeIndex = i; + assert(disjointRects.Disjoint(bestNode)); + break; + } +*/ // Does the rectangle fit upright? + else if (width <= freeRectangles[i].width && height <= freeRectangles[i].height) + { + int score = ScoreByHeuristic(width, height, freeRectangles[i], rectChoice); + + if (score < bestScore) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = width; + bestNode.height = height; + bestScore = score; + *nodeIndex = i; + assert(disjointRects.Disjoint(bestNode)); + } + } + // Does the rectangle fit sideways? +/* else if (height <= freeRectangles[i].width && width <= freeRectangles[i].height) + { + int score = ScoreByHeuristic(height, width, freeRectangles[i], rectChoice); + + if (score < bestScore) + { + bestNode.x = freeRectangles[i].x; + bestNode.y = freeRectangles[i].y; + bestNode.width = height; + bestNode.height = width; + bestScore = score; + *nodeIndex = i; + assert(disjointRects.Disjoint(bestNode)); + } + } +*/ } + return bestNode; +} + +void GuillotineBinPack::SplitFreeRectByHeuristic(const Rect &freeRect, const Rect &placedRect, GuillotineSplitHeuristic method) +{ + // Compute the lengths of the leftover area. + const int w = freeRect.width - placedRect.width; + const int h = freeRect.height - placedRect.height; + + // Placing placedRect into freeRect results in an L-shaped free area, which must be split into + // two disjoint rectangles. This can be achieved with by splitting the L-shape using a single line. + // We have two choices: horizontal or vertical. + + // Use the given heuristic to decide which choice to make. + + bool splitHorizontal; + switch(method) + { + case SplitShorterLeftoverAxis: + // Split along the shorter leftover axis. + splitHorizontal = (w <= h); + break; + case SplitLongerLeftoverAxis: + // Split along the longer leftover axis. + splitHorizontal = (w > h); + break; + case SplitMinimizeArea: + // Maximize the larger area == minimize the smaller area. + // Tries to make the single bigger rectangle. + splitHorizontal = (placedRect.width * h > w * placedRect.height); + break; + case SplitMaximizeArea: + // Maximize the smaller area == minimize the larger area. + // Tries to make the rectangles more even-sized. + splitHorizontal = (placedRect.width * h <= w * placedRect.height); + break; + case SplitShorterAxis: + // Split along the shorter total axis. + splitHorizontal = (freeRect.width <= freeRect.height); + break; + case SplitLongerAxis: + // Split along the longer total axis. + splitHorizontal = (freeRect.width > freeRect.height); + break; + default: + splitHorizontal = true; + assert(false); + } + + // Perform the actual split. + SplitFreeRectAlongAxis(freeRect, placedRect, splitHorizontal); +} + +/// This function will add the two generated rectangles into the freeRectangles array. The caller is expected to +/// remove the original rectangle from the freeRectangles array after that. +void GuillotineBinPack::SplitFreeRectAlongAxis(const Rect &freeRect, const Rect &placedRect, bool splitHorizontal) +{ + // Form the two new rectangles. + Rect bottom; + bottom.x = freeRect.x; + bottom.y = freeRect.y + placedRect.height; + bottom.height = freeRect.height - placedRect.height; + + Rect right; + right.x = freeRect.x + placedRect.width; + right.y = freeRect.y; + right.width = freeRect.width - placedRect.width; + + if (splitHorizontal) + { + bottom.width = freeRect.width; + right.height = placedRect.height; + } + else // Split vertically + { + bottom.width = placedRect.width; + right.height = freeRect.height; + } + + // Add the new rectangles into the free rectangle pool if they weren't degenerate. + if (bottom.width > 0 && bottom.height > 0) + freeRectangles.Push(bottom); + if (right.width > 0 && right.height > 0) + freeRectangles.Push(right); + + assert(disjointRects.Disjoint(bottom)); + assert(disjointRects.Disjoint(right)); +} + +void GuillotineBinPack::MergeFreeList() +{ +#ifdef _DEBUG + DisjointRectCollection test; + for(unsigned i = 0; i < freeRectangles.Size(); ++i) + assert(test.Add(freeRectangles[i]) == true); +#endif + + // Do a Theta(n^2) loop to see if any pair of free rectangles could me merged into one. + // Note that we miss any opportunities to merge three rectangles into one. (should call this function again to detect that) + for(unsigned i = 0; i < freeRectangles.Size(); ++i) + for(unsigned j = i+1; j < freeRectangles.Size(); ++j) + { + if (freeRectangles[i].width == freeRectangles[j].width && freeRectangles[i].x == freeRectangles[j].x) + { + if (freeRectangles[i].y == freeRectangles[j].y + freeRectangles[j].height) + { + freeRectangles[i].y -= freeRectangles[j].height; + freeRectangles[i].height += freeRectangles[j].height; + freeRectangles.Delete(j); + --j; + } + else if (freeRectangles[i].y + freeRectangles[i].height == freeRectangles[j].y) + { + freeRectangles[i].height += freeRectangles[j].height; + freeRectangles.Delete(j); + --j; + } + } + else if (freeRectangles[i].height == freeRectangles[j].height && freeRectangles[i].y == freeRectangles[j].y) + { + if (freeRectangles[i].x == freeRectangles[j].x + freeRectangles[j].width) + { + freeRectangles[i].x -= freeRectangles[j].width; + freeRectangles[i].width += freeRectangles[j].width; + freeRectangles.Delete(j); + --j; + } + else if (freeRectangles[i].x + freeRectangles[i].width == freeRectangles[j].x) + { + freeRectangles[i].width += freeRectangles[j].width; + freeRectangles.Delete(j); + --j; + } + } + } + +#ifdef _DEBUG + test.Clear(); + for(unsigned i = 0; i < freeRectangles.Size(); ++i) + assert(test.Add(freeRectangles[i]) == true); +#endif +} diff --git a/src/GuillotineBinPack.h b/src/GuillotineBinPack.h new file mode 100644 index 000000000..54cd77a9e --- /dev/null +++ b/src/GuillotineBinPack.h @@ -0,0 +1,135 @@ +/** @file GuillotineBinPack.h + @author Jukka Jylänki + + @brief Implements different bin packer algorithms that use the GUILLOTINE data structure. + + This work is released to Public Domain, do whatever you want with it. +*/ +#pragma once + +#include "tarray.h" + +#include "Rect.h" + +/** GuillotineBinPack implements different variants of bin packer algorithms that use the GUILLOTINE data structure + to keep track of the free space of the bin where rectangles may be placed. */ +class GuillotineBinPack +{ +public: + /// The initial bin size will be (0,0). Call Init to set the bin size. + GuillotineBinPack(); + + /// Initializes a new bin of the given size. + GuillotineBinPack(int width, int height); + + /// (Re)initializes the packer to an empty bin of width x height units. Call whenever + /// you need to restart with a new bin. + void Init(int width, int height); + + /// Specifies the different choice heuristics that can be used when deciding which of the free subrectangles + /// to place the to-be-packed rectangle into. + enum FreeRectChoiceHeuristic + { + RectBestAreaFit, ///< -BAF + RectBestShortSideFit, ///< -BSSF + RectBestLongSideFit, ///< -BLSF + RectWorstAreaFit, ///< -WAF + RectWorstShortSideFit, ///< -WSSF + RectWorstLongSideFit ///< -WLSF + }; + + /// Specifies the different choice heuristics that can be used when the packer needs to decide whether to + /// subdivide the remaining free space in horizontal or vertical direction. + enum GuillotineSplitHeuristic + { + SplitShorterLeftoverAxis, ///< -SLAS + SplitLongerLeftoverAxis, ///< -LLAS + SplitMinimizeArea, ///< -MINAS, Try to make a single big rectangle at the expense of making the other small. + SplitMaximizeArea, ///< -MAXAS, Try to make both remaining rectangles as even-sized as possible. + SplitShorterAxis, ///< -SAS + SplitLongerAxis ///< -LAS + }; + + /// Inserts a single rectangle into the bin. The packer might rotate the rectangle, in which case the returned + /// struct will have the width and height values swapped. + /// @param merge If true, performs free Rectangle Merge procedure after packing the new rectangle. This procedure + /// tries to defragment the list of disjoint free rectangles to improve packing performance, but also takes up + /// some extra time. + /// @param rectChoice The free rectangle choice heuristic rule to use. + /// @param splitMethod The free rectangle split heuristic rule to use. + Rect Insert(int width, int height, bool merge, FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod); + + /// Inserts a list of rectangles into the bin. + /// @param rects The list of rectangles to add. This list will be destroyed in the packing process. + /// @param dst The outputted list of rectangles. Note that the indices will not correspond to the input indices. + /// @param merge If true, performs Rectangle Merge operations during the packing process. + /// @param rectChoice The free rectangle choice heuristic rule to use. + /// @param splitMethod The free rectangle split heuristic rule to use. + void Insert(TArray &rects, TArray &dst, bool merge, + FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod); + +// Implements GUILLOTINE-MAXFITTING, an experimental heuristic that's really cool but didn't quite work in practice. +// void InsertMaxFitting(TArray &rects, TArray &dst, bool merge, +// FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod); + + /// Computes the ratio of used/total surface area. 0.00 means no space is yet used, 1.00 means the whole bin is used. + float Occupancy() const; + + /// Returns the internal list of disjoint rectangles that track the free area of the bin. You may alter this vector + /// any way desired, as long as the end result still is a list of disjoint rectangles. + TArray &GetFreeRectangles() { return freeRectangles; } + + /// Returns the list of packed rectangles. You may alter this vector at will, for example, you can move a Rect from + /// this list to the Free Rectangles list to free up space on-the-fly, but notice that this causes fragmentation. + TArray &GetUsedRectangles() { return usedRectangles; } + + /// Performs a Rectangle Merge operation. This procedure looks for adjacent free rectangles and merges them if they + /// can be represented with a single rectangle. Takes up Theta(|freeRectangles|^2) time. + void MergeFreeList(); + +#ifdef _DEBUG + void DelDisjoint(const Rect &r) { disjointRects.Del(r); } +#endif + +private: + int binWidth; + int binHeight; + + /// Stores a list of all the rectangles that we have packed so far. This is used only to compute the Occupancy ratio, + /// so if you want to have the packer consume less memory, this can be removed. + TArray usedRectangles; + + /// Stores a list of rectangles that represents the free area of the bin. This rectangles in this list are disjoint. + TArray freeRectangles; + +#ifdef _DEBUG + /// Used to track that the packer produces proper packings. + DisjointRectCollection disjointRects; +#endif + + /// Goes through the list of free rectangles and finds the best one to place a rectangle of given size into. + /// Running time is Theta(|freeRectangles|). + /// @param nodeIndex [out] The index of the free rectangle in the freeRectangles array into which the new + /// rect was placed. + /// @return A Rect structure that represents the placement of the new rect into the best free rectangle. + Rect FindPositionForNewNode(int width, int height, FreeRectChoiceHeuristic rectChoice, int *nodeIndex); + + static int ScoreByHeuristic(int width, int height, const Rect &freeRect, FreeRectChoiceHeuristic rectChoice); + // The following functions compute (penalty) score values if a rect of the given size was placed into the + // given free rectangle. In these score values, smaller is better. + + static int ScoreBestAreaFit(int width, int height, const Rect &freeRect); + static int ScoreBestShortSideFit(int width, int height, const Rect &freeRect); + static int ScoreBestLongSideFit(int width, int height, const Rect &freeRect); + + static int ScoreWorstAreaFit(int width, int height, const Rect &freeRect); + static int ScoreWorstShortSideFit(int width, int height, const Rect &freeRect); + static int ScoreWorstLongSideFit(int width, int height, const Rect &freeRect); + + /// Splits the given L-shaped free rectangle into two new free rectangles after placedRect has been placed into it. + /// Determines the split axis by using the given heuristic. + void SplitFreeRectByHeuristic(const Rect &freeRect, const Rect &placedRect, GuillotineSplitHeuristic method); + + /// Splits the given L-shaped free rectangle into two new free rectangles along the given fixed split axis. + void SplitFreeRectAlongAxis(const Rect &freeRect, const Rect &placedRect, bool splitHorizontal); +}; diff --git a/src/Rect.h b/src/Rect.h new file mode 100644 index 000000000..8b7ba1e2a --- /dev/null +++ b/src/Rect.h @@ -0,0 +1,94 @@ +/** @file Rect.h + @author Jukka Jylänki + + This work is released to Public Domain, do whatever you want with it. +*/ +#pragma once + +#include + +struct RectSize +{ + int width; + int height; +}; + +struct Rect +{ + int x; + int y; + int width; + int height; +}; + +/// Performs a lexicographic compare on (rect short side, rect long side). +/// @return -1 if the smaller side of a is shorter than the smaller side of b, 1 if the other way around. +/// If they are equal, the larger side length is used as a tie-breaker. +/// If the rectangles are of same size, returns 0. +int CompareRectShortSide(const Rect &a, const Rect &b); + +/// Performs a lexicographic compare on (x, y, width, height). +int NodeSortCmp(const Rect &a, const Rect &b); + +/// Returns true if a is contained in b. +bool IsContainedIn(const Rect &a, const Rect &b); + +#ifdef _DEBUG +class DisjointRectCollection +{ +public: + TArray rects; + + bool Add(const Rect &r) + { + // Degenerate rectangles are ignored. + if (r.width == 0 || r.height == 0) + return true; + + if (!Disjoint(r)) + return false; + rects.Push(r); + return true; + } + + bool Del(const Rect &r) + { + for(unsigned i = 0; i < rects.Size(); ++i) + { + if(r.x == rects[i].x && r.y == rects[i].y && r.width == rects[i].width && r.height == rects[i].height) + { + rects.Delete(i); + return true; + } + } + return false; + } + + void Clear() + { + rects.Clear(); + } + + bool Disjoint(const Rect &r) const + { + // Degenerate rectangles are ignored. + if (r.width == 0 || r.height == 0) + return true; + + for(unsigned i = 0; i < rects.Size(); ++i) + if (!Disjoint(rects[i], r)) + return false; + return true; + } + + static bool Disjoint(const Rect &a, const Rect &b) + { + if (a.x + a.width <= b.x || + b.x + b.width <= a.x || + a.y + a.height <= b.y || + b.y + b.height <= a.y) + return true; + return false; + } +}; +#endif diff --git a/src/SkylineBinPack.cpp b/src/SkylineBinPack.cpp new file mode 100644 index 000000000..578a248a5 --- /dev/null +++ b/src/SkylineBinPack.cpp @@ -0,0 +1,329 @@ +/** @file SkylineBinPack.cpp + @author Jukka Jylänki + + @brief Implements different bin packer algorithms that use the SKYLINE data structure. + + This work is released to Public Domain, do whatever you want with it. +*/ + +#include + +#include "SkylineBinPack.h" + +using namespace std; + +SkylineBinPack::SkylineBinPack() +:binWidth(0), +binHeight(0) +{ +} + +SkylineBinPack::SkylineBinPack(int width, int height, bool useWasteMap) +{ + Init(width, height, useWasteMap); +} + +void SkylineBinPack::Init(int width, int height, bool useWasteMap_) +{ + binWidth = width; + binHeight = height; + + useWasteMap = useWasteMap_; + +#ifdef _DEBUG + disjointRects.Clear(); +#endif + + usedSurfaceArea = 0; + skyLine.Clear(); + SkylineNode node; + node.x = 0; + node.y = 0; + node.width = binWidth; + skyLine.Push(node); + + if (useWasteMap) + { + wasteMap.Init(width, height); + wasteMap.GetFreeRectangles().Clear(); + } +} + +void SkylineBinPack::Insert(TArray &rects, TArray &dst) +{ + dst.Clear(); + + while(rects.Size() > 0) + { + Rect bestNode; + int bestScore1 = INT_MAX; + int bestScore2 = INT_MAX; + int bestSkylineIndex = -1; + int bestRectIndex = -1; + for(unsigned i = 0; i < rects.Size(); ++i) + { + Rect newNode; + int score1; + int score2; + int index; + newNode = FindPositionForNewNodeMinWaste(rects[i].width, rects[i].height, score2, score1, index); + assert(disjointRects.Disjoint(newNode)); + if (newNode.height != 0) + { + if (score1 < bestScore1 || (score1 == bestScore1 && score2 < bestScore2)) + { + bestNode = newNode; + bestScore1 = score1; + bestScore2 = score2; + bestSkylineIndex = index; + bestRectIndex = i; + } + } + } + + if (bestRectIndex == -1) + return; + + // Perform the actual packing. + assert(disjointRects.Disjoint(bestNode)); +#ifdef _DEBUG + disjointRects.Add(bestNode); +#endif + AddSkylineLevel(bestSkylineIndex, bestNode); + usedSurfaceArea += rects[bestRectIndex].width * rects[bestRectIndex].height; + rects.Delete(bestRectIndex); + dst.Push(bestNode); + } +} + +Rect SkylineBinPack::Insert(int width, int height) +{ + // First try to pack this rectangle into the waste map, if it fits. + Rect node = wasteMap.Insert(width, height, true, GuillotineBinPack::RectBestShortSideFit, + GuillotineBinPack::SplitMaximizeArea); + assert(disjointRects.Disjoint(node)); + + if (node.height != 0) + { + Rect newNode; + newNode.x = node.x; + newNode.y = node.y; + newNode.width = node.width; + newNode.height = node.height; + usedSurfaceArea += width * height; + assert(disjointRects.Disjoint(newNode)); +#ifdef _DEBUG + disjointRects.Add(newNode); +#endif + return newNode; + } + + return InsertMinWaste(width, height); +} + +bool SkylineBinPack::RectangleFits(int skylineNodeIndex, int width, int height, int &y) const +{ + int x = skyLine[skylineNodeIndex].x; + if (x + width > binWidth) + return false; + int widthLeft = width; + int i = skylineNodeIndex; + y = skyLine[skylineNodeIndex].y; + while(widthLeft > 0) + { + y = max(y, skyLine[i].y); + if (y + height > binHeight) + return false; + widthLeft -= skyLine[i].width; + ++i; + assert(i < (int)skyLine.Size() || widthLeft <= 0); + } + return true; +} + +int SkylineBinPack::ComputeWastedArea(int skylineNodeIndex, int width, int height, int y) const +{ + int wastedArea = 0; + const int rectLeft = skyLine[skylineNodeIndex].x; + const int rectRight = rectLeft + width; + for(; skylineNodeIndex < (int)skyLine.Size() && skyLine[skylineNodeIndex].x < rectRight; ++skylineNodeIndex) + { + if (skyLine[skylineNodeIndex].x >= rectRight || skyLine[skylineNodeIndex].x + skyLine[skylineNodeIndex].width <= rectLeft) + break; + + int leftSide = skyLine[skylineNodeIndex].x; + int rightSide = min(rectRight, leftSide + skyLine[skylineNodeIndex].width); + assert(y >= skyLine[skylineNodeIndex].y); + wastedArea += (rightSide - leftSide) * (y - skyLine[skylineNodeIndex].y); + } + return wastedArea; +} + +bool SkylineBinPack::RectangleFits(int skylineNodeIndex, int width, int height, int &y, int &wastedArea) const +{ + bool fits = RectangleFits(skylineNodeIndex, width, height, y); + if (fits) + wastedArea = ComputeWastedArea(skylineNodeIndex, width, height, y); + + return fits; +} + +void SkylineBinPack::AddWasteMapArea(int skylineNodeIndex, int width, int height, int y) +{ + int wastedArea = 0; + const int rectLeft = skyLine[skylineNodeIndex].x; + const int rectRight = rectLeft + width; + for(; skylineNodeIndex < (int)skyLine.Size() && skyLine[skylineNodeIndex].x < rectRight; ++skylineNodeIndex) + { + if (skyLine[skylineNodeIndex].x >= rectRight || skyLine[skylineNodeIndex].x + skyLine[skylineNodeIndex].width <= rectLeft) + break; + + int leftSide = skyLine[skylineNodeIndex].x; + int rightSide = min(rectRight, leftSide + skyLine[skylineNodeIndex].width); + assert(y >= skyLine[skylineNodeIndex].y); + + Rect waste; + waste.x = leftSide; + waste.y = skyLine[skylineNodeIndex].y; + waste.width = rightSide - leftSide; + waste.height = y - skyLine[skylineNodeIndex].y; + + assert(disjointRects.Disjoint(waste)); + wasteMap.GetFreeRectangles().Push(waste); + } +} + +void SkylineBinPack::AddWaste(const Rect &waste) +{ + wasteMap.GetFreeRectangles().Push(waste); +#ifdef _DEBUG + disjointRects.Del(waste); + wasteMap.DelDisjoint(waste); +#endif +} + +void SkylineBinPack::AddSkylineLevel(int skylineNodeIndex, const Rect &rect) +{ + // First track all wasted areas and mark them into the waste map if we're using one. + if (useWasteMap) + AddWasteMapArea(skylineNodeIndex, rect.width, rect.height, rect.y); + + SkylineNode newNode; + newNode.x = rect.x; + newNode.y = rect.y + rect.height; + newNode.width = rect.width; + skyLine.Insert(skylineNodeIndex, newNode); + + assert(newNode.x + newNode.width <= binWidth); + assert(newNode.y <= binHeight); + + for(unsigned i = skylineNodeIndex+1; i < skyLine.Size(); ++i) + { + assert(skyLine[i-1].x <= skyLine[i].x); + + if (skyLine[i].x < skyLine[i-1].x + skyLine[i-1].width) + { + int shrink = skyLine[i-1].x + skyLine[i-1].width - skyLine[i].x; + + skyLine[i].x += shrink; + skyLine[i].width -= shrink; + + if (skyLine[i].width <= 0) + { + skyLine.Delete(i); + --i; + } + else + break; + } + else + break; + } + MergeSkylines(); +} + +void SkylineBinPack::MergeSkylines() +{ + for(unsigned i = 0; i < skyLine.Size()-1; ++i) + if (skyLine[i].y == skyLine[i+1].y) + { + skyLine[i].width += skyLine[i+1].width; + skyLine.Delete(i+1); + --i; + } +} + +Rect SkylineBinPack::InsertMinWaste(int width, int height) +{ + int bestHeight; + int bestWastedArea; + int bestIndex; + Rect newNode = FindPositionForNewNodeMinWaste(width, height, bestHeight, bestWastedArea, bestIndex); + + if (bestIndex != -1) + { + assert(disjointRects.Disjoint(newNode)); + // Perform the actual packing. + AddSkylineLevel(bestIndex, newNode); + + usedSurfaceArea += width * height; +#ifdef _DEBUG + disjointRects.Add(newNode); +#endif + } + else + memset(&newNode, 0, sizeof(newNode)); + + return newNode; +} + +Rect SkylineBinPack::FindPositionForNewNodeMinWaste(int width, int height, int &bestHeight, int &bestWastedArea, int &bestIndex) const +{ + bestHeight = INT_MAX; + bestWastedArea = INT_MAX; + bestIndex = -1; + Rect newNode; + memset(&newNode, 0, sizeof(newNode)); + for(unsigned i = 0; i < skyLine.Size(); ++i) + { + int y; + int wastedArea; + + if (RectangleFits(i, width, height, y, wastedArea)) + { + if (wastedArea < bestWastedArea || (wastedArea == bestWastedArea && y + height < bestHeight)) + { + bestHeight = y + height; + bestWastedArea = wastedArea; + bestIndex = i; + newNode.x = skyLine[i].x; + newNode.y = y; + newNode.width = width; + newNode.height = height; + assert(disjointRects.Disjoint(newNode)); + } + } +/* if (RectangleFits(i, height, width, y, wastedArea)) + { + if (wastedArea < bestWastedArea || (wastedArea == bestWastedArea && y + width < bestHeight)) + { + bestHeight = y + width; + bestWastedArea = wastedArea; + bestIndex = i; + newNode.x = skyLine[i].x; + newNode.y = y; + newNode.width = height; + newNode.height = width; + assert(disjointRects.Disjoint(newNode)); + } + }*/ + } + + return newNode; +} + +/// Computes the ratio of used surface area. +float SkylineBinPack::Occupancy() const +{ + return (float)usedSurfaceArea / (binWidth * binHeight); +} diff --git a/src/SkylineBinPack.h b/src/SkylineBinPack.h new file mode 100644 index 000000000..f93ddd29d --- /dev/null +++ b/src/SkylineBinPack.h @@ -0,0 +1,88 @@ +/** @file SkylineBinPack.h + @author Jukka Jylänki + + @brief Implements different bin packer algorithms that use the SKYLINE data structure. + + This work is released to Public Domain, do whatever you want with it. +*/ +#pragma once + +#include "tarray.h" + +#include "Rect.h" +#include "GuillotineBinPack.h" + +/** Implements bin packing algorithms that use the SKYLINE data structure to store the bin contents. Uses + GuillotineBinPack as the waste map. */ +class SkylineBinPack +{ +public: + /// Instantiates a bin of size (0,0). Call Init to create a new bin. + SkylineBinPack(); + + /// Instantiates a bin of the given size. + SkylineBinPack(int binWidth, int binHeight, bool useWasteMap); + + /// (Re)initializes the packer to an empty bin of width x height units. Call whenever + /// you need to restart with a new bin. + void Init(int binWidth, int binHeight, bool useWasteMap); + + /// Inserts the given list of rectangles in an offline/batch mode, possibly rotated. + /// @param rects The list of rectangles to insert. This vector will be destroyed in the process. + /// @param dst [out] This list will contain the packed rectangles. The indices will not correspond to that of rects. + /// @param method The rectangle placement rule to use when packing. + void Insert(TArray &rects, TArray &dst); + + /// Inserts a single rectangle into the bin, possibly rotated. + Rect Insert(int width, int height); + + /// Adds a rectangle to the waste list. It must have been previously returned by + /// Insert or the results are undefined. + void AddWaste(const Rect &rect); + + /// Computes the ratio of used surface area to the total bin area. + float Occupancy() const; + +private: + int binWidth; + int binHeight; + +#ifdef _DEBUG + DisjointRectCollection disjointRects; +#endif + + /// Represents a single level (a horizontal line) of the skyline/horizon/envelope. + struct SkylineNode + { + /// The starting x-coordinate (leftmost). + int x; + + /// The y-coordinate of the skyline level line. + int y; + + /// The line width. The ending coordinate (inclusive) will be x+width-1. + int width; + }; + + TArray skyLine; + + unsigned long usedSurfaceArea; + + /// If true, we use the GuillotineBinPack structure to recover wasted areas into a waste map. + bool useWasteMap; + GuillotineBinPack wasteMap; + + Rect InsertMinWaste(int width, int height); + Rect FindPositionForNewNodeMinWaste(int width, int height, int &bestHeight, int &bestWastedArea, int &bestIndex) const; + + bool RectangleFits(int skylineNodeIndex, int width, int height, int &y) const; + bool RectangleFits(int skylineNodeIndex, int width, int height, int &y, int &wastedArea) const; + int ComputeWastedArea(int skylineNodeIndex, int width, int height, int y) const; + + void AddWasteMapArea(int skylineNodeIndex, int width, int height, int y); + + void AddSkylineLevel(int skylineNodeIndex, const Rect &rect); + + /// Merges all skyline nodes that are at the same level. + void MergeSkylines(); +}; diff --git a/src/win32/fb_d3d9.cpp b/src/win32/fb_d3d9.cpp index 273fcde44..1641df365 100644 --- a/src/win32/fb_d3d9.cpp +++ b/src/win32/fb_d3d9.cpp @@ -72,6 +72,7 @@ #include "v_palette.h" #include "w_wad.h" #include "r_data/colormaps.h" +#include "SkylineBinPack.h" // MACROS ------------------------------------------------------------------ @@ -92,7 +93,7 @@ struct D3DFB::PackedTexture { D3DFB::Atlas *Owner; - PackedTexture *Next, **Prev; + PackedTexture **Prev, *Next; // Pixels this image covers RECT Area; @@ -109,18 +110,14 @@ struct D3DFB::Atlas Atlas(D3DFB *fb, int width, int height, D3DFORMAT format); ~Atlas(); - PackedTexture *GetBestFit(int width, int height, int &area); - void AllocateImage(PackedTexture *box, int width, int height); - PackedTexture *AllocateBox(); - void AddEmptyBox(int left, int top, int right, int bottom); + PackedTexture *AllocateImage(const Rect &rect, bool padded); void FreeBox(PackedTexture *box); + SkylineBinPack Packer; Atlas *Next; IDirect3DTexture9 *Tex; D3DFORMAT Format; PackedTexture *UsedList; // Boxes that contain images - PackedTexture *EmptyList; // Boxes that contain empty space - PackedTexture *FreeList; // Boxes that are just waiting to be used int Width, Height; bool OneUse; }; @@ -1871,7 +1868,8 @@ void D3DFB::DrawPackedTextures(int packnum) continue; } - AddColorOnlyQuad(x-1, y-1-LBOffsetI, 258, 258, D3DCOLOR_XRGB(255,255,0)); + AddColorOnlyRect(x-1, y-1-LBOffsetI, 258, 258, D3DCOLOR_XRGB(255,255,0)); + AddColorOnlyQuad(x, y-LBOffsetI, 256, 256, D3DCOLOR_ARGB(180,0,0,0)); CheckQuadBatch(); @@ -1881,12 +1879,12 @@ void D3DFB::DrawPackedTextures(int packnum) quad->Group1 = 0; if (pack->Format == D3DFMT_L8/* && !tex->IsGray*/) { - quad->Flags = BQF_WrapUV | BQF_GamePalette | BQF_DisableAlphaTest; + quad->Flags = BQF_WrapUV | BQF_GamePalette/* | BQF_DisableAlphaTest*/; quad->ShaderNum = BQS_PalTex; } else { - quad->Flags = BQF_WrapUV | BQF_DisableAlphaTest; + quad->Flags = BQF_WrapUV/* | BQF_DisableAlphaTest*/; quad->ShaderNum = BQS_Plain; } quad->Palette = NULL; @@ -1946,16 +1944,6 @@ void D3DFB::DrawPackedTextures(int packnum) VertexPos += 4; IndexPos += 6; - // Draw entries in the empty list. - PackedTexture *box; - int emptynum; - for (box = pack->EmptyList, emptynum = 0; box != NULL; box = box->Next, emptynum++) - { - AddColorOnlyQuad(x + box->Area.left, y + box->Area.top - LBOffsetI, - box->Area.right - box->Area.left, box->Area.bottom - box->Area.top, - empty_colors[emptynum & 7]); - } - x += 256 + 8; if (x > Width - 256) { @@ -1981,54 +1969,43 @@ void D3DFB::DrawPackedTextures(int packnum) D3DFB::PackedTexture *D3DFB::AllocPackedTexture(int w, int h, bool wrapping, D3DFORMAT format) { - Atlas *bestpack; - PackedTexture *bestbox; - int area; + Atlas *pack; + Rect box; + bool padded; // check for 254 to account for padding if (w > 254 || h > 254 || wrapping) { // Create a new texture atlas. - bestpack = new Atlas(this, w, h, format); - bestpack->OneUse = true; - bestbox = bestpack->GetBestFit(w, h, area); - bestbox->Padded = false; + pack = new Atlas(this, w, h, format); + pack->OneUse = true; + box = pack->Packer.Insert(w, h); + padded = false; } else { // Try to find space in an existing texture atlas. w += 2; // Add padding h += 2; - int bestarea = INT_MAX; - int bestareaever = w * h; - bestpack = NULL; - bestbox = NULL; - for (Atlas *pack = Atlases; pack != NULL; pack = pack->Next) + for (pack = Atlases; pack != NULL; pack = pack->Next) { + // Use the first atlas it fits in. if (pack->Format == format) { - PackedTexture *box = pack->GetBestFit(w, h, area); - if (area == bestareaever) - { // An exact fit! Use it! - bestpack = pack; - bestbox = box; - break; - } - if (area < bestarea) + box = pack->Packer.Insert(w, h); + if (box.width != 0) { - bestarea = area; - bestpack = pack; - bestbox = box; + break; } } } - if (bestpack == NULL) + if (pack == NULL) { // Create a new texture atlas. - bestpack = new Atlas(this, 256, 256, format); - bestbox = bestpack->GetBestFit(w, h, bestarea); + pack = new Atlas(this, 256, 256, format); + box = pack->Packer.Insert(w, h); } - bestbox->Padded = true; + padded = true; } - bestpack->AllocateImage(bestbox, w, h); - return bestbox; + assert(box.width != 0 && box.height != 0); + return pack->AllocateImage(box, padded); } //========================================================================== @@ -2038,18 +2015,23 @@ D3DFB::PackedTexture *D3DFB::AllocPackedTexture(int w, int h, bool wrapping, D3D //========================================================================== D3DFB::Atlas::Atlas(D3DFB *fb, int w, int h, D3DFORMAT format) + : Packer(w, h, true) { Tex = NULL; Format = format; UsedList = NULL; - EmptyList = NULL; - FreeList = NULL; OneUse = false; Width = 0; Height = 0; + Next = NULL; - Next = fb->Atlases; - fb->Atlases = this; + // Attach to the end of the atlas list + Atlas **prev = &fb->Atlases; + while (*prev != NULL) + { + prev = &((*prev)->Next); + } + *prev = this; #if 1 if (FAILED(fb->D3DDevice->CreateTexture(w, h, 1, 0, format, D3DPOOL_MANAGED, &Tex, NULL))) @@ -2066,9 +2048,6 @@ D3DFB::Atlas::Atlas(D3DFB *fb, int w, int h, D3DFORMAT format) } Width = w; Height = h; - - // The whole texture is initially empty. - AddEmptyBox(0, 0, w, h); } //========================================================================== @@ -2087,53 +2066,6 @@ D3DFB::Atlas::~Atlas() next = box->Next; delete box; } - for (box = EmptyList; box != NULL; box = next) - { - next = box->Next; - delete box; - } - for (box = FreeList; box != NULL; box = next) - { - next = box->Next; - delete box; - } -} - -//========================================================================== -// -// Atlas :: GetBestFit -// -// Returns the empty box that provides the best fit for the requested -// dimensions, or NULL if none of them are large enough. -// -//========================================================================== - -D3DFB::PackedTexture *D3DFB::Atlas::GetBestFit(int w, int h, int &area) -{ - PackedTexture *box; - int smallestarea = INT_MAX; - PackedTexture *smallestbox = NULL; - - for (box = EmptyList; box != NULL; box = box->Next) - { - int boxw = box->Area.right - box->Area.left; - int boxh = box->Area.bottom - box->Area.top; - if (boxw >= w && boxh >= h) - { - int boxarea = boxw * boxh; - if (boxarea < smallestarea) - { - smallestarea = boxarea; - smallestbox = box; - if (boxw == w && boxh == h) - { // An exact fit! Use it! - break; - } - } - } - } - area = smallestarea; - return smallestbox; } //========================================================================== @@ -2148,24 +2080,22 @@ D3DFB::PackedTexture *D3DFB::Atlas::GetBestFit(int w, int h, int &area) // //========================================================================== -void D3DFB::Atlas::AllocateImage(D3DFB::PackedTexture *box, int w, int h) +D3DFB::PackedTexture *D3DFB::Atlas::AllocateImage(const Rect &rect, bool padded) { - RECT start = box->Area; + PackedTexture *box = new PackedTexture; - box->Area.right = box->Area.left + w; - box->Area.bottom = box->Area.top + h; + box->Owner = this; + box->Area.left = rect.x; + box->Area.top = rect.y; + box->Area.right = rect.x + rect.width; + box->Area.bottom = rect.y + rect.height; - box->Left = float(box->Area.left + box->Padded) / Width; - box->Right = float(box->Area.right - box->Padded) / Width; - box->Top = float(box->Area.top + box->Padded) / Height; - box->Bottom = float(box->Area.bottom - box->Padded) / Height; + box->Left = float(box->Area.left + padded) / Width; + box->Right = float(box->Area.right - padded) / Width; + box->Top = float(box->Area.top + padded) / Height; + box->Bottom = float(box->Area.bottom - padded) / Height; - // Remove it from the empty list. - *(box->Prev) = box->Next; - if (box->Next != NULL) - { - box->Next->Prev = box->Prev; - } + box->Padded = padded; // Add it to the used list. box->Next = UsedList; @@ -2176,96 +2106,6 @@ void D3DFB::Atlas::AllocateImage(D3DFB::PackedTexture *box, int w, int h) UsedList = box; box->Prev = &UsedList; - // If we didn't use the whole box, split the remainder into the empty list. - if (box->Area.bottom + 7 < start.bottom && box->Area.right + 7 < start.right) - { - // Split like this: - // +---+------+ - // |###| | - // +---+------+ - // | | - // | | - // +----------+ - if (box->Area.bottom < start.bottom) - { - AddEmptyBox(start.left, box->Area.bottom, start.right, start.bottom); - } - if (box->Area.right < start.right) - { - AddEmptyBox(box->Area.right, start.top, start.right, box->Area.bottom); - } - } - else - { - // Split like this: - // +---+------+ - // |###| | - // +---+ | - // | | | - // | | | - // +---+------+ - if (box->Area.bottom < start.bottom) - { - AddEmptyBox(start.left, box->Area.bottom, box->Area.right, start.bottom); - } - if (box->Area.right < start.right) - { - AddEmptyBox(box->Area.right, start.top, start.right, start.bottom); - } - } -} - -//========================================================================== -// -// Atlas :: AddEmptyBox -// -// Adds a box with the specified dimensions to the empty list. -// -//========================================================================== - -void D3DFB::Atlas::AddEmptyBox(int left, int top, int right, int bottom) -{ - PackedTexture *box = AllocateBox(); - box->Area.left = left; - box->Area.top = top; - box->Area.right = right; - box->Area.bottom = bottom; - box->Next = EmptyList; - if (box->Next != NULL) - { - box->Next->Prev = &box->Next; - } - box->Prev = &EmptyList; - EmptyList = box; -} - -//========================================================================== -// -// Atlas :: AllocateBox -// -// Returns a new PackedTexture box, either by retrieving one off the free -// list or by creating a new one. The box is not linked into a list. -// -//========================================================================== - -D3DFB::PackedTexture *D3DFB::Atlas::AllocateBox() -{ - PackedTexture *box; - - if (FreeList != NULL) - { - box = FreeList; - FreeList = box->Next; - if (box->Next != NULL) - { - box->Next->Prev = &FreeList; - } - } - else - { - box = new PackedTexture; - box->Owner = this; - } return box; } @@ -2273,10 +2113,9 @@ D3DFB::PackedTexture *D3DFB::Atlas::AllocateBox() // // Atlas :: FreeBox // -// Removes a box from its current list and adds it to the empty list, -// updating EmptyArea. If there are no boxes left in the used list, then -// the empty list is replaced with a single box, so the texture can be -// subdivided again. +// Removes a box from the used list and deletes it. Space is returned to the +// waste list. Once all boxes for this atlas are freed, the entire bin +// packer is reinitialized for maximum efficiency. // //========================================================================== @@ -2287,47 +2126,16 @@ void D3DFB::Atlas::FreeBox(D3DFB::PackedTexture *box) { box->Next->Prev = box->Prev; } - box->Next = EmptyList; - box->Prev = &EmptyList; - if (EmptyList != NULL) - { - EmptyList->Prev = &box->Next; - } - EmptyList = box; + Rect waste; + waste.x = box->Area.left; + waste.y = box->Area.top; + waste.width = box->Area.right - box->Area.left; + waste.height = box->Area.bottom - box->Area.top; + box->Owner->Packer.AddWaste(waste); + delete box; if (UsedList == NULL) - { // No more space in use! Move all but this into the free list. - if (box->Next != NULL) - { - D3DFB::PackedTexture *lastbox; - - // Find the last box in the free list. - lastbox = FreeList; - if (lastbox != NULL) - { - while (lastbox->Next != NULL) - { - lastbox = lastbox->Next; - } - } - // Chain the empty list to the end of the free list. - if (lastbox != NULL) - { - lastbox->Next = box->Next; - box->Next->Prev = &lastbox->Next; - } - else - { - FreeList = box->Next; - box->Next->Prev = &FreeList; - } - box->Next = NULL; - } - // Now this is the only box in the empty list, so it should - // contain the whole texture. - box->Area.left = 0; - box->Area.top = 0; - box->Area.right = Width; - box->Area.bottom = Height; + { + Packer.Init(Width, Height, true); } } @@ -2412,6 +2220,7 @@ bool D3DTex::CheckWrapping(bool wrapping) bool D3DTex::Create(D3DFB *fb, bool wrapping) { + assert(Box == NULL); if (Box != NULL) { Box->Owner->FreeBox(Box); @@ -3441,6 +3250,22 @@ void D3DFB::AddColorOnlyQuad(int left, int top, int width, int height, D3DCOLOR IndexPos += 6; } +//========================================================================== +// +// D3DFB :: AddColorOnlyRect +// +// Like AddColorOnlyQuad, except it's hollow. +// +//========================================================================== + +void D3DFB::AddColorOnlyRect(int left, int top, int width, int height, D3DCOLOR color) +{ + AddColorOnlyQuad(left, top, width - 1, 1, color); // top + AddColorOnlyQuad(left + width - 1, top, 1, height - 1, color); // right + AddColorOnlyQuad(left + 1, top + height - 1, width - 1, 1, color); // bottom + AddColorOnlyQuad(left, top + 1, 1, height - 1, color); // left +} + //========================================================================== // // D3DFB :: CheckQuadBatch diff --git a/src/win32/win32iface.h b/src/win32/win32iface.h index a64b1dd82..8b0c88f45 100644 --- a/src/win32/win32iface.h +++ b/src/win32/win32iface.h @@ -375,6 +375,7 @@ private: static void SetColorOverlay(DWORD color, float alpha, D3DCOLOR &color0, D3DCOLOR &color1); void DoWindowedGamma(); void AddColorOnlyQuad(int left, int top, int width, int height, D3DCOLOR color); + void AddColorOnlyRect(int left, int top, int width, int height, D3DCOLOR color); void CheckQuadBatch(int numtris=2, int numverts=4); void BeginQuadBatch(); void EndQuadBatch(); diff --git a/zdoom.vcproj b/zdoom.vcproj index a2587840f..597b56f2f 100644 --- a/zdoom.vcproj +++ b/zdoom.vcproj @@ -666,6 +666,10 @@ RelativePath=".\src\gitinfo.cpp" > + + @@ -1026,6 +1030,10 @@ RelativePath=".\src\skins.cpp" > + + @@ -1311,6 +1319,10 @@ RelativePath=".\src\gstrings.h" > + + @@ -1491,6 +1503,10 @@ RelativePath=".\src\po_man.h" > + + @@ -1515,6 +1531,10 @@ RelativePath=".\src\skins.h" > + +