diff --git a/CMakeLists.txt b/CMakeLists.txt index 48b06611c7..bcd8b7139b 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) @@ -356,6 +357,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 +399,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 +408,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/blob.dox b/doc/blob.dox new file mode 100644 index 0000000000..fc8c890a51 --- /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/building.dox b/doc/building.dox index d83e6f2a27..26eed92930 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 192d5bf67b..f8bdbb0193 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 @@ -269,6 +270,11 @@ 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 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 @@ -630,6 +636,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/doc/cmake.dox b/doc/cmake.dox index 5617decf9f..a53f544c8d 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/features.dox b/doc/features.dox index cdf60989e6..fc8fbad667 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/plugins.dox b/doc/plugins.dox index fd3602f954..fd9d930753 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 @@ -192,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/doc/snippets/CMakeLists.txt b/doc/snippets/CMakeLists.txt index b38d4c5daa..aac0b6ca95 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 dce0d4d4e1..64e4b09e11 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 a6a5ae09e1..027fcbb9a2 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/MagnumTrade.cpp b/doc/snippets/MagnumTrade.cpp index a0460181c3..a3974993e4 100644 --- a/doc/snippets/MagnumTrade.cpp +++ b/doc/snippets/MagnumTrade.cpp @@ -65,6 +65,22 @@ 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 = + Utility::Directory::mapRead("extremely-huge-spaceship.blob"); + +Containers::Optional spaceship = + Trade::MeshData::deserialize(blob); +if(!spaceship) Fatal{} << "oh no"; + +// ... +/* [blob-deserialize-mesh] */ +} +#endif + { /* [AbstractImporter-usage] */ PluginManager::Manager manager; diff --git a/doc/snippets/debugtools-frameprofiler.ansi b/doc/snippets/debugtools-frameprofiler.ansi new file mode 100644 index 0000000000..ebd265f26f --- /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 0000000000..4f4ffb81b5 --- /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/modules/FindMagnum.cmake b/modules/FindMagnum.cmake index 30161db04a..121727c7d9 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 @@ -53,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 @@ -175,6 +178,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 @@ -354,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) @@ -456,7 +463,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 +554,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 +1126,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 +1156,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 +1165,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/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 0ab7c0642d..3f418875f9 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 8fa8a21c56..8572eee983 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 2cd2963098..b55f51dee7 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 fe09aa288e..851686bf02 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 61f69baeb8..6697201e76 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 a036b7f158..82183898a5 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 18b02e4ea4..cc4a32d33f 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 fbaec668b3..a77a49d91a 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 c4998b9343..067979a665 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 00ad22b87a..0fae3814c8 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 98c16041ba..b3adcbe51a 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 4e883768b9..ee6b9ba64e 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 7067de768e..40bb601cc3 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 5bb5d87d25..f4078fb1d0 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 01606f3c01..5dcbb9b388 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 7ca76d5119..b78153ee39 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 3570df1baa..db44712298 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 ffccc86fa8..e86120aed3 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 1e636a8cc3..8cc12a36b3 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 e7d6ee39d0..c2ad4771d0 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 cd93b02b99..4f44ef206d 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 ce69848d1f..7e9b756019 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 4b9ad216c9..6414b66aa2 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 409ec498f5..fa53336551 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 8f3d4c9002..9a7f6b2243 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 090882740a..b1ba1d3c9f 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 420eed6470..17642843e9 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 a81b6ef837..fb0f7f0767 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 553989a3cc..878884ac12 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 8e886ecf4a..a246c667d2 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 48b87cc024..af66da8400 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 482d8bdb5c..e6f42e9e45 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 a4eb16c4b6..85f972db5a 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 e52627d3d9..3748916634 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 0b81f66d0f..6799f39dee 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 728bb2dbda..2cb5bb5941 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/Magnum/DebugTools/CMakeLists.txt b/src/Magnum/DebugTools/CMakeLists.txt index 767d5d0af3..6c12102868 100644 --- a/src/Magnum/DebugTools/CMakeLists.txt +++ b/src/Magnum/DebugTools/CMakeLists.txt @@ -24,21 +24,26 @@ # set(MagnumDebugTools_SRCS - ColorMap.cpp - Profiler.cpp) + ColorMap.cpp) -set(MagnumDebugTools_GracefulAssert_SRCS ) +set(MagnumDebugTools_GracefulAssert_SRCS + FrameProfiler.cpp) set(MagnumDebugTools_HEADERS ColorMap.h DebugTools.h - Profiler.h + FrameProfiler.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/FrameProfiler.cpp b/src/Magnum/DebugTools/FrameProfiler.cpp new file mode 100644 index 0000000000..d6a04e1714 --- /dev/null +++ b/src/Magnum/DebugTools/FrameProfiler.cpp @@ -0,0 +1,715 @@ +/* + 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 + +#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 + +namespace { + +constexpr const char* GLFrameProfilerValueNames[] { + "FrameTime", + "CpuDuration", + "GpuDuration", + "VertexFetchRatio", + "PrimitiveClipRatio" +}; + +} + +Debug& operator<<(Debug& debug, const GLFrameProfiler::Value value) { + debug << "DebugTools::GLFrameProfiler::Value" << Debug::nospace; + + 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 << ")"; +} + +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 + +}} + +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 new file mode 100644 index 0000000000..684f1816b3 --- /dev/null +++ b/src/Magnum/DebugTools/FrameProfiler.h @@ -0,0 +1,754 @@ +#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 + +}} + +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/Profiler.cpp b/src/Magnum/DebugTools/Profiler.cpp index c506ebc601..168b2f4fe9 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 91bc31899e..0e2b387c1e 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 diff --git a/src/Magnum/DebugTools/Test/CMakeLists.txt b/src/Magnum/DebugTools/Test/CMakeLists.txt index fab9805afe..9e7df9b00e 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 0000000000..f069f28a52 --- /dev/null +++ b/src/Magnum/DebugTools/Test/FrameProfilerGLTest.cpp @@ -0,0 +1,244 @@ +/* + 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); + + /* 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); + 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 0000000000..b8eb11bfea --- /dev/null +++ b/src/Magnum/DebugTools/Test/FrameProfilerTest.cpp @@ -0,0 +1,1097 @@ +/* + 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/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(); + + void configurationGLValue(); + void configurationGLValues(); + #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, + + &FrameProfilerTest::configurationGLValue, + &FrameProfilerTest::configurationGLValues + #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); + + /* MSVC 2015 needs the {} */ + 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"); +} + +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 + +}}}} + +CORRADE_TEST_MAIN(Magnum::DebugTools::Test::FrameProfilerTest) diff --git a/src/Magnum/Trade/AbstractSceneConverter.cpp b/src/Magnum/Trade/AbstractSceneConverter.cpp new file mode 100644 index 0000000000..959afd3401 --- /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 0000000000..623972e05b --- /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 e41287968a..e7eb5bfb1e 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 @@ -38,8 +37,10 @@ set(MagnumTrade_SRCS set(MagnumTrade_GracefulAssert_SRCS AbstractImageConverter.cpp AbstractImporter.cpp + AbstractSceneConverter.cpp AnimationData.cpp CameraData.cpp + Data.cpp ImageData.cpp MeshData.cpp ObjectData2D.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/Data.cpp b/src/Magnum/Trade/Data.cpp index b7efc41c89..4f24953c4b 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 4e1684ffac..fff5c85741 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,195 @@ 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. + +@see @ref blob +*/ +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 blob, @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 + +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 +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. +@see @ref blob +*/ +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 blob, @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 blob, @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/Implementation/converterUtilities.h b/src/Magnum/Trade/Implementation/converterUtilities.h index 4d617874a3..387b0f148c 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/MeshData.cpp b/src/Magnum/Trade/MeshData.cpp index a4a949bc2f..24da51c35d 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 858f922252..26d54116d4 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(). 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 +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,11 +1848,36 @@ 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 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/.gitattributes b/src/Magnum/Trade/Test/.gitattributes new file mode 100644 index 0000000000..6f9689e398 --- /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/AbstractSceneConverterTest.cpp b/src/Magnum/Trade/Test/AbstractSceneConverterTest.cpp new file mode 100644 index 0000000000..67953e4895 --- /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 f89f44e453..49dd18e151 100644 --- a/src/Magnum/Trade/Test/CMakeLists.txt +++ b/src/Magnum/Trade/Test/CMakeLists.txt @@ -42,13 +42,34 @@ 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) +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) @@ -62,6 +83,7 @@ set_property(TARGET set_target_properties( TradeAbstractImageConverterTest TradeAbstractImporterTest + TradeAbstractSceneConverterTest TradeAnimationDataTest TradeCameraDataTest TradeImageDataTest diff --git a/src/Magnum/Trade/Test/DataTest.cpp b/src/Magnum/Trade/Test/DataTest.cpp index e1253d498b..7810eba000 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/Test/MeshDataTest.cpp b/src/Magnum/Trade/Test/MeshDataTest.cpp index 301d305f4f..918376e4fa 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 0000000000..e7ce8a76f0 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-be32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-be64.blob b/src/Magnum/Trade/Test/mesh-be64.blob new file mode 100644 index 0000000000..80909c72b6 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-be64.blob differ diff --git a/src/Magnum/Trade/Test/mesh-empty-be32.blob b/src/Magnum/Trade/Test/mesh-empty-be32.blob new file mode 100644 index 0000000000..925d22b98f Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-empty-be32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-empty-be64.blob b/src/Magnum/Trade/Test/mesh-empty-be64.blob new file mode 100644 index 0000000000..3e4c868638 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-empty-be64.blob differ diff --git a/src/Magnum/Trade/Test/mesh-empty-le32.blob b/src/Magnum/Trade/Test/mesh-empty-le32.blob new file mode 100644 index 0000000000..a4e55832c9 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-empty-le32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-empty-le64.blob b/src/Magnum/Trade/Test/mesh-empty-le64.blob new file mode 100644 index 0000000000..4b78520f2d Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-empty-le64.blob differ diff --git a/src/Magnum/Trade/Test/mesh-le32.blob b/src/Magnum/Trade/Test/mesh-le32.blob new file mode 100644 index 0000000000..f0869ebee9 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-le32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-le64.blob b/src/Magnum/Trade/Test/mesh-le64.blob new file mode 100644 index 0000000000..909d364db1 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-le64.blob differ diff --git a/src/Magnum/Trade/Test/mesh-nonindexed-be32.blob b/src/Magnum/Trade/Test/mesh-nonindexed-be32.blob new file mode 100644 index 0000000000..c7b11350ae Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-nonindexed-be32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-nonindexed-be64.blob b/src/Magnum/Trade/Test/mesh-nonindexed-be64.blob new file mode 100644 index 0000000000..6ad77013ca Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-nonindexed-be64.blob differ diff --git a/src/Magnum/Trade/Test/mesh-nonindexed-le32.blob b/src/Magnum/Trade/Test/mesh-nonindexed-le32.blob new file mode 100644 index 0000000000..2a2485d5e6 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-nonindexed-le32.blob differ diff --git a/src/Magnum/Trade/Test/mesh-nonindexed-le64.blob b/src/Magnum/Trade/Test/mesh-nonindexed-le64.blob new file mode 100644 index 0000000000..de09529b16 Binary files /dev/null and b/src/Magnum/Trade/Test/mesh-nonindexed-le64.blob differ diff --git a/src/Magnum/Trade/Trade.h b/src/Magnum/Trade/Trade.h index d4df385bde..28b8233aae 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; @@ -61,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; diff --git a/src/Magnum/Trade/configure.h.cmake b/src/Magnum/Trade/configure.h.cmake index 024c1178fd..c969a20cba 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}" diff --git a/src/Magnum/Trade/imageconverter.cpp b/src/Magnum/Trade/imageconverter.cpp index df3ec9d1fb..73b0673327 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 b380e6f7d9..b16165d537 100644 --- a/src/Magnum/Trade/sceneconverter.cpp +++ b/src/Magnum/Trade/sceneconverter.cpp @@ -24,6 +24,7 @@ */ #include +#include #include #include #include @@ -34,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 { @@ -62,78 +64,162 @@ 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] [--] - 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 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`; configuration subgroups are delimited with +`/`. @see @ref magnum-imageconverter */ + } 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; +}; + +/* 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 { + /* GCC 4.8 and old Clang has problems with an implicit cast here */ + return Trade::MeshData::deserialize(Containers::ArrayView(_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) { 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") - /** @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; 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; - /* Print file info, if requested */ - if(args.isSet("info")) { - /* Open the file, but don't fail when an image can't be opened */ + /* 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 +248,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,9 +337,80 @@ 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; } - 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, 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; + } + + 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 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")) { + Debug{} << "Import took" << UnsignedInt(std::chrono::duration_cast(importTime).count())/1.0e3f << "seconds, conversion" + << UnsignedInt(std::chrono::duration_cast(conversionTime).count())/1.0e3f << "seconds"; + } } diff --git a/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.conf b/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.conf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.cpp b/src/MagnumPlugins/AnySceneConverter/AnySceneConverter.cpp new file mode 100644 index 0000000000..1ad64f482e --- /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 0000000000..bafcfab7ea --- /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 0000000000..43cc632627 --- /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 0000000000..d7698f7abb --- /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 0000000000..6f2b60d8b3 --- /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 0000000000..6bf97d62a7 --- /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 0000000000..ddc3b37382 --- /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 0000000000..3ca3c865e0 --- /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/AnySceneImporter/AnySceneImporter.cpp b/src/MagnumPlugins/AnySceneImporter/AnySceneImporter.cpp index a6c6859c77..8580a5b25d 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 9b37b9499f..dfb0284f9d 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 9ab10472fe..6b930f1bfb 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"}, diff --git a/src/MagnumPlugins/CMakeLists.txt b/src/MagnumPlugins/CMakeLists.txt index 55641e2534..43d5aea357 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()