diff --git a/src/Magnum/Trade/MeshData.cpp b/src/Magnum/Trade/MeshData.cpp index d12d889179..c8342d9025 100644 --- a/src/Magnum/Trade/MeshData.cpp +++ b/src/Magnum/Trade/MeshData.cpp @@ -795,6 +795,156 @@ Containers::Array MeshData::releaseVertexData() { return out; } +namespace { + struct MeshDataHeader: DataChunkHeader { + UnsignedInt indexCount; + UnsignedInt vertexCount; + MeshPrimitive primitive; + MeshIndexType indexType; + Byte:8; + UnsignedShort attributeCount; + std::size_t indexOffset; + std::size_t indexDataSize; + std::size_t vertexDataSize; + }; + + static_assert(sizeof(MeshDataHeader) == (sizeof(void*) == 4 ? 48 : 64), + "MeshDataHeader has unexpected size"); +} + +Containers::Optional MeshData::deserialize(Containers::ArrayView data) { + /* Validate the header. If that fails, the error has been already printed, + so just propagate */ + const DataChunkHeader* chunk = dataChunkHeaderDeserialize(data); + if(!chunk) return Containers::NullOpt; + + /* Basic header validity */ + if(chunk->type != DataChunkType::Mesh) { + Error{} << "Trade::MeshData::deserialize(): expected data chunk type" << DataChunkType::Mesh << "but got" << chunk->type; + return Containers::NullOpt; + } + if(chunk->typeVersion != 0) { + Error{} << "Trade::MeshData::deserialize(): invalid chunk type version, expected 0 but got" << chunk->typeVersion; + return Containers::NullOpt; + } + if(chunk->size < sizeof(MeshDataHeader)) { + Error{} << "Trade::MeshData::deserialize(): expected at least a" << sizeof(MeshDataHeader) << Debug::nospace << "-byte chunk for a header but got" << chunk->size; + return Containers::NullOpt; + } + + /* Reinterpret as a mesh data and check that everything can fit */ + const MeshDataHeader& header = static_cast(*chunk); + const std::size_t size = sizeof(MeshDataHeader) + header.attributeCount*sizeof(MeshAttributeData) + header.indexDataSize + header.vertexDataSize; + if(chunk->size != size) { + Error{} << "Trade::MeshData::deserialize(): expected a" << size << Debug::nospace << "-byte chunk but got" << chunk->size; + return Containers::NullOpt; + } + + Containers::ArrayView attributeData{reinterpret_cast(reinterpret_cast(data.data()) + sizeof(MeshDataHeader)), header.attributeCount}; + Containers::ArrayView vertexData{reinterpret_cast(data.data()) + sizeof(MeshDataHeader) + header.attributeCount*sizeof(MeshAttributeData) + header.indexDataSize, header.vertexDataSize}; + + /* Check bounds of indices and all attributes */ + /** @todo this will assert on invalid index type */ + Containers::ArrayView indexData; + MeshIndexData indices; + if(header.indexType != MeshIndexType{}) { + const std::size_t indexEnd = header.indexOffset + header.indexCount*meshIndexTypeSize(header.indexType); + if(indexEnd > header.indexDataSize) { + Error{} << "Trade::MeshData::deserialize(): indices [" << Debug::nospace << header.indexOffset << Debug::nospace << ":" << Debug::nospace << indexEnd << Debug::nospace << "] out of range for" << header.indexDataSize << "bytes of index data"; + return Containers::NullOpt; + } + + indexData = Containers::ArrayView{reinterpret_cast(data.data()) + sizeof(MeshDataHeader) + header.attributeCount*sizeof(MeshAttributeData), header.indexDataSize}; + indices = MeshIndexData{header.indexType, indexData.suffix(header.indexOffset)}; + } + for(std::size_t i = 0; i != attributeData.size(); ++i) { + const MeshAttributeData& attribute = attributeData[i]; + + /** @todo this will assert on invalid vertex format */ + /** @todo check also consistency of vertex count and _isOffsetOnly? */ + /* Check that the view fits into the provided vertex data array. For + implementation-specific formats we don't know the size so use 0 to + check at least partially. */ + const UnsignedInt typeSize = + isVertexFormatImplementationSpecific(attribute._format) ? 0 : + vertexFormatSize(attribute._format); + const std::size_t attributeEnd = attribute._data.offset + (header.vertexCount - 1)*attribute._stride + typeSize; + if(header.vertexCount && attributeEnd > header.vertexDataSize) { + Error{} << "Trade::MeshData::deserialize(): attribute" << i << "[" << Debug::nospace << attribute._data.offset << Debug::nospace << ":" << Debug::nospace << attributeEnd << Debug::nospace << "] out of range for" << header.vertexDataSize << "bytes of vertex data"; + return Containers::NullOpt; + } + } + + return MeshData{header.primitive, + {}, indexData, indices, + {}, vertexData, meshAttributeDataNonOwningArray(attributeData), + header.vertexCount}; +} + +std::size_t MeshData::serializedSize() const { + return sizeof(MeshDataHeader) + sizeof(MeshAttributeData)*_attributes.size() + + _indexData.size() + _vertexData.size(); +} + +std::size_t MeshData::serializeInto(Containers::ArrayView out) const { + #ifndef CORRADE_NO_DEBUG + const std::size_t size = serializedSize(); + CORRADE_ASSERT(out.size() == size, "Trade::MeshData::serializeInto(): data too small, expected at least" << size << "bytes but got" << out.size(), {}); + #endif + + /* Serialize the header */ + dataChunkHeaderSerializeInto(out, DataChunkType::Mesh, 0); + + /* Memset the header to avoid padding getting random values */ + std::memset(out.data() + sizeof(DataChunkHeader), 0, sizeof(MeshDataHeader) + _attributes.size()*sizeof(MeshAttributeData) - sizeof(DataChunkHeader)); + + MeshDataHeader& header = *reinterpret_cast(out.data()); + header.indexCount = _indexCount; + header.vertexCount = _vertexCount; + header.primitive = _primitive; + header.indexType = _indexType; + header.attributeCount = _attributes.size(); + header.indexOffset = _indices - _indexData.data(); + header.indexDataSize = _indexData.size(); + header.vertexDataSize = _vertexData.size(); + + std::size_t offset = sizeof(MeshDataHeader); + + /* Copy the attribute data, turning them into offset-only */ + auto outAttributeData = Containers::arrayCast(out.slice(offset, offset + sizeof(MeshAttributeData)*_attributes.size())); + for(std::size_t i = 0; i != outAttributeData.size(); ++i) { + if(_attributes[i]._isOffsetOnly) + outAttributeData[i]._data.offset = _attributes[i]._data.offset; + else + outAttributeData[i]._data.offset = reinterpret_cast(_attributes[i]._data.pointer) - _vertexData; + outAttributeData[i]._vertexCount = _attributes[i]._vertexCount; + outAttributeData[i]._format = _attributes[i]._format; + outAttributeData[i]._stride = _attributes[i]._stride; + outAttributeData[i]._name = _attributes[i]._name; + outAttributeData[i]._arraySize = _attributes[i]._arraySize; + outAttributeData[i]._isOffsetOnly = true; + } + offset += sizeof(MeshAttributeData)*_attributes.size(); + + /* Copy the index data */ + Utility::copy(_indexData, out.slice(offset, offset + _indexData.size())); + offset += _indexData.size(); + + /* Copy the vertex data */ + Utility::copy(_vertexData, out.slice(offset, offset + _vertexData.size())); + offset += _vertexData.size(); + + /* Check we calculated correctly, return number of bytes written */ + CORRADE_INTERNAL_ASSERT(offset == size); + return offset; +} + +Containers::Array MeshData::serialize() const { + Containers::Array out{Containers::NoInit, serializedSize()}; + serializeInto(out); + return out; +} + Debug& operator<<(Debug& debug, const MeshAttribute value) { debug << "Trade::MeshAttribute" << Debug::nospace; diff --git a/src/Magnum/Trade/MeshData.h b/src/Magnum/Trade/MeshData.h index 6b97f63713..521a43ddff 100644 --- a/src/Magnum/Trade/MeshData.h +++ b/src/Magnum/Trade/MeshData.h @@ -31,6 +31,7 @@ */ #include +#include #include #include "Magnum/Mesh.h" @@ -709,6 +710,53 @@ you can also supply implementation-specific values that are not available in the generic @ref MeshPrimitive enum, similarly see also @ref Trade-MeshAttributeData-custom-vertex-format for details on implementation-specific @ref VertexFormat values. + +@section Trade-MeshData-serialization Memory-mappable serialization format + +Using @ref serialize(), an instance of this class can be serialized into a +binary format, and deserialized back using @ref deserialize(). The +deserialization only involves various sanity checks followed by a creation of a +new @ref MeshData instance referencing the index, vertex and attribute data. +It thus makes it possible to operate for example directly on a memory-mapped +file. The binary representation begins with @ref DataChunkHeader of type +@ref DataChunkType::Mesh and type version @cpp 0 @ce. The rest is defined like +below, depending on bitness and endianness defined by the header signature. +Fields that are stored in an endian-dependent way are marked with +@m_class{m-label m-primary} **E**: + +@m_class{m-fullwidth} + +Byte offset | Byte size | Contents +----------- | --------- | ----------------------------------------------------- +20 or 24 | 4 @m_class{m-label m-primary} **E** | Index count, or @cpp 0 @ce if the mesh has no indices +24 or 28 | 4 @m_class{m-label m-primary} **E** | Vertex count, or @cpp 0 @ce if the mesh has no vertices +28 or 32 | 4 @m_class{m-label m-primary} **E** | Mesh primitive, defined with @ref MeshPrimitive +32 or 36 | 1 | Index type, defined with @ref MeshIndexType, or zero if the mesh is not indexed +33 or 37 | 1 | @m_class{m-text m-dim} *Padding / reserved* +34 or 38 | 2 @m_class{m-label m-primary} **E** | Attribute count +36 or 40 | 4 or 8 @m_class{m-label m-primary} **E** | Index offset in the index data array +40 or 44 | 4 or 8 @m_class{m-label m-primary} **E** | Index data size in bytes +44 or 56 | 4 or 8 @m_class{m-label m-primary} **E** | Vertex data size in bytes +48 or 64 | ... @m_class{m-label m-primary} **E** | List of @ref MeshAttributeData entries, count defined by attribute count above +... | ... @m_class{m-label m-primary} **E** | Index data, byte count defined by index data size above +... | ... @m_class{m-label m-primary} **E** | Vertex data, byte count defined by vertex data size above + +For the attribute list, each @ref MeshAttributeData entry is either 20 or 24 +bytes, with fields defined like this. In this case it exactly matches the +internals of @ref MeshAttributeData to allow the attribute array to be +referenced directly from the original memory: + +Byte offset | Byte size | Contents +----------- | --------- | ----------------------------------------------------- +0 | 4 @m_class{m-label m-primary} **E** | Vertex format, defined with @ref VertexFormat +4 | 2 @m_class{m-label m-primary} **E** | Mesh attribute name, defined with @ref MeshAttribute +6 | 1 | Whether the attribute is offset-only. Always @cpp 1 @ce. +7 | 1 | @m_class{m-text m-dim} *Padding / reserved* +8 | 4 @m_class{m-label m-primary} **E** | Vertex count. Same value as the vertex count field above. +12 | 2 @m_class{m-label m-primary} **E** | Vertex stride. Always positive and not larger than @cpp 32767 @ce. +14 | 2 @m_class{m-label m-primary} **E** | Attribute array size +16 | 4 or 8 @m_class{m-label m-primary} **E** | Attribute offset in the vertex data array + @see @ref AbstractImporter::mesh() */ class MAGNUM_TRADE_EXPORT MeshData { @@ -721,6 +769,30 @@ class MAGNUM_TRADE_EXPORT MeshData { ImplicitVertexCount = ~UnsignedInt{} }; + /** + * @brief Try to deserialize from a memory-mappable representation + * + * If @p data is a valid serialized representation of @ref MeshData + * matching current platform, returns a @ref MeshData instance + * referencing the original data. On failure prints an error message + * and returns @ref Containers::NullOpt. + * + * The returned instance doesn't provide mutable access to the original + * data, pass a non-const view to the overload below to get that. + * @see @ref serialize() + */ + static Containers::Optional deserialize(Containers::ArrayView data); + + /** @overload */ + template>::value>::type> static Containers::Optional deserialize(T&& data) { + Containers::Optional out = deserialize(Containers::ArrayView{data}); + if(out) { + out->_indexDataFlags = DataFlag::Mutable; + out->_vertexDataFlags = DataFlag::Mutable; + } + return out; + } + /** * @brief Construct an indexed mesh data * @param primitive Primitive @@ -1775,6 +1847,30 @@ class MAGNUM_TRADE_EXPORT MeshData { */ const void* importerState() const { return _importerState; } + /** + * @brief Size of serialized data + * + * Amount of bytes written by @ref serializeInto() or @ref serialize(). + */ + std::size_t serializedSize() const; + + /** + * @brief Serialize to a memory-mappable representation + * + * @see @ref serializeInto(), @ref deserialize() + */ + Containers::Array serialize() const; + + /** + * @brief Serialize to a memory-mappable representation into an existing array + * @param[out] out Where to write the output + * @return Number of bytes written. Same as @ref serializedSize(). + * + * Expects that @p data is at least @ref serializedSize(). + * @see @ref serialize(), @ref deserialize() + */ + std::size_t serializeInto(Containers::ArrayView out) const; + private: /* For custom deleter checks. Not done in the constructors here because the restriction is pointless when used outside of plugin diff --git a/src/Magnum/Trade/Test/CMakeLists.txt b/src/Magnum/Trade/Test/CMakeLists.txt index b05842bd87..49dd18e151 100644 --- a/src/Magnum/Trade/Test/CMakeLists.txt +++ b/src/Magnum/Trade/Test/CMakeLists.txt @@ -52,7 +52,24 @@ corrade_add_test(TradeDataTest DataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeImageDataTest ImageDataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeLightDataTest LightDataTest.cpp LIBRARIES MagnumTrade) corrade_add_test(TradeMaterialDataTest MaterialDataTest.cpp LIBRARIES MagnumTradeTestLib) -corrade_add_test(TradeMeshDataTest MeshDataTest.cpp LIBRARIES MagnumTradeTestLib) + +corrade_add_test(TradeMeshDataTest MeshDataTest.cpp + LIBRARIES MagnumTradeTestLib + FILES + mesh-be32.blob + mesh-be64.blob + mesh-le32.blob + mesh-le64.blob + mesh-empty-be32.blob + mesh-empty-be64.blob + mesh-empty-le32.blob + mesh-empty-le64.blob + mesh-nonindexed-be32.blob + mesh-nonindexed-be64.blob + mesh-nonindexed-le32.blob + mesh-nonindexed-le64.blob) +target_include_directories(TradeMeshDataTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + corrade_add_test(TradeObjectData2DTest ObjectData2DTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeObjectData3DTest ObjectData3DTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeSceneDataTest SceneDataTest.cpp LIBRARIES MagnumTrade) diff --git a/src/Magnum/Trade/Test/MeshDataTest.cpp b/src/Magnum/Trade/Test/MeshDataTest.cpp index 911462cc73..b9ee3b4669 100644 --- a/src/Magnum/Trade/Test/MeshDataTest.cpp +++ b/src/Magnum/Trade/Test/MeshDataTest.cpp @@ -26,12 +26,18 @@ #include #include #include +#include +#include #include +#include +#include #include "Magnum/Math/Color.h" #include "Magnum/Math/Half.h" #include "Magnum/Trade/MeshData.h" +#include "configure.h" + namespace Magnum { namespace Trade { namespace Test { namespace { struct MeshDataTest: TestSuite::Tester { @@ -164,6 +170,13 @@ struct MeshDataTest: TestSuite::Tester { void releaseIndexData(); void releaseAttributeData(); void releaseVertexData(); + + void serialize(); + void serializeEmpty(); + void serializeIntoTooSmall(); + + void deserialize(); + void deserializeInvalid(); }; const struct { @@ -194,6 +207,87 @@ const struct { {"mutable", DataFlag::Mutable} }; +const struct { + const char* name; + const char* filePrefix; + bool indexed; +} SerializeData[] { + {"", "mesh", true}, + {"non-indexed", "mesh-nonindexed", false} +}; + +const struct { + const char* name; + std::size_t size; + std::size_t offset; + Containers::Array replace; + const char* message; +} DeserializeInvalidData[] { + /* This checks we correctly propagate chunk header errors, the rest is + verified in DataTest */ + {"too short to contain a chunk header", + sizeof(void*) == 4 ? 19 : 23, 0, nullptr, + sizeof(void*) == 4 ? + "dataChunkHeaderDeserialize(): expected at least 20 bytes for a header but got 19" : + "dataChunkHeaderDeserialize(): expected at least 24 bytes for a header but got 23"}, + + {"chunk too short to contain a meshdata header", + 0, 16, /* not cutting the file, only adapting header */ + #ifndef CORRADE_TARGET_BIG_ENDIAN + sizeof(void*) == 4 ? Containers::array({0x2f, 0, 0, 0}) : + Containers::array({0x3f, 0, 0, 0, 0, 0, 0, 0}), + #else + sizeof(void*) == 4 ? Containers::array({0, 0, 0, 0x2f}) : + Containers::array({0, 0, 0, 0, 0, 0, 0, 0x3f}), + #endif + sizeof(void*) == 4 ? + "MeshData::deserialize(): expected at least a 48-byte chunk for a header but got 47" : + "MeshData::deserialize(): expected at least a 64-byte chunk for a header but got 63"}, + {"chunk too short to contain all data", + 0, 16, /* not cutting the file, only adapting header */ + #ifndef CORRADE_TARGET_BIG_ENDIAN + sizeof(void*) == 4 ? Containers::array({'\xd3', 0, 0, 0}) : + Containers::array({'\xf3', 0, 0, 0, 0, 0, 0, 0}), + #else + sizeof(void*) == 4 ? Containers::array({0, 0, 0, '\xd3'}) : + Containers::array({0, 0, 0, 0, 0, 0, 0, '\xf3'}), + #endif + sizeof(void*) == 4 ? + "MeshData::deserialize(): expected a 212-byte chunk but got 211" : + "MeshData::deserialize(): expected a 244-byte chunk but got 243"}, + {"invalid type", + 0, 12, Containers::array({'M', 'e', 'h', 'h'}), + "MeshData::deserialize(): expected data chunk type Trade::DataChunkType('M', 'e', 's', 'h') but got Trade::DataChunkType('M', 'e', 'h', 'h')"}, + {"invalid type version", + 0, 10, + #ifndef CORRADE_TARGET_BIG_ENDIAN + Containers::array({1, 0}), + #else + Containers::array({0, 1}), + #endif + "MeshData::deserialize(): invalid chunk type version, expected 0 but got 1"}, + {"index array out of bounds", + 0, sizeof(void*) == 4 ? 36 : 40, + #ifndef CORRADE_TARGET_BIG_ENDIAN + sizeof(void*) == 4 ? Containers::array({5, 0, 0, 0}) : + Containers::array({5, 0, 0, 0, 0, 0, 0, 0}), + #else + sizeof(void*) == 4 ? Containers::array({0, 0, 0, 5}) : + Containers::array({0, 0, 0, 0, 0, 0, 0, 5}), + #endif + "MeshData::deserialize(): indices [5:13] out of range for 12 bytes of index data"}, + {"attribute out of bounds", + 0, sizeof(void*) == 4 ? 48 + 20 + 16 : 64 + 24 + 16, + #ifndef CORRADE_TARGET_BIG_ENDIAN + sizeof(void*) == 4 ? Containers::array({23, 0, 0, 0}) : + Containers::array({23, 0, 0, 0, 0, 0, 0, 0}), + #else + sizeof(void*) == 4 ? Containers::array({0, 0, 0, 23}) : + Containers::array({0, 0, 0, 0, 0, 0, 0, 23}), + #endif + "MeshData::deserialize(): attribute 1 [23:73] out of range for 72 bytes of vertex data"} +}; + MeshDataTest::MeshDataTest() { addTests({&MeshDataTest::customAttributeName, &MeshDataTest::customAttributeNameTooLarge, @@ -382,6 +476,18 @@ MeshDataTest::MeshDataTest() { &MeshDataTest::releaseIndexData, &MeshDataTest::releaseAttributeData, &MeshDataTest::releaseVertexData}); + + addInstancedTests({&MeshDataTest::serialize}, + Containers::arraySize(SerializeData)); + + addTests({&MeshDataTest::serializeEmpty, + &MeshDataTest::serializeIntoTooSmall}); + + addInstancedTests({&MeshDataTest::deserialize}, + Containers::arraySize(SerializeData)); + + addInstancedTests({&MeshDataTest::deserializeInvalid}, + Containers::arraySize(DeserializeInvalidData)); } void MeshDataTest::customAttributeName() { @@ -2783,6 +2889,191 @@ void MeshDataTest::releaseVertexData() { CORRADE_COMPARE(data.attributeOffset(0), 48); } +constexpr char BlobFileSuffix[] { + '-', + #ifndef CORRADE_TARGET_BIG_ENDIAN + 'l', + #else + 'b', + #endif + 'e', sizeof(void*) == 4 ? '3' : '6', sizeof(void*) == 4 ? '2' : '4', + '.', 'b', 'l', 'o', 'b', '\0' +}; + +struct SerializedVertex { + Vector2 position; + Vector2ub textureCoordinates; + UnsignedShort props[2]; + UnsignedShort:16; + Double weight; +}; + +void MeshDataTest::serialize() { + auto&& data = SerializeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* Clang on iOS and Android doesn't like constexpr here */ + const SerializedVertex vertexData[] { + {{1.0f, 0.5f}, {23, 15}, {3247, 1256}, 1.1}, + {{2.0f, 1.5f}, {232, 144}, {6243, 1241}, 1.2}, + {{3.0f, 2.5f}, {17, 242}, {15, 2323}, 1.3} + }; + + constexpr UnsignedShort indexData[] { + 2555, 3241, 1, 0, 1, 0 + }; + + Containers::ArrayView indexView; + MeshIndexData indices; + if(data.indexed) { + indexView = indexData; + indices = MeshIndexData{Containers::arrayView(indexData).suffix(2)}; + } + + MeshData meshData{MeshPrimitive::TriangleFan, + {}, indexView, indices, + {}, vertexData, { + /* Test all attribute type sizes (2, 4, 8) for endian swapping in + the MagnumImporter / MagnumSceneConverter plugins */ + MeshAttributeData{MeshAttribute::Position, + Containers::StridedArrayView1D{vertexData, &vertexData[0].position, 3, sizeof(SerializedVertex)}}, + MeshAttributeData{MeshAttribute::TextureCoordinates, + Containers::StridedArrayView1D{vertexData, &vertexData[0].textureCoordinates, 3, sizeof(SerializedVertex)}}, + /* Test array attribs */ + MeshAttributeData{meshAttributeCustom(23), + VertexFormat::UnsignedShort, 2, + Containers::StridedArrayView1D{vertexData, &vertexData[0].props[0], 3, sizeof(SerializedVertex)}}, + /* Test offset-only attribs as well */ + MeshAttributeData{meshAttributeCustom(14), VertexFormat::Double, + 16, 3, sizeof(SerializedVertex)} + }}; + + Containers::Array blob = meshData.serialize(); + CORRADE_COMPARE_AS((std::string{blob.data(), blob.size()}), + Utility::Directory::join(TRADE_TEST_DIR, std::string{data.filePrefix} + BlobFileSuffix), + TestSuite::Compare::StringToFile); +} + +void MeshDataTest::serializeEmpty() { + MeshData meshData{MeshPrimitive::Edges, 1256}; + + Containers::Array blob = meshData.serialize(); + CORRADE_COMPARE_AS((std::string{blob.data(), blob.size()}), + Utility::Directory::join(TRADE_TEST_DIR, std::string{"mesh-empty"} + BlobFileSuffix), + TestSuite::Compare::StringToFile); +} + +void MeshDataTest::serializeIntoTooSmall() { + constexpr UnsignedInt indexData[]{0, 1, 0}; + + MeshData meshData{MeshPrimitive::Faces, + {}, indexData, MeshIndexData{indexData}, 2}; + + std::ostringstream out; + Error redirectError{&out}; + char blob[sizeof(void*) == 4 ? 59 : 75]; + meshData.serializeInto(blob); + if(sizeof(void*) == 4) CORRADE_COMPARE(out.str(), + "Trade::MeshData::serializeInto(): data too small, expected at least 60 bytes but got 59\n"); + else CORRADE_COMPARE(out.str(), + "Trade::MeshData::serializeInto(): data too small, expected at least 76 bytes but got 75\n"); +} + +void MeshDataTest::deserialize() { + auto&& data = SerializeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array blob = Utility::Directory::read(Utility::Directory::join(TRADE_TEST_DIR, std::string{data.filePrefix} + BlobFileSuffix)); + + Containers::Optional meshData = MeshData::deserialize(blob); + CORRADE_VERIFY(meshData); + CORRADE_COMPARE(meshData->attributeCount(), 4); + CORRADE_COMPARE(meshData->vertexCount(), 3); + CORRADE_COMPARE(meshData->indexDataFlags(), DataFlag::Mutable); + CORRADE_COMPARE(meshData->vertexDataFlags(), DataFlag::Mutable); + + CORRADE_COMPARE(meshData->attributeName(0), MeshAttribute::Position); + CORRADE_COMPARE(meshData->attributeFormat(0), VertexFormat::Vector2); + CORRADE_COMPARE(meshData->attributeOffset(0), 0); + CORRADE_COMPARE(meshData->attributeStride(0), 24); + CORRADE_COMPARE(meshData->attributeArraySize(0), 0); + CORRADE_COMPARE_AS(meshData->attribute(0), + Containers::arrayView({ + {1.0f, 0.5f}, {2.0f, 1.5f}, {3.0f, 2.5f} + }), TestSuite::Compare::Container); + + CORRADE_COMPARE(meshData->attributeName(1), MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(meshData->attributeFormat(1), VertexFormat::Vector2ub); + CORRADE_COMPARE(meshData->attributeOffset(1), 8); + CORRADE_COMPARE(meshData->attributeStride(1), 24); + CORRADE_COMPARE(meshData->attributeArraySize(1), 0); + CORRADE_COMPARE_AS(meshData->attribute(1), + Containers::arrayView({ + {23, 15}, {232, 144}, {17, 242} + }), TestSuite::Compare::Container); + + CORRADE_COMPARE(meshData->attributeName(2), meshAttributeCustom(23)); + CORRADE_COMPARE(meshData->attributeFormat(2), VertexFormat::UnsignedShort); + CORRADE_COMPARE(meshData->attributeOffset(2), 10); + CORRADE_COMPARE(meshData->attributeStride(2), 24); + CORRADE_COMPARE(meshData->attributeArraySize(2), 2); + CORRADE_COMPARE_AS((meshData->attribute(2).transposed<0, 1>()[0]), + Containers::arrayView({3247, 6243, 15}), TestSuite::Compare::Container); + CORRADE_COMPARE_AS((meshData->attribute(2).transposed<0, 1>()[1]), + Containers::arrayView({1256, 1241, 2323}), TestSuite::Compare::Container); + + CORRADE_COMPARE(meshData->attributeName(3), meshAttributeCustom(14)); + CORRADE_COMPARE(meshData->attributeFormat(3), VertexFormat::Double); + CORRADE_COMPARE(meshData->attributeOffset(3), 16); + CORRADE_COMPARE(meshData->attributeStride(3), 24); + CORRADE_COMPARE(meshData->attributeArraySize(3), 0); + CORRADE_COMPARE_AS(meshData->attribute(3), + Containers::arrayView({ + 1.1, 1.2, 1.3 + }), TestSuite::Compare::Container); + + if(data.indexed) { + CORRADE_VERIFY(meshData->isIndexed()); + CORRADE_COMPARE(meshData->indexCount(), 4); + CORRADE_COMPARE(meshData->indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(meshData->indexOffset(), 4); + CORRADE_COMPARE_AS(meshData->indices(), + Containers::arrayView({1, 0, 1, 0}), + TestSuite::Compare::Container); + } else CORRADE_VERIFY(!meshData->isIndexed()); + + /* Constant data should not have mutable flags set. Test just basics + otherwise, as all this should be mostly handled by the same code. */ + meshData = MeshData::deserialize(Containers::arrayView(blob)); + CORRADE_VERIFY(meshData); + CORRADE_COMPARE(meshData->attributeCount(), 4); + CORRADE_COMPARE(meshData->vertexCount(), 3); + CORRADE_COMPARE(meshData->indexDataFlags(), DataFlags{}); + CORRADE_COMPARE(meshData->vertexDataFlags(), DataFlags{}); + if(data.indexed) { + CORRADE_VERIFY(meshData->isIndexed()); + CORRADE_COMPARE(meshData->indexCount(), 4); + } +} + +void MeshDataTest::deserializeInvalid() { + auto&& data = DeserializeInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array blob = Utility::Directory::read(Utility::Directory::join(TRADE_TEST_DIR, std::string{"mesh"} + BlobFileSuffix)); + CORRADE_VERIFY(blob); + + Containers::ArrayView view = blob; + if(data.size) view = view.prefix(data.size); + if(data.replace) Utility::copy(data.replace, view.slice(data.offset, data.offset + data.replace.size())); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!MeshData::deserialize(view)); + CORRADE_COMPARE(out.str(), + Utility::formatString("Trade::{}\n", data.message)); +} + }}}} CORRADE_TEST_MAIN(Magnum::Trade::Test::MeshDataTest) diff --git a/src/Magnum/Trade/Test/mesh-be32.blob b/src/Magnum/Trade/Test/mesh-be32.blob new file mode 100644 index 0000000000..e7ce8a76f0 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-be32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-be64.blob b/src/Magnum/Trade/Test/mesh-be64.blob new file mode 100644 index 0000000000..80909c72b6 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-be64.blob differ diff --git a/src/Magnum/Trade/Test/mesh-empty-be32.blob b/src/Magnum/Trade/Test/mesh-empty-be32.blob new file mode 100644 index 0000000000..925d22b98f Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-empty-be32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-empty-be64.blob b/src/Magnum/Trade/Test/mesh-empty-be64.blob new file mode 100644 index 0000000000..3e4c868638 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-empty-be64.blob differ diff --git a/src/Magnum/Trade/Test/mesh-empty-le32.blob b/src/Magnum/Trade/Test/mesh-empty-le32.blob new file mode 100644 index 0000000000..a4e55832c9 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-empty-le32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-empty-le64.blob b/src/Magnum/Trade/Test/mesh-empty-le64.blob new file mode 100644 index 0000000000..4b78520f2d Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-empty-le64.blob differ diff --git a/src/Magnum/Trade/Test/mesh-le32.blob b/src/Magnum/Trade/Test/mesh-le32.blob new file mode 100644 index 0000000000..f0869ebee9 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-le32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-le64.blob b/src/Magnum/Trade/Test/mesh-le64.blob new file mode 100644 index 0000000000..909d364db1 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-le64.blob differ diff --git a/src/Magnum/Trade/Test/mesh-nonindexed-be32.blob b/src/Magnum/Trade/Test/mesh-nonindexed-be32.blob new file mode 100644 index 0000000000..c7b11350ae Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-nonindexed-be32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-nonindexed-be64.blob b/src/Magnum/Trade/Test/mesh-nonindexed-be64.blob new file mode 100644 index 0000000000..6ad77013ca Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-nonindexed-be64.blob differ diff --git a/src/Magnum/Trade/Test/mesh-nonindexed-le32.blob b/src/Magnum/Trade/Test/mesh-nonindexed-le32.blob new file mode 100644 index 0000000000..2a2485d5e6 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-nonindexed-le32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-nonindexed-le64.blob b/src/Magnum/Trade/Test/mesh-nonindexed-le64.blob new file mode 100644 index 0000000000..de09529b16 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-nonindexed-le64.blob differ