diff --git a/CMakeLists.txt b/CMakeLists.txt index 19e6c2d0d..9cb988163 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,7 @@ option(WITH_DEBUGTOOLS "Build DebugTools library" ON) cmake_dependent_option(WITH_MESHTOOLS "Build MeshTools library" ON "NOT WITH_OBJIMPORTER;NOT WITH_SCENECONVERTER" ON) option(WITH_SCENEGRAPH "Build SceneGraph library" ON) option(WITH_SHADERS "Build Shaders library" ON) +option(WITH_SHADERTOOLS "Build ShaderTools library" ON) cmake_dependent_option(WITH_TEXT "Build Text library" ON "NOT WITH_FONTCONVERTER;NOT WITH_MAGNUMFONT;NOT WITH_MAGNUMFONTCONVERTER" ON) cmake_dependent_option(WITH_TEXTURETOOLS "Build TextureTools library" ON "NOT WITH_TEXT;NOT WITH_DISTANCEFIELDCONVERTER" ON) cmake_dependent_option(WITH_TRADE "Build Trade library" ON "NOT WITH_MESHTOOLS;NOT WITH_PRIMITIVES;NOT WITH_IMAGECONVERTER;NOT WITH_ANYIMAGEIMPORTER;NOT WITH_ANYIMAGECONVERTER;NOT WITH_ANYSCENEIMPORTER;NOT WITH_OBJIMPORTER;NOT WITH_TGAIMAGECONVERTER;NOT WITH_TGAIMPORTER" ON) @@ -362,6 +363,10 @@ set(MAGNUM_PLUGINS_SCENECONVERTER_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBU 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_SHADERCONVERTER_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_BINARY_INSTALL_DIR}/shaderconverters) +set(MAGNUM_PLUGINS_SHADERCONVERTER_DEBUG_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR}/shaderconverters) +set(MAGNUM_PLUGINS_SHADERCONVERTER_RELEASE_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR}/shaderconverters) +set(MAGNUM_PLUGINS_SHADERCONVERTER_RELEASE_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_LIBRARY_INSTALL_DIR}/shaderconverters) 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) @@ -396,6 +401,7 @@ set(MAGNUM_PLUGINS_DIR "" # Plugin directories. Set only if the above are non-empty. otherwise empty as # well. if(MAGNUM_PLUGINS_DIR) + set(MAGNUM_PLUGINS_SHADERCONVERTER_DIR ${MAGNUM_PLUGINS_DIR}/shaderconverters) set(MAGNUM_PLUGINS_FONT_DIR ${MAGNUM_PLUGINS_DIR}/fonts) set(MAGNUM_PLUGINS_FONTCONVERTER_DIR ${MAGNUM_PLUGINS_DIR}/fontconverters) set(MAGNUM_PLUGINS_IMAGECONVERTER_DIR ${MAGNUM_PLUGINS_DIR}/imageconverters) @@ -404,6 +410,7 @@ if(MAGNUM_PLUGINS_DIR) set(MAGNUM_PLUGINS_AUDIOIMPORTER_DIR ${MAGNUM_PLUGINS_DIR}/audioimporters) endif() if(MAGNUM_PLUGINS_DEBUG_DIR) + set(MAGNUM_PLUGINS_SHADERCONVERTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/shaderconverters) set(MAGNUM_PLUGINS_FONT_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/fonts) set(MAGNUM_PLUGINS_FONTCONVERTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/fontconverters) set(MAGNUM_PLUGINS_IMAGECONVERTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/imageconverters) @@ -413,6 +420,7 @@ if(MAGNUM_PLUGINS_DEBUG_DIR) set(MAGNUM_PLUGINS_AUDIOIMPORTER_DEBUG_DIR ${MAGNUM_PLUGINS_DEBUG_DIR}/audioimporters) endif() if(MAGNUM_PLUGINS_RELEASE_DIR) + set(MAGNUM_PLUGINS_SHADERCONVERTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/shaderconverters) 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) diff --git a/doc/building.dox b/doc/building.dox index e6028f86a..10bc4fb30 100644 --- a/doc/building.dox +++ b/doc/building.dox @@ -490,6 +490,7 @@ specify which parts will be built and which not: - `WITH_SCENEGRAPH` --- Build the @ref SceneGraph library - `WITH_SHADERS` --- Build the @ref Shaders library. Enables also building of the GL library. +- `WITH_SHADERTOOLS` --- Build the @ref ShaderTools library - `WITH_TEXT` --- Build the @ref Text library. Enables also building of the TextureTools library. - `WITH_TEXTURETOOLS` --- Build the @ref TextureTools library. Enabled diff --git a/doc/changelog.dox b/doc/changelog.dox index 4af8fbe1f..4cd27f16c 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -87,6 +87,12 @@ See also: - Added @ref Shaders::Phong::setLightSpecularColors() for better control over speculat highlights +@subsubsection changelog-latest-new-shadertools ShaderTools library + +- New @ref ShaderTools library that provides a + @ref ShaderTools::AbstractConverter plugin interface for shader validation, + conversion, compilation and optimization + @subsubsection changelog-latest-new-scenegraph SceneGraph library - Added @ref SceneGraph::Object::move() diff --git a/doc/cmake.dox b/doc/cmake.dox index 661c0f983..12bc6036c 100644 --- a/doc/cmake.dox +++ b/doc/cmake.dox @@ -197,6 +197,7 @@ the components. The optional components are: - `Primitives` --- @ref Primitives library - `SceneGraph` --- @ref SceneGraph library - `Shaders` --- @ref Shaders library +- `ShaderTools` --- @ref ShaderTools library - `Text` --- @ref Text library - `TextureTools` --- @ref TextureTools library - `Trade` --- @ref Trade library diff --git a/doc/compilation-speedup.dox b/doc/compilation-speedup.dox index d3a882a29..6d8773f07 100644 --- a/doc/compilation-speedup.dox +++ b/doc/compilation-speedup.dox @@ -62,6 +62,7 @@ available, each namespace has its own: - @ref Magnum/OvrIntegration/OvrIntegration.h - @ref Magnum/Platform/Platform.h - @ref Magnum/SceneGraph/SceneGraph.h +- @ref Magnum/ShaderTools/ShaderTools.h - @ref Magnum/Shaders/Shaders.h - @ref Magnum/Text/Text.h - @ref Magnum/Trade/Trade.h diff --git a/doc/custom-buildsystems-order.dot b/doc/custom-buildsystems-order.dot index fb7e18b01..978cada41 100644 --- a/doc/custom-buildsystems-order.dot +++ b/doc/custom-buildsystems-order.dot @@ -45,6 +45,7 @@ digraph "Magnum library dependency order" { MagnumPrimitives [label="Magnum\nPrimitives" class="m-info"] MagnumSceneGraph [label="Magnum\nSceneGraph" class="m-info"] MagnumShaders [label="Magnum\nShaders" class="m-info"] + MagnumShaderTools [label="Magnum\nShaderTools" class="m-info"] MagnumText [label="Magnum\nText" class="m-info"] MagnumTextureTools [label="Magnum\nTextureTools" class="m-info"] MagnumTrade [label="Magnum\nTrade" class="m-info"] @@ -85,6 +86,7 @@ digraph "Magnum library dependency order" { MagnumSceneGraph -> Magnum MagnumShaders -> MagnumGL + MagnumShaderTools -> Magnum MagnumText -> MagnumTextureTools MagnumText -> MagnumGL [style=dotted] diff --git a/doc/namespaces.dox b/doc/namespaces.dox index c651a85b4..4db8b43ae 100644 --- a/doc/namespaces.dox +++ b/doc/namespaces.dox @@ -493,6 +493,32 @@ See @ref building, @ref cmake and @ref shaders for more information. */ +/** @dir Magnum/ShaderTools + * @brief Namespace @ref Magnum::ShaderTools + * @m_since_latest + */ +/** @namespace Magnum::ShaderTools +@brief Shader tools +@m_since_latest + +Shader validation, conversion, compilation and reflection. + +This library is built if `WITH_SHADERTOOLS` is enabled when building Magnum. To +use this library with CMake, request the `ShaderTools` component of the +`Magnum` package and link to the `Magnum::ShaderTools` target: + +@code{.cmake} +find_package(Magnum REQUIRED ShaderTools) + +# ... +target_link_libraries(your-app PRIVATE Magnum::ShaderTools) +@endcode + +Additional plugins and utilities are built separately. See particular +`*Converter` class documentation, @ref building, @ref building-plugins, +@ref cmake, @ref cmake-plugins and @ref plugins for more information. +*/ + /** @dir Magnum/Text * @brief Namespace @ref Magnum::Text */ diff --git a/doc/plugins.dox b/doc/plugins.dox index d292a630b..aca3b6761 100644 --- a/doc/plugins.dox +++ b/doc/plugins.dox @@ -75,6 +75,9 @@ of given type. Magnum provides these plugin interfaces: - @ref Audio::AbstractImporter --- importers for audio formats. See `*Importer` classes in the @ref Audio namespace for available audio importer plugins. +- @ref ShaderTools::AbstractConverter --- shader conversion, compilation, + optimization and validation. See `*Converter` classes in the + @ref ShaderTools namespace for available shader converter plugins. @section plugins-loading Loading and instantiating plugins diff --git a/doc/snippets/CMakeLists.txt b/doc/snippets/CMakeLists.txt index eced9f7db..c4bdb6433 100644 --- a/doc/snippets/CMakeLists.txt +++ b/doc/snippets/CMakeLists.txt @@ -100,6 +100,13 @@ if(WITH_MESHTOOLS) set_target_properties(snippets-MagnumMeshTools PROPERTIES FOLDER "Magnum/doc/snippets") endif() +if(WITH_SHADERTOOLS) + add_library(snippets-MagnumShaderTools STATIC + MagnumShaderTools.cpp) + target_link_libraries(snippets-MagnumShaderTools PRIVATE MagnumShaderTools) + set_target_properties(snippets-MagnumShaderTools PROPERTIES FOLDER "Magnum/doc/snippets") +endif() + if(WITH_TRADE) add_library(snippets-MagnumTrade STATIC plugins.cpp diff --git a/doc/snippets/MagnumShaderTools.cpp b/doc/snippets/MagnumShaderTools.cpp new file mode 100644 index 000000000..3052b89d2 --- /dev/null +++ b/doc/snippets/MagnumShaderTools.cpp @@ -0,0 +1,95 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020 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 "Magnum/FileCallback.h" +#include "Magnum/ShaderTools/AbstractConverter.h" + +#define DOXYGEN_IGNORE(...) __VA_ARGS__ + +using namespace Magnum; + +int main() { +{ +Containers::Pointer converter; +Containers::Array extract(const std::string&, const std::string&); +/* [AbstractConverter-usage-callbacks] */ +struct Data { + std::unordered_map> files; +} data; + +converter->setInputFileCallback([](const std::string& filename, + InputFileCallbackPolicy policy, Data& data) + -> Containers::Optional> + { + auto found = data.files.find(filename); + + /* Discard the loaded file, if not needed anymore */ + if(policy == InputFileCallbackPolicy::Close) { + if(found != data.files.end()) data.files.erase(found); + return {}; + } + + /* Extract from an archive if not there yet */ + if(found == data.files.end()) found = data.files.emplace( + filename, extract("shaders.zip", filename)).first; + + return Containers::arrayView(found->second); + }, data); + +/* extracted from a ZIP */ +auto result = converter->validateFile(ShaderTools::Stage::Fragment, "ssao.frag"); +/* [AbstractConverter-usage-callbacks] */ +} + +{ +Containers::Pointer converter; +/* [AbstractConverter-setInputFileCallback] */ +converter->setInputFileCallback([](const std::string& filename, + InputFileCallbackPolicy, void*) { + Utility::Resource rs("data"); + return Containers::optional(rs.getRaw(filename)); + }); +/* [AbstractConverter-setInputFileCallback] */ +} + +{ +Containers::Pointer converter; +/* [AbstractConverter-setInputFileCallback-template] */ +const Utility::Resource rs{"data"}; +converter->setInputFileCallback([](const std::string& filename, + InputFileCallbackPolicy, const Utility::Resource& rs) { + return Containers::optional(rs.getRaw(filename)); + }, rs); +/* [AbstractConverter-setInputFileCallback-template] */ +} + +} diff --git a/modules/FindMagnum.cmake b/modules/FindMagnum.cmake index 156c4be58..ce743858e 100644 --- a/modules/FindMagnum.cmake +++ b/modules/FindMagnum.cmake @@ -64,6 +64,7 @@ # Primitives - Primitives library # SceneGraph - SceneGraph library # Shaders - Shaders library +# ShaderTools - ShaderTools library # Text - Text library # TextureTools - TextureTools library # Trade - Trade library @@ -164,6 +165,10 @@ # installation directory # MAGNUM_PLUGINS_[DEBUG|RELEASE]_LIBRARY_INSTALL_DIR - Plugin library # installation directory +# MAGNUM_PLUGINS_SHADERCONVERTER_[DEBUG|RELEASE]_BINARY_INSTALL_DIR - Shader +# converter plugin binary installation directory +# MAGNUM_PLUGINS_SHADERCONVERTER_[DEBUG|RELEASE]_LIBRARY_INSTALL_DIR - Shader +# converter plugin library installation directory # MAGNUM_PLUGINS_FONT_[DEBUG|RELEASE]_BINARY_INSTALL_DIR - Font plugin binary # installation directory # MAGNUM_PLUGINS_FONT_[DEBUG|RELEASE]_LIBRARY_INSTALL_DIR - Font plugin @@ -224,7 +229,7 @@ foreach(_component ${Magnum_FIND_COMPONENTS}) # Unrolling the transitive dependencies here so this doesn't need to be # after resolving inter-component dependencies. Listing also all plugins. - if(_component MATCHES "^(Audio|DebugTools|MeshTools|Primitives|Text|TextureTools|Trade|.+Importer|.+ImageConverter|.+Font)$") + if(_component MATCHES "^(Audio|DebugTools|MeshTools|Primitives|ShaderTools|Text|TextureTools|Trade|.+Importer|.+ImageConverter|.+Font|.+ShaderConverter)$") set(_MAGNUM_${_COMPONENT}_CORRADE_DEPENDENCIES PluginManager) endif() @@ -355,8 +360,8 @@ endif() # Component distinction (listing them explicitly to avoid mistakes with finding # components from other repositories) set(_MAGNUM_LIBRARY_COMPONENT_LIST - Audio DebugTools GL MeshTools Primitives SceneGraph Shaders Text - TextureTools Trade Vk + Audio DebugTools GL MeshTools Primitives SceneGraph Shaders ShaderTools + Text TextureTools Trade Vk AndroidApplication EmscriptenApplication GlfwApplication GlxApplication Sdl2Application XEglApplication WindowlessCglApplication WindowlessEglApplication WindowlessGlxApplication WindowlessIosApplication @@ -470,6 +475,8 @@ 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 ".+ShaderConverter") + list(APPEND _MAGNUM_${_component}_DEPENDENCIES ShaderTools) elseif(_component MATCHES ".+(Importer|ImageConverter|SceneConverter)") list(APPEND _MAGNUM_${_component}_DEPENDENCIES Trade) elseif(_component MATCHES ".+(Font|FontConverter)") @@ -549,6 +556,10 @@ foreach(_component ${Magnum_FIND_COMPONENTS}) string(REPLACE "AudioImporter" "Importer" _MAGNUM_${_COMPONENT}_HEADER_NAME "${_component}") set(_MAGNUM_${_COMPONENT}_INCLUDE_PATH_NAMES ${_MAGNUM_${_COMPONENT}_HEADER_NAME}.h) + # ShaderConverter plugin specific name suffixes + elseif(_component MATCHES ".+ShaderConverter$") + set(_MAGNUM_${_COMPONENT}_PATH_SUFFIX shaderconverters) + # Importer plugin specific name suffixes elseif(_component MATCHES ".+Importer$") set(_MAGNUM_${_COMPONENT}_PATH_SUFFIX importers) @@ -862,6 +873,12 @@ foreach(_component ${Magnum_FIND_COMPONENTS}) set(_MAGNUM_${_COMPONENT}_INCLUDE_PATH_NAMES Cube.h) # No special setup for SceneGraph library + + # ShaderTools library + elseif(_component STREQUAL ShaderTools) + set_property(TARGET Magnum::${_component} APPEND PROPERTY + INTERFACE_LINK_LIBRARIES Corrade::PluginManager) + # No special setup for Shaders library # Text library @@ -1116,6 +1133,10 @@ set(MAGNUM_PLUGINS_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_BINARY_INSTALL_DIR}/magnum- set(MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR ${MAGNUM_LIBRARY_INSTALL_DIR}/magnum-d) set(MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR ${MAGNUM_BINARY_INSTALL_DIR}/magnum) set(MAGNUM_PLUGINS_RELEASE_LIBRARY_INSTALL_DIR ${MAGNUM_LIBRARY_INSTALL_DIR}/magnum) +set(MAGNUM_PLUGINS_SHADERCONVERTER_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_BINARY_INSTALL_DIR}/shaderconverters) +set(MAGNUM_PLUGINS_SHADERCONVERTER_DEBUG_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR}/shaderconverters) +set(MAGNUM_PLUGINS_SHADERCONVERTER_RELEASE_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_LIBRARY_INSTALL_DIR}/shaderconverters) +set(MAGNUM_PLUGINS_SHADERCONVERTER_RELEASE_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR}/shaderconverters) set(MAGNUM_PLUGINS_FONT_DEBUG_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_BINARY_INSTALL_DIR}/fonts) set(MAGNUM_PLUGINS_FONT_DEBUG_LIBRARY_INSTALL_DIR ${MAGNUM_PLUGINS_DEBUG_LIBRARY_INSTALL_DIR}/fonts) set(MAGNUM_PLUGINS_FONT_RELEASE_BINARY_INSTALL_DIR ${MAGNUM_PLUGINS_RELEASE_BINARY_INSTALL_DIR}/fonts) diff --git a/src/Magnum/CMakeLists.txt b/src/Magnum/CMakeLists.txt index 207ae657e..9a9a269f7 100644 --- a/src/Magnum/CMakeLists.txt +++ b/src/Magnum/CMakeLists.txt @@ -215,6 +215,10 @@ if(WITH_SHADERS) add_subdirectory(Shaders) endif() +if(WITH_SHADERTOOLS) + add_subdirectory(ShaderTools) +endif() + if(WITH_TEXT) add_subdirectory(Text) endif() diff --git a/src/Magnum/ShaderTools/AbstractConverter.cpp b/src/Magnum/ShaderTools/AbstractConverter.cpp new file mode 100644 index 000000000..35fa24c2d --- /dev/null +++ b/src/Magnum/ShaderTools/AbstractConverter.cpp @@ -0,0 +1,429 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020 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 "AbstractConverter.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "Magnum/FileCallback.h" + +#ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT +#include "Magnum/ShaderTools/configure.h" +#endif + +namespace Magnum { namespace ShaderTools { + +std::string AbstractConverter::pluginInterface() { + return +/* [interface] */ +"cz.mosra.magnum.ShaderTools.AbstractConverter/0.1" +/* [interface] */ + ; +} + +#ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT +std::vector AbstractConverter::pluginSearchPaths() { + return PluginManager::implicitPluginSearchPaths( + #ifndef MAGNUM_BUILD_STATIC + Utility::Directory::libraryLocation(&pluginInterface), + #else + {}, + #endif + #ifdef CORRADE_IS_DEBUG_BUILD + MAGNUM_PLUGINS_SHADERCONVERTER_DEBUG_DIR, + #else + MAGNUM_PLUGINS_SHADERCONVERTER_DIR, + #endif + #ifdef CORRADE_IS_DEBUG_BUILD + "magnum-d/" + #else + "magnum/" + #endif + "shaderconverters"); +} +#endif + +AbstractConverter::AbstractConverter() = default; + +AbstractConverter::AbstractConverter(PluginManager::Manager& manager): PluginManager::AbstractManagingPlugin{manager} {} + +AbstractConverter::AbstractConverter(PluginManager::AbstractManager& manager, const std::string& plugin): PluginManager::AbstractManagingPlugin{manager, plugin} {} + +ConverterFeatures AbstractConverter::features() const { + const ConverterFeatures features = doFeatures(); + CORRADE_ASSERT(features & ~ConverterFeature::InputFileCallback, "ShaderTools::AbstractConverter::features(): implementation reported no features", {}); + return features; +} + +void AbstractConverter::setFlags(const ConverterFlags flags) { + _flags = flags; + doSetFlags(flags); +} + +void AbstractConverter::doSetFlags(ConverterFlags) {} + +void AbstractConverter::setInputFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* const userData) { + /* Clearing the *File bits as those are present in *Data as well and thus + this would pass even if only file conversion/validation is supported, + which is wrong */ + CORRADE_ASSERT(features() & (ConverterFeature::InputFileCallback|ConverterFeature::ValidateData|ConverterFeature::ConvertData) & ~(ConverterFeature::ValidateFile|ConverterFeature::ConvertFile), + "ShaderTools::AbstractConverter::setInputFileCallback(): converter supports neither loading from data nor via callbacks, callbacks can't be used", ); + + _inputFileCallback = callback; + _inputFileCallbackUserData = userData; + doSetInputFileCallback(callback, userData); +} + +void AbstractConverter::doSetInputFileCallback(Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*), void*) {} + +std::pair AbstractConverter::validateData(const Stage stage, const Containers::ArrayView data) { + CORRADE_ASSERT(features() & ConverterFeature::ValidateData, + "ShaderTools::AbstractConverter::validateData(): feature not supported", {}); + + /* Cast to a non-void type for more convenience */ + std::pair out = doValidateData(stage, Containers::arrayCast(data)); + CORRADE_ASSERT(out.second.isSmall() || !out.second.deleter(), + "ShaderTools::AbstractConverter::validateData(): implementation is not allowed to use a custom String deleter", {}); + return out; +} + +std::pair AbstractConverter::doValidateData(Stage, Containers::ArrayView) { + CORRADE_ASSERT_UNREACHABLE("ShaderTools::AbstractConverter::validateData(): feature advertised but not implemented", {}); +} + +std::pair AbstractConverter::validateFile(const Stage stage, const Containers::StringView filename) { + CORRADE_ASSERT(features() & (ConverterFeature::ValidateFile|ConverterFeature::ValidateData), + "ShaderTools::AbstractConverter::validateFile(): feature not supported", {}); + + std::pair out; + + /* If input file callbacks are not set or the converter supports handling + them directly, call into the implementation */ + if(!_inputFileCallback || (doFeatures() & ConverterFeature::InputFileCallback)) { + out = doValidateFile(stage, filename); + + /* Otherwise, if validating data is supported, use the callback and pass + the data through to validateData(). Mark the file as ready to be closed + once validating is finished. */ + } else if(doFeatures() & ConverterFeature::ValidateData) { + /* This needs to be duplicated here and in the doValidateFile() + implementation in order to support both following cases: + - plugins that don't support InputFileCallback but have their own + doValidateFile() implementation (callback needs to be used here, + because the base doValidateFile() implementation might never get + called) + - plugins that support InputFileCallback but want to delegate the + actual file loading to the default implementation (callback used + in the base doValidateFile() implementation, because this branch + is never taken in that case) */ + const Containers::Optional> data = _inputFileCallback(filename, InputFileCallbackPolicy::LoadTemporary, _inputFileCallbackUserData); + if(!data) { + Error{} << "ShaderTools::AbstractConverter::validateFile(): cannot open file" << filename; + return {}; + } + out = doValidateData(stage, *data); + _inputFileCallback(filename, InputFileCallbackPolicy::Close, _inputFileCallbackUserData); + + /* Shouldn't get here, the assert is fired already in setFileCallback() */ + } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + + CORRADE_ASSERT(out.second.isSmall() || !out.second.deleter(), + "ShaderTools::AbstractConverter::validateFile(): implementation is not allowed to use a custom String deleter", {}); + return out; +} + +std::pair AbstractConverter::doValidateFile(const Stage stage, const Containers::StringView filename) { + CORRADE_ASSERT(features() >= ConverterFeature::ValidateData, "ShaderTools::AbstractConverter::validateFile(): feature advertised but not implemented", {}); + + /* If callbacks are set, use them. This is the same implementation as in + validateFile(), see the comment there for details. */ + if(_inputFileCallback) { + const Containers::Optional> data = _inputFileCallback(filename, InputFileCallbackPolicy::LoadTemporary, _inputFileCallbackUserData); + if(!data) { + Error{} << "ShaderTools::AbstractConverter::validateFile(): cannot open file" << filename; + return {}; + } + std::pair out = doValidateData(stage, *data); + _inputFileCallback(filename, InputFileCallbackPolicy::Close, _inputFileCallbackUserData); + return out; + + /* Otherwise open the file directly */ + } else { + if(!Utility::Directory::exists(filename)) { + Error() << "ShaderTools::AbstractConverter::validateFile(): cannot open file" << filename; + return {}; + } + + return doValidateData(stage, Utility::Directory::read(filename)); + } +} + +Containers::Array AbstractConverter::convertDataToData(const Stage stage, const Containers::ArrayView data) { + CORRADE_ASSERT(features() >= ConverterFeature::ConvertData, + "ShaderTools::AbstractConverter::convertDataToData(): feature not supported", {}); + + /* Cast to a non-void type for more convenience */ + Containers::Array out = doConvertDataToData(stage, Containers::arrayCast(data)); + CORRADE_ASSERT(!out.deleter(), + "ShaderTools::AbstractConverter::convertDataToData(): implementation is not allowed to use a custom Array deleter", {}); + return out; +} + +Containers::Array AbstractConverter::doConvertDataToData(Stage, Containers::ArrayView) { + CORRADE_ASSERT_UNREACHABLE("ShaderTools::AbstractConverter::convertDataToData(): feature advertised but not implemented", {}); +} + +bool AbstractConverter::convertDataToFile(const Stage stage, const Containers::ArrayView data, const Containers::StringView to) { + CORRADE_ASSERT(features() >= ConverterFeature::ConvertData, + "ShaderTools::AbstractConverter::convertDataToFile(): feature not supported", {}); + + /** @todo this needs expansion once output callbacks are supported as well */ + + /* Cast to a non-void type for more convenience */ + Containers::Array out = doConvertDataToData(stage, Containers::arrayCast(data)); + if(!out) return false; + + if(!Utility::Directory::write(to, out)) { + Error{} << "ShaderTools::AbstractConverter::convertDataToFile(): cannot write to file" << to; + return false; + } + + return true; +} + +Containers::Array AbstractConverter::convertDataToDataUsingInputFileCallbacks(const char* const prefix, const Stage stage, const Containers::StringView from) { + const Containers::Optional> data = _inputFileCallback(from, InputFileCallbackPolicy::LoadTemporary, _inputFileCallbackUserData); + if(!data) { + Error{} << prefix << "cannot open file" << from; + return {}; + } + Containers::Array out = doConvertDataToData(stage, *data); + _inputFileCallback(from, InputFileCallbackPolicy::Close, _inputFileCallbackUserData); + return out; +} + +bool AbstractConverter::convertFileToFile(const Stage stage, const Containers::StringView from, const Containers::StringView to) { + CORRADE_ASSERT(features() & (ConverterFeature::ConvertFile|ConverterFeature::ConvertData), + "ShaderTools::AbstractConverter::convertFileToFile(): feature not supported", {}); + + /** @todo this needs expansion once output callbacks are supported as well */ + + /* If input file callbacks are not set or the converter supports handling + them directly, call into the implementation */ + if(!_inputFileCallback || (doFeatures() & ConverterFeature::InputFileCallback)) { + return doConvertFileToFile(stage, from, to); + + /* Otherwise, if converting data is supported, use the callback and pass + the data through to convertDataToData(). Mark the file as ready to be + closed once conversion is finished. */ + } else if(doFeatures() & ConverterFeature::ConvertData) { + /* This needs to be duplicated here and in the doConvertFileToFile() + implementation in order to support both following cases: + - plugins that don't support InputFileCallback but have their own + doConvertFileToFile() implementation (callback needs to be used + here, because the base doConvertFileToFile() implementation might + never get called) + - plugins that support InputFileCallback but want to delegate the + actual file loading to the default implementation (callback used + in the base doConvertFileToFile() implementation, because this + branch is never taken in that case) */ + Containers::Array out = convertDataToDataUsingInputFileCallbacks("ShaderTools::AbstractConverter::convertFileToFile():", stage, from); + if(!out) return false; + + if(!Utility::Directory::write(to, out)) { + Error{} << "ShaderTools::AbstractConverter::convertFileToFile(): cannot write to file" << to; + return false; + } + + return true; + + /* Shouldn't get here, the assert is fired already in setFileCallback() */ + } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +bool AbstractConverter::doConvertFileToFile(const Stage stage, const Containers::StringView from, const Containers::StringView to) { + CORRADE_ASSERT(features() >= ConverterFeature::ConvertData, "ShaderTools::AbstractConverter::convertFileToFile(): feature advertised but not implemented", {}); + + /** @todo this needs expansion once output callbacks are supported as well */ + Containers::Array out; + + /* If callbacks are set, use them. This is the same implementation as in + convertFileToFile(), see the comment there for details. */ + if(_inputFileCallback) { + out = convertDataToDataUsingInputFileCallbacks("ShaderTools::AbstractConverter::convertFileToFile():", stage, from); + + /* Otherwise open the file directly */ + } else { + if(!Utility::Directory::exists(from)) { + Error() << "ShaderTools::AbstractConverter::convertFileToFile(): cannot open file" << from; + return {}; + } + + out = doConvertDataToData(stage, Utility::Directory::read(from)); + } + + if(!out) return false; + + if(!Utility::Directory::write(to, out)) { + Error{} << "ShaderTools::AbstractConverter::convertFileToFile(): cannot write to file" << to; + return false; + } + + return true; +} + +Containers::Array AbstractConverter::convertFileToData(const Stage stage, const Containers::StringView from) { + CORRADE_ASSERT(features() >= ConverterFeature::ConvertData, + "ShaderTools::AbstractConverter::convertFileToData(): feature not supported", {}); + + Containers::Array out; + + /* If input file callbacks are not set or the converter supports handling + them directly, call into the implementation */ + if(!_inputFileCallback || (doFeatures() & ConverterFeature::InputFileCallback)) { + out = doConvertFileToData(stage, from); + + /* Otherwise use the callback and pass the data through to + convertDataToData(). Mark the file as ready to be closed once conversion + is finished. */ + } else { + /* This needs to be duplicated here and in the doConvertFileToData() + implementation in order to support both following cases: + - plugins that don't support InputFileCallback but have their own + doConvertFileToData() implementation (callback needs to be used + here, because the base doConvertFileToData() implementation might + never get called) + - plugins that support InputFileCallback but want to delegate the + actual file loading to the default implementation (callback used + in the base doConvertFileToData() implementation, because this + branch is never taken in that case) */ + out = convertDataToDataUsingInputFileCallbacks("ShaderTools::AbstractConverter::convertFileToData():", stage, from); + } + + CORRADE_ASSERT(!out.deleter(), + "ShaderTools::AbstractConverter::convertFileToData(): implementation is not allowed to use a custom Array deleter", {}); + return out; +} + +Containers::Array AbstractConverter::doConvertFileToData(const Stage stage, const Containers::StringView from) { + /* If callbacks are set, use them. This is the same implementation as in + convertFileToFile(), see the comment there for details. */ + if(_inputFileCallback) { + return convertDataToDataUsingInputFileCallbacks("ShaderTools::AbstractConverter::convertFileToData():", stage, from); + + /* Otherwise open the file directly */ + } else { + if(!Utility::Directory::exists(from)) { + Error() << "ShaderTools::AbstractConverter::convertFileToData(): cannot open file" << from; + return {}; + } + + return doConvertDataToData(stage, Utility::Directory::read(from)); + } +} + +Debug& operator<<(Debug& debug, const ConverterFeature value) { + debug << "ShaderTools::ConverterFeature" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case ConverterFeature::v: return debug << "::" #v; + _c(ValidateData) + _c(ValidateFile) + _c(ConvertData) + _c(ConvertFile) + _c(InputFileCallback) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedByte(value)) << Debug::nospace << ")"; +} + +Debug& operator<<(Debug& debug, const ConverterFeatures value) { + return Containers::enumSetDebugOutput(debug, value, "ShaderTools::ConverterFeatures{}", { + ConverterFeature::ValidateData, + /* Implied by ValidateData, has to be after */ + ConverterFeature::ValidateFile, + ConverterFeature::ConvertData, + /* Implied by ConvertData, has to be after */ + ConverterFeature::ConvertFile, + ConverterFeature::InputFileCallback}); +} + +Debug& operator<<(Debug& debug, const ConverterFlag value) { + debug << "ShaderTools::ConverterFlag" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case ConverterFlag::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 ConverterFlags value) { + return Containers::enumSetDebugOutput(debug, value, "ShaderTools::ConverterFlags{}", { + ConverterFlag::Verbose}); +} + +Debug& operator<<(Debug& debug, const Stage value) { + debug << "ShaderTools::Stage" << Debug::nospace; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(v) case Stage::v: return debug << "::" #v; + _c(Unspecified) + _c(Vertex) + _c(Fragment) + _c(Geometry) + _c(TessellationControl) + _c(TessellationEvaluation) + _c(Compute) + _c(RayGeneration) + _c(RayAnyHit) + _c(RayClosestHit) + _c(RayMiss) + _c(RayIntersection) + _c(RayCallable) + _c(MeshTask) + _c(Mesh) + #undef _c + /* LCOV_EXCL_STOP */ + } + + return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedByte(value)) << Debug::nospace << ")"; +} + +}} diff --git a/src/Magnum/ShaderTools/AbstractConverter.h b/src/Magnum/ShaderTools/AbstractConverter.h new file mode 100644 index 000000000..ee7900356 --- /dev/null +++ b/src/Magnum/ShaderTools/AbstractConverter.h @@ -0,0 +1,651 @@ +#ifndef Magnum_ShaderTools_AbstractConverter_h +#define Magnum_ShaderTools_AbstractConverter_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020 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::ShaderTools::AbstractConverter, enum @ref Magnum::ShaderTools::ConverterFeature, @ref Magnum::ShaderTools::ConverterFlag, @ref Magnum::ShaderTools::Stage, enum set @ref Magnum::ShaderTools::ConverterFeatures, @ref Magnum::ShaderTools::ConverterFlags + * @m_since_latest + */ + +#include +#include + +#include "Magnum/Magnum.h" +#include "Magnum/ShaderTools/visibility.h" + +namespace Magnum { namespace ShaderTools { + +/** +@brief Features supported by a shader converter +@m_since_latest + +@see @ref ConverterFeatures, @ref AbstractConverter::features() +*/ +enum class ConverterFeature: UnsignedInt { + /** + * Validate shader file with @ref AbstractConverter::validateFile() + */ + ValidateFile = 1 << 0, + + /** + * Validate shader data with @ref AbstractConverter::validateData(). + * Implies @ref ConverterFeature::ValidateData. + */ + ValidateData = ValidateFile|(1 << 1), + + /** + * Convert shader file to a file with @ref AbstractConverter::convertFileToFile() + */ + ConvertFile = 1 << 2, + + /** + * Convert shader data to data with + * @ref AbstractConverter::convertDataToData() or any of the other + * @ref AbstractConverter::convertDataToFile(), + * @ref AbstractConverter::convertFileToData() combinations. Implies + * @ref ConverterFeature::ConvertFile. + */ + ConvertData = ConvertFile|(1 << 3), + + /** + * Specifying input file callbacks for additional files referenced from the + * main file using @ref AbstractConverter::setInputFileCallback(). If the + * converter doesn't expose this feature, the format is either single-file + * or input file callbacks are not supported. + * + * See @ref ShaderTools-AbstractConverter-usage-callbacks and particular + * converter documentation for more information. + */ + InputFileCallback = 1 << 4 +}; + +/** +@brief Features supported by a shader converter +@m_since_latest + +@see @ref AbstractConverter::features() +*/ +typedef Containers::EnumSet ConverterFeatures; + +CORRADE_ENUMSET_OPERATORS(ConverterFeatures) + +/** +@debugoperatorenum{ConverterFeature} +@m_since_latest +*/ +MAGNUM_SHADERTOOLS_EXPORT Debug& operator<<(Debug& debug, ConverterFeature value); + +/** +@debugoperatorenum{ConverterFeatures} +@m_since_latest +*/ +MAGNUM_SHADERTOOLS_EXPORT Debug& operator<<(Debug& debug, ConverterFeatures value); + +/** +@brief Shader converter flag +@m_since_latest + +@see @ref ConverterFlags, @ref AbstractConverter::setFlags() +*/ +enum class ConverterFlag: UnsignedInt { + /** + * Print verbose diagnostic. By default the converter only prints warnings + * and errors. + */ + Verbose = 1 << 0 +}; + +/** +@brief Shader converter flags +@m_since_latest + +@see @ref AbstractConverter::setFlags() +*/ +typedef Containers::EnumSet ConverterFlags; + +CORRADE_ENUMSET_OPERATORS(ConverterFlags) + +/** +@debugoperatorenum{ConverterFlag} +@m_since_latest +*/ +MAGNUM_SHADERTOOLS_EXPORT Debug& operator<<(Debug& debug, ConverterFlag value); + +/** +@debugoperatorenum{ConverterFlags} +@m_since_latest +*/ +MAGNUM_SHADERTOOLS_EXPORT Debug& operator<<(Debug& debug, ConverterFlags value); + +/** +@brief Shader stage +@m_since_latest + +@see @ref AbstractConverter +*/ +enum class Stage: UnsignedInt { + /** + * Unspecified stage. When used in the + * @ref AbstractConverter::validateFile(), + * @ref AbstractConverter::convertFileToFile() "convertFileToFile()", + * @ref AbstractConverter::convertFileToData() "convertFileToData()" APIs, + * particular plugins may attempt to detect the stage from filename, the + * shader stage might also be encoded directly in certain formats. Leaving + * the stage unspecified might limit validation and conversion + * capabilities, see documentation of a particular converter for concrete + * behavior. + * + * This value is guaranteed to be @cpp 0 @ce, which means you're encouraged + * to simply use @cpp {} @ce in function calls and elsewhere. + */ + Unspecified = 0, + + Vertex, /**< Vertex stage */ + Fragment, /**< Fragment stage */ + Geometry, /**< Geometry stage */ + TessellationControl, /**< Tessellation control stage */ + TessellationEvaluation, /**< Tessellation evaluation stage */ + Compute, /**< Compute stage */ + + RayGeneration, /**< Ray generation stage */ + RayAnyHit, /**< Ray any hit stage */ + RayClosestHit, /**< Ray closest hit stage */ + RayMiss, /**< Ray miss stage */ + RayIntersection, /**< Ray intersection stage */ + RayCallable, /**< Ray callable stage */ + + MeshTask, /**< Mesh task stage */ + Mesh /**< Mesh stage */ +}; + +/** +@debugoperatorenum{Stage} +@m_since_latest +*/ +MAGNUM_SHADERTOOLS_EXPORT Debug& operator<<(Debug& debug, Stage value); + +/** +@brief Base for shader converter plugins +@m_since_latest + +Provides functionality for validating and converting shader code between +different representations or performing optimizations and other operations on +them. See @ref plugins for more information and `*ShaderConverter` classes in +the @ref ShaderTools namespace for available scene converter plugins. + +@section ShaderTools-AbstractConverter-usage Usage + +Shader converters are most commonly implemented as plugins. Depending on +exposed @ref features(), a plugin can support shader validation, conversion or +linking. + +@subsection ShaderTools-AbstractConverter-usage-validation Shader validation + +@subsection ShaderTools-AbstractConverter-usage-conversion Shader conversion + +@subsection ShaderTools-AbstractConverter-usage-callbacks Loading shaders from memory, using file callbacks + +Besides loading shaders directly from the filesystem using @ref validateFile() +/ @ref convertFileToFile() like shown above, it's possible to use +@ref validateData(), @ref convertDataToData() and variants to load data from +memory. Note that the particular converter implementation has to support +@ref ConverterFeature::ValidateData / @ref ConverterFeature::ConvertData for +this method to work. + +Textual shader sources sometimes @cpp #include @ce other sources and in that +case you may want to intercept those references and load them in a custom way +as well. For converters that advertise support for this with +@ref ConverterFeature::InputFileCallback this is done by specifying an input +file callback using @ref setInputFileCallback(). The callback gets a filename, +@ref InputFileCallbackPolicy and an user pointer as parameters; returns a +non-owning view on the loaded data or a +@ref Corrade::Containers::NullOpt "Containers::NullOpt" to indicate the file +loading failed. For example, validating a shader from compiled-in resources +could look like below. Note that the input file callback affects +@ref validateFile() / @ref convertFileToFile() / @ref convertFileToData() as +well --- you don't have to load the top-level file manually and pass it to +@ref validateData() / @ref convertDataToData(), any converter supporting the +callback feature handles that correctly. + +@snippet MagnumShaderTools.cpp AbstractConverter-usage-callbacks + +For converters that don't support @ref ConverterFeature::InputFileCallback +directly, the base @ref validateFile() / @ref convertFileToFile() / +@ref convertFileToData() implementations will use the file callback to pass +the loaded data through to @ref validateData() / @ref convertDataToData(), in +case the converter supports at least @ref ConverterFeature::ValidateData +/ @ref ConverterFeature::ConvertData. If the converter supports none of +@ref ConverterFeature::InputFileCallback, @ref ConverterFeature::ValidateData +or @ref ConverterFeature::ConvertData, @ref setInputFileCallback() doesn't +allow the callbacks to be set. + +The input file callback signature is the same for +@ref ShaderTools::AbstractConverter, @ref Trade::AbstractImporter and +@ref Text::AbstractFont to allow code reuse. + +@section ShaderTools-AbstractConverter-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 returned data valid --- all returned +@ref Corrade::Containers::String and @ref Corrade::Containers::Array instances +are only allowed to have default deleters and this is to avoid potential +dangling function pointer calls when destructing such instances after the +plugin module has been unloaded. + +@section ShaderTools-AbstractConverter-subclassing Subclassing + +The plugin needs to implement the @ref doFeatures() function and one or more of +@ref doValidateData(), @ref doValidateFile(), @ref doConvertDataToData(), +@ref doConvertFileToData(), or @ref doConvertFileToFile() 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 doValidateData() is called only if + @ref ConverterFeature::ValidateData is supported. +- The function @ref doValidateFile() is called only if + @ref ConverterFeature::ValidateFile is supported. +- Functions @ref doConvertDataToData() and @ref doConvertFileToData() are + called only if @ref ConverterFeature::ConvertData is supported. +- The function @ref doConvertFileToFile() is called only if + @ref ConverterFeature::ConvertFile is supported. + +@m_class{m-block m-warning} + +@par Dangling function pointers on plugin unload + As @ref ShaderTools-AbstractConverter-data-dependency "mentioned above", + @ref Corrade::Containers::String and @ref Corrade::Containers::Array + instances returned from plugin implementations are not allowed to use + anything else than the default deleter, 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_SHADERTOOLS_EXPORT AbstractConverter: public PluginManager::AbstractManagingPlugin { + public: + /** + * @brief Plugin interface + * + * @snippet Magnum/ShaderTools/AbstractConverter.cpp interface + */ + static std::string pluginInterface(); + + #ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT + /** + * @brief Plugin search paths + * + * Looks into `magnum/shaderconverters/` or `magnum-d/shaderconverters/` + * next to the dynamic @ref ShaderTools 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 AbstractConverter(); + + /** @brief Constructor with access to plugin manager */ + explicit AbstractConverter(PluginManager::Manager& manager); + + /** @brief Plugin manager constructor */ + explicit AbstractConverter(PluginManager::AbstractManager& manager, const std::string& plugin); + + /** @brief Features supported by this converter */ + ConverterFeatures features() const; + + /** @brief Converter flags */ + ConverterFlags 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 ConverterFlag for more + * information. By default no flags are set. + */ + void setFlags(ConverterFlags flags); + + /** + * @brief Input file callback function + * + * @see @ref ShaderTools-AbstractConverter-usage-callbacks + */ + auto inputFileCallback() const -> Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*) { return _inputFileCallback; } + + /** + * @brief Input file callback user data + * + * @see @ref ShaderTools-AbstractConverter-usage-callbacks + */ + void* inputFileCallbackUserData() const { return _inputFileCallbackUserData; } + + /** + * @brief Set input file callback + * + * In case the converter supports @ref ConverterFeature::InputFileCallback, + * files opened through @ref validateFile(), @ref convertFileToData() + * and @ref convertFileToFile() will be loaded through the provided + * callback. Besides that, all external files referenced by the + * top-level file will be loaded through the callback function as well, + * usually on demand. The callback function gets a filename, + * @ref InputFileCallbackPolicy and the @p userData pointer as input + * and returns a non-owning view on the loaded data as output or a + * @ref Corrade::Containers::NullOpt if loading failed --- because + * empty files might also be valid in some circumstances, + * @cpp nullptr @ce can't be used to indicate a failure. + * + * In case the converter doesn't support + * @ref ConverterFeature::InputFileCallback but supports at least + * @ref ConverterFeature::ValidateData / + * @ref ConverterFeature::ConvertData, a file opened through + * @ref validateFile(), @ref convertFileToData() or + * @ref convertFileToFile() will be internally loaded through the + * provided callback and then passed to @ref validateData() or + * @ref convertDataToData(). First the file is loaded with + * @ref InputFileCallbackPolicy::LoadTemporary passed to the callback, + * then the returned memory view is passed to @ref validateData() / + * @ref convertDataToData() (sidestepping the potential + * @ref validateFile() / @ref convertFileToFile() implementation of + * that particular converter) and after that the callback is called + * again with @ref InputFileCallbackPolicy::Close. In case you need a + * different behavior, use @ref validateData() / + * @ref convertDataToData() directly. + * + * In case @p callback is @cpp nullptr @ce, the current callback (if + * any) is reset. This function expects that the converter supports + * either @ref ConverterFeature::InputFileCallback or at least one of + * @ref ConverterFeature::ValidateData, + * @ref ConverterFeature::ConvertData. If a converter supports neither, + * callbacks can't be used. + * + * Following is an example of setting up an input file callback for + * fetching compiled-in resources from @ref Corrade::Utility::Resource. + * See the overload below for a more convenient type-safe way to pass + * the user data pointer. + * + * @snippet MagnumShaderTools.cpp AbstractConverter-setInputFileCallback + * + * @see @ref ShaderTools-AbstractConverter-usage-callbacks + */ + void setInputFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* userData = nullptr); + + /** + * @brief Set file opening callback + * + * Equivalent to calling the above with a lambda wrapper that casts + * @cpp void* @ce back to @cpp T* @ce and dereferences it in order to + * pass it to @p callback. Example usage --- this reuses an existing + * @ref Corrade::Utility::Resource instance to avoid a potentially slow + * resource group lookup every time: + * + * @snippet MagnumShaderTools.cpp AbstractConverter-setInputFileCallback-template + * + * @see @ref ShaderTools-AbstractConverter-usage-callbacks + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + template void setInputFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, T&), T& userData); + #else + /* Otherwise the user would be forced to use the + operator to convert + a lambda to a function pointer and (besides being weird and + annoying) it's also not portable because it doesn't work on MSVC + 2015 and older versions of MSVC 2017. */ + template void setInputFileCallback(Callback callback, T& userData); + #endif + + /** + * @brief Validate a shader + * + * Available only if @ref ConverterFeature::ValidateData is + * supported. Returns + * + * - @cpp true @ce and an empty string if validation passes without + * warnings, + * - @cpp true @ce and a non-empty string if validation passes with + * warnings, and + * - @cpp false @ce if validation doesn't pass. If an external error + * occurs (for example a referenced file not being found), it may + * also happen that the returned string is empty and a message is + * printed to error output instead. + * + * @see @ref features(), @ref validateFile() + */ + std::pair validateData(Stage stage, Containers::ArrayView data); + + /** + * @brief Validate a shader + * + * Available only if @ref ConverterFeature::ValidateFile or + * @ref ConverterFeature::ValidateData is supported. Returns + * + * - @cpp true @ce and an empty string if validation passes without + * warnings, + * - @cpp true @ce and a non-empty string if validation passes with + * warnings, and + * - @cpp false @ce if validation doesn't pass. If an external error + * occurs (for example when a file cannot be read), it may also + * happen that the returned string is empty and a message is + * printed to error output instead. + * + * @see @ref features(), @ref validateData() + */ + std::pair validateFile(Stage stage, Containers::StringView filename); + + /** + * @brief Convert shader data to a data + * + * Available only if @ref ConverterFeature::ConvertData is supported. + * On failure the function prints an error message and returns + * @cpp nullptr @ce. + * @see @ref features(), @ref convertDataToFile(), + * @ref convertFileToData(), @ref convertFileToFile() + */ + Containers::Array convertDataToData(Stage stage, Containers::ArrayView data); + + /** + * @brief Convert shader data to a file + * + * Available only if @ref ConverterFeature::ConvertData is supported. + * Returns @cpp true @ce on success, prints an error message and + * returns @cpp false @ce otherwise. + * @see @ref features(), @ref convertDataToData(), + * @ref convertFileToData(), @ref convertFileToFile() + */ + bool convertDataToFile(Stage stage, Containers::ArrayView data, Containers::StringView to); + + /** + * @brief Convert shader file to a file + * + * Available only if @ref ConverterFeature::ConvertFile or + * @ref ConverterFeature::ConvertData is supported. Returns + * @cpp true @ce on success, prints an error message and returns + * @cpp false @ce otherwise. + * @see @ref features(), @ref convertFileToData(), + * @ref convertDataToData(), @ref convertDataToFile() + */ + bool convertFileToFile(Stage stage, Containers::StringView from, Containers::StringView to); + + /** + * @brief Convert shader data to a file + * + * Available only if @ref ConverterFeature::ConvertData is supported. + * On failure the function prints an error message and returns + * @cpp nullptr @ce. + * @see @ref features(), @ref convertFileToFile(), + * @ref convertDataToFile(), @ref convertDataToData() + */ + Containers::Array convertFileToData(Stage stage, const Containers::StringView from); + + protected: + /** + * @brief Implementation for @ref validateFile() + * + * If @ref ConverterFeature::ValidateData is supported, default + * implementation opens the file and calls @ref doValidateData() with + * its contents. It is allowed to call this function from your + * @ref doValidateFile() implementation --- in particular, this + * implementation will also correctly handle callbacks set through + * @ref setInputFileCallback(). + * + * This function is not called when file callbacks are set through + * @ref setInputFileCallback() and @ref ConverterFeature::InputFileCallback + * is not supported --- instead, file is loaded though the callback and + * data passed through to @ref doValidateData(). + */ + virtual std::pair doValidateFile(Stage stage, Containers::StringView filename); + + /** + * @brief Implementation for @ref convertFileToFile() + * + * If @ref ConverterFeature::ConvertData is supported, default + * implementation opens the file and calls @ref doConvertDataToData() + * with its contents, then saving the output to a file. It is allowed + * to call this function from your @ref doConvertFileToFile() + * implementation --- in particular, this implementation will also + * correctly handle callbacks set through @ref setInputFileCallback(). + * + * This function is not called when file callbacks are set through + * @ref setInputFileCallback() and @ref ConverterFeature::InputFileCallback + * is not supported --- instead, file is loaded though the callback and + * data passed through to @ref doConvertDataToData(). + */ + virtual bool doConvertFileToFile(Stage stage, Containers::StringView from, Containers::StringView to); + + /** + * @brief Implementation for @ref convertFileToData() + * + * Default implementation opens the file and calls + * @ref doConvertDataToData() with its contents --- you only need to + * implement this if you need to do extra work with file inputs. It is + * allowed to call this function from your @ref doConvertFileToData() + * implementation --- in particular, this implementation will also + * correctly handle callbacks set through @ref setInputFileCallback(). + * + * This function is not called when file callbacks are set through + * @ref setInputFileCallback() and @ref ConverterFeature::InputFileCallback + * is not supported --- instead, file is loaded though the callback and + * data passed through to @ref doConvertDataToData(). + */ + virtual Containers::Array doConvertFileToData(Stage stage, Containers::StringView from); + + private: + /** + * @brief Implementation for @ref features() + * + * Has to be implemented always, the implementation is expected to + * support at least one feature. + */ + virtual ConverterFeatures 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(ConverterFlags flags); + + /** + * @brief Implementation for @ref setInputFileCallback() + * + * Useful when the converter needs to modify some internal state on + * callback setup. Default implementation does nothing and this + * function doesn't need to be implemented --- the callback function + * and user data pointer are available through @ref inputFileCallback() + * and @ref inputFileCallbackUserData(). + */ + virtual void doSetInputFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* userData); + + /** + * @brief Implementation for @ref validateData() + * + * Has to be implemented if @ref ConverterFeature::ValidateData + * is supported. While @ref validateData() uses a @cpp void @ce view in + * order to accept any type, this function gets it cast to + * @cpp char @ce for more convenience. + */ + virtual std::pair doValidateData(Stage stage, Containers::ArrayView data); + + /* Used by convertFileToFile(), doConvertFileToFile(), + convertFileToData() and doConvertFileToData() */ + MAGNUM_SHADERTOOLS_LOCAL Containers::Array convertDataToDataUsingInputFileCallbacks(const char* prefix, const Stage stage, Containers::StringView from); + + /** + * @brief Implementation for @ref convertDataToData() + * + * Has to be implemented if @ref ConverterFeature::ConvertData is + * supported. While @ref convertDataToData() uses a @cpp void @ce view + * in order to accept any type, this function gets it cast to + * @cpp char @ce for more convenience. + */ + virtual Containers::Array doConvertDataToData(Stage stage, Containers::ArrayView data); + + ConverterFlags _flags; + + Containers::Optional>(*_inputFileCallback)(const std::string&, InputFileCallbackPolicy, void*){}; + void* _inputFileCallbackUserData{}; + + /* Used by the templated version only */ + struct FileCallbackTemplate { + void(*callback)(); + const void* userData; + /* GCC 4.8 complains loudly about missing initializers otherwise */ + } _inputFileCallbackTemplate{nullptr, nullptr}; +}; + +#ifndef DOXYGEN_GENERATING_OUTPUT +template void AbstractConverter::setInputFileCallback(Callback callback, T& userData) { + /* Don't try to wrap a null function pointer. Need to cast first because + MSVC (even 2017) can't apply ! to a lambda. Ugh. */ + const auto callbackPtr = static_cast>(*)(const std::string&, InputFileCallbackPolicy, T&)>(callback); + if(!callbackPtr) return setInputFileCallback(nullptr); + + _inputFileCallbackTemplate = { reinterpret_cast(callbackPtr), static_cast(&userData) }; + setInputFileCallback([](const std::string& filename, const InputFileCallbackPolicy flags, void* const userData) { + auto& s = *reinterpret_cast(userData); + return reinterpret_cast>(*)(const std::string&, InputFileCallbackPolicy, T&)>(s.callback)(filename, flags, *static_cast(const_cast(s.userData))); + }, &_inputFileCallbackTemplate); +} +#endif + +}} + +#endif diff --git a/src/Magnum/ShaderTools/CMakeLists.txt b/src/Magnum/ShaderTools/CMakeLists.txt new file mode 100644 index 000000000..2553f8e4b --- /dev/null +++ b/src/Magnum/ShaderTools/CMakeLists.txt @@ -0,0 +1,102 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020 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) + +# Files shared between main library and unit test library +set(MagnumShaderTools_SRCS ) + +# Files compiled with different flags for main library and unit test library +set(MagnumShaderTools_GracefulAssert_SRCS + AbstractConverter.cpp) + +set(MagnumShaderTools_HEADERS + AbstractConverter.h + ShaderTools.h + + visibility.h) + +if(NOT CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h) +endif() + +# # Objects shared between main and test library +# add_library(MagnumShaderToolsObjects OBJECT +# ${MagnumShaderTools_SRCS} +# ${MagnumShaderTools_HEADERS}) +# target_include_directories(MagnumShaderToolsObjects PUBLIC $) +# if(NOT BUILD_STATIC) +# target_compile_definitions(MagnumShaderToolsObjects PRIVATE "MagnumShaderToolsObjects_EXPORTS") +# endif() +# if(NOT BUILD_STATIC OR BUILD_STATIC_PIC) +# set_target_properties(MagnumShaderToolsObjects PROPERTIES POSITION_INDEPENDENT_CODE ON) +# endif() +# set_target_properties(MagnumShaderToolsObjects PROPERTIES FOLDER "Magnum/ShaderTools") + +# Main ShaderTools library +add_library(MagnumShaderTools ${SHARED_OR_STATIC} +# $ + ${MagnumShaderTools_GracefulAssert_SRCS}) +set_target_properties(MagnumShaderTools PROPERTIES + DEBUG_POSTFIX "-d" + FOLDER "Magnum/ShaderTools") +if(NOT BUILD_STATIC) + set_target_properties(MagnumShaderTools PROPERTIES VERSION ${MAGNUM_LIBRARY_VERSION} SOVERSION ${MAGNUM_LIBRARY_SOVERSION}) +elseif(BUILD_STATIC_PIC) + set_target_properties(MagnumShaderTools PROPERTIES POSITION_INDEPENDENT_CODE ON) +endif() +target_link_libraries(MagnumShaderTools PUBLIC + Magnum + Corrade::PluginManager) + +install(TARGETS MagnumShaderTools + RUNTIME DESTINATION ${MAGNUM_BINARY_INSTALL_DIR} + LIBRARY DESTINATION ${MAGNUM_LIBRARY_INSTALL_DIR} + ARCHIVE DESTINATION ${MAGNUM_LIBRARY_INSTALL_DIR}) +install(FILES ${MagnumShaderTools_HEADERS} DESTINATION ${MAGNUM_INCLUDE_INSTALL_DIR}/ShaderTools) + +if(BUILD_TESTS) + # Library with graceful assert for testing + add_library(MagnumShaderToolsTestLib ${SHARED_OR_STATIC} +# $ + ${MagnumShaderTools_GracefulAssert_SRCS}) + set_target_properties(MagnumShaderToolsTestLib PROPERTIES + DEBUG_POSTFIX "-d" + FOLDER "Magnum/ShaderTools") + target_compile_definitions(MagnumShaderToolsTestLib PRIVATE + "CORRADE_GRACEFUL_ASSERT" "MagnumShaderTools_EXPORTS") + if(BUILD_STATIC_PIC) + set_target_properties(MagnumShaderToolsTestLib PROPERTIES POSITION_INDEPENDENT_CODE ON) + endif() + target_link_libraries(MagnumShaderToolsTestLib PUBLIC + Magnum + Corrade::PluginManager) + + add_subdirectory(Test) +endif() + +# Magnum ShaderTools target alias for superprojects +add_library(Magnum::ShaderTools ALIAS MagnumShaderTools) diff --git a/src/Magnum/ShaderTools/ShaderTools.h b/src/Magnum/ShaderTools/ShaderTools.h new file mode 100644 index 000000000..1272bf342 --- /dev/null +++ b/src/Magnum/ShaderTools/ShaderTools.h @@ -0,0 +1,40 @@ +#ifndef Magnum_Trade_ShaderTools_h +#define Magnum_Trade_ShaderTools_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020 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 Forward declarations for the @ref Magnum::ShaderTools namespace + */ + +namespace Magnum { namespace Trade { + +#ifndef DOXYGEN_GENERATING_OUTPUT +class AbstractConverter; +#endif + +}} + +#endif diff --git a/src/Magnum/ShaderTools/Test/AbstractConverterTest.cpp b/src/Magnum/ShaderTools/Test/AbstractConverterTest.cpp new file mode 100644 index 000000000..b5a946602 --- /dev/null +++ b/src/Magnum/ShaderTools/Test/AbstractConverterTest.cpp @@ -0,0 +1,1572 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020 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 +#include +#include + +#include "Magnum/FileCallback.h" +#include "Magnum/ShaderTools/AbstractConverter.h" + +#include "configure.h" + +namespace Magnum { namespace ShaderTools { namespace Test { namespace { + +struct AbstractConverterTest: TestSuite::Tester { + explicit AbstractConverterTest(); + + void featuresNone(); + + void setFlags(); + void setFlagsNotImplemented(); + + void validateData(); + void validateDataNotSupported(); + void validateDataNotImplemented(); + void validateDataCustomStringDeleter(); + + void validateFile(); + void validateFileAsData(); + void validateFileAsDataNotFound(); + void validateFileNotSupported(); + void validateFileNotImplemented(); + void validateFileCustomStringDeleter(); + + void convertDataToData(); + void convertDataToDataNotSupported(); + void convertDataToDataNotImplemented(); + void convertDataToDataCustomDeleter(); + void convertDataToFileThroughData(); + void convertDataToFileThroughDataFailed(); + void convertDataToFileThroughDataNotWritable(); + void convertDataToFileNotSupported(); + void convertDataToFileNotImplemented(); + + void convertFileToFile(); + void convertFileToFileThroughData(); + void convertFileToFileThroughDataNotFound(); + void convertFileToFileThroughDataFailed(); + void convertFileToFileThroughDataNotWritable(); + void convertFileToFileNotSupported(); + void convertFileToFileNotImplemented(); + void convertFileToData(); + void convertFileToDataAsData(); + void convertFileToDataAsDataNotFound(); + void convertFileToDataNotSupported(); + void convertFileToDataNotImplemented(); + void convertFileToDataCustomDeleter(); + + void setInputFileCallback(); + void setInputFileCallbackTemplate(); + void setInputFileCallbackTemplateNull(); + void setInputFileCallbackTemplateConst(); + void setInputFileCallbackNotImplemented(); + void setInputFileCallbackNotSupported(); + + void setInputFileCallbackValidateFileDirectly(); + void setInputFileCallbackValidateFileThroughBaseImplementation(); + void setInputFileCallbackValidateFileThroughBaseImplementationFailed(); + void setInputFileCallbackValidateFileAsData(); + void setInputFileCallbackValidateFileAsDataFailed(); + + void setInputFileCallbackConvertFileToFileDirectly(); + void setInputFileCallbackConvertFileToFileThroughBaseImplementation(); + void setInputFileCallbackConvertFileToFileThroughBaseImplementationFailed(); + void setInputFileCallbackConvertFileToFileAsData(); + void setInputFileCallbackConvertFileToFileAsDataFailed(); + void setInputFileCallbackConvertFileToFileAsDataNotWritable(); + + void setInputFileCallbackConvertFileToDataDirectly(); + void setInputFileCallbackConvertFileToDataThroughBaseImplementation(); + void setInputFileCallbackConvertFileToDataThroughBaseImplementationFailed(); + void setInputFileCallbackConvertFileToDataAsData(); + void setInputFileCallbackConvertFileToDataAsDataFailed(); + + void debugFeature(); + void debugFeatures(); + void debugFlag(); + void debugFlags(); + void debugStage(); +}; + +AbstractConverterTest::AbstractConverterTest() { + addTests({&AbstractConverterTest::featuresNone, + + &AbstractConverterTest::setFlags, + &AbstractConverterTest::setFlagsNotImplemented, + + &AbstractConverterTest::validateData, + &AbstractConverterTest::validateDataNotSupported, + &AbstractConverterTest::validateDataNotImplemented, + &AbstractConverterTest::validateDataCustomStringDeleter, + + &AbstractConverterTest::validateFile, + &AbstractConverterTest::validateFileAsData, + &AbstractConverterTest::validateFileAsDataNotFound, + &AbstractConverterTest::validateFileNotSupported, + &AbstractConverterTest::validateFileNotImplemented, + &AbstractConverterTest::validateFileCustomStringDeleter, + + &AbstractConverterTest::convertDataToData, + &AbstractConverterTest::convertDataToDataNotSupported, + &AbstractConverterTest::convertDataToDataNotImplemented, + &AbstractConverterTest::convertDataToDataCustomDeleter, + &AbstractConverterTest::convertDataToFileThroughData, + &AbstractConverterTest::convertDataToFileThroughDataFailed, + &AbstractConverterTest::convertDataToFileThroughDataNotWritable, + &AbstractConverterTest::convertDataToFileNotSupported, + &AbstractConverterTest::convertDataToFileNotImplemented, + + &AbstractConverterTest::convertFileToFile, + &AbstractConverterTest::convertFileToFileThroughData, + &AbstractConverterTest::convertFileToFileThroughDataNotFound, + &AbstractConverterTest::convertFileToFileThroughDataFailed, + &AbstractConverterTest::convertFileToFileThroughDataNotWritable, + &AbstractConverterTest::convertFileToFileNotSupported, + &AbstractConverterTest::convertFileToFileNotImplemented, + &AbstractConverterTest::convertFileToData, + &AbstractConverterTest::convertFileToDataAsData, + &AbstractConverterTest::convertFileToDataAsDataNotFound, + &AbstractConverterTest::convertFileToDataNotSupported, + &AbstractConverterTest::convertFileToDataNotImplemented, + &AbstractConverterTest::convertFileToDataCustomDeleter, + + &AbstractConverterTest::setInputFileCallback, + &AbstractConverterTest::setInputFileCallbackTemplate, + &AbstractConverterTest::setInputFileCallbackTemplateNull, + &AbstractConverterTest::setInputFileCallbackTemplateConst, + &AbstractConverterTest::setInputFileCallbackNotImplemented, + &AbstractConverterTest::setInputFileCallbackNotSupported, + + &AbstractConverterTest::setInputFileCallbackValidateFileDirectly, + &AbstractConverterTest::setInputFileCallbackValidateFileThroughBaseImplementation, + &AbstractConverterTest::setInputFileCallbackValidateFileThroughBaseImplementationFailed, + &AbstractConverterTest::setInputFileCallbackValidateFileAsData, + &AbstractConverterTest::setInputFileCallbackValidateFileAsDataFailed, + + &AbstractConverterTest::setInputFileCallbackConvertFileToFileDirectly, + &AbstractConverterTest::setInputFileCallbackConvertFileToFileThroughBaseImplementation, + &AbstractConverterTest::setInputFileCallbackConvertFileToFileThroughBaseImplementationFailed, + &AbstractConverterTest::setInputFileCallbackConvertFileToFileAsData, + &AbstractConverterTest::setInputFileCallbackConvertFileToFileAsDataFailed, + &AbstractConverterTest::setInputFileCallbackConvertFileToFileAsDataNotWritable, + + &AbstractConverterTest::setInputFileCallbackConvertFileToDataDirectly, + &AbstractConverterTest::setInputFileCallbackConvertFileToDataThroughBaseImplementation, + &AbstractConverterTest::setInputFileCallbackConvertFileToDataThroughBaseImplementationFailed, + &AbstractConverterTest::setInputFileCallbackConvertFileToDataAsData, + &AbstractConverterTest::setInputFileCallbackConvertFileToDataAsDataFailed, + + &AbstractConverterTest::debugFeature, + &AbstractConverterTest::debugFeatures, + &AbstractConverterTest::debugFlag, + &AbstractConverterTest::debugFlags, + &AbstractConverterTest::debugStage}); + + /* Create testing dir */ + Utility::Directory::mkpath(SHADERTOOLS_TEST_OUTPUT_DIR); +} + +void AbstractConverterTest::featuresNone() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + /* These aren't real features, so it should still complain */ + return ConverterFeature::InputFileCallback; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.features(); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::features(): implementation reported no features\n"); +} + +void AbstractConverterTest::setFlags() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + /* Assuming this bit is unused */ + return ConverterFeature(1 << 15); + } + void doSetFlags(ConverterFlags flags) override { + _flags = flags; + } + + ConverterFlags _flags; + } converter; + + CORRADE_COMPARE(converter.flags(), ConverterFlags{}); + CORRADE_COMPARE(converter._flags, ConverterFlags{}); + converter.setFlags(ConverterFlag::Verbose); + CORRADE_COMPARE(converter.flags(), ConverterFlag::Verbose); + CORRADE_COMPARE(converter._flags, ConverterFlag::Verbose); +} + +void AbstractConverterTest::setFlagsNotImplemented() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + /* Assuming this bit is unused */ + return ConverterFeature(1 << 15); + } + } converter; + + CORRADE_COMPARE(converter.flags(), ConverterFlags{}); + converter.setFlags(ConverterFlag::Verbose); + CORRADE_COMPARE(converter.flags(), ConverterFlag::Verbose); + /* Should just work, no need to implement the function */ +} + +void AbstractConverterTest::validateData() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData; + } + + std::pair doValidateData(const Stage stage, const Containers::ArrayView data) override { + return {data.size() == 5*4 && stage == Stage::MeshTask, "Yes, this is valid"}; + } + } converter; + + std::pair out = converter.validateData(Stage::MeshTask, Containers::arrayView({0x07230203, 99, 0xcafebabeu, 50, 0})); + CORRADE_VERIFY(out.first); + CORRADE_COMPARE(out.second, "Yes, this is valid"); +} + +void AbstractConverterTest::validateDataNotSupported() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.validateData({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::validateData(): feature not supported\n"); +} + +void AbstractConverterTest::validateDataNotImplemented() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.validateData({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::validateData(): feature advertised but not implemented\n"); +} + +void AbstractConverterTest::validateDataCustomStringDeleter() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData; + } + + std::pair doValidateData(Stage, const Containers::ArrayView) override { + return {{}, Containers::String{"", 0, [](char*, std::size_t){}}}; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.validateData({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::validateData(): implementation is not allowed to use a custom String deleter\n"); +} + +void AbstractConverterTest::validateFile() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateFile; + } + + std::pair doValidateFile(const Stage stage, const Containers::StringView filename) override { + return {stage == Stage::Vertex && filename.size() == 8, "Yes, this is valid"}; + } + } converter; + + std::pair out = converter.validateFile(Stage::Vertex, "file.spv"); + CORRADE_VERIFY(out.first); + CORRADE_COMPARE(out.second, "Yes, this is valid"); +} + +void AbstractConverterTest::validateFileAsData() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData; + } + + std::pair doValidateData(const Stage stage, const Containers::ArrayView data) override { + return {stage == Stage::Compute && data.size() == 5, "Yes, this is valid"}; + } + } converter; + + std::pair out = converter.validateFile(Stage::Compute, Utility::Directory::join(SHADERTOOLS_TEST_DIR, "file.dat")); + CORRADE_VERIFY(out.first); + CORRADE_COMPARE(out.second, "Yes, this is valid"); +} + +void AbstractConverterTest::validateFileAsDataNotFound() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData; + } + + std::pair doValidateData(Stage, Containers::ArrayView) override { + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + std::pair out2 = converter.validateFile({}, "nonexistent.bin"); + CORRADE_VERIFY(!out2.first); + CORRADE_COMPARE(out2.second, ""); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::validateFile(): cannot open file nonexistent.bin\n"); +} + +void AbstractConverterTest::validateFileNotSupported() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.validateFile({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::validateFile(): feature not supported\n"); +} + +void AbstractConverterTest::validateFileNotImplemented() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateFile; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.validateFile({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::validateFile(): feature advertised but not implemented\n"); +} + +void AbstractConverterTest::validateFileCustomStringDeleter() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData; + } + + std::pair doValidateFile(Stage, Containers::StringView) override { + return {{}, Containers::String{"", 0, [](char*, std::size_t){}}}; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.validateFile({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::validateFile(): implementation is not allowed to use a custom String deleter\n"); +} + +void AbstractConverterTest::convertDataToData() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertDataToData(Stage, Containers::ArrayView data) override { + return Containers::array({data.back(), data.front()}); + } + } converter; + + const char data[] = {'S', 'P', 'I', 'R', 'V'}; + Containers::Array out = converter.convertDataToData({}, data); + CORRADE_COMPARE_AS(out, Containers::arrayView({'V', 'S'}), + TestSuite::Compare::Container); +} + +void AbstractConverterTest::convertDataToDataNotSupported() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertFile; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertDataToData({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertDataToData(): feature not supported\n"); +} + +void AbstractConverterTest::convertDataToDataNotImplemented() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertDataToData({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertDataToData(): feature advertised but not implemented\n"); +} + +void AbstractConverterTest::convertDataToDataCustomDeleter() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + + Containers::Array doConvertDataToData(Stage, Containers::ArrayView) override { + return Containers::Array{nullptr, 0, [](char*, std::size_t){}}; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertDataToData({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertDataToData(): implementation is not allowed to use a custom Array deleter\n"); +} + +void AbstractConverterTest::convertDataToFileThroughData() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertDataToData(Stage, Containers::ArrayView data) override { + return Containers::array({data.back(), data.front()}); + } + } converter; + + const std::string filename = Utility::Directory::join(SHADERTOOLS_TEST_OUTPUT_DIR, "file.dat"); + + /* Remove previous file, if any */ + Utility::Directory::rm(filename); + CORRADE_VERIFY(!Utility::Directory::exists(filename)); + + const char data[] = {'S', 'P', 'I', 'R', 'V'}; + CORRADE_VERIFY(converter.convertDataToFile({}, data, filename)); + CORRADE_COMPARE_AS(filename, "VS", + TestSuite::Compare::FileToString); +} + +void AbstractConverterTest::convertDataToFileThroughDataFailed() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertDataToData(Stage, Containers::ArrayView) override { + return {}; + } + } converter; + + const std::string filename = Utility::Directory::join(SHADERTOOLS_TEST_OUTPUT_DIR, "file.dat"); + + /* Remove previous file, if any */ + Utility::Directory::rm(filename); + CORRADE_VERIFY(!Utility::Directory::exists(filename)); + + /* Function should fail, no file should get written and no error output + should be printed (the base implementation assumes the plugin does it) */ + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter.convertDataToFile({}, {}, filename)); + CORRADE_VERIFY(!Utility::Directory::exists(filename)); + CORRADE_COMPARE(out.str(), ""); +} + +void AbstractConverterTest::convertDataToFileThroughDataNotWritable() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertDataToData(Stage, Containers::ArrayView) override { + return Containers::Array{1}; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter.convertDataToFile({}, nullptr, "/some/path/that/does/not/exist")); + CORRADE_COMPARE(out.str(), + "Utility::Directory::write(): can't open /some/path/that/does/not/exist\n" + "ShaderTools::AbstractConverter::convertDataToFile(): cannot write to file /some/path/that/does/not/exist\n"); +} + +void AbstractConverterTest::convertDataToFileNotSupported() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertFile; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertDataToFile({}, {}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertDataToFile(): feature not supported\n"); +} + +void AbstractConverterTest::convertDataToFileNotImplemented() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertDataToFile({}, {}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertDataToData(): feature advertised but not implemented\n"); +} + +void AbstractConverterTest::convertFileToFile() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertFile; + } + bool doConvertFileToFile(Stage, const Containers::StringView from, const Containers::StringView to) override { + Containers::Array data = Utility::Directory::read(from); + return Utility::Directory::write(to, Containers::array({data.back(), data.front()})); + } + } converter; + + const std::string filename = Utility::Directory::join(SHADERTOOLS_TEST_OUTPUT_DIR, "file.dat"); + + /* Remove previous file, if any */ + Utility::Directory::rm(filename); + CORRADE_VERIFY(!Utility::Directory::exists(filename)); + + CORRADE_VERIFY(converter.convertFileToFile({}, Utility::Directory::join(SHADERTOOLS_TEST_DIR, "file.dat"), filename)); + CORRADE_COMPARE_AS(filename, "VS", + TestSuite::Compare::FileToString); +} + +void AbstractConverterTest::convertFileToFileThroughData() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertDataToData(Stage, Containers::ArrayView data) override { + return Containers::array({data.back(), data.front()}); + } + } converter; + + const std::string filename = Utility::Directory::join(SHADERTOOLS_TEST_OUTPUT_DIR, "file.dat"); + + /* Remove previous file, if any */ + Utility::Directory::rm(filename); + CORRADE_VERIFY(!Utility::Directory::exists(filename)); + + CORRADE_VERIFY(converter.convertFileToFile({}, Utility::Directory::join(SHADERTOOLS_TEST_DIR, "file.dat"), filename)); + CORRADE_COMPARE_AS(filename, "VS", + TestSuite::Compare::FileToString); +} + +void AbstractConverterTest::convertFileToFileThroughDataNotFound() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertDataToData(Stage, Containers::ArrayView) override { + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter.convertFileToFile({}, "nonexistent.bin", "file.dat")); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertFileToFile(): cannot open file nonexistent.bin\n"); +} + +void AbstractConverterTest::convertFileToFileThroughDataFailed() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertDataToData(Stage, Containers::ArrayView) override { + return {}; + } + } converter; + + const std::string filename = Utility::Directory::join(SHADERTOOLS_TEST_OUTPUT_DIR, "file.dat"); + + /* Remove previous file, if any */ + Utility::Directory::rm(filename); + CORRADE_VERIFY(!Utility::Directory::exists(filename)); + + /* Function should fail, no file should get written and no error output + should be printed (the base implementation assumes the plugin does it) */ + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter.convertFileToFile({}, Utility::Directory::join(SHADERTOOLS_TEST_DIR, "file.dat"), filename)); + CORRADE_VERIFY(!Utility::Directory::exists(filename)); + CORRADE_COMPARE(out.str(), ""); +} + +void AbstractConverterTest::convertFileToFileThroughDataNotWritable() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertDataToData(Stage, Containers::ArrayView) override { + return Containers::Array{1}; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter.convertFileToFile({}, Utility::Directory::join(SHADERTOOLS_TEST_DIR, "file.dat"), "/some/path/that/does/not/exist")); + CORRADE_COMPARE(out.str(), + "Utility::Directory::write(): can't open /some/path/that/does/not/exist\n" + "ShaderTools::AbstractConverter::convertFileToFile(): cannot write to file /some/path/that/does/not/exist\n"); +} + +void AbstractConverterTest::convertFileToFileNotSupported() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertFileToFile({}, {}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertFileToFile(): feature not supported\n"); +} + +void AbstractConverterTest::convertFileToFileNotImplemented() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertFile; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertFileToFile({}, {}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertFileToFile(): feature advertised but not implemented\n"); +} + +void AbstractConverterTest::convertFileToData() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertFileToData(Stage, const Containers::StringView from) override { + Containers::Array data = Utility::Directory::read(from); + return Containers::array({data.back(), data.front()}); + } + } converter; + + Containers::Array out = converter.convertFileToData({}, Utility::Directory::join(SHADERTOOLS_TEST_DIR, "file.dat")); + CORRADE_COMPARE_AS(out, Containers::arrayView({'V', 'S'}), + TestSuite::Compare::Container); +} + +void AbstractConverterTest::convertFileToDataAsData() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertDataToData(Stage, Containers::ArrayView data) override { + return Containers::array({data.back(), data.front()}); + } + } converter; + + Containers::Array out = converter.convertFileToData({}, Utility::Directory::join(SHADERTOOLS_TEST_DIR, "file.dat")); + CORRADE_COMPARE_AS(out, Containers::arrayView({'V', 'S'}), + TestSuite::Compare::Container); +} + +void AbstractConverterTest::convertFileToDataAsDataNotFound() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + Containers::Array doConvertDataToData(Stage, Containers::ArrayView) override { + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter.convertFileToData({}, "nonexistent.bin")); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertFileToData(): cannot open file nonexistent.bin\n"); +} + +void AbstractConverterTest::convertFileToDataNotSupported() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertFile; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertFileToData({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertFileToData(): feature not supported\n"); +} + +void AbstractConverterTest::convertFileToDataNotImplemented() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertFileToData({}, Utility::Directory::join(SHADERTOOLS_TEST_DIR, "file.dat")); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertDataToData(): feature advertised but not implemented\n"); +} + +void AbstractConverterTest::convertFileToDataCustomDeleter() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + + Containers::Array doConvertFileToData(Stage, const Containers::StringView) override { + return Containers::Array{nullptr, 0, [](char*, std::size_t){}}; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + converter.convertFileToData({}, {}); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertFileToData(): implementation is not allowed to use a custom Array deleter\n"); +} + +void AbstractConverterTest::setInputFileCallback() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + void doSetInputFileCallback(Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*), void* userData) override { + *static_cast(userData) = 1337; + } + } converter; + + int a = 0; + auto lambda = [](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }; + converter.setInputFileCallback(lambda, &a); + CORRADE_COMPARE(converter.inputFileCallback(), lambda); + CORRADE_COMPARE(converter.inputFileCallbackUserData(), &a); + CORRADE_COMPARE(a, 1337); +} + +void AbstractConverterTest::setInputFileCallbackTemplate() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + void doSetInputFileCallback(Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*), void*) override { + called = true; + } + + bool called = false; + } converter; + + int a = 0; + auto lambda = [](const std::string&, InputFileCallbackPolicy, int&) { + return Containers::Optional>{}; + }; + converter.setInputFileCallback(lambda, a); + CORRADE_VERIFY(converter.inputFileCallback()); + CORRADE_VERIFY(converter.inputFileCallbackUserData()); + CORRADE_VERIFY(converter.called); + + /* The data pointers should be wrapped, thus not the same */ + CORRADE_VERIFY(reinterpret_cast(converter.inputFileCallback()) != reinterpret_cast(static_cast>(*)(const std::string&, InputFileCallbackPolicy, int&)>(lambda))); + CORRADE_VERIFY(converter.inputFileCallbackUserData() != &a); +} + +void AbstractConverterTest::setInputFileCallbackTemplateNull() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + void doSetInputFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* userData) override { + called = !callback && !userData; + } + + bool called = false; + } converter; + + int a = 0; + converter.setInputFileCallback(static_cast>(*)(const std::string&, InputFileCallbackPolicy, int&)>(nullptr), a); + CORRADE_VERIFY(!converter.inputFileCallback()); + CORRADE_VERIFY(!converter.inputFileCallbackUserData()); + CORRADE_VERIFY(converter.called); +} + +void AbstractConverterTest::setInputFileCallbackTemplateConst() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + void doSetInputFileCallback(Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*), void*) override { + called = true; + } + + bool called = false; + } converter; + + /* Just verify we can have const parameters */ + const int a = 0; + auto lambda = [](const std::string&, InputFileCallbackPolicy, const int&) { + return Containers::Optional>{}; + }; + converter.setInputFileCallback(lambda, a); + CORRADE_VERIFY(converter.inputFileCallback()); + CORRADE_VERIFY(converter.inputFileCallbackUserData()); + CORRADE_VERIFY(converter.called); +} + +void AbstractConverterTest::setInputFileCallbackNotImplemented() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + } converter; + + int a; + auto lambda = [](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }; + converter.setInputFileCallback(lambda, &a); + CORRADE_COMPARE(converter.inputFileCallback(), lambda); + CORRADE_COMPARE(converter.inputFileCallbackUserData(), &a); + /* Should just work, no need to implement the function */ +} + +void AbstractConverterTest::setInputFileCallbackNotSupported() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertFile; + } + } converter; + + std::ostringstream out; + Error redirectError{&out}; + + int a; + converter.setInputFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }, &a); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::setInputFileCallback(): converter supports neither loading from data nor via callbacks, callbacks can't be used\n"); +} + +void AbstractConverterTest::setInputFileCallbackValidateFileDirectly() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateFile|ConverterFeature::InputFileCallback; + } + + std::pair doValidateFile(Stage, const Containers::StringView filename) override { + return {filename == "file.dat" && inputFileCallback() && inputFileCallbackUserData(), "it's what it is!"}; + } + + std::pair doValidateData(Stage, Containers::ArrayView) override { + CORRADE_VERIFY(!"this should not be reached"); + return {}; + } + } converter; + + int a{}; + converter.setInputFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + CORRADE_VERIFY(!"this should not be reached"); + return Containers::Optional>{}; + }, &a); + + CORRADE_COMPARE(converter.validateFile({}, "file.dat"), std::make_pair(true, "it's what it is!")); +} + +void AbstractConverterTest::setInputFileCallbackValidateFileThroughBaseImplementation() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData|ConverterFeature::InputFileCallback; + } + + std::pair doValidateFile(Stage stage, const Containers::StringView filename) override { + validateFileCalled = true; + + if(filename != "file.dat" || !inputFileCallback() || !inputFileCallbackUserData()) + return {}; + + return AbstractConverter::doValidateFile(stage, filename); + } + + std::pair doValidateData(Stage stage, Containers::ArrayView data) override { + return {stage == Stage::RayCallable && data.size() == 1 && data[0] == '\xb0', "yep!!"}; + } + + bool validateFileCalled = false; + } converter; + + struct State { + const char data = '\xb0'; + bool loaded = false; + bool closed = false; + } state; + + converter.setInputFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, State& state) -> Containers::Optional> { + if(filename == "file.dat" && policy == InputFileCallbackPolicy::LoadTemporary) { + state.loaded = true; + return Containers::arrayView(&state.data, 1); + } + + if(filename == "file.dat" && policy == InputFileCallbackPolicy::Close) { + state.closed = true; + return {}; + } + + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + }, state); + + CORRADE_COMPARE(converter.validateFile(Stage::RayCallable, "file.dat"), std::make_pair(true, "yep!!")); + CORRADE_VERIFY(converter.validateFileCalled); + CORRADE_VERIFY(state.loaded); + CORRADE_VERIFY(state.closed); +} + +void AbstractConverterTest::setInputFileCallbackValidateFileThroughBaseImplementationFailed() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData|ConverterFeature::InputFileCallback; + } + + std::pair doValidateFile(Stage stage, const Containers::StringView filename) override { + validateFileCalled = true; + return AbstractConverter::doValidateFile(stage, filename); + } + + bool validateFileCalled = false; + } converter; + + converter.setInputFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_COMPARE(converter.validateFile({}, "file.dat"), std::make_pair(false, "")); + CORRADE_VERIFY(converter.validateFileCalled); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::validateFile(): cannot open file file.dat\n"); +} + +void AbstractConverterTest::setInputFileCallbackValidateFileAsData() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData; + } + + std::pair doValidateFile(Stage, const Containers::StringView) override { + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + } + + std::pair doValidateData(Stage stage, Containers::ArrayView data) override { + return {stage == Stage::Fragment && data.size() == 1 && data[0] == '\xb0', "yep!!"}; + } + } converter; + + struct State { + const char data = '\xb0'; + bool loaded = false; + bool closed = false; + } state; + + converter.setInputFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, State& state) -> Containers::Optional> { + if(filename == "file.dat" && policy == InputFileCallbackPolicy::LoadTemporary) { + state.loaded = true; + return Containers::arrayView(&state.data, 1); + } + + if(filename == "file.dat" && policy == InputFileCallbackPolicy::Close) { + state.closed = true; + return {}; + } + + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + }, state); + + CORRADE_COMPARE(converter.validateFile(Stage::Fragment, "file.dat"), std::make_pair(true, "yep!!")); + CORRADE_VERIFY(state.loaded); + CORRADE_VERIFY(state.closed); +} + +void AbstractConverterTest::setInputFileCallbackValidateFileAsDataFailed() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ValidateData; + } + + std::pair doValidateFile(Stage, const Containers::StringView) override { + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + } + } converter; + + converter.setInputFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_COMPARE(converter.validateFile({}, "file.dat"), std::make_pair(false, "")); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::validateFile(): cannot open file file.dat\n"); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToFileDirectly() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertFile|ConverterFeature::InputFileCallback; + } + + bool doConvertFileToFile(Stage stage, const Containers::StringView from, const Containers::StringView to) override { + return stage == Stage::Mesh && from == "file.dat" && to == "file.out" && inputFileCallback() && inputFileCallbackUserData(); + } + + Containers::Array doConvertDataToData(Stage, Containers::ArrayView) override { + CORRADE_VERIFY(!"this should not be reached"); + return {}; + } + } converter; + + int a{}; + converter.setInputFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + CORRADE_VERIFY(!"this should not be reached"); + return Containers::Optional>{}; + }, &a); + + CORRADE_VERIFY(converter.convertFileToFile(Stage::Mesh, "file.dat", "file.out")); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToFileThroughBaseImplementation() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData|ConverterFeature::InputFileCallback; + } + + bool doConvertFileToFile(Stage stage, const Containers::StringView from, const Containers::StringView to) override { + convertFileToFileCalled = true; + + if(stage != Stage::Geometry || from != "file.dat" || !to.hasSuffix("file.out") || !inputFileCallback() || !inputFileCallbackUserData()) + return {}; + + return AbstractConverter::doConvertFileToFile(stage, from, to); + } + + Containers::Array doConvertDataToData(Stage stage, Containers::ArrayView data) override { + if(stage == Stage::Geometry && data.size() == 1 && data[0] == '\xb0') + return Containers::array({'y', 'e', 'p'}); + return {}; + } + + bool convertFileToFileCalled = false; + } converter; + + struct State { + const char data = '\xb0'; + bool loaded = false; + bool closed = false; + } state; + + converter.setInputFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, State& state) -> Containers::Optional> { + if(filename == "file.dat" && policy == InputFileCallbackPolicy::LoadTemporary) { + state.loaded = true; + return Containers::arrayView(&state.data, 1); + } + + if(filename == "file.dat" && policy == InputFileCallbackPolicy::Close) { + state.closed = true; + return {}; + } + + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + }, state); + + /* Remove previous file, if any */ + const std::string filename = Utility::Directory::join(SHADERTOOLS_TEST_OUTPUT_DIR, "file.out"); + Utility::Directory::rm(filename); + CORRADE_VERIFY(!Utility::Directory::exists(filename)); + + CORRADE_VERIFY(converter.convertFileToFile(Stage::Geometry, "file.dat", filename)); + CORRADE_VERIFY(converter.convertFileToFileCalled); + CORRADE_VERIFY(state.loaded); + CORRADE_VERIFY(state.closed); + CORRADE_COMPARE_AS(filename, "yep", TestSuite::Compare::FileToString); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToFileThroughBaseImplementationFailed() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData|ConverterFeature::InputFileCallback; + } + + bool doConvertFileToFile(Stage stage, const Containers::StringView from, const Containers::StringView to) override { + convertFileToFileCalled = true; + return AbstractConverter::doConvertFileToFile(stage, from, to); + } + + bool convertFileToFileCalled = false; + } converter; + + converter.setInputFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!converter.convertFileToFile({}, "file.dat", "/some/path/that/does/not/exist")); + CORRADE_VERIFY(converter.convertFileToFileCalled); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertFileToFile(): cannot open file file.dat\n"); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToFileAsData() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + + bool doConvertFileToFile(Stage, Containers::StringView, Containers::StringView) override { + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + } + + Containers::Array doConvertDataToData(Stage stage, Containers::ArrayView data) override { + if(stage == Stage::RayAnyHit && data.size() == 1 && data[0] == '\xb0') + return Containers::array({'y', 'e', 'p'}); + return {}; + } + } converter; + + struct State { + const char data = '\xb0'; + bool loaded = false; + bool closed = false; + } state; + + converter.setInputFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, State& state) -> Containers::Optional> { + if(filename == "file.dat" && policy == InputFileCallbackPolicy::LoadTemporary) { + state.loaded = true; + return Containers::arrayView(&state.data, 1); + } + + if(filename == "file.dat" && policy == InputFileCallbackPolicy::Close) { + state.closed = true; + return {}; + } + + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + }, state); + + /* Remove previous file, if any */ + const std::string filename = Utility::Directory::join(SHADERTOOLS_TEST_OUTPUT_DIR, "file.out"); + Utility::Directory::rm(filename); + CORRADE_VERIFY(!Utility::Directory::exists(filename)); + + CORRADE_VERIFY(converter.convertFileToFile(Stage::RayAnyHit, "file.dat", filename)); + CORRADE_VERIFY(state.loaded); + CORRADE_VERIFY(state.closed); + CORRADE_COMPARE_AS(filename, "yep", TestSuite::Compare::FileToString); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToFileAsDataFailed() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + + bool doConvertFileToFile(Stage, Containers::StringView, Containers::StringView) override { + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + } + } converter; + + converter.setInputFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!converter.convertFileToFile({}, "file.dat", "/some/path/that/does/not/exist")); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertFileToFile(): cannot open file file.dat\n"); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToFileAsDataNotWritable() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + + bool doConvertFileToFile(Stage, Containers::StringView, Containers::StringView) override { + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + } + + Containers::Array doConvertDataToData(Stage, Containers::ArrayView) override { + return Containers::Array{1}; + } + } converter; + + struct State { + const char data = '\xb0'; + bool loaded = false; + bool closed = false; + } state; + + converter.setInputFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, State& state) -> Containers::Optional> { + if(filename == "file.dat" && policy == InputFileCallbackPolicy::LoadTemporary) { + state.loaded = true; + return Containers::arrayView(&state.data, 1); + } + + if(filename == "file.dat" && policy == InputFileCallbackPolicy::Close) { + state.closed = true; + return {}; + } + + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + }, state); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter.convertFileToFile({}, "file.dat", "/some/path/that/does/not/exist")); + CORRADE_VERIFY(state.loaded); + CORRADE_VERIFY(state.closed); + CORRADE_COMPARE(out.str(), + "Utility::Directory::write(): can't open /some/path/that/does/not/exist\n" + "ShaderTools::AbstractConverter::convertFileToFile(): cannot write to file /some/path/that/does/not/exist\n"); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToDataDirectly() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData|ConverterFeature::InputFileCallback; + } + + Containers::Array doConvertFileToData(Stage stage, Containers::StringView from) override { + if(stage == Stage::Compute && from == "file.dat" && inputFileCallback() && inputFileCallbackUserData()) + return Containers::array({'y', 'e', 'p'}); + return {}; + } + + Containers::Array doConvertDataToData(Stage, Containers::ArrayView) override { + CORRADE_VERIFY(!"this should not be reached"); + return {}; + } + } converter; + + int a{}; + converter.setInputFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + CORRADE_VERIFY(!"this should not be reached"); + return Containers::Optional>{}; + }, &a); + + CORRADE_COMPARE_AS(converter.convertFileToData(Stage::Compute, "file.dat"), + Containers::arrayView({'y', 'e', 'p'}), + TestSuite::Compare::Container); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToDataThroughBaseImplementation() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData|ConverterFeature::InputFileCallback; + } + + Containers::Array doConvertFileToData(Stage stage, Containers::StringView from) override { + convertFileToDataCalled = true; + + if(stage != Stage::TessellationEvaluation || from != "file.dat" || !inputFileCallback() || !inputFileCallbackUserData()) + return {}; + + return AbstractConverter::doConvertFileToData(stage, from); + } + + Containers::Array doConvertDataToData(Stage stage, Containers::ArrayView data) override { + if(stage == Stage::TessellationEvaluation && data.size() == 1 && data[0] == '\xb0') + return Containers::array({'y', 'e', 'p'}); + return {}; + } + + bool convertFileToDataCalled = false; + } converter; + + struct State { + const char data = '\xb0'; + bool loaded = false; + bool closed = false; + } state; + + converter.setInputFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, State& state) -> Containers::Optional> { + if(filename == "file.dat" && policy == InputFileCallbackPolicy::LoadTemporary) { + state.loaded = true; + return Containers::arrayView(&state.data, 1); + } + + if(filename == "file.dat" && policy == InputFileCallbackPolicy::Close) { + state.closed = true; + return {}; + } + + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + }, state); + + CORRADE_COMPARE_AS(converter.convertFileToData(Stage::TessellationEvaluation, "file.dat"), + Containers::arrayView({'y', 'e', 'p'}), + TestSuite::Compare::Container); + CORRADE_VERIFY(converter.convertFileToDataCalled); + CORRADE_VERIFY(state.loaded); + CORRADE_VERIFY(state.closed); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToDataThroughBaseImplementationFailed() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData|ConverterFeature::InputFileCallback; + } + + Containers::Array doConvertFileToData(Stage stage, Containers::StringView from) override { + convertFileToDataCalled = true; + return AbstractConverter::doConvertFileToData(stage, from); + } + + bool convertFileToDataCalled = false; + } converter; + + converter.setInputFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!converter.convertFileToData({}, "file.dat")); + CORRADE_VERIFY(converter.convertFileToDataCalled); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertFileToData(): cannot open file file.dat\n"); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToDataAsData() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + + Containers::Array doConvertFileToData(Stage, Containers::StringView) override { + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + } + + Containers::Array doConvertDataToData(Stage stage, Containers::ArrayView data) override { + if(stage == Stage::RayGeneration && data.size() == 1 && data[0] == '\xb0') + return Containers::array({'y', 'e', 'p'}); + return {}; + } + } converter; + + struct State { + const char data = '\xb0'; + bool loaded = false; + bool closed = false; + } state; + + converter.setInputFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, State& state) -> Containers::Optional> { + if(filename == "file.dat" && policy == InputFileCallbackPolicy::LoadTemporary) { + state.loaded = true; + return Containers::arrayView(&state.data, 1); + } + + if(filename == "file.dat" && policy == InputFileCallbackPolicy::Close) { + state.closed = true; + return {}; + } + + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + }, state); + + CORRADE_COMPARE_AS(converter.convertFileToData(Stage::RayGeneration, "file.dat"), + Containers::arrayView({'y', 'e', 'p'}), + TestSuite::Compare::Container); + CORRADE_VERIFY(state.loaded); + CORRADE_VERIFY(state.closed); +} + +void AbstractConverterTest::setInputFileCallbackConvertFileToDataAsDataFailed() { + struct: AbstractConverter { + ConverterFeatures doFeatures() const override { + return ConverterFeature::ConvertData; + } + + Containers::Array doConvertFileToData(Stage, Containers::StringView) override { + CORRADE_VERIFY(!"this shouldn't be reached"); + return {}; + } + } converter; + + converter.setInputFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!converter.convertFileToData({}, "file.dat")); + CORRADE_COMPARE(out.str(), "ShaderTools::AbstractConverter::convertFileToData(): cannot open file file.dat\n"); +} + +void AbstractConverterTest::debugFeature() { + std::ostringstream out; + + Debug{&out} << ConverterFeature::ConvertData << ConverterFeature(0xf0); + CORRADE_COMPARE(out.str(), "ShaderTools::ConverterFeature::ConvertData ShaderTools::ConverterFeature(0xf0)\n"); +} + +void AbstractConverterTest::debugFeatures() { + std::ostringstream out; + + Debug{&out} << (ConverterFeature::ValidateData|ConverterFeature::ConvertFile) << ConverterFeatures{}; + CORRADE_COMPARE(out.str(), "ShaderTools::ConverterFeature::ValidateData|ShaderTools::ConverterFeature::ConvertFile ShaderTools::ConverterFeatures{}\n"); +} + +void AbstractConverterTest::debugFlag() { + std::ostringstream out; + + Debug{&out} << ConverterFlag::Verbose << ConverterFlag(0xf0); + CORRADE_COMPARE(out.str(), "ShaderTools::ConverterFlag::Verbose ShaderTools::ConverterFlag(0xf0)\n"); +} + +void AbstractConverterTest::debugFlags() { + std::ostringstream out; + + Debug{&out} << (ConverterFlag::Verbose|ConverterFlag(0xf0)) << ConverterFlags{}; + CORRADE_COMPARE(out.str(), "ShaderTools::ConverterFlag::Verbose|ShaderTools::ConverterFlag(0xf0) ShaderTools::ConverterFlags{}\n"); +} + +void AbstractConverterTest::debugStage() { + std::ostringstream out; + + Debug{&out} << Stage::RayMiss << Stage(0xf0); + CORRADE_COMPARE(out.str(), "ShaderTools::Stage::RayMiss ShaderTools::Stage(0xf0)\n"); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::ShaderTools::Test::AbstractConverterTest) diff --git a/src/Magnum/ShaderTools/Test/CMakeLists.txt b/src/Magnum/ShaderTools/Test/CMakeLists.txt new file mode 100644 index 000000000..fb754cbcc --- /dev/null +++ b/src/Magnum/ShaderTools/Test/CMakeLists.txt @@ -0,0 +1,41 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020 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(SHADERTOOLS_TEST_DIR ".") + set(SHADERTOOLS_TEST_OUTPUT_DIR "./write") +else() + set(SHADERTOOLS_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + set(SHADERTOOLS_TEST_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}) +endif() + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h) + +corrade_add_test(ShaderToolsAbstractConverterTest AbstractConverterTest.cpp + LIBRARIES MagnumShaderToolsTestLib + FILES file.dat) + +target_include_directories(ShaderToolsAbstractConverterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/Magnum/ShaderTools/Test/configure.h.cmake b/src/Magnum/ShaderTools/Test/configure.h.cmake new file mode 100644 index 000000000..464c1701d --- /dev/null +++ b/src/Magnum/ShaderTools/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, + 2020 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. +*/ + +#define SHADERTOOLS_TEST_DIR "${SHADERTOOLS_TEST_DIR}" +#define SHADERTOOLS_TEST_OUTPUT_DIR "${SHADERTOOLS_TEST_OUTPUT_DIR}" diff --git a/src/Magnum/ShaderTools/Test/file.dat b/src/Magnum/ShaderTools/Test/file.dat new file mode 100644 index 000000000..74183c400 --- /dev/null +++ b/src/Magnum/ShaderTools/Test/file.dat @@ -0,0 +1 @@ +SPIRV \ No newline at end of file diff --git a/src/Magnum/ShaderTools/configure.h.cmake b/src/Magnum/ShaderTools/configure.h.cmake new file mode 100644 index 000000000..50a36f95d --- /dev/null +++ b/src/Magnum/ShaderTools/configure.h.cmake @@ -0,0 +1,27 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020 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. +*/ + +#define MAGNUM_PLUGINS_SHADERCONVERTER_DIR "${MAGNUM_PLUGINS_SHADERCONVERTER_DIR}" +#define MAGNUM_PLUGINS_SHADERCONVERTER_DEBUG_DIR "${MAGNUM_PLUGINS_SHADERCONVERTER_DEBUG_DIR}" diff --git a/src/Magnum/ShaderTools/visibility.h b/src/Magnum/ShaderTools/visibility.h new file mode 100644 index 000000000..b2efc965c --- /dev/null +++ b/src/Magnum/ShaderTools/visibility.h @@ -0,0 +1,48 @@ +#ifndef Magnum_ShaderTools_visibility_h +#define Magnum_ShaderTools_visibility_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include + +#include "Magnum/configure.h" + +#ifndef DOXYGEN_GENERATING_OUTPUT +#ifndef MAGNUM_BUILD_STATIC + #if defined(MagnumShaderTools_EXPORTS) || defined(MagnumShaderToolsObjects_EXPORTS) + #define MAGNUM_SHADERTOOLS_EXPORT CORRADE_VISIBILITY_EXPORT + #else + #define MAGNUM_SHADERTOOLS_EXPORT CORRADE_VISIBILITY_IMPORT + #endif +#else + #define MAGNUM_SHADERTOOLS_EXPORT CORRADE_VISIBILITY_STATIC +#endif +#define MAGNUM_SHADERTOOLS_LOCAL CORRADE_VISIBILITY_LOCAL +#else +#define MAGNUM_SHADERTOOLS_EXPORT +#define MAGNUM_SHADERTOOLS_LOCAL +#endif + +#endif diff --git a/src/Magnum/Text/AbstractFont.h b/src/Magnum/Text/AbstractFont.h index d61129203..9f1276367 100644 --- a/src/Magnum/Text/AbstractFont.h +++ b/src/Magnum/Text/AbstractFont.h @@ -145,8 +145,9 @@ loaded data through to @ref openData(), in case the importer supports at least @ref FontFeature::FileCallback nor @ref FontFeature::OpenData, @ref setFileCallback() doesn't allow the callbacks to be set. -The input file callback signature is the same for @ref Text::AbstractFont and -@ref Trade::AbstractImporter to allow code reuse. +The input file callback signature is the same for @ref Text::AbstractFont, +@ref ShaderTools::AbstractConverter and @ref Trade::AbstractImporter to allow +code reuse. @section Text-AbstractFont-subclassing Subclassing diff --git a/src/Magnum/Trade/AbstractImporter.h b/src/Magnum/Trade/AbstractImporter.h index 3e9e4351e..ca079d7ab 100644 --- a/src/Magnum/Trade/AbstractImporter.h +++ b/src/Magnum/Trade/AbstractImporter.h @@ -188,8 +188,9 @@ loaded data through to @ref openData(), in case the importer supports at least @ref ImporterFeature::FileCallback nor @ref ImporterFeature::OpenData, @ref setFileCallback() doesn't allow the callbacks to be set. -The input file callback signature is the same for @ref Trade::AbstractImporter -and @ref Text::AbstractFont to allow code reuse. +The input file callback signature is the same for @ref Trade::AbstractImporter, +@ref ShaderTools::AbstractConverter and @ref Text::AbstractFont to allow code +reuse. @subsection Trade-AbstractImporter-usage-state Internal importer state