Browse Source

TextureTools: don't allocate the output in atlasArrayPowerOfTwo().

I mean, yes, it was already SIGNIFICANTLY better than the atlas() that
took a vector and returned one as well, but still. One of the usual use
cases is that there's an array of structs containing both sizes offsets
and the offsets get written back.

The original versions are deprecated as I really don't see any
convenient use case for these, especially given they return a pair and
not just an array.
pull/168/head
Vladimír Vondruš 3 years ago
parent
commit
f9402f2242
  1. 26
      src/Magnum/TextureTools/Atlas.cpp
  2. 46
      src/Magnum/TextureTools/Atlas.h
  3. 88
      src/Magnum/TextureTools/Test/AtlasTest.cpp

26
src/Magnum/TextureTools/Atlas.cpp

@ -28,8 +28,8 @@
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>
#include <Corrade/Containers/Array.h> #include <Corrade/Containers/Array.h>
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Containers/Pair.h> #include <Corrade/Containers/Pair.h>
#include <Corrade/Containers/StridedArrayView.h>
#include "Magnum/Math/Functions.h" #include "Magnum/Math/Functions.h"
#include "Magnum/Math/Range.h" #include "Magnum/Math/Range.h"
@ -65,15 +65,15 @@ std::vector<Range2Di> atlas(const Vector2i& atlasSize, const std::vector<Vector2
return atlas; return atlas;
} }
Containers::Pair<Int, Containers::Array<Vector3i>> atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::StridedArrayView1D<const Vector2i>& sizes) { Int atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector3i>& offsets) {
CORRADE_ASSERT(offsets.size() == sizes.size(),
"TextureTools::atlasArrayPowerOfTwo(): expected sizes and offsets views to have the same size, got" << sizes.size() << "and" << offsets.size(), {});
CORRADE_ASSERT(layerSize.product() && layerSize.x() == layerSize.y() && (layerSize & (layerSize - Vector2i{1})).isZero(), CORRADE_ASSERT(layerSize.product() && layerSize.x() == layerSize.y() && (layerSize & (layerSize - Vector2i{1})).isZero(),
"TextureTools::atlasArrayPowerOfTwo(): expected layer size to be a non-zero power-of-two square, got" << Debug::packed << layerSize, {}); "TextureTools::atlasArrayPowerOfTwo(): expected layer size to be a non-zero power-of-two square, got" << Debug::packed << layerSize, {});
if(sizes.isEmpty()) if(sizes.isEmpty())
return {}; return {};
Containers::Array<Vector3i> output{NoInit, sizes.size()};
/* Copy the input to a sorted array, together with a mapping to the /* Copy the input to a sorted array, together with a mapping to the
original order stored in Z. Can't really reuse the output allocation original order stored in Z. Can't really reuse the output allocation
as it would be overwritten in random order. */ as it would be overwritten in random order. */
@ -131,16 +131,30 @@ Containers::Pair<Int, Containers::Array<Vector3i>> atlasArrayPowerOfTwo(const Ve
} }
/* Save to the output in the original order */ /* Save to the output in the original order */
output[size.second()] = {coordinates, layer}; offsets[size.second()] = {coordinates, layer};
previousSize = size.first(); previousSize = size.first();
--free; --free;
} }
return {layer + 1, Utility::move(output)}; return layer + 1;
}
Int atlasArrayPowerOfTwo(const Vector2i& layerSize, const std::initializer_list<Vector2i> sizes, const Containers::StridedArrayView1D<Vector3i>& offsets) {
return atlasArrayPowerOfTwo(layerSize, Containers::stridedArrayView(sizes), offsets);
}
#ifdef MAGNUM_BUILD_DEPRECATED
Containers::Pair<Int, Containers::Array<Vector3i>> atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::StridedArrayView1D<const Vector2i>& sizes) {
Containers::Array<Vector3i> offsets{NoInit, sizes.size()};
Int layers = atlasArrayPowerOfTwo(layerSize, sizes, offsets);
return {layers, Utility::move(offsets)};
} }
Containers::Pair<Int, Containers::Array<Vector3i>> atlasArrayPowerOfTwo(const Vector2i& layerSize, const std::initializer_list<Vector2i> sizes) { Containers::Pair<Int, Containers::Array<Vector3i>> atlasArrayPowerOfTwo(const Vector2i& layerSize, const std::initializer_list<Vector2i> sizes) {
CORRADE_IGNORE_DEPRECATED_PUSH
return atlasArrayPowerOfTwo(layerSize, Containers::stridedArrayView(sizes)); return atlasArrayPowerOfTwo(layerSize, Containers::stridedArrayView(sizes));
CORRADE_IGNORE_DEPRECATED_POP
} }
#endif
}} }}

46
src/Magnum/TextureTools/Atlas.h

