From f6872bb7e3d4c7012d890ea154c295efa8298eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 15 Apr 2020 20:25:23 +0200 Subject: [PATCH] Trade: serialization for MeshData. At first I attempted to make the whole thing reinterpret_cast-able from a blob of memory (i.e., truly zero-overhead), but while that sounded cool and all, it moved the overhead to basically all other code -- each function had to special-case access to attribute/vertex/index data as the pointers were no longer pointers, the binary representation had various weird unexplainable gaps ("here an array deleter is stored, set that to null and don't ask"), release*() functions got more complicated and when I got to issues with move construction/assignment I knew this was not the right path. Now the MeshData internals are packed to a much more compact representation (with the first attempt it was 128 bytes, now it's just 64) and the serialization doesn't make everything else slower, more complex or harder to test, which is a win. --- src/Magnum/Trade/MeshData.cpp | 150 +++++++++ src/Magnum/Trade/MeshData.h | 97 ++++++ src/Magnum/Trade/Test/.gitattributes | 10 + src/Magnum/Trade/Test/CMakeLists.txt | 19 +- src/Magnum/Trade/Test/MeshDataTest.cpp | 293 ++++++++++++++++++ src/Magnum/Trade/Test/mesh-be32.blob | Bin 0 -> 212 bytes src/Magnum/Trade/Test/mesh-be64.blob | Bin 0 -> 244 bytes src/Magnum/Trade/Test/mesh-empty-be32.blob | Bin 0 -> 48 bytes src/Magnum/Trade/Test/mesh-empty-be64.blob | Bin 0 -> 64 bytes src/Magnum/Trade/Test/mesh-empty-le32.blob | Bin 0 -> 48 bytes src/Magnum/Trade/Test/mesh-empty-le64.blob | Bin 0 -> 64 bytes src/Magnum/Trade/Test/mesh-le32.blob | Bin 0 -> 212 bytes src/Magnum/Trade/Test/mesh-le64.blob | Bin 0 -> 244 bytes .../Trade/Test/mesh-nonindexed-be32.blob | Bin 0 -> 200 bytes .../Trade/Test/mesh-nonindexed-be64.blob | Bin 0 -> 232 bytes .../Trade/Test/mesh-nonindexed-le32.blob | Bin 0 -> 200 bytes .../Trade/Test/mesh-nonindexed-le64.blob | Bin 0 -> 232 bytes 17 files changed, 568 insertions(+), 1 deletion(-) create mode 100644 src/Magnum/Trade/Test/.gitattributes create mode 100644 src/Magnum/Trade/Test/mesh-be32.blob create mode 100644 src/Magnum/Trade/Test/mesh-be64.blob create mode 100644 src/Magnum/Trade/Test/mesh-empty-be32.blob create mode 100644 src/Magnum/Trade/Test/mesh-empty-be64.blob create mode 100644 src/Magnum/Trade/Test/mesh-empty-le32.blob create mode 100644 src/Magnum/Trade/Test/mesh-empty-le64.blob create mode 100644 src/Magnum/Trade/Test/mesh-le32.blob create mode 100644 src/Magnum/Trade/Test/mesh-le64.blob create mode 100644 src/Magnum/Trade/Test/mesh-nonindexed-be32.blob create mode 100644 src/Magnum/Trade/Test/mesh-nonindexed-be64.blob create mode 100644 src/Magnum/Trade/Test/mesh-nonindexed-le32.blob create mode 100644 src/Magnum/Trade/Test/mesh-nonindexed-le64.blob diff --git a/src/Magnum/Trade/MeshData.cpp b/src/Magnum/Trade/MeshData.cpp index a4a949bc2..24da51c35 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 d3946bf28..357a5c207 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,54 @@ 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 +Magnum's memory-mappable serialization 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 in the original memory view. The binary representation begins with +@ref DataChunkHeader of type @ref DataChunkType::Mesh and type version +@cpp 0 @ce, the rest is defined like below, with bitness and endianness +matching 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 +770,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 +1848,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/.gitattributes b/src/Magnum/Trade/Test/.gitattributes new file mode 100644 index 000000000..6f9689e39 --- /dev/null +++ b/src/Magnum/Trade/Test/.gitattributes @@ -0,0 +1,10 @@ +# You have to add the following to your .git/config or global +# ~/.gitconfig to make the binary diffs work (without the comment +# character, of course): +# +# [diff "hex"] +# textconv = hexdump -v -C +# binary = true +# + +*.blob binary diff=hex diff --git a/src/Magnum/Trade/Test/CMakeLists.txt b/src/Magnum/Trade/Test/CMakeLists.txt index b05842bd8..49dd18e15 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 301d305f4..918376e4f 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() { @@ -2963,6 +3069,193 @@ 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' +}; + +void MeshDataTest::serialize() { + auto&& data = SerializeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* Clang on iOS and Android doesn't like constexpr here */ + constexpr struct Vertex { + Vector2 position; + Vector2ub textureCoordinates; + UnsignedShort props[2]; + /* I'd use UnsignedShort:16 here but (at least on Android) the bytes + get random values, breaking the test. On iOS and Android that would + also make the compiler complain about constexpr, and finally MSVC + 2015 chokes on the : if this is an inline struct. */ + UnsignedShort _padding; + Double weight; + } vertexData[] { + {{1.0f, 0.5f}, {23, 15}, {3247, 1256}, 0, 1.1}, + {{2.0f, 1.5f}, {232, 144}, {6243, 1241}, 0, 1.2}, + {{3.0f, 2.5f}, {17, 242}, {15, 2323}, 0, 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(Vertex)}}, + MeshAttributeData{MeshAttribute::TextureCoordinates, + Containers::StridedArrayView1D{vertexData, &vertexData[0].textureCoordinates, 3, sizeof(Vertex)}}, + /* Test array attribs */ + MeshAttributeData{meshAttributeCustom(23), + VertexFormat::UnsignedShort, 2, + Containers::StridedArrayView1D{vertexData, &vertexData[0].props[0], 3, sizeof(Vertex)}}, + /* Test offset-only attribs as well */ + MeshAttributeData{meshAttributeCustom(14), VertexFormat::Double, + 16, 3, sizeof(Vertex)} + }}; + + 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 0000000000000000000000000000000000000000..e7ce8a76f0b850c5ed292ab5431f135dc45aa659 GIT binary patch literal 212 zcmZqR;^lJk&v9Y^0^ii)3?Ov{h*^M`8Hm}LAZ!qy2Z%j@n2&*x5l8{$B|sDy2r{su zh;aZ#IU2;_VnAaUxPX|sfe$VQ(kQ_Bn`b2hNC=4S8-N(d6zAtz{{m>E{l}RgFv|fb oW`BTz;l%`rJxfd>%tF)%WM*vt$P zAPNoy8CWsIIe@Yp4dQTdpwVE!1tggp_~7DDVFm%t-#jZBK*B(5-vGowrZ_*(`WHag w*?*h~0<#=|V)h3Z7+y?}NWKXY|7;8f4nPS91qKGePYnFRoIvp}XF%XA0C_+jVE_OC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..925d22b98ff894173ab4f5f5e94895fcd148c3d6 GIT binary patch literal 48 kcmZqR;^lJk&v9Y^0^ii)3?O9yVlyzXyZ{nhU?vg)0HM$XlmGw# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3e4c868638bf36bd0960ec4dc008e111af122be2 GIT binary patch literal 64 kcmZqR;^lJk_ir0jfIJYN2Z%j@m=A~Gf>=_XFVh;e|`yOEc literal 0 HcmV?d00001 diff --git a/src/Magnum/Trade/Test/mesh-le64.blob b/src/Magnum/Trade/Test/mesh-le64.blob new file mode 100644 index 0000000000000000000000000000000000000000..909d364db17443075e427f1ac7ee681c61ba9b57 GIT binary patch literal 244 zcmZqR;^lJk@pobX0^ii)j4vPt5U>C-GZ3=_F%ysn;)4JWgktc3(tJRg5ePwQC7@y; zN)U)yk;FNmav)l~0j8dbfeR`Q($5DMXJ8P3ivQ+Z$-@X_gD?X_gFTS27w2Ek^8)Cg wSu;W4BS_4Ff#HDtiwVgRH-X~DVDK3z?f?{5a1i{&&mhdnz;N~q2z;>z0Ds;dVE_OC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c7b11350ae3569849f810451064728f380252956 GIT binary patch literal 200 zcmZqR;^lJk&v9Y^0^ii)3?Ov^#0COpAZ7<*77zytJb*MG10y4d%gi7F6=h%$WMD-R z;{eKVG>F5+n1FmPke&uUxEM&YfPDi4gFTQT&d;;{1<)q@k267FmIF}C{s05RiwP3R eH$mc`jlsYHDB++0wD1!Hzc43I{L2{-I12#mf*gbZ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6ad77013ca67d301c497c12309796f46b8c6dc55 GIT binary patch literal 232 zcmZqR;^lJk_i$*@G&qlg2b5_Bv7OU8CWsI zIe;oS8pPq^Oi&$MApH$|aB-MH0`?6I4E8{tI6u$&7eH6of1C*dvmAh8_6L9togk5X e6D0oG7z`YM5)KLs41%8+_=P!v;$O~yz*zu-B^-nR literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2a2485d5e628d3ea85a54ce75a71edc68b147436 GIT binary patch literal 200 zcmZqR;^lJ6@pobX0^ii)j1wRR5HJHVJD9}+CP9P;5c2^sBM^e*C4d+N1c8_pNsI%? z2VwCBm^vl~E+8MIhYv2sz#stRGcYvR0||R^{`EXBfL6_#2?8HMVh#)p2kc)=NS3$> e6gLKg&p>epptypA;3s|tVNM2yvu8lyi#-7AHynfj literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..de09529b16027135432c28d07c285db4a9f44d9b GIT binary patch literal 232 zcmZqR;^lJk@pobX0^ii)j29pV6fgs6b}*X-CIqKFpe#Nh%?N}bwGwby1_nVOn-xi% z11bli#T#JinHadB;voHeaB&6(0jM|wLxVk#X)n&dp63P78M9`Bz( literal 0 HcmV?d00001