From 09132ac61052e1b68ad76dac7568926f4532944f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Mon, 27 Apr 2020 23:40:24 +0200 Subject: [PATCH 01/16] DebugTools: initial implementation of a new frame profiler. Replaces the old & extremely useless Profiler. Doesn't have everything I want yet (missing stddev and fancier GPU queries), that'll come later. --- doc/changelog.dox | 1 + doc/snippets/CMakeLists.txt | 6 + doc/snippets/MagnumDebugTools-gl.cpp | 41 + doc/snippets/MagnumDebugTools.cpp | 63 +- doc/snippets/debugtools-frameprofiler.ansi | 6 + doc/snippets/debugtools-frameprofiler.cpp | 112 ++ src/Magnum/DebugTools/CMakeLists.txt | 4 +- src/Magnum/DebugTools/FrameProfiler.cpp | 665 +++++++++++ src/Magnum/DebugTools/FrameProfiler.h | 704 +++++++++++ src/Magnum/DebugTools/Test/CMakeLists.txt | 8 + .../DebugTools/Test/FrameProfilerGLTest.cpp | 242 ++++ .../DebugTools/Test/FrameProfilerTest.cpp | 1056 +++++++++++++++++ 12 files changed, 2905 insertions(+), 3 deletions(-) create mode 100644 doc/snippets/debugtools-frameprofiler.ansi create mode 100644 doc/snippets/debugtools-frameprofiler.cpp create mode 100644 src/Magnum/DebugTools/FrameProfiler.cpp create mode 100644 src/Magnum/DebugTools/FrameProfiler.h create mode 100644 src/Magnum/DebugTools/Test/FrameProfilerGLTest.cpp create mode 100644 src/Magnum/DebugTools/Test/FrameProfilerTest.cpp diff --git a/doc/changelog.dox b/doc/changelog.dox index 192d5bf67..865853496 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -72,6 +72,7 @@ See also: - New @ref DebugTools::ColorMap namespace containing a few presets for gradient visualization +- New @ref DebugTools::FrameProfiler utility for CPU and GPU profiling @subsubsection changelog-latest-new-gl GL library diff --git a/doc/snippets/CMakeLists.txt b/doc/snippets/CMakeLists.txt index b38d4c5da..aac0b6ca9 100644 --- a/doc/snippets/CMakeLists.txt +++ b/doc/snippets/CMakeLists.txt @@ -119,6 +119,12 @@ if(WITH_DEBUGTOOLS) target_link_libraries(snippets-MagnumDebugTools PRIVATE MagnumDebugTools) set_target_properties(snippets-MagnumDebugTools PROPERTIES FOLDER "Magnum/doc/snippets") + if(BUILD_GL_TESTS AND NOT MAGNUM_TARGET_GLES) + add_executable(debugtools-frameprofiler debugtools-frameprofiler.cpp) + target_link_libraries(debugtools-frameprofiler PRIVATE + MagnumDebugTools MagnumOpenGLTester) + endif() + # TODO: causes spurious linker errors on Travis iOS build, so I'm disabling it if(NOT CORRADE_TARGET_IOS) set(SNIPPETS_DIR ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/doc/snippets/MagnumDebugTools-gl.cpp b/doc/snippets/MagnumDebugTools-gl.cpp index dce0d4d4e..64e4b09e1 100644 --- a/doc/snippets/MagnumDebugTools-gl.cpp +++ b/doc/snippets/MagnumDebugTools-gl.cpp @@ -37,6 +37,7 @@ #include "Magnum/DebugTools/TextureImage.h" #include "Magnum/GL/Framebuffer.h" #include "Magnum/GL/CubeMapTexture.h" +#include "Magnum/GL/SampleQuery.h" #include "Magnum/GL/Texture.h" #include "Magnum/GL/TextureFormat.h" #include "Magnum/Math/Range.h" @@ -48,6 +49,10 @@ #include "Magnum/GL/BufferImage.h" #endif +#if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) +#include "Magnum/DebugTools/FrameProfiler.h" +#endif + using namespace Magnum; using namespace Magnum::Math::Literals; @@ -103,6 +108,33 @@ new DebugTools::ForceRenderer3D(manager, *object, {0.3f, 1.5f, -0.7f}, force, /* [ForceRenderer] */ } +#ifndef MAGNUM_TARGET_GLES +{ +/* [FrameProfiler-setup-delayed] */ +GL::SampleQuery queries[3]{ + GL::SampleQuery{GL::SampleQuery::Target::SamplesPassed}, + GL::SampleQuery{GL::SampleQuery::Target::SamplesPassed}, + GL::SampleQuery{GL::SampleQuery::Target::SamplesPassed} +}; +DebugTools::FrameProfiler profiler{{ + DebugTools::FrameProfiler::Measurement{"Samples", + DebugTools::FrameProfiler::Units::Count, + UnsignedInt(Containers::arraySize(queries)), + [](void* state, UnsignedInt current) { + static_cast(state)[current].begin(); + }, + [](void* state, UnsignedInt current) { + static_cast(state)[current].end(); + }, + [](void* state, UnsignedInt previous, UnsignedInt) { + return static_cast(state)[previous] + .result(); + }, queries} +}, 50}; +/* [FrameProfiler-setup-delayed] */ +} +#endif + { SceneGraph::Object* object{}; /* [ObjectRenderer] */ @@ -116,6 +148,15 @@ manager.set("my", DebugTools::ObjectRendererOptions{}.setSize(0.3f)); new DebugTools::ObjectRenderer3D{manager, *object, "my", &debugDrawables}; /* [ObjectRenderer] */ } + +{ +/* [GLFrameProfiler-usage] */ +DebugTools::GLFrameProfiler profiler{ + DebugTools::GLFrameProfiler::Value::FrameTime| + DebugTools::GLFrameProfiler::Value::GpuDuration, 50}; +/* [GLFrameProfiler-usage] */ +} + { GL::Texture2D texture; Range2Di rect; diff --git a/doc/snippets/MagnumDebugTools.cpp b/doc/snippets/MagnumDebugTools.cpp index a6a5ae09e..027fcbb9a 100644 --- a/doc/snippets/MagnumDebugTools.cpp +++ b/doc/snippets/MagnumDebugTools.cpp @@ -23,6 +23,7 @@ DEALINGS IN THE SOFTWARE. */ +#include #include #include #include @@ -31,6 +32,7 @@ #include "Magnum/ImageView.h" #include "Magnum/PixelFormat.h" #include "Magnum/DebugTools/CompareImage.h" +#include "Magnum/DebugTools/FrameProfiler.h" #include "Magnum/Math/Color.h" #include "Magnum/Trade/AbstractImporter.h" @@ -101,5 +103,62 @@ CORRADE_COMPARE_WITH(actual.pixels().flipped<0>(), expected, } }; -/* To prevent macOS ranlib complaining that there are no symbols */ -int main() {} +struct MyApp { + void drawEvent(); + void drawEventAgain(); + void swapBuffers(); + void redraw(); + + DebugTools::FrameProfiler _profiler; +}; + +/* [FrameProfiler-usage] */ +void MyApp::drawEvent() { + _profiler.beginFrame(); + + // actual drawing code … + + _profiler.endFrame(); + + // possibly other code (such as UI) you don't want to have included in the + // measurements … + + swapBuffers(); + redraw(); +} +/* [FrameProfiler-usage] */ + +void MyApp::drawEventAgain() { +/* [FrameProfiler-usage-console] */ + _profiler.endFrame(); + _profiler.printStatistics(10); + + swapBuffers(); + if(_profiler.isEnabled()) redraw(); +} +/* [FrameProfiler-usage-console] */ + +int main() { +{ +/* [FrameProfiler-setup-immediate] */ +using std::chrono::high_resolution_clock; + +high_resolution_clock::time_point frameBeginTime; +DebugTools::FrameProfiler profiler{{ + DebugTools::FrameProfiler::Measurement{"CPU time", + DebugTools::FrameProfiler::Units::Nanoseconds, + [](void* state) { + *static_cast(state) + = high_resolution_clock::now(); + }, + [](void* state) { + return UnsignedLong( + std::chrono::duration_cast( + *static_cast(state) + - high_resolution_clock::now()).count()); + }, &frameBeginTime} +}, 50}; +/* [FrameProfiler-setup-immediate] */ +} + +} diff --git a/doc/snippets/debugtools-frameprofiler.ansi b/doc/snippets/debugtools-frameprofiler.ansi new file mode 100644 index 000000000..ebd265f26 --- /dev/null +++ b/doc/snippets/debugtools-frameprofiler.ansi @@ -0,0 +1,6 @@ +Last 50 frames: +  Frame time: 16.65 ms +  CPU duration: 14.72 ms +  GPU duration: 10.89 ms +  Vertex fetch ratio: 0.24 +  Primitives clipped: 59.67 % diff --git a/doc/snippets/debugtools-frameprofiler.cpp b/doc/snippets/debugtools-frameprofiler.cpp new file mode 100644 index 000000000..4f4ffb81b --- /dev/null +++ b/doc/snippets/debugtools-frameprofiler.cpp @@ -0,0 +1,112 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 "Magnum/DebugTools/FrameProfiler.h" + +/* Hacking around the fugly windowlessapp setup by including OpenGLTester */ +#include "Magnum/GL/OpenGLTester.h" + +using namespace Magnum; + +class FrameProfiler: public Platform::WindowlessApplication { + public: + explicit FrameProfiler(const Arguments& arguments); + + int exec() override { return 0; } +}; + +FrameProfiler::FrameProfiler(const Arguments& arguments): Platform::WindowlessApplication{arguments} { + /* Enable everything in the GL profiler and then introspect it to fake + its output 1:1 */ + DebugTools::GLFrameProfiler glProfiler{ + DebugTools::GLFrameProfiler::Value::FrameTime| + DebugTools::GLFrameProfiler::Value::CpuDuration| + DebugTools::GLFrameProfiler::Value::GpuDuration| + DebugTools::GLFrameProfiler::Value::VertexFetchRatio| + DebugTools::GLFrameProfiler::Value::PrimitiveClipRatio + , 50}; + + DebugTools::FrameProfiler profiler{{ + DebugTools::FrameProfiler::Measurement{ + glProfiler.measurementName(0), + glProfiler.measurementUnits(0), + glProfiler.measurementDelay(2), + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt, UnsignedInt) { + return UnsignedLong{16651567}; + }, nullptr}, + DebugTools::FrameProfiler::Measurement{ + glProfiler.measurementName(1), + glProfiler.measurementUnits(1), + glProfiler.measurementDelay(2), + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt, UnsignedInt) { + return UnsignedLong{14720000}; + }, nullptr}, + DebugTools::FrameProfiler::Measurement{ + glProfiler.measurementName(2), + glProfiler.measurementUnits(2), + glProfiler.measurementDelay(2), + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt, UnsignedInt) { + return UnsignedLong{10890000}; + }, nullptr}, + DebugTools::FrameProfiler::Measurement{ + glProfiler.measurementName(3), + glProfiler.measurementUnits(3), + glProfiler.measurementDelay(3), + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt, UnsignedInt) { + return UnsignedLong{240}; + }, nullptr}, + DebugTools::FrameProfiler::Measurement{ + glProfiler.measurementName(4), + glProfiler.measurementUnits(4), + glProfiler.measurementDelay(4), + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt, UnsignedInt) { + return UnsignedLong{59670}; + }, nullptr}, + }, 50}; + + for(std::size_t i = 0; i != 100; ++i) { + profiler.beginFrame(); + profiler.endFrame(); + } + + std::ostringstream out; /* we don't want a TTY */ + profiler.printStatistics(Debug{&out}, 1); + Debug{Debug::Flag::NoNewlineAtTheEnd} << out.str(); +} + +MAGNUM_WINDOWLESSAPPLICATION_MAIN(FrameProfiler) diff --git a/src/Magnum/DebugTools/CMakeLists.txt b/src/Magnum/DebugTools/CMakeLists.txt index 767d5d0af..b831a583d 100644 --- a/src/Magnum/DebugTools/CMakeLists.txt +++ b/src/Magnum/DebugTools/CMakeLists.txt @@ -27,11 +27,13 @@ set(MagnumDebugTools_SRCS ColorMap.cpp Profiler.cpp) -set(MagnumDebugTools_GracefulAssert_SRCS ) +set(MagnumDebugTools_GracefulAssert_SRCS + FrameProfiler.cpp) set(MagnumDebugTools_HEADERS ColorMap.h DebugTools.h + FrameProfiler.h Profiler.h visibility.h) diff --git a/src/Magnum/DebugTools/FrameProfiler.cpp b/src/Magnum/DebugTools/FrameProfiler.cpp new file mode 100644 index 000000000..48077e881 --- /dev/null +++ b/src/Magnum/DebugTools/FrameProfiler.cpp @@ -0,0 +1,665 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 "FrameProfiler.h" + +#include +#include +#include +#include +#include +#include + +#include "Magnum/Math/Functions.h" +#ifdef MAGNUM_TARGET_GL +#include "Magnum/GL/TimeQuery.h" +#ifndef MAGNUM_TARGET_GLES +#include "Magnum/GL/PipelineStatisticsQuery.h" +#endif +#endif + +namespace Magnum { namespace DebugTools { + +FrameProfiler::Measurement::Measurement(const std::string& name, const Units units, void(*const begin)(void*), UnsignedLong(*const end)(void*), void* const state): _name{name}, _end{nullptr}, _state{state}, _units{units}, _delay{0} { + _begin.immediate = begin; + _query.immediate = end; +} + +FrameProfiler::Measurement::Measurement(const std::string& name, const Units units, const UnsignedInt delay, void(*const begin)(void*, UnsignedInt), void(*const end)(void*, UnsignedInt), UnsignedLong(*const query)(void*, UnsignedInt, UnsignedInt), void* const state): _name{name}, _state{state}, _units{units}, _delay{delay} { + CORRADE_ASSERT(delay >= 1, "DebugTools::FrameProfiler::Measurement: delay can't be zero", ); + _begin.delayed = begin; + _end = end; + _query.delayed = query; +} + +FrameProfiler::FrameProfiler() noexcept = default; + +FrameProfiler::FrameProfiler(Containers::Array&& measurements, std::size_t maxFrameCount) noexcept { + setup(std::move(measurements), maxFrameCount); +} + +FrameProfiler::FrameProfiler(const std::initializer_list measurements, const std::size_t maxFrameCount): FrameProfiler{Containers::array(measurements), maxFrameCount} {} + +FrameProfiler::FrameProfiler(FrameProfiler&& other) noexcept: + _enabled{other._enabled}, + #ifndef CORRADE_NO_ASSERT + _beginFrameCalled{other._beginFrameCalled}, + #endif + _currentData{other._currentData}, + _maxFrameCount{other._maxFrameCount}, + _measuredFrameCount{other._measuredFrameCount}, + _measurements{std::move(other._measurements)}, + _data{std::move(other._data)} +{ + /* For all state pointers that point to &other patch them to point to this + instead, to account for 90% of use cases of derived classes */ + for(Measurement& measurement: _measurements) + if(measurement._state == &other) measurement._state = this; +} + +FrameProfiler& FrameProfiler::operator=(FrameProfiler&& other) noexcept { + using std::swap; + swap(_enabled, other._enabled); + #ifndef CORRADE_NO_ASSERT + swap(_beginFrameCalled, other._beginFrameCalled); + #endif + swap(_currentData, other._currentData); + swap(_maxFrameCount, other._maxFrameCount); + swap(_measuredFrameCount, other._measuredFrameCount); + swap(_measurements, other._measurements); + swap(_data, other._data); + + /* For all state pointers that point to &other patch them to point to this + instead, to account for 90% of use cases of derived classes */ + for(Measurement& measurement: _measurements) + if(measurement._state == &other) measurement._state = this; + + /* And the same the other way to avoid the other instance accidentally + affecting out measurements */ + for(Measurement& measurement: other._measurements) + if(measurement._state == this) measurement._state = &other; + + return *this; +} + +void FrameProfiler::setup(Containers::Array&& measurements, const std::size_t maxFrameCount) { + CORRADE_ASSERT(maxFrameCount >= 1, "DebugTools::FrameProfiler::setup(): max frame count can't be zero", ); + + _maxFrameCount = maxFrameCount; + _measurements = std::move(measurements); + arrayReserve(_data, maxFrameCount*_measurements.size()); + + /* Calculate the max delay, which signalizes when data will be available. + Non-delayed measurements are distinguished by _delay set to 0, so start + with 1 to exclude these. */ + for(const Measurement& measurement: _measurements) { + /* Max frame count is always >= 1, so even if _delay is 0 the condition + makes sense and we don't need to do a max() */ + CORRADE_ASSERT(maxFrameCount >= measurement._delay, + "DebugTools::FrameProfiler::setup(): max delay" << measurement._delay << "is larger than max frame count" << maxFrameCount, ); + } + + /* Reset to have a clean slate in case we did some other measurements + before */ + enable(); +} + +void FrameProfiler::setup(const std::initializer_list measurements, const std::size_t maxFrameCount) { + setup(Containers::array(measurements), maxFrameCount); +} + +void FrameProfiler::enable() { + _enabled = true; + #ifndef CORRADE_NO_ASSERT + _beginFrameCalled = false; + #endif + _currentData = 0; + _measuredFrameCount = 0; + arrayResize(_data, 0); + + /* Wipe out no longer relevant moving sums from all measurements, and + delayed measurement indices as well (tho for these it's not so + important) */ + for(Measurement& measurement: _measurements) { + measurement._movingSum = 0; + measurement._current = 0; + } +} + +void FrameProfiler::disable() { + _enabled = false; +} + +void FrameProfiler::beginFrame() { + if(!_enabled) return; + + CORRADE_ASSERT(!_beginFrameCalled, "DebugTools::FrameProfiler::beginFrame(): expected end of frame", ); + #ifndef CORRADE_NO_ASSERT + _beginFrameCalled = true; + #endif + + /* For all measurements call the begin function */ + for(const Measurement& measurement: _measurements) { + if(!measurement._delay) + measurement._begin.immediate(measurement._state); + else + measurement._begin.delayed(measurement._state, measurement._current); + } +} + +/* For delay = 1 returns _currentData */ +std::size_t FrameProfiler::delayedCurrentData(UnsignedInt delay) const { + CORRADE_INTERNAL_ASSERT(delay >= 1); + + /* The delayed frame is current or before current */ + if(_currentData >= delay - 1) + return _currentData - delay + 1; + + /* If we have all data, wrap around. If we don't have all data yet, such + value doesn't exist and thus this will return an OOB index. If + everything is implemented correctly, it won't be accessed in any way. */ + return _maxFrameCount + _currentData - delay + 1; +} + +void FrameProfiler::endFrame() { + if(!_enabled) return; + + CORRADE_ASSERT(_beginFrameCalled, "DebugTools::FrameProfiler::endFrame(): expected begin of frame", ); + #ifndef CORRADE_NO_ASSERT + _beginFrameCalled = false; + #endif + + /* If we don't have all frames yet, enlarge the array */ + if(++_measuredFrameCount <= _maxFrameCount) { + CORRADE_INTERNAL_ASSERT(_measurements.empty() || _currentData == _data.size()/_measurements.size()); + arrayAppend(_data, Containers::NoInit, _measurements.size()); + } + + /* Wrap up measurements for this frame */ + for(std::size_t i = 0; i != _measurements.size(); ++i) { + Measurement& measurement = _measurements[i]; + const UnsignedInt measurementDelay = Math::max(1u, measurement._delay); + + /* Where to save currently queried data. For _delay of 0 or 1, + delayedCurrentData(Math::max(1u, measurement._delay)) is equal to + _currentData. */ + UnsignedLong& currentMeasurementData = _data[delayedCurrentData(measurementDelay)*_measurements.size() + i]; + + /* If we're wrapping around, subtract the oldest data from the moving + average so we can reuse the memory for currently queried data */ + if(_measuredFrameCount > _maxFrameCount + measurementDelay - 1) { + CORRADE_INTERNAL_ASSERT(measurement._movingSum >= currentMeasurementData); + measurement._movingSum -= currentMeasurementData; + } + + /* Simply save the data if not delayed */ + if(!measurement._delay) + currentMeasurementData = measurement._query.immediate(measurement._state); + + /* For delayed measurements call the end function for current frame and + then save the data for the delayed frame */ + else { + measurement._end(measurement._state, measurement._current); + + /* The slot from which we just retrieved a delayed value will be + reused for a a new value next frame */ + const UnsignedInt previous = (measurement._current + 1) % measurement._delay; + if(_measuredFrameCount >= measurement._delay) { + currentMeasurementData = + measurement._query.delayed(measurement._state, previous, measurement._current); + } + measurement._current = previous; + } + } + + /* Process the new data if we have enough frames even for the largest + delay */ + for(std::size_t i = 0; i != _measurements.size(); ++i) { + Measurement& measurement = _measurements[i]; + const UnsignedInt measurementDelay = Math::max(1u, measurement._delay); + + /* If we have enough frames, add the new measurement to the moving sum. + For _delay of 0 or 1, delayedCurrentData(Math::max(1u, measurement._delay)) + is equal to _currentData. */ + if(_measuredFrameCount >= measurementDelay) + _measurements[i]._movingSum += _data[delayedCurrentData(measurementDelay)*_measurements.size() + i]; + } + + /* Advance & wraparound the index where data will be saved for the next + frame */ + _currentData = (_currentData + 1) % _maxFrameCount; +} + +std::string FrameProfiler::measurementName(const std::size_t id) const { + CORRADE_ASSERT(id < _measurements.size(), + "DebugTools::FrameProfiler::measurementName(): index" << id << "out of range for" << _measurements.size() << "measurements", {}); + return _measurements[id]._name; +} + +FrameProfiler::Units FrameProfiler::measurementUnits(const std::size_t id) const { + CORRADE_ASSERT(id < _measurements.size(), + "DebugTools::FrameProfiler::measurementUnits(): index" << id << "out of range for" << _measurements.size() << "measurements", {}); + return _measurements[id]._units; +} + +UnsignedInt FrameProfiler::measurementDelay(const std::size_t id) const { + CORRADE_ASSERT(id < _measurements.size(), + "DebugTools::FrameProfiler::measurementDelay(): index" << id << "out of range for" << _measurements.size() << "measurements", {}); + return Math::max(_measurements[id]._delay, 1u); +} + +bool FrameProfiler::isMeasurementAvailable(const std::size_t id) const { + CORRADE_ASSERT(id < _measurements.size(), + "DebugTools::FrameProfiler::measurementDelay(): index" << id << "out of range for" << _measurements.size() << "measurements", {}); + return _measuredFrameCount >= Math::max(_measurements[id]._delay, 1u); +} + +Double FrameProfiler::measurementDataInternal(const Measurement& measurement) const { + return Double(measurement._movingSum)/ + Math::min(_measuredFrameCount - Math::max(measurement._delay, 1u) + 1, _maxFrameCount); +} + +Double FrameProfiler::measurementMean(const std::size_t id) const { + CORRADE_ASSERT(id < _measurements.size(), + "DebugTools::FrameProfiler::measurementMean(): index" << id << "out of range for" << _measurements.size() << "measurements", {}); + CORRADE_ASSERT(_measuredFrameCount >= Math::max(_measurements[id]._delay, 1u), "DebugTools::FrameProfiler::measurementMean(): measurement data available after" << Math::max(_measurements[id]._delay, 1u) - _measuredFrameCount << "more frames", {}); + + return measurementDataInternal(_measurements[id]); +} + +namespace { + +/* Based on Corrade/TestSuite/Implementation/BenchmarkStats.h */ + +void printValue(Utility::Debug& out, const Double mean, const Double divisor, const char* const unitPrefix, const char* const units) { + out << Debug::boldColor(Debug::Color::Green) + << Utility::formatString("{:.2f}", mean/divisor) << Debug::resetColor + << Debug::nospace << unitPrefix << Debug::nospace << units; +} + +void printTime(Utility::Debug& out, const Double mean) { + if(mean >= 1000000000.0) + printValue(out, mean, 1000000000.0, " ", "s"); + else if(mean >= 1000000.0) + printValue(out, mean, 1000000.0, " m", "s"); + else if(mean >= 1000.0) + printValue(out, mean, 1000.0, " µ", "s"); + else + printValue(out, mean, 1.0, " n", "s"); +} + +void printCount(Utility::Debug& out, const Double mean, Double multiplier, const char* const units) { + if(mean >= multiplier*multiplier*multiplier) + printValue(out, mean, multiplier*multiplier*multiplier, " G", units); + else if(mean >= multiplier*multiplier) + printValue(out, mean, multiplier*multiplier, " M", units); + else if(mean >= multiplier) + printValue(out, mean, multiplier, " k", units); + else + printValue(out, mean, 1.0, std::strlen(units) ? " " : "", units); +} + +} + +void FrameProfiler::printStatisticsInternal(Debug& out) const { + out << Debug::boldColor(Debug::Color::Default) << "Last" + << Debug::boldColor(Debug::Color::Cyan) + << Math::min(_measuredFrameCount, _maxFrameCount) + << Debug::boldColor(Debug::Color::Default) << "frames:"; + + for(const Measurement& measurement: _measurements) { + out << Debug::newline << " " << Debug::boldColor(Debug::Color::Default) + << measurement._name << Debug::nospace << ":" << Debug::resetColor; + + /* If this measurement is not available yet, print a placeholder */ + if(_measuredFrameCount < Math::max(measurement._delay, 1u)) { + const char* units = nullptr; + switch(measurement._units) { + case Units::Count: + case Units::RatioThousandths: + units = ""; + break; + case Units::Nanoseconds: + units = "s"; + break; + case Units::Bytes: + units = "B"; + break; + case Units::PercentageThousandths: + units = "%"; + break; + } + CORRADE_INTERNAL_ASSERT(units); + + out << Debug::color(Debug::Color::Blue) << "-.--" + << Debug::resetColor; + if(units[0] != '\0') out << units; + + /* Otherwise format the value */ + } else { + const Double mean = measurementDataInternal(measurement); + switch(measurement._units) { + case Units::Nanoseconds: + printTime(out, mean); + continue; + case Units::Bytes: + printCount(out, mean, 1024.0, "B"); + continue; + case Units::Count: + printCount(out, mean, 1000.0, ""); + continue; + case Units::RatioThousandths: + printCount(out, mean/1000.0, 1000.0, ""); + continue; + case Units::PercentageThousandths: + printValue(out, mean, 1000.0, " ", "%"); + continue; + } + + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + } + } +} + +std::string FrameProfiler::statistics() const { + std::ostringstream out; + Debug d{&out, Debug::Flag::NoNewlineAtTheEnd|Debug::Flag::DisableColors}; + printStatisticsInternal(d); + return out.str(); +} + +void FrameProfiler::printStatistics(const std::size_t frequency) const { + Debug::Flags flags; + if(!Debug::isTty()) flags |= Debug::Flag::DisableColors; + printStatistics(Debug{flags}, frequency); +} + +void FrameProfiler::printStatistics(Debug& out, const std::size_t frequency) const { + if(!isEnabled() || _measuredFrameCount % frequency != 0) return; + + /* If on a TTY and we printed at least something already, scroll back up to + overwrite previous output */ + if(out.isTty() && _measuredFrameCount > frequency) + out << Debug::nospace << "\033[" << Debug::nospace + << _measurements.size() + 1 << Debug::nospace << "A\033[J" + << Debug::nospace; + + printStatisticsInternal(out); + + /* Unconditionally finish with a newline so the TTY scrollback works + correctly */ + if(out.flags() & Debug::Flag::NoNewlineAtTheEnd) + out << Debug::newline; +} + +Debug& operator<<(Debug& debug, const FrameProfiler::Units value) { + debug << "DebugTools::FrameProfiler::Units" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case FrameProfiler::Units::v: return debug << "::" #v; + _c(Nanoseconds) + _c(Bytes) + _c(Count) + _c(RatioThousandths) + _c(PercentageThousandths) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedByte(value)) << Debug::nospace << ")"; +} + +#ifdef MAGNUM_TARGET_GL +struct GLFrameProfiler::State { + UnsignedShort cpuDurationIndex = 0xffff, + gpuDurationIndex = 0xffff, + frameTimeIndex = 0xffff; + #ifndef MAGNUM_TARGET_GLES + UnsignedShort vertexFetchRatioIndex = 0xffff, + primitiveClipRatioIndex = 0xffff; + #endif + UnsignedLong frameTimeStartFrame[2]; + UnsignedLong cpuDurationStartFrame; + GL::TimeQuery timeQueries[3]{GL::TimeQuery{NoCreate}, GL::TimeQuery{NoCreate}, GL::TimeQuery{NoCreate}}; + #ifndef MAGNUM_TARGET_GLES + GL::PipelineStatisticsQuery verticesSubmittedQueries[3]{GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}}; + GL::PipelineStatisticsQuery vertexShaderInvocationsQueries[3]{GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}}; + GL::PipelineStatisticsQuery clippingInputPrimitivesQueries[3]{GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}}; + GL::PipelineStatisticsQuery clippingOutputPrimitivesQueries[3]{GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}}; + #endif +}; + +GLFrameProfiler::GLFrameProfiler(): _state{Containers::InPlaceInit} {} + +GLFrameProfiler::GLFrameProfiler(const Values values, const std::size_t maxFrameCount): GLFrameProfiler{} +{ + setup(values, maxFrameCount); +} + +GLFrameProfiler::GLFrameProfiler(GLFrameProfiler&&) noexcept = default; + +GLFrameProfiler& GLFrameProfiler::operator=(GLFrameProfiler&&) noexcept = default; + +GLFrameProfiler::~GLFrameProfiler() = default; + +void GLFrameProfiler::setup(const Values values, const std::size_t maxFrameCount) { + UnsignedShort index = 0; + Containers::Array measurements; + if(values & Value::FrameTime) { + /* Fucking hell, STL. When I first saw std::chrono back in 2010 I + should have flipped the table and learn carpentry instead. BUT NO, + I'm still suffering this abomination a decade later! */ + arrayAppend(measurements, Containers::InPlaceInit, + "Frame time", Units::Nanoseconds, UnsignedInt(Containers::arraySize(_state->frameTimeStartFrame)), + [](void* state, UnsignedInt current) { + static_cast(state)->frameTimeStartFrame[current] = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + }, + [](void*, UnsignedInt) {}, + [](void* state, UnsignedInt previous, UnsignedInt current) { + auto& self = *static_cast(state); + return self.frameTimeStartFrame[current] - + self.frameTimeStartFrame[previous]; + }, _state.get()); + _state->frameTimeIndex = index++; + } + if(values & Value::CpuDuration) { + arrayAppend(measurements, Containers::InPlaceInit, + "CPU duration", Units::Nanoseconds, + [](void* state) { + static_cast(state)->cpuDurationStartFrame = std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + }, + [](void* state) { + return std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count() - static_cast(state)->cpuDurationStartFrame; + }, _state.get()); + _state->cpuDurationIndex = index++; + } + if(values & Value::GpuDuration) { + for(GL::TimeQuery& q: _state->timeQueries) + q = GL::TimeQuery{GL::TimeQuery::Target::TimeElapsed}; + arrayAppend(measurements, Containers::InPlaceInit, + "GPU duration", Units::Nanoseconds, + UnsignedInt(Containers::arraySize(_state->timeQueries)), + [](void* state, UnsignedInt current) { + static_cast(state)->timeQueries[current].begin(); + }, + [](void* state, UnsignedInt current) { + static_cast(state)->timeQueries[current].end(); + }, + [](void* state, UnsignedInt previous, UnsignedInt) { + return static_cast(state)->timeQueries[previous].result(); + }, _state.get()); + _state->gpuDurationIndex = index++; + } + #ifndef MAGNUM_TARGET_GLES + if(values & Value::VertexFetchRatio) { + for(GL::PipelineStatisticsQuery& q: _state->verticesSubmittedQueries) + q = GL::PipelineStatisticsQuery{GL::PipelineStatisticsQuery::Target::VerticesSubmitted}; + for(GL::PipelineStatisticsQuery& q: _state->vertexShaderInvocationsQueries) + q = GL::PipelineStatisticsQuery{GL::PipelineStatisticsQuery::Target::VertexShaderInvocations}; + arrayAppend(measurements, Containers::InPlaceInit, + "Vertex fetch ratio", Units::RatioThousandths, + UnsignedInt(Containers::arraySize(_state->verticesSubmittedQueries)), + [](void* state, UnsignedInt current) { + static_cast(state)->verticesSubmittedQueries[current].begin(); + static_cast(state)->vertexShaderInvocationsQueries[current].begin(); + }, + [](void* state, UnsignedInt current) { + static_cast(state)->verticesSubmittedQueries[current].end(); + static_cast(state)->vertexShaderInvocationsQueries[current].end(); + }, + [](void* state, UnsignedInt previous, UnsignedInt) { + /* Avoid division by zero if a frame doesn't have any draws */ + const auto submitted = static_cast(state)->verticesSubmittedQueries[previous].result(); + if(!submitted) return UnsignedLong{}; + + return static_cast(state)->vertexShaderInvocationsQueries[previous].result()*1000/submitted; + }, _state.get()); + _state->vertexFetchRatioIndex = index++; + } + if(values & Value::PrimitiveClipRatio) { + for(GL::PipelineStatisticsQuery& q: _state->clippingInputPrimitivesQueries) + q = GL::PipelineStatisticsQuery{GL::PipelineStatisticsQuery::Target::ClippingInputPrimitives}; + for(GL::PipelineStatisticsQuery& q: _state->clippingOutputPrimitivesQueries) + q = GL::PipelineStatisticsQuery{GL::PipelineStatisticsQuery::Target::ClippingOutputPrimitives}; + arrayAppend(measurements, Containers::InPlaceInit, + "Primitives clipped", Units::PercentageThousandths, + UnsignedInt(Containers::arraySize(_state->clippingInputPrimitivesQueries)), + [](void* state, UnsignedInt current) { + static_cast(state)->clippingInputPrimitivesQueries[current].begin(); + static_cast(state)->clippingOutputPrimitivesQueries[current].begin(); + }, + [](void* state, UnsignedInt current) { + static_cast(state)->clippingInputPrimitivesQueries[current].end(); + static_cast(state)->clippingOutputPrimitivesQueries[current].end(); + }, + [](void* state, UnsignedInt previous, UnsignedInt) { + /* Avoid division by zero if a frame doesn't have any draws */ + const auto input = static_cast(state)->clippingInputPrimitivesQueries[previous].result(); + if(!input) return UnsignedLong{}; + + return 100000 - static_cast(state)->clippingOutputPrimitivesQueries[previous].result()*100000/input; + }, _state.get()); + _state->primitiveClipRatioIndex = index++; + } + #endif + setup(std::move(measurements), maxFrameCount); +} + +auto GLFrameProfiler::values() const -> Values { + Values values; + if(_state->frameTimeIndex != 0xffff) values |= Value::FrameTime; + if(_state->cpuDurationIndex != 0xffff) values |= Value::CpuDuration; + if(_state->gpuDurationIndex != 0xffff) values |= Value::GpuDuration; + #ifndef MAGNUM_TARGET_GLES + if(_state->vertexFetchRatioIndex != 0xffff) values |= Value::VertexFetchRatio; + if(_state->primitiveClipRatioIndex != 0xffff) values |= Value::PrimitiveClipRatio; + #endif + return values; +} + +bool GLFrameProfiler::isMeasurementAvailable(const Value value) const { + const UnsignedShort* index = nullptr; + switch(value) { + case Value::FrameTime: index = &_state->frameTimeIndex; break; + case Value::CpuDuration: index = &_state->cpuDurationIndex; break; + case Value::GpuDuration: index = &_state->gpuDurationIndex; break; + #ifndef MAGNUM_TARGET_GLES + case Value::VertexFetchRatio: index = &_state->vertexFetchRatioIndex; break; + case Value::PrimitiveClipRatio: index = &_state->primitiveClipRatioIndex; break; + #endif + } + CORRADE_INTERNAL_ASSERT(index); + CORRADE_ASSERT(*index < measurementCount(), + "DebugTools::GLFrameProfiler::isMeasurementAvailable():" << value << "not enabled", {}); + return isMeasurementAvailable(*index); +} + +Double GLFrameProfiler::frameTimeMean() const { + CORRADE_ASSERT(_state->frameTimeIndex < measurementCount(), + "DebugTools::GLFrameProfiler::frameTimeMean(): not enabled", {}); + return measurementMean(_state->frameTimeIndex); +} + +Double GLFrameProfiler::cpuDurationMean() const { + CORRADE_ASSERT(_state->cpuDurationIndex < measurementCount(), + "DebugTools::GLFrameProfiler::cpuDurationMean(): not enabled", {}); + return measurementMean(_state->cpuDurationIndex); +} + +Double GLFrameProfiler::gpuDurationMean() const { + CORRADE_ASSERT(_state->gpuDurationIndex < measurementCount(), + "DebugTools::GLFrameProfiler::gpuDurationMean(): not enabled", {}); + return measurementMean(_state->gpuDurationIndex); +} + +#ifndef MAGNUM_TARGET_GLES +Double GLFrameProfiler::vertexFetchRatioMean() const { + CORRADE_ASSERT(_state->vertexFetchRatioIndex < measurementCount(), + "DebugTools::GLFrameProfiler::vertexFetchRatioMean(): not enabled", {}); + return measurementMean(_state->vertexFetchRatioIndex); +} + +Double GLFrameProfiler::primitiveClipRatioMean() const { + CORRADE_ASSERT(_state->primitiveClipRatioIndex < measurementCount(), + "DebugTools::GLFrameProfiler::primitiveClipRatioMean(): not enabled", {}); + return measurementMean(_state->primitiveClipRatioIndex); +} +#endif + +Debug& operator<<(Debug& debug, const GLFrameProfiler::Value value) { + debug << "DebugTools::GLFrameProfiler::Value" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case GLFrameProfiler::Value::v: return debug << "::" #v; + _c(FrameTime) + _c(CpuDuration) + _c(GpuDuration) + #ifndef MAGNUM_TARGET_GLES + _c(VertexFetchRatio) + _c(PrimitiveClipRatio) + #endif + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedShort(value)) << Debug::nospace << ")"; +} + +Debug& operator<<(Debug& debug, const GLFrameProfiler::Values value) { + return Containers::enumSetDebugOutput(debug, value, "DebugTools::GLFrameProfiler::Values{}", { + GLFrameProfiler::Value::FrameTime, + GLFrameProfiler::Value::CpuDuration, + GLFrameProfiler::Value::GpuDuration, + #ifndef MAGNUM_TARGET_GLES + GLFrameProfiler::Value::VertexFetchRatio, + GLFrameProfiler::Value::PrimitiveClipRatio + #endif + }); +} +#endif + +}} diff --git a/src/Magnum/DebugTools/FrameProfiler.h b/src/Magnum/DebugTools/FrameProfiler.h new file mode 100644 index 000000000..c5fc55e71 --- /dev/null +++ b/src/Magnum/DebugTools/FrameProfiler.h @@ -0,0 +1,704 @@ +#ifndef Magnum_DebugTools_FrameProfiler_h +#define Magnum_DebugTools_FrameProfiler_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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::FrameProfiler, @ref Magnum::DebugTools::GLFrameProfiler + * @m_since_latest + */ + +#include +#include +#include + +#include "Magnum/Magnum.h" +#include "Magnum/DebugTools/visibility.h" + +namespace Magnum { namespace DebugTools { + +/** +@brief Frame profiler +@m_since_latest + +A generic implementation of a frame profiler supporting a moving average over +a set of frames as well as delayed measurements to avoid stalls when querying +the results. This class alone doesn't provide any pre-defined measurements, see +for example @ref GLFrameProfiler that provides common measurements like CPU and +GPU time. + +@experimental + +@section DebugTools-FrameProfiler-usage Basic usage + +Measurements are performed by calling @ref beginFrame() and @ref endFrame() at +designated points in the frame: + +@snippet MagnumDebugTools.cpp FrameProfiler-usage + +In order to have stable profiling results, the application needs to redraw +constantly. However for applications that otherwise redraw only on change it +might be wasteful --- to account for this, it's possible to toggle the profiler +operation using @ref enable() / @ref disable() and then +@ref Platform::Sdl2Application::redraw() "redraw()" can be called only if +@ref isEnabled() returns @cpp true @ce. + +Data for all measurements is then available through @ref measurementName(), +@ref measurementUnits() and @ref measurementMean(). For a convenient overview +of all measured values you can call @ref statistics() and feed its output to a +UI library or something that can render text. Alternatively, if you don't want +to bother with text rendering, call @ref printStatistics() to have the output +periodically printed to the console. If an interactive terminal is detected, +the output will be colored and refreshing itself in place. Together with the +on-demand profiling, it could look like this, refreshing the output every 10 +frames: + +@snippet MagnumDebugTools.cpp FrameProfiler-usage-console + +And here's a sample output on the terminal --- using a fully configured +@link GLFrameProfiler @endlink: + +@include debugtools-frameprofiler.ansi + +@section DebugTools-FrameProfiler-setup Setting up measurements + +Unless you're using this class through @ref GLFrameProfiler, measurements +have to be set up by passing @ref Measurement instances to the @ref setup() +function or to the constructor, together with specifying count of frames for +the moving average. A CPU duration measurements using the @ref std::chrono APIs +over last 50 frames can be done like this: + +@snippet MagnumDebugTools.cpp FrameProfiler-setup-immediate + +In the above case, the measurement result is available immediately on frame +end. That's not always the case, and for example GPU queries need a few frames +delay to avoid stalls from CPU/GPU synchronization. The following snippet sets +up sample count measurement with a delay, using three separate +@ref GL::SampleQuery instances that are cycled through each frame and retrieved +two frames later. The profiler automatically takes care of choosing one of the +three instances for each measurement via additional `current` / `previous` +parameters passed to each callback: + +@snippet MagnumDebugTools-gl.cpp FrameProfiler-setup-delayed + + + +@m_class{m-block m-warning} + +@par Move construction and state pointers in callbacks + The @ref FrameProfiler class is movable, which could potentially mean that + state pointers passed to callback functions become dangling. It's not a + problem in the above snippets because the state is always external to the + instance, but care has to be taken when passing pointers to subclasses. +@par + When setting up measurements from a subclass, it's recommended to always + pass @cpp this @ce as the state pointer. The base class recognizes it + during a move and patches the state to point to the new instance instead. + If you don't or can't use @cpp this @ce as a state pointer, you need to + either provide a dedicated move constructor and assignment to do the + required patching or disable moves altogether to avoid accidents. +*/ +class MAGNUM_DEBUGTOOLS_EXPORT FrameProfiler { + public: + /** + * @brief Measurement units + * + * @see @ref Measurement + */ + enum class Units: UnsignedByte { + /** + * Nanoseconds, measuring for example elapsed time. Depending on + * the magnitude, @ref statistics() can show them as microseconds, + * milliseconds or seconds. + */ + Nanoseconds, + + /** + * Bytes, measuring for example memory usage, bandwidth. Depending + * on the magnitude, @ref statistics() can show them as kB, MB, GB + * (with a multiplier of 1024). + */ + Bytes, + + /** + * Generic count. For discrete values that don't fit any of the + * above. Depending on the magnitude, @ref statistics() can show + * the value as k, M or G (with a multiplier of 1000). + */ + Count, + + /** + * Ratio expressed in 1/1000s. @ref statistics() divides the value + * by 1000 and depending on the magnitude it can show it also as k, + * M or G (with a multiplier of 1000). + */ + RatioThousandths, + + /** + * Percentage expressed in 1/1000s. @ref statistics() divides the + * value by 1000 and appends a % sign. + */ + PercentageThousandths + }; + + class Measurement; + + /** + * @brief Default constructor + * + * Call @ref setup() to populate the profiler with measurements. + */ + explicit FrameProfiler() noexcept; + + /** + * @brief Constructor + * + * Equivalent to default-constructing an instance and calling + * @ref setup() afterwards. + */ + explicit FrameProfiler(Containers::Array&& measurements, std::size_t maxFrameCount) noexcept; + + /** @overload */ + explicit FrameProfiler(std::initializer_list measurements, std::size_t maxFrameCount); + + /** @brief Copying is not allowed */ + FrameProfiler(const FrameProfiler&) = delete; + + /** @brief Move constructor */ + FrameProfiler(FrameProfiler&&) noexcept; + + /** @brief Copying is not allowed */ + FrameProfiler& operator=(const FrameProfiler&) = delete; + + /** @brief Move assignment */ + FrameProfiler& operator=(FrameProfiler&&) noexcept; + + /** + * @brief Setup measurements + * @param measurements List of measurements + * @param maxFrameCount Max frame count over which to calculate a + * moving average. Expected to be at least @cpp 1 @ce. + * + * Calling @ref setup() on an already set up profiler will replace + * existing measurements with @p measurements and reset + * @ref measuredFrameCount() back to @cpp 0 @ce. + */ + void setup(Containers::Array&& measurements, std::size_t maxFrameCount); + + /** @overload */ + void setup(std::initializer_list measurements, std::size_t maxFrameCount); + + /** @brief Whether the profiling is enabled */ + bool isEnabled() const { return _enabled; } + + /** + * @brief Enable the profiler + * + * The profiler is enabled implicitly after construction. When this + * function is called, it discards all measured data, effectively + * making @ref measuredFrameCount() zero. If you want to reset the + * profiler to measure different values as well, call @ref setup(). + */ + void enable(); + + /** + * @brief Disable the profiler + * + * Disabling the profiler will make @ref beginFrame() and + * @ref endFrame() a no-op, effectively freezing all reported + * measurements until the profiler is enabled again. + */ + void disable(); + + /** + * @brief Begin a frame + * + * Has to be called at the beginning of a frame and be paired with a + * corresponding @ref endFrame(). Calls @p begin functions in all + * @ref Measurement instances passed to @ref setup(). If the profiler + * is disabled, the function is a no-op. + * @see @ref isEnabled() + */ + void beginFrame(); + + /** + * @brief End a frame + * + * Has to be called at the end of frame, before buffer swap, and be + * paired with a corresponding @ref beginFrame(). Calls @p end + * functions in all @ref Measurement instances passed to @ref setup() + * and @p query functions on all measurements that are sufficiently + * delayed, saving their output. If the profiler is disabled, the + * function is a no-op. + * @see @ref isEnabled() + */ + void endFrame(); + + /** + * @brief Max count of measured frames + * + * How many frames to calculate a moving average from. At the beginning + * of a measurement when there's not enough frames, the average is + * calculated only from @ref measuredFrameCount(). Always at least + * @cpp 1 @ce. + */ + std::size_t maxFrameCount() const { return _maxFrameCount; } + + /** + * @brief Count of measured frames + * + * Count of times @ref endFrame() was called, but at most + * @ref maxFrameCount(), after which the profiler calculates a moving + * average over last @ref maxFrameCount() frames only. Actual data + * availability depends on @ref measurementDelay(). + */ + std::size_t measuredFrameCount() const { return _measuredFrameCount; } + + /** + * @brief Measurement count + * + * Count of @ref Measurement instances passed to @ref setup(). If + * @ref setup() was not called yet, returns @cpp 0 @ce. + */ + std::size_t measurementCount() const { return _measurements.size(); } + + /** + * @brief Measurement name + * + * The @p id corresponds to the index of the measurement in the list + * passed to @ref setup(). Expects that @p id is less than + * @ref measurementCount(). + */ + std::string measurementName(std::size_t id) const; + + /** + * @brief Measurement units + * + * The @p id corresponds to the index of the measurement in the list + * passed to @ref setup(). Expects that @p id is less than + * @ref measurementCount(). + */ + Units measurementUnits(std::size_t id) const; + + /** + * @brief Measurement delay + * + * How many @ref beginFrame() / @ref endFrame() call pairs needs to be + * performed before a value for given measurement is available. Always + * at least @cpp 1 @ce. The @p id corresponds to the index of the + * measurement in the list passed to @ref setup(). Expects that @p id + * is less than @ref measurementCount(). + */ + UnsignedInt measurementDelay(std::size_t id) const; + + /** + * @brief Whether given measurement is available + * + * Returns @cpp true @ce if @ref measuredFrameCount() is at least + * @ref measurementDelay() for given @p id, @cpp false @ce otherwise. + * The @p id corresponds to the index of the measurement in the list + * passed to @ref setup(). Expects that @p id is less than + * @ref measurementCount(). + */ + bool isMeasurementAvailable(std::size_t id) const; + + /** + * @brief Measurement mean + * + * Returns a moving average of @f$ n @f$ previous measurements out of + * the total @f$ M @f$ values: @f[ + * \bar{x}_\text{SM} = \dfrac{1}{n} \sum\limits_{i=0}^{n-1} x_{M -i} + * @f] + * + * The @p id corresponds to the index of the measurement in the list + * passed to @ref setup(). Expects that @p id is less than + * @ref measurementCount() and that the measurement is available. + * @see @ref isMeasurementAvailable() + */ + Double measurementMean(std::size_t id) const; + + /** + * @brief Overview of all measurements + * + * Returns a formatted string with names, means and units of all + * measurements in the order they were added. If some measurement data + * is available yet, prints placeholder values for these; if the + * @see @ref isMeasurementAvailable(), @ref isEnabled() + */ + std::string statistics() const; + + /** + * @brief Print an overview of all measurements to a console at given rate + * + * Expected to be called every frame. On every `frequency`th frame + * prints the same information as @ref statistics(), but in addition, + * if the output is a TTY, it's colored and overwrites itself instead + * of filling up the terminal history. + * @see @ref isMeasurementAvailable(), @ref isEnabled() + * @ref Corrade::Utility::Debug::isTty() + */ + void printStatistics(std::size_t frequency) const; + + /** + * @brief Print an overview of all measurements to given output at given rate + * + * Compared to @ref printStatistics(std::size_t) const prints to given + * @p out (which can be also @ref Corrade::Utility::Warning or + * @ref Corrade::Utility::Error) and uses it to decide whether the + * output is a TTY and whether to print colors. + * @see @ref Corrade::Utility::Debug::isTty(), + * @ref Corrade::Utility::Debug::Flag::DisableColors + */ + void printStatistics(Debug& out, std::size_t frequency) const; + + /** @overload */ + void printStatistics(Debug&& out, std::size_t frequency) const { + printStatistics(out, frequency); + } + + private: + std::size_t delayedCurrentData(UnsignedInt delay) const; + Double measurementDataInternal(const Measurement& measurement) const; + void printStatisticsInternal(Debug& out) const; + + bool _enabled = true; + #ifndef CORRADE_NO_ASSERT + /* Here it shouldn't cause the class to have a different size when + asserts get disabled */ + bool _beginFrameCalled{}; + #endif + std::size_t _currentData{}, _maxFrameCount{1}, _measuredFrameCount{}; + Containers::Array _measurements; + Containers::Array _data; +}; + +/** +@brief Measurement + +Describes a single measurement passed to @ref FrameProfiler::setup(). See +@ref DebugTools-FrameProfiler-setup for introduction and examples. +*/ +class MAGNUM_DEBUGTOOLS_EXPORT FrameProfiler::Measurement { + public: + /** + * @brief Construct an immediate measurement + * @param name Measurement name, used in + * @ref FrameProfiler::measurementName() and + * @ref FrameProfiler::statistics() + * @param units Measurement units, used in + * @ref FrameProfiler::measurementUnits() and + * @ref FrameProfiler::statistics() + * @param begin Function to call at the beginning of a frame + * @param end Function to call at the end of a frame, returning + * the measured value + * @param state State pointer passed to both @p begin and @p end + * as a first argument + */ + explicit Measurement(const std::string& name, Units units, void(*begin)(void*), UnsignedLong(*end)(void*), void* state); + + /** + * @brief Construct a delayed measurement + * @param name Measurement name, used in + * @ref FrameProfiler::measurementName() and + * @ref FrameProfiler::statistics() + * @param units Measurement units, used in + * @ref FrameProfiler::measurementUnits() and + * @ref FrameProfiler::statistics() + * @param delay How many @ref FrameProfiler::endFrame() calls has + * to happen before a measured value can be retrieved using + * @p query. Has to be at least @cpp 1 @ce, delay of @cpp 1 @ce is + * equal in behavior to immediate measurements. + * @param begin Function to call at the beginning of a frame. + * Second argument is a `current` index that's guaranteed to be + * less than @p delay and always different in each consecutive + * call. + * @param end Function to call at the end of a frame. + * Second argument is a `current` index that's guaranteed to be + * less than @p delay and always different in each consecutive + * call. + * @param query Function to call to get the measured value. Called + * after @p delay frames. First argument is a `previous` index + * that is the same as the `current` argument passed to a + * corresponding @p begin / @p end function of the measurement to + * query the value of. Second argument is a `current` index that + * corresponds to current frame. + * @param state State pointer passed to both @p begin and @p end + * as a first argument + */ + explicit Measurement(const std::string& name, Units units, UnsignedInt delay, void(*begin)(void*, UnsignedInt), void(*end)(void*, UnsignedInt), UnsignedLong(*query)(void*, UnsignedInt, UnsignedInt), void* state); + + private: + friend FrameProfiler; + + std::string _name; + union { + void(*immediate)(void*); + void(*delayed)(void*, UnsignedInt); + } _begin; + void(*_end)(void*, UnsignedInt); + union { + UnsignedLong(*immediate)(void*); + UnsignedLong(*delayed)(void*, UnsignedInt, UnsignedInt); + } _query; + void* _state; + Units _units; + /* Set to 0 to distinguish immediate measurements (first + constructor), however always used as max(_delay, 1) */ + UnsignedInt _delay; + + UnsignedInt _current{}; + UnsignedLong _movingSum{}; +}; + +/** +@debugoperatorclassenum{FrameProfiler,FrameProfiler::Units} +@m_since_latest +*/ +MAGNUM_DEBUGTOOLS_EXPORT Debug& operator<<(Debug& debug, FrameProfiler::Units value); + +#ifdef MAGNUM_TARGET_GL +/** +@brief OpenGL frame profiler +@m_since_latest + +A @ref FrameProfiler with OpenGL-specific measurements. Instantiate with a +desired subset of measured values and then continue the same way as described +in the @ref DebugTools-FrameProfiler-usage "FrameProfiler usage documentation": + +@snippet MagnumDebugTools-gl.cpp GLFrameProfiler-usage + +If none if @ref Value::GpuDuration, @ref Value::VertexFetchRatio and +@ref Value::PrimitiveClipRatio is not enabled, the class can operate without an +active OpenGL context. + +@experimental +*/ +class MAGNUM_DEBUGTOOLS_EXPORT GLFrameProfiler: public FrameProfiler { + public: + /** + * @brief Measured value + * + * @see @ref Values, @ref GLFrameProfiler(Values, std::size_t), + * @ref setup() + */ + enum class Value: UnsignedShort { + /** + * Measure total frame time (i.e., time between consecutive + * @ref beginFrame() calls). Reported in @ref Units::Nanoseconds + * with a delay of 2 frames. When converted to seconds, the value + * is an inverse of FPS. + */ + FrameTime = 1 << 0, + + /** + * Measure CPU frame duration (i.e., CPU time spent between + * @ref beginFrame() and @ref endFrame()). Reported in + * @ref Units::Nanoseconds with a delay of 1 frame. + */ + CpuDuration = 1 << 1, + + /** + * Measure GPU frame duration (i.e., time between @ref beginFrame() + * and @ref endFrame()). Reported in @ref Units::Nanoseconds + * with a delay of 3 frames. This value requires an active OpenGL + * context. + * @requires_gl33 Extension @gl_extension{ARB,timer_query} + * @requires_es_extension Extension @gl_extension{EXT,disjoint_timer_query} + * @requires_webgl_extension Extension @webgl_extension{EXT,disjoint_timer_query} + * on WebGL 1, @gl_extension{EXT,disjoint_timer_query_webgl2} + * on WebGL 2 + */ + GpuDuration = 1 << 2, + + #ifndef MAGNUM_TARGET_GLES + /** + * Ratio of vertex shader invocations to count of vertices + * submitted. For a non-indexed draw the ratio will be 1, for + * indexed draws ratio is less than 1. The lower the value is, the + * better a mesh is optimized for post-transform vertex cache. + * Reported in @ref Units::RatioThousandths with a delay of 3 + * frames. This value requires an active OpenGL context. + * @requires_gl46 Extension @gl_extension{ARB,pipeline_statistics_query} + */ + VertexFetchRatio = 1 << 3, + + /** + * Ratio of primitives discarded by the clipping stage to count of + * primitives submitted. The ratio is 0 when all primitives pass + * the clipping stage and 1 when all are discarded. Can be used to + * measure efficiency of a frustum culling algorithm. Reported in + * @ref Units::PercentageThousandths with a delay of 3 frames. This + * value requires an active OpenGL context. + * @requires_gl46 Extension @gl_extension{ARB,pipeline_statistics_query} + */ + PrimitiveClipRatio = 1 << 4 + #endif + }; + + /** + * @brief Measured values + * + * @see @ref GLFrameProfiler(Values, std::size_t), @ref setup() + */ + typedef Containers::EnumSet Values; + + /** + * @brief Default constructor + * + * Call @ref setup() to populate the profiler with measurements. + */ + explicit GLFrameProfiler(); + + /** + * @brief Constructor + * + * Equivalent to default-constructing an instance and calling + * @ref setup() afterwards. + */ + explicit GLFrameProfiler(Values values, std::size_t maxFrameCount); + + /** @brief Copying is not allowed */ + GLFrameProfiler(const GLFrameProfiler&) = delete; + + /** @brief Move constructor */ + GLFrameProfiler(GLFrameProfiler&&) noexcept; + + /** @brief Copying is not allowed */ + GLFrameProfiler& operator=(const GLFrameProfiler&) = delete; + + /** @brief Move assignment */ + GLFrameProfiler& operator=(GLFrameProfiler&&) noexcept; + + ~GLFrameProfiler(); + + /** + * @brief Setup measured values + * @param values List of measuremed values + * @param maxFrameCount Max frame count over which to calculate a + * moving average. Expected to be at least @cpp 1 @ce. + * + * Calling @ref setup() on an already set up profiler will replace + * existing measurements with @p measurements and reset + * @ref measuredFrameCount() back to @cpp 0 @ce. + */ + void setup(Values values, std::size_t maxFrameCount); + + /** + * @brief Measured values + * + * Corresponds to the @p values parameter passed to + * @ref GLFrameProfiler(Values, std::size_t) or @ref setup(). + */ + Values values() const; + + /** + * @brief Whether given measurement is available + * + * Returns @cpp true @ce if enough frames was captured to calculate + * given @p value, @cpp false @ce otherwise. Expects that @p value was + * enabled. + */ + bool isMeasurementAvailable(Value value) const; + + using FrameProfiler::isMeasurementAvailable; + + /** + * @brief Mean frame time in nanoseconds + * + * Expects that @ref Value::FrameTime was enabled, and that measurement + * data is available. See the flag documentation for more information. + * @see @ref isMeasurementAvailable(), @ref measurementMean() + */ + Double frameTimeMean() const; + + /** + * @brief Mean CPU frame duration in nanoseconds + * + * Expects that @ref Value::CpuDuration was enabled, and that + * measurement data is available. See the flag documentation for more + * information. + * @see @ref isMeasurementAvailable(), @ref measurementMean() + */ + Double cpuDurationMean() const; + + /** + * @brief Mean GPU frame duration in nanoseconds + * + * Expects that @ref Value::GpuDuration was enabled, and that + * measurement data is available. See the flag documentation for more + * information. + * @see @ref isMeasurementAvailable(), @ref measurementMean() + */ + Double gpuDurationMean() const; + + #ifndef MAGNUM_TARGET_GLES + /** + * @brief Mean vertex fetch ratio in thousandths + * + * Expects that @ref Value::VertexFetchRatio was enabled, and that + * measurement data is available. See the flag documentation for more + * information. + * @requires_gl46 Extension @gl_extension{ARB,pipeline_statistics_query} + * @see @ref isMeasurementAvailable(), @ref measurementMean() + */ + Double vertexFetchRatioMean() const; + + /** + * @brief Mean primitive clip ratio in percentage thousandths + * + * Expects that @ref Value::PrimitiveClipRatio was enabled, and that + * measurement data is available. See the flag documentation for more + * information. + * @requires_gl46 Extension @gl_extension{ARB,pipeline_statistics_query} + * @see @ref isMeasurementAvailable(), @ref measurementMean() + */ + Double primitiveClipRatioMean() const; + #endif + + private: + using FrameProfiler::setup; + + struct State; + Containers::Pointer _state; +}; + +CORRADE_ENUMSET_OPERATORS(GLFrameProfiler::Values) + +/** +@debugoperatorclassenum{GLFrameProfiler,GLFrameProfiler::Value} +@m_since_latest +*/ +MAGNUM_DEBUGTOOLS_EXPORT Debug& operator<<(Debug& debug, GLFrameProfiler::Value value); + +/** +@debugoperatorclassenum{GLFrameProfiler,GLFrameProfiler::Values} +@m_since_latest +*/ +MAGNUM_DEBUGTOOLS_EXPORT Debug& operator<<(Debug& debug, GLFrameProfiler::Values value); +#endif + +}} + +#endif diff --git a/src/Magnum/DebugTools/Test/CMakeLists.txt b/src/Magnum/DebugTools/Test/CMakeLists.txt index fab9805af..9e7df9b00 100644 --- a/src/Magnum/DebugTools/Test/CMakeLists.txt +++ b/src/Magnum/DebugTools/Test/CMakeLists.txt @@ -23,6 +23,10 @@ # DEALINGS IN THE SOFTWARE. # +corrade_add_test(DebugToolsFrameProfilerTest FrameProfilerTest.cpp + LIBRARIES MagnumDebugToolsTestLib) +set_target_properties(DebugToolsFrameProfilerTest PROPERTIES FOLDER "Magnum/DebugTools/Test") + if(WITH_TRADE) # Otherwise CMake complains that Corrade::PluginManager is not found, wtf find_package(Corrade REQUIRED PluginManager) @@ -94,6 +98,10 @@ if(TARGET_GL) endif() if(BUILD_GL_TESTS) + corrade_add_test(DebugToolsFrameProfilerGLTest FrameProfilerGLTest.cpp + LIBRARIES MagnumDebugTools MagnumOpenGLTester) + set_target_properties(DebugToolsFrameProfilerTest PROPERTIES FOLDER "Magnum/DebugTools/Test") + corrade_add_test(DebugToolsTextureImageGLTest TextureImageGLTest.cpp LIBRARIES MagnumDebugTools MagnumOpenGLTester) set_target_properties(DebugToolsTextureImageGLTest PROPERTIES FOLDER "Magnum/DebugTools/Test") diff --git a/src/Magnum/DebugTools/Test/FrameProfilerGLTest.cpp b/src/Magnum/DebugTools/Test/FrameProfilerGLTest.cpp new file mode 100644 index 000000000..2cf573f74 --- /dev/null +++ b/src/Magnum/DebugTools/Test/FrameProfilerGLTest.cpp @@ -0,0 +1,242 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 "Magnum/DebugTools/FrameProfiler.h" +#include "Magnum/GL/Context.h" +#include "Magnum/GL/Extensions.h" +#include "Magnum/GL/Framebuffer.h" +#include "Magnum/GL/Mesh.h" +#include "Magnum/GL/OpenGLTester.h" +#include "Magnum/GL/Renderbuffer.h" +#include "Magnum/GL/RenderbufferFormat.h" +#include "Magnum/MeshTools/Compile.h" +#include "Magnum/Primitives/Cube.h" +#include "Magnum/Shaders/Flat.h" +#include "Magnum/Trade/MeshData.h" + +namespace Magnum { namespace DebugTools { namespace Test { namespace { + +struct FrameProfilerGLTest: GL::OpenGLTester { + explicit FrameProfilerGLTest(); + + void test(); + #ifndef MAGNUM_TARGET_GLES + void vertexFetchRatioDivisionByZero(); + void primitiveClipRatioDivisionByZero(); + #endif +}; + +struct { + const char* name; + GLFrameProfiler::Values values; +} Data[]{ + {"gpu duration", GLFrameProfiler::Value::GpuDuration}, + {"cpu duration + gpu duration", GLFrameProfiler::Value::CpuDuration|GLFrameProfiler::Value::GpuDuration}, + {"frame time + gpu duration", GLFrameProfiler::Value::FrameTime|GLFrameProfiler::Value::GpuDuration}, + #ifndef MAGNUM_TARGET_GLES + {"gpu duration + vertex fetch ratio", GLFrameProfiler::Value::GpuDuration|GLFrameProfiler::Value::VertexFetchRatio}, + {"vertex fetch ratio + primitive clip ratio", GLFrameProfiler::Value::VertexFetchRatio|GLFrameProfiler::Value::PrimitiveClipRatio} + #endif +}; + +FrameProfilerGLTest::FrameProfilerGLTest() { + addInstancedTests({&FrameProfilerGLTest::test}, + Containers::arraySize(Data)); + + #ifndef MAGNUM_TARGET_GLES + addTests({&FrameProfilerGLTest::vertexFetchRatioDivisionByZero, + &FrameProfilerGLTest::primitiveClipRatioDivisionByZero}); + #endif +} + +void FrameProfilerGLTest::test() { + auto&& data = Data[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + if(data.values & GLFrameProfiler::Value::GpuDuration) { + #ifndef MAGNUM_TARGET_GLES + if(!GL::Context::current().isExtensionSupported()) + CORRADE_SKIP(GL::Extensions::ARB::timer_query::string() + std::string(" is not available")); + #elif defined(MAGNUM_TARGET_WEBGL) && !defined(MAGNUM_TARGET_GLES2) + if(!GL::Context::current().isExtensionSupported()) + CORRADE_SKIP(GL::Extensions::EXT::disjoint_timer_query_webgl2::string() + std::string(" is not available")); + #else + if(!GL::Context::current().isExtensionSupported()) + CORRADE_SKIP(GL::Extensions::EXT::disjoint_timer_query::string() + std::string(" is not available")); + #endif + } + + #ifndef MAGNUM_TARGET_GLES + if((data.values & GLFrameProfiler::Value::VertexFetchRatio) && !GL::Context::current().isExtensionSupported()) + CORRADE_SKIP(GL::Extensions::ARB::pipeline_statistics_query::string() + std::string(" is not available")); + #endif + + /* Bind some FB to avoid errors on contexts w/o default FB */ + GL::Renderbuffer color; + color.setStorage( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + GL::RenderbufferFormat::RGBA8, + #else + GL::RenderbufferFormat::RGBA4, + #endif + Vector2i{32}); + GL::Framebuffer fb{{{}, Vector2i{32}}}; + fb.attachRenderbuffer(GL::Framebuffer::ColorAttachment{0}, color) + .bind(); + + Shaders::Flat3D shader; + GL::Mesh mesh = MeshTools::compile(Primitives::cubeSolid()); + + GLFrameProfiler profiler{data.values, 4}; + CORRADE_COMPARE(profiler.maxFrameCount(), 4); + + for(auto value: {GLFrameProfiler::Value::CpuDuration, + GLFrameProfiler::Value::GpuDuration, + #ifndef MAGNUM_TARGET_GLES + GLFrameProfiler::Value::VertexFetchRatio, + GLFrameProfiler::Value::PrimitiveClipRatio + #endif + }) + if(data.values & value) + CORRADE_VERIFY(!profiler.isMeasurementAvailable(value)); + + profiler.beginFrame(); + shader.draw(mesh); + Utility::System::sleep(1); + profiler.endFrame(); + + profiler.beginFrame(); + shader.draw(mesh); + profiler.endFrame(); + + Utility::System::sleep(10); + + profiler.beginFrame(); + shader.draw(mesh); + Utility::System::sleep(1); + profiler.endFrame(); + + profiler.beginFrame(); + shader.draw(mesh); + Utility::System::sleep(1); + profiler.endFrame(); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + /* The GPU time should not be a total zero. Can't test upper bound because + (especially on overloaded CIs) it all takes a magnitude more than + expected. */ + if(data.values & GLFrameProfiler::Value::GpuDuration) { + CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::GpuDuration)); + CORRADE_COMPARE_AS(profiler.gpuDurationMean(), 100, + TestSuite::Compare::Greater); + } + + /* 3/4 frames took 1 ms, the ideal average is 0.75 ms. Can't test upper + bound because (especially on overloaded CIs) it all takes a magnitude + more than expected. */ + if(data.values & GLFrameProfiler::Value::CpuDuration) { + CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::CpuDuration)); + CORRADE_COMPARE_AS(profiler.cpuDurationMean(), 0.70*1000*1000, + TestSuite::Compare::GreaterOrEqual); + } + + #ifndef MAGNUM_TARGET_GLES + /* 24 unique vertices in 12 triangles, ideal ratio is 24/36 */ + if(data.values & GLFrameProfiler::Value::VertexFetchRatio) { + CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::VertexFetchRatio)); + CORRADE_COMPARE_WITH(profiler.vertexFetchRatioMean()/1000, 0.6667, + TestSuite::Compare::around(0.1)); + } + + /* We use a default transformation, which means the whole cube should be + visible, nothing clipped */ + if(data.values & GLFrameProfiler::Value::PrimitiveClipRatio) { + CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::PrimitiveClipRatio)); + CORRADE_COMPARE(profiler.primitiveClipRatioMean()/1000, 0.0); + } + #endif +} + +#ifndef MAGNUM_TARGET_GLES +void FrameProfilerGLTest::vertexFetchRatioDivisionByZero() { + if(!GL::Context::current().isExtensionSupported()) + CORRADE_SKIP(GL::Extensions::ARB::pipeline_statistics_query::string() + std::string(" is not available")); + + GLFrameProfiler profiler{GLFrameProfiler::Value::VertexFetchRatio, 4}; + + profiler.beginFrame(); + profiler.endFrame(); + + profiler.beginFrame(); + profiler.endFrame(); + + profiler.beginFrame(); + profiler.endFrame(); + + profiler.beginFrame(); + profiler.endFrame(); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + /* No draws happened, so the ratio should be 0 (and not crashing with a + division by zero) */ + CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::VertexFetchRatio)); + CORRADE_COMPARE(profiler.vertexFetchRatioMean(), 0.0); +} + +void FrameProfilerGLTest::primitiveClipRatioDivisionByZero() { + if(!GL::Context::current().isExtensionSupported()) + CORRADE_SKIP(GL::Extensions::ARB::pipeline_statistics_query::string() + std::string(" is not available")); + + GLFrameProfiler profiler{GLFrameProfiler::Value::PrimitiveClipRatio, 4}; + + profiler.beginFrame(); + profiler.endFrame(); + + profiler.beginFrame(); + profiler.endFrame(); + + profiler.beginFrame(); + profiler.endFrame(); + + profiler.beginFrame(); + profiler.endFrame(); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + /* No draws happened, so the ratio should be 0 (and not crashing with a + division by zero) */ + CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::PrimitiveClipRatio)); + CORRADE_COMPARE(profiler.primitiveClipRatioMean(), 0.0); +} +#endif + +}}}} + +CORRADE_TEST_MAIN(Magnum::DebugTools::Test::FrameProfilerGLTest) diff --git a/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp b/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp new file mode 100644 index 000000000..a5ea4158d --- /dev/null +++ b/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp @@ -0,0 +1,1056 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 +#include + +#include "Magnum/DebugTools/FrameProfiler.h" + +namespace Magnum { namespace DebugTools { namespace Test { namespace { + +struct FrameProfilerTest: TestSuite::Tester { + explicit FrameProfilerTest(); + + void defaultConstructed(); + void noMeasurements(); + + void singleFrame(); + void multipleFrames(); + + void enableDisable(); + void reSetup(); + + void copy(); + void move(); + + void frameCountZero(); + void delayZero(); + void delayTooLittleFrames(); + void startStopFrameUnexpected(); + void measurementOutOfBounds(); + void dataNotAvailableYet(); + + void statistics(); + + #ifdef MAGNUM_TARGET_GL + void gl(); + void glNotEnabled(); + #endif + + void debugUnits(); + #ifdef MAGNUM_TARGET_GL + void debugGLValue(); + void debugGLValues(); + #endif +}; + +struct { + const char* name; + bool delayed; +} SingleFrameData[]{ + {"", false}, + {"delayed by 1", true} +}; + +struct { + const char* name; + bool delayed; + UnsignedInt delay; +} MultipleFramesData[]{ + {"", false, 1}, + {"delayed by 1", true, 1}, + {"delayed by 2", true, 2}, + {"delayed by 3", true, 3} +}; + +#ifdef MAGNUM_TARGET_GL +struct { + const char* name; + GLFrameProfiler::Values values; + UnsignedInt measurementCount; + UnsignedInt measurementDelay; +} GLData[]{ + {"empty", {}, 0, 1}, + {"frame time", GLFrameProfiler::Value::FrameTime, 1, 2}, + {"cpu duration", GLFrameProfiler::Value::CpuDuration, 1, 1}, + {"frame time + cpu duration", GLFrameProfiler::Value::FrameTime|GLFrameProfiler::Value::CpuDuration, 2, 2} +}; +#endif + +FrameProfilerTest::FrameProfilerTest() { + addTests({&FrameProfilerTest::defaultConstructed, + &FrameProfilerTest::noMeasurements}); + + addInstancedTests({&FrameProfilerTest::singleFrame}, + Containers::arraySize(SingleFrameData)); + addInstancedTests({&FrameProfilerTest::multipleFrames}, + Containers::arraySize(MultipleFramesData)); + + addTests({&FrameProfilerTest::enableDisable, + &FrameProfilerTest::reSetup, + + &FrameProfilerTest::copy, + &FrameProfilerTest::move, + + &FrameProfilerTest::frameCountZero, + &FrameProfilerTest::delayZero, + &FrameProfilerTest::delayTooLittleFrames, + &FrameProfilerTest::startStopFrameUnexpected, + &FrameProfilerTest::measurementOutOfBounds, + &FrameProfilerTest::dataNotAvailableYet, + + &FrameProfilerTest::statistics}); + + #ifdef MAGNUM_TARGET_GL + addInstancedTests({&FrameProfilerTest::gl}, + Containers::arraySize(GLData)); + #endif + + addTests({ + #ifdef MAGNUM_TARGET_GL + &FrameProfilerTest::glNotEnabled, + #endif + + &FrameProfilerTest::debugUnits, + #ifdef MAGNUM_TARGET_GL + &FrameProfilerTest::debugGLValue, + &FrameProfilerTest::debugGLValues, + #endif + }); +} + +void FrameProfilerTest::defaultConstructed() { + FrameProfiler profiler; + CORRADE_COMPARE(profiler.maxFrameCount(), 1); + CORRADE_COMPARE(profiler.measuredFrameCount(), 0); + CORRADE_COMPARE(profiler.measurementCount(), 0); + CORRADE_COMPARE(profiler.statistics(), "Last 0 frames:"); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measuredFrameCount(), 1); + + /* Shouldn't crash on any silly division by zero even when called a second + time */ + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measuredFrameCount(), 2); +} + +void FrameProfilerTest::noMeasurements() { + FrameProfiler profiler{{}, 3}; + CORRADE_COMPARE(profiler.maxFrameCount(), 3); + CORRADE_COMPARE(profiler.measuredFrameCount(), 0); + CORRADE_COMPARE(profiler.measurementCount(), 0); + CORRADE_COMPARE(profiler.statistics(), "Last 0 frames:"); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measuredFrameCount(), 1); + + /* Shouldn't crash on any silly division by zero even after a wraparound */ + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measuredFrameCount(), 2); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measuredFrameCount(), 3); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measuredFrameCount(), 4); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measuredFrameCount(), 5); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measuredFrameCount(), 6); +} + +void FrameProfilerTest::singleFrame() { + auto&& data = SingleFrameData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + UnsignedLong time = 0, memory = 50; + FrameProfiler profiler; + if(!data.delayed) { + profiler.setup({ + FrameProfiler::Measurement{ + "Lag", FrameProfiler::Units::Nanoseconds, + [](void* state) { + *static_cast(state) += 15; + }, + [](void* state) { + return *static_cast(state) - 15; + }, &time}, + FrameProfiler::Measurement{ + "Bloat", FrameProfiler::Units::Bytes, + [](void* state) { + *static_cast(state) *= 2; + }, + [](void* state) { + return *static_cast(state) - 100; + }, &memory}, + FrameProfiler::Measurement{ + "Constant", FrameProfiler::Units::Count, + [](void*) {}, + [](void*) { + return UnsignedLong{100000}; + }, nullptr} + }, 1); + } else { + profiler.setup({ + FrameProfiler::Measurement{ + "Lag", FrameProfiler::Units::Nanoseconds, 1, + [](void* state, UnsignedInt current) { + CORRADE_COMPARE(current, 0); + static_cast(state)[current] += 30; + }, + [](void* state, UnsignedInt current) { + CORRADE_COMPARE(current, 0); + static_cast(state)[current] -= 15; + }, + [](void* state, UnsignedInt previous, UnsignedInt current) { + CORRADE_COMPARE(previous, 0); + CORRADE_COMPARE(current, 0); + return static_cast(state)[previous] - 15; + }, &time}, + FrameProfiler::Measurement{ + "Bloat", FrameProfiler::Units::Bytes, 1, + [](void* state, UnsignedInt current) { + CORRADE_COMPARE(current, 0); + static_cast(state)[current] *= 4; + }, + [](void* state, UnsignedInt current) { + CORRADE_COMPARE(current, 0); + static_cast(state)[current] /= 2; + }, + [](void* state, UnsignedInt previous, UnsignedInt current) { + CORRADE_COMPARE(previous, 0); + CORRADE_COMPARE(current, 0); + return static_cast(state)[previous] - 100; + }, &memory}, + FrameProfiler::Measurement{ + "Constant", FrameProfiler::Units::Count, 1, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt, UnsignedInt) { + return UnsignedLong{100000}; + }, nullptr} + }, 1); + } + CORRADE_COMPARE(profiler.maxFrameCount(), 1); + CORRADE_COMPARE(profiler.measuredFrameCount(), 0); + CORRADE_COMPARE(profiler.measurementCount(), 3); + + CORRADE_COMPARE(profiler.measurementName(0), "Lag"); + CORRADE_COMPARE(profiler.measurementUnits(0), FrameProfiler::Units::Nanoseconds); + CORRADE_COMPARE(profiler.measurementDelay(0), 1); + + CORRADE_COMPARE(profiler.measurementName(1), "Bloat"); + CORRADE_COMPARE(profiler.measurementUnits(1), FrameProfiler::Units::Bytes); + CORRADE_COMPARE(profiler.measurementDelay(1), 1); + + CORRADE_COMPARE(profiler.measurementName(2), "Constant"); + CORRADE_COMPARE(profiler.measurementUnits(2), FrameProfiler::Units::Count); + CORRADE_COMPARE(profiler.measurementDelay(2), 1); + + CORRADE_VERIFY(!profiler.isMeasurementAvailable(0)); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(1)); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(2)); + + profiler.beginFrame(); + CORRADE_COMPARE(time, data.delayed ? 30 : 15); + CORRADE_COMPARE(memory, data.delayed ? 200 : 100); + CORRADE_COMPARE(profiler.measuredFrameCount(), 0); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(0)); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(1)); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(2)); + + profiler.endFrame(); + CORRADE_COMPARE(time, 15); + CORRADE_COMPARE(memory, 100); + CORRADE_COMPARE(profiler.measuredFrameCount(), 1); + CORRADE_VERIFY(profiler.isMeasurementAvailable(0)); + CORRADE_VERIFY(profiler.isMeasurementAvailable(1)); + CORRADE_VERIFY(profiler.isMeasurementAvailable(2)); + CORRADE_COMPARE(profiler.measurementMean(0), 0.0); + CORRADE_COMPARE(profiler.measurementMean(1), 0.0); + CORRADE_COMPARE(profiler.measurementMean(2), 100000.0); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(time, 30); + CORRADE_COMPARE(memory, 200); + CORRADE_COMPARE(profiler.measuredFrameCount(), 2); + CORRADE_COMPARE(profiler.measurementMean(0), 15.0); + CORRADE_COMPARE(profiler.measurementMean(1), 100.0); + CORRADE_COMPARE(profiler.measurementMean(2), 100000.0); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(time, 45); + CORRADE_COMPARE(memory, 400); + CORRADE_COMPARE(profiler.measurementMean(0), 30.0); + CORRADE_COMPARE(profiler.measurementMean(1), 300.0); + CORRADE_COMPARE(profiler.measurementMean(2), 100000.0); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(time, 60); + CORRADE_COMPARE(memory, 800); + CORRADE_COMPARE(profiler.measurementMean(0), 45.0); + CORRADE_COMPARE(profiler.measurementMean(1), 700.0); + CORRADE_COMPARE(profiler.measurementMean(2), 100000.0); +} + +void FrameProfilerTest::multipleFrames() { + auto&& data = MultipleFramesData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct State { + UnsignedLong currentTime, currentMemory; + UnsignedLong time[3]; + UnsignedLong memory[3]; + UnsignedInt delay; + } state {0, 50, {0, 0, 0}, {50, 0, 0}, data.delay}; + FrameProfiler profiler; + if(!data.delayed) { + profiler.setup({ + FrameProfiler::Measurement{ + "Lag", FrameProfiler::Units::Nanoseconds, + [](void* state) { + *static_cast(state) += 15; + }, + [](void* state) { + return *static_cast(state) - 15; + }, &state.time[0]}, + FrameProfiler::Measurement{ + "Bloat", FrameProfiler::Units::Bytes, + [](void* state) { + *static_cast(state) *= 2; + }, + [](void* state) { + return *static_cast(state) - 100; + }, &state.memory[0]}, + FrameProfiler::Measurement{ + "Constant", FrameProfiler::Units::Count, + [](void*) {}, + [](void*) { + return UnsignedLong{100000}; + }, nullptr} + }, 3); + } else { + profiler.setup({ + FrameProfiler::Measurement{ + "Lag", FrameProfiler::Units::Nanoseconds, data.delay, + [](void* state, UnsignedInt current) { + auto& s = *static_cast(state); + CORRADE_COMPARE_AS(current, s.delay, TestSuite::Compare::Less); + s.time[current] = (s.currentTime += 15) + 15; + }, + [](void* state, UnsignedInt current) { + auto& s = *static_cast(state); + CORRADE_COMPARE_AS(current, s.delay, TestSuite::Compare::Less); + s.time[current] -= 15; + }, + [](void* state, UnsignedInt previous, UnsignedInt current) { + auto& s = *static_cast(state); + CORRADE_COMPARE_AS(previous, s.delay, TestSuite::Compare::Less); + CORRADE_COMPARE_AS(current, s.delay, TestSuite::Compare::Less); + CORRADE_VERIFY(current + 1 == previous || (current == s.delay - 1 && previous == 0)); + return s.time[previous] - 15; + }, &state}, + FrameProfiler::Measurement{ + "Bloat", FrameProfiler::Units::Bytes, data.delay, + [](void* state, UnsignedInt current) { + auto& s = *static_cast(state); + CORRADE_COMPARE_AS(current, s.delay, TestSuite::Compare::Less); + s.memory[current] = (s.currentMemory *= 2)*2; + }, + [](void* state, UnsignedInt current) { + auto& s = *static_cast(state); + CORRADE_COMPARE_AS(current, s.delay, TestSuite::Compare::Less); + s.memory[current] /= 2; + }, + [](void* state, UnsignedInt previous, UnsignedInt current) { + auto& s = *static_cast(state); + CORRADE_COMPARE_AS(previous, s.delay, TestSuite::Compare::Less); + CORRADE_COMPARE_AS(current, s.delay, TestSuite::Compare::Less); + CORRADE_VERIFY(current + 1 == previous || (current == s.delay - 1 && previous == 0)); + return s.memory[previous] - 100; + }, &state}, + FrameProfiler::Measurement{ + "Undelayed constant", FrameProfiler::Units::Count, 1, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt, UnsignedInt) { + return UnsignedLong{100000}; + }, nullptr} + }, 3); + } + CORRADE_COMPARE(profiler.maxFrameCount(), 3); + CORRADE_COMPARE(profiler.measuredFrameCount(), 0); + CORRADE_COMPARE(profiler.measurementDelay(0), data.delay); + CORRADE_COMPARE(profiler.measurementDelay(1), data.delay); + CORRADE_COMPARE(profiler.measurementDelay(2), 1); + + for(std::size_t i = 0; i != data.delay - 1; ++i) { + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(state.time[i], 15*(i + 1)); + CORRADE_COMPARE(state.memory[i], 100*(i + 1)); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(0)); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(1)); + CORRADE_VERIFY(profiler.isMeasurementAvailable(2)); + } + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(state.time[0 % data.delay], 15); + CORRADE_COMPARE(state.memory[0 % data.delay], 100); + CORRADE_VERIFY(profiler.isMeasurementAvailable(0)); + CORRADE_VERIFY(profiler.isMeasurementAvailable(1)); + CORRADE_VERIFY(profiler.isMeasurementAvailable(2)); + CORRADE_COMPARE(profiler.measuredFrameCount(), 0 + data.delay); + CORRADE_COMPARE(profiler.measurementMean(0), 0.0); + CORRADE_COMPARE(profiler.measurementMean(1), 0.0); + CORRADE_COMPARE(profiler.measurementMean(2), 100000.0); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(state.time[1 % data.delay], 30); + CORRADE_COMPARE(state.memory[1 % data.delay], 200); + CORRADE_COMPARE(profiler.measuredFrameCount(), 1 + data.delay); + CORRADE_COMPARE(profiler.measurementMean(0), (15.0 + 0.0)/2); + CORRADE_COMPARE(profiler.measurementMean(1), (100.0 + 0.0)/2); + CORRADE_COMPARE(profiler.measurementMean(2), 100000.0); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(state.time[2 % data.delay], 45); + CORRADE_COMPARE(state.memory[2 % data.delay], 400); + CORRADE_COMPARE(profiler.measuredFrameCount(), 2 + data.delay); + CORRADE_COMPARE(profiler.measurementMean(0), (30.0 + 15.0)/3); + CORRADE_COMPARE(profiler.measurementMean(1), (300.0 + 100.0)/3); + CORRADE_COMPARE(profiler.measurementMean(2), 100000.0); + + /* At this point it wraps around and should be evicting old values from the + moving average */ + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(state.time[3 % data.delay], 60); + CORRADE_COMPARE(state.memory[3 % data.delay], 800); + CORRADE_COMPARE(profiler.measuredFrameCount(), 3 + data.delay); + CORRADE_COMPARE(profiler.measurementMean(0), (45.0 + 30.0 + 15.0)/3); + CORRADE_COMPARE(profiler.measurementMean(1), (700.0 + 300.0 + 100.0)/3); + CORRADE_COMPARE(profiler.measurementMean(2), 100000.0); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(state.time[4 % data.delay], 75); + CORRADE_COMPARE(state.memory[4 % data.delay], 1600); + CORRADE_COMPARE(profiler.measuredFrameCount(), 4 + data.delay); + CORRADE_COMPARE(profiler.measurementMean(0), (60 + 45.0 + 30.0)/3); + CORRADE_COMPARE(profiler.measurementMean(1), (1500.0 + 700.0 + 300.0)/3); + CORRADE_COMPARE(profiler.measurementMean(2), 100000.0); + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(state.time[5 % data.delay], 90); + CORRADE_COMPARE(state.memory[5 % data.delay], 3200); + CORRADE_COMPARE(profiler.measuredFrameCount(), 5 + data.delay); + CORRADE_COMPARE(profiler.measurementMean(0), (75.0 + 60.0 + 45.0)/3); + CORRADE_COMPARE(profiler.measurementMean(1), (3100.0 + 1500.0 + 700.0)/3); + CORRADE_COMPARE(profiler.measurementMean(2), 100000.0); +} + +void FrameProfilerTest::enableDisable() { + UnsignedLong i = 15; + FrameProfiler profiler{{ + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, 2, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void* state, UnsignedInt, UnsignedInt) { + return (*static_cast(state))++; + }, &i}, + }, 5}; + + profiler.beginFrame(); + profiler.endFrame(); + profiler.beginFrame(); + profiler.endFrame(); + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measurementCount(), 1); + CORRADE_COMPARE(profiler.measuredFrameCount(), 3); + CORRADE_COMPARE(profiler.measurementDelay(0), 2); + CORRADE_VERIFY(profiler.isMeasurementAvailable(0)); + CORRADE_COMPARE(profiler.measurementMean(0), 15.5); + + /* It should only freeze everything, not wipe out any data */ + profiler.disable(); + CORRADE_COMPARE(profiler.measurementCount(), 1); + CORRADE_COMPARE(profiler.measuredFrameCount(), 3); + CORRADE_COMPARE(profiler.measurementDelay(0), 2); + CORRADE_VERIFY(profiler.isMeasurementAvailable(0)); + CORRADE_COMPARE(profiler.measurementMean(0), 15.5); + + /* These are a no-op now */ + profiler.beginFrame(); + profiler.endFrame(); + profiler.beginFrame(); + CORRADE_COMPARE(profiler.measurementCount(), 1); + CORRADE_COMPARE(profiler.measuredFrameCount(), 3); + CORRADE_COMPARE(profiler.measurementDelay(0), 2); + CORRADE_VERIFY(profiler.isMeasurementAvailable(0)); + CORRADE_COMPARE(profiler.measurementMean(0), 15.5); + + /* Enabling should reset the data to have a clean slate, but not the + measurements */ + profiler.enable(); + CORRADE_COMPARE(profiler.measurementCount(), 1); + CORRADE_COMPARE(profiler.maxFrameCount(), 5); + CORRADE_COMPARE(profiler.measuredFrameCount(), 0); + CORRADE_COMPARE(profiler.measurementDelay(0), 2); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(0)); + + /* Even though there was no call to endFrame() before, reset() should make + beginFrame() expected again */ + i = 0; + profiler.beginFrame(); + profiler.endFrame(); + profiler.beginFrame(); + profiler.endFrame(); + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measurementCount(), 1); + CORRADE_COMPARE(profiler.measuredFrameCount(), 3); + CORRADE_COMPARE(profiler.measurementDelay(0), 2); + CORRADE_VERIFY(profiler.isMeasurementAvailable(0)); + /* The per-measurement moving sum should be reset by enable() as well, so + the 15s from before won't contribute to the mean anymore */ + CORRADE_COMPARE(profiler.measurementMean(0), 0.5); +} + +void FrameProfilerTest::reSetup() { + FrameProfiler profiler{{ + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, 3, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt, UnsignedInt) { return UnsignedLong{}; }, nullptr}, + }, 5}; + + profiler.beginFrame(); + profiler.endFrame(); + profiler.beginFrame(); + + /* Setup should replace everything */ + profiler.setup({ + FrameProfiler::Measurement{ + "Lag", FrameProfiler::Units::Nanoseconds, + [](void*) {}, + [](void*) { return UnsignedLong{}; }, + nullptr}, + FrameProfiler::Measurement{ + "Bloat", FrameProfiler::Units::Bytes, + [](void*) {}, + [](void*) { return UnsignedLong{}; }, + nullptr}, + }, 10); + CORRADE_COMPARE(profiler.measurementCount(), 2); + CORRADE_COMPARE(profiler.maxFrameCount(), 10); + CORRADE_COMPARE(profiler.measuredFrameCount(), 0); + CORRADE_COMPARE(profiler.measurementDelay(0), 1); + CORRADE_COMPARE(profiler.measurementDelay(1), 1); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(0)); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(1)); + + /* Even though there was no call to endFrame() before, setup() should make + beginFrame() expected again */ + profiler.beginFrame(); + profiler.endFrame(); +} + +void FrameProfilerTest::copy() { + CORRADE_VERIFY(!(std::is_constructible{})); + CORRADE_VERIFY(!(std::is_assignable{})); +} + +void FrameProfilerTest::move() { + /* Have two state variables, one in a subclass, one outside. On move the + pointer to a subclass should get patched but the outside not */ + + UnsignedLong i = 15; + struct MyProfiler: FrameProfiler { + UnsignedLong j = 30; + } a; + a.setup({ + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, + [](void*) {}, + [](void* state) { + return (*static_cast(state))++; + }, &i}, + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, 2, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void* state, UnsignedInt, UnsignedInt) { + return static_cast(state)->j++; + }, &a}, + }, 5); + + /* Move construction */ + MyProfiler b{std::move(a)}; + a.j = 100; /* This shouldn't affect b's measurements anymore */ + b.beginFrame(); + b.endFrame(); + b.beginFrame(); + b.endFrame(); + b.beginFrame(); + b.endFrame(); + CORRADE_COMPARE(b.measurementCount(), 2); + CORRADE_COMPARE(b.measuredFrameCount(), 3); + CORRADE_COMPARE(b.measurementDelay(0), 1); + CORRADE_COMPARE(b.measurementDelay(1), 2); + CORRADE_COMPARE(b.measurementMean(0), 16.0); + CORRADE_COMPARE(b.measurementMean(1), 30.5); + + /* Another fully populated instance */ + UnsignedLong k = 45; + MyProfiler c; + c.j = 60; + c.setup({ + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, + [](void*) {}, + [](void* state) { + return (*static_cast(state))++; + }, &k}, + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, 3, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void* state, UnsignedInt, UnsignedInt) { + return static_cast(state)->j++; + }, &c}, + }, 5); + c.beginFrame(); + c.endFrame(); + c.beginFrame(); + c.endFrame(); + c.beginFrame(); + c.endFrame(); + c.beginFrame(); + c.endFrame(); + CORRADE_COMPARE(c.measurementCount(), 2); + CORRADE_COMPARE(c.measuredFrameCount(), 4); + CORRADE_COMPARE(c.measurementDelay(0), 1); + CORRADE_COMPARE(c.measurementDelay(1), 3); + CORRADE_COMPARE(c.measurementMean(0), 46.5); + CORRADE_COMPARE(c.measurementMean(1), 60.5); + + /* Move assignment */ + CORRADE_COMPARE(c.j, 62); + c = std::move(b); + b.j = 62; /* std::move() didn't swap this one, so we do; this shouldn't + affect c's measurements anymore */ + c.beginFrame(); + c.endFrame(); + c.beginFrame(); + c.endFrame(); + CORRADE_COMPARE(c.measurementCount(), 2); + CORRADE_COMPARE(c.measuredFrameCount(), 5); + CORRADE_COMPARE(c.measurementDelay(0), 1); + CORRADE_COMPARE(c.measurementDelay(1), 2); + CORRADE_COMPARE(c.measurementMean(0), 17.0); + CORRADE_COMPARE(c.measurementMean(1), 31.5); + + /* Calling these on the swapped instance should affect only itself */ + b.beginFrame(); + b.endFrame(); + CORRADE_COMPARE(b.measurementMean(0), 47.0); /* originally c */ + CORRADE_COMPARE(b.measurementMean(1), 61.0); /* originally c */ + CORRADE_COMPARE(c.measurementCount(), 2); + CORRADE_COMPARE(c.measuredFrameCount(), 5); + CORRADE_COMPARE(c.measurementDelay(0), 1); + CORRADE_COMPARE(c.measurementDelay(1), 2); + CORRADE_COMPARE(c.measurementMean(0), 17.0); + CORRADE_COMPARE(c.measurementMean(1), 31.5); + + CORRADE_VERIFY(std::is_nothrow_move_constructible::value); + CORRADE_VERIFY(std::is_nothrow_move_assignable::value); +} + +void FrameProfilerTest::delayZero() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + std::ostringstream out; + Error redirectError{&out}; + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, 0, + nullptr, nullptr, nullptr, nullptr}; + CORRADE_COMPARE(out.str(), "DebugTools::FrameProfiler::Measurement: delay can't be zero\n"); +} + +void FrameProfilerTest::frameCountZero() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + std::ostringstream out; + Error redirectError{&out}; + FrameProfiler{{}, 0}; + CORRADE_COMPARE(out.str(), "DebugTools::FrameProfiler::setup(): max frame count can't be zero\n"); +} + +void FrameProfilerTest::delayTooLittleFrames() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + std::ostringstream out; + Error redirectError{&out}; + FrameProfiler profiler{{ + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, 3, + nullptr, nullptr, nullptr, nullptr} + }, 2}; + CORRADE_COMPARE(out.str(), "DebugTools::FrameProfiler::setup(): max delay 3 is larger than max frame count 2\n"); +} + +void FrameProfilerTest::startStopFrameUnexpected() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + FrameProfiler profiler; + + std::ostringstream out; + { + Error redirectError{&out}; + profiler.endFrame(); + } + profiler.beginFrame(); /* this is not an error */ + { + Error redirectError{&out}; + profiler.beginFrame(); + } + CORRADE_COMPARE(out.str(), + "DebugTools::FrameProfiler::endFrame(): expected begin of frame\n" + "DebugTools::FrameProfiler::beginFrame(): expected end of frame\n"); +} + +void FrameProfilerTest::measurementOutOfBounds() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + FrameProfiler profiler{{ + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, + nullptr, nullptr, nullptr}, + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, + nullptr, nullptr, nullptr} + }, 1}; + + std::ostringstream out; + Error redirectError{&out}; + profiler.measurementName(2); + profiler.measurementUnits(2); + profiler.measurementDelay(2); + profiler.measurementMean(2); + CORRADE_COMPARE(out.str(), + "DebugTools::FrameProfiler::measurementName(): index 2 out of range for 2 measurements\n" + "DebugTools::FrameProfiler::measurementUnits(): index 2 out of range for 2 measurements\n" + "DebugTools::FrameProfiler::measurementDelay(): index 2 out of range for 2 measurements\n" + "DebugTools::FrameProfiler::measurementMean(): index 2 out of range for 2 measurements\n"); +} + +void FrameProfilerTest::dataNotAvailableYet() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + FrameProfiler profiler{{ + FrameProfiler::Measurement{"", FrameProfiler::Units::Count, 3, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt, UnsignedInt) { return UnsignedLong{}; }, nullptr}, + }, 5}; + + profiler.beginFrame(); + profiler.endFrame(); + CORRADE_COMPARE(profiler.measurementDelay(0), 3); + CORRADE_COMPARE(profiler.measuredFrameCount(), 1); + CORRADE_VERIFY(!profiler.isMeasurementAvailable(0)); + + std::ostringstream out; + Error redirectError{&out}; + profiler.measurementMean(0); + CORRADE_COMPARE(out.str(), + "DebugTools::FrameProfiler::measurementMean(): measurement data available after 2 more frames\n"); +} + +void FrameProfilerTest::statistics() { + UnsignedLong time = 0; + FrameProfiler profiler{{ + FrameProfiler::Measurement{ + "Lag", FrameProfiler::Units::Nanoseconds, 2, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void* state, UnsignedInt, UnsignedInt) { + return *static_cast(state) += 15; + }, &time}, + FrameProfiler::Measurement{ + "Bloat", FrameProfiler::Units::Bytes, + [](void*) {}, + [](void*) { + return UnsignedLong{1007300*1024*1024ull}; + }, nullptr}, + FrameProfiler::Measurement{ + "Age", FrameProfiler::Units::Nanoseconds, + [](void*) {}, + [](void*) { + return UnsignedLong{273*1000*1000}; + }, nullptr}, + FrameProfiler::Measurement{ + "GC", FrameProfiler::Units::Nanoseconds, 3, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt) {}, + [](void*, UnsignedInt, UnsignedInt) { + return UnsignedLong{52660}; + }, nullptr}, + FrameProfiler::Measurement{ + "Optimizations", FrameProfiler::Units::Count, + [](void*) {}, + [](void*) { + return UnsignedLong{0}; + }, nullptr}, + FrameProfiler::Measurement{ + "Frame time", FrameProfiler::Units::Nanoseconds, + [](void*) {}, + [](void*) { + return UnsignedLong{1000*1000*1000ull}; + }, nullptr}, + FrameProfiler::Measurement{ + "Sanity ratio", FrameProfiler::Units::RatioThousandths, + [](void*) {}, + [](void*) { + return UnsignedLong{855}; + }, nullptr}, + FrameProfiler::Measurement{ + "CPU usage", FrameProfiler::Units::PercentageThousandths, + [](void*) {}, + [](void*) { + return UnsignedLong{98655}; + }, nullptr} + }, 3}; + + CORRADE_COMPARE(profiler.statistics(), + "Last 0 frames:\n" + " Lag: -.-- s\n" + " Bloat: -.-- B\n" + " Age: -.-- s\n" + " GC: -.-- s\n" + " Optimizations: -.--\n" + " Frame time: -.-- s\n" + " Sanity ratio: -.--\n" + " CPU usage: -.-- %"); + + profiler.beginFrame(); + profiler.endFrame(); + + CORRADE_COMPARE(profiler.statistics(), + "Last 1 frames:\n" + " Lag: -.-- s\n" + " Bloat: 983.69 GB\n" + " Age: 273.00 ms\n" + " GC: -.-- s\n" + " Optimizations: 0.00\n" + " Frame time: 1.00 s\n" + " Sanity ratio: 0.85\n" + " CPU usage: 98.66 %"); + + profiler.beginFrame(); + profiler.endFrame(); + profiler.beginFrame(); + profiler.endFrame(); + profiler.beginFrame(); + profiler.endFrame(); + profiler.beginFrame(); + profiler.endFrame(); + profiler.beginFrame(); + profiler.endFrame(); + + CORRADE_COMPARE(profiler.statistics(), + "Last 3 frames:\n" + " Lag: 60.00 ns\n" + " Bloat: 983.69 GB\n" + " Age: 273.00 ms\n" + " GC: 52.66 µs\n" + " Optimizations: 0.00\n" + " Frame time: 1.00 s\n" + " Sanity ratio: 0.85\n" + " CPU usage: 98.66 %"); + + /* Disabling should print the last known state */ + profiler.disable(); + CORRADE_COMPARE(profiler.statistics(), + "Last 3 frames:\n" + " Lag: 60.00 ns\n" + " Bloat: 983.69 GB\n" + " Age: 273.00 ms\n" + " GC: 52.66 µs\n" + " Optimizations: 0.00\n" + " Frame time: 1.00 s\n" + " Sanity ratio: 0.85\n" + " CPU usage: 98.66 %"); + + /* Enabling again should go back to initial state */ + profiler.enable(); + CORRADE_COMPARE(profiler.statistics(), + "Last 0 frames:\n" + " Lag: -.-- s\n" + " Bloat: -.-- B\n" + " Age: -.-- s\n" + " GC: -.-- s\n" + " Optimizations: -.--\n" + " Frame time: -.-- s\n" + " Sanity ratio: -.--\n" + " CPU usage: -.-- %"); +} + +#ifdef MAGNUM_TARGET_GL +void FrameProfilerTest::gl() { + auto&& data = GLData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* Test that we use the right state pointers to survive a move */ + Containers::Pointer profiler_{Containers::InPlaceInit, + data.values, 4u}; + GLFrameProfiler profiler = std::move(*profiler_); + profiler_ = nullptr; + CORRADE_COMPARE(profiler.values(), data.values); + CORRADE_COMPARE(profiler.maxFrameCount(), 4); + CORRADE_COMPARE(profiler.measurementCount(), data.measurementCount); + + for(auto value: {GLFrameProfiler::Value::CpuDuration, + GLFrameProfiler::Value::FrameTime}) + if(data.values & value) + CORRADE_VERIFY(!profiler.isMeasurementAvailable(value)); + + profiler.beginFrame(); + Utility::System::sleep(1); + profiler.endFrame(); + + profiler.beginFrame(); + profiler.endFrame(); + + Utility::System::sleep(10); + + profiler.beginFrame(); + Utility::System::sleep(1); + profiler.endFrame(); + + profiler.beginFrame(); + Utility::System::sleep(1); + profiler.endFrame(); + + for(std::size_t i = 0; i != data.measurementCount; ++i) + CORRADE_VERIFY(profiler.isMeasurementAvailable(i)); + + /* 3/4 frames took 1 ms, the ideal average is 0.75 ms. Can't test upper + bound because (especially on overloaded CIs) it all takes a magnitude + more than expected. */ + if(data.values & GLFrameProfiler::Value::CpuDuration) { + CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::CpuDuration)); + CORRADE_COMPARE_AS(profiler.cpuDurationMean(), 0.70*1000*1000, + TestSuite::Compare::GreaterOrEqual); + } + + /* 3/4 frames took 1 ms, and one 10 ms, the ideal average is 3.25 ms. Can't + test upper bound because (especially on overloaded CIs) it all takes a + magnitude more than expected. */ + if(data.values & GLFrameProfiler::Value::FrameTime) { + CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::FrameTime)); + CORRADE_COMPARE_AS(profiler.frameTimeMean(), 3.20*1000*1000, + TestSuite::Compare::GreaterOrEqual); + } + + /* GPU time tested separately */ +} + +void FrameProfilerTest::glNotEnabled() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + GLFrameProfiler profiler{{}, 5}; + + std::ostringstream out; + Error redirectError{&out}; + profiler.isMeasurementAvailable(GLFrameProfiler::Value::CpuDuration); + profiler.frameTimeMean(); + profiler.cpuDurationMean(); + profiler.gpuDurationMean(); + CORRADE_COMPARE(out.str(), + "DebugTools::GLFrameProfiler::isMeasurementAvailable(): DebugTools::GLFrameProfiler::Value::CpuDuration not enabled\n" + "DebugTools::GLFrameProfiler::frameTimeMean(): not enabled\n" + "DebugTools::GLFrameProfiler::cpuDurationMean(): not enabled\n" + "DebugTools::GLFrameProfiler::gpuDurationMean(): not enabled\n"); +} +#endif + +void FrameProfilerTest::debugUnits() { + std::ostringstream out; + + Debug{&out} << FrameProfiler::Units::Nanoseconds << FrameProfiler::Units(0xf0); + CORRADE_COMPARE(out.str(), "DebugTools::FrameProfiler::Units::Nanoseconds DebugTools::FrameProfiler::Units(0xf0)\n"); +} + +#ifdef MAGNUM_TARGET_GL +void FrameProfilerTest::debugGLValue() { + std::ostringstream out; + + Debug{&out} << GLFrameProfiler::Value::GpuDuration << GLFrameProfiler::Value(0xfff0); + CORRADE_COMPARE(out.str(), "DebugTools::GLFrameProfiler::Value::GpuDuration DebugTools::GLFrameProfiler::Value(0xfff0)\n"); +} + +void FrameProfilerTest::debugGLValues() { + std::ostringstream out; + + Debug{&out} << (GLFrameProfiler::Value::CpuDuration|GLFrameProfiler::Value::FrameTime) << GLFrameProfiler::Values{}; + CORRADE_COMPARE(out.str(), "DebugTools::GLFrameProfiler::Value::FrameTime|DebugTools::GLFrameProfiler::Value::CpuDuration DebugTools::GLFrameProfiler::Values{}\n"); +} +#endif + +}}}} + +CORRADE_TEST_MAIN(Magnum::DebugTools::Test::FrameProfilerTest) From a2c7d9a43c18a39cbf70661500811cdd09db701a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 30 Apr 2020 22:33:25 +0200 Subject: [PATCH 02/16] DebugTools: wow MSVC thanks for existing. --- src/Magnum/DebugTools/Test/FrameProfilerGLTest.cpp | 4 +++- src/Magnum/DebugTools/Test/FrameProfilerTest.cpp | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Magnum/DebugTools/Test/FrameProfilerGLTest.cpp b/src/Magnum/DebugTools/Test/FrameProfilerGLTest.cpp index 2cf573f74..f069f28a5 100644 --- a/src/Magnum/DebugTools/Test/FrameProfilerGLTest.cpp +++ b/src/Magnum/DebugTools/Test/FrameProfilerGLTest.cpp @@ -115,15 +115,17 @@ void FrameProfilerGLTest::test() { GLFrameProfiler profiler{data.values, 4}; CORRADE_COMPARE(profiler.maxFrameCount(), 4); + /* MSVC 2015 needs the {} */ for(auto value: {GLFrameProfiler::Value::CpuDuration, GLFrameProfiler::Value::GpuDuration, #ifndef MAGNUM_TARGET_GLES GLFrameProfiler::Value::VertexFetchRatio, GLFrameProfiler::Value::PrimitiveClipRatio #endif - }) + }) { if(data.values & value) CORRADE_VERIFY(!profiler.isMeasurementAvailable(value)); + } profiler.beginFrame(); shader.draw(mesh); diff --git a/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp b/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp index a5ea4158d..23e66e127 100644 --- a/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp +++ b/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp @@ -961,10 +961,12 @@ void FrameProfilerTest::gl() { CORRADE_COMPARE(profiler.maxFrameCount(), 4); CORRADE_COMPARE(profiler.measurementCount(), data.measurementCount); + /* MSVC 2015 needs the {} */ for(auto value: {GLFrameProfiler::Value::CpuDuration, - GLFrameProfiler::Value::FrameTime}) + GLFrameProfiler::Value::FrameTime}) { if(data.values & value) CORRADE_VERIFY(!profiler.isMeasurementAvailable(value)); + } profiler.beginFrame(); Utility::System::sleep(1); From 99f286f19983cfbfe021e16b3f67368ff175d332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Tue, 28 Apr 2020 23:31:11 +0200 Subject: [PATCH 03/16] DebugTools: deprecate the old Profiler. --- doc/changelog.dox | 2 ++ src/Magnum/DebugTools/CMakeLists.txt | 9 ++++++--- src/Magnum/DebugTools/Profiler.cpp | 2 ++ src/Magnum/DebugTools/Profiler.h | 20 ++++++++++++++++---- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index 865853496..37f9f1eac 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -631,6 +631,8 @@ See also: This also means it's no longer possible to override equality comparison epsilons at compile time, but that was a rarely (if ever) used feature. +- @cpp DebugTools::Profiler @ce is obsolete, replaced with a much more + flexible and extensible @ref DebugTools::FrameProfiler @subsection changelog-latest-compatibility Potential compatibility breakages, removed APIs diff --git a/src/Magnum/DebugTools/CMakeLists.txt b/src/Magnum/DebugTools/CMakeLists.txt index b831a583d..6c1210286 100644 --- a/src/Magnum/DebugTools/CMakeLists.txt +++ b/src/Magnum/DebugTools/CMakeLists.txt @@ -24,8 +24,7 @@ # set(MagnumDebugTools_SRCS - ColorMap.cpp - Profiler.cpp) + ColorMap.cpp) set(MagnumDebugTools_GracefulAssert_SRCS FrameProfiler.cpp) @@ -34,13 +33,17 @@ set(MagnumDebugTools_HEADERS ColorMap.h DebugTools.h FrameProfiler.h - Profiler.h visibility.h) # Header files to display in project view of IDEs only set(MagnumDebugTools_PRIVATE_HEADERS ) +if(MAGNUM_BUILD_DEPRECATED) + list(APPEND MagnumDebugTools_SRCS Profiler.cpp) + list(APPEND MagnumDebugTools_HEADERS Profiler.h) +endif() + if(TARGET_GL) list(APPEND MagnumDebugTools_SRCS ResourceManager.cpp diff --git a/src/Magnum/DebugTools/Profiler.cpp b/src/Magnum/DebugTools/Profiler.cpp index c506ebc60..168b2f4fe 100644 --- a/src/Magnum/DebugTools/Profiler.cpp +++ b/src/Magnum/DebugTools/Profiler.cpp @@ -23,6 +23,8 @@ DEALINGS IN THE SOFTWARE. */ +#define _MAGNUM_NO_DEPRECATED_PROFILER + #include "Profiler.h" #include diff --git a/src/Magnum/DebugTools/Profiler.h b/src/Magnum/DebugTools/Profiler.h index 91bc31899..0e2b387c1 100644 --- a/src/Magnum/DebugTools/Profiler.h +++ b/src/Magnum/DebugTools/Profiler.h @@ -25,22 +25,36 @@ DEALINGS IN THE SOFTWARE. */ +#ifdef MAGNUM_BUILD_DEPRECATED /** @file * @brief Class @ref Magnum::DebugTools::Profiler + * @m_deprecated_since_latest Obsolete, use + * @ref Magnum/DebugTools/FrameProfiler.h and the + * @ref Magnum::DebugTools::FrameProfiler class instead. */ +#endif + +#include "Magnum/configure.h" +#ifdef MAGNUM_BUILD_DEPRECATED #include #include #include #include +#include #include "Magnum/Types.h" #include "Magnum/DebugTools/visibility.h" +#ifndef _MAGNUM_NO_DEPRECATED_PROFILER +CORRADE_DEPRECATED_FILE("use Magnum/DebugTools/FrameProfiler.h and the FrameProfiler class instead") +#endif + namespace Magnum { namespace DebugTools { /** @brief Profiler +@m_deprecated_since_latest Obsolete, use @ref FrameProfiler instead. Measures time passed during specified sections of each frame. It's meant to be used in rendering and event loops (e.g. @ref Platform::Sdl2Application::drawEvent()), @@ -97,11 +111,8 @@ p.printStatistics(); It's possible to start profiler only for certain parts of the code and then stop it again using @ref stop(), if you are not interested in profiling the rest. - -@todo Some unit testing -@todo More time intervals */ -class MAGNUM_DEBUGTOOLS_EXPORT Profiler { +class CORRADE_DEPRECATED("use FrameProfiler instead") MAGNUM_DEBUGTOOLS_EXPORT Profiler { public: /** * @brief Section ID @@ -213,5 +224,6 @@ class MAGNUM_DEBUGTOOLS_EXPORT Profiler { }; }} +#endif #endif From c8a9422d1c26b2f4c35367c5d141ee05df11d35a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 30 Apr 2020 23:16:13 +0200 Subject: [PATCH 04/16] DebugTools: make GLFrameProfiler::Values storeable in Configuration. I want to use these in command-line arguments. --- src/Magnum/DebugTools/FrameProfiler.cpp | 76 +++++++++++++++---- src/Magnum/DebugTools/FrameProfiler.h | 50 ++++++++++++ .../DebugTools/Test/FrameProfilerTest.cpp | 39 ++++++++++ 3 files changed, 152 insertions(+), 13 deletions(-) diff --git a/src/Magnum/DebugTools/FrameProfiler.cpp b/src/Magnum/DebugTools/FrameProfiler.cpp index 48077e881..d6a04e171 100644 --- a/src/Magnum/DebugTools/FrameProfiler.cpp +++ b/src/Magnum/DebugTools/FrameProfiler.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include "Magnum/Math/Functions.h" #ifdef MAGNUM_TARGET_GL @@ -629,22 +630,24 @@ Double GLFrameProfiler::primitiveClipRatioMean() const { } #endif +namespace { + +constexpr const char* GLFrameProfilerValueNames[] { + "FrameTime", + "CpuDuration", + "GpuDuration", + "VertexFetchRatio", + "PrimitiveClipRatio" +}; + +} + Debug& operator<<(Debug& debug, const GLFrameProfiler::Value value) { debug << "DebugTools::GLFrameProfiler::Value" << Debug::nospace; - switch(value) { - /* LCOV_EXCL_START */ - #define _c(v) case GLFrameProfiler::Value::v: return debug << "::" #v; - _c(FrameTime) - _c(CpuDuration) - _c(GpuDuration) - #ifndef MAGNUM_TARGET_GLES - _c(VertexFetchRatio) - _c(PrimitiveClipRatio) - #endif - #undef _c - /* LCOV_EXCL_STOP */ - } + const UnsignedInt bit = Math::log2(UnsignedShort(value)); + if(1 << bit == UnsignedShort(value)) + return debug << "::" << Debug::nospace << GLFrameProfilerValueNames[bit]; return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedShort(value)) << Debug::nospace << ")"; } @@ -663,3 +666,50 @@ Debug& operator<<(Debug& debug, const GLFrameProfiler::Values value) { #endif }} + +namespace Corrade { namespace Utility { + +using namespace Magnum; + +std::string ConfigurationValue::toString(const DebugTools::GLFrameProfiler::Value value, ConfigurationValueFlags) { + const UnsignedInt bit = Math::log2(UnsignedShort(value)); + if(1 << bit == UnsignedShort(value)) + return DebugTools::GLFrameProfilerValueNames[bit]; + return ""; +} + +DebugTools::GLFrameProfiler::Value ConfigurationValue::fromString(const std::string& value, ConfigurationValueFlags) { + for(std::size_t i = 0; i != Containers::arraySize(DebugTools::GLFrameProfilerValueNames); ++i) + if(DebugTools::GLFrameProfilerValueNames[i] == value) + return DebugTools::GLFrameProfiler::Value(1 << i); + + return DebugTools::GLFrameProfiler::Value{}; +} + +std::string ConfigurationValue::toString(const DebugTools::GLFrameProfiler::Values value, ConfigurationValueFlags) { + std::string out; + + for(std::size_t i = 0; i != Containers::arraySize(DebugTools::GLFrameProfilerValueNames); ++i) { + const auto bit = DebugTools::GLFrameProfiler::Value(1 << i); + if(value & bit) { + if(!out.empty()) out += ' '; + out += DebugTools::GLFrameProfilerValueNames[i]; + } + } + + return out; +} + +DebugTools::GLFrameProfiler::Values ConfigurationValue::fromString(const std::string& value, ConfigurationValueFlags) { + const std::vector bits = Utility::String::splitWithoutEmptyParts(value); + + DebugTools::GLFrameProfiler::Values values; + for(const std::string& bit: bits) + for(std::size_t i = 0; i != Containers::arraySize(DebugTools::GLFrameProfilerValueNames); ++i) + if(DebugTools::GLFrameProfilerValueNames[i] == bit) + values |= DebugTools::GLFrameProfiler::Value(1 << i); + + return values; +} + +}} diff --git a/src/Magnum/DebugTools/FrameProfiler.h b/src/Magnum/DebugTools/FrameProfiler.h index c5fc55e71..684f1816b 100644 --- a/src/Magnum/DebugTools/FrameProfiler.h +++ b/src/Magnum/DebugTools/FrameProfiler.h @@ -701,4 +701,54 @@ MAGNUM_DEBUGTOOLS_EXPORT Debug& operator<<(Debug& debug, GLFrameProfiler::Values }} +namespace Corrade { namespace Utility { + +/** +@configurationvalue{Magnum::DebugTools::GLFrameProfiler::Value} +@m_since_latest +*/ +template<> struct MAGNUM_DEBUGTOOLS_EXPORT ConfigurationValue { + ConfigurationValue() = delete; + + /** + * @brief Writes enum value as a string + * + * If the value is invalid, returns an empty string. + */ + static std::string toString(Magnum::DebugTools::GLFrameProfiler::Value value, ConfigurationValueFlags); + + /** + * @brief Reads enum value as a string + * + * If the string is invalid, returns a zero (invalid) value. + */ + static Magnum::DebugTools::GLFrameProfiler::Value fromString(const std::string& stringValue, ConfigurationValueFlags); +}; + +/** +@configurationvalue{Magnum::DebugTools::GLFrameProfiler::Values} +@m_since_latest +*/ +template<> struct MAGNUM_DEBUGTOOLS_EXPORT ConfigurationValue { + ConfigurationValue() = delete; + + /** + * @brief Writes enum set value as a string + * + * Writes the enum set as a sequence of flag names separated by spaces. If + * the value is invalid, returns an empty string. + */ + static std::string toString(Magnum::DebugTools::GLFrameProfiler::Values value, ConfigurationValueFlags); + + /** + * @brief Reads enum set value as a string + * + * Assumes the string is a sequence of flag names separated by spaces. If + * the value is invalid, returns an empty set. + */ + static Magnum::DebugTools::GLFrameProfiler::Values fromString(const std::string& stringValue, ConfigurationValueFlags); +}; + +}} + #endif diff --git a/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp b/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp index 23e66e127..b8eb11bfe 100644 --- a/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp +++ b/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "Magnum/DebugTools/FrameProfiler.h" @@ -67,6 +68,9 @@ struct FrameProfilerTest: TestSuite::Tester { #ifdef MAGNUM_TARGET_GL void debugGLValue(); void debugGLValues(); + + void configurationGLValue(); + void configurationGLValues(); #endif }; @@ -141,6 +145,9 @@ FrameProfilerTest::FrameProfilerTest() { #ifdef MAGNUM_TARGET_GL &FrameProfilerTest::debugGLValue, &FrameProfilerTest::debugGLValues, + + &FrameProfilerTest::configurationGLValue, + &FrameProfilerTest::configurationGLValues #endif }); } @@ -1051,6 +1058,38 @@ void FrameProfilerTest::debugGLValues() { Debug{&out} << (GLFrameProfiler::Value::CpuDuration|GLFrameProfiler::Value::FrameTime) << GLFrameProfiler::Values{}; CORRADE_COMPARE(out.str(), "DebugTools::GLFrameProfiler::Value::FrameTime|DebugTools::GLFrameProfiler::Value::CpuDuration DebugTools::GLFrameProfiler::Values{}\n"); } + +void FrameProfilerTest::configurationGLValue() { + Utility::ConfigurationGroup c; + + c.setValue("value", GLFrameProfiler::Value::GpuDuration); + CORRADE_COMPARE(c.value("value"), "GpuDuration"); + CORRADE_COMPARE(c.value("value"), GLFrameProfiler::Value::GpuDuration); + + c.setValue("zero", GLFrameProfiler::Value{}); + CORRADE_COMPARE(c.value("zero"), ""); + CORRADE_COMPARE(c.value("zero"), GLFrameProfiler::Value{}); + + c.setValue("invalid", GLFrameProfiler::Value(0xdead)); + CORRADE_COMPARE(c.value("invalid"), ""); + CORRADE_COMPARE(c.value("invalid"), GLFrameProfiler::Value{}); +} + +void FrameProfilerTest::configurationGLValues() { + Utility::ConfigurationGroup c; + + c.setValue("value", GLFrameProfiler::Value::FrameTime|GLFrameProfiler::Value::CpuDuration|GLFrameProfiler::Value::GpuDuration); + CORRADE_COMPARE(c.value("value"), "FrameTime CpuDuration GpuDuration"); + CORRADE_COMPARE(c.value("value"), GLFrameProfiler::Value::FrameTime|GLFrameProfiler::Value::CpuDuration|GLFrameProfiler::Value::GpuDuration); + + c.setValue("empty", GLFrameProfiler::Values{}); + CORRADE_COMPARE(c.value("empty"), ""); + CORRADE_COMPARE(c.value("empty"), GLFrameProfiler::Values{}); + + c.setValue("invalid", GLFrameProfiler::Value::CpuDuration|GLFrameProfiler::Value::GpuDuration|GLFrameProfiler::Value(0xff00)); + CORRADE_COMPARE(c.value("invalid"), "CpuDuration GpuDuration"); + CORRADE_COMPARE(c.value("invalid"), GLFrameProfiler::Value::CpuDuration|GLFrameProfiler::Value::GpuDuration); +} #endif }}}} From 2bf953f1727d9574129e4171e6c28e33a12d80cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 14 Feb 2020 11:33:44 +0100 Subject: [PATCH 05/16] Trade: bootstrap scene converter plugins. --- CMakeLists.txt | 7 + doc/changelog.dox | 1 + doc/plugins.dox | 3 + modules/FindMagnum.cmake | 19 +- src/Magnum/Trade/AbstractSceneConverter.cpp | 198 +++++++ src/Magnum/Trade/AbstractSceneConverter.h | 323 ++++++++++++ src/Magnum/Trade/CMakeLists.txt | 2 + src/Magnum/Trade/MeshData.h | 1 + .../Trade/Test/AbstractSceneConverterTest.cpp | 484 ++++++++++++++++++ src/Magnum/Trade/Test/CMakeLists.txt | 5 + src/Magnum/Trade/Trade.h | 1 + src/Magnum/Trade/configure.h.cmake | 2 + 12 files changed, 1045 insertions(+), 1 deletion(-) create mode 100644 src/Magnum/Trade/AbstractSceneConverter.cpp create mode 100644 src/Magnum/Trade/AbstractSceneConverter.h create mode 100644 src/Magnum/Trade/Test/AbstractSceneConverterTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 48b06611c..da7d6f852 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,6 +356,10 @@ set(MAGNUM_PLUGINS_IMPORTER_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_BINA set(MAGNUM_PLUGINS_IMPORTER_DEBUG_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR}/importers) set(MAGNUM_PLUGINS_IMPORTER_RELEASE_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR}/importers) set(MAGNUM_PLUGINS_IMPORTER_RELEASE_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_LIBRARY_INSTALL_DIR}/importers) +set(MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_BINARY_INSTALL_DIR}/sceneconverters) +set(MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR}/sceneconverters) +set(MAGNUM_PLUGINS_SCENECONVERTER_RELEASE_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR}/sceneconverters) +set(MAGNUM_PLUGINS_SCENECONVERTER_RELEASE_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_LIBRARY_INSTALL_DIR}/sceneconverters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_BINARY_INSTALL_DIR}/audioimporters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_DEBUG_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR}/audioimporters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_RELEASE_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR}/audioimporters) @@ -394,6 +398,7 @@ if(MAGNUM_PLUGINS_DIR) set(MAGNUM_PLUGINS_FONTCONVERTER_DIR ${MAGNUM_PLUGINS_DIR}/fontconverters) set(MAGNUM_PLUGINS_IMAGECONVERTER_DIR ${MAGNUM_PLUGINS_DIR}/imageconverters) set(MAGNUM_PLUGINS_IMPORTER_DIR ${MAGNUM_PLUGINS_DIR}/importers) + set(MAGNUM_PLUGINS_SCENECONVERTER_DIR ${MAGNUM_PLUGINS_DIR}/sceneconverters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_DIR ${MAGNUM_PLUGINS_DIR}/audioimporters) endif() if(MAGNUM_PLUGINS_DEBUG_DIR) @@ -402,12 +407,14 @@ if(MAGNUM_PLUGINS_DEBUG_DIR) set(MAGNUM_PLUGINS_IMAGECONVERTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/imageconverters) set(MAGNUM_PLUGINS_IMPORTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/importers) set(MAGNUM_PLUGINS_FONT_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/fonts) + set(MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/sceneconverters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/audioimporters) endif() if(MAGNUM_PLUGINS_RELEASE_DIR) set(MAGNUM_PLUGINS_FONTCONVERTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/fontconverters) set(MAGNUM_PLUGINS_IMAGECONVERTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/imageconverters) set(MAGNUM_PLUGINS_IMPORTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/importers) + set(MAGNUM_PLUGINS_SCENECONVERTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/sceneconverters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/audioimporters) endif() diff --git a/doc/changelog.dox b/doc/changelog.dox index 37f9f1eac..e2a8a149f 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -270,6 +270,7 @@ See also: - New @ref magnum-sceneconverter "magnum-sceneconverter" tool, similar to @ref magnum-imageconverter "magnum-imageconverter" but suited for general scene formats +- New @ref Trade::AbstractSceneConverter plugin interface - Ability to import image mip levels via an additional parameter in @ref Trade::AbstractImporter::image2D(), @ref Trade::AbstractImporter::image2DLevelCount() and similar APIs for 1D diff --git a/doc/plugins.dox b/doc/plugins.dox index fd3602f95..a2d746e79 100644 --- a/doc/plugins.dox +++ b/doc/plugins.dox @@ -64,6 +64,9 @@ of given type. Magnum provides these plugin interfaces: - @ref Trade::AbstractImageConverter --- conversion among various image formats. See `*ImageConverter` classes in the @ref Trade namespace for available image converter plugins. +- @ref Trade::AbstractSceneConverter --- conversion among various scene + formats, mesh optimization etc. See `*SceneConverter` classes in the + @ref Trade namespace for available scene converter plugins. - @ref Text::AbstractFont --- font loading and glyph layout. See `*Font` classes in the @ref Text namespace for available font plugins. - @ref Text::AbstractFontConverter --- font and glyph cache conversion. See diff --git a/modules/FindMagnum.cmake b/modules/FindMagnum.cmake index 30161db04..ea82c72d9 100644 --- a/modules/FindMagnum.cmake +++ b/modules/FindMagnum.cmake @@ -32,6 +32,8 @@ # font converter plugins # MAGNUM_PLUGINS_IMAGECONVERTER[|_DEBUG|_RELEASE]_DIR - Directory with dynamic # image converter plugins +# MAGNUM_PLUGINS_SCENECONVERTER[|_DEBUG|_RELEASE]_DIR - Directory with dynamic +# scene converter plugins # MAGNUM_PLUGINS_IMPORTER[|_DEBUG|_RELEASE]_DIR - Directory with dynamic # importer plugins # MAGNUM_PLUGINS_AUDIOIMPORTER[|_DEBUG|_RELEASE]_DIR - Directory with dynamic @@ -175,6 +177,10 @@ # plugin binary installation directory # MAGNUM_PLUGINS_IMPORTER_[DEBUG|RELEASE]_LIBRARY_INSTALL_DIR - Importer # plugin library installation directory +# MAGNUM_PLUGINS_SCENECONVERTER_[DEBUG|RELEASE]_BINARY_INSTALL_DIR - Scene +# converter plugin binary installation directory +# MAGNUM_PLUGINS_SCENECONVERTER_[DEBUG|RELEASE]_LIBRARY_INSTALL_DIR - Scene +# converter plugin library installation directory # MAGNUM_PLUGINS_AUDIOIMPORTER_[DEBUG|RELEASE]_BINARY_INSTALL_DIR - Audio # importer plugin binary installation directory # MAGNUM_PLUGINS_AUDIOIMPORTER_[DEBUG|RELEASE]_LIBRARY_INSTALL_DIR - Audio @@ -456,7 +462,7 @@ set(_MAGNUM_ObjImporter_DEPENDENCIES MeshTools) # and below foreach(_component ${_MAGNUM_PLUGIN_COMPONENT_LIST}) if(_component MATCHES ".+AudioImporter") list(APPEND _MAGNUM_${_component}_DEPENDENCIES Audio) - elseif(_component MATCHES ".+(Importer|ImageConverter)") + elseif(_component MATCHES ".+(Importer|ImageConverter|SceneConverter)") list(APPEND _MAGNUM_${_component}_DEPENDENCIES Trade) elseif(_component MATCHES ".+(Font|FontConverter)") list(APPEND _MAGNUM_${_component}_DEPENDENCIES Text TextureTools) @@ -547,6 +553,10 @@ foreach(_component ${Magnum_FIND_COMPONENTS}) elseif(_component MATCHES ".+ImageConverter$") set(_MAGNUM_${_COMPONENT}_PATH_SUFFIX imageconverters) + # SceneConverter plugin specific name suffixes + elseif(_component MATCHES ".+SceneConverter$") + set(_MAGNUM_${_COMPONENT}_PATH_SUFFIX sceneconverters) + # FontConverter plugin specific name suffixes elseif(_component MATCHES ".+FontConverter$") set(_MAGNUM_${_COMPONENT}_PATH_SUFFIX fontconverters) @@ -1115,6 +1125,10 @@ set(MAGNUM_PLUGINS_IMPORTER_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_BINA set(MAGNUM_PLUGINS_IMPORTER_DEBUG_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR}/importers) set(MAGNUM_PLUGINS_IMPORTER_RELEASE_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR}/importers) set(MAGNUM_PLUGINS_IMPORTER_RELEASE_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_LIBRARY_INSTALL_DIR}/importers) +set(MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_BINARY_INSTALL_DIR}/sceneconverters) +set(MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR}/sceneconverters) +set(MAGNUM_PLUGINS_SCENECONVERTER_RELEASE_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_LIBRARY_INSTALL_DIR}/sceneconverters) +set(MAGNUM_PLUGINS_SCENECONVERTER_RELEASE_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR}/sceneconverters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_BINARY_INSTALL_DIR}/audioimporters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_DEBUG_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR}/audioimporters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_RELEASE_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR}/audioimporters) @@ -1141,6 +1155,7 @@ if(MAGNUM_PLUGINS_DIR) set(MAGNUM_PLUGINS_FONTCONVERTER_DIR ${MAGNUM_PLUGINS_DIR}/fontconverters) set(MAGNUM_PLUGINS_IMAGECONVERTER_DIR ${MAGNUM_PLUGINS_DIR}/imageconverters) set(MAGNUM_PLUGINS_IMPORTER_DIR ${MAGNUM_PLUGINS_DIR}/importers) + set(MAGNUM_PLUGINS_SCENECONVERTER_DIR ${MAGNUM_PLUGINS_DIR}/sceneconverters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_DIR ${MAGNUM_PLUGINS_DIR}/audioimporters) endif() if(MAGNUM_PLUGINS_DEBUG_DIR) @@ -1149,11 +1164,13 @@ if(MAGNUM_PLUGINS_DEBUG_DIR) set(MAGNUM_PLUGINS_IMAGECONVERTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/imageconverters) set(MAGNUM_PLUGINS_IMPORTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/importers) set(MAGNUM_PLUGINS_FONT_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/fonts) + set(MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/sceneconverters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/audioimporters) endif() if(MAGNUM_PLUGINS_RELEASE_DIR) set(MAGNUM_PLUGINS_FONTCONVERTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/fontconverters) set(MAGNUM_PLUGINS_IMAGECONVERTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/imageconverters) set(MAGNUM_PLUGINS_IMPORTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/importers) + set(MAGNUM_PLUGINS_SCENECONVERTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/sceneconverters) set(MAGNUM_PLUGINS_AUDIOIMPORTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/audioimporters) endif() diff --git a/src/Magnum/Trade/AbstractSceneConverter.cpp b/src/Magnum/Trade/AbstractSceneConverter.cpp new file mode 100644 index 000000000..959afd340 --- /dev/null +++ b/src/Magnum/Trade/AbstractSceneConverter.cpp @@ -0,0 +1,198 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 "AbstractSceneConverter.h" + +#include +#include +#include +#include +#include + +#include "Magnum/Trade/ArrayAllocator.h" +#include "Magnum/Trade/MeshData.h" + +#ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT +#include "Magnum/Trade/configure.h" +#endif + +namespace Magnum { namespace Trade { + +std::string AbstractSceneConverter::pluginInterface() { + return "cz.mosra.magnum.Trade.AbstractSceneConverter/0.1"; +} + +#ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT +std::vector AbstractSceneConverter::pluginSearchPaths() { + return PluginManager::implicitPluginSearchPaths( + #ifndef MAGNUM_BUILD_STATIC + Utility::Directory::libraryLocation(&pluginInterface), + #else + {}, + #endif + #ifdef CORRADE_IS_DEBUG_BUILD + MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_DIR, + #else + MAGNUM_PLUGINS_SCENECONVERTER_DIR, + #endif + #ifdef CORRADE_IS_DEBUG_BUILD + "magnum-d/" + #else + "magnum/" + #endif + "sceneconverters"); +} +#endif + +AbstractSceneConverter::AbstractSceneConverter() = default; + +AbstractSceneConverter::AbstractSceneConverter(PluginManager::Manager& manager): PluginManager::AbstractManagingPlugin{manager} {} + +AbstractSceneConverter::AbstractSceneConverter(PluginManager::AbstractManager& manager, const std::string& plugin): PluginManager::AbstractManagingPlugin{manager, plugin} {} + +SceneConverterFeatures AbstractSceneConverter::features() const { + const SceneConverterFeatures features = doFeatures(); + CORRADE_ASSERT(features, "Trade::AbstractSceneConverter::features(): implementation reported no features", {}); + return features; +} + +void AbstractSceneConverter::setFlags(SceneConverterFlags flags) { + _flags = flags; + doSetFlags(flags); +} + +void AbstractSceneConverter::doSetFlags(SceneConverterFlags) {} + +Containers::Optional AbstractSceneConverter::convert(const MeshData& mesh) { + CORRADE_ASSERT(features() & SceneConverterFeature::ConvertMesh, + "Trade::AbstractSceneConverter::convert(): mesh conversion not supported", {}); + + Containers::Optional out = doConvert(mesh); + CORRADE_ASSERT(!out || ( + (!out->_indexData.deleter() || out->_indexData.deleter() == Implementation::nonOwnedArrayDeleter || out->_indexData.deleter() == ArrayAllocator::deleter) && + (!out->_vertexData.deleter() || out->_vertexData.deleter() == Implementation::nonOwnedArrayDeleter || out->_vertexData.deleter() == ArrayAllocator::deleter) && + (!out->_attributes.deleter() || out->_attributes.deleter() == reinterpret_cast(Implementation::nonOwnedArrayDeleter))), + "Trade::AbstractSceneConverter::convert(): implementation is not allowed to use a custom Array deleter", {}); + return out; +} + +Containers::Optional AbstractSceneConverter::doConvert(const MeshData&) { + CORRADE_ASSERT_UNREACHABLE("Trade::AbstractSceneConverter::convert(): mesh conversion advertised but not implemented", {}); +} + +bool AbstractSceneConverter::convertInPlace(MeshData& mesh) { + CORRADE_ASSERT(features() & SceneConverterFeature::ConvertMeshInPlace, + "Trade::AbstractSceneConverter::convertInPlace(): mesh conversion not supported", {}); + + return doConvertInPlace(mesh); +} + +bool AbstractSceneConverter::doConvertInPlace(MeshData&) { + CORRADE_ASSERT_UNREACHABLE("Trade::AbstractSceneConverter::convertInPlace(): mesh conversion advertised but not implemented", {}); +} + +Containers::Array AbstractSceneConverter::convertToData(const MeshData& mesh) { + CORRADE_ASSERT(features() & SceneConverterFeature::ConvertMeshToData, + "Trade::AbstractSceneConverter::convertToData(): mesh conversion not supported", {}); + + Containers::Array out = doConvertToData(mesh); + CORRADE_ASSERT(!out || !out.deleter() || out.deleter() == Implementation::nonOwnedArrayDeleter || out.deleter() == ArrayAllocator::deleter, + "Trade::AbstractSceneConverter::convertToData(): implementation is not allowed to use a custom Array deleter", {}); + return out; +} + +Containers::Array AbstractSceneConverter::doConvertToData(const MeshData&) { + CORRADE_ASSERT_UNREACHABLE("Trade::AbstractSceneConverter::convertToData(): mesh conversion advertised but not implemented", {}); +} + +bool AbstractSceneConverter::convertToFile(const std::string& filename, const MeshData& mesh) { + CORRADE_ASSERT(features() >= SceneConverterFeature::ConvertMeshToFile, + "Trade::AbstractSceneConverter::convertToFile(): mesh conversion not supported", {}); + + return doConvertToFile(filename, mesh); +} + +bool AbstractSceneConverter::doConvertToFile(const std::string& filename, const MeshData& mesh) { + CORRADE_ASSERT(features() >= SceneConverterFeature::ConvertMeshToData, "Trade::AbstractSceneConverter::convertToFile(): mesh conversion advertised but not implemented", false); + + const auto data = doConvertToData(mesh); + /* No deleter checks as it doesn't matter here */ + if(!data) return false; + + /* Open file */ + if(!Utility::Directory::write(filename, data)) { + Error() << "Trade::AbstractSceneConverter::convertToFile(): cannot write to file" << filename; + return false; + } + + return true; +} + +Debug& operator<<(Debug& debug, const SceneConverterFeature value) { + debug << "Trade::SceneConverterFeature" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case SceneConverterFeature::v: return debug << "::" #v; + _c(ConvertMesh) + _c(ConvertMeshInPlace) + _c(ConvertMeshToData) + _c(ConvertMeshToFile) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedByte(value)) << Debug::nospace << ")"; +} + +Debug& operator<<(Debug& debug, const SceneConverterFeatures value) { + return Containers::enumSetDebugOutput(debug, value, "Trade::SceneConverterFeatures{}", { + SceneConverterFeature::ConvertMesh, + SceneConverterFeature::ConvertMeshInPlace, + SceneConverterFeature::ConvertMeshToData, + /* Implied by ConvertMeshToData, has to be after */ + SceneConverterFeature::ConvertMeshToFile}); +} + +Debug& operator<<(Debug& debug, const SceneConverterFlag value) { + debug << "Trade::SceneConverterFlag" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case SceneConverterFlag::v: return debug << "::" #v; + _c(Verbose) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedByte(value)) << Debug::nospace << ")"; +} + +Debug& operator<<(Debug& debug, const SceneConverterFlags value) { + return Containers::enumSetDebugOutput(debug, value, "Trade::SceneConverterFlags{}", { + SceneConverterFlag::Verbose}); +} + +}} diff --git a/src/Magnum/Trade/AbstractSceneConverter.h b/src/Magnum/Trade/AbstractSceneConverter.h new file mode 100644 index 000000000..623972e05 --- /dev/null +++ b/src/Magnum/Trade/AbstractSceneConverter.h @@ -0,0 +1,323 @@ +#ifndef Magnum_Trade_AbstractSceneConverter_h +#define Magnum_Trade_AbstractSceneConverter_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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::Trade::AbstractSceneConverter, enum @ref Magnum::Trade::SceneConverterFeature, enum set @ref Magnum::Trade::SceneConverterFeatures + * @m_since_latest + */ + +#include + +#include "Magnum/Magnum.h" +#include "Magnum/Trade/Trade.h" +#include "Magnum/Trade/visibility.h" + +namespace Magnum { namespace Trade { + +/** +@brief Features supported by a scene converter +@m_since_latest + +@see @ref SceneConverterFeatures, @ref AbstractSceneConverter::features() +*/ +enum class SceneConverterFeature: UnsignedByte { + /** + * Convert a mesh with + * @ref AbstractSceneConverter::convert(const MeshData&). + */ + ConvertMesh = 1 << 0, + + /** + * Convert a mesh in-place with + * @ref AbstractSceneConverter::convertInPlace(MeshData&). + */ + ConvertMeshInPlace = 1 << 1, + + /** + * Converting a mesh to a file with + * @ref AbstractSceneConverter::convertToFile(const std::string&, const MeshData&). + */ + ConvertMeshToFile = 1 << 2, + + /** + * Converting a mesh to raw data with + * @ref AbstractSceneConverter::convertToData(const MeshData&). Implies + * @ref SceneConverterFeature::ConvertMeshToFile. + */ + ConvertMeshToData = ConvertMeshToFile|(1 << 3) +}; + +/** +@brief Features supported by a scene converter +@m_since_latest + +@see @ref AbstractSceneConverter::features() +*/ +typedef Containers::EnumSet SceneConverterFeatures; + +CORRADE_ENUMSET_OPERATORS(SceneConverterFeatures) + +/** @debugoperatorenum{SceneConverterFeature} */ +MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, SceneConverterFeature value); + +/** @debugoperatorenum{SceneConverterFeatures} */ +MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, SceneConverterFeatures value); + +/** +@brief Scene converter flag +@m_since_latest + +@see @ref SceneConverterFlags, @ref AbstractSceneConverter::setFlags() +*/ +enum class SceneConverterFlag: UnsignedByte { + /** + * Print verbose diagnostic during import. By default the importer only + * prints messages on error or when some operation might cause unexpected + * data modification or loss. + */ + Verbose = 1 << 0 + + /** @todo Y flip */ +}; + +/** +@brief Scene converter flags +@m_since_latest + +@see @ref AbstractImporter::setFlags() +*/ +typedef Containers::EnumSet SceneConverterFlags; + +CORRADE_ENUMSET_OPERATORS(SceneConverterFlags) + +/** +@debugoperatorenum{SceneConverterFlag} +@m_since_latest +*/ +MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, SceneConverterFlag value); + +/** +@debugoperatorenum{SceneConverterFlags} +@m_since_latest +*/ +MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, SceneConverterFlags value); + +/** +@brief Base for scene converter plugins +@m_since_latest + +Provides functionality for converting meshes and other scene data between +various formats or performing optimizations and other operations on them. See +@ref plugins for more information and `*SceneConverter` classes in the +@ref Trade namespace for available scene converter plugins. + +@section Trade-AbstractSceneConverter-data-dependency Data dependency + +The instances returned from various functions *by design* have no dependency on +the importer instance and neither on the dynamic plugin module. In other words, +you don't need to keep the importer instance (or the plugin manager instance) +around in order to have the `*Data` instances valid. Moreover, all +@ref Corrade::Containers::Array instances returned through @ref MeshData and +others are only allowed to have default deleters --- this is to avoid potential +dangling function pointer calls when destructing such instances after the +plugin module has been unloaded. + +@section Trade-AbstractSceneConverter-subclassing Subclassing + +The plugin needs to implement the @ref doFeatures() function and one or more of +@ref doConvert(), @ref doConvertInPlace(), @ref doConvertToData() or +@ref doConvertToFile() functions based on what features are supported. + +You don't need to do most of the redundant sanity checks, these things are +checked by the implementation: + +- The function @ref doConvert(const MeshData&) is called only if + @ref SceneConverterFeature::ConvertMesh is supported. +- The function @ref doConvertInPlace(MeshData&) is called only if + @ref SceneConverterFeature::ConvertMeshInPlace is supported. +- The function @ref doConvertToData(const MeshData&) is called only if + @ref SceneConverterFeature::ConvertMeshToData is supported. +- The function @ref doConvertToFile(const std::string&, const MeshData&) is + called only if @ref SceneConverterFeature::ConvertMeshToFile is supported. + +@m_class{m-block m-warning} + +@par Dangling function pointers on plugin unload + As @ref Trade-AbstractSceneConverter-data-dependency "mentioned above", + @ref Corrade::Containers::Array instances returned from plugin + implementations are not allowed to use anything else than the default + deleter or the deleter used by @ref Trade::ArrayAllocator, otherwise this + could cause dangling function pointer call on array destruction if the + plugin gets unloaded before the array is destroyed. This is asserted by the + base implementation on return. +*/ +class MAGNUM_TRADE_EXPORT AbstractSceneConverter: public PluginManager::AbstractManagingPlugin { + public: + /** + * @brief Plugin interface + * + * @code{.cpp} + * "cz.mosra.magnum.Trade.AbstractSceneConverter/0.1" + * @endcode + */ + static std::string pluginInterface(); + + #ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT + /** + * @brief Plugin search paths + * + * Looks into `magnum/sceneconverters/` or `magnum-d/sceneconverters/` + * next to the dynamic @ref Trade library, next to the executable and + * elsewhere according to the rules documented in + * @ref Corrade::PluginManager::implicitPluginSearchPaths(). The search + * directory can be also hardcoded using the `MAGNUM_PLUGINS_DIR` CMake + * variables, see @ref building for more information. + * + * Not defined on platforms without + * @ref CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT "dynamic plugin support". + */ + static std::vector pluginSearchPaths(); + #endif + + /** @brief Default constructor */ + explicit AbstractSceneConverter(); + + /** @brief Constructor with access to plugin manager */ + explicit AbstractSceneConverter(PluginManager::Manager& manager); + + /** @brief Plugin manager constructor */ + explicit AbstractSceneConverter(PluginManager::AbstractManager& manager, const std::string& plugin); + + /** @brief Features supported by this converter */ + SceneConverterFeatures features() const; + + /** @brief Converter flags */ + SceneConverterFlags flags() const { return _flags; } + + /** + * @brief Set converter flags + * + * Some flags can be set only if the converter supports particular + * features, see documentation of each @ref SceneConverterFlag for more + * information. By default no flags are set. + */ + void setFlags(SceneConverterFlags flags); + + /** + * @brief Convert a mesh + * + * Depending on the plugin, can perform for example vertex format + * conversion, overdraw optimization or decimation / subdivision. + * Available only if @ref SceneConverterFeature::ConvertMesh is + * supported. + * @see @ref features(), @ref convertInPlace(MeshData&) + */ + Containers::Optional convert(const MeshData& mesh); + + /** + * @brief Convert a mesh in-place + * + * Depending on the plugin, can perform for example index buffer + * reordering for better vertex cache use or overdraw optimization. + * Available only if @ref SceneConverterFeature::ConvertMeshInPlace is + * supported. Returns @cpp true @ce if the operation succeeded. On + * failure the function prints an error message and returns + * @cpp false @ce, @p mesh is guaranteed to stay unchanged. + * @see @ref features(), @ref convert(const MeshData&) + */ + bool convertInPlace(MeshData& mesh); + + /** + * @brief Convert a mesh to a raw data + * + * Depending on the plugin, can convert the mesh to a file format that + * can be saved to disk. Available only if + * @ref SceneConverterFeature::ConvertMeshToData is supported. On + * failure the function prints an error message and returns + * @cpp nullptr @ce. + * @see @ref features(), @ref convertToFile() + */ + Containers::Array convertToData(const MeshData& mesh); + + /** + * @brief Convert a mesh to a file + * + * Available only if @ref SceneConverterFeature::ConvertMeshToFile or + * @ref SceneConverterFeature::ConvertMeshToData is supported. Returns + * @cpp true @ce on success, prints an error message and returns + * @cpp false @ce otherwise. + * @see @ref features(), @ref convertToData() + */ + bool convertToFile(const std::string& filename, const MeshData& mesh); + + private: + /** + * @brief Implementation of @ref features() + * + * The implementation is expected to support at least one feature. + */ + virtual SceneConverterFeatures doFeatures() const = 0; + + /** + * @brief Implementation for @ref setFlags() + * + * Useful when the converter needs to modify some internal state on + * flag setup. Default implementation does nothing and this + * function doesn't need to be implemented --- the flags are available + * through @ref flags(). + * + * To reduce the amount of error checking on user side, this function + * isn't expected to fail --- if a flag combination is invalid / + * unsuported, error reporting should be delayed to various conversion + * functions, where the user is expected to do error handling anyway. + */ + virtual void doSetFlags(SceneConverterFlags flags); + + /** @brief Implementation of @ref convert(const MeshData&) */ + virtual Containers::Optional doConvert(const MeshData& mesh); + + /** @brief Implementation of @ref convertInPlace(MeshData&) */ + virtual bool doConvertInPlace(MeshData& mesh); + + /** @brief Implementation of @ref convertToData(const MeshData&) */ + virtual Containers::Array doConvertToData(const MeshData& mesh); + + /** + * @brief Implementation of @ref convertToFile(const std::string&, const MeshData&) + * + * If @ref SceneConverterFeature::ConvertMeshToData is supported, + * default implementation calls @ref doConvertToData(const MeshData&) + * and saves the result to given file. + */ + virtual bool doConvertToFile(const std::string& filename, const MeshData& mesh); + + SceneConverterFlags _flags; +}; + +}} + +#endif diff --git a/src/Magnum/Trade/CMakeLists.txt b/src/Magnum/Trade/CMakeLists.txt index e41287968..27c4ac30c 100644 --- a/src/Magnum/Trade/CMakeLists.txt +++ b/src/Magnum/Trade/CMakeLists.txt @@ -38,6 +38,7 @@ set(MagnumTrade_SRCS set(MagnumTrade_GracefulAssert_SRCS AbstractImageConverter.cpp AbstractImporter.cpp + AbstractSceneConverter.cpp AnimationData.cpp CameraData.cpp ImageData.cpp @@ -50,6 +51,7 @@ set(MagnumTrade_HEADERS AbstractImporter.h AbstractImageConverter.h AbstractMaterialData.h + AbstractSceneConverter.h AnimationData.h ArrayAllocator.h CameraData.h diff --git a/src/Magnum/Trade/MeshData.h b/src/Magnum/Trade/MeshData.h index 858f92225..d3946bf28 100644 --- a/src/Magnum/Trade/MeshData.h +++ b/src/Magnum/Trade/MeshData.h @@ -1780,6 +1780,7 @@ class MAGNUM_TRADE_EXPORT MeshData { the restriction is pointless when used outside of plugin implementations. */ friend AbstractImporter; + friend AbstractSceneConverter; /* Internal helper that doesn't assert, unlike attributeId() */ UnsignedInt attributeFor(MeshAttribute name, UnsignedInt id) const; diff --git a/src/Magnum/Trade/Test/AbstractSceneConverterTest.cpp b/src/Magnum/Trade/Test/AbstractSceneConverterTest.cpp new file mode 100644 index 000000000..67953e489 --- /dev/null +++ b/src/Magnum/Trade/Test/AbstractSceneConverterTest.cpp @@ -0,0 +1,484 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 +#include +#include + +#include "Magnum/Math/Vector3.h" +#include "Magnum/Trade/ArrayAllocator.h" +#include "Magnum/Trade/AbstractSceneConverter.h" +#include "Magnum/Trade/MeshData.h" + +#include "configure.h" + +namespace Magnum { namespace Trade { namespace Test { namespace { + +struct AbstractSceneConverterTest: TestSuite::Tester { + explicit AbstractSceneConverterTest(); + + void featuresNone(); + + void setFlags(); + void setFlagsNotImplemented(); + + void thingNotSupported(); + + void convertMesh(); + void convertMeshNotImplemented(); + void convertMeshNonOwningDeleters(); + void convertMeshGrowableDeleters(); + void convertMeshCustomIndexDataDeleter(); + void convertMeshCustomVertexDataDeleter(); + void convertMeshCustomAttributeDataDeleter(); + + void convertMeshInPlace(); + void convertMeshInPlaceNotImplemented(); + + void convertMeshToData(); + void convertMeshToDataNotImplemented(); + void convertMeshToDataCustomDeleter(); + + void convertMeshToFile(); + void convertMeshToFileThroughData(); + void convertMeshToFileThroughDataNotWritable(); + void convertMeshToFileNotImplemented(); + + void debugFeature(); + void debugFeatures(); + void debugFlag(); + void debugFlags(); +}; + +AbstractSceneConverterTest::AbstractSceneConverterTest() { + addTests({&AbstractSceneConverterTest::featuresNone, + + &AbstractSceneConverterTest::setFlags, + &AbstractSceneConverterTest::setFlagsNotImplemented, + + &AbstractSceneConverterTest::thingNotSupported, + + &AbstractSceneConverterTest::convertMesh, + &AbstractSceneConverterTest::convertMeshNotImplemented, + &AbstractSceneConverterTest::convertMeshNonOwningDeleters, + &AbstractSceneConverterTest::convertMeshGrowableDeleters, + &AbstractSceneConverterTest::convertMeshCustomIndexDataDeleter, + &AbstractSceneConverterTest::convertMeshCustomVertexDataDeleter, + &AbstractSceneConverterTest::convertMeshCustomAttributeDataDeleter, + + &AbstractSceneConverterTest::convertMeshInPlace, + &AbstractSceneConverterTest::convertMeshInPlaceNotImplemented, + + &AbstractSceneConverterTest::convertMeshToData, + &AbstractSceneConverterTest::convertMeshToDataNotImplemented, + &AbstractSceneConverterTest::convertMeshToDataCustomDeleter, + + &AbstractSceneConverterTest::convertMeshToFile, + &AbstractSceneConverterTest::convertMeshToFileThroughData, + &AbstractSceneConverterTest::convertMeshToFileThroughDataNotWritable, + &AbstractSceneConverterTest::convertMeshToFileNotImplemented, + + &AbstractSceneConverterTest::debugFeature, + &AbstractSceneConverterTest::debugFeatures, + &AbstractSceneConverterTest::debugFlag, + &AbstractSceneConverterTest::debugFlags}); + + /* Create testing dir */ + Utility::Directory::mkpath(TRADE_TEST_OUTPUT_DIR); +} + +void AbstractSceneConverterTest::featuresNone() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return {}; } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.features(); + CORRADE_COMPARE(out.str(), "Trade::AbstractSceneConverter::features(): implementation reported no features\n"); +} + +void AbstractSceneConverterTest::setFlags() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { + /* Assuming this bit is unused */ + return SceneConverterFeature(1 << 7); + } + void doSetFlags(SceneConverterFlags flags) override { + _flags = flags; + } + + SceneConverterFlags _flags; + } converter; + + CORRADE_COMPARE(converter.flags(), SceneConverterFlags{}); + CORRADE_COMPARE(converter._flags, SceneConverterFlags{}); + converter.setFlags(SceneConverterFlag::Verbose); + CORRADE_COMPARE(converter.flags(), SceneConverterFlag::Verbose); + CORRADE_COMPARE(converter._flags, SceneConverterFlag::Verbose); +} + +void AbstractSceneConverterTest::setFlagsNotImplemented() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { + /* Assuming this bit is unused */ + return SceneConverterFeature(1 << 7); + } + } converter; + + CORRADE_COMPARE(converter.flags(), SceneConverterFlags{}); + converter.setFlags(SceneConverterFlag::Verbose); + CORRADE_COMPARE(converter.flags(), SceneConverterFlag::Verbose); + /* Should just work, no need to implement the function */ +} + +void AbstractSceneConverterTest::thingNotSupported() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { + /* Assuming this bit is unused */ + return SceneConverterFeature(1 << 7); + } + } converter; + + MeshData mesh{MeshPrimitive::Triangles, 3}; + + std::ostringstream out; + Error redirectError{&out}; + converter.convert(mesh); + converter.convertInPlace(mesh); + converter.convertToData(mesh); + converter.convertToFile(Utility::Directory::join(TRADE_TEST_OUTPUT_DIR, "mesh.out"), mesh); + CORRADE_COMPARE(out.str(), + "Trade::AbstractSceneConverter::convert(): mesh conversion not supported\n" + "Trade::AbstractSceneConverter::convertInPlace(): mesh conversion not supported\n" + "Trade::AbstractSceneConverter::convertToData(): mesh conversion not supported\n" + "Trade::AbstractSceneConverter::convertToFile(): mesh conversion not supported\n"); +} + +void AbstractSceneConverterTest::convertMesh() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMesh; } + + Containers::Optional doConvert(const MeshData& mesh) override { + if(mesh.primitive() == MeshPrimitive::Triangles) + return MeshData{MeshPrimitive::Lines, mesh.vertexCount()*2}; + return {}; + } + } converter; + + Containers::Optional out = converter.convert(MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_VERIFY(out); + CORRADE_COMPARE(out->primitive(), MeshPrimitive::Lines); + CORRADE_COMPARE(out->vertexCount(), 12); +} + +void AbstractSceneConverterTest::convertMeshNotImplemented() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMesh; } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convert(MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_COMPARE(out.str(), "Trade::AbstractSceneConverter::convert(): mesh conversion advertised but not implemented\n"); +} + +void AbstractSceneConverterTest::convertMeshNonOwningDeleters() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMesh; } + + Containers::Optional doConvert(const MeshData&) override { + return MeshData{MeshPrimitive::Triangles, + Containers::Array{indexData, 1, Implementation::nonOwnedArrayDeleter}, MeshIndexData{MeshIndexType::UnsignedByte, indexData}, + Containers::Array{nullptr, 0, Implementation::nonOwnedArrayDeleter}, + meshAttributeDataNonOwningArray(attributes)}; + } + + char indexData[1]; + MeshAttributeData attributes[1]{ + MeshAttributeData{MeshAttribute::Position, VertexFormat::Vector3, nullptr} + }; + } converter; + + Containers::Optional out = converter.convert(MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_VERIFY(out); + CORRADE_COMPARE(static_cast(out->indexData()), converter.indexData); +} + +void AbstractSceneConverterTest::convertMeshGrowableDeleters() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMesh; } + + Containers::Optional doConvert(const MeshData&) override { + Containers::Array indexData; + Containers::arrayAppend(indexData, '\xab'); + Containers::Array vertexData; + Containers::arrayAppend(vertexData, Vector3{}); + MeshIndexData indices{MeshIndexType::UnsignedByte, indexData}; + MeshAttributeData positions{MeshAttribute::Position, Containers::arrayView(vertexData)}; + + return MeshData{MeshPrimitive::Triangles, + std::move(indexData), indices, + Containers::arrayAllocatorCast(std::move(vertexData)), {positions}}; + } + + char indexData[1]; + MeshAttributeData attributes[1]{ + MeshAttributeData{MeshAttribute::Position, VertexFormat::Vector3, nullptr} + }; + } converter; + + Containers::Optional out = converter.convert(MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_VERIFY(out); + CORRADE_COMPARE(out->indexData()[0], '\xab'); + CORRADE_COMPARE(out->vertexData().size(), 12); +} + +void AbstractSceneConverterTest::convertMeshCustomIndexDataDeleter() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMesh; } + + Containers::Optional doConvert(const MeshData&) override { + return MeshData{MeshPrimitive::Triangles, Containers::Array{data, 1, [](char*, std::size_t) {}}, MeshIndexData{MeshIndexType::UnsignedByte, data}, 1}; + } + + char data[1]; + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convert(MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_COMPARE(out.str(), + "Trade::AbstractSceneConverter::convert(): implementation is not allowed to use a custom Array deleter\n"); +} + +void AbstractSceneConverterTest::convertMeshCustomVertexDataDeleter() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMesh; } + + Containers::Optional doConvert(const MeshData&) override { + return MeshData{MeshPrimitive::Triangles, Containers::Array{data, 1, [](char*, std::size_t) {}}, MeshIndexData{MeshIndexType::UnsignedByte, data}, 1}; + } + + char data[1]; + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convert(MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_COMPARE(out.str(), + "Trade::AbstractSceneConverter::convert(): implementation is not allowed to use a custom Array deleter\n"); +} + +void AbstractSceneConverterTest::convertMeshCustomAttributeDataDeleter() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMesh; } + + Containers::Optional doConvert(const MeshData&) override { + return MeshData{MeshPrimitive::Triangles, Containers::Array{data, 1, [](char*, std::size_t) {}}, MeshIndexData{MeshIndexType::UnsignedByte, data}, 1}; + } + + char data[1]; + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convert(MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_COMPARE(out.str(), + "Trade::AbstractSceneConverter::convert(): implementation is not allowed to use a custom Array deleter\n"); +} + +void AbstractSceneConverterTest::convertMeshInPlace() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMeshInPlace; } + + bool doConvertInPlace(MeshData& mesh) override { + auto indices = mesh.mutableIndices(); + for(std::size_t i = 0; i != indices.size()/2; ++i) + std::swap(indices[i], indices[indices.size() - i -1]); + return true; + } + } converter; + + UnsignedInt indices[]{1, 2, 3, 4, 2, 0}; + MeshData mesh{MeshPrimitive::Triangles, + DataFlag::Mutable, indices, MeshIndexData{indices}, 5}; + CORRADE_VERIFY(converter.convertInPlace(mesh)); + CORRADE_COMPARE_AS(mesh.indices(), + Containers::arrayView({0, 2, 4, 3, 2, 1}), + TestSuite::Compare::Container); +} + +void AbstractSceneConverterTest::convertMeshInPlaceNotImplemented() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMeshInPlace; } + } converter; + + MeshData mesh{MeshPrimitive::Triangles, 3}; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertInPlace(mesh); + CORRADE_COMPARE(out.str(), "Trade::AbstractSceneConverter::convertInPlace(): mesh conversion advertised but not implemented\n"); +} + +void AbstractSceneConverterTest::convertMeshToData() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMeshToData; } + + Containers::Array doConvertToData(const MeshData& mesh) override { + return Containers::Array{nullptr, mesh.vertexCount()}; + } + } converter; + + Containers::Array data = converter.convertToData(MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_COMPARE(data.size(), 6); +} + +void AbstractSceneConverterTest::convertMeshToDataNotImplemented() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMeshToData; } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertToData(MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_COMPARE(out.str(), "Trade::AbstractSceneConverter::convertToData(): mesh conversion advertised but not implemented\n"); +} + +void AbstractSceneConverterTest::convertMeshToDataCustomDeleter() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMeshToData; } + + Containers::Array doConvertToData(const MeshData&) override { + return Containers::Array{data, 1, [](char*, std::size_t) {}}; + } + + char data[1]; + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertToData(MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_COMPARE(out.str(), "Trade::AbstractSceneConverter::convertToData(): implementation is not allowed to use a custom Array deleter\n"); +} + +void AbstractSceneConverterTest::convertMeshToFile() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMeshToFile; } + + bool doConvertToFile(const std::string& filename, const MeshData& mesh) override { + return Utility::Directory::write(filename, Containers::arrayView( {char(mesh.vertexCount())})); + } + } converter; + + /* Remove previous file */ + Utility::Directory::rm(Utility::Directory::join(TRADE_TEST_OUTPUT_DIR, "mesh.out")); + + CORRADE_VERIFY(converter.convertToFile(Utility::Directory::join(TRADE_TEST_OUTPUT_DIR, "mesh.out"), MeshData{MeshPrimitive::Triangles, 0xef})); + CORRADE_COMPARE_AS(Utility::Directory::join(TRADE_TEST_OUTPUT_DIR, "mesh.out"), + "\xef", TestSuite::Compare::FileToString); +} + +void AbstractSceneConverterTest::convertMeshToFileThroughData() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMeshToData; } + + Containers::Array doConvertToData(const Magnum::Trade::MeshData & mesh) override { + return Containers::array({char(mesh.vertexCount())}); + } + } converter; + + /* Remove previous file */ + Utility::Directory::rm(Utility::Directory::join(TRADE_TEST_OUTPUT_DIR, "mesh.out")); + + CORRADE_VERIFY(converter.convertToFile(Utility::Directory::join(TRADE_TEST_OUTPUT_DIR, "mesh.out"), MeshData{MeshPrimitive::Triangles, 0xef})); + CORRADE_COMPARE_AS(Utility::Directory::join(TRADE_TEST_OUTPUT_DIR, "mesh.out"), + "\xef", TestSuite::Compare::FileToString); +} + +void AbstractSceneConverterTest::convertMeshToFileThroughDataNotWritable() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMeshToData; } + + Containers::Array doConvertToData(const Magnum::Trade::MeshData & mesh) override { + return Containers::array({char(mesh.vertexCount())}); + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter.convertToFile("/some/path/that/does/not/exist", MeshData{MeshPrimitive::Triangles, 0xef})); + CORRADE_COMPARE(out.str(), + "Utility::Directory::write(): can't open /some/path/that/does/not/exist\n" + "Trade::AbstractSceneConverter::convertToFile(): cannot write to file /some/path/that/does/not/exist\n"); +} + +void AbstractSceneConverterTest::convertMeshToFileNotImplemented() { + struct: AbstractSceneConverter { + SceneConverterFeatures doFeatures() const override { return SceneConverterFeature::ConvertMeshToFile; } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertToFile(Utility::Directory::join(TRADE_TEST_OUTPUT_DIR, "mesh.out"), MeshData{MeshPrimitive::Triangles, 6}); + CORRADE_COMPARE(out.str(), "Trade::AbstractSceneConverter::convertToFile(): mesh conversion advertised but not implemented\n"); +} + +void AbstractSceneConverterTest::debugFeature() { + std::ostringstream out; + + Debug{&out} << SceneConverterFeature::ConvertMeshInPlace << SceneConverterFeature(0xf0); + CORRADE_COMPARE(out.str(), "Trade::SceneConverterFeature::ConvertMeshInPlace Trade::SceneConverterFeature(0xf0)\n"); +} + +void AbstractSceneConverterTest::debugFeatures() { + std::ostringstream out; + + Debug{&out} << (SceneConverterFeature::ConvertMesh|SceneConverterFeature::ConvertMeshToFile) << SceneConverterFeatures{}; + CORRADE_COMPARE(out.str(), "Trade::SceneConverterFeature::ConvertMesh|Trade::SceneConverterFeature::ConvertMeshToFile Trade::SceneConverterFeatures{}\n"); +} + +void AbstractSceneConverterTest::debugFlag() { + std::ostringstream out; + + Debug{&out} << SceneConverterFlag::Verbose << SceneConverterFlag(0xf0); + CORRADE_COMPARE(out.str(), "Trade::SceneConverterFlag::Verbose Trade::SceneConverterFlag(0xf0)\n"); +} + +void AbstractSceneConverterTest::debugFlags() { + std::ostringstream out; + + Debug{&out} << (SceneConverterFlag::Verbose|SceneConverterFlag(0xf0)) << SceneConverterFlags{}; + CORRADE_COMPARE(out.str(), "Trade::SceneConverterFlag::Verbose|Trade::SceneConverterFlag(0xf0) Trade::SceneConverterFlags{}\n"); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Trade::Test::AbstractSceneConverterTest) diff --git a/src/Magnum/Trade/Test/CMakeLists.txt b/src/Magnum/Trade/Test/CMakeLists.txt index f89f44e45..244b018be 100644 --- a/src/Magnum/Trade/Test/CMakeLists.txt +++ b/src/Magnum/Trade/Test/CMakeLists.txt @@ -42,6 +42,10 @@ corrade_add_test(TradeAbstractImporterTest AbstractImporterTest.cpp FILES file.bin) target_include_directories(TradeAbstractImporterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +corrade_add_test(TradeAbstractSceneConverterTest AbstractSceneConverterTest.cpp + LIBRARIES MagnumTradeTestLib) +target_include_directories(TradeAbstractSceneConverterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + corrade_add_test(TradeAnimationDataTest AnimationDataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeCameraDataTest CameraDataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeDataTest DataTest.cpp LIBRARIES MagnumTrade) @@ -62,6 +66,7 @@ set_property(TARGET set_target_properties( TradeAbstractImageConverterTest TradeAbstractImporterTest + TradeAbstractSceneConverterTest TradeAnimationDataTest TradeCameraDataTest TradeImageDataTest diff --git a/src/Magnum/Trade/Trade.h b/src/Magnum/Trade/Trade.h index d4df385bd..e146d5ef1 100644 --- a/src/Magnum/Trade/Trade.h +++ b/src/Magnum/Trade/Trade.h @@ -42,6 +42,7 @@ namespace Magnum { namespace Trade { #ifndef DOXYGEN_GENERATING_OUTPUT class AbstractImageConverter; class AbstractImporter; +class AbstractSceneConverter; #ifdef MAGNUM_BUILD_DEPRECATED typedef CORRADE_DEPRECATED("use InputFileCallbackPolicy instead") InputFileCallbackPolicy ImporterFileCallbackPolicy; diff --git a/src/Magnum/Trade/configure.h.cmake b/src/Magnum/Trade/configure.h.cmake index 024c1178f..c969a20cb 100644 --- a/src/Magnum/Trade/configure.h.cmake +++ b/src/Magnum/Trade/configure.h.cmake @@ -27,3 +27,5 @@ #define MAGNUM_PLUGINS_IMPORTER_DEBUG_DIR "${MAGNUM_PLUGINS_IMPORTER_DEBUG_DIR}" #define MAGNUM_PLUGINS_IMAGECONVERTER_DIR "${MAGNUM_PLUGINS_IMAGECONVERTER_DIR}" #define MAGNUM_PLUGINS_IMAGECONVERTER_DEBUG_DIR "${MAGNUM_PLUGINS_IMAGECONVERTER_DEBUG_DIR}" +#define MAGNUM_PLUGINS_SCENECONVERTER_DIR "${MAGNUM_PLUGINS_SCENECONVERTER_DIR}" +#define MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_DIR "${MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_DIR}" From e8d66222658836b1a6b4362fe11d1890f2605906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 14 Feb 2020 11:35:21 +0100 Subject: [PATCH 06/16] AnySceneConverter: new plugin. --- CMakeLists.txt | 1 + doc/building.dox | 2 + doc/changelog.dox | 3 +- doc/cmake.dox | 2 + doc/plugins.dox | 2 + modules/FindMagnum.cmake | 7 +- package/archlinux/PKGBUILD | 1 + package/archlinux/PKGBUILD-android-arm64 | 1 + package/archlinux/PKGBUILD-clang | 1 + .../archlinux/PKGBUILD-clang-addressanitizer | 1 + package/archlinux/PKGBUILD-clang-analyzer | 1 + package/archlinux/PKGBUILD-clang-libc++ | 1 + package/archlinux/PKGBUILD-coverage | 1 + package/archlinux/PKGBUILD-emscripten | 1 + package/archlinux/PKGBUILD-emscripten-wasm | 1 + .../archlinux/PKGBUILD-emscripten-wasm-webgl2 | 1 + package/archlinux/PKGBUILD-emscripten-webgl2 | 1 + package/archlinux/PKGBUILD-es2 | 1 + package/archlinux/PKGBUILD-es2desktop | 1 + package/archlinux/PKGBUILD-es3 | 1 + package/archlinux/PKGBUILD-es3desktop | 1 + package/archlinux/PKGBUILD-gcc48 | 1 + package/archlinux/PKGBUILD-mingw-w64 | 2 + package/archlinux/PKGBUILD-release | 2 + package/archlinux/magnum-git/PKGBUILD | 1 + package/archlinux/magnum/PKGBUILD | 1 + package/ci/appveyor-desktop-gles.bat | 1 + package/ci/appveyor-desktop-mingw.bat | 1 + package/ci/appveyor-desktop-vulkan.bat | 1 + package/ci/appveyor-desktop.bat | 1 + package/ci/appveyor-rt.bat | 1 + package/ci/travis-android-arm.sh | 1 + package/ci/travis-desktop-gles.sh | 1 + package/ci/travis-desktop-vulkan.sh | 1 + package/ci/travis-desktop.sh | 1 + package/ci/travis-emscripten.sh | 1 + package/ci/travis-ios-simulator.sh | 1 + package/debian/rules | 1 + .../gentoo/dev-libs/magnum/magnum-9999.ebuild | 1 + package/homebrew/magnum.rb | 2 +- package/msys/PKGBUILD | 1 + package/msys/magnum/PKGBUILD | 1 + .../AnySceneConverter/AnySceneConverter.conf | 0 .../AnySceneConverter/AnySceneConverter.cpp | 80 ++++++++++++ .../AnySceneConverter/AnySceneConverter.h | 111 ++++++++++++++++ .../AnySceneConverter/CMakeLists.txt | 68 ++++++++++ .../Test/AnySceneConverterTest.cpp | 121 ++++++++++++++++++ .../AnySceneConverter/Test/CMakeLists.txt | 71 ++++++++++ .../AnySceneConverter/Test/configure.h.cmake | 27 ++++ .../AnySceneConverter/configure.h.cmake | 26 ++++ .../AnySceneConverter/importStaticPlugin.cpp | 35 +++++ src/MagnumPlugins/CMakeLists.txt | 4 + 52 files changed, 594 insertions(+), 5 deletions(-) create mode 100644 src/MagnumPlugins/AnySceneConverter/AnySceneConverter.conf create mode 100644 src/MagnumPlugins/AnySceneConverter/AnySceneConverter.cpp create mode 100644 src/MagnumPlugins/AnySceneConverter/AnySceneConverter.h create mode 100644 src/MagnumPlugins/AnySceneConverter/CMakeLists.txt create mode 100644 src/MagnumPlugins/AnySceneConverter/Test/AnySceneConverterTest.cpp create mode 100644 src/MagnumPlugins/AnySceneConverter/Test/CMakeLists.txt create mode 100644 src/MagnumPlugins/AnySceneConverter/Test/configure.h.cmake create mode 100644 src/MagnumPlugins/AnySceneConverter/configure.h.cmake create mode 100644 src/MagnumPlugins/AnySceneConverter/importStaticPlugin.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index da7d6f852..bcd8b7139 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,7 @@ option(WITH_AL_INFO "Build magnum-al-info utility" OFF) option(WITH_ANYIMAGEIMPORTER "Build AnyImageImporter plugin" OFF) option(WITH_ANYAUDIOIMPORTER "Build AnyAudioImporter plugin" OFF) option(WITH_ANYIMAGECONVERTER "Build AnyImageConverter plugin" OFF) +option(WITH_ANYSCENECONVERTER "Build AnySceneConverter plugin" OFF) option(WITH_ANYSCENEIMPORTER "Build AnySceneImporter plugin" OFF) option(WITH_WAVAUDIOIMPORTER "Build WavAudioImporter plugin" OFF) option(WITH_MAGNUMFONT "Build MagnumFont plugin" OFF) diff --git a/doc/building.dox b/doc/building.dox index d83e6f2a2..26eed9293 100644 --- a/doc/building.dox +++ b/doc/building.dox @@ -650,6 +650,8 @@ default. building of the @ref Trade library. - `WITH_ANYIMAGEIMPORTER` --- Build the @ref Trade::AnyImageImporter "AnyImageImporter" plugin. Enables also building of the @ref Trade library. +- `WITH_ANYSCENECONVERTER` --- Build the @ref Trade::AnySceneConverter "AnySceneConverter" + plugin. Enables also building of the @ref Trade library. - `WITH_ANYSCENEIMPORTER` --- Build the @ref Trade::AnySceneImporter "AnySceneImporter" plugin. Enables also building of the @ref Trade library. - `WITH_MAGNUMFONT` --- Build the @ref Text::MagnumFont "MagnumFont" plugin. diff --git a/doc/changelog.dox b/doc/changelog.dox index e2a8a149f..8df4b4fbb 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -270,7 +270,8 @@ See also: - New @ref magnum-sceneconverter "magnum-sceneconverter" tool, similar to @ref magnum-imageconverter "magnum-imageconverter" but suited for general scene formats -- New @ref Trade::AbstractSceneConverter plugin interface +- New @ref Trade::AbstractSceneConverter plugin interface and an + @ref Trade::AnySceneConverter "AnySceneConverter" plugin - Ability to import image mip levels via an additional parameter in @ref Trade::AbstractImporter::image2D(), @ref Trade::AbstractImporter::image2DLevelCount() and similar APIs for 1D diff --git a/doc/cmake.dox b/doc/cmake.dox index 5617decf9..a53f544c8 100644 --- a/doc/cmake.dox +++ b/doc/cmake.dox @@ -246,6 +246,8 @@ dependencies, you need to find the dependency and then link to it. plugin - `AnyImageImporter` --- @ref Trade::AnyImageImporter "AnyImageImporter" plugin +- `AnySceneConverter` --- @ref Trade::AnySceneConverter "AnySceneConverter" + plugin - `AnySceneImporter` --- @ref Trade::AnySceneImporter "AnySceneImporter" plugin - `MagnumFont` --- @ref Text::MagnumFont "MagnumFont" plugin diff --git a/doc/plugins.dox b/doc/plugins.dox index a2d746e79..fd9d93075 100644 --- a/doc/plugins.dox +++ b/doc/plugins.dox @@ -195,6 +195,8 @@ So far, the following plugins have the "any format" ability: format - @ref Trade::AnySceneImporter "AnySceneImporter" --- imports any scene format +- @ref Trade::AnySceneConverter "AnySceneConverter" --- converts to any scene + format - @ref Audio::AnyImporter "AnyImporter" --- imports any audio format @section plugins-configuration Plugin-specific configuration diff --git a/modules/FindMagnum.cmake b/modules/FindMagnum.cmake index ea82c72d9..121727c7d 100644 --- a/modules/FindMagnum.cmake +++ b/modules/FindMagnum.cmake @@ -55,6 +55,7 @@ # AnyAudioImporter - Any audio importer # AnyImageConverter - Any image converter # AnyImageImporter - Any image importer +# AnySceneConverter - Any scene converter # AnySceneImporter - Any scene importer # Audio - Audio library # DebugTools - DebugTools library @@ -360,9 +361,9 @@ set(_MAGNUM_LIBRARY_COMPONENT_LIST CglContext EglContext GlxContext WglContext OpenGLTester) set(_MAGNUM_PLUGIN_COMPONENT_LIST - AnyAudioImporter AnyImageConverter AnyImageImporter AnySceneImporter - MagnumFont MagnumFontConverter ObjImporter TgaImageConverter TgaImporter - WavAudioImporter) + AnyAudioImporter AnyImageConverter AnyImageImporter AnySceneConverter + AnySceneImporter MagnumFont MagnumFontConverter ObjImporter + TgaImageConverter TgaImporter WavAudioImporter) set(_MAGNUM_EXECUTABLE_COMPONENT_LIST distancefieldconverter fontconverter imageconverter sceneconverter gl-info al-info) diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 0ab7c0642..3f418875f 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -38,6 +38,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-android-arm64 b/package/archlinux/PKGBUILD-android-arm64 index 8fa8a21c5..8572eee98 100644 --- a/package/archlinux/PKGBUILD-android-arm64 +++ b/package/archlinux/PKGBUILD-android-arm64 @@ -35,6 +35,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=OFF \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_OBJIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-clang b/package/archlinux/PKGBUILD-clang index 2cd296309..b55f51dee 100644 --- a/package/archlinux/PKGBUILD-clang +++ b/package/archlinux/PKGBUILD-clang @@ -41,6 +41,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-clang-addressanitizer b/package/archlinux/PKGBUILD-clang-addressanitizer index fe09aa288..851686bf0 100644 --- a/package/archlinux/PKGBUILD-clang-addressanitizer +++ b/package/archlinux/PKGBUILD-clang-addressanitizer @@ -42,6 +42,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-clang-analyzer b/package/archlinux/PKGBUILD-clang-analyzer index 61f69baeb..6697201e7 100644 --- a/package/archlinux/PKGBUILD-clang-analyzer +++ b/package/archlinux/PKGBUILD-clang-analyzer @@ -33,6 +33,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-clang-libc++ b/package/archlinux/PKGBUILD-clang-libc++ index a036b7f15..82183898a 100644 --- a/package/archlinux/PKGBUILD-clang-libc++ +++ b/package/archlinux/PKGBUILD-clang-libc++ @@ -44,6 +44,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-coverage b/package/archlinux/PKGBUILD-coverage index 18b02e4ea..cc4a32d33 100644 --- a/package/archlinux/PKGBUILD-coverage +++ b/package/archlinux/PKGBUILD-coverage @@ -39,6 +39,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-emscripten b/package/archlinux/PKGBUILD-emscripten index fbaec668b..a77a49d91 100644 --- a/package/archlinux/PKGBUILD-emscripten +++ b/package/archlinux/PKGBUILD-emscripten @@ -35,6 +35,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_OBJIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-emscripten-wasm b/package/archlinux/PKGBUILD-emscripten-wasm index c4998b934..067979a66 100644 --- a/package/archlinux/PKGBUILD-emscripten-wasm +++ b/package/archlinux/PKGBUILD-emscripten-wasm @@ -35,6 +35,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_OBJIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 b/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 index 00ad22b87..0fae3814c 100644 --- a/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 +++ b/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 @@ -36,6 +36,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_OBJIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-emscripten-webgl2 b/package/archlinux/PKGBUILD-emscripten-webgl2 index 98c16041b..b3adcbe51 100644 --- a/package/archlinux/PKGBUILD-emscripten-webgl2 +++ b/package/archlinux/PKGBUILD-emscripten-webgl2 @@ -36,6 +36,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_OBJIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-es2 b/package/archlinux/PKGBUILD-es2 index 4e883768b..ee6b9ba64 100644 --- a/package/archlinux/PKGBUILD-es2 +++ b/package/archlinux/PKGBUILD-es2 @@ -31,6 +31,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_OBJIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-es2desktop b/package/archlinux/PKGBUILD-es2desktop index 7067de768..40bb601cc 100644 --- a/package/archlinux/PKGBUILD-es2desktop +++ b/package/archlinux/PKGBUILD-es2desktop @@ -35,6 +35,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_OBJIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-es3 b/package/archlinux/PKGBUILD-es3 index 5bb5d87d2..f4078fb1d 100644 --- a/package/archlinux/PKGBUILD-es3 +++ b/package/archlinux/PKGBUILD-es3 @@ -31,6 +31,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_OBJIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-es3desktop b/package/archlinux/PKGBUILD-es3desktop index 01606f3c0..5dcbb9b38 100644 --- a/package/archlinux/PKGBUILD-es3desktop +++ b/package/archlinux/PKGBUILD-es3desktop @@ -35,6 +35,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_OBJIMPORTER=ON \ diff --git a/package/archlinux/PKGBUILD-gcc48 b/package/archlinux/PKGBUILD-gcc48 index 7ca76d511..b78153ee3 100644 --- a/package/archlinux/PKGBUILD-gcc48 +++ b/package/archlinux/PKGBUILD-gcc48 @@ -44,6 +44,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-mingw-w64 b/package/archlinux/PKGBUILD-mingw-w64 index 3570df1ba..db4471229 100644 --- a/package/archlinux/PKGBUILD-mingw-w64 +++ b/package/archlinux/PKGBUILD-mingw-w64 @@ -28,6 +28,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ @@ -61,6 +62,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-release b/package/archlinux/PKGBUILD-release index ffccc86fa..e86120aed 100644 --- a/package/archlinux/PKGBUILD-release +++ b/package/archlinux/PKGBUILD-release @@ -33,6 +33,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ @@ -70,6 +71,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/archlinux/magnum-git/PKGBUILD b/package/archlinux/magnum-git/PKGBUILD index 1e636a8cc..8cc12a36b 100644 --- a/package/archlinux/magnum-git/PKGBUILD +++ b/package/archlinux/magnum-git/PKGBUILD @@ -36,6 +36,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/archlinux/magnum/PKGBUILD b/package/archlinux/magnum/PKGBUILD index e7d6ee39d..c2ad4771d 100644 --- a/package/archlinux/magnum/PKGBUILD +++ b/package/archlinux/magnum/PKGBUILD @@ -25,6 +25,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_AUDIO=ON \ -DWITH_DISTANCEFIELDCONVERTER=ON \ diff --git a/package/ci/appveyor-desktop-gles.bat b/package/ci/appveyor-desktop-gles.bat index cd93b02b9..4f44ef206 100644 --- a/package/ci/appveyor-desktop-gles.bat +++ b/package/ci/appveyor-desktop-gles.bat @@ -33,6 +33,7 @@ cmake .. ^ -DWITH_ANYAUDIOIMPORTER=ON ^ -DWITH_ANYIMAGECONVERTER=ON ^ -DWITH_ANYIMAGEIMPORTER=ON ^ + -DWITH_ANYSCENECONVERTER=ON ^ -DWITH_ANYSCENEIMPORTER=ON ^ -DWITH_MAGNUMFONT=ON ^ -DWITH_MAGNUMFONTCONVERTER=ON ^ diff --git a/package/ci/appveyor-desktop-mingw.bat b/package/ci/appveyor-desktop-mingw.bat index ce69848d1..7e9b75601 100644 --- a/package/ci/appveyor-desktop-mingw.bat +++ b/package/ci/appveyor-desktop-mingw.bat @@ -32,6 +32,7 @@ cmake .. ^ -DWITH_ANYAUDIOIMPORTER=ON ^ -DWITH_ANYIMAGECONVERTER=ON ^ -DWITH_ANYIMAGEIMPORTER=ON ^ + -DWITH_ANYSCENECONVERTER=ON ^ -DWITH_ANYSCENEIMPORTER=ON ^ -DWITH_MAGNUMFONT=ON ^ -DWITH_MAGNUMFONTCONVERTER=ON ^ diff --git a/package/ci/appveyor-desktop-vulkan.bat b/package/ci/appveyor-desktop-vulkan.bat index 4b9ad216c..6414b66aa 100644 --- a/package/ci/appveyor-desktop-vulkan.bat +++ b/package/ci/appveyor-desktop-vulkan.bat @@ -43,6 +43,7 @@ cmake .. ^ -DWITH_ANYAUDIOIMPORTER=OFF ^ -DWITH_ANYIMAGECONVERTER=OFF ^ -DWITH_ANYIMAGEIMPORTER=OFF ^ + -DWITH_ANYSCENECONVERTER=ON ^ -DWITH_ANYSCENEIMPORTER=OFF ^ -DWITH_MAGNUMFONT=OFF ^ -DWITH_MAGNUMFONTCONVERTER=OFF ^ diff --git a/package/ci/appveyor-desktop.bat b/package/ci/appveyor-desktop.bat index 409ec498f..fa5333655 100644 --- a/package/ci/appveyor-desktop.bat +++ b/package/ci/appveyor-desktop.bat @@ -45,6 +45,7 @@ cmake .. ^ -DWITH_ANYAUDIOIMPORTER=ON ^ -DWITH_ANYIMAGECONVERTER=ON ^ -DWITH_ANYIMAGEIMPORTER=ON ^ + -DWITH_ANYSCENECONVERTER=ON ^ -DWITH_ANYSCENEIMPORTER=ON ^ -DWITH_MAGNUMFONT=ON ^ -DWITH_MAGNUMFONTCONVERTER=ON ^ diff --git a/package/ci/appveyor-rt.bat b/package/ci/appveyor-rt.bat index 8f3d4c900..9a7f6b224 100644 --- a/package/ci/appveyor-rt.bat +++ b/package/ci/appveyor-rt.bat @@ -71,6 +71,7 @@ cmake .. ^ -DWITH_ANYAUDIOIMPORTER=OFF ^ -DWITH_ANYIMAGECONVERTER=ON ^ -DWITH_ANYIMAGEIMPORTER=ON ^ + -DWITH_ANYSCENECONVERTER=ON ^ -DWITH_ANYSCENEIMPORTER=ON ^ -DWITH_MAGNUMFONT=ON ^ -DWITH_MAGNUMFONTCONVERTER=ON ^ diff --git a/package/ci/travis-android-arm.sh b/package/ci/travis-android-arm.sh index 090882740..b1ba1d3c9 100755 --- a/package/ci/travis-android-arm.sh +++ b/package/ci/travis-android-arm.sh @@ -63,6 +63,7 @@ cmake .. \ -DWITH_ANYAUDIOIMPORTER=OFF \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/ci/travis-desktop-gles.sh b/package/ci/travis-desktop-gles.sh index 420eed647..17642843e 100755 --- a/package/ci/travis-desktop-gles.sh +++ b/package/ci/travis-desktop-gles.sh @@ -37,6 +37,7 @@ cmake .. \ -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/ci/travis-desktop-vulkan.sh b/package/ci/travis-desktop-vulkan.sh index a81b6ef83..fb0f7f076 100755 --- a/package/ci/travis-desktop-vulkan.sh +++ b/package/ci/travis-desktop-vulkan.sh @@ -47,6 +47,7 @@ cmake .. \ -DWITH_ANYAUDIOIMPORTER=OFF \ -DWITH_ANYIMAGECONVERTER=OFF \ -DWITH_ANYIMAGEIMPORTER=OFF \ + -DWITH_ANYSCENECONVERTER=OFF \ -DWITH_ANYSCENEIMPORTER=OFF \ -DWITH_MAGNUMFONT=OFF \ -DWITH_MAGNUMFONTCONVERTER=OFF \ diff --git a/package/ci/travis-desktop.sh b/package/ci/travis-desktop.sh index 553989a3c..878884ac1 100755 --- a/package/ci/travis-desktop.sh +++ b/package/ci/travis-desktop.sh @@ -33,6 +33,7 @@ cmake .. \ -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/ci/travis-emscripten.sh b/package/ci/travis-emscripten.sh index 8e886ecf4..a246c667d 100755 --- a/package/ci/travis-emscripten.sh +++ b/package/ci/travis-emscripten.sh @@ -56,6 +56,7 @@ cmake .. \ -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/ci/travis-ios-simulator.sh b/package/ci/travis-ios-simulator.sh index 48b87cc02..af66da840 100755 --- a/package/ci/travis-ios-simulator.sh +++ b/package/ci/travis-ios-simulator.sh @@ -53,6 +53,7 @@ cmake .. \ -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/debian/rules b/package/debian/rules index 482d8bdb5..e6f42e9e4 100755 --- a/package/debian/rules +++ b/package/debian/rules @@ -26,6 +26,7 @@ override_dh_auto_configure: -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/gentoo/dev-libs/magnum/magnum-9999.ebuild b/package/gentoo/dev-libs/magnum/magnum-9999.ebuild index a4eb16c4b..85f972db5 100644 --- a/package/gentoo/dev-libs/magnum/magnum-9999.ebuild +++ b/package/gentoo/dev-libs/magnum/magnum-9999.ebuild @@ -35,6 +35,7 @@ src_configure() { -DWITH_ANYAUDIOIMPORTER=ON -DWITH_ANYIMAGECONVERTER=ON -DWITH_ANYIMAGEIMPORTER=ON + -DWITH_ANYSCENECONVERTER=ON -DWITH_ANYSCENEIMPORTER=ON -DWITH_MAGNUMFONT=ON -DWITH_MAGNUMFONTCONVERTER=ON diff --git a/package/homebrew/magnum.rb b/package/homebrew/magnum.rb index e52627d3d..374891663 100644 --- a/package/homebrew/magnum.rb +++ b/package/homebrew/magnum.rb @@ -14,7 +14,7 @@ class Magnum < Formula def install system "mkdir build" cd "build" do - system "cmake", "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_PREFIX=#{prefix}", "-DMAGNUM_PLUGINS_DIR=#{HOMEBREW_PREFIX}/lib/magnum", "-DWITH_AUDIO=ON", "-DWITH_GLFWAPPLICATION=ON", "-DWITH_SDL2APPLICATION=ON", "-DWITH_WINDOWLESSCGLAPPLICATION=ON", "-DWITH_CGLCONTEXT=ON", "-DWITH_OPENGLTESTER=ON", "-DWITH_ANYAUDIOIMPORTER=ON", "-DWITH_ANYIMAGECONVERTER=ON", "-DWITH_ANYIMAGEIMPORTER=ON", "-DWITH_ANYSCENEIMPORTER=ON", "-DWITH_MAGNUMFONT=ON", "-DWITH_MAGNUMFONTCONVERTER=ON", "-DWITH_OBJIMPORTER=ON", "-DWITH_TGAIMAGECONVERTER=ON", "-DWITH_TGAIMPORTER=ON", "-DWITH_WAVAUDIOIMPORTER=ON", "-DWITH_DISTANCEFIELDCONVERTER=ON", "-DWITH_FONTCONVERTER=ON", "-DWITH_IMAGECONVERTER=ON", "-DWITH_SCENECONVERTER=ON", "-DWITH_GL_INFO=ON", "-DWITH_AL_INFO=ON", ".." + system "cmake", "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_PREFIX=#{prefix}", "-DMAGNUM_PLUGINS_DIR=#{HOMEBREW_PREFIX}/lib/magnum", "-DWITH_AUDIO=ON", "-DWITH_GLFWAPPLICATION=ON", "-DWITH_SDL2APPLICATION=ON", "-DWITH_WINDOWLESSCGLAPPLICATION=ON", "-DWITH_CGLCONTEXT=ON", "-DWITH_OPENGLTESTER=ON", "-DWITH_ANYAUDIOIMPORTER=ON", "-DWITH_ANYIMAGECONVERTER=ON", "-DWITH_ANYIMAGEIMPORTER=ON", "-DWITH_ANYSCENECONVERTER=ON", "-DWITH_ANYSCENEIMPORTER=ON", "-DWITH_MAGNUMFONT=ON", "-DWITH_MAGNUMFONTCONVERTER=ON", "-DWITH_OBJIMPORTER=ON", "-DWITH_TGAIMAGECONVERTER=ON", "-DWITH_TGAIMPORTER=ON", "-DWITH_WAVAUDIOIMPORTER=ON", "-DWITH_DISTANCEFIELDCONVERTER=ON", "-DWITH_FONTCONVERTER=ON", "-DWITH_IMAGECONVERTER=ON", "-DWITH_SCENECONVERTER=ON", "-DWITH_GL_INFO=ON", "-DWITH_AL_INFO=ON", ".." system "cmake", "--build", "." system "cmake", "--build", ".", "--target", "install" end diff --git a/package/msys/PKGBUILD b/package/msys/PKGBUILD index 0b81f66d0..6799f39de 100644 --- a/package/msys/PKGBUILD +++ b/package/msys/PKGBUILD @@ -40,6 +40,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_MAGNUMFONT=ON \ -DWITH_MAGNUMFONTCONVERTER=ON \ diff --git a/package/msys/magnum/PKGBUILD b/package/msys/magnum/PKGBUILD index 728bb2dbd..2cb5bb594 100644 --- a/package/msys/magnum/PKGBUILD +++ b/package/msys/magnum/PKGBUILD @@ -44,6 +44,7 @@ build() { -DWITH_ANYAUDIOIMPORTER=ON \ -DWITH_ANYIMAGECONVERTER=ON \ -DWITH_ANYIMAGEIMPORTER=ON \ + -DWITH_ANYSCENECONVERTER=ON \ -DWITH_ANYSCENEIMPORTER=ON \ -DWITH_AUDIO=ON \ -DWITH_DISTANCEFIELDCONVERTER=ON \ diff --git a/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.conf b/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.conf new file mode 100644 index 000000000..e69de29bb diff --git a/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.cpp b/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.cpp new file mode 100644 index 000000000..1ad64f482 --- /dev/null +++ b/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.cpp @@ -0,0 +1,80 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 "AnySceneConverter.h" + +#include +#include +#include +#include + +#include "Magnum/Trade/ImageData.h" + +namespace Magnum { namespace Trade { + +AnySceneConverter::AnySceneConverter(PluginManager::Manager& manager): AbstractSceneConverter{manager} {} + +AnySceneConverter::AnySceneConverter(PluginManager::AbstractManager& manager, const std::string& plugin): AbstractSceneConverter{manager, plugin} {} + +AnySceneConverter::~AnySceneConverter() = default; + +SceneConverterFeatures AnySceneConverter::doFeatures() const { + return SceneConverterFeature::ConvertMeshToFile; +} + +bool AnySceneConverter::doConvertToFile(const std::string& filename, const MeshData& mesh) { + CORRADE_INTERNAL_ASSERT(manager()); + + /** @todo lowercase only the extension, once Directory::split() is done */ + const std::string normalized = Utility::String::lowercase(filename); + + /* Detect the plugin from extension */ + std::string plugin; + if(Utility::String::endsWith(normalized, ".blob")) + plugin = "MagnumSceneConverter"; + else { + Error{} << "Trade::AnySceneConverter::convertToFile(): cannot determine the format of" << filename; + return false; + } + + /* Try to load the plugin */ + if(!(manager()->load(plugin) & PluginManager::LoadState::Loaded)) { + Error{} << "Trade::AnySceneConverter::convertToFile(): cannot load the" << plugin << "plugin"; + return false; + } + + /* Instantiate the plugin, propagate flags */ + Containers::Pointer converter = static_cast*>(manager())->instantiate(plugin); + converter->setFlags(flags()); + + /* Try to convert the file (error output should be printed by the plugin + itself) */ + return converter->convertToFile(filename, mesh); +} + +}} + +CORRADE_PLUGIN_REGISTER(AnySceneConverter, Magnum::Trade::AnySceneConverter, + "cz.mosra.magnum.Trade.AbstractSceneConverter/0.1") diff --git a/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.h b/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.h new file mode 100644 index 000000000..bafcfab7e --- /dev/null +++ b/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.h @@ -0,0 +1,111 @@ +#ifndef Magnum_Trade_AnySceneConverter_h +#define Magnum_Trade_AnySceneConverter_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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::Trade::AnySceneConverter + */ + +#include "Magnum/Trade/AbstractSceneConverter.h" +#include "MagnumPlugins/AnySceneConverter/configure.h" + +#ifndef DOXYGEN_GENERATING_OUTPUT +#ifndef MAGNUM_ANYSCENECONVERTER_BUILD_STATIC + #ifdef AnySceneConverter_EXPORTS + #define MAGNUM_ANYSCENECONVERTER_EXPORT CORRADE_VISIBILITY_EXPORT + #else + #define MAGNUM_ANYSCENECONVERTER_EXPORT CORRADE_VISIBILITY_IMPORT + #endif +#else + #define MAGNUM_ANYSCENECONVERTER_EXPORT CORRADE_VISIBILITY_STATIC +#endif +#define MAGNUM_ANYSCENECONVERTER_LOCAL CORRADE_VISIBILITY_LOCAL +#else +#define MAGNUM_ANYSCENECONVERTER_EXPORT +#define MAGNUM_ANYSCENECONVERTER_LOCAL +#endif + +namespace Magnum { namespace Trade { + +/** +@brief Any scene converter plugin + +Detects file type based on file extension, loads corresponding plugin and then +tries to convert the file with it. Supported formats: + +- @ref blob "Magnum's memory-mappable serialization format" (`*.blob`), + converted with @ref MagnumSceneConverter + +Only converting to files is supported. + +@section Trade-AnySceneConverter-usage Usage + +This plugin depends on the @ref Trade library and is built if +`WITH_ANYSCENECONVERTER` is enabled when building Magnum. To use as a dynamic +plugin, load @cpp "AnySceneConverter" @ce via +@ref Corrade::PluginManager::Manager. + +Additionally, if you're using Magnum as a CMake subproject, do the following: + +@code{.cmake} +set(WITH_ANYSCENECONVERTER ON CACHE BOOL "" FORCE) +add_subdirectory(magnum EXCLUDE_FROM_ALL) + +# So the dynamically loaded plugin gets built implicitly +add_dependencies(your-app Magnum::AnySceneConverter) +@endcode + +To use as a static plugin or as a dependency of another plugin with CMake, you +need to request the `AnySceneConverter` component of the `Magnum` package and +link to the `Magnum::AnySceneConverter` target: + +@code{.cmake} +find_package(Magnum REQUIRED AnySceneConverter) + +# ... +target_link_libraries(your-app PRIVATE Magnum::AnySceneConverter) +@endcode + +See @ref building, @ref cmake and @ref plugins for more information. +*/ +class MAGNUM_ANYSCENECONVERTER_EXPORT AnySceneConverter: public AbstractSceneConverter { + public: + /** @brief Constructor with access to plugin manager */ + explicit AnySceneConverter(PluginManager::Manager& manager); + + /** @brief Plugin manager constructor */ + explicit AnySceneConverter(PluginManager::AbstractManager& manager, const std::string& plugin); + + ~AnySceneConverter(); + + private: + MAGNUM_ANYSCENECONVERTER_LOCAL SceneConverterFeatures doFeatures() const override; + MAGNUM_ANYSCENECONVERTER_LOCAL bool doConvertToFile(const std::string& filename, const MeshData& mesh) override; +}; + +}} + +#endif diff --git a/src/MagnumPlugins/AnySceneConverter/CMakeLists.txt b/src/MagnumPlugins/AnySceneConverter/CMakeLists.txt new file mode 100644 index 000000000..43cc63262 --- /dev/null +++ b/src/MagnumPlugins/AnySceneConverter/CMakeLists.txt @@ -0,0 +1,68 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 +# 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. +# + +find_package(Corrade REQUIRED PluginManager) + +if(BUILD_PLUGINS_STATIC) + set(MAGNUM_ANYSCENECONVERTER_BUILD_STATIC 1) +endif() + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h) + +# AnySceneConverter plugin +add_plugin(AnySceneConverter + "${MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_BINARY_INSTALL_DIR};${MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_LIBRARY_INSTALL_DIR}" + "${MAGNUM_PLUGINS_SCENECONVERTER_RELEASE_BINARY_INSTALL_DIR};${MAGNUM_PLUGINS_SCENECONVERTER_RELEASE_LIBRARY_INSTALL_DIR}" + AnySceneConverter.conf + AnySceneConverter.cpp + AnySceneConverter.h) +if(BUILD_PLUGINS_STATIC AND BUILD_STATIC_PIC) + set_target_properties(AnySceneConverter PROPERTIES POSITION_INDEPENDENT_CODE ON) +endif() +target_link_libraries(AnySceneConverter PUBLIC MagnumTrade) +# Modify output location only if all are set, otherwise it makes no sense +if(CMAKE_RUNTIME_OUTPUT_DIRECTORY AND CMAKE_LIBRARY_OUTPUT_DIRECTORY AND CMAKE_ARCHIVE_OUTPUT_DIRECTORY) + set_target_properties(AnySceneConverter PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/magnum$<$:-d>/imageconverters + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/magnum$<$:-d>/imageconverters + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/magnum$<$:-d>/imageconverters) +endif() + +install(FILES AnySceneConverter.h ${CMAKE_CURRENT_BINARY_DIR}/configure.h + DESTINATION ${MAGNUM_PLUGINS_INCLUDE_INSTALL_DIR}/AnySceneConverter) + +# Automatic static plugin import +if(BUILD_PLUGINS_STATIC) + install(FILES importStaticPlugin.cpp DESTINATION ${MAGNUM_PLUGINS_INCLUDE_INSTALL_DIR}/AnySceneConverter) + target_sources(AnySceneConverter INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/importStaticPlugin.cpp) +endif() + +if(BUILD_TESTS) + add_subdirectory(Test) +endif() + +# Magnum AnySceneConverter target alias for superprojects +add_library(Magnum::AnySceneConverter ALIAS AnySceneConverter) diff --git a/src/MagnumPlugins/AnySceneConverter/Test/AnySceneConverterTest.cpp b/src/MagnumPlugins/AnySceneConverter/Test/AnySceneConverterTest.cpp new file mode 100644 index 000000000..d7698f7ab --- /dev/null +++ b/src/MagnumPlugins/AnySceneConverter/Test/AnySceneConverterTest.cpp @@ -0,0 +1,121 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 +#include + +#include "Magnum/Trade/AbstractSceneConverter.h" +#include "Magnum/Trade/MeshData.h" + +#include "configure.h" + +namespace Magnum { namespace Trade { namespace Test { namespace { + +struct AnySceneConverterTest: TestSuite::Tester { + explicit AnySceneConverterTest(); + + void load(); + void detect(); + + void unknown(); + + void propagateFlags(); + + /* Explicitly forbid system-wide plugin dependencies */ + PluginManager::Manager _manager{"nonexistent"}; +}; + +constexpr struct { + const char* name; + const char* filename; + const char* plugin; +} DetectData[]{ + {"BLOB", "huge.blob", "MagnumSceneConverter"} +}; + +AnySceneConverterTest::AnySceneConverterTest() { + addTests({&AnySceneConverterTest::load}); + + addInstancedTests({&AnySceneConverterTest::detect}, + Containers::arraySize(DetectData)); + + addTests({&AnySceneConverterTest::unknown, + + &AnySceneConverterTest::propagateFlags}); + + /* Load the plugin directly from the build tree. Otherwise it's static and + already loaded. */ + #ifdef ANYSCENECONVERTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT(_manager.load(ANYSCENECONVERTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif + + /* Create the output directory if it doesn't exist yet */ + CORRADE_INTERNAL_ASSERT(Utility::Directory::mkpath(ANYSCENECONVERTER_TEST_DIR)); +} + +void AnySceneConverterTest::load() { + CORRADE_SKIP("No scene converter plugin available to test."); +} + +void AnySceneConverterTest::detect() { + auto&& data = DetectData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer converter = _manager.instantiate("AnySceneConverter"); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter->convertToFile(data.filename, MeshData{MeshPrimitive::Triangles, 0})); + /* Can't use raw string literals in macros on GCC 4.8 */ + #ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT + CORRADE_COMPARE(out.str(), Utility::formatString( +"PluginManager::Manager::load(): plugin {0} is not static and was not found in nonexistent\nTrade::AnySceneConverter::convertToFile(): cannot load the {0} plugin\n", data.plugin)); + #else + CORRADE_COMPARE(out.str(), Utility::formatString( +"PluginManager::Manager::load(): plugin {0} was not found\nTrade::AnySceneConverter::convertToFile(): cannot load the {0} plugin\n", data.plugin)); + #endif +} + +void AnySceneConverterTest::unknown() { + std::ostringstream output; + Error redirectError{&output}; + + Containers::Pointer converter = _manager.instantiate("AnySceneConverter"); + CORRADE_VERIFY(!converter->convertToFile("mesh.obj", MeshData{MeshPrimitive::Triangles, 0})); + + CORRADE_COMPARE(output.str(), "Trade::AnySceneConverter::convertToFile(): cannot determine the format of mesh.obj\n"); +} + +void AnySceneConverterTest::propagateFlags() { + CORRADE_SKIP("No plugin with verbose output available to test."); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Trade::Test::AnySceneConverterTest) diff --git a/src/MagnumPlugins/AnySceneConverter/Test/CMakeLists.txt b/src/MagnumPlugins/AnySceneConverter/Test/CMakeLists.txt new file mode 100644 index 000000000..6f2b60d8b --- /dev/null +++ b/src/MagnumPlugins/AnySceneConverter/Test/CMakeLists.txt @@ -0,0 +1,71 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 +# 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. +# + +if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID) + set(ANYSCENECONVERTER_TEST_DIR "write") +else() + set(ANYSCENECONVERTER_TEST_DIR ${CMAKE_CURRENT_BINARY_DIR}) +endif() + +# CMake before 3.8 has broken $ expressions for iOS (see +# https://gitlab.kitware.com/cmake/cmake/merge_requests/404) and since Corrade +# doesn't support dynamic plugins on iOS, this sorta works around that. Should +# be revisited when updating Travis to newer Xcode (xcode7.3 has CMake 3.6). +if(NOT BUILD_PLUGINS_STATIC) + set(ANYSCENECONVERTER_PLUGIN_FILENAME $) + if(WITH_TGAIMAGECONVERTER) + set(TGAIMAGECONVERTER_PLUGIN_FILENAME $) + endif() +endif() + +# First replace ${} variables, then $<> generator expressions +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/configure.h + INPUT ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) + +corrade_add_test(AnySceneConverterTest AnySceneConverterTest.cpp + LIBRARIES MagnumTrade) +target_include_directories(AnySceneConverterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) +if(BUILD_PLUGINS_STATIC) + target_link_libraries(AnySceneConverterTest PRIVATE AnySceneConverter) + if(WITH_TGAIMAGECONVERTER) + target_link_libraries(AnySceneConverterTest PRIVATE TgaImageConverter) + endif() +else() + # So the plugins get properly built when building the test + add_dependencies(AnySceneConverterTest AnySceneConverter) + if(WITH_TGAIMAGECONVERTER) + add_dependencies(AnySceneConverterTest TgaImageConverter) + endif() +endif() +set_target_properties(AnySceneConverterTest PROPERTIES FOLDER "MagnumPlugins/AnySceneConverter/Test") +if(CORRADE_BUILD_STATIC AND NOT BUILD_PLUGINS_STATIC) + # CMake < 3.4 does this implicitly, but 3.4+ not anymore (see CMP0065). + # That's generally okay, *except if* the build is static, the executable + # uses a plugin manager and needs to share globals with the plugins (such + # as output redirection and so on). + set_target_properties(AnySceneConverterTest PROPERTIES ENABLE_EXPORTS ON) +endif() diff --git a/src/MagnumPlugins/AnySceneConverter/Test/configure.h.cmake b/src/MagnumPlugins/AnySceneConverter/Test/configure.h.cmake new file mode 100644 index 000000000..6bf97d62a --- /dev/null +++ b/src/MagnumPlugins/AnySceneConverter/Test/configure.h.cmake @@ -0,0 +1,27 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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. +*/ + +#cmakedefine ANYSCENECONVERTER_PLUGIN_FILENAME "${ANYSCENECONVERTER_PLUGIN_FILENAME}" +#define ANYSCENECONVERTER_TEST_DIR "${ANYSCENECONVERTER_TEST_DIR}" diff --git a/src/MagnumPlugins/AnySceneConverter/configure.h.cmake b/src/MagnumPlugins/AnySceneConverter/configure.h.cmake new file mode 100644 index 000000000..ddc3b3738 --- /dev/null +++ b/src/MagnumPlugins/AnySceneConverter/configure.h.cmake @@ -0,0 +1,26 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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. +*/ + +#cmakedefine MAGNUM_ANYSCENECONVERTER_BUILD_STATIC diff --git a/src/MagnumPlugins/AnySceneConverter/importStaticPlugin.cpp b/src/MagnumPlugins/AnySceneConverter/importStaticPlugin.cpp new file mode 100644 index 000000000..3ca3c865e --- /dev/null +++ b/src/MagnumPlugins/AnySceneConverter/importStaticPlugin.cpp @@ -0,0 +1,35 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 "MagnumPlugins/AnySceneConverter/configure.h" + +#ifdef MAGNUM_ANYSCENECONVERTER_BUILD_STATIC +#include + +static int magnumAnySceneConverterStaticImporter() { + CORRADE_PLUGIN_IMPORT(AnySceneConverter) + return 1; +} CORRADE_AUTOMATIC_INITIALIZER(magnumAnySceneConverterStaticImporter) +#endif diff --git a/src/MagnumPlugins/CMakeLists.txt b/src/MagnumPlugins/CMakeLists.txt index 55641e253..43d5aea35 100644 --- a/src/MagnumPlugins/CMakeLists.txt +++ b/src/MagnumPlugins/CMakeLists.txt @@ -46,6 +46,10 @@ if(WITH_ANYIMAGECONVERTER) add_subdirectory(AnyImageConverter) endif() +if(WITH_ANYSCENECONVERTER) + add_subdirectory(AnySceneConverter) +endif() + if(WITH_ANYSCENEIMPORTER) add_subdirectory(AnySceneImporter) endif() From 778d407bcdc07daa17fe44124423d5533181b4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 17 Apr 2020 20:17:22 +0200 Subject: [PATCH 07/16] AnySceneImporter: recognize Magnum's blob files. --- src/MagnumPlugins/AnySceneImporter/AnySceneImporter.cpp | 2 ++ src/MagnumPlugins/AnySceneImporter/AnySceneImporter.h | 2 ++ .../AnySceneImporter/Test/AnySceneImporterTest.cpp | 1 + 3 files changed, 5 insertions(+) diff --git a/src/MagnumPlugins/AnySceneImporter/AnySceneImporter.cpp b/src/MagnumPlugins/AnySceneImporter/AnySceneImporter.cpp index a6c6859c7..8580a5b25 100644 --- a/src/MagnumPlugins/AnySceneImporter/AnySceneImporter.cpp +++ b/src/MagnumPlugins/AnySceneImporter/AnySceneImporter.cpp @@ -80,6 +80,8 @@ void AnySceneImporter::doOpenFile(const std::string& filename) { plugin = "Ac3dImporter"; else if(Utility::String::endsWith(normalized, ".blend")) plugin = "BlenderImporter"; + else if(Utility::String::endsWith(normalized, ".blob")) + plugin = "MagnumImporter"; else if(Utility::String::endsWith(normalized, ".bvh")) plugin = "BvhImporter"; else if(Utility::String::endsWith(normalized, ".csm")) diff --git a/src/MagnumPlugins/AnySceneImporter/AnySceneImporter.h b/src/MagnumPlugins/AnySceneImporter/AnySceneImporter.h index 9b37b9499..dfb0284f9 100644 --- a/src/MagnumPlugins/AnySceneImporter/AnySceneImporter.h +++ b/src/MagnumPlugins/AnySceneImporter/AnySceneImporter.h @@ -61,6 +61,8 @@ tries to open the file with it. Supported formats: - AC3D (`*.ac`), loaded with any plugin that provides `Ac3dImporter` - Blender 3D (`*.blend`), loaded with any plugin that provides `BlenderImporter` +- @ref blob "Magnum's memory-mappable serialization format" (`*.blob`), + loaded with @ref MagnumImporter - Biovision BVH (`*.bvh`), loaded with any plugin that provides `BvhImporter` - CharacterStudio Motion (`*.csm`), loaded with any plugin that provides `CsmImporter` diff --git a/src/MagnumPlugins/AnySceneImporter/Test/AnySceneImporterTest.cpp b/src/MagnumPlugins/AnySceneImporter/Test/AnySceneImporterTest.cpp index 9ab10472f..6b930f1bf 100644 --- a/src/MagnumPlugins/AnySceneImporter/Test/AnySceneImporterTest.cpp +++ b/src/MagnumPlugins/AnySceneImporter/Test/AnySceneImporterTest.cpp @@ -75,6 +75,7 @@ constexpr struct { const char* plugin; } DetectData[]{ {"Blender", "suzanne.blend", "BlenderImporter"}, + {"BLOB", "messy.blob", "MagnumImporter"}, {"COLLADA", "xml.dae", "ColladaImporter"}, {"FBX", "autodesk.fbx", "FbxImporter"}, {"glTF", "khronos.gltf", "GltfImporter"}, From 7aa9a9b23c51c83e8a886250e76533cded437cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 23 Apr 2020 20:21:39 +0200 Subject: [PATCH 08/16] sceneconverter: add a --profile option. --- src/Magnum/Trade/sceneconverter.cpp | 49 +++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Magnum/Trade/sceneconverter.cpp b/src/Magnum/Trade/sceneconverter.cpp index b380e6f7d..0d461d03a 100644 --- a/src/Magnum/Trade/sceneconverter.cpp +++ b/src/Magnum/Trade/sceneconverter.cpp @@ -24,6 +24,7 @@ */ #include +#include #include #include #include @@ -63,8 +64,8 @@ information. @code{.sh} magnum-sceneconverter [-h|--help] [--importer IMPORTER] [--plugin-dir DIR] - [-i|--importer-options key=val,key2=val2,…] [--info] [-v|--verbose] [--] - input + [-i|--importer-options key=val,key2=val2,…] [--info] [-v|--verbose] + [--profile] [--] input @endcode Arguments: @@ -78,6 +79,7 @@ Arguments: pass to the importer - `--info` --- print info about the input file and exit - `-v`, `--verbose` --- verbose output from importer plugins +- `--profile` --- measure import time If `--info` is given, the utility will print information about all meshes and images present in the file. **This option is currently mandatory.** @@ -92,6 +94,22 @@ character is omitted, it's equivalent to saying `key=true`. using namespace Magnum; +namespace { + +struct Duration { + explicit Duration(std::chrono::high_resolution_clock::duration& output): _output(output), _t{std::chrono::high_resolution_clock::now()} {} + + ~Duration() { + _output += std::chrono::high_resolution_clock::now() - _t; + } + + private: + std::chrono::high_resolution_clock::duration& _output; + std::chrono::high_resolution_clock::time_point _t; +}; + +} + int main(int argc, char** argv) { Utility::Arguments args; args.addArgument("input").setHelp("input", "input file") @@ -100,6 +118,7 @@ int main(int argc, char** argv) { .addOption('i', "importer-options").setHelp("importer-options", "configuration options to pass to the importer", "key=val,key2=val2,…") .addBooleanOption("info").setHelp("info", "print info about the input file and exit") .addBooleanOption('v', "verbose").setHelp("verbose", "verbose output from importer plugins") + .addBooleanOption("profile").setHelp("profile", "measure import time") /** @todo add the parse error callback from imageconverter once there's an output argument, also remove the "mandatory" from all docs */ .setGlobalHelp(R"(Converts scenes of different formats. @@ -126,14 +145,19 @@ is omitted, it's equivalent to saying key=true.)") if(args.isSet("verbose")) importer->setFlags(Trade::ImporterFlag::Verbose); Trade::Implementation::setOptions(*importer, args.value("importer-options")); - /* Print file info, if requested */ - if(args.isSet("info")) { - /* Open the file, but don't fail when an image can't be opened */ + std::chrono::high_resolution_clock::duration importTime; + + /* Open the file */ + { + Duration d{importTime}; if(!importer->openFile(args.value("input"))) { Error() << "Cannot open file" << args.value("input"); return 3; } + } + /* Print file info, if requested */ + if(args.isSet("info")) { if(!importer->meshCount() && !importer->image1DCount() && !importer->image2DCount() && !importer->image2DCount()) { Debug{} << "No meshes or images found."; return 0; @@ -162,10 +186,13 @@ is omitted, it's equivalent to saying key=true.)") Containers::Array meshInfos; for(UnsignedInt i = 0; i != importer->meshCount(); ++i) { for(UnsignedInt j = 0; j != importer->meshLevelCount(i); ++j) { - Containers::Optional mesh = importer->mesh(i, j); - if(!mesh) { - error = true; - continue; + Containers::Optional mesh; + { + Duration d{importTime}; + if(!(mesh = importer->mesh(i, j))) { + error = true; + continue; + } } MeshInfo info{}; @@ -248,6 +275,10 @@ is omitted, it's equivalent to saying key=true.)") else d << Math::Vector<1, Int>(info.size.x()); } + if(args.isSet("profile")) { + Debug{} << "Import took" << UnsignedInt(std::chrono::duration_cast(importTime).count())/1.0e3f << "seconds"; + } + return error ? 1 : 0; } From 4b2534bea5f8e8b08c5d7bdf3fcc39146a620d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Mon, 6 Apr 2020 20:32:26 +0200 Subject: [PATCH 09/16] sceneconverter: hook scene converter plugins into this thing. --- src/Magnum/Trade/sceneconverter.cpp | 89 +++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/src/Magnum/Trade/sceneconverter.cpp b/src/Magnum/Trade/sceneconverter.cpp index 0d461d03a..9a7b484d6 100644 --- a/src/Magnum/Trade/sceneconverter.cpp +++ b/src/Magnum/Trade/sceneconverter.cpp @@ -35,6 +35,7 @@ #include "Magnum/PixelFormat.h" #include "Magnum/Trade/AbstractImporter.h" #include "Magnum/Trade/MeshData.h" +#include "Magnum/Trade/AbstractSceneConverter.h" #include "Magnum/Trade/Implementation/converterUtilities.h" namespace Magnum { @@ -63,33 +64,41 @@ information. @section magnum-sceneconverter-usage Usage @code{.sh} -magnum-sceneconverter [-h|--help] [--importer IMPORTER] [--plugin-dir DIR] - [-i|--importer-options key=val,key2=val2,…] [--info] [-v|--verbose] - [--profile] [--] input +magnum-sceneconverter [-h|--help] [--importer IMPORTER] [--converter CONVERTER] + [--plugin-dir DIR] [-i|--importer-options key=val,key2=val2,…] + [-c|--converter-options key=val,key2=val2,…] [--info] [-v|--verbose] + [--profile] [--] input output @endcode Arguments: - `input` --- input file +- `output` --- output file - `-h`, `--help` --- display this help message and exit - `--importer IMPORTER` --- scene importer plugin (default: @ref Trade::AnySceneImporter "AnySceneImporter") +- `--converter CONVERTER` --- scene converter plugin (default: + @ref Trade::AnyImageConverter "AnySceneConverter") - `--plugin-dir DIR` --- override base plugin dir - `-i`, `--importer-options key=val,key2=val2,…` --- configuration options to pass to the importer +- `-c`, `--converter-options key=val,key2=val2,…` --- configuration options + to pass to the converter - `--info` --- print info about the input file and exit - `-v`, `--verbose` --- verbose output from importer plugins -- `--profile` --- measure import time +- `--profile` --- measure import and conversion time If `--info` is given, the utility will print information about all meshes and images present in the file. **This option is currently mandatory.** -The `-i` / `--importer-options` argument accepts a comma-separated list of -key/value pairs to set in the importer plugin configuration. If the `=` -character is omitted, it's equivalent to saying `key=true`. +The `-i` / `--importer-options` and `-c` / `--converter-options` arguments +accept a comma-separated list of key/value pairs to set in the importer / +converter plugin configuration. If the `=` character is omitted, it's +equivalent to saying `key=true`. @see @ref magnum-imageconverter */ + } using namespace Magnum; @@ -113,22 +122,32 @@ struct Duration { int main(int argc, char** argv) { Utility::Arguments args; args.addArgument("input").setHelp("input", "input file") + .addArgument("output").setHelp("output", "output file") .addOption("importer", "AnySceneImporter").setHelp("importer", "scene importer plugin") + .addOption("converter", "AnySceneConverter").setHelp("converter", "scene converter plugin") .addOption("plugin-dir").setHelp("plugin-dir", "override base plugin dir", "DIR") .addOption('i', "importer-options").setHelp("importer-options", "configuration options to pass to the importer", "key=val,key2=val2,…") + .addOption('c', "converter-options").setHelp("converter-options", "configuration options to pass to the converter", "key=val,key2=val2,…") .addBooleanOption("info").setHelp("info", "print info about the input file and exit") - .addBooleanOption('v', "verbose").setHelp("verbose", "verbose output from importer plugins") - .addBooleanOption("profile").setHelp("profile", "measure import time") - /** @todo add the parse error callback from imageconverter once there's - an output argument, also remove the "mandatory" from all docs */ + .addBooleanOption('v', "verbose").setHelp("verbose", "verbose output from importer and converter plugins") + .addBooleanOption("profile").setHelp("profile", "measure import and conversion time") + .setParseErrorCallback([](const Utility::Arguments& args, Utility::Arguments::ParseError error, const std::string& key) { + /* If --info is passed, we don't need the output argument */ + if(error == Utility::Arguments::ParseError::MissingArgument && + key == "output" && args.isSet("info")) return true; + + /* Handle all other errors as usual */ + return false; + }) .setGlobalHelp(R"(Converts scenes of different formats. If --info is given, the utility will print information about all meshes and -images present in the file. This option is currently mandatory. +images present in the file. -The -i / --importer-options argument accepts a comma-separated list of -key/value pairs to set in the importer plugin configuration. If the = character -is omitted, it's equivalent to saying key=true.)") +The -i / --importer-options and -c / --converter-options arguments accept a +comma-separated list of key/value pairs to set in the importer / converter +plugin configuration. If the = character is omitted, it's equivalent to saying +key=true.)") .parse(argc, argv); PluginManager::Manager importerManager{ @@ -282,6 +301,42 @@ is omitted, it's equivalent to saying key=true.)") return error ? 1 : 0; } - Error{} << "Sorry, only the --info option is currently implemented"; - return 6; + Containers::Optional mesh; + { + Duration d{importTime}; + if(!importer->meshCount() || !(mesh = importer->mesh(0))) { + Error{} << "Cannot import mesh 0"; + return 4; + } + } + + /* Load converter plugin */ + PluginManager::Manager converterManager{ + args.value("plugin-dir").empty() ? std::string{} : + Utility::Directory::join(args.value("plugin-dir"), Trade::AbstractSceneConverter::pluginSearchPaths()[0])}; + Containers::Pointer converter = converterManager.loadAndInstantiate(args.value("converter")); + if(!converter) { + Debug{} << "Available converter plugins:" << Utility::String::join(converterManager.aliasList(), ", "); + return 2; + } + + /* Set options, if passed */ + if(args.isSet("verbose")) converter->setFlags(Trade::SceneConverterFlag::Verbose); + Trade::Implementation::setOptions(*converter, args.value("converter-options")); + + std::chrono::high_resolution_clock::duration conversionTime; + + /* Save output file */ + { + Duration d{conversionTime}; + if(!converter->convertToFile(args.value("output"), *mesh)) { + Error{} << "Cannot save file" << args.value("output"); + return 5; + } + } + + if(args.isSet("profile")) { + Debug{} << "Import took" << UnsignedInt(std::chrono::duration_cast(importTime).count())/1.0e3f << "seconds, conversion" + << UnsignedInt(std::chrono::duration_cast(conversionTime).count())/1.0e3f << "seconds"; + } } From 9edd90d8525f526e2745acb0da8e9132720c958e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 22 Apr 2020 16:07:35 +0200 Subject: [PATCH 10/16] imageconverter,sceneconverter: support option subgroups as well. --- .../Trade/Implementation/converterUtilities.h | 20 ++++++++++++++++--- src/Magnum/Trade/imageconverter.cpp | 5 +++-- src/Magnum/Trade/sceneconverter.cpp | 5 +++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Magnum/Trade/Implementation/converterUtilities.h b/src/Magnum/Trade/Implementation/converterUtilities.h index 4d617874a..387b0f148 100644 --- a/src/Magnum/Trade/Implementation/converterUtilities.h +++ b/src/Magnum/Trade/Implementation/converterUtilities.h @@ -46,20 +46,34 @@ void setOptions(PluginManager::AbstractPlugin& plugin, const std::string& option Utility::String::trimInPlace(keyValue[0]); Utility::String::trimInPlace(keyValue[2]); + std::vector keyParts = Utility::String::split(keyValue[0], '/'); + CORRADE_INTERNAL_ASSERT(!keyParts.empty()); + Utility::ConfigurationGroup* group = &plugin.configuration(); + bool groupNotRecognized = false; + for(std::size_t i = 0; i != keyParts.size() - 1; ++i) { + Utility::ConfigurationGroup* subgroup = group->group(keyParts[i]); + if(!subgroup) { + groupNotRecognized = true; + subgroup = group->addGroup(keyParts[i]); + } + group = subgroup; + } + /* Provide a warning message in case the plugin doesn't define given option in its default config. The plugin is not *required* to have those tho (could be backward compatibility entries, for example), so not an error. */ - if(!plugin.configuration().valueCount(keyValue[0])) + if(groupNotRecognized || !group->hasValue(keyParts.back())) { Warning{} << "Option" << keyValue[0] << "not recognized by" << plugin.plugin(); + } /* If the option doesn't have an =, treat it as a boolean flag that's set to true. While there's no similar way to do an inverse, it's still nicer than causing a fatal error with those. */ if(keyValue[1].empty()) - plugin.configuration().setValue(keyValue[0], true); + group->setValue(keyParts.back(), true); else - plugin.configuration().setValue(keyValue[0], keyValue[2]); + group->setValue(keyParts.back(), keyValue[2]); } } diff --git a/src/Magnum/Trade/imageconverter.cpp b/src/Magnum/Trade/imageconverter.cpp index df3ec9d1f..73b067332 100644 --- a/src/Magnum/Trade/imageconverter.cpp +++ b/src/Magnum/Trade/imageconverter.cpp @@ -99,7 +99,8 @@ need to be specified. The `-i` / `--importer-options` and `-c` / `--converter-options` arguments accept a comma-separated list of key/value pairs to set in the importer / converter plugin configuration. If the `=` character is omitted, it's -equivalent to saying `key=true`. +equivalent to saying `key=true`; configuration subgroups are delimited with +`/`. @section magnum-imageconverter-example Example usage @@ -168,7 +169,7 @@ be specified. The -i / --importer-options and -c / --converter-options arguments accept a comma-separated list of key/value pairs to set in the importer / converter plugin configuration. If the = character is omitted, it's equivalent to saying -key=true.)") +key=true; configuration subgroups are delimited with /.)") .parse(argc, argv); PluginManager::Manager importerManager{ diff --git a/src/Magnum/Trade/sceneconverter.cpp b/src/Magnum/Trade/sceneconverter.cpp index 9a7b484d6..3e81983c3 100644 --- a/src/Magnum/Trade/sceneconverter.cpp +++ b/src/Magnum/Trade/sceneconverter.cpp @@ -94,7 +94,8 @@ and images present in the file. **This option is currently mandatory.** The `-i` / `--importer-options` and `-c` / `--converter-options` arguments accept a comma-separated list of key/value pairs to set in the importer / converter plugin configuration. If the `=` character is omitted, it's -equivalent to saying `key=true`. +equivalent to saying `key=true`; configuration subgroups are delimited with +`/`. @see @ref magnum-imageconverter */ @@ -147,7 +148,7 @@ images present in the file. The -i / --importer-options and -c / --converter-options arguments accept a comma-separated list of key/value pairs to set in the importer / converter plugin configuration. If the = character is omitted, it's equivalent to saying -key=true.)") +key=true; configuration subgroups are delimited with /.)") .parse(argc, argv); PluginManager::Manager importerManager{ From 471cef5095ad577c6ea1218e53cabe539fa6f9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 14 Feb 2020 11:37:10 +0100 Subject: [PATCH 11/16] Trade: base structures for memory-mappable serialization format. --- src/Magnum/Trade/CMakeLists.txt | 2 +- src/Magnum/Trade/Data.cpp | 94 ++++++++++++ src/Magnum/Trade/Data.h | 188 ++++++++++++++++++++++- src/Magnum/Trade/Test/CMakeLists.txt | 2 +- src/Magnum/Trade/Test/DataTest.cpp | 214 ++++++++++++++++++++++++++- src/Magnum/Trade/Trade.h | 4 + 6 files changed, 500 insertions(+), 4 deletions(-) diff --git a/src/Magnum/Trade/CMakeLists.txt b/src/Magnum/Trade/CMakeLists.txt index 27c4ac30c..e7eb5bfb1 100644 --- a/src/Magnum/Trade/CMakeLists.txt +++ b/src/Magnum/Trade/CMakeLists.txt @@ -28,7 +28,6 @@ find_package(Corrade REQUIRED PluginManager) set(MagnumTrade_SRCS AbstractMaterialData.cpp ArrayAllocator.cpp - Data.cpp LightData.cpp MeshObjectData2D.cpp MeshObjectData3D.cpp @@ -41,6 +40,7 @@ set(MagnumTrade_GracefulAssert_SRCS AbstractSceneConverter.cpp AnimationData.cpp CameraData.cpp + Data.cpp ImageData.cpp MeshData.cpp ObjectData2D.cpp diff --git a/src/Magnum/Trade/Data.cpp b/src/Magnum/Trade/Data.cpp index b7efc41c8..4f24953c4 100644 --- a/src/Magnum/Trade/Data.cpp +++ b/src/Magnum/Trade/Data.cpp @@ -25,10 +25,17 @@ #include "Data.h" +#include +#include #include namespace Magnum { namespace Trade { +static_assert(sizeof(DataChunkHeader) == (sizeof(void*) == 4 ? 20 : 24), + "DataChunkHeader has unexpected size"); +static_assert(alignof(DataChunkHeader) == sizeof(std::size_t), + "DataChunkHeader has unexpected alignment"); + Debug& operator<<(Debug& debug, const DataFlag value) { debug << "Trade::DataFlag" << Debug::nospace; @@ -50,6 +57,93 @@ Debug& operator<<(Debug& debug, const DataFlags value) { DataFlag::Mutable}); } +namespace { + Debug& printFourCC(Debug& debug, UnsignedInt value, const char* name) { + debug << name << Debug::nospace; + + for(std::size_t i = 0; i != 4; ++i) { + if(i) debug << Debug::nospace << ","; + + const int c = value & 255; + if(std::isprint(c)) { + const char data[] = {'\'', char(c), '\'', '\0'}; + debug << data; + } else { + debug << reinterpret_cast(c); + } + + value >>= 8; + } + + return debug << Debug::nospace << ")"; + } +} + +Debug& operator<<(Debug& debug, const DataChunkType value) { + return printFourCC(debug, Containers::enumCastUnderlyingType(value), "Trade::DataChunkType("); +} + +Debug& operator<<(Debug& debug, const DataChunkSignature value) { + return printFourCC(debug, Containers::enumCastUnderlyingType(value), "Trade::DataChunkSignature("); +} + +namespace { + constexpr DataChunkHeader DataChunkHeaderPrefix{ + 128, {'\x0a'}, {'\x0d', '\x0a'}, DataChunkSignature::Current, 0, 0, + /* Type and size isn't checked when validating and gets overwritten + when serializing */ + DataChunkType{}, 0 + }; + + static_assert(DataChunkHeaderPrefix.version & 0x80, + "version needs the high bit set to prevent detection as a text file"); +} + +bool isDataChunk(Containers::ArrayView data) { + return data && data.size() >= sizeof(DataChunkHeader) && + std::memcmp(data.data(), &DataChunkHeaderPrefix, 10) == 0 && + reinterpret_cast(data.data())->size <= data.size(); +} + +const DataChunkHeader* dataChunkHeaderDeserialize(const Containers::ArrayView data) { + if(data.size() < sizeof(DataChunkHeader)) { + Error{} << "Trade::dataChunkHeaderDeserialize(): expected at least" << sizeof(DataChunkHeader) << "bytes for a header but got" << data.size(); + return nullptr; + } + + const auto& header = *reinterpret_cast(data.data()); + if(header.version != DataChunkHeaderPrefix.version) { + Error{} << "Trade::dataChunkHeaderDeserialize(): expected version" << DataChunkHeaderPrefix.version << "but got" << header.version; + return nullptr; + } + if(header.signature != DataChunkSignature::Current) { + Error{} << "Trade::dataChunkHeaderDeserialize(): expected signature" << DataChunkSignature::Current << "but got" << header.signature; + return nullptr; + } + if(std::memcmp(data.data(), &DataChunkHeaderPrefix, 10) != 0) { + Error{} << "Trade::dataChunkHeaderDeserialize(): invalid header check bytes"; + return nullptr; + } + if(header.size > data.size()) { + Error{} << "Trade::dataChunkHeaderDeserialize(): expected at least" << header.size << "bytes but got" << data.size(); + return nullptr; + } + + return reinterpret_cast(data.data()); +} + +std::size_t dataChunkHeaderSerializeInto(const Containers::ArrayView out, const DataChunkType type, const UnsignedShort typeVersion) { + CORRADE_ASSERT(out.size() >= sizeof(DataChunkHeader), + "Trade::dataChunkHeaderSerializeInto(): data too small, expected at least" << sizeof(DataChunkHeader) << "bytes but got" << out.size(), {}); + + auto& header = *reinterpret_cast(out.data()); + header = DataChunkHeaderPrefix; + header.typeVersion = typeVersion; + header.type = type; + header.size = out.size(); + return sizeof(DataChunkHeader); +} + namespace Implementation { void nonOwnedArrayDeleter(char*, std::size_t) { /* does nothing */ } } diff --git a/src/Magnum/Trade/Data.h b/src/Magnum/Trade/Data.h index 4e1684ffa..b43726dea 100644 --- a/src/Magnum/Trade/Data.h +++ b/src/Magnum/Trade/Data.h @@ -26,11 +26,13 @@ */ /** @file - * @brief Enum @ref Magnum::Trade::DataFlag, enum set @ref Magnum::Trade::DataFlags + * @brief Struct @ref Magnum::Trade::DataChunkHeader, enum @ref Magnum::Trade::DataFlag, @ref Magnum::Trade::DataChunkSignature, @ref Magnum::Trade::DataChunkType, enum set @ref Magnum::Trade::DataFlags, function @ref Magnum::Trade::isDataChunk(), @ref Magnum::Trade::dataChunkHeaderDeserialize(), @ref Magnum::Trade::dataChunkHeaderSerializeInto() * @m_since_latest */ +#include #include +#include #include "Magnum/Magnum.h" #include "Magnum/Trade/visibility.h" @@ -85,6 +87,190 @@ CORRADE_ENUMSET_OPERATORS(DataFlags) */ MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, DataFlags value); +/** +@brief Memory-mappable data chunk type +@m_since_latest + +A [FourCC](https://en.wikipedia.org/wiki/FourCC)-like identifier of the data +contained in the chunk. Used together with @ref DataChunkHeader::typeVersion to +identify version of a particular chunk type. + +@section Trade-DataChunkType-custom Custom data chunk types + +All identifiers starting with an uppercase leter are reserved for Magnum +itself, custom application-specific data types should use a lowercase first +letter instead. Casing of the three remaining characters doesn't have any +specified effect in the current version of the header. It doesn't need to be +alphanumeric either, but for additional versioning of a particular chunk type +it's recommended to use @ref DataChunkHeader::typeVersion, keeping the chunk +type FourCC clearly recognizable. +*/ +enum class DataChunkType: UnsignedInt { + /** + * Serialized @ref MeshData. The letters `Mesh`. + * + * Current version is @cpp 0 @ce. + */ + Mesh = Utility::Endianness::fourCC('M', 'e', 's', 'h'), + + #if 0 + /* None of these used yet, here just to lay out the naming scheme */ + Animation = Utility::Endianness::fourCC('A', 'n', 'i', 'm'), + Camera = Utility::Endianness::fourCC('C', 'a', 'm', 0), + Image1D = Utility::Endianness::fourCC('I', 'm', 'g', '1'), + Image2D = Utility::Endianness::fourCC('I', 'm', 'g', '2'), + Image3D = Utility::Endianness::fourCC('I', 'm', 'g', '3'), + Light = Utility::Endianness::fourCC('L', 'i', 'g', 't'), + Material = Utility::Endianness::fourCC('M', 't', 'l', 0), + Scene = Utility::Endianness::fourCC('S', 'c', 'n', 0), + Texture = Utility::Endianness::fourCC('T', 'e', 'x', 0) + #endif +}; + +/** +@debugoperatorenum{DataChunkType} +@m_since_latest +*/ +MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, DataChunkType value); + +/** +@brief Memory-mappable data chunk signature +@m_since_latest + +Reads as `BLOB` letters for a Little-Endian 64 bit data chunk. For Big-Endian +the order is reversed (thus `BOLB`), 32-bit data have the `L` letter lowercase. +@see @ref DataChunkHeader::signature +*/ +enum class DataChunkSignature: UnsignedInt { + /** Little-Endian 32-bit data. The letters `BlOB`. */ + Little32 = Utility::Endianness::fourCC('B', 'l', 'O', 'B'), + + /** Little-Endian 64-bit data. The letters `BLOB`. */ + Little64 = Utility::Endianness::fourCC('B', 'L', 'O', 'B'), + + /** Big-Endian 32-bit data. The letters `BOlB`. */ + Big32 = Utility::Endianness::fourCC('B', 'O', 'l', 'B'), + + /** Big-Endian 64-bit data. The letters `BOLB`. */ + Big64 = Utility::Endianness::fourCC('B', 'O', 'L', 'B'), + + /** Signature matching this platform. Alias to one of the above. */ + Current + #ifndef DOXYGEN_GENERATING_OUTPUT + = + #ifndef CORRADE_TARGET_BIG_ENDIAN + sizeof(std::size_t) == 8 ? Little64 : Little32 + #else + sizeof(std::size_t) == 8 ? Big64 : Big32 + #endif + #endif +}; + +/** +@debugoperatorenum{DataChunkSignature} +@m_since_latest +*/ +MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, DataChunkSignature value); + +/** +@brief Header for memory-mappable data chunks +@m_since_latest + +Since the goal of the serialization format is to be a direct equivalent to the +in-memory data layout, there's four different variants of the header based on +whether it's running on a 32-bit or 64-bit system and whether the machine is +Little- or Big-Endian. A 64-bit variant of the header has 24 bytes to support +data larger than 4 GB, the 32-bit variant is 20 bytes. Apart from the @ref size +member, the header is designed to contain the same amount of information on +both, and its size is chosen so the immediately following data can be aligned +to either 4 or 8 bytes without needing to add extra padding. + +The header contents are as follows, vaguely inspired by the +[PNG file header](https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header). +All fields except @ref typeVersion and @ref size (marked with +@m_class{m-label m-primary} **E**) are stored in an endian-independent way, +otherwise the endian matches the signature field. + +@m_class{m-row m-container-inflate} + +@parblock + +@m_class{m-fullwidth} + +Byte offset | Byte size      | Member | Contents +----------- | --------- | ------------- | ------------------------------------- +0 | 1 | @ref DataChunkHeader::version "version" | Header version. Has the high bit set to prevent the file from being detected as text. Currently set to @cpp 128 @ce. +1 | 1 | @ref eolUnix | Unix EOL (LF, @cpp '\x0a' @ce), to detect unwanted Unix-to-DOS line ending conversion +2 | 2 | @ref eolDos | DOS EOL (CR+LF, @cpp '\x0d', '\x0a' @ce), to detect unwanted DOS-to-Unix line ending conversion +4 | 4 | @ref signature | File signature. Differs based on bitness and endianness, see @ref DataChunkSignature for more information. +8 | 2 | @ref zero | Two zero bytes (@cpp '\x00', '\x00' @ce), to prevent the data from being treated and copied as a null-terminated (wide) string. +10 | 2 @m_class{m-label m-primary} **E** | @ref typeVersion | Data chunk type version. Use is chunk-specific, see @ref DataChunkType for more information. +12 | 4 | @ref type | Data chunk type, see @ref DataChunkType for more information +16 | 4 or 8 @m_class{m-label m-primary} **E** | @ref size | Data chunk size, including the header size. Stored in size matching the signature field. + +@endparblock + +For a particular header variant the first 10 bytes is static and thus can be +used for file validation. After the header are directly the chunk data. For performance reasons it's recommended to have the data padded to be a multiple +of 4 or 8 bytes to ensure the immediately following chunk is correctly aligned +as well, but it's not a strict recommendation and not enforced in any way in +current version of the format. + +Current version of the header doesn't have any checksum field in order to make +it easy to modify the data in-place, this might change in the future. +@see @ref DataChunkSignature, @ref DataChunkType, @ref isDataChunk(), + @ref dataChunkHeaderDeserialize(), @ref dataChunkHeaderSerializeInto() +*/ +struct DataChunkHeader { + UnsignedByte version; /**< @brief Header version */ + char eolUnix[1]; /**< @brief Unix EOL */ + char eolDos[2]; /**< @brief Dos EOL */ + DataChunkSignature signature; /**< @brief Signature */ + UnsignedShort zero; /**< @brief Two zero bytes */ + UnsignedShort typeVersion; /**< @brief Chunk type version */ + DataChunkType type; /**< @brief Chunk type */ + std::size_t size; /**< @brief Chunk size */ +}; + +/** +@brief Check if given data blob is a valid data chunk +@m_since_latest + +Returns @cpp true @ce if @p data is a valid @ref DataChunkHeader, matches +current platform and @p data is large enough to contain the whole chunk, +@cpp false @ce otherwise. The function doesn't print any diagnostic messages on +validation failure, use @ref dataChunkHeaderDeserialize() instead if you need +to know why. +*/ +MAGNUM_TRADE_EXPORT bool isDataChunk(Containers::ArrayView data); + +/** +@brief Try to deserialize a data chunk from a memory-mappable representation +@m_since_latest + +Checks that @p data is large enough to contain a valid data chunk, validates +the header and then returns @p data reinterpreted as a @ref DataChunkHeader +pointer. On failure prints an error message and returns @cpp nullptr @ce. +@see @ref isDataChunk(), @ref dataChunkHeaderSerializeInto() +*/ +MAGNUM_TRADE_EXPORT const DataChunkHeader* dataChunkHeaderDeserialize(Containers::ArrayView data); + +/** +@brief Serialize a data chunk header into existing array +@param[out] out Where to write the output +@param[out] type Data chunk type +@param[out] typeVersion Data chunk type version +@return Number of bytes written. Same as size of @ref DataChunkHeader. +@m_since_latest + +Expects that @p data is at least the size of @ref DataChunkHeader. Fills in +@ref DataChunkHeader::typeVersion and @ref DataChunkHeader::type with passed +values used in constructor, and @ref DataChunkHeader::size with @p data size. + +@see @ref dataChunkHeaderDeserialize() +*/ +MAGNUM_TRADE_EXPORT std::size_t dataChunkHeaderSerializeInto(Containers::ArrayView out, DataChunkType type, UnsignedShort typeVersion); + namespace Implementation { /* Used internally by MeshData */ MAGNUM_TRADE_EXPORT void nonOwnedArrayDeleter(char*, std::size_t); diff --git a/src/Magnum/Trade/Test/CMakeLists.txt b/src/Magnum/Trade/Test/CMakeLists.txt index 244b018be..b05842bd8 100644 --- a/src/Magnum/Trade/Test/CMakeLists.txt +++ b/src/Magnum/Trade/Test/CMakeLists.txt @@ -48,7 +48,7 @@ target_include_directories(TradeAbstractSceneConverterTest PRIVATE ${CMAKE_CURRE corrade_add_test(TradeAnimationDataTest AnimationDataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeCameraDataTest CameraDataTest.cpp LIBRARIES MagnumTradeTestLib) -corrade_add_test(TradeDataTest DataTest.cpp LIBRARIES MagnumTrade) +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) diff --git a/src/Magnum/Trade/Test/DataTest.cpp b/src/Magnum/Trade/Test/DataTest.cpp index e1253d498..7810eba00 100644 --- a/src/Magnum/Trade/Test/DataTest.cpp +++ b/src/Magnum/Trade/Test/DataTest.cpp @@ -24,8 +24,12 @@ */ #include +#include #include +#include +#include #include +#include #include "Magnum/Trade/Data.h" @@ -34,13 +38,207 @@ namespace Magnum { namespace Trade { namespace Test { namespace { struct DataTest: TestSuite::Tester { explicit DataTest(); + void dataChunkHeaderDeserialize(); + void dataChunkHeaderDeserializeInvalid(); + + void dataChunkHeaderSerialize(); + void dataChunkHeaderSerializeTooShort(); + void debugDataFlag(); void debugDataFlags(); + + void debugDataChunkType(); + void debugDataChunkSignature(); +}; + +constexpr char Data32[]{ + '\x80', '\x0a', '\x0d', '\x0a', 'B', + #ifndef CORRADE_TARGET_BIG_ENDIAN + 'l', 'O', + #else + 'O', 'l', + #endif + 'B', 0, 0, + #ifndef CORRADE_TARGET_BIG_ENDIAN + 42, 0, + #else + 0, 42, + #endif + 'W', 'a', 'v', 'e', + #ifndef CORRADE_TARGET_BIG_ENDIAN + 20 + 5, 0, 0, 0, + #else + 0, 0, 0, 20 + 5, + #endif + + 'h', 'e', 'l', 'l', 'o' +}; + +constexpr char Data64[]{ + '\x80', '\x0a', '\x0d', '\x0a', 'B', + #ifndef CORRADE_TARGET_BIG_ENDIAN + 'L', 'O', + #else + 'O', 'L', + #endif + 'B', 0, 0, + #ifndef CORRADE_TARGET_BIG_ENDIAN + 42, 0, + #else + 0, 42, + #endif + 'W', 'a', 'v', 'e', + #ifndef CORRADE_TARGET_BIG_ENDIAN + 24 + 5, 0, 0, 0, 0, 0, 0, 0, + #else + 0, 0, 0, 0, 0, 0, 0, 24 + 5, + #endif + + 'h', 'e', 'l', 'l', 'o' +}; + +constexpr Containers::ArrayView Data = sizeof(void*) == 4 ? + Containers::arrayView(Data32) : Containers::arrayView(Data64); + +const struct { + const char* name; + std::size_t size; + std::size_t offset; + Containers::Array replace; + const char* message; +} DataChunkDeserializeInvalidData[] { + {"too short header", + sizeof(void*) == 4 ? 19 : 23, 0, {}, + sizeof(void*) == 4 ? + "expected at least 20 bytes for a header but got 19" : + "expected at least 24 bytes for a header but got 23"}, + {"too short chunk", + sizeof(void*) == 4 ? 24 : 28, 0, {}, + sizeof(void*) == 4 ? + "expected at least 25 bytes but got 24" : + "expected at least 29 bytes but got 28"}, + {"wrong version", + 0, 0, Containers::array({'\x7f'}), + "expected version 128 but got 127"}, + {"invalid signature", + 0, 4, + /* Using the 32-bit signature on 64-bit and vice versa */ + #ifndef CORRADE_TARGET_BIG_ENDIAN + sizeof(void*) == 4 ? + Containers::array({'B', 'L', 'O', 'B'}) : + Containers::array({'B', 'l', 'O', 'B'}), + sizeof(void*) == 4 ? + "expected signature Trade::DataChunkSignature('B', 'l', 'O', 'B') but got Trade::DataChunkSignature('B', 'L', 'O', 'B')" : + "expected signature Trade::DataChunkSignature('B', 'L', 'O', 'B') but got Trade::DataChunkSignature('B', 'l', 'O', 'B')" + #else + sizeof(void*) == 4 ? + Containers::array({'B', 'O', 'L', 'B'}) : + Containers::array({'B', 'O', 'l', 'B'}), + sizeof(void*) == 4 ? + "expected signature Trade::DataChunkSignature('B', 'O', 'l', 'B') but got Trade::DataChunkSignature('B', 'O', 'L', 'B')" : + "expected signature Trade::DataChunkSignature('B', 'O', 'L', 'B') but got Trade::DataChunkSignature('B', 'O', 'l', 'B')" + #endif + }, + {"invalid check bytes", + 0, 8, Containers::array({1, 0}), + "invalid header check bytes"}, +}; + +constexpr struct { + const char* name; + std::size_t size; +} DataChunkSerializeData[] { + {"no extra data", sizeof(DataChunkHeader)}, + {"1735 bytes extra data", sizeof(DataChunkHeader) + 1735} }; DataTest::DataTest() { + addTests({&DataTest::dataChunkHeaderDeserialize}); + + addInstancedTests({&DataTest::dataChunkHeaderDeserializeInvalid}, + Containers::arraySize(DataChunkDeserializeInvalidData)); + + addInstancedTests({&DataTest::dataChunkHeaderSerialize}, + Containers::arraySize(DataChunkSerializeData)); + + addTests({&DataTest::dataChunkHeaderSerializeTooShort}); + addTests({&DataTest::debugDataFlag, - &DataTest::debugDataFlags}); + &DataTest::debugDataFlags, + + &DataTest::debugDataChunkType, + &DataTest::debugDataChunkSignature}); +} + +void DataTest::dataChunkHeaderDeserialize() { + CORRADE_VERIFY(isDataChunk(Data)); + const DataChunkHeader* chunk = Trade::dataChunkHeaderDeserialize(Data); + CORRADE_VERIFY(chunk); +} + +void DataTest::dataChunkHeaderDeserializeInvalid() { + auto&& data = DataChunkDeserializeInvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array blob{Containers::NoInit, Data.size()}; + Utility::copy(Data, 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(!isDataChunk(view)); + CORRADE_VERIFY(!Trade::dataChunkHeaderDeserialize(view)); + CORRADE_COMPARE(out.str(), + Utility::formatString("Trade::dataChunkHeaderDeserialize(): {}\n", data.message)); +} + +void DataTest::dataChunkHeaderSerialize() { + auto&& data = DataChunkSerializeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array out{Containers::NoInit, data.size}; + std::size_t size = dataChunkHeaderSerializeInto(out, DataChunkType(Utility::Endianness::fourCC('r', 't', 'F', 'm')), 0xfeed); + CORRADE_COMPARE(size, sizeof(DataChunkHeader)); + #ifndef CORRADE_TARGET_BIG_ENDIAN + if(sizeof(void*) == 4) CORRADE_COMPARE_AS(out.prefix(size), + Containers::arrayView({ + '\x80', '\x0a', '\x0d', '\x0a', 'B', 'l', 'O', 'B', 0, 0, + '\xed', '\xfe', 'r', 't', 'F', 'm', + char(data.size & 0xff), char(data.size >> 8 & 0xff), 0, 0, + }), TestSuite::Compare::Container); + else CORRADE_COMPARE_AS(out.prefix(size), + Containers::arrayView({ + '\x80', '\x0a', '\x0d', '\x0a', 'B', 'L', 'O', 'B', 0, 0, + '\xed', '\xfe', 'r', 't', 'F', 'm', + char(data.size & 0xff), char(data.size >> 8 & 0xff), 0, 0, 0, 0, 0, 0 + }), TestSuite::Compare::Container); + #else + if(sizeof(void*) == 4) CORRADE_COMPARE_AS(out.prefix(size), + Containers::arrayView({ + '\x80', '\x0a', '\x0d', '\x0a', 'B', 'O', 'l', 'B', 0, 0, + '\xed', '\xfe', 'r', 't', 'F', 'm', + 0, 0, char(data.size >> 8 & 0xff), char(data.size & 0xff) + }), TestSuite::Compare::Container); + else CORRADE_COMPARE_AS(out.prefix(size), + Containers::arrayView({ + '\x80', '\x0a', '\x0d', '\x0a', 'B', 'O', 'L', 'B', 0, 0, + '\xed', '\xfe', 'r', 't', 'F', 'm', + 0, 0, 0, 0, 0, 0, char(data.size >> 8 & 0xff), char(data.size & 0xff) + }), TestSuite::Compare::Container); + #endif +} + +void DataTest::dataChunkHeaderSerializeTooShort() { + std::ostringstream out; + Error redirectError{&out}; + char data[sizeof(DataChunkHeader) - 1]; + dataChunkHeaderSerializeInto(data, DataChunkType{}, 0); + CORRADE_COMPARE(out.str(), sizeof(void*) == 4 ? + "Trade::dataChunkHeaderSerializeInto(): data too small, expected at least 20 bytes but got 19\n" : + "Trade::dataChunkHeaderSerializeInto(): data too small, expected at least 24 bytes but got 23\n"); } void DataTest::debugDataFlag() { @@ -57,6 +255,20 @@ void DataTest::debugDataFlags() { CORRADE_COMPARE(out.str(), "Trade::DataFlag::Owned|Trade::DataFlag::Mutable Trade::DataFlags{}\n"); } +void DataTest::debugDataChunkType() { + std::ostringstream out; + + Debug{&out} << DataChunkType(Utility::Endianness::fourCC('M', 's', 'h', '\xab')) << DataChunkType{}; + CORRADE_COMPARE(out.str(), "Trade::DataChunkType('M', 's', 'h', 0xab) Trade::DataChunkType(0x0, 0x0, 0x0, 0x0)\n"); +} + +void DataTest::debugDataChunkSignature() { + std::ostringstream out; + + Debug{&out} << DataChunkSignature::Little64 << DataChunkSignature{}; + CORRADE_COMPARE(out.str(), "Trade::DataChunkSignature('B', 'L', 'O', 'B') Trade::DataChunkSignature(0x0, 0x0, 0x0, 0x0)\n"); +} + }}}} CORRADE_TEST_MAIN(Magnum::Trade::Test::DataTest) diff --git a/src/Magnum/Trade/Trade.h b/src/Magnum/Trade/Trade.h index e146d5ef1..28b8233aa 100644 --- a/src/Magnum/Trade/Trade.h +++ b/src/Magnum/Trade/Trade.h @@ -62,6 +62,10 @@ class CameraData; enum class DataFlag: UnsignedByte; typedef Containers::EnumSet DataFlags; +struct DataChunkHeader; +class DataChunk; +enum class DataChunkSignature: UnsignedInt; +enum class DataChunkType: UnsignedInt; template class ImageData; typedef ImageData<1> ImageData1D; 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 12/16] 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 From 59a85ed3473364283c3045fb6cbd53ed54bb73f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 17 Apr 2020 20:19:30 +0200 Subject: [PATCH 13/16] doc: a high-level intro to the new serialization format. --- doc/blob.dox | 107 +++++++++++++++++++++++++++++++++++ doc/changelog.dox | 3 + doc/features.dox | 1 + doc/snippets/MagnumTrade.cpp | 13 +++++ src/Magnum/Trade/Data.h | 11 +++- src/Magnum/Trade/MeshData.h | 2 +- 6 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 doc/blob.dox diff --git a/doc/blob.dox b/doc/blob.dox new file mode 100644 index 000000000..fc8c890a5 --- /dev/null +++ b/doc/blob.dox @@ -0,0 +1,107 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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. +*/ + +namespace Magnum { +/** @page blob Magnum's memory-mappable serialization format +@brief Efficient and extensible format for storing binary data +@m_since_latest + +@tableofcontents +@m_footernavigation + +Apart from various data import and conversion plugins, described in the +@ref plugins "previous chapter", Magnum provides its own binary format. Files +stored in this format have a `*.blob` extension and are identified by various +permutations of the letters `BLOB` in their first few bytes. + +The goal of the format is being usable directly without having to process the +data payload in any way. That allows for example the file contents to be +memory-mapped and operated on directly. In order to achieve this, there's four +different variants of the format based on whether it's running on a 32-bit or +64-bit system and whether the machine is Little- or Big-Endian. The @ref Trade +library itself provides serialization and deserialization of blob formats +matching the platform it's running on. Import and conversion of blobs with +different endianness or bitness (as well as compatibility with previous format +versions as the format will evolve) is handled by the +@ref Trade::MagnumImporter "MagnumImporter" and +@ref Trade::MagnumSceneConverter "MagnumSceneConverter" plugins --- since this +functionality is not strictly needed when shipping an application, it's +provided separately. + +@section blob-implementation Implementation + +The binary format consists of "chunks" similar to [RIFF](https://en.wikipedia.org/wiki/Resource_Interchange_File_Format), +and the main property is an ability to combine arbitrary chunks together in the +most trivial way possible as well as extracting them back. Each chunk has a +@ref Trade::DataChunkHeader containing a [FourCC](https://en.wikipedia.org/wiki/FourCC)-like @ref Trade::DataChunkType identifier and a chunk size, allowing applications to pick chunks that they're interested in and reliably skip the +others. Compared to RIFF the file doesn't have any "global" chunk in order to +make trivial file concatenation possible: + +@code{.sh} +cat chair.blob table.blob > furniture.blob +@endcode + +@section blob-iteration Chunk iteration + +To be designed & written first. + +@section blob-meshdata Mesh data + +Currently there's just a single serializable data type, @ref Trade::MeshData. +You can create serialized blobs using @ref Trade::MeshData::serialize() or +alternatively using the @ref magnum-sceneconverter "magnum-sceneconverter" +tool, for example: + +@code{.sh} +magnum-sceneconverter avocado.glb avocado.blob +@endcode + +Deserialization is then done with @ref Trade::MeshData::deserialize(). The +function takes a memory view as an input and returns a @ref Trade::MeshData +instance pointing to that view, without copying or processing the data in any +way. A recommended way to access serialized data is thus via memory-mapping the +file (for example using @ref Utility::Directory::mapRead() or any other way +your platform allows), and keeping it around for as long as you need: + +@snippet MagnumTrade.cpp blob-deserialize-mesh + +@section blob-custom Custom chunk types + +As said above, the format is designed to allow custom chunk types to be mixed +together with data recognized by Magnum. To make a custom chunk, create your +own @ref Trade::DataChunkType using @ref Corrade::Utility::Endianness::fourCC() +--- identifiers starting with an uppercase letter are reserved for Magnum +itself, custom application-specific data types should use a lowercase first +letter instead. + +Then write a serialization/deserialization API similar to +@ref Trade::MeshData::serialize() / @ref Trade::MeshData::deserialize() with +the help of low-level @ref Trade::dataChunkHeaderSerializeInto() and +@ref Trade::dataChunkHeaderDeserialize(). Those functions will take care of +properly filling in required chunk header fields when serializing and checking +chunk validity when deserializing. Validation of the chunk data itself is then +up to you. +*/ +} diff --git a/doc/changelog.dox b/doc/changelog.dox index 8df4b4fbb..f8bdbb019 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -272,6 +272,9 @@ See also: scene formats - New @ref Trade::AbstractSceneConverter plugin interface and an @ref Trade::AnySceneConverter "AnySceneConverter" plugin +- Efficient and extensible memory-mappable serialization format for binary + data. See @ref blob for an introduction, see also + [mosra/magnum#427](https://github.com/mosra/magnum/pull/427). - Ability to import image mip levels via an additional parameter in @ref Trade::AbstractImporter::image2D(), @ref Trade::AbstractImporter::image2DLevelCount() and similar APIs for 1D diff --git a/doc/features.dox b/doc/features.dox index cdf60989e..fc8fbad66 100644 --- a/doc/features.dox +++ b/doc/features.dox @@ -37,6 +37,7 @@ necessary to read through everything, pick only what you need. - @subpage transformations --- @copybrief transformations - @subpage animation --- @copybrief animation - @subpage plugins --- @copybrief plugins +- @subpage blob --- @copybrief blob - @subpage opengl-wrapping --- @copybrief opengl-wrapping - @subpage shaders --- @copybrief shaders - @subpage scenegraph --- @copybrief scenegraph diff --git a/doc/snippets/MagnumTrade.cpp b/doc/snippets/MagnumTrade.cpp index a0460181c..94ba05ff8 100644 --- a/doc/snippets/MagnumTrade.cpp +++ b/doc/snippets/MagnumTrade.cpp @@ -65,6 +65,19 @@ using namespace Magnum::Math::Literals; int main() { +{ +/* [blob-deserialize-mesh] */ +Containers::Array blob = + Utility::Directory::mapRead("extremely-huge-spaceship.blob"); + +Containers::Optional spaceship = + Trade::MeshData::deserialize(blob); +if(!spaceship) Fatal{} << "oh no"; + +// ... +/* [blob-deserialize-mesh] */ +} + { /* [AbstractImporter-usage] */ PluginManager::Manager manager; diff --git a/src/Magnum/Trade/Data.h b/src/Magnum/Trade/Data.h index b43726dea..fff5c8574 100644 --- a/src/Magnum/Trade/Data.h +++ b/src/Magnum/Trade/Data.h @@ -104,6 +104,8 @@ specified effect in the current version of the header. It doesn't need to be alphanumeric either, but for additional versioning of a particular chunk type it's recommended to use @ref DataChunkHeader::typeVersion, keeping the chunk type FourCC clearly recognizable. + +@see @ref blob */ enum class DataChunkType: UnsignedInt { /** @@ -139,7 +141,7 @@ MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, DataChunkType value); Reads as `BLOB` letters for a Little-Endian 64 bit data chunk. For Big-Endian the order is reversed (thus `BOLB`), 32-bit data have the `L` letter lowercase. -@see @ref DataChunkHeader::signature +@see @ref blob, @ref DataChunkHeader::signature */ enum class DataChunkSignature: UnsignedInt { /** Little-Endian 32-bit data. The letters `BlOB`. */ @@ -176,6 +178,8 @@ MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, DataChunkSignature value); @brief Header for memory-mappable data chunks @m_since_latest +See @ref blob for an introduction. + Since the goal of the serialization format is to be a direct equivalent to the in-memory data layout, there's four different variants of the header based on whether it's running on a 32-bit or 64-bit system and whether the machine is @@ -241,6 +245,7 @@ current platform and @p data is large enough to contain the whole chunk, @cpp false @ce otherwise. The function doesn't print any diagnostic messages on validation failure, use @ref dataChunkHeaderDeserialize() instead if you need to know why. +@see @ref blob */ MAGNUM_TRADE_EXPORT bool isDataChunk(Containers::ArrayView data); @@ -251,7 +256,7 @@ MAGNUM_TRADE_EXPORT bool isDataChunk(Containers::ArrayView data); Checks that @p data is large enough to contain a valid data chunk, validates the header and then returns @p data reinterpreted as a @ref DataChunkHeader pointer. On failure prints an error message and returns @cpp nullptr @ce. -@see @ref isDataChunk(), @ref dataChunkHeaderSerializeInto() +@see @ref blob, @ref isDataChunk(), @ref dataChunkHeaderSerializeInto() */ MAGNUM_TRADE_EXPORT const DataChunkHeader* dataChunkHeaderDeserialize(Containers::ArrayView data); @@ -267,7 +272,7 @@ Expects that @p data is at least the size of @ref DataChunkHeader. Fills in @ref DataChunkHeader::typeVersion and @ref DataChunkHeader::type with passed values used in constructor, and @ref DataChunkHeader::size with @p data size. -@see @ref dataChunkHeaderDeserialize() +@see @ref blob, @ref dataChunkHeaderDeserialize() */ MAGNUM_TRADE_EXPORT std::size_t dataChunkHeaderSerializeInto(Containers::ArrayView out, DataChunkType type, UnsignedShort typeVersion); diff --git a/src/Magnum/Trade/MeshData.h b/src/Magnum/Trade/MeshData.h index 357a5c207..26d54116d 100644 --- a/src/Magnum/Trade/MeshData.h +++ b/src/Magnum/Trade/MeshData.h @@ -715,7 +715,7 @@ implementation-specific @ref VertexFormat values. Using @ref serialize(), an instance of this class can be serialized into Magnum's memory-mappable serialization format, and deserialized back using -@ref deserialize(). +@ref deserialize(). See @ref blob for a high-level introduction. The deserialization only involves various sanity checks followed by a creation of a new @ref MeshData instance referencing the index, vertex and attribute From c8f169657fe452681fa41431e7ea2baa235776f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 17 Apr 2020 21:07:07 +0200 Subject: [PATCH 14/16] sceneconverter: a faster mmap handling of blobs. Time to do the following (with a 482 MB file) magnum-sceneconverter lucy.blob b.blob goes down from about 1.9 seconds to 450 ms, ~equivalent to what cp lucy.blob b.blob takes (and of course this includes all range and validity checks). --- src/Magnum/Trade/sceneconverter.cpp | 95 ++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/src/Magnum/Trade/sceneconverter.cpp b/src/Magnum/Trade/sceneconverter.cpp index 3e81983c3..b0510f265 100644 --- a/src/Magnum/Trade/sceneconverter.cpp +++ b/src/Magnum/Trade/sceneconverter.cpp @@ -118,6 +118,39 @@ struct Duration { std::chrono::high_resolution_clock::time_point _t; }; +/* Direct shims for fast deserialization / serialization of blob data. Compared + to MagnumImporter / MagnumSceneConverter these don't make the whole file + resident in memory, so *much* faster. */ +class BlobImporter: public Trade::AbstractImporter { + Trade::ImporterFeatures doFeatures() const override { return {}; } + + bool doIsOpened() const override { return _in; } + void doClose() override { _in = nullptr; } + void doOpenFile(const std::string& filename) override { + _in = Utility::Directory::mapRead(filename); + } + + UnsignedInt doMeshCount() const override { return 1; } + Containers::Optional doMesh(UnsignedInt, UnsignedInt) override { + return Trade::MeshData::deserialize(_in); + } + + Containers::Array _in; +}; + +class BlobSceneConverter: public Trade::AbstractSceneConverter { + Trade::SceneConverterFeatures doFeatures() const override { + return Trade::SceneConverterFeature::ConvertMeshToFile; + } + + bool doConvertToFile(const std::string& filename, const Magnum::Trade::MeshData& mesh) override { + Containers::Array out = Utility::Directory::mapWrite(filename, mesh.serializedSize()); + if(!out) return false; + mesh.serializeInto(out); + return true; + } +}; + } int main(int argc, char** argv) { @@ -151,20 +184,28 @@ plugin configuration. If the = character is omitted, it's equivalent to saying key=true; configuration subgroups are delimited with /.)") .parse(argc, argv); - PluginManager::Manager importerManager{ - args.value("plugin-dir").empty() ? std::string{} : - Utility::Directory::join(args.value("plugin-dir"), Trade::AbstractImporter::pluginSearchPaths()[0])}; + /* Load importer plugin, or use the blob shim in case the extension + matches and we're not overriding the converter to something specific */ + Containers::Optional> importerManager; + Containers::Pointer importer; + if(Utility::String::endsWith(args.value("input"), ".blob") && args.value("importer") == "AnySceneImporter") { + importer.reset(new BlobImporter); + if(!args.value("importer-options").empty()) + Warning{} << "Importer options" << args.value("importer-options") << "ignored when loading a blob file"; + } else { + importerManager.emplace( + args.value("plugin-dir").empty() ? std::string{} : + Utility::Directory::join(args.value("plugin-dir"), Trade::AbstractImporter::pluginSearchPaths()[0])); + importer = importerManager->loadAndInstantiate(args.value("importer")); + if(!importer) { + Debug{} << "Available importer plugins:" << Utility::String::join(importerManager->aliasList(), ", "); + return 1; + } - Containers::Pointer importer = importerManager.loadAndInstantiate(args.value("importer")); - if(!importer) { - Debug{} << "Available importer plugins:" << Utility::String::join(importerManager.aliasList(), ", "); - return 1; + if(args.isSet("verbose")) importer->setFlags(Trade::ImporterFlag::Verbose); + Trade::Implementation::setOptions(*importer, args.value("importer-options")); } - /* Set options, if passed */ - if(args.isSet("verbose")) importer->setFlags(Trade::ImporterFlag::Verbose); - Trade::Implementation::setOptions(*importer, args.value("importer-options")); - std::chrono::high_resolution_clock::duration importTime; /* Open the file */ @@ -311,19 +352,27 @@ key=true; configuration subgroups are delimited with /.)") } } - /* Load converter plugin */ - PluginManager::Manager converterManager{ - args.value("plugin-dir").empty() ? std::string{} : - Utility::Directory::join(args.value("plugin-dir"), Trade::AbstractSceneConverter::pluginSearchPaths()[0])}; - Containers::Pointer converter = converterManager.loadAndInstantiate(args.value("converter")); - if(!converter) { - Debug{} << "Available converter plugins:" << Utility::String::join(converterManager.aliasList(), ", "); - return 2; - } + /* Load converter plugin, or use the blob shim in case the extension + matches and we're not overriding the converter to something specific */ + Containers::Optional> converterManager; + Containers::Pointer converter; + if(Utility::String::endsWith(args.value("output"), ".blob") && args.value("converter") == "AnySceneConverter") { + converter.reset(new BlobSceneConverter); + if(!args.value("converter-options").empty()) + Warning{} << "Converter options" << args.value("converter-options") << "ignored when writing a blob file"; + } else { + converterManager.emplace( + args.value("plugin-dir").empty() ? std::string{} : + Utility::Directory::join(args.value("plugin-dir"), Trade::AbstractSceneConverter::pluginSearchPaths()[0])); + converter = converterManager->loadAndInstantiate(args.value("converter")); + if(!converter) { + Debug{} << "Available converter plugins:" << Utility::String::join(converterManager->aliasList(), ", "); + return 2; + } - /* Set options, if passed */ - if(args.isSet("verbose")) converter->setFlags(Trade::SceneConverterFlag::Verbose); - Trade::Implementation::setOptions(*converter, args.value("converter-options")); + if(args.isSet("verbose")) converter->setFlags(Trade::SceneConverterFlag::Verbose); + Trade::Implementation::setOptions(*converter, args.value("converter-options")); + } std::chrono::high_resolution_clock::duration conversionTime; From c653cf34978bb0370d0ad3523e25fbf793dc3c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 17 Apr 2020 22:46:24 +0200 Subject: [PATCH 15/16] Trade: c'mon, old compilers. --- doc/snippets/MagnumTrade.cpp | 3 +++ src/Magnum/Trade/sceneconverter.cpp | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/snippets/MagnumTrade.cpp b/doc/snippets/MagnumTrade.cpp index 94ba05ff8..a3974993e 100644 --- a/doc/snippets/MagnumTrade.cpp +++ b/doc/snippets/MagnumTrade.cpp @@ -65,6 +65,8 @@ using namespace Magnum::Math::Literals; int main() { +/* GCC 4.8 and Clang 3.8 has problems with an implicit cast here */ +#if (defined(CORRADE_TARGET_UNIX) || (defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT))) && (!defined(CORRADE_TARGET_GCC) || __GNUC__ > 5 || (!defined(CORRADE_TARGET_APPLE_CLANG) && __clang_major__ >= 4) || (defined(CORRADE_TARGET_APPLE_CLANG) && __clang_major__ >= 9)) { /* [blob-deserialize-mesh] */ Containers::Array blob = @@ -77,6 +79,7 @@ if(!spaceship) Fatal{} << "oh no"; // ... /* [blob-deserialize-mesh] */ } +#endif { /* [AbstractImporter-usage] */ diff --git a/src/Magnum/Trade/sceneconverter.cpp b/src/Magnum/Trade/sceneconverter.cpp index b0510f265..9d1297e09 100644 --- a/src/Magnum/Trade/sceneconverter.cpp +++ b/src/Magnum/Trade/sceneconverter.cpp @@ -132,7 +132,8 @@ class BlobImporter: public Trade::AbstractImporter { UnsignedInt doMeshCount() const override { return 1; } Containers::Optional doMesh(UnsignedInt, UnsignedInt) override { - return Trade::MeshData::deserialize(_in); + /* GCC 4.8 and old Clang has problems with an implicit cast here */ + return Trade::MeshData::deserialize(Containers::ArrayView(_in)); } Containers::Array _in; From 260dee1565b933cea1476af47ec47aa724f6549b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 23 Apr 2020 23:47:21 +0200 Subject: [PATCH 16/16] [wip] sceneconverter: support also plugins that can't output a file. Those will simply produce serialized blobs on output. TODO: make this more generic? like, if I specify a *.ply at the end, it uses some other converter after that? --- src/Magnum/Trade/sceneconverter.cpp | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Magnum/Trade/sceneconverter.cpp b/src/Magnum/Trade/sceneconverter.cpp index 9d1297e09..b16165d53 100644 --- a/src/Magnum/Trade/sceneconverter.cpp +++ b/src/Magnum/Trade/sceneconverter.cpp @@ -377,13 +377,36 @@ key=true; configuration subgroups are delimited with /.)") std::chrono::high_resolution_clock::duration conversionTime; - /* Save output file */ - { + /* Save output file directly, if it supports that */ + if(converter->features() & Trade::SceneConverterFeature::ConvertMeshToFile) { Duration d{conversionTime}; if(!converter->convertToFile(args.value("output"), *mesh)) { Error{} << "Cannot save file" << args.value("output"); return 5; } + + /* Otherwise convert the meshdata and then save as a blob */ + } else if(converter->features() & (Trade::SceneConverterFeature::ConvertMesh|Trade::SceneConverterFeature::ConvertMeshInPlace)) { + if(converter->features() & Trade::SceneConverterFeature::ConvertMesh) { + Duration d{conversionTime}; + if(!(mesh = converter->convert(*mesh))) { + Error{} << "Cannot convert the mesh"; + return 5; + } + } else if(converter->features() & Trade::SceneConverterFeature::ConvertMeshInPlace) { + Duration d{conversionTime}; + if(!converter->convertInPlace(*mesh)) { + Error{} << "Cannot convert the mesh in-place"; + return 5; + } + } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + + Containers::Array out = Utility::Directory::mapWrite(args.value("output"), mesh->serializedSize()); + if(!out) { + Error{} << "Cannot save file" << args.value("output"); + return 6; + } + mesh->serializeInto(out); } if(args.isSet("profile")) {