From cdf71fa8bf66a5f57ae3f612e6827493b3f02f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 5 Nov 2022 14:33:03 +0100 Subject: [PATCH] DebugTools: new CompareMaterial class. Now that there will be a whole new MaterialTools library, I really need this helper. --- doc/changelog.dox | 2 + doc/snippets/CMakeLists.txt | 14 +- doc/snippets/debugtools-comparematerial.ansi | 16 + doc/snippets/debugtools-comparematerial.cpp | 69 +++ src/Magnum/DebugTools/CMakeLists.txt | 6 +- src/Magnum/DebugTools/CompareImage.h | 2 + src/Magnum/DebugTools/CompareMaterial.cpp | 395 ++++++++++++++++++ src/Magnum/DebugTools/CompareMaterial.h | 109 +++++ src/Magnum/DebugTools/Test/CMakeLists.txt | 2 + .../DebugTools/Test/CompareMaterialTest.cpp | 323 ++++++++++++++ 10 files changed, 930 insertions(+), 8 deletions(-) create mode 100644 doc/snippets/debugtools-comparematerial.ansi create mode 100644 doc/snippets/debugtools-comparematerial.cpp create mode 100644 src/Magnum/DebugTools/CompareMaterial.cpp create mode 100644 src/Magnum/DebugTools/CompareMaterial.h create mode 100644 src/Magnum/DebugTools/Test/CompareMaterialTest.cpp diff --git a/doc/changelog.dox b/doc/changelog.dox index 29e98365c..63be13d86 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -68,6 +68,8 @@ See also: - Added @ref DebugTools::ColorMap::coolWarmSmooth() and @ref DebugTools::ColorMap::coolWarmBent() (see [mosra/magnum#473](https://github.com/mosra/magnum/pull/473)) +- New @ref DebugTools::CompareMaterial comparator for convenient comparison + of @ref Trade::MaterialData instances @subsubsection changelog-latest-new-gl GL library diff --git a/doc/snippets/CMakeLists.txt b/doc/snippets/CMakeLists.txt index 84a485333..cb3f12ebf 100644 --- a/doc/snippets/CMakeLists.txt +++ b/doc/snippets/CMakeLists.txt @@ -132,14 +132,14 @@ if(MAGNUM_WITH_DEBUGTOOLS) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/configure.h) - # CompareImage documentation snippet. I need it executable so I can - # copy&paste the output to the documentation. Also not using - # corrade_add_test() because it shouldn't be run as part of CTest as it - # purposely fails. + # CompareImage / CompareMaterial documentation snippets. I need them + # executable so I can include the colored output in the documentation. + # Also not using corrade_add_test() because it shouldn't be run as part + # of CTest as it purposely fails. add_executable(debugtools-compareimage debugtools-compareimage.cpp) - target_link_libraries(debugtools-compareimage PRIVATE - MagnumDebugTools - MagnumTrade) + add_executable(debugtools-comparematerial debugtools-comparematerial.cpp) + target_link_libraries(debugtools-compareimage PRIVATE MagnumDebugTools) + target_link_libraries(debugtools-comparematerial PRIVATE MagnumDebugTools) target_include_directories(debugtools-compareimage PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) endif() diff --git a/doc/snippets/debugtools-comparematerial.ansi b/doc/snippets/debugtools-comparematerial.ansi new file mode 100644 index 000000000..612ca48ba --- /dev/null +++ b/doc/snippets/debugtools-comparematerial.ansi @@ -0,0 +1,16 @@ +Starting MaterialTest with 1 test cases... + FAIL [1] conversion() at …/debugtools-comparematerial.cpp:65 + Materials actual and expected have different layers. Actual (+) vs expected (-): +  -Types: PbrMetallicRoughness +  +Types: PbrMetallicRoughness|PbrClearCoat + Base layer: + BaseColor @ Vector4: {0.851206, 0.687386, 0.416013, 1} +  - DoubleSided @ Bool: false +  + DoubleSided @ Bool: true + Metalness @ Float: 0.603401 + Roughness @ Float: 0.105112 +  +Layer 1: +  + LayerName @ String: ClearCoat +  + LayerFactor @ Float: 0.02 +  + Roughness @ Float: 0.320856 +Finished MaterialTest with 1 errors out of 1 checks. diff --git a/doc/snippets/debugtools-comparematerial.cpp b/doc/snippets/debugtools-comparematerial.cpp new file mode 100644 index 000000000..0f2b6dafd --- /dev/null +++ b/doc/snippets/debugtools-comparematerial.cpp @@ -0,0 +1,69 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include + +#include "Magnum/Math/Color.h" +#include "Magnum/Trade/MaterialData.h" +#include "Magnum/DebugTools/CompareMaterial.h" + +using namespace Magnum; + +#define DOXYGEN_ELLIPSIS(...) __VA_ARGS__ + +struct MaterialTest: TestSuite::Tester { + explicit MaterialTest(); + + void conversion(); +}; + +MaterialTest::MaterialTest() { + addTests({&MaterialTest::conversion}); +} + +void MaterialTest::conversion() { + Trade::MaterialData actual{Trade::MaterialType::PbrClearCoat|Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColor, Color4{0.851206f, 0.687386f, 0.416013f}}, + {Trade::MaterialAttribute::Metalness, 0.603401f}, + {Trade::MaterialAttribute::Roughness, 0.105112f}, + {Trade::MaterialAttribute::DoubleSided, true}, + {Trade::MaterialLayer::ClearCoat}, + {Trade::MaterialAttribute::LayerFactor, 0.02f}, + {Trade::MaterialAttribute::Roughness, 0.320856f}, + }, {4, 7}}; + + Trade::MaterialData expected{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColor, Color4{0.851206f, 0.687386f, 0.416013f}}, + {Trade::MaterialAttribute::Metalness, 0.603401f}, + {Trade::MaterialAttribute::Roughness, 0.105112f}, + {Trade::MaterialAttribute::DoubleSided, false} + }}; + +/* [usage] */ +CORRADE_COMPARE_AS(actual, expected, DebugTools::CompareMaterial); +/* [usage] */ +} + +CORRADE_TEST_MAIN(MaterialTest) diff --git a/src/Magnum/DebugTools/CMakeLists.txt b/src/Magnum/DebugTools/CMakeLists.txt index 94e9de55f..30c38e4b4 100644 --- a/src/Magnum/DebugTools/CMakeLists.txt +++ b/src/Magnum/DebugTools/CMakeLists.txt @@ -97,11 +97,15 @@ endif() # Build the TestSuite-related functionality only if it is present find_package(Corrade COMPONENTS TestSuite) if(Corrade_TestSuite_FOUND AND MAGNUM_WITH_TRADE) + list(APPEND MagnumDebugTools_SRCS + CompareMaterial.cpp) + list(APPEND MagnumDebugTools_GracefulAssert_SRCS CompareImage.cpp) list(APPEND MagnumDebugTools_HEADERS - CompareImage.h) + CompareImage.h + CompareMaterial.h) endif() # Objects shared between main and test library diff --git a/src/Magnum/DebugTools/CompareImage.h b/src/Magnum/DebugTools/CompareImage.h index c9d3435cc..e1598b4fb 100644 --- a/src/Magnum/DebugTools/CompareImage.h +++ b/src/Magnum/DebugTools/CompareImage.h @@ -329,6 +329,8 @@ then autodetected from the passed type, with normalized formats preferred. In practice this means e.g. @ref Math::Vector2 "Math::Vector2" will be understood as @ref PixelFormat::RG8Unorm and there's currently no way to interpret it as @ref PixelFormat::RG8UI, for example. + +@see @ref CompareMaterial */ class CompareImage { public: diff --git a/src/Magnum/DebugTools/CompareMaterial.cpp b/src/Magnum/DebugTools/CompareMaterial.cpp new file mode 100644 index 000000000..77e2880cc --- /dev/null +++ b/src/Magnum/DebugTools/CompareMaterial.cpp @@ -0,0 +1,395 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "CompareMaterial.h" + +#include +#include +#include +#include /* max() that works with enums */ +#include + +#include "Magnum/Math/Matrix.h" +#include "Magnum/Math/Vector4.h" +#include "Magnum/Trade/MaterialData.h" + +namespace Corrade { namespace TestSuite { + +using namespace Magnum; + +namespace { + +/* Higher values mean bigger change */ +enum class MaterialState { + Same, + DifferentTypes, + DifferentAttributeValues, + DifferentAttributeTypes, + DifferentAttributes, + DifferentLayers +}; + +enum class AttributeState { + Same, + DifferentValue, + DifferentType, + OnlyInExpected, + OnlyInActual +}; + +} + +struct Comparator::State { + MaterialState materialState = MaterialState::Same; + + /* Second is attribute ID in the actual material (unless AttributeState is + OnlyInExpected), third is attribute ID in the expected material (unless + AttributeState is OnlyInActual). */ + Containers::Array> attributes; + /* Offsets into the attributes array for each layer. I.e., layer i is + stored in `attributes[layerOffsets[i]]` to + `attributes[layerOffsets[i + 1]]`. */ + Containers::Array layerOffsets; + + const Trade::MaterialData* actual{}; + const Trade::MaterialData* expected{}; +}; + +Comparator::Comparator(): _state{InPlaceInit} {} + +Comparator::~Comparator() = default; + +namespace { + +bool attributesEqual(const Trade::MaterialAttributeData& a, const Trade::MaterialAttributeData& b) { + #ifdef CORRADE_TARGET_GCC + #pragma GCC diagnostic push + #pragma GCC diagnostic error "-Wswitch" + #endif + CORRADE_INTERNAL_ASSERT(a.type() == b.type()); + switch(a.type()) { + #define _c(type) case Trade::MaterialAttributeType::type: \ + return a.value() == b.value(); + #define _ct(name, type) case Trade::MaterialAttributeType::name: \ + return a.value() == b.value(); + _ct(Bool, bool) + /* LCOV_EXCL_START */ + _c(Float) + _c(Deg) + _c(Rad) + _c(UnsignedInt) + _c(Int) + _c(UnsignedLong) + _c(Long) + _c(Vector2) + _c(Vector2ui) + _c(Vector2i) + _c(Vector3) + _c(Vector3ui) + _c(Vector3i) + _c(Vector4) + _c(Vector4ui) + _c(Vector4i) + _c(Matrix2x2) + _c(Matrix2x3) + _c(Matrix2x4) + _c(Matrix3x2) + _c(Matrix3x3) + _c(Matrix3x4) + _c(Matrix4x2) + _c(Matrix4x3) + /* LCOV_EXCL_STOP */ + _ct(Pointer, const void*) + _ct(MutablePointer, void*) + _ct(String, Containers::StringView) + _ct(TextureSwizzle, Trade::MaterialTextureSwizzle) + #undef _c + #undef _ct + case Trade::MaterialAttributeType::Buffer: + return Containers::StringView{Containers::arrayCast(a.value>())} == Containers::StringView{Containers::arrayCast(b.value>())}; + } + #ifdef CORRADE_TARGET_GCC + #pragma GCC diagnostic pop + #endif + + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +} + +TestSuite::ComparisonStatusFlags Comparator::operator()(const Trade::MaterialData& actual, const Trade::MaterialData& expected) { + _state->actual = &actual; + _state->expected = &expected; + + if(actual.types() != expected.types()) + _state->materialState = MaterialState::DifferentTypes; + + /* The layer offset array has one extra item for the last layer count */ + const std::size_t layerMax = Math::max(actual.layerCount(), expected.layerCount()); + _state->layerOffsets = Containers::Array{NoInit, layerMax + 1}; + + /* Go over all layers that are in both materials */ + std::size_t layer = 0; + for(const std::size_t layerMin = Math::min(actual.layerCount(), expected.layerCount()); layer != layerMin; ++layer) { + _state->layerOffsets[layer] = _state->attributes.size(); + UnsignedInt inActual = 0; + UnsignedInt inExpected = 0; + + /* Take the earliest-sorted attribute from either material */ + while(inActual != actual.attributeCount(layer) && inExpected != expected.attributeCount(layer)) { + if(actual.attributeName(layer, inActual) == expected.attributeName(layer, inExpected)) { + AttributeState attributeState; + if(actual.attributeType(layer, inActual) != expected.attributeType(layer, inExpected)) { + attributeState = AttributeState::DifferentType; + _state->materialState = Utility::max(_state->materialState, MaterialState::DifferentAttributeTypes); + } else if(!attributesEqual(actual.attributeData(layer, inActual), expected.attributeData(layer, inExpected))) { + attributeState = AttributeState::DifferentValue; + _state->materialState = Utility::max(_state->materialState, MaterialState::DifferentAttributeValues); + } else { + attributeState = AttributeState::Same; + } + + arrayAppend(_state->attributes, InPlaceInit, attributeState, inActual, inExpected); + ++inActual; + ++inExpected; + } else if(actual.attributeName(inActual) < expected.attributeName(inExpected)) { + arrayAppend(_state->attributes, InPlaceInit, AttributeState::OnlyInActual, inActual, ~UnsignedInt{}); + _state->materialState = Utility::max(_state->materialState, MaterialState::DifferentAttributes); + ++inActual; + } else if(actual.attributeName(inActual) > expected.attributeName(inExpected)) { + arrayAppend(_state->attributes, InPlaceInit, AttributeState::OnlyInExpected, ~UnsignedInt{}, inExpected); + _state->materialState = Utility::max(_state->materialState, MaterialState::DifferentAttributes); + ++inExpected; + } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + } + + /* Consume remaining leftover attributes in either. Only one of these + loops get entered. */ + while(inActual < actual.attributeCount(layer)) { + arrayAppend(_state->attributes, InPlaceInit, AttributeState::OnlyInActual, inActual, ~UnsignedInt{}); + _state->materialState = Utility::max(_state->materialState, MaterialState::DifferentAttributes); + ++inActual; + } + while(inExpected < expected.attributeCount(layer)) { + arrayAppend(_state->attributes, InPlaceInit, AttributeState::OnlyInExpected, ~UnsignedInt{}, inExpected); + _state->materialState = Utility::max(_state->materialState, MaterialState::DifferentAttributes); + ++inExpected; + } + } + + /* Go over remaining actual/expected layers which weren't in + expected/actual. Only one of these loops get entered. */ + for(; layer < actual.layerCount(); ++layer) { + _state->layerOffsets[layer] = _state->attributes.size(); + + for(UnsignedInt inActual = 0, inActualMax = actual.attributeCount(layer); inActual != inActualMax; ++inActual) + arrayAppend(_state->attributes, InPlaceInit, AttributeState::OnlyInActual, inActual, ~UnsignedInt{}); + + _state->materialState = Utility::max(_state->materialState, MaterialState::DifferentLayers); + } + for(; layer < expected.layerCount(); ++layer) { + _state->layerOffsets[layer] = _state->attributes.size(); + + for(UnsignedInt inExpected = 0, inExpectedMax = expected.attributeCount(layer); inExpected != inExpectedMax; ++inExpected) + arrayAppend(_state->attributes, InPlaceInit, AttributeState::OnlyInExpected, ~UnsignedInt{}, inExpected); + + _state->materialState = Utility::max(_state->materialState, MaterialState::DifferentLayers); + } + + CORRADE_INTERNAL_ASSERT(layer == _state->layerOffsets.size() - 1); + _state->layerOffsets[layer] = _state->attributes.size(); + + /** @todo If there's a large sequence of Same attributes, elide the middle + (mark it with SameEllipsis, e.g., and leave at most 3 before and 3 + after each difference), and skip them when printing unless Verbose is + set. Would become practically useful only once there's really a lot + material attributes in a single layer, currently not really. */ + + return _state->materialState == MaterialState::Same ? TestSuite::ComparisonStatusFlags{} : TestSuite::ComparisonStatusFlag::Failed; +} + +namespace { + +void printAttribute(Debug& out, const Trade::MaterialAttributeData& attribute, const AttributeState state, const bool isActual) { + out << Debug::newline << " "; + if(state == AttributeState::Same) + out << " "; + else if(isActual) + out << Debug::color(Debug::Color::Green) << "+"; + else + out << Debug::color(Debug::Color::Red) << "-"; + if(state == AttributeState::DifferentType || + state == AttributeState::DifferentValue) + out << Debug::resetColor; + + out << "" << attribute.name() << "@"; + + if(state == AttributeState::DifferentType) + out << Debug::color(isActual ? Debug::Color::Green : Debug::Color::Red); + out << Debug::packed << attribute.type(); + if(state == AttributeState::DifferentType) + out << Debug::resetColor; + out << Debug::nospace << ":"; + + if(state == AttributeState::DifferentType || + state == AttributeState::DifferentValue) + out << Debug::color(isActual ? Debug::Color::Green : Debug::Color::Red); + #ifdef CORRADE_TARGET_GCC + #pragma GCC diagnostic push + #pragma GCC diagnostic error "-Wswitch" + #endif + switch(attribute.type()) { + #define _c(type) case Trade::MaterialAttributeType::type: \ + out << Debug::packed << attribute.value(); \ + break; + #define _ct(name, type) case Trade::MaterialAttributeType::name: \ + out << Debug::packed << attribute.value(); \ + break; + _ct(Bool, bool) + /* LCOV_EXCL_START */ + _c(Float) + _c(Deg) + _c(Rad) + _c(UnsignedInt) + _c(Int) + _c(UnsignedLong) + _c(Long) + _c(Vector2) + _c(Vector2ui) + _c(Vector2i) + _c(Vector3) + _c(Vector3ui) + _c(Vector3i) + _c(Vector4) + _c(Vector4ui) + _c(Vector4i) + _c(Matrix2x2) + _c(Matrix2x3) + _c(Matrix2x4) + _c(Matrix3x2) + _c(Matrix3x3) + _c(Matrix3x4) + _c(Matrix4x2) + _c(Matrix4x3) + /* LCOV_EXCL_STOP */ + _ct(Pointer, const void*) + _ct(MutablePointer, void*) + _ct(String, Containers::StringView) + _ct(TextureSwizzle, Trade::MaterialTextureSwizzle) + #undef _c + #undef _ct + case Trade::MaterialAttributeType::Buffer: + out << Containers::arrayCast(attribute.value>()); + } + #ifdef CORRADE_TARGET_GCC + #pragma GCC diagnostic pop + #endif + + if(state != AttributeState::Same) + out << Debug::resetColor; +} + +} + +void Comparator::printMessage(const TestSuite::ComparisonStatusFlags, Debug& out, const Containers::StringView actual, const Containers::StringView expected) const { + out << "Materials" << actual << "and" << expected; + + if(_state->materialState == MaterialState::DifferentLayers) { + out << "have different layers."; + } else if(_state->materialState == MaterialState::DifferentAttributes) { + out << "have different attributes."; + } else if(_state->materialState == MaterialState::DifferentAttributeTypes) { + out << "have different attribute types."; + } else if(_state->materialState == MaterialState::DifferentAttributeValues) { + out << "have different attribute values."; + } else if(_state->materialState == MaterialState::DifferentTypes) { + out << "have different types."; + } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + + out << Debug::color(Debug::Color::Green) << "Actual (+)" + << Debug::resetColor << "vs" << Debug::color(Debug::Color::Red) + << "expected (-)" << Debug::resetColor << Debug::nospace << ":"; + + /* Print the type, or both if they differ */ + /** @todo Or maybe if it's MaterialState::DifferentType print just this and + skip the attributes? Because everything else that's printed is the + same... */ + if(_state->expected->types()) { + out << Debug::newline << " "; + if(_state->expected->types() != _state->actual->types()) + out << Debug::color(Debug::Color::Red) << "-" << Debug::nospace; + else + out << ""; + if(_state->actual->types() && _state->expected->types() != _state->actual->types()) + out << Debug::resetColor; + out << "Types:"; + if(_state->actual->types() && _state->expected->types() != _state->actual->types()) + out << Debug::color(Debug::Color::Red); + out << Debug::packed << _state->expected->types() << Debug::resetColor; + } + if(_state->actual->types() && _state->actual->types() != _state->expected->types()) { + out << Debug::newline << " " << Debug::color(Debug::Color::Green) << "+" << Debug::nospace; + if(_state->expected->types()) out << Debug::resetColor; + out << "Types:"; + if(_state->expected->types()) out << Debug::color(Debug::Color::Green); + out << Debug::packed << _state->actual->types() << Debug::resetColor; + } + + /* Print content of both materials, interleaved, with layers and attributes + that differ marked with +/- */ + for(UnsignedInt layer = 0, layerMax = _state->layerOffsets.size() - 1; layer != layerMax; ++layer) { + /* Show layer header only if there's more than one and the base + layer isn't empty */ + if(_state->layerOffsets.size() != 2 || _state->layerOffsets[1] != 0) { + out << Debug::newline << " "; + if(layer >= _state->actual->layerCount()) + out << Debug::color(Debug::Color::Red) << "-" << Debug::nospace; + else if(layer >= _state->expected->layerCount()) + out << Debug::color(Debug::Color::Green) << "+" << Debug::nospace; + else + out << ""; + if(layer == 0) + out << "Base layer:"; + else + out << "Layer" << layer << Debug::nospace << ":"; + if(layer >= _state->actual->layerCount() || layer >= _state->expected->layerCount()) + out << Debug::resetColor; + } + + /* Print attribute values indented only if there's more than one + layer */ + for(UnsignedInt id = _state->layerOffsets[layer], idMax = _state->layerOffsets[layer + 1]; id != idMax; ++id) { + const Containers::Triple attribute = _state->attributes[id]; + const AttributeState state =attribute.first(); + if(state != AttributeState::OnlyInActual) + printAttribute(out, _state->expected->attributeData(layer, attribute.third()), state, false); + if(state != AttributeState::Same && state != AttributeState::OnlyInExpected) { + printAttribute(out, _state->actual->attributeData(layer, attribute.second()), state, true); + } + } + } +} + +}} diff --git a/src/Magnum/DebugTools/CompareMaterial.h b/src/Magnum/DebugTools/CompareMaterial.h new file mode 100644 index 000000000..bdfcfc340 --- /dev/null +++ b/src/Magnum/DebugTools/CompareMaterial.h @@ -0,0 +1,109 @@ +#ifndef Magnum_DebugTools_CompareMaterial_h +#define Magnum_DebugTools_CompareMaterial_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file + * @brief Class @ref Magnum::DebugTools::CompareMaterial + * @m_since_latest + */ + +#include +#include + +#include "Magnum/Magnum.h" +#include "Magnum/DebugTools/visibility.h" +#include "Magnum/Trade/Trade.h" + +namespace Magnum { namespace DebugTools { + +class CompareMaterial; + +}} + +#ifndef DOXYGEN_GENERATING_OUTPUT +/* If Doxygen sees this, all @ref Corrade::TestSuite links break (prolly + because the namespace is undocumented in this project) */ +namespace Corrade { namespace TestSuite { + +template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator { + public: + explicit Comparator(); + ~Comparator(); + + ComparisonStatusFlags operator()(const Magnum::Trade::MaterialData& actual, const Magnum::Trade::MaterialData& expected); + + void printMessage(ComparisonStatusFlags flags, Utility::Debug& out, Containers::StringView actual, Containers::StringView expected) const; + + private: + struct State; + Containers::Pointer _state; +}; + +}} +#endif + +namespace Magnum { namespace DebugTools { + +/** +@brief Material comparator for @ref Corrade::TestSuite +@m_since_latest + +Compares @ref Trade::MaterialData instances, printing the differences in the +two if they have a different type, different layer count, different attributes, +and different type or different value of the same attribute. Pass the +comparator to @ref CORRADE_COMPARE_AS() along with an actual and expected +material: + +@snippet debugtools-comparematerial.cpp usage + +Based on actual materials used, in case of a comparison failure the comparator +can give for example the following output: + +@m_class{m-console-wrap} + +@include debugtools-comparematerial.ansi + +All @ref Trade::MaterialAttributeType are supported. + +@see @ref CompareImage +*/ +class MAGNUM_DEBUGTOOLS_EXPORT CompareMaterial { + public: + explicit CompareMaterial(); + + #ifndef DOXYGEN_GENERATING_OUTPUT + TestSuite::Comparator& comparator() { + return _c; + } + #endif + + private: + TestSuite::Comparator _c; +}; + +}} + +#endif diff --git a/src/Magnum/DebugTools/Test/CMakeLists.txt b/src/Magnum/DebugTools/Test/CMakeLists.txt index 6e9e3db21..ee2fdf909 100644 --- a/src/Magnum/DebugTools/Test/CMakeLists.txt +++ b/src/Magnum/DebugTools/Test/CMakeLists.txt @@ -100,6 +100,8 @@ if(MAGNUM_WITH_TRADE) add_dependencies(DebugToolsCompareImageTest TgaImporter) endif() endif() + + corrade_add_test(DebugToolsCompareMaterialTest CompareMaterialTest.cpp LIBRARIES MagnumDebugToolsTestLib) endif() if(MAGNUM_TARGET_GL) diff --git a/src/Magnum/DebugTools/Test/CompareMaterialTest.cpp b/src/Magnum/DebugTools/Test/CompareMaterialTest.cpp new file mode 100644 index 000000000..7039da249 --- /dev/null +++ b/src/Magnum/DebugTools/Test/CompareMaterialTest.cpp @@ -0,0 +1,323 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include + +#include "Magnum/DebugTools/CompareMaterial.h" +#include "Magnum/Math/Color.h" +#include "Magnum/Trade/MaterialData.h" + +namespace Magnum { namespace DebugTools { namespace Test { namespace { + +struct CompareMaterialTest: TestSuite::Tester { + explicit CompareMaterialTest(); + + void same(); + void different(); + void differentReverse(); +}; + +using namespace Containers::Literals; +using namespace Math::Literals; + +const struct { + const char* name; + Trade::MaterialData material; +} SameData[]{ + {"empty", Trade::MaterialData{{}, {}}}, + {"empty with types", Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness|Trade::MaterialType::PbrClearCoat, {}}}, + {"base attributes", Trade::MaterialData{Trade::MaterialType::Phong, { + {Trade::MaterialAttribute::DiffuseColor, 0x556699aa_rgbaf}, + {Trade::MaterialAttribute::NormalTexture, 5u}, + {"name", "hello"_s}, + }}}, + {"layers", Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness|Trade::MaterialType::PbrClearCoat, { + {Trade::MaterialAttribute::NormalTexture, 5u}, + {Trade::MaterialLayer::ClearCoat}, + {Trade::MaterialAttribute::LayerFactor, 0.76f}, + {"name", "hello"_s}, + }, {1, 3, 4}}}, +}; + +const struct { + const char* name; + Trade::MaterialData actual; + Trade::MaterialData expected; + const char* message; + const char* messageReverse; +} DifferentData[]{ + {"empty, different types", + Trade::MaterialData{Trade::MaterialType::Flat, {}}, + Trade::MaterialData{Trade::MaterialType::PbrClearCoat|Trade::MaterialType::PbrMetallicRoughness, {}}, + "Materials a and b have different types. Actual (+) vs expected (-):\n" + " -Types: PbrMetallicRoughness|PbrClearCoat\n" + " +Types: Flat\n", + "Materials b and a have different types. Actual (+) vs expected (-):\n" + " -Types: Flat\n" + " +Types: PbrMetallicRoughness|PbrClearCoat\n"}, + {"different types", + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff00ffff_rgbaf}, + }}, + Trade::MaterialData{Trade::MaterialType::PbrClearCoat|Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColor, 0xff00ffff_rgbaf}, + }}, + "Materials a and b have different types. Actual (+) vs expected (-):\n" + " -Types: PbrMetallicRoughness|PbrClearCoat\n" + " +Types: Flat\n" + " Base layer:\n" + " BaseColor @ Vector4: {1, 0, 1, 1}\n", + "Materials b and a have different types. Actual (+) vs expected (-):\n" + " -Types: Flat\n" + " +Types: PbrMetallicRoughness|PbrClearCoat\n" + " Base layer:\n" + " BaseColor @ Vector4: {1, 0, 1, 1}\n"}, + {"different types, one empty", + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff00ffff_rgbaf} + }}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::BaseColor, 0xff00ffff_rgbaf} + }}, + "Materials a and b have different types. Actual (+) vs expected (-):\n" + " +Types: Flat\n" + " Base layer:\n" + " BaseColor @ Vector4: {1, 0, 1, 1}\n", + "Materials b and a have different types. Actual (+) vs expected (-):\n" + " -Types: Flat\n" + " Base layer:\n" + " BaseColor @ Vector4: {1, 0, 1, 1}\n"}, + {"different attributes", + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::DoubleSided, true}, + {Trade::MaterialAttribute::NormalTexture, 5u}, + {Trade::MaterialAttribute::NormalTextureScale, 0.5f}, + {Trade::MaterialAttribute::OcclusionTexture, 3u}, + }}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::Metalness, 5.5f}, + {Trade::MaterialAttribute::NormalTexture, 5u}, + {Trade::MaterialAttribute::NormalTextureLayer, 2u}, + }}, + "Materials a and b have different attributes. Actual (+) vs expected (-):\n" + " Types: PbrMetallicRoughness\n" + " Base layer:\n" + " + DoubleSided @ Bool: true\n" + " - Metalness @ Float: 5.5\n" + " NormalTexture @ UnsignedInt: 5\n" + " - NormalTextureLayer @ UnsignedInt: 2\n" + " + NormalTextureScale @ Float: 0.5\n" + " + OcclusionTexture @ UnsignedInt: 3\n", + "Materials b and a have different attributes. Actual (+) vs expected (-):\n" + " Types: PbrMetallicRoughness\n" + " Base layer:\n" + " - DoubleSided @ Bool: true\n" + " + Metalness @ Float: 5.5\n" + " NormalTexture @ UnsignedInt: 5\n" + " + NormalTextureLayer @ UnsignedInt: 2\n" + " - NormalTextureScale @ Float: 0.5\n" + " - OcclusionTexture @ UnsignedInt: 3\n"}, + {"different attribute types", + Trade::MaterialData{{}, { + {"pointer", reinterpret_cast(0xdead)}, + {"integer", 5u}, + {"scale", 0.5f}, + }}, + Trade::MaterialData{{}, { + {"pointer", reinterpret_cast(0xdead)}, + {"integer", 5}, + {"scale", "small"}, + }}, + "Materials a and b have different attribute types. Actual (+) vs expected (-):\n" + " Base layer:\n" + " - integer @ Int: 5\n" + " + integer @ UnsignedInt: 5\n" + " - pointer @ Pointer: 0xdead\n" + " + pointer @ MutablePointer: 0xdead\n" + " - scale @ String: small\n" + " + scale @ Float: 0.5\n", + "Materials b and a have different attribute types. Actual (+) vs expected (-):\n" + " Base layer:\n" + " - integer @ UnsignedInt: 5\n" + " + integer @ Int: 5\n" + " - pointer @ MutablePointer: 0xdead\n" + " + pointer @ Pointer: 0xdead\n" + " - scale @ Float: 0.5\n" + " + scale @ String: small\n"}, + {"different attribute values", + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::NormalTexture, 5u}, + {Trade::MaterialAttribute::NormalTextureSwizzle, Trade::MaterialTextureSwizzle::RGB}, + {"buffer", Containers::ArrayView{"\x56\x78\x22"}}, + {"pointer", reinterpret_cast(0xbeef)}, + {"pointerMutable", reinterpret_cast(0xdead)}, + }}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::Metalness, 5.5f}, + {Trade::MaterialAttribute::NormalTexture, 5u}, + {Trade::MaterialAttribute::NormalTextureSwizzle, Trade::MaterialTextureSwizzle::RG}, + {"buffer", Containers::ArrayView{"\x56\x78\x22"}}, + {"pointer", reinterpret_cast(0xbeef)}, + {"pointerMutable", reinterpret_cast(0xdead)}, + }}, + "Materials a and b have different attribute values. Actual (+) vs expected (-):\n" + " Base layer:\n" + " - Metalness @ Float: 5.5\n" + " + Metalness @ Float: 0.3\n" + " NormalTexture @ UnsignedInt: 5\n" + " - NormalTextureSwizzle @ TextureSwizzle: RG\n" + " + NormalTextureSwizzle @ TextureSwizzle: RGB\n" + " buffer @ Buffer: {86, 120, 34, 0}\n" + " pointer @ Pointer: 0xbeef\n" + " pointerMutable @ MutablePointer: 0xdead\n", + "Materials b and a have different attribute values. Actual (+) vs expected (-):\n" + " Base layer:\n" + " - Metalness @ Float: 0.3\n" + " + Metalness @ Float: 5.5\n" + " NormalTexture @ UnsignedInt: 5\n" + " - NormalTextureSwizzle @ TextureSwizzle: RGB\n" + " + NormalTextureSwizzle @ TextureSwizzle: RG\n" + " buffer @ Buffer: {86, 120, 34, 0}\n" + " pointer @ Pointer: 0xbeef\n" + " pointerMutable @ MutablePointer: 0xdead\n"}, + {"different attributes in layers", + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::DoubleSided, true}, + {Trade::MaterialAttribute::NormalTexture, 5u}, + {Trade::MaterialAttribute::NormalTextureScale, 0.5f}, + {Trade::MaterialAttribute::OcclusionTexture, 3u}, + {"texturePointer", reinterpret_cast(0xdead)}, + }, {3, 5}}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::DoubleSided, false}, + {Trade::MaterialAttribute::NormalTexture, 5u}, + {Trade::MaterialAttribute::OcclusionTexture, 3u}, + {"texturePointer", reinterpret_cast(0xdead)}, + {Trade::MaterialAttribute::NormalTextureLayer, 2u}, + }, {2, 4, 5}}, + "Materials a and b have different layers. Actual (+) vs expected (-):\n" + " Base layer:\n" + " - DoubleSided @ Bool: false\n" + " + DoubleSided @ Bool: true\n" + " NormalTexture @ UnsignedInt: 5\n" + " + NormalTextureScale @ Float: 0.5\n" + " Layer 1:\n" + " OcclusionTexture @ UnsignedInt: 3\n" + " - texturePointer @ Pointer: 0xdead\n" + " + texturePointer @ MutablePointer: 0xdead\n" + " -Layer 2:\n" + " - NormalTextureLayer @ UnsignedInt: 2\n", + "Materials b and a have different layers. Actual (+) vs expected (-):\n" + " Base layer:\n" + " - DoubleSided @ Bool: true\n" + " + DoubleSided @ Bool: false\n" + " NormalTexture @ UnsignedInt: 5\n" + " - NormalTextureScale @ Float: 0.5\n" + " Layer 1:\n" + " OcclusionTexture @ UnsignedInt: 3\n" + " - texturePointer @ MutablePointer: 0xdead\n" + " + texturePointer @ Pointer: 0xdead\n" + " +Layer 2:\n" + " + NormalTextureLayer @ UnsignedInt: 2\n"}, + {"different (empty) layer count", + Trade::MaterialData{{}, {}}, + Trade::MaterialData{{}, {}, {0, 0, 0}}, + "Materials a and b have different layers. Actual (+) vs expected (-):\n" + " Base layer:\n" + " -Layer 1:\n" + " -Layer 2:\n", + "Materials b and a have different layers. Actual (+) vs expected (-):\n" + " Base layer:\n" + " +Layer 1:\n" + " +Layer 2:\n"}, +}; + +CompareMaterialTest::CompareMaterialTest() { + addInstancedTests({&CompareMaterialTest::same}, + Containers::arraySize(SameData)); + + addInstancedTests({&CompareMaterialTest::different, + &CompareMaterialTest::differentReverse}, + Containers::arraySize(DifferentData)); +} + +void CompareMaterialTest::same() { + auto&& data = SameData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + CORRADE_COMPARE_AS(data.material, data.material, CompareMaterial); +} + +void CompareMaterialTest::different() { + auto&& data = DifferentData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + TestSuite::Comparator compare; + TestSuite::ComparisonStatusFlags flags = compare(data.actual, data.expected); + CORRADE_COMPARE(flags, TestSuite::ComparisonStatusFlag::Failed); + + CORRADE_INFO("Visual color verification:"); + { + Debug out; + compare.printMessage(flags, out, "a", "b"); + } + + std::ostringstream out; + { + Debug dc{&out, Debug::Flag::DisableColors}; + compare.printMessage(flags, dc, "a", "b"); + } + CORRADE_COMPARE(out.str(), data.message); +} + +void CompareMaterialTest::differentReverse() { + auto&& data = DifferentData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + TestSuite::Comparator compare; + TestSuite::ComparisonStatusFlags flags = compare(data.expected, data.actual); + CORRADE_COMPARE(flags, TestSuite::ComparisonStatusFlag::Failed); + + CORRADE_INFO("Visual color verification:"); + { + Debug out; + compare.printMessage(flags, out, "b", "a"); + } + + std::ostringstream out; + { + Debug dc{&out, Debug::Flag::DisableColors}; + compare.printMessage(flags, dc, "b", "a"); + } + CORRADE_COMPARE(out.str(), data.messageReverse); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::DebugTools::Test::CompareMaterialTest)