@ -54,32 +54,48 @@ std::vector<Range2Di> MAGNUM_TEXTURETOOLS_EXPORT atlas(const Vector2i& atlasSize
/** /**
@brief Pack square power-of-two textures into a texture atlas array @brief Pack square power-of-two textures into a texture atlas array
@param layerSize Size of the texture layer @param[in] layerSize Size of a single layer in the texture atlas
@param sizes Sizes of all textures in the atlas @param[in] sizes Sizes of all textures in the atlas
@return Total layer count and offsets of all textures in the atlas, with the Z @param[out] offsets Resulting offsets in the atlas
coordinate being the layer index @return Total layer count
@m_since_latest @m_since_latest
The @p layerSize is expected to be non-zero, square and power-of-two. All items The @p sizes and @p offsets views are expected to have the same size. The
in @p sizes are expected to be non-zero, square, power-of-two and not larger @p layerSize is expected to be non-zero, square and power-of-two. All items in
than @p layerSize. With such constraints the packing is optimal with no wasted @p sizes are expected to be non-zero, square, power-of-two and not larger than
space in all but the last layer. Setting @p layerSize to the size of the @p layerSize. With such constraints the packing is optimal with no wasted space
largest texture in the set will lead to the least wasted space in the last in all but the last layer. Setting @p layerSize to the size of the largest
layer. texture in the set will lead to the least wasted space in the last layer.
The algorithm first sorts the textures by size using @ref std::stable_sort(), The algorithm first sorts the textures by size using @ref std::stable_sort(),
which is usually @f$ \mathcal{O}(n \log{} n) @f$, and then performs the actual which is usually @f$ \mathcal{O}(n \log{} n) @f$, and then performs the actual
atlasing in a single @f$ \mathcal{O}(n) @f$ operation. Due to the sort atlasing in a single @f$ \mathcal{O}(n) @f$ operation. Memory complexity is
involved, a temporary allocation holds the sorted array and additionally @f$ \mathcal{0}(n) @f$ with @f$ n @f$ being a sorted copy of the input size
@ref std::stable_sort() performs its own allocation. array, additionally @ref std::stable_sort() performs its own allocation.
*/ */
Containers::Pair<Int, Containers::Array<Vector3i>> MAGNUM_TEXTURETOOLS_EXPORT atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::StridedArrayView1D<const Vector2i>& sizes); MAGNUM_TEXTURETOOLS_EXPORT Int atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector3i>& offsets);
/** /**
* @overload * @overload
* @m_since_latest * @m_since_latest
*/ */
Containers::Pair<Int, Containers::Array<Vector3i>> MAGNUM_TEXTURETOOLS_EXPORT atlasArrayPowerOfTwo(const Vector2i& layerSize, std::initializer_list<Vector2i> sizes); MAGNUM_TEXTURETOOLS_EXPORT Int atlasArrayPowerOfTwo(const Vector2i& layerSize, std::initializer_list<Vector2i> sizes, const Containers::StridedArrayView1D<Vector3i>& offsets);
#ifdef MAGNUM_BUILD_DEPRECATED
/**
@brief @copybrief atlasArrayPowerOfTwo(const Vector2i&, const Containers::StridedArrayView1D<const Vector2i>&, const Containers::StridedArrayView1D<Vector3i>&)
@m_deprecated_since_latest Use @ref atlasArrayPowerOfTwo(const Vector2i&, const Containers::StridedArrayView1D<const Vector2i>&, const Containers::StridedArrayView1D<Vector3i>&)
instead.
*/
MAGNUM_TEXTURETOOLS_EXPORT CORRADE_DEPRECATED("use the overload taking offsets as an output view instead") Containers::Pair<Int, Containers::Array<Vector3i>> atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::StridedArrayView1D<const Vector2i>& sizes);
/**
@overload
@m_deprecated_since_latest Use @ref atlasArrayPowerOfTwo(const Vector2i&, std::initializer_list<Vector2i>, const Containers::StridedArrayView1D<Vector3i>&)
instead.
*/
MAGNUM_TEXTURETOOLS_EXPORT CORRADE_DEPRECATED("use the overload taking offsets as an output view instead") Containers::Pair<Int, Containers::Array<Vector3i>> atlasArrayPowerOfTwo(const Vector2i& layerSize, std::initializer_list<Vector2i> sizes);
#endif
}} }}

88
src/Magnum/TextureTools/Test/AtlasTest.cpp

