From adb243798416ac5d0ccd048023bf90d4a015f14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 30 Sep 2023 20:52:25 +0200 Subject: [PATCH] TextureTools: merge AtlasLandfill and AtlasLandfillArray. The overhead of maintaining two classes with only very slight differences in the API and the internals being basically identical is not worth it. Too much potential for inconsistencies and doc errors. Additionally, when I attempted to use it for the reworked Text glyph cache, I realized I'd need to wrap them both under a common interface, allowing easy use for both 2D and 2D array textures. And then it's easier to just have the Atlas class done that way directly instead of papering over that in a downstream API. --- doc/changelog.dox | 3 +- doc/generated/atlas.cpp | 2 +- doc/snippets/MagnumTextureTools.cpp | 10 +- src/Magnum/TextureTools/Atlas.cpp | 204 +++++-------- src/Magnum/TextureTools/Atlas.h | 280 +++++------------- .../TextureTools/Test/AtlasBenchmark.cpp | 2 +- src/Magnum/TextureTools/Test/AtlasTest.cpp | 237 +++++++-------- 7 files changed, 278 insertions(+), 460 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index 4a1c99cf9..9a0899ad7 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -372,8 +372,7 @@ See also: @subsubsection changelog-latest-new-texturetools TextureTools library -- New @ref TextureTools::AtlasLandfill and - @relativeref{TextureTools,AtlasLandfillArray} texture atlas packers (see +- New @ref TextureTools::AtlasLandfill texture atlas packer (see [mosra/magnum#2](https://github.com/mosra/magnum/issues/2)) - New @ref TextureTools::atlasArrayPowerOfTwo() utility for optimal packing of power-of-two textures into a texture atlas array diff --git a/doc/generated/atlas.cpp b/doc/generated/atlas.cpp index 897df67cf..1e1b0f42b 100644 --- a/doc/generated/atlas.cpp +++ b/doc/generated/atlas.cpp @@ -137,7 +137,7 @@ int main() { Containers::BitArray rotations{NoInit, sizes.size()}; CORRADE_INTERNAL_ASSERT(atlas.add(sizes, offsets, rotations)); - Range2Di viewBox{{}, atlas.filledSize()}; + Range2Di viewBox{{}, atlas.filledSize().xy()}; std::string out; Utility::formatInto(out, out.size(), R"( diff --git a/doc/snippets/MagnumTextureTools.cpp b/doc/snippets/MagnumTextureTools.cpp index a9f8fedb2..240848835 100644 --- a/doc/snippets/MagnumTextureTools.cpp +++ b/doc/snippets/MagnumTextureTools.cpp @@ -52,7 +52,7 @@ TextureTools::AtlasLandfill atlas{{1024, 0}}; atlas.add(stridedArrayView(images).slice(&ImageView2D::size), offsets, rotations); /* Copy the image data to the atlas, assuming all are RGBA8Unorm as well */ -Image2D output{PixelFormat::RGBA8Unorm, atlas.filledSize(), +Image2D output{PixelFormat::RGBA8Unorm, atlas.filledSize().xy(), Containers::Array{ValueInit, std::size_t(atlas.filledSize().product())}}; Containers::StridedArrayView2D dst = output.pixels(); for(std::size_t i = 0; i != images.size(); ++i) { @@ -77,7 +77,7 @@ atlas.clearFlags(TextureTools::AtlasLandfillFlag::RotatePortrait| .add(stridedArrayView(images).slice(&ImageView2D::size), offsets); /* Copy the image data to the atlas, assuming all are RGBA8Unorm as well */ -Image2D output{PixelFormat::RGBA8Unorm, atlas.filledSize(), +Image2D output{PixelFormat::RGBA8Unorm, atlas.filledSize().xy(), Containers::Array{ValueInit, std::size_t(atlas.filledSize().product())}}; Containers::StridedArrayView2D dst = output.pixels(); for(std::size_t i = 0; i != images.size(); ++i) { @@ -90,13 +90,13 @@ for(std::size_t i = 0; i != images.size(); ++i) { } { -/* [AtlasLandfillArray-usage] */ +/* [AtlasLandfill-usage-array] */ Containers::ArrayView images = DOXYGEN_ELLIPSIS({}); Containers::Array offsets{NoInit, images.size()}; Containers::BitArray rotations{NoInit, images.size()}; /* Fill the atlas with an unbounded depth */ -TextureTools::AtlasLandfillArray atlas{{1024, 1024, 0}}; +TextureTools::AtlasLandfill atlas{{1024, 1024, 0}}; atlas.add(stridedArrayView(images).slice(&ImageView2D::size), offsets, rotations); /* Copy the image data to the atlas, assuming all are RGBA8Unorm as well */ @@ -114,7 +114,7 @@ for(std::size_t i = 0; i != images.size(); ++i) { std::size_t(offsets[i].y()), std::size_t(offsets[i].x())}, src.size())); } -/* [AtlasLandfillArray-usage] */ +/* [AtlasLandfill-usage-array] */ } { diff --git a/src/Magnum/TextureTools/Atlas.cpp b/src/Magnum/TextureTools/Atlas.cpp index 166e01a12..d38c49e40 100644 --- a/src/Magnum/TextureTools/Atlas.cpp +++ b/src/Magnum/TextureTools/Atlas.cpp @@ -187,16 +187,77 @@ bool atlasLandfillAddSortedFlipped(Implementation::AtlasLandfillState& state, co return true; } -bool atlasLandfillAdd(const char* messagePrefix, Implementation::AtlasLandfillState& state, const Containers::StridedArrayView1D sizes, const Containers::StridedArrayView1D offsets, const Containers::StridedArrayView1D zOffsets, const Containers::MutableBitArrayView rotations) { +} + +AtlasLandfill::AtlasLandfill(const Vector3i& size):_state{InPlaceInit} { + CORRADE_ASSERT(size.x(), "TextureTools::AtlasLandfill: expected non-zero width, got" << Debug::packed << size, ); + CORRADE_ASSERT(size.y() || size.z() == 1, "TextureTools::AtlasLandfill: expected a single array slice for unbounded height, got" << Debug::packed << size, ); + CORRADE_ASSERT(size.x() <= 65536, "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got" << Debug::packed << size, ); + + /* Change y / z = 0 to y / z = MAX so the algorithm doesn't need to branch + on that internally */ + _state->size = {size.x(), + size.y() ? size.y() : 0x7fffffff, + size.z() ? size.z() : 0x7fffffff}; +} + +AtlasLandfill::AtlasLandfill(const Vector2i& size): AtlasLandfill{{size, 1}} {} + +AtlasLandfill::AtlasLandfill(AtlasLandfill&&) noexcept = default; + +AtlasLandfill::~AtlasLandfill() = default; + +AtlasLandfill& AtlasLandfill::operator=(AtlasLandfill&&) noexcept = default; + +Vector3i AtlasLandfill::size() const { + /* Change y / z = MAX (that's there so the algorithm doesn't need to branch + on that internally) back to y / z = 0 */ + return {_state->size.x(), + _state->size.y() == 0x7fffffff ? 0 : _state->size.y(), + _state->size.z() == 0x7fffffff ? 0 : _state->size.z()}; +} + +Vector3i AtlasLandfill::filledSize() const { + if(_state->size.z() == 1) + return {_state->size.x(), Math::max(_state->yOffsets), 1}; + + CORRADE_INTERNAL_ASSERT(_state->size.y()); + return {_state->size.xy(), Int(_state->slices.size())}; +} + +Vector2i AtlasLandfill::padding() const { + return _state->padding; +} + +AtlasLandfill& AtlasLandfill::setPadding(const Vector2i& padding) { + _state->padding = padding; + return *this; +} + +AtlasLandfillFlags AtlasLandfill::flags() const { + return _state->flags; +} + +AtlasLandfill& AtlasLandfill::setFlags(AtlasLandfillFlags flags) { + CORRADE_ASSERT(!(flags & AtlasLandfillFlag::RotatePortrait) || + !(flags & AtlasLandfillFlag::RotateLandscape), + "TextureTools::AtlasLandfill::setFlags(): only one of RotatePortrait and RotateLandscape can be set", *this); + CORRADE_ASSERT(!(flags & AtlasLandfillFlag::WidestFirst) || + !(flags & AtlasLandfillFlag::NarrowestFirst), + "TextureTools::AtlasLandfill::setFlags(): only one of WidestFirst and NarrowestFirst can be set", *this); + _state->flags = flags; + return *this; +} + +namespace { + +bool atlasLandfillAdd(Implementation::AtlasLandfillState& state, const Containers::StridedArrayView1D sizes, const Containers::StridedArrayView1D offsets, const Containers::StridedArrayView1D zOffsets, const Containers::MutableBitArrayView rotations) { CORRADE_ASSERT(offsets.size() == sizes.size(), - messagePrefix << "expected sizes and offsets views to have the same size, got" << sizes.size() << "and" << offsets.size(), {}); + "TextureTools::AtlasLandfill::add(): expected sizes and offsets views to have the same size, got" << sizes.size() << "and" << offsets.size(), {}); CORRADE_ASSERT((!(state.flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) && rotations.isEmpty()) || rotations.size() == sizes.size(), - messagePrefix << "expected sizes and rotations views to have the same size, got" << sizes.size() << "and" << rotations.size(), {}); + "TextureTools::AtlasLandfill::add(): expected sizes and rotations views to have the same size, got" << sizes.size() << "and" << rotations.size(), {}); /* These are sliced internally from a Vector3i input, so should match */ CORRADE_INTERNAL_ASSERT(!zOffsets || zOffsets.size() == sizes.size()); - #ifdef CORRADE_NO_ASSERT - static_cast(messagePrefix); - #endif /* Nothing is flipped by default */ rotations.resetAll(); @@ -224,10 +285,10 @@ bool atlasLandfillAdd(const char* messagePrefix, Implementation::AtlasLandfillSt #ifndef CORRADE_NO_ASSERT if(state.padding.isZero()) CORRADE_ASSERT(size.product() && sizePadded <= state.size.xy(), - messagePrefix << "expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size, {}); + "TextureTools::AtlasLandfill::add(): expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size, {}); else CORRADE_ASSERT(size.product() && sizePadded <= state.size.xy(), - messagePrefix << "expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size << "and padding" << Debug::packed << padding, {}); + "TextureTools::AtlasLandfill::add(): expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size << "and padding" << Debug::packed << padding, {}); #endif sortedFlippedSizes[i] = {sizePadded, UnsignedInt(i)}; @@ -261,142 +322,41 @@ bool atlasLandfillAdd(const char* messagePrefix, Implementation::AtlasLandfillSt } -AtlasLandfill::AtlasLandfill(const Vector2i& size):_state{InPlaceInit} { - CORRADE_ASSERT(size.x(), "TextureTools::AtlasLandfill: expected non-zero width, got" << Debug::packed << size, ); - CORRADE_ASSERT(size.x() <= 65536, "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got" << Debug::packed << size, ); - - /* Change y = 0 to y = MAX so the algorithm doesn't need to branch on that - internally */ - _state->size = {size.x(), - size.y() ? size.y() : 0x7fffffff, - 1}; -} - -AtlasLandfill::AtlasLandfill(AtlasLandfill&&) noexcept = default; - -AtlasLandfill::~AtlasLandfill() = default; - -AtlasLandfill& AtlasLandfill::operator=(AtlasLandfill&&) noexcept = default; - -Vector2i AtlasLandfill::size() const { - /* Change y = MAX (that's there so the algorithm doesn't need to branch on - that internally) back to y = 0 */ - return {_state->size.x(), - _state->size.y() == 0x7fffffff ? 0 : _state->size.y()}; -} - -Vector2i AtlasLandfill::filledSize() const { - return {_state->size.x(), Math::max(_state->yOffsets)}; +bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { + return atlasLandfillAdd(*_state, sizes, offsets.slice(&Vector3i::xy), offsets.slice(&Vector3i::z), flips); } -Vector2i AtlasLandfill::padding() const { - return _state->padding; -} - -AtlasLandfill& AtlasLandfill::setPadding(const Vector2i& padding) { - _state->padding = padding; - return *this; -} - -AtlasLandfillFlags AtlasLandfill::flags() const { - return _state->flags; -} - -AtlasLandfill& AtlasLandfill::setFlags(AtlasLandfillFlags flags) { - CORRADE_ASSERT(!(flags & AtlasLandfillFlag::RotatePortrait) || - !(flags & AtlasLandfillFlag::RotateLandscape), - "TextureTools::AtlasLandfill::setFlags(): only one of RotatePortrait and RotateLandscape can be set", *this); - CORRADE_ASSERT(!(flags & AtlasLandfillFlag::WidestFirst) || - !(flags & AtlasLandfillFlag::NarrowestFirst), - "TextureTools::AtlasLandfill::setFlags(): only one of WidestFirst and NarrowestFirst can be set", *this); - _state->flags = flags; - return *this; -} - -bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { - return atlasLandfillAdd("TextureTools::AtlasLandfill::add():", *_state, sizes, offsets, nullptr, flips); -} - -bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { +bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { return add(Containers::stridedArrayView(sizes), offsets, flips); } -bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets) { +bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets) { CORRADE_ASSERT(!(_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)), "TextureTools::AtlasLandfill::add():" << (_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) << "set, expected a rotations view", {}); return add(sizes, offsets, nullptr); } -bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets) { +bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets) { return add(Containers::stridedArrayView(sizes), offsets); } -AtlasLandfillArray::AtlasLandfillArray(const Vector3i& size):_state{InPlaceInit} { - CORRADE_ASSERT(size.xy().product(), "TextureTools::AtlasLandfillArray: expected non-zero width and height, got" << Debug::packed << size, ); - CORRADE_ASSERT(size.x() <= 65536, "TextureTools::AtlasLandfillArray: expected width to fit into 16 bits, got" << Debug::packed << size, ); - - /* Change z = 0 to z = MAX so the algorithm doesn't need to branch on that - internally */ - _state->size = {size.xy(), - size.z() ? size.z() : 0x7fffffff}; -} - -AtlasLandfillArray::AtlasLandfillArray(AtlasLandfillArray&&) noexcept = default; - -AtlasLandfillArray::~AtlasLandfillArray() = default; - -AtlasLandfillArray& AtlasLandfillArray::operator=(AtlasLandfillArray&&) noexcept = default; - -Vector3i AtlasLandfillArray::size() const { - /* Change z = MAX (that's there so the algorithm doesn't need to branch on - that internally) back to z = 0 */ - return {_state->size.xy(), - _state->size.z() == 0x7fffffff ? 0 : _state->size.z()}; -} - -Vector3i AtlasLandfillArray::filledSize() const { - return {_state->size.xy(), Int(_state->slices.size())}; -} - -Vector2i AtlasLandfillArray::padding() const { - return _state->padding; -} - -AtlasLandfillArray& AtlasLandfillArray::setPadding(const Vector2i& padding) { - _state->padding = padding; - return *this; -} - -AtlasLandfillFlags AtlasLandfillArray::flags() const { - return _state->flags; -} - -AtlasLandfillArray& AtlasLandfillArray::setFlags(AtlasLandfillFlags flags) { - CORRADE_ASSERT(!(flags & AtlasLandfillFlag::RotatePortrait) || - !(flags & AtlasLandfillFlag::RotateLandscape), - "TextureTools::AtlasLandfillArray::setFlags(): only one of RotatePortrait and RotateLandscape can be set", *this); - CORRADE_ASSERT(!(flags & AtlasLandfillFlag::WidestFirst) || - !(flags & AtlasLandfillFlag::NarrowestFirst), - "TextureTools::AtlasLandfillArray::setFlags(): only one of WidestFirst and NarrowestFirst can be set", *this); - _state->flags = flags; - return *this; -} - -bool AtlasLandfillArray::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { - return atlasLandfillAdd("TextureTools::AtlasLandfillArray::add():", *_state, sizes, offsets.slice(&Vector3i::xy), offsets.slice(&Vector3i::z), flips); +bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { + CORRADE_ASSERT(_state->size.z() == 1, + "TextureTools::AtlasLandfill::add(): use the three-component overload for an array atlas", {}); + return atlasLandfillAdd(*_state, sizes, offsets, nullptr, flips); } -bool AtlasLandfillArray::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { +bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { return add(Containers::stridedArrayView(sizes), offsets, flips); } -bool AtlasLandfillArray::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets) { +bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets) { CORRADE_ASSERT(!(_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)), - "TextureTools::AtlasLandfillArray::add():" << (_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) << "set, expected a rotations view", {}); + "TextureTools::AtlasLandfill::add():" << (_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) << "set, expected a rotations view", {}); return add(sizes, offsets, nullptr); } -bool AtlasLandfillArray::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets) { +bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets) { return add(Containers::stridedArrayView(sizes), offsets); } diff --git a/src/Magnum/TextureTools/Atlas.h b/src/Magnum/TextureTools/Atlas.h index 6ca3bde53..320aec680 100644 --- a/src/Magnum/TextureTools/Atlas.h +++ b/src/Magnum/TextureTools/Atlas.h @@ -26,7 +26,7 @@ */ /** @file - * @brief Class @ref Magnum::TextureTools::AtlasLandfill, @ref Magnum::TextureTools::AtlasLandfillArray, enum @ref Magnum::TextureTools::AtlasLandfillFlag, enum set @ref Magnum::TextureTools::AtlasLandfillFlags, function @ref Magnum::TextureTools::atlas(), @ref Magnum::TextureTools::atlasArrayPowerOfTwo() + * @brief Class @ref Magnum::TextureTools::AtlasLandfill, enum @ref Magnum::TextureTools::AtlasLandfillFlag, enum set @ref Magnum::TextureTools::AtlasLandfillFlags, function @ref Magnum::TextureTools::atlas(), @ref Magnum::TextureTools::atlasArrayPowerOfTwo() */ #include @@ -51,9 +51,7 @@ namespace Implementation { @m_since_latest @see @ref AtlasLandfillFlags, @ref AtlasLandfill::setFlags(), - @ref AtlasLandfill::addFlags(), @ref AtlasLandfill::clearFlags(), - @ref AtlasLandfillArray::setFlags(), @ref AtlasLandfillArray::addFlags(), - @ref AtlasLandfillArray::clearFlags() + @ref AtlasLandfill::addFlags(), @ref AtlasLandfill::clearFlags() */ enum class AtlasLandfillFlag { /** @@ -106,8 +104,7 @@ MAGNUM_TEXTURETOOLS_EXPORT Debug& operator<<(Debug& output, AtlasLandfillFlag va @m_since_latest @see @ref Flags, @ref AtlasLandfill::setFlags(), @ref AtlasLandfill::addFlags(), - @ref AtlasLandfill::clearFlags(), @ref AtlasLandfillArray::setFlags(), - @ref AtlasLandfillArray::addFlags(), @ref AtlasLandfillArray::clearFlags() + @ref AtlasLandfill::clearFlags() */ typedef Containers::EnumSet AtlasLandfillFlags; @@ -121,10 +118,10 @@ MAGNUM_TEXTURETOOLS_EXPORT Debug& operator<<(Debug& output, AtlasLandfillFlags v @m_since_latest Keeps track of currently filled height at every pixel with the aim to fill the -available space bottom-up as evenly as possible. Packs to a 2D texture with the -height optionally unbounded. See @ref AtlasLandfillArray for a variant that -works with 2D texture arrays, and @ref atlasArrayPowerOfTwo() for a variant -that always provides optimal packing for power-of-two sizes. +available space bottom-up as evenly as possible. Packs to a 2D or a 2D array +texture with either the height or depth optionally unbounded. See also +@ref atlasArrayPowerOfTwo() for a variant that always provides optimal packing +for power-of-two sizes. @htmlinclude atlas-landfill.svg @@ -147,6 +144,14 @@ overload without the rotations argument. @snippet MagnumTextureTools.cpp AtlasLandfill-usage-no-rotation +@subsection TextureTools-AtlasLandfill-usage-atlas Array atlas + +The packing can be extended to a third dimension as well, in which case the +packing overflows to next slices instead of expanding to potentially unbounded +height. + +@snippet MagnumTextureTools.cpp AtlasLandfill-usage-array + @section TextureTools-AtlasLandfill-process Packing process On every @ref add(), the algorithm first makes all sizes the same orientation @@ -170,9 +175,9 @@ bounded and the next item cannot fit there anymore. The sort is performed using @ref std::stable_sort(), which is usually @f$ \mathcal{O}(n \log{} n) @f$, the actual atlasing is a single @f$ \mathcal{O}(n) @f$ operation. Memory complexity is -@f$ \mathcal{O}(n + w) @f$ with @f$ n @f$ being a sorted copy of the input size -array and @f$ w @f$ being a 16-bit integer for every pixel of atlas width, -additionally @ref std::stable_sort() performs its own allocation. +@f$ \mathcal{O}(n + wc) @f$ with @f$ n @f$ being a sorted copy of the input +size array and @f$ wc @f$ being a 16-bit integer for every pixel of atlas width +times filled atlas depth. Additionally @ref std::stable_sort() performs its own allocation. @section TextureTools-AtlasLandfill-incremental Incremental population @@ -182,6 +187,12 @@ ideal scenario, if the previous fill resulted in an uniform height the newly added data will be added in an optimal way as well, but in practice calling @ref add() with all data just once will always result in a more optimal packing than an incremental one. + +In case of an array atlas, the incremental process always starts from the first +slice, finding the first that can fit the first (sorted) item. Then it attempts +to place as many items as possible and on overflow continues searching for the +next slice that can fit the first remaining item. If all slices are exhausted, +adds a new one for as long as the depth (if bounded) allows. */ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { public: @@ -189,8 +200,17 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { * @brief Constructor * * The @p size is expected to have non-zero width, and height not - * larger than 65536. If height is zero, the dimension is treated as - * unbounded, i.e. @ref add() never fails. + * larger than 65536. If height is @cpp 0 @ce, depth is expected to be + * @cpp 1 @ce and the height is treated as unbounded, i.e. @ref add() + * never fails. Otherwise, if depth is @cpp 0 @ce, depth is treated as + * unbounded. + */ + explicit AtlasLandfill(const Vector3i& size); + + /** + * @brief Construct a non-array atlas + * + * Same as calling @ref AtlasLandfill with depth set to @cpp 1 @ce. */ explicit AtlasLandfill(const Vector2i& size); @@ -213,17 +233,24 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { * * @see @ref filledSize() */ - Vector2i size() const; + Vector3i size() const; /** * @brief Currently filled size * - * Width is always taken from @ref size(). The height is @cpp 0 @ce - * initially, and at most the height of @ref size() if it's bounded. - * The size is calculated with a @f$ \mathcal{O}(w) @f$ complexity, - * with @f$ w @f$ being the atlas width. + * Width is always taken from @ref size(). + * + * If @ref size() depth is @cpp 1 @ce, the returned depth is always + * @cpp 1 @ce, height is @cpp 0 @ce initially, and at most the + * height of @ref size() if it's bounded. It's calculated with a + * @f$ \mathcal{O}(w) @f$ complexity, with @f$ w @f$ being the atlas + * width. + * + * Otherwise, if @ref size() depth is not @cpp 1 @ce, the height is + * taken from @ref size() and the depth is @cpp 0 @ce initially, and + * at most @ref size() depth if the size is bounded. */ - Vector2i filledSize() const; + Vector3i filledSize() const; /** * @brief Behavior flags @@ -235,15 +262,18 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { /** * @brief Set behavior flags + * @return Reference to self (for method chaining) * - * Can be called with different values before each particular - * @ref add(). + * Note that some flags are mutually exclusive, see documentation of + * particular @ref AtlasLandfillFlag values for more information. Can + * be called with different values before each particular @ref add(). * @see @ref addFlags(), @ref clearFlags() */ AtlasLandfill& setFlags(AtlasLandfillFlags flags); /** * @brief Add behavior flags + * @return Reference to self (for method chaining) * * Calls @ref setFlags() with the existing flags ORed with @p flags. * Useful for preserving the defaults. @@ -255,6 +285,7 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { /** * @brief Clear behavior flags + * @return Reference to self (for method chaining) * * Calls @ref setFlags() with the existing flags ANDed with the inverse * of @p flags. Useful for preserving the defaults. @@ -273,9 +304,11 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { /** * @brief Set padding around each texture + * @return Reference to self (for method chaining) * * Sizes are extended with twice the padding value before placement but - * the returned offsets are without padding again. In order to have + * the returned offsets are without padding again. The third dimension + * isn't treated in any special way. In order to have * @ref AtlasLandfillFlag::RotatePortrait and * @relativeref{AtlasLandfillFlag,RotateLandscape} work well also * with non-uniform padding, the padding is applied *before* a @@ -305,8 +338,8 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { * @relativeref{AtlasLandfillFlag,RotateLandscape} is set, the * @p rotations view can be also empty or you can use the * @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) - * overload. The @p offsets always point to the original potentially - * rotated sizes without padding applied. + * overload. The resulting @p offsets always point to the original + * (potentially rotated) sizes without padding applied. * * On success returns @cpp true @ce and updates @ref filledSize(). If * @ref size() is bounded, can return @cpp false @ce if the items @@ -315,208 +348,49 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { * @ref size() returns @cpp true @ce always. * @see @ref setFlags(), @ref setPadding() */ - bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); + bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); /** @overload */ - bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); + bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); /** * @brief Add textures to the atlas with rotations disabled * - * Equivalent to calling @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, Containers::MutableBitArrayView) + * Equivalent to calling @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, Containers::MutableBitArrayView) * with the @p rotations view being empty. Can be called only if * neither @ref AtlasLandfillFlag::RotatePortrait nor * @relativeref{AtlasLandfillFlag,RotateLandscape} is set. * @see @ref clearFlags() */ - bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets); + bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets); /** @overload */ - bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets); - - private: - Containers::Pointer _state; -}; - -/** -@brief Landfill texture atlas packer -@m_since_latest - -Extends @ref AtlasLandfill to a third dimension. Instead of expanding to an -unbounded height, on overflow a new texture slice is made. See also -@ref atlasArrayPowerOfTwo() for a variant that always provides optimal packing -for power-of-two sizes. - -@section TextureTools-AtlasLandfillArray-usage Example usage - -Compared to the @ref TextureTools-AtlasLandfill-usage "2D usage" it's extended -to three dimensions: - -@snippet MagnumTextureTools.cpp AtlasLandfillArray-usage - -@section TextureTools-AtlasLandfillArray-process Packing process - -Apart from expanding to new slices on height overflow, the underlying process -is @ref TextureTools-AtlasLandfill-process "the same as in AtlasLandfill". - -In this case, memory complexity is @f$ \mathcal{O}(n + wd) @f$ with @f$ n @f$ -being a sorted copy of the input size array and @f$ wd @f$ being a 16-bit -integer for every pixel of atlas width times atlas depth. - -@section TextureTools-AtlasLandfillArray-incremental Incremental population - -Compared to the @ref TextureTools-AtlasLandfill-incremental "2D incremental population", -the incremental process always starts from the first slice, finding the first -that can fit the first (sorted) item. Then it attempts to place as many items -as possible and on overflow continues searching for the next slice that can fit -the first remaining item. If all slices are exhausted, adds a new one. -*/ -class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfillArray { - public: - /** - * @brief Constructor - * - * The @p size has to have non-zero width and height. If depth is - * @cpp 0 @ce, the dimension is treated as unbounded, i.e. @ref add() - * never fails. If depth is @cpp 1 @ce, behaves the same as - * @ref AtlasLandfillArray with a bounded height. - */ - explicit AtlasLandfillArray(const Vector3i& size); - - /** @brief Copying is not allowed */ - AtlasLandfillArray(const AtlasLandfillArray&) = delete; - - /** @brief Move constructor */ - AtlasLandfillArray(AtlasLandfillArray&&) noexcept; - - ~AtlasLandfillArray(); - - /** @brief Copying is not allowed */ - AtlasLandfillArray& operator=(const AtlasLandfillArray&) = delete; - - /** @brief Move assignment */ - AtlasLandfillArray& operator=(AtlasLandfillArray&&) noexcept; - - /** - * @brief Atlas size specified in constructor - * - * @see @ref filledSize() - */ - Vector3i size() const; - - /** - * @brief Currently filled size - * - * Width and height is always taken from @ref size(). The depth is - * @cpp 0 @ce initially, and at most @ref size() depth if the size is - * bounded. - */ - Vector3i filledSize() const; - - /** @brief Behavior flags */ - AtlasLandfillFlags flags() const; - - /** - * @brief Set behavior flags - * - * Can be called with different values before each particular - * @ref add(). Default is @ref AtlasLandfillFlag::RotatePortrait. - * @see @ref addFlags(), @ref clearFlags() - */ - AtlasLandfillArray& setFlags(AtlasLandfillFlags flags); - - /** - * @brief Add behavior flags - * - * Calls @ref setFlags() with the existing flags ORed with @p flags. - * Useful for preserving the defaults. - * @see @ref clearFlags() - */ - AtlasLandfillArray& addFlags(AtlasLandfillFlags flags) { - return setFlags(this->flags()|flags); - } - - /** - * @brief Clear behavior flags - * - * Calls @ref setFlags() with the existing flags ANDed with the inverse - * of @p flags. Useful for preserving the defaults. - * @see @ref addFlags() - */ - AtlasLandfillArray& clearFlags(AtlasLandfillFlags flags) { - return setFlags(this->flags() & ~flags); - } - - /** - * @brief Padding around each texture - * - * Default is a zero vector. - */ - Vector2i padding() const; - - /** - * @brief Set padding around each texture - * - * Sizes are extended with twice the padding value before placement but - * the returned offsets are without padding again. The third dimension - * isn't treated in any special way. In order to have - * @ref AtlasLandfillFlag::RotatePortrait and - * @relativeref{AtlasLandfillFlag,RotateLandscape} work well also - * with non-uniform padding, the padding is applied *before* a - * potential rotation. I.e., the horizontal padding value is always - * applied on input image width independently on how it's rotated - * after. If you need different behavior, disable rotations with - * @ref clearFlags() or pre-pad the input sizes directly instead of - * using this function. - * - * Can be called with different values before each particular - * @ref add(). - */ - AtlasLandfillArray& setPadding(const Vector2i& padding); + bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets); /** - * @brief Add textures to the atlas - * @param[in] sizes Texture sizes - * @param[out] offsets Resulting offsets in the atlas - * @param[out] rotations Which textures got rotated - * - * The @p sizes, @p offsets and @p rotations views are expected to have - * the same size. The @p sizes are all expected to be non-zero and not - * larger than @ref size() after applying padding and then a rotation - * based on @ref AtlasLandfillFlag::RotatePortrait or - * @relativeref{AtlasLandfillFlag,RotateLandscape} being set. If - * neither @relativeref{AtlasLandfillFlag,RotatePortrait} nor - * @relativeref{AtlasLandfillFlag,RotateLandscape} is set, the - * @p rotations view can be also empty or you can use the - * @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) - * overload. The @p offsets always point to the original potentially - * rotated sizes without padding applied. + * @brief Add textures to a non-array atlas * - * On success returns @cpp true @ce and updates @ref filledSize(). If - * @ref size() is bounded, can return @cpp false @ce if the items - * didn't fit, in which case the internals and contents of @p offsets - * and @p rotations are left in an undefined state. For an unbounded - * @ref size() returns @cpp true @ce always. - * @see @ref setFlags(), @ref setPadding() + * Can be called only if @ref size() depth is @cpp 1 @ce. */ - bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); + bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); /** @overload */ - bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); + bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); /** - * @brief Add textures to the atlas with rotations disabled + * @brief Add textures to a non-array atlas with rotations disabled * - * Equivalent to calling @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, Containers::MutableBitArrayView) + * Equivalent to calling @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, Containers::MutableBitArrayView) * with the @p rotations view being empty. Can be called only if - * neither @ref AtlasLandfillFlag::RotatePortrait nor + * @ref size() depth is @cpp 1 @ce and neither + * @ref AtlasLandfillFlag::RotatePortrait nor * @relativeref{AtlasLandfillFlag,RotateLandscape} is set. * @see @ref clearFlags() */ - bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets); + bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets); /** @overload */ - bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets); + bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets); private: Containers::Pointer _state; @@ -572,9 +446,9 @@ array, additionally @ref std::stable_sort() performs its own allocation. See the [Zero-waste single-pass packing of power-of-two textures](https://blog.magnum.graphics/backstage/pot-array-packing/) article for a detailed description of the algorithm. -See the @ref AtlasLandfill and @ref AtlasLandfillArray classes for an -alternative that isn't restricted to power-of-two sizes and can be used in an -incremental way but doesn't always produce optimal packing. +See the @ref AtlasLandfill class for an alternative that isn't restricted to +power-of-two sizes and can be used in an incremental way but doesn't always +produce optimal packing. */ MAGNUM_TEXTURETOOLS_EXPORT Int atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets); diff --git a/src/Magnum/TextureTools/Test/AtlasBenchmark.cpp b/src/Magnum/TextureTools/Test/AtlasBenchmark.cpp index ada82015e..d11632409 100644 --- a/src/Magnum/TextureTools/Test/AtlasBenchmark.cpp +++ b/src/Magnum/TextureTools/Test/AtlasBenchmark.cpp @@ -381,7 +381,7 @@ void AtlasBenchmark::landfill() { CORRADE_COMPARE_WITH( Containers::pair(Containers::StridedArrayView1D{offsets}, Containers::BitArrayView{flips}), _sizes, - (CompareAtlasPacking{data.image, atlas.filledSize()})); + (CompareAtlasPacking{data.image, atlas.filledSize().xy()})); } void AtlasBenchmark::stbRectPack() { diff --git a/src/Magnum/TextureTools/Test/AtlasTest.cpp b/src/Magnum/TextureTools/Test/AtlasTest.cpp index 395a93378..bfe6870b6 100644 --- a/src/Magnum/TextureTools/Test/AtlasTest.cpp +++ b/src/Magnum/TextureTools/Test/AtlasTest.cpp @@ -65,13 +65,12 @@ struct AtlasTest: TestSuite::Tester { void landfillArrayIncremental(); void landfillArrayPadded(); void landfillArrayNoFit(); - void landfillArrayCopy(); - void landfillArrayMove(); void landfillInvalidSize(); void landfillSetFlagsInvalid(); void landfillAddMissingRotations(); void landfillAddInvalidViewSizes(); + void landfillAddTwoComponentForArray(); void landfillAddTooLargeElement(); void landfillAddTooLargeElementPadded(); @@ -114,14 +113,14 @@ const Vector2i LandfillSizes[]{ const struct { const char* name; AtlasLandfillFlags flags; - Vector2i size; - Vector2i filledSize; + Vector3i size; + Vector3i filledSize; Containers::Pair offsetsFlips[Containers::arraySize(LandfillSizes)]; } LandfillData[]{ /* In all of these, rectangles with the same size should keep their order. 5 after 3, 9 after 8 after 6 (and b after a after 7 if they're rotated to the same orientation) */ - {"no rotation, no width sorting", {}, {11, 12}, {11, 9}, { + {"no rotation, no width sorting", {}, {11, 12, 1}, {11, 9, 1}, { /* Here it discovers that item 8 is higher than 5 and so it begins from the opposite end in the same direction again, instead of flipping the direction at item 8. @@ -149,7 +148,7 @@ const struct { {{8, 6}, false}, /* b */ {{3, 8}, false}}}, /* c */ /* No rotation with width sorting omitted, not interesting */ - {"portrait, no width sorting", AtlasLandfillFlag::RotatePortrait, {11, 12}, {11, 9}, { + {"portrait, no width sorting", AtlasLandfillFlag::RotatePortrait, {11, 12, 1}, {11, 9, 1}, { /* Here it should compare against the height of item 8, not item 0. Which is again higher than item 4 on the other side so it again begins from the opposite side. @@ -176,7 +175,7 @@ const struct { {{8, 7}, true}, /* a */ {{7, 7}, false}, /* b */ {{6, 7}, false}}}, /* c */ - {"portrait, widest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 12}, {11, 8}, { + {"portrait, widest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 12, 1}, {11, 8, 1}, { /* 9988 cba7 99886644ba7 000 6644555 @@ -198,7 +197,7 @@ const struct { {{9, 6}, true}, /* a */ {{8, 6}, false}, /* b */ {{7, 7}, false}}}, /* c */ - {"portrait, widest first, unbounded height", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 0}, {11, 8}, { + {"portrait, widest first, unbounded height", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 0, 1}, {11, 8, 1}, { /* Should have the same result as above. * 9988 cba7 @@ -222,7 +221,7 @@ const struct { {{9, 6}, true}, /* a */ {{8, 6}, false}, /* b */ {{7, 7}, false}}}, /* c */ - {"portrait, widest first, reverse direction always", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst|AtlasLandfillFlag::ReverseDirectionAlways, {11, 12}, {11, 10}, { + {"portrait, widest first, reverse direction always", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst|AtlasLandfillFlag::ReverseDirectionAlways, {11, 12, 1}, {11, 10, 1}, { /* Here it continues in reverse direction after placing item 9 even though it's higher than item 5 as it's forced to. @@ -249,7 +248,7 @@ const struct { {{1, 8}, true}, /* a */ {{2, 8}, false}, /* b */ {{3, 8}, false}}}, /* c */ - {"portrait, narrowest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::NarrowestFirst, {11, 12}, {11, 9}, { + {"portrait, narrowest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::NarrowestFirst, {11, 12, 1}, {11, 9, 1}, { /* 99 66b c9988 66ba7555 88 @@ -272,7 +271,7 @@ const struct { {{3, 5}, true}, /* a */ {{2, 6}, false}, /* b */ {{6, 7}, false}}}, /* c */ - {"landscape, no width sorting", AtlasLandfillFlag::RotateLandscape, {11, 12}, {11, 9}, { + {"landscape, no width sorting", AtlasLandfillFlag::RotateLandscape, {11, 12, 1}, {11, 9, 1}, { /* After placing 3 it continues in reverse direction as 0 isn't lower (i.e., same behavior as if reversal was forced, and makes sense); after placing 1 it continues in reverse direction with 2 again; @@ -300,7 +299,7 @@ const struct { {{4, 7}, false}, /* a */ {{6, 8}, true}, /* b */ {{8, 8}, false}}}, /* c */ - {"landscape, widest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst, {11, 12}, {11, 9}, { + {"landscape, widest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst, {11, 12, 1}, {11, 9, 1}, { /* No change compared to "no width sorting" in this case. 99 bbc @@ -325,7 +324,7 @@ const struct { {{4, 7}, false}, /* a */ {{6, 8}, true}, /* b */ {{8, 8}, false}}}, /* c */ - {"landscape, narrowest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst, {11, 12}, {11, 10}, { + {"landscape, narrowest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst, {11, 12, 1}, {11, 10, 1}, { /* No special behavior worth commenting on here. Flips direction after placing 5, after 8, and doesn't after placing 2. @@ -489,13 +488,12 @@ AtlasTest::AtlasTest() { addTests({&AtlasTest::landfillArrayIncremental, &AtlasTest::landfillArrayPadded, &AtlasTest::landfillArrayNoFit, - &AtlasTest::landfillArrayCopy, - &AtlasTest::landfillArrayMove, &AtlasTest::landfillInvalidSize, &AtlasTest::landfillSetFlagsInvalid, &AtlasTest::landfillAddMissingRotations, &AtlasTest::landfillAddInvalidViewSizes, + &AtlasTest::landfillAddTwoComponentForArray, &AtlasTest::landfillAddTooLargeElement, &AtlasTest::landfillAddTooLargeElementPadded, @@ -544,8 +542,8 @@ void AtlasTest::landfillFullFit() { a tight fit */ AtlasLandfill atlas{{4, 6}}; - CORRADE_COMPARE(atlas.size(), (Vector2i{4, 6})); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{4, 0})); + CORRADE_COMPARE(atlas.size(), (Vector3i{4, 6, 1})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 0, 1})); CORRADE_COMPARE(atlas.flags(), AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst); CORRADE_COMPARE(atlas.padding(), Vector2i{}); @@ -559,7 +557,7 @@ void AtlasTest::landfillFullFit() { {2, 3}, /* 2 */ {2, 2}, /* 3 */ }, offsets, rotations)); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{4, 6})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 6, 1})); CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ false, false, false, false }).sliceBit(0), TestSuite::Compare::Container); @@ -639,25 +637,25 @@ void AtlasTest::landfillIncremental() { Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(sizeData)}; AtlasLandfill atlas{{11, 8}}; - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 0})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 0, 1})); CORRADE_VERIFY(atlas.add( sizes.prefix(5), offsets.prefix(5), rotations.prefix(5))); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 6})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 1})); CORRADE_VERIFY(atlas.add( sizes.slice(5, 9), offsets.slice(5, 9), rotations.slice(5, 9))); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 8})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 8, 1})); CORRADE_VERIFY(atlas.add( sizes.exceptPrefix(9), offsets.exceptPrefix(9), rotations.exceptPrefix(9))); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 8})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 8, 1})); CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ true, false, false, true, false, false, false, false, true, false, @@ -706,7 +704,7 @@ void AtlasTest::landfillPadded() { {1, 1}, /* 5, padded to {3, 5} */ }, offsets, rotations)); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{15, 13})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{15, 13, 1})); CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ true, false, true, false, false, false }).sliceBit(0), TestSuite::Compare::Container); @@ -754,20 +752,20 @@ void AtlasTest::landfillCopy() { } void AtlasTest::landfillMove() { - AtlasLandfill a{{16, 24}}; + AtlasLandfill a{{16, 24, 8}}; - Vector2i offsets[2]; + Vector3i offsets[2]; UnsignedByte rotations[1]; - CORRADE_VERIFY(a.add({{15, 17}, {2, 3}}, offsets, Containers::MutableBitArrayView{rotations, 0, 2})); + CORRADE_VERIFY(a.add({{12, 17}, {5, 12}}, offsets, Containers::MutableBitArrayView{rotations, 0, 2})); AtlasLandfill b = Utility::move(a); - CORRADE_COMPARE(b.size(), (Vector2i{16, 24})); - CORRADE_COMPARE(b.filledSize(), (Vector2i{16, 20})); + CORRADE_COMPARE(b.size(), (Vector3i{16, 24, 8})); + CORRADE_COMPARE(b.filledSize(), (Vector3i{16, 24, 2})); - AtlasLandfill c{{16, 12}}; + AtlasLandfill c{{16, 12, 1}}; c = Utility::move(b); - CORRADE_COMPARE(c.size(), (Vector2i{16, 24})); - CORRADE_COMPARE(c.filledSize(), (Vector2i{16, 20})); + CORRADE_COMPARE(c.size(), (Vector3i{16, 24, 8})); + CORRADE_COMPARE(c.filledSize(), (Vector3i{16, 24, 2})); CORRADE_VERIFY(std::is_nothrow_move_constructible::value); CORRADE_VERIFY(std::is_nothrow_move_assignable::value); @@ -777,7 +775,7 @@ void AtlasTest::landfillArrayFullFit() { /* Trivial case to verify there are no off-by-one errors that would prevent a tight fit */ - AtlasLandfillArray atlas{{4, 5, 2}}; + AtlasLandfill atlas{{4, 5, 2}}; CORRADE_COMPARE(atlas.size(), (Vector3i{4, 5, 2})); CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 5, 0})); CORRADE_COMPARE(atlas.flags(), AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst); @@ -819,7 +817,7 @@ void AtlasTest::landfillArray() { auto&& data = LandfillArrayData[testCaseInstanceId()]; setTestCaseDescription(data.name); - AtlasLandfillArray atlas{data.size}; + AtlasLandfill atlas{data.size}; /* For unbounded sizes it should return 0 again */ CORRADE_COMPARE(atlas.size(), data.size); @@ -871,7 +869,7 @@ void AtlasTest::landfillArrayIncremental() { UnsignedByte rotationData[2]; Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(sizeData)}; - AtlasLandfillArray atlas{{11, 6, 2}}; + AtlasLandfill atlas{{11, 6, 2}}; CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 0})); CORRADE_VERIFY(atlas.add( @@ -917,9 +915,9 @@ void AtlasTest::landfillArrayIncremental() { } void AtlasTest::landfillArrayPadded() { - /* Like landfillPadded(), but item 5 overlflowing to the next slice */ + /* Like landfillPadded(), but item 5 overflowing to the next slice */ - AtlasLandfillArray atlas{{15, 12, 3}}; + AtlasLandfill atlas{{15, 12, 3}}; atlas.setPadding({1, 2}); CORRADE_COMPARE(atlas.padding(), (Vector2i{1, 2})); @@ -968,7 +966,7 @@ void AtlasTest::landfillArrayNoFit() { /* Same as landfillArray(portrait, widest first) (which is the default flags) which fits into {11, 6, 2} but limiting depth to 1 */ - AtlasLandfillArray atlas{{11, 6, 1}}; + AtlasLandfill atlas{{11, 6, 1}}; Vector3i offsets[Containers::arraySize(LandfillArraySizes)]; UnsignedByte rotationData[2]; @@ -976,53 +974,28 @@ void AtlasTest::landfillArrayNoFit() { CORRADE_VERIFY(!atlas.add(LandfillArraySizes, offsets, rotations)); } -void AtlasTest::landfillArrayCopy() { - CORRADE_VERIFY(!std::is_copy_constructible{}); - CORRADE_VERIFY(!std::is_copy_assignable{}); -} - -void AtlasTest::landfillArrayMove() { - AtlasLandfillArray a{{16, 24, 8}}; - - Vector3i offsets[2]; - UnsignedByte rotations[1]; - CORRADE_VERIFY(a.add({{12, 17}, {5, 12}}, offsets, Containers::MutableBitArrayView{rotations, 0, 2})); - - AtlasLandfillArray b = Utility::move(a); - CORRADE_COMPARE(b.size(), (Vector3i{16, 24, 8})); - CORRADE_COMPARE(b.filledSize(), (Vector3i{16, 24, 2})); - - AtlasLandfillArray c{{16, 12, 1}}; - c = Utility::move(b); - CORRADE_COMPARE(c.size(), (Vector3i{16, 24, 8})); - CORRADE_COMPARE(c.filledSize(), (Vector3i{16, 24, 2})); - - CORRADE_VERIFY(std::is_nothrow_move_constructible::value); - CORRADE_VERIFY(std::is_nothrow_move_assignable::value); -} - void AtlasTest::landfillInvalidSize() { CORRADE_SKIP_IF_NO_ASSERT(); /* These are fine */ AtlasLandfill{{16, 0}}; AtlasLandfill{{65536, 16}}; - AtlasLandfillArray{{16, 16, 0}}; - AtlasLandfillArray{{65536, 16, 16}}; + AtlasLandfill{{16, 16, 0}}; + AtlasLandfill{{65536, 16, 16}}; std::ostringstream out; Error redirectError{&out}; AtlasLandfill{{0, 16}}; AtlasLandfill{{65537, 16}}; - AtlasLandfillArray{{0, 16, 16}}; - AtlasLandfillArray{{16, 0, 16}}; - AtlasLandfillArray{{65537, 16, 16}}; + AtlasLandfill{{0, 16, 16}}; + AtlasLandfill{{16, 0, 16}}; + AtlasLandfill{{65537, 16, 16}}; CORRADE_COMPARE_AS(out.str(), - "TextureTools::AtlasLandfill: expected non-zero width, got {0, 16}\n" - "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got {65537, 16}\n" - "TextureTools::AtlasLandfillArray: expected non-zero width and height, got {0, 16, 16}\n" - "TextureTools::AtlasLandfillArray: expected non-zero width and height, got {16, 0, 16}\n" - "TextureTools::AtlasLandfillArray: expected width to fit into 16 bits, got {65537, 16, 16}\n", + "TextureTools::AtlasLandfill: expected non-zero width, got {0, 16, 1}\n" + "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got {65537, 16, 1}\n" + "TextureTools::AtlasLandfill: expected non-zero width, got {0, 16, 16}\n" + "TextureTools::AtlasLandfill: expected a single array slice for unbounded height, got {16, 0, 16}\n" + "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got {65537, 16, 16}\n", TestSuite::Compare::String); } @@ -1030,49 +1003,40 @@ void AtlasTest::landfillSetFlagsInvalid() { CORRADE_SKIP_IF_NO_ASSERT(); AtlasLandfill atlas{{16, 16}}; - AtlasLandfillArray array{{16, 16, 1}}; std::ostringstream out; Error redirectError{&out}; atlas.setFlags(AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape); - array.setFlags(AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape); atlas.setFlags(AtlasLandfillFlag::WidestFirst|AtlasLandfillFlag::NarrowestFirst); - array.setFlags(AtlasLandfillFlag::WidestFirst|AtlasLandfillFlag::NarrowestFirst); CORRADE_COMPARE_AS(out.str(), "TextureTools::AtlasLandfill::setFlags(): only one of RotatePortrait and RotateLandscape can be set\n" - "TextureTools::AtlasLandfillArray::setFlags(): only one of RotatePortrait and RotateLandscape can be set\n" - "TextureTools::AtlasLandfill::setFlags(): only one of WidestFirst and NarrowestFirst can be set\n" - "TextureTools::AtlasLandfillArray::setFlags(): only one of WidestFirst and NarrowestFirst can be set\n", + "TextureTools::AtlasLandfill::setFlags(): only one of WidestFirst and NarrowestFirst can be set\n", TestSuite::Compare::String); } void AtlasTest::landfillAddMissingRotations() { CORRADE_SKIP_IF_NO_ASSERT(); - AtlasLandfill atlasPortrait{{16, 23}}; - AtlasLandfill atlasLandscape{{16, 23}}; - AtlasLandfillArray arrayPortrait{{16, 23, 2}}; - AtlasLandfillArray arrayLandscape{{16, 23, 2}}; - atlasPortrait.setFlags(AtlasLandfillFlag::RotatePortrait); - arrayPortrait.setFlags(AtlasLandfillFlag::RotatePortrait); - atlasLandscape.setFlags(AtlasLandfillFlag::RotateLandscape); - arrayLandscape.setFlags(AtlasLandfillFlag::RotateLandscape); + AtlasLandfill portrait{{16, 23}}; + AtlasLandfill landscape{{16, 23}}; + portrait.setFlags(AtlasLandfillFlag::RotatePortrait); + landscape.setFlags(AtlasLandfillFlag::RotateLandscape); Vector2i sizes[2]; Vector2i offsets[2]; Vector3i offsets3[2]; std::ostringstream out; Error redirectError{&out}; - atlasPortrait.add(sizes, offsets); - arrayPortrait.add(sizes, offsets3); + portrait.add(sizes, offsets); + portrait.add(sizes, offsets3); /* "Testing" the rotation-less init list variants too */ - atlasLandscape.add({{}, {}}, offsets); - arrayLandscape.add({{}, {}}, offsets3); + landscape.add({{}, {}}, offsets); + landscape.add({{}, {}}, offsets3); CORRADE_COMPARE(out.str(), "TextureTools::AtlasLandfill::add(): TextureTools::AtlasLandfillFlag::RotatePortrait set, expected a rotations view\n" - "TextureTools::AtlasLandfillArray::add(): TextureTools::AtlasLandfillFlag::RotatePortrait set, expected a rotations view\n" + "TextureTools::AtlasLandfill::add(): TextureTools::AtlasLandfillFlag::RotatePortrait set, expected a rotations view\n" "TextureTools::AtlasLandfill::add(): TextureTools::AtlasLandfillFlag::RotateLandscape set, expected a rotations view\n" - "TextureTools::AtlasLandfillArray::add(): TextureTools::AtlasLandfillFlag::RotateLandscape set, expected a rotations view\n"); + "TextureTools::AtlasLandfill::add(): TextureTools::AtlasLandfillFlag::RotateLandscape set, expected a rotations view\n"); } void AtlasTest::landfillAddInvalidViewSizes() { @@ -1095,16 +1059,38 @@ void AtlasTest::landfillAddInvalidViewSizes() { "TextureTools::AtlasLandfill::add(): expected sizes and rotations views to have the same size, got 2 and 3\n"); } +void AtlasTest::landfillAddTwoComponentForArray() { + CORRADE_SKIP_IF_NO_ASSERT(); + + AtlasLandfill atlas{{16, 23, 3}}; + atlas.clearFlags(AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape); + Vector2i sizes[2]; + Vector2i offsets[2]; + UnsignedByte rotationsData[1]; + Containers::MutableBitArrayView rotations{rotationsData, 0, 2}; + + std::ostringstream out; + Error redirectError{&out}; + atlas.add(sizes, offsets, rotations); + atlas.add(sizes, offsets); + atlas.add({}, offsets, rotations); + atlas.add({}, offsets); + CORRADE_COMPARE(out.str(), + "TextureTools::AtlasLandfill::add(): use the three-component overload for an array atlas\n" + "TextureTools::AtlasLandfill::add(): use the three-component overload for an array atlas\n" + "TextureTools::AtlasLandfill::add(): use the three-component overload for an array atlas\n" + "TextureTools::AtlasLandfill::add(): use the three-component overload for an array atlas\n"); +} + void AtlasTest::landfillAddTooLargeElement() { CORRADE_SKIP_IF_NO_ASSERT(); - /* The atlas makes the sizes portrait first, the array landscape instead */ - AtlasLandfill atlas{{16, 23}}; - AtlasLandfill atlas2{{16, 13}}; - AtlasLandfillArray array{{23, 16, 3}}; - AtlasLandfillArray array2{{13, 16, 3}}; - array.setFlags(AtlasLandfillFlag::RotateLandscape); - array2.setFlags(AtlasLandfillFlag::RotateLandscape); + AtlasLandfill portrait{{16, 23}}; + AtlasLandfill portrait2{{16, 13}}; + AtlasLandfill landscape{{23, 16}}; + AtlasLandfill landscape2{{13, 16}}; + landscape.setFlags(AtlasLandfillFlag::RotateLandscape); + landscape2.setFlags(AtlasLandfillFlag::RotateLandscape); Vector2i offsets[2]; Vector3i offsets3[2]; UnsignedByte rotationsData[1]; @@ -1112,20 +1098,20 @@ void AtlasTest::landfillAddTooLargeElement() { std::ostringstream out; Error redirectError{&out}; - atlas.add({{16, 23}, {0, 23}}, offsets, rotations); - array.add({{23, 16}, {23, 0}}, offsets3, rotations); - atlas.add({{16, 23}, {17, 23}}, offsets, rotations); - array.add({{23, 16}, {23, 17}}, offsets3, rotations); + portrait.add({{16, 23}, {0, 23}}, offsets, rotations); + landscape.add({{23, 16}, {23, 0}}, offsets3, rotations); + portrait.add({{16, 23}, {17, 23}}, offsets, rotations); + landscape.add({{23, 16}, {23, 17}}, offsets3, rotations); /* Sizes that fit but don't after a flip */ - atlas2.add({{13, 13}, {15, 13}}, offsets, rotations); - array2.add({{13, 13}, {13, 15}}, offsets3, rotations); + portrait2.add({{13, 13}, {15, 13}}, offsets, rotations); + landscape2.add({{13, 13}, {13, 15}}, offsets3, rotations); CORRADE_COMPARE_AS(out.str(), "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {0, 23}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {23, 0}\n" + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {23, 0}\n" "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {17, 23}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {23, 17}\n" + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {23, 17}\n" "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 13} but got {13, 15}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {13, 16} but got {15, 13}\n", + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {13, 16} but got {15, 13}\n", TestSuite::Compare::String); } @@ -1134,16 +1120,15 @@ void AtlasTest::landfillAddTooLargeElementPadded() { CORRADE_SKIP_IF_NO_ASSERT(); - /* The atlas makes the sizes portrait first, the array landscape instead */ - AtlasLandfill atlas{{16, 23}}; - AtlasLandfill atlas2{{16, 13}}; - AtlasLandfillArray array{{23, 16, 3}}; - AtlasLandfillArray array2{{13, 16, 3}}; - atlas.setPadding({2, 1}); - atlas2.setPadding({2, 1}); - array.setPadding({1, 2}) + AtlasLandfill portrait{{16, 23}}; + AtlasLandfill portrait2{{16, 13}}; + AtlasLandfill landscape{{23, 16}}; + AtlasLandfill landscape2{{13, 16}}; + portrait.setPadding({2, 1}); + portrait2.setPadding({2, 1}); + landscape.setPadding({1, 2}) .setFlags(AtlasLandfillFlag::RotateLandscape); - array2.setPadding({1, 2}) + landscape2.setPadding({1, 2}) .setFlags(AtlasLandfillFlag::RotateLandscape); Vector2i offsets[2]; Vector3i offsets3[2]; @@ -1152,20 +1137,20 @@ void AtlasTest::landfillAddTooLargeElementPadded() { std::ostringstream out; Error redirectError{&out}; - atlas.add({{12, 21}, {0, 21}}, offsets, rotations); - array.add({{21, 12}, {21, 0}}, offsets3, rotations); - atlas.add({{12, 21}, {13, 21}}, offsets, rotations); - array.add({{21, 12}, {21, 13}}, offsets3, rotations); + portrait.add({{12, 21}, {0, 21}}, offsets, rotations); + landscape.add({{21, 12}, {21, 0}}, offsets3, rotations); + portrait.add({{12, 21}, {13, 21}}, offsets, rotations); + landscape.add({{21, 12}, {21, 13}}, offsets3, rotations); /* Sizes that fit but don't after a flip */ - atlas2.add({{9, 11}, {12, 11}}, offsets, rotations); - array2.add({{11, 9}, {11, 12}}, offsets3, rotations); + portrait2.add({{9, 11}, {12, 11}}, offsets, rotations); + landscape2.add({{11, 9}, {11, 12}}, offsets3, rotations); CORRADE_COMPARE_AS(out.str(), "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {0, 21} and padding {2, 1}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {21, 0} and padding {1, 2}\n" + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {21, 0} and padding {1, 2}\n" "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {13, 21} and padding {2, 1}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {21, 13} and padding {1, 2}\n" + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {21, 13} and padding {1, 2}\n" "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 13} but got {11, 12} and padding {1, 2}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {13, 16} but got {12, 11} and padding {2, 1}\n", + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {13, 16} but got {12, 11} and padding {2, 1}\n", TestSuite::Compare::String); }