From 56047b030e09f9ba4aac4d9f960130bec9dcc3be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 11 Sep 2021 16:30:51 +0200 Subject: [PATCH] Trade: make SceneField::MeshMaterial signed. Because the material index has to share the object mapping with a mesh, there needs to be a way to express meshes without associated materials. --- src/Magnum/Trade/SceneData.cpp | 57 +++++++++++++++++-------- src/Magnum/Trade/SceneData.h | 27 +++++++----- src/Magnum/Trade/Test/SceneDataTest.cpp | 42 +++++++++--------- 3 files changed, 77 insertions(+), 49 deletions(-) diff --git a/src/Magnum/Trade/SceneData.cpp b/src/Magnum/Trade/SceneData.cpp index b75787194..02efa08ce 100644 --- a/src/Magnum/Trade/SceneData.cpp +++ b/src/Magnum/Trade/SceneData.cpp @@ -1106,7 +1106,7 @@ Containers::Array SceneData::transformations3DAsArray() const { return out; } -void SceneData::indexFieldIntoInternal(const UnsignedInt fieldId, const std::size_t offset, const Containers::StridedArrayView1D& destination) const { +void SceneData::unsignedIndexFieldIntoInternal(const UnsignedInt fieldId, const std::size_t offset, const Containers::StridedArrayView1D& destination) const { /* fieldId, offset and destination.size() is assumed to be in bounds, checked by the callers */ @@ -1123,8 +1123,31 @@ void SceneData::indexFieldIntoInternal(const UnsignedInt fieldId, const std::siz else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ } -Containers::Array SceneData::indexFieldAsArrayInternal(const UnsignedInt fieldId) const { +void SceneData::indexFieldIntoInternal(const UnsignedInt fieldId, const std::size_t offset, const Containers::StridedArrayView1D& destination) const { + /* fieldId, offset and destination.size() is assumed to be in bounds, + checked by the callers */ + + const SceneFieldData& field = _fields[fieldId]; + const Containers::StridedArrayView1D fieldData = fieldDataFieldViewInternal(field, offset, destination.size()); + const auto destination1ui = Containers::arrayCast<2, Int>(destination); + + if(field._fieldType == SceneFieldType::Int) + Utility::copy(Containers::arrayCast(fieldData), destination); + else if(field._fieldType == SceneFieldType::Short) + Math::castInto(Containers::arrayCast<2, const Short>(fieldData, 1), destination1ui); + else if(field._fieldType == SceneFieldType::Byte) + Math::castInto(Containers::arrayCast<2, const Byte>(fieldData, 1), destination1ui); + else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +Containers::Array SceneData::unsignedIndexFieldAsArrayInternal(const UnsignedInt fieldId) const { Containers::Array out{NoInit, std::size_t(_fields[fieldId]._size)}; + unsignedIndexFieldIntoInternal(fieldId, 0, out); + return out; +} + +Containers::Array SceneData::indexFieldAsArrayInternal(const UnsignedInt fieldId) const { + Containers::Array out{NoInit, std::size_t(_fields[fieldId]._size)}; indexFieldIntoInternal(fieldId, 0, out); return out; } @@ -1135,7 +1158,7 @@ void SceneData::meshesInto(const Containers::StridedArrayView1D& de "Trade::SceneData::meshesInto(): field not found", ); CORRADE_ASSERT(destination.size() == _fields[fieldId]._size, "Trade::SceneData::meshesInto(): expected a view with" << _fields[fieldId]._size << "elements but got" << destination.size(), ); - indexFieldIntoInternal(fieldId, 0, destination); + unsignedIndexFieldIntoInternal(fieldId, 0, destination); } std::size_t SceneData::meshesInto(const std::size_t offset, const Containers::StridedArrayView1D& destination) const { @@ -1145,7 +1168,7 @@ std::size_t SceneData::meshesInto(const std::size_t offset, const Containers::St CORRADE_ASSERT(offset <= _fields[fieldId]._size, "Trade::SceneData::meshesInto(): offset" << offset << "out of bounds for a field of size" << _fields[fieldId]._size, {}); const std::size_t size = Math::min(destination.size(), std::size_t(_fields[fieldId]._size) - offset); - indexFieldIntoInternal(fieldId, offset, destination.prefix(size)); + unsignedIndexFieldIntoInternal(fieldId, offset, destination.prefix(size)); return size; } @@ -1155,10 +1178,10 @@ Containers::Array SceneData::meshesAsArray() const { /* Using the same message as in Into() to avoid too many redundant strings in the binary */ "Trade::SceneData::meshesInto(): field not found", {}); - return indexFieldAsArrayInternal(fieldId); + return unsignedIndexFieldAsArrayInternal(fieldId); } -void SceneData::meshMaterialsInto(const Containers::StridedArrayView1D& destination) const { +void SceneData::meshMaterialsInto(const Containers::StridedArrayView1D& destination) const { const UnsignedInt fieldId = fieldFor(SceneField::MeshMaterial); CORRADE_ASSERT(fieldId != ~UnsignedInt{}, "Trade::SceneData::meshMaterialsInto(): field not found", ); @@ -1167,7 +1190,7 @@ void SceneData::meshMaterialsInto(const Containers::StridedArrayView1D& destination) const { +std::size_t SceneData::meshMaterialsInto(const std::size_t offset, const Containers::StridedArrayView1D& destination) const { const UnsignedInt fieldId = fieldFor(SceneField::MeshMaterial); CORRADE_ASSERT(fieldId != ~UnsignedInt{}, "Trade::SceneData::meshMaterialsInto(): field not found", {}); @@ -1178,7 +1201,7 @@ std::size_t SceneData::meshMaterialsInto(const std::size_t offset, const Contain return size; } -Containers::Array SceneData::meshMaterialsAsArray() const { +Containers::Array SceneData::meshMaterialsAsArray() const { const UnsignedInt fieldId = fieldFor(SceneField::MeshMaterial); CORRADE_ASSERT(fieldId != ~UnsignedInt{}, /* Using the same message as in Into() to avoid too many redundant @@ -1193,7 +1216,7 @@ void SceneData::lightsInto(const Containers::StridedArrayView1D& de "Trade::SceneData::lightsInto(): field not found", ); CORRADE_ASSERT(destination.size() == _fields[fieldId]._size, "Trade::SceneData::lightsInto(): expected a view with" << _fields[fieldId]._size << "elements but got" << destination.size(), ); - indexFieldIntoInternal(fieldId, 0, destination); + unsignedIndexFieldIntoInternal(fieldId, 0, destination); } std::size_t SceneData::lightsInto(const std::size_t offset, const Containers::StridedArrayView1D& destination) const { @@ -1203,7 +1226,7 @@ std::size_t SceneData::lightsInto(const std::size_t offset, const Containers::St CORRADE_ASSERT(offset <= _fields[fieldId]._size, "Trade::SceneData::lightsInto(): offset" << offset << "out of bounds for a field of size" << _fields[fieldId]._size, {}); const std::size_t size = Math::min(destination.size(), std::size_t(_fields[fieldId]._size) - offset); - indexFieldIntoInternal(fieldId, offset, destination.prefix(size)); + unsignedIndexFieldIntoInternal(fieldId, offset, destination.prefix(size)); return size; } @@ -1213,7 +1236,7 @@ Containers::Array SceneData::lightsAsArray() const { /* Using the same message as in Into() to avoid too many redundant strings in the binary */ "Trade::SceneData::lightsInto(): field not found", {}); - return indexFieldAsArrayInternal(fieldId); + return unsignedIndexFieldAsArrayInternal(fieldId); } void SceneData::camerasInto(const Containers::StridedArrayView1D& destination) const { @@ -1222,7 +1245,7 @@ void SceneData::camerasInto(const Containers::StridedArrayView1D& d "Trade::SceneData::camerasInto(): field not found", ); CORRADE_ASSERT(destination.size() == _fields[fieldId]._size, "Trade::SceneData::camerasInto(): expected a view with" << _fields[fieldId]._size << "elements but got" << destination.size(), ); - indexFieldIntoInternal(fieldId, 0, destination); + unsignedIndexFieldIntoInternal(fieldId, 0, destination); } std::size_t SceneData::camerasInto(const std::size_t offset, const Containers::StridedArrayView1D& destination) const { @@ -1232,7 +1255,7 @@ std::size_t SceneData::camerasInto(const std::size_t offset, const Containers::S CORRADE_ASSERT(offset <= _fields[fieldId]._size, "Trade::SceneData::camerasInto(): offset" << offset << "out of bounds for a field of size" << _fields[fieldId]._size, {}); const std::size_t size = Math::min(destination.size(), std::size_t(_fields[fieldId]._size) - offset); - indexFieldIntoInternal(fieldId, offset, destination.prefix(size)); + unsignedIndexFieldIntoInternal(fieldId, offset, destination.prefix(size)); return size; } @@ -1242,7 +1265,7 @@ Containers::Array SceneData::camerasAsArray() const { /* Using the same message as in Into() to avoid too many redundant strings in the binary */ "Trade::SceneData::camerasInto(): field not found", {}); - return indexFieldAsArrayInternal(fieldId); + return unsignedIndexFieldAsArrayInternal(fieldId); } void SceneData::skinsInto(const Containers::StridedArrayView1D& destination) const { @@ -1251,7 +1274,7 @@ void SceneData::skinsInto(const Containers::StridedArrayView1D& des "Trade::SceneData::skinsInto(): field not found", ); CORRADE_ASSERT(destination.size() == _fields[fieldId]._size, "Trade::SceneData::skinsInto(): expected a view with" << _fields[fieldId]._size << "elements but got" << destination.size(), ); - indexFieldIntoInternal(fieldId, 0, destination); + unsignedIndexFieldIntoInternal(fieldId, 0, destination); } std::size_t SceneData::skinsInto(const std::size_t offset, const Containers::StridedArrayView1D& destination) const { @@ -1261,7 +1284,7 @@ std::size_t SceneData::skinsInto(const std::size_t offset, const Containers::Str CORRADE_ASSERT(offset <= _fields[fieldId]._size, "Trade::SceneData::skinsInto(): offset" << offset << "out of bounds for a field of size" << _fields[fieldId]._size, {}); const std::size_t size = Math::min(destination.size(), std::size_t(_fields[fieldId]._size) - offset); - indexFieldIntoInternal(fieldId, offset, destination.prefix(size)); + unsignedIndexFieldIntoInternal(fieldId, offset, destination.prefix(size)); return size; } @@ -1271,7 +1294,7 @@ Containers::Array SceneData::skinsAsArray() const { /* Using the same message as in Into() to avoid too many redundant strings in the binary */ "Trade::SceneData::skinsInto(): field not found", {}); - return indexFieldAsArrayInternal(fieldId); + return unsignedIndexFieldAsArrayInternal(fieldId); } Containers::Array SceneData::releaseFieldData() { diff --git a/src/Magnum/Trade/SceneData.h b/src/Magnum/Trade/SceneData.h index 84ac44e7a..8e7b7b034 100644 --- a/src/Magnum/Trade/SceneData.h +++ b/src/Magnum/Trade/SceneData.h @@ -208,10 +208,10 @@ enum class SceneField: UnsignedInt { /** * ID of a material for a @ref SceneField::Mesh, corresponding to the ID - * passed to @ref AbstractImporter::material(). Type is usually - * @ref SceneFieldType::UnsignedInt, but can be also any of - * @relativeref{SceneFieldType,UnsignedByte} or - * @relativeref{SceneFieldType,UnsignedShort}. Expected to share the + * passed to @ref AbstractImporter::material() or @cpp -1 @ce if the mesh + * has no material associated. Type is usually @ref SceneFieldType::Int, + * but can be also any of @relativeref{SceneFieldType,Byte} or + * @relativeref{SceneFieldType,Short}. Expected to share the * object mapping view with @ref SceneField::Mesh. * @see @ref SceneData::meshMaterialsAsArray() */ @@ -1492,7 +1492,7 @@ class MAGNUM_TRADE_EXPORT SceneData { * newly-allocated array. The field is expected to exist. * @see @ref meshMaterialsInto(), @ref hasField() */ - Containers::Array meshMaterialsAsArray() const; + Containers::Array meshMaterialsAsArray() const; /** * @brief Mesh material IDs as 32-bit integers into a pre-allocated view @@ -1503,20 +1503,20 @@ class MAGNUM_TRADE_EXPORT SceneData { * @p destination is sized to contain exactly all data. * @see @ref fieldSize(SceneField) const */ - void meshMaterialsInto(const Containers::StridedArrayView1D& destination) const; + void meshMaterialsInto(const Containers::StridedArrayView1D& destination) const; /** * @brief A subrange of mesh material IDs as 32-bit integers into a pre-allocated view * @m_since_latest * - * Compared to @ref meshMaterialsInto(const Containers::StridedArrayView1D&) const + * Compared to @ref meshMaterialsInto(const Containers::StridedArrayView1D&) const * extracts only a subrange of the field defined by @p offset and size * of the @p destination view, returning the count of items actually * extracted. The @p offset is expected to not be larger than the field * size. * @see @ref fieldSize(SceneField) const */ - std::size_t meshMaterialsInto(std::size_t offset, const Containers::StridedArrayView1D& destination) const; + std::size_t meshMaterialsInto(std::size_t offset, const Containers::StridedArrayView1D& destination) const; /** * @brief Light IDs as 32-bit integers @@ -1689,8 +1689,10 @@ class MAGNUM_TRADE_EXPORT SceneData { MAGNUM_TRADE_LOCAL std::size_t findTransformFields(UnsignedInt& transformationFieldId, UnsignedInt& translationFieldId, UnsignedInt& rotationFieldId, UnsignedInt& scalingFieldId) const; MAGNUM_TRADE_LOCAL void transformations2DIntoInternal(UnsignedInt transformationFieldId, UnsignedInt translationFieldId, UnsignedInt rotationFieldId, UnsignedInt scalingFieldId, std::size_t offset, const Containers::StridedArrayView1D& destination) const; MAGNUM_TRADE_LOCAL void transformations3DIntoInternal(UnsignedInt transformationFieldId, UnsignedInt translationFieldId, UnsignedInt rotationFieldId, UnsignedInt scalingFieldId, std::size_t offset, const Containers::StridedArrayView1D& destination) const; - MAGNUM_TRADE_LOCAL void indexFieldIntoInternal(const UnsignedInt fieldId, std::size_t offset, const Containers::StridedArrayView1D& destination) const; - MAGNUM_TRADE_LOCAL Containers::Array indexFieldAsArrayInternal(const UnsignedInt fieldId) const; + MAGNUM_TRADE_LOCAL void unsignedIndexFieldIntoInternal(const UnsignedInt fieldId, std::size_t offset, const Containers::StridedArrayView1D& destination) const; + MAGNUM_TRADE_LOCAL void indexFieldIntoInternal(const UnsignedInt fieldId, std::size_t offset, const Containers::StridedArrayView1D& destination) const; + MAGNUM_TRADE_LOCAL Containers::Array unsignedIndexFieldAsArrayInternal(const UnsignedInt fieldId) const; + MAGNUM_TRADE_LOCAL Containers::Array indexFieldAsArrayInternal(const UnsignedInt fieldId) const; DataFlags _dataFlags; SceneObjectType _objectType; @@ -1857,13 +1859,16 @@ namespace Implementation { type == SceneFieldType::Quaternion || type == SceneFieldType::Quaterniond)) || ((name == SceneField::Mesh || - name == SceneField::MeshMaterial || name == SceneField::Light || name == SceneField::Camera || name == SceneField::Skin) && (type == SceneFieldType::UnsignedByte || type == SceneFieldType::UnsignedShort || type == SceneFieldType::UnsignedInt)) || + (name == SceneField::MeshMaterial && + (type == SceneFieldType::Byte || + type == SceneFieldType::Short || + type == SceneFieldType::Int)) || /* Custom fields can be anything */ isSceneFieldCustom(name); } diff --git a/src/Magnum/Trade/Test/SceneDataTest.cpp b/src/Magnum/Trade/Test/SceneDataTest.cpp index c9630c62d..fce55fd68 100644 --- a/src/Magnum/Trade/Test/SceneDataTest.cpp +++ b/src/Magnum/Trade/Test/SceneDataTest.cpp @@ -303,9 +303,9 @@ SceneDataTest::SceneDataTest() { Containers::arraySize(IntoArrayOffsetData)); addTests({&SceneDataTest::meshesIntoArrayInvalidSizeOrOffset, - &SceneDataTest::meshMaterialsAsArray, - &SceneDataTest::meshMaterialsAsArray, - &SceneDataTest::meshMaterialsAsArray}); + &SceneDataTest::meshMaterialsAsArray, + &SceneDataTest::meshMaterialsAsArray, + &SceneDataTest::meshMaterialsAsArray}); addInstancedTests({&SceneDataTest::meshMaterialsIntoArray}, Containers::arraySize(IntoArrayOffsetData)); @@ -1314,7 +1314,7 @@ void SceneDataTest::constructZeroFields() { void SceneDataTest::constructZeroObjects() { int importerState; SceneFieldData meshes{SceneField::Mesh, SceneObjectType::UnsignedInt, nullptr, SceneFieldType::UnsignedShort, nullptr}; - SceneFieldData materials{SceneField::MeshMaterial, SceneObjectType::UnsignedInt, nullptr, SceneFieldType::UnsignedInt, nullptr}; + SceneFieldData materials{SceneField::MeshMaterial, SceneObjectType::UnsignedInt, nullptr, SceneFieldType::Int, nullptr}; SceneData scene{SceneObjectType::UnsignedInt, 0, nullptr, {meshes, materials}, &importerState}; CORRADE_COMPARE(scene.dataFlags(), DataFlag::Owned|DataFlag::Mutable); CORRADE_VERIFY(!scene.fieldData().empty()); @@ -1327,7 +1327,7 @@ void SceneDataTest::constructZeroObjects() { /* Field property access by name */ CORRADE_COMPARE(scene.fieldType(SceneField::Mesh), SceneFieldType::UnsignedShort); - CORRADE_COMPARE(scene.fieldType(SceneField::MeshMaterial), SceneFieldType::UnsignedInt); + CORRADE_COMPARE(scene.fieldType(SceneField::MeshMaterial), SceneFieldType::Int); CORRADE_COMPARE(scene.fieldSize(SceneField::Mesh), 0); CORRADE_COMPARE(scene.fieldSize(SceneField::MeshMaterial), 0); CORRADE_COMPARE(scene.objects(SceneField::Mesh).data(), nullptr); @@ -1381,7 +1381,7 @@ void SceneDataTest::constructDuplicateField() { /* Builtin fields are checked using a bitfield, as they have monotonic numbering */ SceneFieldData meshes{SceneField::Mesh, SceneObjectType::UnsignedInt, nullptr, SceneFieldType::UnsignedShort, nullptr}; - SceneFieldData materials{SceneField::MeshMaterial, SceneObjectType::UnsignedInt, nullptr, SceneFieldType::UnsignedInt, nullptr}; + SceneFieldData materials{SceneField::MeshMaterial, SceneObjectType::UnsignedInt, nullptr, SceneFieldType::Int, nullptr}; SceneFieldData meshesAgain{SceneField::Mesh, SceneObjectType::UnsignedInt, nullptr, SceneFieldType::UnsignedInt, nullptr}; std::ostringstream out; @@ -1413,7 +1413,7 @@ void SceneDataTest::constructInconsistentObjectType() { #endif SceneFieldData meshes{SceneField::Mesh, SceneObjectType::UnsignedInt, nullptr, SceneFieldType::UnsignedShort, nullptr}; - SceneFieldData materials{SceneField::MeshMaterial, SceneObjectType::UnsignedShort, nullptr, SceneFieldType::UnsignedInt, nullptr}; + SceneFieldData materials{SceneField::MeshMaterial, SceneObjectType::UnsignedShort, nullptr, SceneFieldType::Int, nullptr}; std::ostringstream out; Error redirectError{&out}; @@ -1439,12 +1439,12 @@ void SceneDataTest::constructObjectDataNotContained() { }}; /* Second a view that's in a completely different location */ SceneData{SceneObjectType::UnsignedShort, 5, {}, data, { - SceneFieldData{SceneField::MeshMaterial, dataIn, dataIn}, + SceneFieldData{SceneField::Light, dataIn, dataIn}, SceneFieldData{SceneField::Mesh, dataOut, dataIn} }}; /* Verify the owning constructor does the checks as well */ SceneData{SceneObjectType::UnsignedShort, 5, std::move(data), { - SceneFieldData{SceneField::MeshMaterial, dataIn, dataIn}, + SceneFieldData{SceneField::Light, dataIn, dataIn}, SceneFieldData{SceneField::Mesh, dataOut, dataIn} }}; /* And if we have no data at all, it doesn't try to dereference them but @@ -1486,7 +1486,7 @@ void SceneDataTest::constructFieldDataNotContained() { }}; /* Second a view that's in a completely different location */ SceneData{SceneObjectType::UnsignedShort, 5, {}, data, { - SceneFieldData{SceneField::MeshMaterial, dataIn, dataIn}, + SceneFieldData{SceneField::Light, dataIn, dataIn}, SceneFieldData{SceneField::Mesh, dataIn, dataOut} }}; /* Verify array size is taken into account as well. If not, the data would @@ -1496,7 +1496,7 @@ void SceneDataTest::constructFieldDataNotContained() { }}; /* Verify the owning constructor does the checks as well */ SceneData{SceneObjectType::UnsignedShort, 5, std::move(data), { - SceneFieldData{SceneField::MeshMaterial, dataIn, dataIn}, + SceneFieldData{SceneField::Light, dataIn, dataIn}, SceneFieldData{SceneField::Mesh, dataIn, dataOut} }}; /* Not checking for nullptr data, since that got checked for object view @@ -1614,8 +1614,8 @@ void SceneDataTest::constructMismatchedMeshMaterialView() { reinterpret_cast(data.data() + 0x0c), 3}; Containers::ArrayView meshMaterialObjectData{ reinterpret_cast(data.data() + 0x18), 3}; - Containers::ArrayView meshMaterialFieldData{ - reinterpret_cast(data.data() + 0x24), 3}; + Containers::ArrayView meshMaterialFieldData{ + reinterpret_cast(data.data() + 0x24), 3}; SceneFieldData meshes{SceneField::Mesh, meshObjectData, meshFieldData}; SceneFieldData meshMaterialsDifferent{SceneField::MeshMaterial, meshMaterialObjectData, meshMaterialFieldData}; @@ -2892,7 +2892,7 @@ template void SceneDataTest::meshMaterialsAsArray() { T meshMaterial; } fields[]{ {0, T(15)}, - {1, T(37)}, + {1, T(-1)}, {15, T(44)} }; @@ -2905,7 +2905,7 @@ template void SceneDataTest::meshMaterialsAsArray() { }}; CORRADE_COMPARE_AS(scene.meshMaterialsAsArray(), - Containers::arrayView({15, 37, 44}), + Containers::arrayView({15, -1, 44}), TestSuite::Compare::Container); } @@ -2919,10 +2919,10 @@ void SceneDataTest::meshMaterialsIntoArray() { struct Field { UnsignedInt object; - UnsignedInt meshMaterial; + Int meshMaterial; } fields[]{ {1, 15}, - {0, 37}, + {0, -1}, {4, 44} }; @@ -2938,7 +2938,7 @@ void SceneDataTest::meshMaterialsIntoArray() { /* The offset-less overload should give back all data */ { - UnsignedInt out[3]; + Int out[3]; scene.meshMaterialsInto(out); CORRADE_COMPARE_AS(Containers::stridedArrayView(out), view.slice(&Field::meshMaterial), @@ -2946,7 +2946,7 @@ void SceneDataTest::meshMaterialsIntoArray() { /* The offset variant only a subset */ } { - Containers::Array out{data.size}; + Containers::Array out{data.size}; CORRADE_COMPARE(scene.meshMaterialsInto(data.offset, out), data.expectedSize); CORRADE_COMPARE_AS(out.prefix(data.expectedSize), view.slice(&Field::meshMaterial) @@ -2962,7 +2962,7 @@ void SceneDataTest::meshMaterialsIntoArrayInvalidSizeOrOffset() { struct Field { UnsignedInt object; - UnsignedInt meshMaterial; + Int meshMaterial; } fields[3]{}; Containers::StridedArrayView1D view = fields; @@ -2973,7 +2973,7 @@ void SceneDataTest::meshMaterialsIntoArrayInvalidSizeOrOffset() { std::ostringstream out; Error redirectError{&out}; - UnsignedInt destination[2]; + Int destination[2]; scene.meshMaterialsInto(destination); scene.meshMaterialsInto(4, destination); CORRADE_COMPARE(out.str(),