@ -51,8 +51,12 @@ struct AtlasTest: TestSuite::Tester {
void arrayPowerOfTwoAllSameElements(); void arrayPowerOfTwoAllSameElements();
void arrayPowerOfTwoOneLayer(); void arrayPowerOfTwoOneLayer();
void arrayPowerOfTwoMoreLayers(); void arrayPowerOfTwoMoreLayers();
void arrayPowerOfTwoInvalidViewSizes();
void arrayPowerOfTwoWrongLayerSize(); void arrayPowerOfTwoWrongLayerSize();
void arrayPowerOfTwoWrongSize(); void arrayPowerOfTwoWrongSize();
#ifdef MAGNUM_BUILD_DEPRECATED
void arrayPowerOfTwoDeprecated();
#endif
}; };
/* Could make order[15] and then Containers::arraySize(), but then it won't /* Could make order[15] and then Containers::arraySize(), but then it won't
@ -104,13 +108,18 @@ AtlasTest::AtlasTest() {
addInstancedTests({&AtlasTest::arrayPowerOfTwoOneLayer}, addInstancedTests({&AtlasTest::arrayPowerOfTwoOneLayer},
Containers::arraySize(ArrayPowerOfTwoOneLayerData)); Containers::arraySize(ArrayPowerOfTwoOneLayerData));
addTests({&AtlasTest::arrayPowerOfTwoMoreLayers}); addTests({&AtlasTest::arrayPowerOfTwoMoreLayers,
&AtlasTest::arrayPowerOfTwoInvalidViewSizes});
addInstancedTests({&AtlasTest::arrayPowerOfTwoWrongLayerSize}, addInstancedTests({&AtlasTest::arrayPowerOfTwoWrongLayerSize},
Containers::arraySize(ArrayPowerOfTwoWrongLayerSizeData)); Containers::arraySize(ArrayPowerOfTwoWrongLayerSizeData));
addInstancedTests({&AtlasTest::arrayPowerOfTwoWrongSize}, addInstancedTests({&AtlasTest::arrayPowerOfTwoWrongSize},
Containers::arraySize(ArrayPowerOfTwoWrongSizeData)); Containers::arraySize(ArrayPowerOfTwoWrongSizeData));
#ifdef MAGNUM_BUILD_DEPRECATED
addTests({&AtlasTest::arrayPowerOfTwoDeprecated});
#endif
} }
void AtlasTest::basic() { void AtlasTest::basic() {
@ -160,29 +169,27 @@ void AtlasTest::tooSmall() {
} }
void AtlasTest::arrayPowerOfTwoEmpty() { void AtlasTest::arrayPowerOfTwoEmpty() {
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({128, 128}, {}); Containers::ArrayView<Vector3i> offsets;
CORRADE_COMPARE(out.first(), 0); CORRADE_COMPARE(atlasArrayPowerOfTwo({128, 128}, {}, offsets), 0);
CORRADE_COMPARE_AS(out.second(), Containers::arrayView<Vector3i>({
}), TestSuite::Compare::Container);
} }
void AtlasTest::arrayPowerOfTwoSingleElement() { void AtlasTest::arrayPowerOfTwoSingleElement() {
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({128, 128}, {{128, 128}}); Vector3i offsets[1];
CORRADE_COMPARE(out.first(), 1); CORRADE_COMPARE(atlasArrayPowerOfTwo({128, 128}, {{128, 128}}, offsets), 1);
CORRADE_COMPARE_AS(out.second(), Containers::arrayView<Vector3i>({ CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView<Vector3i>({
{0, 0, 0} {0, 0, 0}
}), TestSuite::Compare::Container); }), TestSuite::Compare::Container);
} }
void AtlasTest::arrayPowerOfTwoAllSameElements() { void AtlasTest::arrayPowerOfTwoAllSameElements() {
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({128, 128}, { Vector3i offsets[4];
CORRADE_COMPARE(atlasArrayPowerOfTwo({128, 128}, {
{64, 64}, {64, 64},
{64, 64}, {64, 64},
{64, 64}, {64, 64},
{64, 64}, {64, 64},
}); }, offsets), 1);
CORRADE_COMPARE(out.first(), 1); CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView<Vector3i>({
CORRADE_COMPARE_AS(out.second(), Containers::arrayView<Vector3i>({
{0, 0, 0}, {0, 0, 0},
{64, 0, 0}, {64, 0, 0},
{0, 64, 0}, {0, 64, 0},
@ -245,15 +252,16 @@ void AtlasTest::arrayPowerOfTwoOneLayer() {
expected[i] = expectedSorted[data.order[i]]; expected[i] = expectedSorted[data.order[i]];
} }
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({2048, 2048}, input); Vector3i offsets[ArrayPowerOfTwoOneLayerImageCount];
CORRADE_COMPARE(out.first(), 1); CORRADE_COMPARE(atlasArrayPowerOfTwo({2048, 2048}, input, offsets), 1);
CORRADE_COMPARE_AS(out.second(), CORRADE_COMPARE_AS(Containers::arrayView(offsets),
Containers::ArrayView<const Vector3i>{expected}, Containers::arrayView(expected),
TestSuite::Compare::Container); TestSuite::Compare::Container);
} }
void AtlasTest::arrayPowerOfTwoMoreLayers() { void AtlasTest::arrayPowerOfTwoMoreLayers() {
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({2048, 2048}, { Vector3i offsets[11];
CORRADE_COMPARE(atlasArrayPowerOfTwo({2048, 2048}, {
{2048, 2048}, {2048, 2048},
{1024, 1024}, {1024, 1024},
@ -267,9 +275,8 @@ void AtlasTest::arrayPowerOfTwoMoreLayers() {
{512, 512}, {512, 512},
{256, 256}, {256, 256},
{256, 256} {256, 256}
}); }, offsets), 3);
CORRADE_COMPARE(out.first(), 3); CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView<Vector3i>({
CORRADE_COMPARE_AS(out.second(), Containers::arrayView<Vector3i>({
{0, 0, 0}, {0, 0, 0},
{0, 0, 1}, {0, 0, 1},
@ -286,6 +293,19 @@ void AtlasTest::arrayPowerOfTwoMoreLayers() {
}), TestSuite::Compare::Container); }), TestSuite::Compare::Container);
} }
void AtlasTest::arrayPowerOfTwoInvalidViewSizes() {
CORRADE_SKIP_IF_NO_ASSERT();
Vector2i sizes[2];
Vector3i offsetsInvalid[3];
std::ostringstream out;
Error redirectError{&out};
atlasArrayPowerOfTwo({}, sizes, offsetsInvalid);
CORRADE_COMPARE(out.str(),
"TextureTools::atlasArrayPowerOfTwo(): expected sizes and offsets views to have the same size, got 2 and 3\n");
}
void AtlasTest::arrayPowerOfTwoWrongLayerSize() { void AtlasTest::arrayPowerOfTwoWrongLayerSize() {
auto&& data = ArrayPowerOfTwoWrongLayerSizeData[testCaseInstanceId()]; auto&& data = ArrayPowerOfTwoWrongLayerSizeData[testCaseInstanceId()];
setTestCaseDescription(data.name); setTestCaseDescription(data.name);
@ -294,7 +314,7 @@ void AtlasTest::arrayPowerOfTwoWrongLayerSize() {
std::ostringstream out; std::ostringstream out;
Error redirectError{&out}; Error redirectError{&out};
atlasArrayPowerOfTwo(data.size, {}); atlasArrayPowerOfTwo(data.size, {}, {});
CORRADE_COMPARE(out.str(), Utility::formatString("TextureTools::atlasArrayPowerOfTwo(): expected layer size to be a non-zero power-of-two square, got {}\n", data.message)); CORRADE_COMPARE(out.str(), Utility::formatString("TextureTools::atlasArrayPowerOfTwo(): expected layer size to be a non-zero power-of-two square, got {}\n", data.message));
} }
@ -304,16 +324,40 @@ void AtlasTest::arrayPowerOfTwoWrongSize() {
CORRADE_SKIP_IF_NO_ASSERT(); CORRADE_SKIP_IF_NO_ASSERT();
Vector3i offsets[3];
std::ostringstream out; std::ostringstream out;
Error redirectError{&out}; Error redirectError{&out};
atlasArrayPowerOfTwo({256, 256}, { atlasArrayPowerOfTwo({256, 256}, {
{64, 64}, {64, 64},
{128, 128}, {128, 128},
data.size data.size
}); }, offsets);
CORRADE_COMPARE(out.str(), Utility::formatString("TextureTools::atlasArrayPowerOfTwo(): expected size 2 to be a non-zero power-of-two square not larger than {{256, 256}} but got {}\n", data.message)); CORRADE_COMPARE(out.str(), Utility::formatString("TextureTools::atlasArrayPowerOfTwo(): expected size 2 to be a non-zero power-of-two square not larger than {{256, 256}} but got {}\n", data.message));
} }
#ifdef MAGNUM_BUILD_DEPRECATED
void AtlasTest::arrayPowerOfTwoDeprecated() {
/* Same as arrayPowerOfTwoAllSameElements(), but with the deprecated API */
CORRADE_IGNORE_DEPRECATED_PUSH
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({128, 128}, {
{64, 64},
{64, 64},
{64, 64},
{64, 64},
});
CORRADE_IGNORE_DEPRECATED_POP
CORRADE_COMPARE(out.first(), 1);
CORRADE_COMPARE_AS(out.second(), Containers::arrayView<Vector3i>({
{0, 0, 0},
{64, 0, 0},
{0, 64, 0},
{64, 64, 0}
}), TestSuite::Compare::Container);
}
#endif
}}}} }}}}
CORRADE_TEST_MAIN(Magnum::TextureTools::Test::AtlasTest) CORRADE_TEST_MAIN(Magnum::TextureTools::Test::AtlasTest)

Loading…
Cancel
Save