diff --git a/doc/generated/atlas.cpp b/doc/generated/atlas.cpp index 36804f219d..bb07c5fd38 100644 --- a/doc/generated/atlas.cpp +++ b/doc/generated/atlas.cpp @@ -25,6 +25,9 @@ #include #include +#include +#include +#include #include #include /** @todo remove once formatString() isn't used */ #include /** @todo remove once growable String exists */ @@ -114,5 +117,40 @@ int main() { )"); CORRADE_INTERNAL_ASSERT_OUTPUT(Utility::Path::write("atlas-array-power-of-two.svg", Containers::StringView{out})); + + /* AtlasLandfill */ + } { + constexpr Float displaySizeDivisor = 1.0f; + + Containers::Optional> sizeData = Utility::Path::read(Utility::Path::join(Utility::Path::split(__FILE__).first(), "../../src/Magnum/TextureTools/Test/oxygen-glyphs.bin")); + CORRADE_INTERNAL_ASSERT(sizeData); + const auto sizes = Containers::arrayCast(*sizeData); + + TextureTools::AtlasLandfill atlas{{512, 512}}; + Containers::Array offsets{NoInit, sizes.size()}; + Containers::BitArray rotations{NoInit, sizes.size()}; + CORRADE_INTERNAL_ASSERT(atlas.add(sizes, offsets, rotations)); + + Range2Di viewBox{{}, atlas.filledSize()}; + + std::string out; + Utility::formatInto(out, out.size(), R"( +)", + viewBox.left(), viewBox.bottom(), viewBox.sizeX(), viewBox.sizeY(), viewBox.sizeX()/displaySizeDivisor, viewBox.sizeY()/displaySizeDivisor); + + for(std::size_t i = 0; i != sizes.size(); ++i) { + const Vector2i size = rotations[i] ? sizes[i].flipped() : sizes[i]; + const Vector2i offset = offsets[i]; + const Color4ub color = DebugTools::ColorMap::turbo()[colorDist(rd)]; + + Utility::formatInto(out, out.size(), R"( +)", + offset.x(), viewBox.sizeY() - size.y() - offset.y(), size.x(), size.y(), color.r(), color.g(), color.b()); + } + + Utility::formatInto(out, out.size(), R"( +)"); + + CORRADE_INTERNAL_ASSERT_OUTPUT(Utility::Path::write("atlas-landfill.svg", Containers::StringView{out})); } } diff --git a/doc/snippets/MagnumTextureTools.cpp b/doc/snippets/MagnumTextureTools.cpp index 0af35df687..a9f8fedb2b 100644 --- a/doc/snippets/MagnumTextureTools.cpp +++ b/doc/snippets/MagnumTextureTools.cpp @@ -24,6 +24,8 @@ */ #include +#include +#include #include #include @@ -39,6 +41,82 @@ using namespace Magnum; int main() { +{ +/* [AtlasLandfill-usage] */ +Containers::ArrayView images = DOXYGEN_ELLIPSIS({}); +Containers::Array offsets{NoInit, images.size()}; +Containers::BitArray rotations{NoInit, images.size()}; + +/* Fill the atlas with an unbounded height */ +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(), + Containers::Array{ValueInit, std::size_t(atlas.filledSize().product())}}; +Containers::StridedArrayView2D dst = output.pixels(); +for(std::size_t i = 0; i != images.size(); ++i) { + /* Rotate 90° counterclockwise if the image is rotated in the atlas */ + Containers::StridedArrayView2D src = rotations[i] ? + images[i].pixels().flipped<1>().transposed<0, 1>() : + images[i].pixels(); + Utility::copy(src, dst.sliceSize( + {std::size_t(offsets[i].y()), + std::size_t(offsets[i].x())}, src.size())); +} +/* [AtlasLandfill-usage] */ +} + +{ +Containers::ArrayView images; +Containers::Array offsets{NoInit, images.size()}; +TextureTools::AtlasLandfill atlas{{1024, 0}}; +/* [AtlasLandfill-usage-no-rotation] */ +atlas.clearFlags(TextureTools::AtlasLandfillFlag::RotatePortrait| + TextureTools::AtlasLandfillFlag::RotateLandscape) + .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(), + Containers::Array{ValueInit, std::size_t(atlas.filledSize().product())}}; +Containers::StridedArrayView2D dst = output.pixels(); +for(std::size_t i = 0; i != images.size(); ++i) { + Containers::StridedArrayView2D src = images[i].pixels(); + Utility::copy(src, dst.sliceSize( + {std::size_t(offsets[i].y()), + std::size_t(offsets[i].x())}, src.size())); +} +/* [AtlasLandfill-usage-no-rotation] */ +} + +{ +/* [AtlasLandfillArray-usage] */ +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}}; +atlas.add(stridedArrayView(images).slice(&ImageView2D::size), offsets, rotations); + +/* Copy the image data to the atlas, assuming all are RGBA8Unorm as well */ +Vector3i outputSize = atlas.filledSize(); +Image3D output{PixelFormat::RGBA8Unorm, outputSize, + Containers::Array{ValueInit, std::size_t(outputSize.product())}}; +Containers::StridedArrayView3D dst = output.pixels(); +for(std::size_t i = 0; i != images.size(); ++i) { + /* Rotate 90° counterclockwise if the image is rotated in the atlas */ + Containers::StridedArrayView3D src = rotations[i] ? + images[i].pixels().flipped<1>().transposed<0, 1>() : + images[i].pixels(); + Utility::copy(src, dst.sliceSize( + {std::size_t(offsets[i].z()), + std::size_t(offsets[i].y()), + std::size_t(offsets[i].x())}, src.size())); +} +/* [AtlasLandfillArray-usage] */ +} + { /* [atlasArrayPowerOfTwo] */ Containers::ArrayView input; diff --git a/doc/snippets/atlas-landfill.svg b/doc/snippets/atlas-landfill.svg new file mode 100644 index 0000000000..a304cb12c5 --- /dev/null +++ b/doc/snippets/atlas-landfill.svg @@ -0,0 +1,669 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Magnum/TextureTools/Atlas.cpp b/src/Magnum/TextureTools/Atlas.cpp index 90a1eb18b9..bba46a0d2d 100644 --- a/src/Magnum/TextureTools/Atlas.cpp +++ b/src/Magnum/TextureTools/Atlas.cpp @@ -28,14 +28,320 @@ #include #include #include +#include +#include +#include #include #include #include "Magnum/Math/Functions.h" +#include "Magnum/Math/FunctionsBatch.h" #include "Magnum/Math/Range.h" namespace Magnum { namespace TextureTools { +Debug& operator<<(Debug& debug, const AtlasLandfillFlag value) { + debug << "TextureTools::AtlasLandfillFlag" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case AtlasLandfillFlag::v: return debug << "::" #v; + _c(RotatePortrait) + _c(RotateLandscape) + _c(WidestFirst) + _c(NarrowestFirst) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedInt(value)) << Debug::nospace << ")"; +} + +Debug& operator<<(Debug& debug, const AtlasLandfillFlags value) { + return Containers::enumSetDebugOutput(debug, value, "TextureTools::AtlasLandfillFlags{}", { + AtlasLandfillFlag::RotatePortrait, + AtlasLandfillFlag::RotateLandscape, + AtlasLandfillFlag::WidestFirst, + AtlasLandfillFlag::NarrowestFirst, + }); +} + +namespace Implementation { + +struct AtlasLandfillState { + struct Slice { + Int direction = +1; /* +1 left-to-right, -1 right-to-left */ + /* If direction is left-to-right, it's offset from the left, otherwise + from the right */ + Int xOffset = 0; + }; + Containers::Array slices; + /* One entry for every size.x() */ + Containers::Array yOffsets; + /* X = MAX and z = 1 is for 2D unbounded, z = MAX is for 3D unbounded */ + Vector3i size; + AtlasLandfillFlags flags = AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst; +}; + +} + +namespace { + +bool atlasLandfillAddSortedFlipped(Implementation::AtlasLandfillState& state, const Int slice, const Containers::StridedArrayView1D> sortedFlippedSizes, const Containers::StridedArrayView1D offsets, const Containers::StridedArrayView1D zOffsets) { + /* Add a new slice if not there yet, extend the yOffsets array */ + if(UnsignedInt(slice) >= state.slices.size()) { + CORRADE_INTERNAL_ASSERT(UnsignedInt(slice) == state.slices.size()); + CORRADE_INTERNAL_ASSERT(state.yOffsets.size() == state.slices.size()*state.size.x()); + arrayAppend(state.slices, InPlaceInit); + /** @todo have an option to always start at the last tile so it doesn't + use a ton of memory when not filling incrementally and doesn't take + ages when incrementally filling a deep array */ + /** @todo Utility::fill() */ + for(UnsignedShort& i: arrayAppend(state.yOffsets, NoInit, state.size.x())) + i = 0; + } + + Implementation::AtlasLandfillState::Slice& sliceState = state.slices[slice]; + + /* View on the Y offsets in current slice and in current fill direction */ + Containers::StridedArrayView1D sliceYOffsets = state.yOffsets.sliceSize(slice*state.size.x(), state.size.x()); + if(sliceState.direction == -1) + sliceYOffsets = sliceYOffsets.flipped<0>(); + + std::size_t i; + for(i = 0; i != sortedFlippedSizes.size(); ++i) { + const Vector2i size = sortedFlippedSizes[i].first(); + + /* If the width cannnot fit into current offset, start a new row in + the opposite direction */ + if(sliceState.xOffset + size.x() > state.size.x()) { + sliceState.xOffset = 0; + sliceState.direction *= -1; + sliceYOffsets = sliceYOffsets.flipped<0>(); + } + + /* Find the lowest Y offset where the width can be placed. If the + height cannot fit in there, bail. */ + const Containers::StridedArrayView1D placementYOffsets = sliceYOffsets.sliceSize(sliceState.xOffset, size.x()); + const Int placementYOffset = Math::max(placementYOffsets); + /** @todo skip it until some smaller fits, and then continue with the + skipped rest to the next slice */ + if(placementYOffset + size.y() > state.size.y()) + break; + + /** @todo Utility::fill() */ + const UnsignedShort newYOffset = placementYOffset + size.y(); + for(UnsignedShort& yOffset: placementYOffsets) + yOffset = newYOffset; + + /* Save the position (X-flip it in case we're in reverse direction), + advance to the next X offset */ + offsets[sortedFlippedSizes[i].second()] = { + sliceState.direction > 0 ? sliceState.xOffset : + state.size.x() - sliceState.xOffset - size.x(), + placementYOffset + }; + sliceState.xOffset += size.x(); + } + + /* If the Z offset array is present, fill it with current slice index for + all items that fit */ + if(zOffsets) for(std::size_t j = 0; j != i; ++j) + zOffsets[sortedFlippedSizes[j].second()] = slice; + + /* If there are items that didn't fit, recurse to the next slice. This + should only happen if the Y size is bounded. */ + if(i < sortedFlippedSizes.size()) { + if(slice + 1 == state.size.z()) + return false; + return atlasLandfillAddSortedFlipped(state, slice + 1, sortedFlippedSizes.exceptPrefix(i), offsets, zOffsets); + } + + /* Everything fit, success */ + 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) { + CORRADE_ASSERT(offsets.size() == sizes.size(), + messagePrefix << "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(), {}); + /* 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(); + + /* Copy all input sizes to a mutable array, flip them if not portrait, + and remember their original order for sorting */ + Containers::Array> sortedFlippedSizes{NoInit, sizes.size()}; + for(std::size_t i = 0; i != sizes.size(); ++i) { + Vector2i size = sizes[i]; + if((state.flags & AtlasLandfillFlag::RotateLandscape && size.x() < size.y()) || + (state.flags & AtlasLandfillFlag::RotatePortrait && size.x() > size.y())) + { + size = size.flipped(); + rotations.set(i); + } + + CORRADE_ASSERT(size.product() && size <= 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, {}); + + sortedFlippedSizes[i] = {size, UnsignedInt(i)}; + } + + /* Sort to have the highest first. Assuming the items are square, + which is checked below in the loop. It's highly likely there are many + textures of the same size, thus use a stable sort to have output + consistent across platforms. */ + /** @todo stable_sort allocates, would be great if i could make it reuse + the memory allocated for output */ + if(state.flags & AtlasLandfillFlag::NarrowestFirst) + std::stable_sort(sortedFlippedSizes.begin(), sortedFlippedSizes.end(), [](const Containers::Pair& a, const Containers::Pair& b) { + return a.first().y() == b.first().y() ? + a.first().x() < b.first().x() : + a.first().y() > b.first().y(); + }); + else if(state.flags & AtlasLandfillFlag::WidestFirst) + std::stable_sort(sortedFlippedSizes.begin(), sortedFlippedSizes.end(), [](const Containers::Pair& a, const Containers::Pair& b) { + return a.first().y() == b.first().y() ? + a.first().x() > b.first().x() : + a.first().y() > b.first().y(); + }); + else + std::stable_sort(sortedFlippedSizes.begin(), sortedFlippedSizes.end(), [](const Containers::Pair& a, const Containers::Pair& b) { + return a.first().y() > b.first().y(); + }); + + return atlasLandfillAddSortedFlipped(state, 0, sortedFlippedSizes, offsets, zOffsets); +} + +} + +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)}; +} + +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) { + return add(Containers::stridedArrayView(sizes), offsets, flips); +} + +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) { + 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())}; +} + +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 AtlasLandfillArray::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) { + CORRADE_ASSERT(!(_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)), + "TextureTools::AtlasLandfillArray::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) { + return add(Containers::stridedArrayView(sizes), offsets); +} + std::vector atlas(const Vector2i& atlasSize, const std::vector& sizes, const Vector2i& padding) { if(sizes.empty()) return {}; diff --git a/src/Magnum/TextureTools/Atlas.h b/src/Magnum/TextureTools/Atlas.h index 224d85f722..67e54a39ff 100644 --- a/src/Magnum/TextureTools/Atlas.h +++ b/src/Magnum/TextureTools/Atlas.h @@ -26,17 +26,435 @@ */ /** @file - * @brief Function @ref Magnum::TextureTools::atlas(), @ref Magnum::TextureTools::atlasArrayPowerOfTwo() + * @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() */ +#include #include #include "Magnum/Magnum.h" #include "Magnum/Math/Vector2.h" #include "Magnum/TextureTools/visibility.h" +#ifdef MAGNUM_BUILD_DEPRECATED +#include +#endif + namespace Magnum { namespace TextureTools { +namespace Implementation { + struct AtlasLandfillState; +} + +/** +@brief Landfill texture atlas packer behavior flag +@m_since_latest + +@see @ref AtlasLandfillFlags, @ref AtlasLandfill::setFlags(), + @ref AtlasLandfill::addFlags(), @ref AtlasLandfill::clearFlags(), + @ref AtlasLandfillArray::setFlags(), @ref AtlasLandfillArray::addFlags(), + @ref AtlasLandfillArray::clearFlags() +*/ +enum class AtlasLandfillFlag { + /** + * Rotate all textures to a portrait orientation. Only one of + * @ref AtlasLandfillFlag::RotatePortrait and + * @relativeref{AtlasLandfillFlag,RotateLandscape} can be set. If neither + * is set, keeps the original orientation. + */ + RotatePortrait = 1 << 0, + + /** + * Rotate all textures to a landscape orientation. Only one of + * @ref AtlasLandfillFlag::RotatePortrait and + * @relativeref{AtlasLandfillFlag,RotateLandscape} can be set. If neither + * is set, keeps the original orientation. + */ + RotateLandscape = 1 << 1, + + /** + * Sort same-height textures widest first. Only one of + * @ref AtlasLandfillFlag::WidestFirst and + * @relativeref{AtlasLandfillFlag,NarrowestFirst} can be set. If neither is + * set, textures of the same height keep their original order. + */ + WidestFirst = 1 << 2, + + /** + * Sort same-height textures narrowest first. Only one of + * @ref AtlasLandfillFlag::WidestFirst and + * @relativeref{AtlasLandfillFlag,NarrowestFirst} can be set. If neither is + * set, textures of the same height keep their original order. + */ + NarrowestFirst = 1 << 3 +}; + +/** @debugoperatorenum{AtlasLandfillFlag} */ +MAGNUM_TEXTURETOOLS_EXPORT Debug& operator<<(Debug& output, AtlasLandfillFlag value); + +/** +@brief Landfill texture atlas packer behavior flags +@m_since_latest + +@see @ref Flags, @ref AtlasLandfill::setFlags(), @ref AtlasLandfill::addFlags(), + @ref AtlasLandfill::clearFlags(), @ref AtlasLandfillArray::setFlags(), + @ref AtlasLandfillArray::addFlags(), @ref AtlasLandfillArray::clearFlags() +*/ +typedef Containers::EnumSet AtlasLandfillFlags; + +CORRADE_ENUMSET_OPERATORS(AtlasLandfillFlags) + +/** @debugoperatorenum{AtlasLandfillFlags} */ +MAGNUM_TEXTURETOOLS_EXPORT Debug& operator<<(Debug& output, AtlasLandfillFlags value); + +/** +@brief Landfill texture atlas packer +@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. + +@htmlinclude atlas-landfill.svg + +* *The Trash Algorithm.* Naming credit goes to [\@lacyyy](https://github.com/lacyyy). + +@section TextureTools-AtlasLandfill-usage Example usage + +The following snippets shows packing a list of images into an atlas with the +width set to 1024 and height unbounded. The algorithm by default makes all +images the same orientation as that significantly improves the layout +efficiency while not making any difference for texture mapping. + +@snippet MagnumTextureTools.cpp AtlasLandfill-usage + +If rotations are undesirable, for example if the resulting atlas is used by a +linear rasterizer later, they can be disabled by clearing appropriate +@ref AtlasLandfillFlags. The process can then also use the +@ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) +overload without the rotations argument. + +@snippet MagnumTextureTools.cpp AtlasLandfill-usage-no-rotation + +@section TextureTools-AtlasLandfill-process Packing process + +On every @ref add(), the algorithm first makes all sizes the same orientation +depending on @ref AtlasLandfillFlag::RotatePortrait or +@relativeref{AtlasLandfillFlag,RotateLandscape} being set and sorts the sizes +highest first and then depending on @ref AtlasLandfillFlag::WidestFirst or +@relativeref{AtlasLandfillFlag,NarrowestFirst} being set. + +A per-pixel array of currently filled `heights`, initially all @cpp 0 @ce, and +a horizontal insertion `cursor`, initially @cpp 0 @ce, is maintained. An item +of given `size` gets placed at a `height` that's +@cpp max(heights[cursor], heights[cursor + size.x]) @ce, this range gets then +set to `height + size.y` and the cursor is updated to `cursor + size.x`. If +cursor reaches the edge that an item cannot fit there anymore, it's reset to +@cpp 0 @ce and the process continues again in the opposite direction. With the +assumption that the texture sizes are uniformly distributed, this results in a +fairly leveled out height. The process is aborted if the atlas height is +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. + +@section TextureTools-AtlasLandfill-incremental Incremental population + +It's possible to call @ref add() multiple times in order to incrementally fill +the atlas with new data as much as the atlas height (if bounded) allows. In an +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. +*/ +class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { + public: + /** + * @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. + */ + explicit AtlasLandfill(const Vector2i& size); + + /** @brief Copying is not allowed */ + AtlasLandfill(const AtlasLandfill&) = delete; + + /** @brief Move constructor */ + AtlasLandfill(AtlasLandfill&&) noexcept; + + ~AtlasLandfill(); + + /** @brief Copying is not allowed */ + AtlasLandfill& operator=(const AtlasLandfill&) = delete; + + /** @brief Move assignment */ + AtlasLandfill& operator=(AtlasLandfill&&) noexcept; + + /** + * @brief Atlas size specified in constructor + * + * @see @ref filledSize() + */ + Vector2i 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. + */ + Vector2i filledSize() const; + + /** + * @brief Behavior flags + * + * Default is @ref AtlasLandfillFlag::RotatePortrait and + * @relativeref{AtlasLandfillFlag,WidestFirst}. + */ + AtlasLandfillFlags flags() const; + + /** + * @brief Set behavior flags + * + * Can be called with different values before each particular + * @ref add(). + * @see @ref addFlags(), @ref clearFlags() + */ + AtlasLandfill& 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() + */ + AtlasLandfill& 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() + */ + AtlasLandfill& clearFlags(AtlasLandfillFlags flags) { + return setFlags(this->flags() & ~flags); + } + + /** + * @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 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. + * + * 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. + */ + 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); + + /** + * @brief Add textures to the atlas with rotations disabled + * + * 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); + + /** @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 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 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. + * + * 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. + */ + 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); + + /** + * @brief Add textures to the atlas with rotations disabled + * + * 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); + + /** @overload */ + bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets); + + private: + Containers::Pointer _state; +}; + /** @brief Pack textures into texture atlas @param atlasSize Size of resulting atlas @@ -80,6 +498,10 @@ atlasing in a single @f$ \mathcal{O}(n) @f$ operation. Memory complexity is 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. */ 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 new file mode 100644 index 0000000000..63b7315637 --- /dev/null +++ b/src/Magnum/TextureTools/Test/AtlasBenchmark.cpp @@ -0,0 +1,440 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Magnum/Image.h" +#include "Magnum/ImageView.h" +#include "Magnum/PixelFormat.h" +#include "Magnum/DebugTools/ColorMap.h" +#include "Magnum/Math/Color.h" +#include "Magnum/Math/PackingBatch.h" +#include "Magnum/Math/Range.h" +#include "Magnum/TextureTools/Atlas.h" +#include "Magnum/Trade/AbstractImageConverter.h" + +#include "configure.h" + +#ifdef __has_include +#if __has_include("AtlasTestFiles/stb_rect_pack.h") +#ifdef CORRADE_TARGET_GCC +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "AtlasTestFiles/stb_rect_pack.h" +#ifdef CORRADE_TARGET_GCC +#pragma GCC diagnostic pop +#endif + +inline void stbSort(stbrp_rect* rects, std::size_t count, std::size_t, int(*compare)(const void*, const void*)) { + std::sort(rects, rects + count, [compare](const stbrp_rect& a, const stbrp_rect& b) { + /* It returns -1 or 1, and -1 is if a dimension is higher, which is + descending, which is what should return true here */ + return compare(&a, &b) < 0; + }); +} + +#define STB_RECT_PACK_IMPLEMENTATION +/* Comment this out to test with qsort instead (considerably slower as the + comparator function call isn't inlined) */ +#define STBRP_SORT stbSort +#ifdef CORRADE_TARGET_GCC +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "AtlasTestFiles/stb_rect_pack.h" +#ifdef CORRADE_TARGET_GCC +#pragma GCC diagnostic pop +#endif +#endif +#endif + +namespace Magnum { namespace TextureTools { namespace Test { namespace { + +struct AtlasBenchmark: TestSuite::Tester { + explicit AtlasBenchmark(); + + /* A bit chaotic here -- the benchmarkBegin() / benchmarkEnd() rely on the + actual case filling _sizes and _filledArea before the + CORRADE_BENCHMARK() ends. Then, the actual verification of the output + (that there is exactly as many filled pixels as was in the input) is + done by a custom CompareAtlasPacking comparator that's implemented + below, and this comparator also produces details about the packing with + --verbose and saves a TGA visualization with --save-diagnostic */ + + void benchmarkBegin(); + std::uint64_t benchmarkEnd(); + + void landfill(); + void stbRectPack(); + + private: + Containers::ArrayView _sizes; + UnsignedInt _filledArea; +}; + +const struct { + const char* name; + const char* filename; + const char* image; + Vector2i size; + Containers::Optional flags; +} LandfillData[]{ + {"Oxygen.ttf, portrait, widest first", + "oxygen-glyphs.bin", + "oxygen-glyphs-landfill-portrait-widest-first.tga", + {512, 256}, {}}, + {"Oxygen.ttf, portrait, narrowest first", + "oxygen-glyphs.bin", + "oxygen-glyphs-landfill-portrait-narrowest-first.tga", + {512, 256}, + ~~AtlasLandfillFlag::NarrowestFirst}, + {"Oxygen.ttf, landscape, widest first", + "oxygen-glyphs.bin", + "oxygen-glyphs-landfill-landscape-widest-first.tga", + {512, 256}, + AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst}, + {"Oxygen.ttf, landscape, narrowest first", + "oxygen-glyphs.bin", + "oxygen-glyphs-landfill-landscape-narrowest-first.tga", + {512, 256}, + AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst}, + {"Noto Serif Tangut, portrait, widest first", + "noto-serif-tangut-glyphs.bin", + "noto-serif-tangut-glyphs-landfill-portrait-widest-first.tga", + {2048, 800}, + {}}, + {"Noto Serif Tangut, portrait, narrowest first", + "noto-serif-tangut-glyphs.bin", + "noto-serif-tangut-glyphs-landfill-portrait-narrowest-first.tga", + {2048, 800}, + ~~AtlasLandfillFlag::NarrowestFirst}, + {"Noto Serif Tangut, landscape, widest first", + "noto-serif-tangut-glyphs.bin", + "noto-serif-tangut-glyphs-landfill-landscape-widest-first.tga", + {2048, 800}, + AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst}, + {"Noto Serif Tangut, landscape, narrowest first", + "noto-serif-tangut-glyphs.bin", + "noto-serif-tangut-glyphs-landfill-landscape-narrowest-first.tga", + {2048, 800}, + AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst}, + {"FP 102344349, landscape, widest first", + "fp-102344349-textures.bin", + "fp-102344349-textures-landfill-portrait-widest-first.tga", + {2048, 2048}, AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst}, + {"FP 103997718_171030855, portrait, widest first", + "fp-103997718-171030855-textures.bin", + "fp-103997718-171030855-textures-landfill-portrait-widest-first.tga", + {8192, 8192}, {}}, +}; + +const struct { + const char* name; + const char* filename; + const char* image; + Vector2i size; + Int rotate; + bool allowOOM; +} StbRectPackData[]{ + {"Oxygen.ttf", + "oxygen-glyphs.bin", + "oxygen-glyph-stb.tga", + {512, 256}, 0, false}, + {"Oxygen.ttf, portrait", + "oxygen-glyphs.bin", + "oxygen-glyphs-stb-portrait.tga", + {512, 256}, 1, false}, + {"Oxygen.ttf, landscape", + "oxygen-glyphs.bin", + "oxygen-glyphs-stb-lanscape.tga", + {512, 256}, -1, false}, + {"Oxygen.ttf, allow OOM", + "oxygen-glyphs.bin", + "oxygen-glyph-stb.tga", + {512, 256}, 0, true}, + {"Noto Serif Tangut", + "noto-serif-tangut-glyphs.bin", + "noto-serif-tangut-glyphs-stb.tga", + {2048, 800}, 0, false}, + {"Noto Serif Tangut, portrait", + "noto-serif-tangut-glyphs.bin", + "noto-serif-tangut-glyphs-stb-portrait.tga", + {2048, 800}, 1, false}, + {"Noto Serif Tangut, landscape", + "noto-serif-tangut-glyphs.bin", + "noto-serif-tangut-glyphs-stb-lanscape.tga", + {2048, 800}, -1, false}, + {"Noto Serif Tangut, allow OOM", + "noto-serif-tangut-glyphs.bin", + "noto-serif-tangut-glyphs-stb.tga", + {2048, 800}, 0, true}, + {"FP 102344349", + "fp-102344349-textures.bin", + "fp-102344349-textures-stb.tga", + {2048, 2048}, 0, false}, + {"FP 103997718_171030855", + "fp-103997718-171030855-textures.bin", + "fp-103997718-171030855-textures-stb.tga", + {8192, 8192}, 0, false}, +}; + +AtlasBenchmark::AtlasBenchmark() { + addCustomInstancedBenchmarks({&AtlasBenchmark::landfill}, 1, + Containers::arraySize(LandfillData), + &AtlasBenchmark::benchmarkBegin, + &AtlasBenchmark::benchmarkEnd, + BenchmarkUnits::PercentageThousandths); + + addCustomInstancedBenchmarks({&AtlasBenchmark::stbRectPack}, 1, + Containers::arraySize(StbRectPackData), + &AtlasBenchmark::benchmarkBegin, + &AtlasBenchmark::benchmarkEnd, + BenchmarkUnits::PercentageThousandths); + + /* Run all benchmarks again but with time measurement instead of + efficiency */ + addInstancedBenchmarks({&AtlasBenchmark::landfill}, 10, + Containers::arraySize(LandfillData)); + + addInstancedBenchmarks({&AtlasBenchmark::stbRectPack}, 10, + Containers::arraySize(StbRectPackData)); +} + +class CompareAtlasPacking; + +}}}} + +namespace Corrade { namespace TestSuite { + +using namespace Magnum; + +template<> class Comparator { + public: + explicit Comparator(Containers::StringView filename, const Vector2i& filledSize): _filename{filename}, _image{PixelFormat::RGBA8Unorm, filledSize, Containers::Array{ValueInit, std::size_t(filledSize.product())*4}} {} + + ComparisonStatusFlags operator()(const Containers::Pair, Containers::BitArrayView>& offsetsRotations, Containers::ArrayView sizes) { + _count = sizes.size(); + Containers::StridedArrayView2D pixels = _image.pixels(); + + /* Generate a random set of colors. Have the same set every time, + so location of corresponding entries can be compared across + different algorithms. */ + std::mt19937 rd; + std::uniform_int_distribution colorDist{0, 255}; + + /* Fill pixels where the items are placed */ + for(std::size_t i = 0; i != sizes.size(); ++i) { + const Color4ub color[]{DebugTools::ColorMap::turbo()[colorDist(rd)]}; + const Containers::StridedArrayView2D src{color, {1, 1}}; + + const Vector2i size = !offsetsRotations.second().isEmpty() && offsetsRotations.second()[i] ? sizes[i].flipped() : sizes[i]; + const Containers::StridedArrayView2D dst = + pixels.sliceSize( + {std::size_t(offsetsRotations.first()[i].y()), + std::size_t(offsetsRotations.first()[i].x())}, + {std::size_t(size.y()), + std::size_t(size.x())}); + + Utility::copy(src.broadcasted<0>(dst.size()[0]) + .broadcasted<1>(dst.size()[1]), dst); + } + + /* Calculate expected area of the input sizes */ + _expectedTotal = 0; + for(const Vector2i& i: sizes) + _expectedTotal += i.product(); + + /* Calculate the actual filled area */ + _actualTotal = 0; + for(Containers::StridedArrayView1D row: pixels) + for(Color4ub pixel: row) + if(pixel != Color4ub{}) + ++_actualTotal; + + return (_actualTotal == _expectedTotal ? ComparisonStatusFlags{} : + ComparisonStatusFlag::Failed)|ComparisonStatusFlag::Diagnostic|ComparisonStatusFlag::Verbose; + } + + void printMessage(ComparisonStatusFlags flags, Debug& out, const char* actual, const char* expected) const { + if(flags & ComparisonStatusFlag::Failed) + out << "Packing" << actual << "from" << expected << "was lossy," << _actualTotal << "filled pixels but expected" << _expectedTotal; + else if(flags & ComparisonStatusFlag::Verbose) + out << "Packed" << _count << "images into" << Debug::packed << _image.size(); + else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + } + + void saveDiagnostic(CORRADE_UNUSED ComparisonStatusFlags flags, Debug& out, Containers::StringView path) { + CORRADE_INTERNAL_ASSERT(flags & ComparisonStatusFlag::Diagnostic); + + PluginManager::Manager imageConverterManager; + Containers::Pointer imageConverter = imageConverterManager.loadAndInstantiate("TgaImageConverter"); + + Containers::String filename = Utility::Path::join(path, _filename); + if(imageConverter->convertToFile(_image, filename)) + out << "->" << filename; + } + + private: + Containers::StringView _filename; + Image2D _image; + UnsignedInt _count, _actualTotal, _expectedTotal; +}; + +}} + +namespace Magnum { namespace TextureTools { namespace Test { namespace { + +class CompareAtlasPacking { + public: + explicit CompareAtlasPacking(Containers::StringView filename, const Vector2i& filledSize): _c{filename, filledSize} {} + + TestSuite::Comparator& comparator() { + return _c; + } + + private: + TestSuite::Comparator _c; +}; + +void AtlasBenchmark::benchmarkBegin() { + setBenchmarkName("efficiency"); + _filledArea = 0; +} + +std::uint64_t AtlasBenchmark::benchmarkEnd() { + /* If the test failed, exit early as continuing would cause a division by + zero. */ + if(!_filledArea) return {}; + + UnsignedInt total = 0; + for(const Vector2i& i: _sizes) + total += i.product(); + + return total*100000ull/_filledArea; +} + +void AtlasBenchmark::landfill() { + auto&& data = LandfillData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Optional> sizeData = Utility::Path::read(Utility::Path::join({TEXTURETOOLS_TEST_DIR, "AtlasTestFiles", data.filename})); + CORRADE_VERIFY(sizeData); + + auto sizes16 = Containers::arrayCast(*sizeData); + Containers::Array sizes{NoInit, sizes16.size()}; + Math::castInto( + Containers::arrayCast<2, const Short>(stridedArrayView(sizes16)), + Containers::arrayCast<2, Int>(stridedArrayView(sizes))); + _sizes = sizes; + + AtlasLandfill atlas{data.size}; + if(data.flags) + atlas.setFlags(*data.flags); + + Containers::Array offsets{NoInit, _sizes.size()}; + Containers::BitArray flips{NoInit, _sizes.size()}; + CORRADE_BENCHMARK(1) { + CORRADE_VERIFY(atlas.add(_sizes, offsets, flips)); + _filledArea = atlas.filledSize().product(); + } + + CORRADE_COMPARE_WITH( + Containers::pair(Containers::StridedArrayView1D{offsets}, Containers::BitArrayView{flips}), + _sizes, + (CompareAtlasPacking{data.image, atlas.filledSize()})); +} + +void AtlasBenchmark::stbRectPack() { + auto&& data = StbRectPackData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + #ifdef STB_RECT_PACK_VERSION + Containers::Optional> sizeData = Utility::Path::read(Utility::Path::join({TEXTURETOOLS_TEST_DIR, "AtlasTestFiles", data.filename})); + CORRADE_VERIFY(sizeData); + + auto sizes16 = Containers::arrayCast(*sizeData); + Containers::Array sizes{NoInit, sizes16.size()}; + Math::castInto( + Containers::arrayCast<2, const Short>(stridedArrayView(sizes16)), + Containers::arrayCast<2, Int>(stridedArrayView(sizes))); + _sizes = sizes; + + if(data.rotate) for(Vector2i& size: _sizes) { + if((data.rotate < 0 && size.x() < size.y()) || + (data.rotate > 0 && size.x() > size.y())) + size = size.flipped(); + } + + stbrp_context ctx; + Containers::Array nodes{NoInit, _sizes.size()}; + stbrp_init_target(&ctx, data.size.x(), data.size.y(), nodes.data(), nodes.size()); + stbrp_setup_allow_out_of_mem(&ctx, data.allowOOM); + + struct MyRect { + int:32; + Vector2i size; + Vector2i offset; + int:32; + }; + static_assert(sizeof(MyRect) == sizeof(stbrp_rect), "failed to fake a rect struct"); + + Containers::Array rects{NoInit, _sizes.size()}; + Utility::copy(_sizes, stridedArrayView(rects).slice(&MyRect::size)); + + Int height = 0; + CORRADE_BENCHMARK(1) { + CORRADE_VERIFY(stbrp_pack_rects(&ctx, reinterpret_cast(rects.data()), rects.size())); + for(const MyRect& i: rects) + height = Math::max(i.size.y() + i.offset.y(), height); + _filledArea = height*data.size.x(); + } + + Vector2i filledSize{data.size.x(), height}; + + CORRADE_COMPARE_WITH( + Containers::pair(Containers::StridedArrayView1D{rects}.slice(&MyRect::offset), Containers::BitArrayView{}), + _sizes, + (CompareAtlasPacking{data.image, filledSize})); + #else + CORRADE_SKIP("stb_rect_pack.h not found, place it next to the test to benchmark it"); + #endif +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::TextureTools::Test::AtlasBenchmark) diff --git a/src/Magnum/TextureTools/Test/AtlasTest.cpp b/src/Magnum/TextureTools/Test/AtlasTest.cpp index 5fc34e25b3..1ba27c0be5 100644 --- a/src/Magnum/TextureTools/Test/AtlasTest.cpp +++ b/src/Magnum/TextureTools/Test/AtlasTest.cpp @@ -26,10 +26,13 @@ #include #include #include +#include #include #include +#include /** @todo remove once Debug is stream-free */ #include #include +#include #include #include @@ -41,6 +44,29 @@ namespace Magnum { namespace TextureTools { namespace Test { namespace { struct AtlasTest: TestSuite::Tester { explicit AtlasTest(); + void debugLandfillFlag(); + void debugLandfillFlags(); + + void landfillFullFit(); + void landfill(); + void landfillIncremental(); + void landfillNoFit(); + void landfillCopy(); + void landfillMove(); + + void landfillArrayFullFit(); + void landfillArray(); + void landfillArrayIncremental(); + void landfillArrayNoFit(); + void landfillArrayCopy(); + void landfillArrayMove(); + + void landfillInvalidSize(); + void landfillSetFlagsInvalid(); + void landfillAddMissingRotations(); + void landfillAddInvalidViewSizes(); + void landfillAddTooLargeElement(); + void basic(); void padding(); void empty(); @@ -59,6 +85,305 @@ struct AtlasTest: TestSuite::Tester { #endif }; +const Vector2i LandfillSizes[]{ + {3, 6}, /* 0 */ + {2, 5}, /* 1 */ + {4, 2}, /* 2 */ + {3, 3}, /* 3 */ + {2, 3}, /* 4 */ + {3, 3}, /* 5 */ + {2, 2}, /* 6 */ + {2, 1}, /* 7 */ + {2, 2}, /* 8 */ + {2, 2}, /* 9 */ + {2, 1}, /* a */ + {1, 2}, /* b */ + {1, 1}, /* c */ +}; + +const struct { + const char* name; + AtlasLandfillFlags flags; + Vector2i size; + Vector2i 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, 10}, { + /* 99b + 99b77 + 8866 aac + 88662222 + 000 2222555 + 00011 555 + 00011 555 + 0001133344 + 0001133344 + 0001133344 */ + {{0, 0}, false}, /* 0 */ + {{3, 0}, false}, /* 1 */ + {{4, 5}, false}, /* 2 */ + {{5, 0}, false}, /* 3 */ + {{8, 0}, false}, /* 4 */ + {{8, 3}, false}, /* 5 */ + {{2, 6}, false}, /* 6 */ + {{3, 8}, false}, /* 7 */ + {{0, 6}, false}, /* 8 */ + {{0, 8}, false}, /* 9 */ + {{5, 7}, false}, /* a */ + {{2, 8}, false}, /* b */ + {{7, 7}, false}}}, /* c */ + /* No rotation with width sorting omitted, not interesting */ + {"portrait, no width sorting", AtlasLandfillFlag::RotatePortrait, {11, 12}, {11, 10}, { + /* 99a + 99ab + 88bc + 88766555 + 00076655544 + 00011 55544 + 0001122 44 + 0001122333 + 0001122333 + 0001122333 */ + {{0, 0}, false}, /* 0 */ + {{3, 0}, false}, /* 1 */ + {{5, 0}, true}, /* 2 */ + {{7, 0}, false}, /* 3 */ + {{9, 3}, false}, /* 4 */ + {{6, 4}, false}, /* 5 */ + {{4, 5}, false}, /* 6 */ + {{3, 5}, true}, /* 7 */ + {{1, 6}, false}, /* 8 */ + {{0, 8}, false}, /* 9 */ + {{2, 8}, true}, /* a */ + {{3, 7}, false}, /* b */ + {{4, 7}, false}}}, /* c */ + {"portrait, widest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 12}, {11, 10}, { + /* 7ab + 7abc + 9988 + 99886644 + 000 6644555 + 00011 44555 + 0001122 555 + 0001122333 + 0001122333 + 0001122333 */ + {{0, 0}, false}, /* 0 */ + {{3, 0}, false}, /* 1 */ + {{5, 0}, true}, /* 2 */ + {{7, 0}, false}, /* 3 */ + {{6, 4}, false}, /* 4 */ + {{8, 3}, false}, /* 5 */ + {{4, 5}, false}, /* 6 */ + {{0, 8}, true}, /* 7 */ + {{2, 6}, false}, /* 8 */ + {{0, 6}, false}, /* 9 */ + {{1, 8}, true}, /* a */ + {{2, 8}, false}, /* b */ + {{3, 8}, false}}}, /* c */ + {"portrait, widest first, unbounded height", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 0}, {11, 10}, { + /* Should have the same result as above + 7ab + 7abc + 9988 + 99886644 + 000 6644555 + 00011 44555 + 0001122 555 + 0001122333 + 0001122333 + 0001122333 */ + {{0, 0}, false}, /* 0 */ + {{3, 0}, false}, /* 1 */ + {{5, 0}, true}, /* 2 */ + {{7, 0}, false}, /* 3 */ + {{6, 4}, false}, /* 4 */ + {{8, 3}, false}, /* 5 */ + {{4, 5}, false}, /* 6 */ + {{0, 8}, true}, /* 7 */ + {{2, 6}, false}, /* 8 */ + {{0, 6}, false}, /* 9 */ + {{1, 8}, true}, /* a */ + {{2, 8}, false}, /* b */ + {{3, 8}, false}}}, /* c */ + {"portrait, narrowest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::NarrowestFirst, {11, 12}, {11, 10}, { + /* 8899 + 8899 + 66b c + 66ba7555 + 000a7555333 + 00011555333 + 0001122 333 + 000112244 + 000112244 + 000112244 */ + {{0, 0}, false}, /* 0 */ + {{3, 0}, false}, /* 1 */ + {{5, 0}, true}, /* 2 */ + {{8, 3}, false}, /* 3 */ + {{7, 0}, false}, /* 4 */ + {{5, 4}, false}, /* 5 */ + {{0, 6}, false}, /* 6 */ + {{4, 5}, true}, /* 7 */ + {{0, 8}, false}, /* 8 */ + {{2, 8}, false}, /* 9 */ + {{3, 5}, true}, /* a */ + {{2, 6}, false}, /* b */ + {{4, 7}, false}}}, /* c */ + {"landscape, no width sorting", AtlasLandfillFlag::RotateLandscape, {11, 12}, {11, 10}, { + /* 99 + 7799 + cbbaa6688 + 22224446688 + 2222444 555 + 11111555 + 11111555 + 000000333 + 000000333 + 000000333 */ + {{0, 0}, true}, /* 0 */ + {{3, 3}, true}, /* 1 */ + {{0, 5}, false}, /* 2 */ + {{6, 0}, false}, /* 3 */ + {{4, 5}, true}, /* 4 */ + {{8, 3}, false}, /* 5 */ + {{7, 6}, false}, /* 6 */ + {{7, 8}, false}, /* 7 */ + {{9, 6}, false}, /* 8 */ + {{9, 8}, false}, /* 9 */ + {{5, 7}, false}, /* a */ + {{3, 7}, true}, /* b */ + {{2, 7}, false}}}, /* c */ + {"landscape, widest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst, {11, 12}, {11, 10}, { + /* No change compared to "no width sorting" in this case + 99 + 7799 + cbbaa6688 + 22224446688 + 2222444 555 + 11111555 + 11111555 + 000000333 + 000000333 + 000000333 */ + {{0, 0}, true}, /* 0 */ + {{3, 3}, true}, /* 1 */ + {{0, 5}, false}, /* 2 */ + {{6, 0}, false}, /* 3 */ + {{4, 5}, true}, /* 4 */ + {{8, 3}, false}, /* 5 */ + {{7, 6}, false}, /* 6 */ + {{7, 8}, false}, /* 7 */ + {{9, 6}, false}, /* 8 */ + {{9, 8}, false}, /* 9 */ + {{5, 7}, false}, /* a */ + {{3, 7}, true}, /* b */ + {{2, 7}, false}}}, /* c */ + {"landscape, narrowest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst, {11, 12}, {11, 10}, { + /* 11111 + bb c11111 + aa772222 + 994442222 + 99444000000 + 8866000000 + 8866000000 + 333555 + 333555 + 333555 */ + {{5, 3}, true}, /* 0 */ + {{6, 8}, true}, /* 1 */ + {{5, 6}, false}, /* 2 */ + {{0, 0}, false}, /* 3 */ + {{2, 5}, true}, /* 4 */ + {{3, 0}, false}, /* 5 */ + {{3, 3}, false}, /* 6 */ + {{3, 7}, false}, /* 7 */ + {{1, 3}, false}, /* 8 */ + {{0, 5}, false}, /* 9 */ + {{1, 7}, false}, /* a */ + {{0, 8}, true}, /* b */ + {{5, 8}, false}}}, /* c */ +}; + +const Vector2i LandfillArraySizes[]{ + {3, 6}, /* 0 */ + {2, 5}, /* 1 */ + {4, 2}, /* 2 */ + {3, 3}, /* 3 */ + {3, 3}, /* 4 */ + {2, 2}, /* 5 */ + {2, 2}, /* 6 */ + {2, 1}, /* 7 */ + {2, 2}, /* 8 */ + {2, 2}, /* 9 */ +}; + +const struct { + const char* name; + AtlasLandfillFlags flags; + Vector3i size; + Vector3i filledSize; + Containers::Pair offsetsFlips[Containers::arraySize(LandfillArraySizes)]; +} LandfillArrayData[]{ + /* Various sorting aspects are tested in landfill() already, this just + checks the array-specific behaviors and the rotation-less overload */ + {"no rotation", {}, {11, 6, 3}, {11, 6, 2}, { + /* 000 + 00011552222 + 00011552222 + 00011333444 + 00011333444 668899 + 00011333444 66889977 */ + {{0, 0, 0}, false}, /* 0 */ + {{3, 0, 0}, false}, /* 1 */ + {{7, 3, 0}, false}, /* 2 */ + {{5, 0, 0}, false}, /* 3 */ + {{8, 0, 0}, false}, /* 4 */ + {{5, 3, 0}, false}, /* 5 */ + {{0, 0, 1}, false}, /* 6 */ + {{6, 0, 1}, false}, /* 7 */ + {{2, 0, 1}, false}, /* 8 */ + {{4, 0, 1}, false}}}, /* 9 */ + {"portrait, widest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 6, 3}, {11, 6, 2}, { + /* 000 55444 + 00011 55444 + 0001122 444 + 0001122333 + 0001122333 6688997 + 0001122333 6688997 */ + {{0, 0, 0}, false}, /* 0 */ + {{3, 0, 0}, false}, /* 1 */ + {{5, 0, 0}, true}, /* 2 */ + {{7, 0, 0}, false}, /* 3 */ + {{8, 3, 0}, false}, /* 4 */ + {{6, 4, 0}, false}, /* 5 */ + {{0, 0, 1}, false}, /* 6 */ + {{6, 0, 1}, true}, /* 7 */ + {{2, 0, 1}, false}, /* 8 */ + {{4, 0, 1}, false}}}, /* 9 */ + {"portrait, widest first, unbounded", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 6, 3}, {11, 6, 2}, { + /* Should have the same result as above + 000 55444 + 00011 55444 + 0001122 444 + 0001122333 + 0001122333 6688997 + 0001122333 6688997 */ + {{0, 0, 0}, false}, /* 0 */ + {{3, 0, 0}, false}, /* 1 */ + {{5, 0, 0}, true}, /* 2 */ + {{7, 0, 0}, false}, /* 3 */ + {{8, 3, 0}, false}, /* 4 */ + {{6, 4, 0}, false}, /* 5 */ + {{0, 0, 1}, false}, /* 6 */ + {{6, 0, 1}, true}, /* 7 */ + {{2, 0, 1}, false}, /* 8 */ + {{4, 0, 1}, false}}}, /* 9 */ +}; + /* Could make order[15] and then Containers::arraySize(), but then it won't work on MSVC2015 and cause overly complicated code elsewhere */ constexpr std::size_t ArrayPowerOfTwoOneLayerImageCount = 15; @@ -96,7 +421,36 @@ const struct { }; AtlasTest::AtlasTest() { - addTests({&AtlasTest::basic, + addTests({&AtlasTest::debugLandfillFlag, + &AtlasTest::debugLandfillFlags, + + &AtlasTest::landfillFullFit}); + + addInstancedTests({&AtlasTest::landfill}, + Containers::arraySize(LandfillData)); + + addTests({&AtlasTest::landfillIncremental, + &AtlasTest::landfillNoFit, + &AtlasTest::landfillCopy, + &AtlasTest::landfillMove, + + &AtlasTest::landfillArrayFullFit}); + + addInstancedTests({&AtlasTest::landfillArray}, + Containers::arraySize(LandfillArrayData)); + + addTests({&AtlasTest::landfillArrayIncremental, + &AtlasTest::landfillArrayNoFit, + &AtlasTest::landfillArrayCopy, + &AtlasTest::landfillArrayMove, + + &AtlasTest::landfillInvalidSize, + &AtlasTest::landfillSetFlagsInvalid, + &AtlasTest::landfillAddMissingRotations, + &AtlasTest::landfillAddInvalidViewSizes, + &AtlasTest::landfillAddTooLargeElement, + + &AtlasTest::basic, &AtlasTest::padding, &AtlasTest::empty, &AtlasTest::tooSmall, @@ -122,6 +476,518 @@ AtlasTest::AtlasTest() { #endif } +void AtlasTest::debugLandfillFlag() { + std::ostringstream out; + Debug{&out} << AtlasLandfillFlag::RotatePortrait << AtlasLandfillFlag(0xcafedead); + CORRADE_COMPARE(out.str(), "TextureTools::AtlasLandfillFlag::RotatePortrait TextureTools::AtlasLandfillFlag(0xcafedead)\n"); +} + +void AtlasTest::debugLandfillFlags() { + std::ostringstream out; + Debug{&out} << (AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst|AtlasLandfillFlag(0xdead0000)) << AtlasLandfillFlags{}; + CORRADE_COMPARE(out.str(), "TextureTools::AtlasLandfillFlag::RotateLandscape|TextureTools::AtlasLandfillFlag::NarrowestFirst|TextureTools::AtlasLandfillFlag(0xdead0000) TextureTools::AtlasLandfillFlags{}\n"); +} + +void AtlasTest::landfillFullFit() { + /* Trivial case to verify there are no off-by-one errors that would prevent + a tight fit */ + + AtlasLandfill atlas{{4, 6}}; + CORRADE_COMPARE(atlas.size(), (Vector2i{4, 6})); + CORRADE_COMPARE(atlas.filledSize(), (Vector2i{4, 0})); + CORRADE_COMPARE(atlas.flags(), AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst); + + Vector2i offsets[4]; + UnsignedByte rotationData[1]; + Containers::MutableBitArrayView rotations{rotationData, 0, 4}; + /* Testing the init list overload here as all others test the view */ + CORRADE_VERIFY(atlas.add({ + {2, 4}, /* 0 */ + {2, 3}, /* 1 */ + {2, 3}, /* 2 */ + {2, 2}, /* 3 */ + }, offsets, rotations)); + CORRADE_COMPARE(atlas.filledSize(), (Vector2i{4, 6})); + CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ + false, false, false, false + }).sliceBit(0), TestSuite::Compare::Container); + + /* 3322 + 3322 + 0022 + 0011 + 0011 + 0011 */ + CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView({ + {0, 0}, /* 0 */ + {2, 0}, /* 1 */ + {2, 3}, /* 2 */ + {0, 4}, /* 3 */ + }), TestSuite::Compare::Container); +} + +void AtlasTest::landfill() { + auto&& data = LandfillData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + AtlasLandfill atlas{data.size}; + /* For unbounded sizes it should return 0 again */ + CORRADE_COMPARE(atlas.size(), data.size); + + Vector2i offsets[Containers::arraySize(LandfillSizes)]; + /* In case rotations aren't enabled, this isn't zero-initialized by + add() */ + UnsignedByte rotationData[2]{}; + Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(LandfillSizes)}; + atlas.setFlags(data.flags); + + /* Test the rotations-less overload if no rotations are enabled */ + if(!(data.flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape))) + CORRADE_VERIFY(atlas.add(LandfillSizes, offsets)); + else + CORRADE_VERIFY(atlas.add(LandfillSizes, offsets, rotations)); + + CORRADE_COMPARE(atlas.filledSize(), data.filledSize); + CORRADE_COMPARE_AS(rotations, + Containers::stridedArrayView(data.offsetsFlips) + .slice(&Containers::Pair::second) + .sliceBit(0), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(offsets), + Containers::stridedArrayView(data.offsetsFlips) + .slice(&Containers::Pair::first), + TestSuite::Compare::Container); +} + +void AtlasTest::landfillIncremental() { + /* Same as landfill(portrait, widest first) (which is the default flags) + but with the data split into three parts (0 to 4, 5 to 8, 9 to c), and + shuffled to verify the sort works as it should */ + + Vector2i sizeData[]{ + {4, 2}, /* 0, rotated */ + {3, 6}, /* 1 */ + {3, 3}, /* 2 */ + {5, 2}, /* 3, rotated */ + {3, 3}, /* 4 */ + {2, 2}, /* 5 */ + {2, 2}, /* 6 */ + {2, 2}, /* 7 */ + {3, 2}, /* 8, rotated */ + {1, 1}, /* 9 */ + {1, 2}, /* a */ + {2, 1}, /* b, rotated */ + {1, 2}, /* c */ + }; + auto sizes = Containers::arrayView(sizeData); + + Vector2i offsetData[Containers::arraySize(sizeData)]; + auto offsets = Containers::arrayView(offsetData); + UnsignedByte rotationData[2]; + Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(sizeData)}; + + AtlasLandfill atlas{{11, 10}}; + CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 0})); + + CORRADE_VERIFY(atlas.add( + sizes.prefix(5), + offsets.prefix(5), + rotations.prefix(5))); + CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 6})); + + CORRADE_VERIFY(atlas.add( + sizes.slice(5, 9), + offsets.slice(5, 9), + rotations.slice(5, 9))); + CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 8})); + + CORRADE_VERIFY(atlas.add( + sizes.exceptPrefix(9), + offsets.exceptPrefix(9), + rotations.exceptPrefix(9))); + CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 10})); + + CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ + true, false, false, true, false, false, false, false, true, false, + false, true, false + }).sliceBit(0), TestSuite::Compare::Container); + + /* abc + abc9 + 7766 + 77665588 + 111 5588444 + 11133 88444 + 1113300 444 + 1113300222 + 1113300222 + 1113300222 */ + CORRADE_COMPARE_AS(offsets, Containers::arrayView({ + {5, 0}, /* 0 */ + {0, 0}, /* 1 */ + {7, 0}, /* 2 */ + {3, 0}, /* 3 */ + {8, 3}, /* 4 */ + {4, 5}, /* 5 */ + {2, 6}, /* 6 */ + {0, 6}, /* 7 */ + {6, 4}, /* 8 */ + {3, 8}, /* 9 */ + {0, 8}, /* a */ + {1, 8}, /* b */ + {2, 8}, /* c */ + }), TestSuite::Compare::Container); +} + +void AtlasTest::landfillNoFit() { + /* Same as landfill(portrait, widest first) (which is the default flags) + which fits into {11, 10} but limiting height to 9 */ + + AtlasLandfill atlas{{11, 9}}; + + Vector2i offsets[Containers::arraySize(LandfillSizes)]; + UnsignedByte rotationData[2]; + Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(LandfillSizes)}; + CORRADE_VERIFY(!atlas.add(LandfillSizes, offsets, rotations)); +} + +void AtlasTest::landfillCopy() { + CORRADE_VERIFY(!std::is_copy_constructible{}); + CORRADE_VERIFY(!std::is_copy_assignable{}); +} + +void AtlasTest::landfillMove() { + AtlasLandfill a{{16, 24}}; + + Vector2i offsets[2]; + UnsignedByte rotations[1]; + CORRADE_VERIFY(a.add({{15, 17}, {2, 3}}, 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})); + + AtlasLandfill c{{16, 12}}; + c = Utility::move(b); + CORRADE_COMPARE(c.size(), (Vector2i{16, 24})); + CORRADE_COMPARE(c.filledSize(), (Vector2i{16, 20})); + + CORRADE_VERIFY(std::is_nothrow_move_constructible::value); + CORRADE_VERIFY(std::is_nothrow_move_assignable::value); +} + +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}}; + CORRADE_COMPARE(atlas.size(), (Vector3i{4, 5, 2})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 5, 0})); + CORRADE_COMPARE(atlas.flags(), AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst); + + Vector3i offsets[6]; + UnsignedByte rotationData[1]; + Containers::MutableBitArrayView rotations{rotationData, 0, 6}; + /* Testing the init list overload as all others test the view */ + CORRADE_VERIFY(atlas.add({ + {3, 5}, /* 0 */ + {1, 5}, /* 1 */ + {3, 3}, /* 2 */ + {1, 3}, /* 3 */ + {2, 2}, /* 4 */ + {2, 2}, /* 5 */ + }, offsets, rotations)); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 5, 2})); + CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ + false, false, false, false, false, false + }).sliceBit(0), TestSuite::Compare::Container); + + /* 0001 5544 + 0001 5544 + 0001 2223 + 0001 2223 + 0001 2223 */ + CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView({ + {0, 0, 0}, /* 0 */ + {3, 0, 0}, /* 1 */ + {0, 0, 1}, /* 2 */ + {3, 0, 1}, /* 3 */ + {2, 3, 1}, /* 4 */ + {0, 3, 1}, /* 5 */ + }), TestSuite::Compare::Container); +} + +void AtlasTest::landfillArray() { + auto&& data = LandfillArrayData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + AtlasLandfillArray atlas{data.size}; + /* For unbounded sizes it should return 0 again */ + CORRADE_COMPARE(atlas.size(), data.size); + + Vector3i offsets[Containers::arraySize(LandfillArraySizes)]; + /* In case rotations aren't enabled, this isn't zero-initialized by + add() */ + UnsignedByte rotationData[2]{}; + Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(LandfillArraySizes)}; + atlas.setFlags(data.flags); + + /* Test the rotations-less overload if no rotations are enabled */ + if(!(data.flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape))) + CORRADE_VERIFY(atlas.add(LandfillArraySizes, offsets)); + else + CORRADE_VERIFY(atlas.add(LandfillArraySizes, offsets, rotations)); + + CORRADE_COMPARE(atlas.filledSize(), data.filledSize); + CORRADE_COMPARE_AS(rotations, + Containers::stridedArrayView(data.offsetsFlips) + .slice(&Containers::Pair::second) + .sliceBit(0), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(offsets), + Containers::stridedArrayView(data.offsetsFlips) + .slice(&Containers::Pair::first), + TestSuite::Compare::Container); +} + +void AtlasTest::landfillArrayIncremental() { + /* 000 55444 + 00011 55444 + 0001122 444 + 0001122333 + 0001122333 6688997 + 0001122333 6688997 */ + + + Vector2i sizeData[]{ + {4, 2}, /* 0, rotated */ + {3, 6}, /* 1 */ + {3, 3}, /* 2 */ + {5, 2}, /* 3, rotated */ + {2, 2}, /* 4 */ + {2, 2}, /* 5 */ + {3, 3}, /* 6 */ + {2, 2}, /* 7 */ + {2, 1}, /* 8, rotated */ + {2, 2}, /* 9 */ + }; + auto sizes = Containers::arrayView(sizeData); + + Vector3i offsetData[Containers::arraySize(sizeData)]; + auto offsets = Containers::arrayView(offsetData); + UnsignedByte rotationData[2]; + Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(sizeData)}; + + AtlasLandfillArray atlas{{11, 6, 2}}; + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 0})); + + CORRADE_VERIFY(atlas.add( + sizes.prefix(4), + offsets.prefix(4), + rotations.prefix(4))); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 1})); + + CORRADE_VERIFY(atlas.add( + sizes.slice(4, 7), + offsets.slice(4, 7), + rotations.slice(4, 7))); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 2})); + + CORRADE_VERIFY(atlas.add( + sizes.exceptPrefix(7), + offsets.exceptPrefix(7), + rotations.exceptPrefix(7))); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 2})); + + CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ + true, false, false, true, false, false, false, false, true, false + }).sliceBit(0), TestSuite::Compare::Container); + + /* 111 44666 + 11133 44666 + 1113300 666 + 1113300222 + 1113300222 5577998 + 1113300222 5577998 */ + CORRADE_COMPARE_AS(offsets, Containers::arrayView({ + {5, 0, 0}, /* 0 */ + {0, 0, 0}, /* 1 */ + {7, 0, 0}, /* 2 */ + {3, 0, 0}, /* 3 */ + {6, 4, 0}, /* 4 */ + {0, 0, 1}, /* 5 */ + {8, 3, 0}, /* 6 */ + {2, 0, 1}, /* 7 */ + {6, 0, 1}, /* 8 */ + {4, 0, 1}, /* 9 */ + }), TestSuite::Compare::Container); +} + +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}}; + + Vector3i offsets[Containers::arraySize(LandfillArraySizes)]; + UnsignedByte rotationData[2]; + Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(LandfillArraySizes)}; + 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}}; + + std::ostringstream out; + Error redirectError{&out}; + AtlasLandfill{{0, 16}}; + AtlasLandfill{{65537, 16}}; + AtlasLandfillArray{{0, 16, 16}}; + AtlasLandfillArray{{16, 0, 16}}; + AtlasLandfillArray{{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", + TestSuite::Compare::String); +} + +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", + 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); + Vector2i sizes[2]; + Vector2i offsets[2]; + Vector3i offsets3[2]; + + std::ostringstream out; + Error redirectError{&out}; + atlasPortrait.add(sizes, offsets); + arrayPortrait.add(sizes, offsets3); + /* "Testing" the rotation-less init list variants too */ + atlasLandscape.add({{}, {}}, offsets); + arrayLandscape.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::RotateLandscape set, expected a rotations view\n" + "TextureTools::AtlasLandfillArray::add(): TextureTools::AtlasLandfillFlag::RotateLandscape set, expected a rotations view\n"); +} + +void AtlasTest::landfillAddInvalidViewSizes() { + CORRADE_SKIP_IF_NO_ASSERT(); + + AtlasLandfill atlas{{16, 23}}; + Vector2i sizes[2]; + Vector2i offsets[2]; + Vector2i offsetsInvalid[3]; + UnsignedByte rotationsData[1]; + Containers::MutableBitArrayView rotations{rotationsData, 0, 2}; + Containers::MutableBitArrayView rotationsInvalid{rotationsData, 0, 3}; + + std::ostringstream out; + Error redirectError{&out}; + atlas.add(sizes, offsetsInvalid, rotations); + atlas.add(sizes, offsets, rotationsInvalid); + CORRADE_COMPARE(out.str(), + "TextureTools::AtlasLandfill::add(): expected sizes and offsets views to have the same size, got 2 and 3\n" + "TextureTools::AtlasLandfill::add(): expected sizes and rotations views to have the same size, got 2 and 3\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); + Vector2i offsets[2]; + Vector3i offsets3[2]; + UnsignedByte rotationsData[1]; + Containers::MutableBitArrayView rotations{rotationsData, 0, 2}; + + 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); + /* 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); + 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 {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 {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", + TestSuite::Compare::String); +} + void AtlasTest::basic() { std::vector atlas = TextureTools::atlas({64, 64}, { {12, 18}, diff --git a/src/Magnum/TextureTools/Test/AtlasTestFiles/.gitignore b/src/Magnum/TextureTools/Test/AtlasTestFiles/.gitignore new file mode 100644 index 0000000000..4f6a8a93f1 --- /dev/null +++ b/src/Magnum/TextureTools/Test/AtlasTestFiles/.gitignore @@ -0,0 +1 @@ +stb_rect_pack.h diff --git a/src/Magnum/TextureTools/Test/AtlasTestFiles/extract-font-glyph-sizes.py b/src/Magnum/TextureTools/Test/AtlasTestFiles/extract-font-glyph-sizes.py new file mode 100755 index 0000000000..6540d2a259 --- /dev/null +++ b/src/Magnum/TextureTools/Test/AtlasTestFiles/extract-font-glyph-sizes.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021, 2022, 2023 Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +# Loads a font and saves sizes of all its glyphs to a binary file that's then +# used by AtlasBenchmark.cpp + +import argparse +import array + +from magnum import math, text +from magnum import * + +parser = argparse.ArgumentParser() +parser.add_argument('input') +parser.add_argument('output') +parser.add_argument('--size', default=16) +args = parser.parse_args() + +font = text.FontManager().load_and_instantiate('FreeTypeFont') +font.open_file(args.input, args.size) + +sizes = [] +for i in range(font.glyph_count): + size = Vector2i(math.ceil(font.glyph_size(i))) + if not size.product(): + continue + sizes += list(size) + +print("Writing {} glyph sizes, {} non-empty".format(font.glyph_count, len(sizes)//2)) + +with open(args.output, 'wb') as output: + array.array('h', sizes).tofile(output) diff --git a/src/Magnum/TextureTools/Test/AtlasTestFiles/extract-texture-sizes.py b/src/Magnum/TextureTools/Test/AtlasTestFiles/extract-texture-sizes.py new file mode 100755 index 0000000000..208bc36e66 --- /dev/null +++ b/src/Magnum/TextureTools/Test/AtlasTestFiles/extract-texture-sizes.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021, 2022, 2023 Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +# Loads a font and saves sizes of all its glyphs to a binary file that's then +# used by AtlasBenchmark.cpp + +import argparse +import array + +from magnum import math, trade +from magnum import * + +parser = argparse.ArgumentParser() +parser.add_argument('input') +parser.add_argument('output') +parser.add_argument('--max', type=int, default=1024) +parser.add_argument('--ratio', type=int, default=1) +args = parser.parse_args() + +importer = trade.ImporterManager().load_and_instantiate('AnySceneImporter') +importer.open_file(args.input) + +print("Importing {} images".format(importer.image2d_count)) + +sizes = [] +total = 0 +max = Vector2i() +for i in range(importer.image2d_count): + image = importer.image2d(i) + size = image.size/args.ratio; + if (size > Vector2i(args.max)).any(): + continue + + sizes += list(size) + total += size.product() + max = math.max(size, max) + +print("Remains {0} images not larger than {1}x{1}".format(len(sizes)//2, args.max)) + +print("Total area: {} ({:.1f}**2), max: {}x{}".format(total, math.sqrt(total), max.x, max.y)) + +with open(args.output, 'wb') as output: + array.array('h', sizes).tofile(output) diff --git a/src/Magnum/TextureTools/Test/AtlasTestFiles/fp-102344349-textures.bin b/src/Magnum/TextureTools/Test/AtlasTestFiles/fp-102344349-textures.bin new file mode 100644 index 0000000000..23d651d76f Binary files /dev/null and b/src/Magnum/TextureTools/Test/AtlasTestFiles/fp-102344349-textures.bin differ diff --git a/src/Magnum/TextureTools/Test/AtlasTestFiles/fp-103997718-171030855-textures.bin b/src/Magnum/TextureTools/Test/AtlasTestFiles/fp-103997718-171030855-textures.bin new file mode 100644 index 0000000000..7272585a0e Binary files /dev/null and b/src/Magnum/TextureTools/Test/AtlasTestFiles/fp-103997718-171030855-textures.bin differ diff --git a/src/Magnum/TextureTools/Test/AtlasTestFiles/noto-serif-tangut-glyphs.bin b/src/Magnum/TextureTools/Test/AtlasTestFiles/noto-serif-tangut-glyphs.bin new file mode 100644 index 0000000000..6d0e7aec46 Binary files /dev/null and b/src/Magnum/TextureTools/Test/AtlasTestFiles/noto-serif-tangut-glyphs.bin differ diff --git a/src/Magnum/TextureTools/Test/AtlasTestFiles/oxygen-glyphs.bin b/src/Magnum/TextureTools/Test/AtlasTestFiles/oxygen-glyphs.bin new file mode 100644 index 0000000000..9985c22231 Binary files /dev/null and b/src/Magnum/TextureTools/Test/AtlasTestFiles/oxygen-glyphs.bin differ diff --git a/src/Magnum/TextureTools/Test/CMakeLists.txt b/src/Magnum/TextureTools/Test/CMakeLists.txt index 6823692e3a..eb6cafd4e2 100644 --- a/src/Magnum/TextureTools/Test/CMakeLists.txt +++ b/src/Magnum/TextureTools/Test/CMakeLists.txt @@ -27,14 +27,6 @@ # property that would have to be set on each target separately. set(CMAKE_FOLDER "Magnum/TextureTools/Test") -corrade_add_test(TextureToolsAtlasTest AtlasTest.cpp LIBRARIES MagnumTextureToolsTestLib) - -if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID) - set(TEXTURETOOLS_TEST_DIR .) -else() - set(TEXTURETOOLS_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}) -endif() - if(MAGNUM_BUILD_GL_TESTS) # Otherwise CMake complains that Corrade::PluginManager is not found, wtf find_package(Corrade REQUIRED PluginManager) @@ -47,12 +39,57 @@ if(MAGNUM_BUILD_GL_TESTS) set(TGAIMPORTER_PLUGIN_FILENAME $) endif() endif() +endif() - # First replace ${} variables, then $<> generator expressions - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake - ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) - file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/configure.h - INPUT ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) +if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID) + set(TEXTURETOOLS_TEST_DIR .) +else() + set(TEXTURETOOLS_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}) +endif() + +# First replace ${} variables, then $<> generator expressions +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/configure.h + INPUT ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) + +corrade_add_test(TextureToolsAtlasTest AtlasTest.cpp LIBRARIES MagnumTextureToolsTestLib) +corrade_add_test(TextureToolsAtlasBenchmark AtlasBenchmark.cpp + LIBRARIES + MagnumDebugTools + MagnumTextureTools + MagnumTrade + FILES + # ./extract-texture-sizes.py ~/Data/fp-scenes/scenes/original/102344349.glb fp-102344349-textures.bin --ratio 9 + AtlasTestFiles/fp-102344349-textures.bin + # ./extract-texture-sizes.py ~/Data/fp-scenes/scenes/original/103997718_171030855.glb fp-103997718-171030855-textures.bin --ratio 8 + AtlasTestFiles/fp-103997718-171030855-textures.bin + # ./extract-font-glyph-sizes.py /usr/share/fonts/noto/NotoSerifTangut-Regular.ttf noto-serif-tangut-glyphs.bin + AtlasTestFiles/noto-serif-tangut-glyphs.bin + # ./extract-font-glyph-sizes.py ~/Code/magnum-plugins/src/MagnumPlugins/FreeTypeFont/Test/Oxygen.ttf oxygen-glyphs.bin + AtlasTestFiles/oxygen-glyphs.bin) +target_include_directories(TextureToolsAtlasBenchmark PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) +if(CORRADE_TARGET_EMSCRIPTEN) + if(CMAKE_VERSION VERSION_LESS 3.13) + message(FATAL_ERROR "CMake 3.13+ is required in order to specify Emscripten linker options") + endif() + # It allocates an 8K image in order to verify no overlaps + target_link_options(TextureToolsAtlasBenchmark PRIVATE "SHELL:-s ALLOW_MEMORY_GROWTH=1") +endif() +if(MAGNUM_BUILD_PLUGINS_STATIC) + if(MAGNUM_WITH_TGAIMAGECONVERTER) + target_link_libraries(TextureToolsAtlasBenchmark PRIVATE TgaImageConverter) + endif() +else() + # So the plugins get properly built when building the test + if(MAGNUM_WITH_TGAIMAGECONVERTER) + add_dependencies(TextureToolsAtlasBenchmark TgaImageConverter) + endif() +endif() + +if(MAGNUM_BUILD_GL_TESTS) + # Otherwise CMake complains that Corrade::PluginManager is not found, wtf + find_package(Corrade REQUIRED PluginManager) set(TextureToolsDistanceFieldGLTest_SRCS DistanceFieldGLTest.cpp) if(CORRADE_TARGET_IOS)