/* 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/Implementation/converterUtilities.h" #include "Magnum/Math/Functions.h" #include "Magnum/ShaderTools/AbstractConverter.h" namespace Magnum { /** @page magnum-shaderconverter Shader conversion utility @brief Converts, compiles, optimizes and links shaders of different formats @m_since_latest @m_footernavigation @m_keywords{magnum-shaderconverter shaderconverter} This utility is built if both `WITH_SHADERTOOLS` and `WITH_SHADERCONVERTER` is enabled when building Magnum. To use this utility with CMake, you need to request the `shaderconverter` component of the `Magnum` package and use the `Magnum::shaderconverter` target for example in a custom command: @code{.cmake} find_package(Magnum REQUIRED shaderconverter) add_custom_command(OUTPUT ... COMMAND Magnum::shaderconverter ...) @endcode See @ref building, @ref cmake and the @ref ShaderTools namespace for more information. @section magnum-shaderconverter-usage Usage @code{.sh} magnum-shaderconverter [-h|--help] [--validate] [--link] [-C|--converter NAME]... [--plugin-dir DIR] [-c|--converter-options key=val,key2=val2,…]... [-q|--quiet] [-v|--verbose] [--warning-as-error] [-E|--preprocess-only] [-D|--define name=value]... [-U|--undefine name]... [-O|--optimize LEVEL] [-g|--debug-info LEVEL] [--input-version VERSION]... [--output-version VERSION]... [--] input... output @endcode Arguments: - `input` --- input file(s) - `output` --- output file, ignored if `--validate` is present - `-h`, `--help` --- display this help message and exit - `--validate` --- validate input - `--link` --- link multiple input files together - `-C`, `--converter CONVERTER` --- shader converter plugin(s) - `--plugin-dir DIR` --- override base plugin dir - `-c`, `--converter-options key=val,key2=val2,…` --- configuration options to pass to the converter(s) - `-q`, `--quiet` --- quiet output from converter plugin(s) - `-v`, `--verbose` --- verbose output from converter plugin(s) - `--warning-as-error` --- treat warnings as errors - `-E`, `--preprocess-only` --- preprocess the input file and exit - `-D`, `--define name=value` --- define a preprocessor macro - `-U`, `--undefine name` --- undefine a preprocessor macro - `-O`, `--optimize LEVEL` --- optimization level to use - `-g`, `--debug-info LEVEL` --- debug info level to use - `--input-version VERSION` --- input format version for each converter - `--output-version VERSION` --- output format version for each converter If `--validate` is given, the utility will validate the `input` file using passed `--converter` (or @ref ShaderTools::AnyConverter "AnyShaderConverter" if none is specified), print the validation log on output and exit with a non-zero code if the validation fails. If `--link` is given, the utility will link all files together using passed `--converter` (or @ref ShaderTools::AnyConverter "AnyShaderConverter" if none is specified) and save it to `output`. If neither is specified, the utility will convert the `input` file using (one or more) passed `--converter` (or @ref ShaderTools::AnyConverter "AnyShaderConverter" if none is specified) and save it to `output`. The `-c` / `--converter-options` argument accept a comma-separated list of key/value pairs to set in the converter plugin configuration. If the `=` character is omitted, it's equivalent to saying `key=true`; configuration subgroups are delimited with `/`. It's possible to specify the `-C` / `--converter` option (and correspondingly also `-c` / `--converter-options`, `--input-version` and `--output-version`) multiple times in order to chain more converters together. All converters in the chain have to support the @ref ShaderTools::ConverterFeature::ConvertData feature, if there's just one converter it's enough for it to support @ref ShaderTools::ConverterFeature::ConvertFile. If no `-C` / `--converter` is specified, @ref ShaderTools::AnyConverter "AnyShaderConverter" is used. The `-D` / `--define`, `-U` / `--undefine`, `-O` / `--optimize`, `-g` / `--debug-info`, `-E` / `--preprocess-only` arguments apply only to the first converter. Split the conversion to multiple passes if you need to pass those to converters later in the chain. Values accepted by `-O` / `--optimize`, `-g` / `--debug-info`, `--input-version` and `--output-version` are converter-specific, see documentation of a particular converter for more information. @section magnum-shaderconverter-example Example usage Validate a SPIR-V file for a Vulkan 1.1 target, using @ref ShaderTools::SpirvToolsConverter "SpirvToolsShaderConverter" picked by @ref ShaderTools::AnyConverter "AnyShaderConverter": @code{.sh} magnum-shaderconverter --validate --output-version vulkan1.1 shader.spv @endcode Converting a GLSL 4.10 file to a SPIR-V, supplying various preprocessor definitions, treating warnings as errors and targeting OpenGL instead of the (default) Vulkan, using @ref ShaderTools::GlslangConverter "GlslangShaderConverter" picked again by @ref ShaderTools::AnyConverter "AnyShaderConverter": @m_class{m-console-wrap} @code{.sh} magnum-shaderconverter phong.frag -DDIFFUSE_TEXTURE -DNORMAL_TEXTURE --input-version "410 core" --output-version opengl4.5 --warning-as-error phong.frag.spv @endcode */ } using namespace Magnum; int main(int argc, char** argv) { Utility::Arguments args; args.addArrayArgument("input").setHelp("input", "input file(s)") .addArgument("output").setHelp("output", "output file, ignored if --validate is present") .addBooleanOption("validate").setHelp("validate", "validate input") .addBooleanOption("link").setHelp("link", "link multiple input files together") .addArrayOption('C', "converter").setHelp("converter", "shader converter plugin(s)") .addOption("plugin-dir").setHelp("plugin-dir", "override base plugin dir", "DIR") .addArrayOption('c', "converter-options").setHelp("converter-options", "configuration options to pass to the converter(s)", "key=val,key2=val2,…") .addBooleanOption('q', "quiet").setHelp("quiet", "quiet output from converter plugin(s)") .addBooleanOption('v', "verbose").setHelp("verbose", "verbose output from converter plugin(s)") .addBooleanOption("warning-as-error").setHelp("warning-as-error", "treat warnings as errors") .addBooleanOption('E', "preprocess-only").setHelp("preprocess-only", "preprocess the input file and exit") .addArrayOption('D', "define").setHelp("define", "define a preprocessor macro", "name=value") .addArrayOption('U', "undefine").setHelp("undefine", "undefine a preprocessor macro", "name") .addOption('O', "optimize").setHelp("optimize", "optimization level to use", "LEVEL") .addOption('g', "debug-info").setHelp("debug-info", "debug info level to use", "LEVEL") .addArrayOption("input-version").setHelp("input-version", "input format version for each converter", "VERSION") .addArrayOption("output-version").setHelp("output-version", "output format version for each converter", "VERSION") .setParseErrorCallback([](const Utility::Arguments& args, Utility::Arguments::ParseError error, const std::string& key) { /* If --validate is passed, we don't need the output argument */ if(error == Utility::Arguments::ParseError::MissingArgument && key == "output" && args.isSet("validate")) return true; /* Handle all other errors as usual */ return false; }) .setGlobalHelp(R"(Converts, compiles, optimizes and links shaders of different formats. If --validate is given, the utility will validate the input file using passed --converter (or AnyShaderConverter if none is specified), print the validation log on output and exit with a non-zero code if the validation fails. If --link is given, the utility will link all files together using passed --converter (or AnyShaderConverter if none is specified) and save it to output. If neither is specified, the utility will convert the input file using (one or more) passed --converter and save it to output. The -c / --converter-options argument accept a comma-separated list of key/value pairs to set in the converter plugin configuration. If the = character is omitted, it's equivalent to saying key=true; configuration subgroups are delimited with /. It's possible to specify the -C / --converter option (and correspondingly also -c / --converter-options, --input-version and --output-version) multiple times in order to chain more converters together. All converters in the chain have to support the ConvertData feature, if there's just one converter it's enough for it to support ConvertFile. If no -C / --converter is specified, AnyShaderConverter is used. The -D / --define, -U / --undefine, -O / --optimize, -g / --debug-info, -E / --preprocess-only arguments apply only to the first converter. Split the conversion to multiple passes if you need to pass those to converters later in the chain. Values accepted by -O / --optimize, -g / --debug-info, --input-version and --output-version are converter-specific, see documentation of a particular converter for more information.)") .parse(argc, argv); /* Generic checks */ if(args.isSet("validate")) { if(!args.value("output").empty()) { Error{} << "Output file shouldn't be set for --validate"; return 1; } } if(!args.isSet("link")) { if(args.arrayValueCount("input") != 1) { Error{} << "Multiple input files are allowed only for --link"; return 3; } } if(args.isSet("validate") || args.isSet("link")) { if(args.isSet("preprocess-only")) { Error{} << "The --preprocess-only option isn't allowed for --validate or --link"; return 4; } if(args.arrayValueCount("converter") > 1) { Error{} << "Cannot use multiple converters with --validate or --link"; return 5; } } if(args.isSet("quiet") && args.isSet("verbose")) { Error{} << "Can't set both --quiet and --verbose"; return 6; } if(args.isSet("quiet") && args.isSet("warning-as-error")) { Error{} << "Can't set both --quiet and --warning-as-error"; return 6; } /* Set up a converter manager */ PluginManager::Manager converterManager{ args.value("plugin-dir").empty() ? std::string{} : Utility::Directory::join(args.value("plugin-dir"), ShaderTools::AbstractConverter::pluginSearchPaths()[0])}; /* Data passed from one converter to another in case there's more than one */ Containers::Array data; /* If there's no converters, it'll be just one AnyShaderConverter. */ for(std::size_t i = 0, converterCount = args.arrayValueCount("converter"); i < Math::max(converterCount, std::size_t{1}); ++i) { const std::string converterName = converterCount ? args.arrayValue("converter", i) : "AnyShaderConverter"; Containers::Pointer converter = converterManager.loadAndInstantiate(converterName); if(!converter) { Debug{} << "Available converter plugins:" << Utility::String::join(converterManager.aliasList(), ", "); return 7; } /* Set options and versions, if passed */ if(i < args.arrayValueCount("converter-options")) Implementation::setOptions(*converter, args.arrayValue("converter-options", i)); if(i < args.arrayValueCount("input-version")) converter->setInputFormat({}, args.arrayValue("input-version", i)); if(i < args.arrayValueCount("output-version")) converter->setOutputFormat({}, args.arrayValue("output-version", i)); ShaderTools::ConverterFlags flags; /* Global flags, applied for all converters */ if(args.isSet("quiet")) flags |= ShaderTools::ConverterFlag::Quiet; if(args.isSet("verbose")) flags |= ShaderTools::ConverterFlag::Verbose; if(args.isSet("warning-as-error")) flags |= ShaderTools::ConverterFlag::WarningAsError; /* Options and flags applied just for the first converter; setting up file list for linking */ Containers::Array> linkInputs; if(i == 0) { if((args.isSet("preprocess-only") || args.arrayValueCount("define") || args.arrayValueCount("undefine"))) { if(!(converter->features() & ShaderTools::ConverterFeature::Preprocess)) { Error{} << "The -E / -D / -U options are set, but" << converterName << "doesn't support preprocessing"; return 8; } if(args.isSet("preprocess-only")) flags |= ShaderTools::ConverterFlag::PreprocessOnly; Containers::Array> definitions; arrayReserve(definitions, args.arrayValueCount("define") + args.arrayValueCount("undefine")); for(std::size_t i = 0; i != args.arrayValueCount("define"); ++i) { const Containers::Array3 define = args.arrayValue("define", i).partition('='); arrayAppend(definitions, Containers::InPlaceInit, define[0], define[2]); } for(std::size_t i = 0; i != args.arrayValueCount("undefine"); ++i) { arrayAppend(definitions, Containers::InPlaceInit, args.arrayValue("undefine", i), nullptr); } converter->setDefinitions(definitions); } if(!args.value("optimize").empty()) { if(!(converter->features() & ShaderTools::ConverterFeature::Optimize)) { Error{} << "The -O option is set, but" << converterName << "doesn't support optimization"; return 9; } converter->setOptimizationLevel(args.value("optimize")); } if(!args.value("debug-info").empty()) { if(!(converter->features() & ShaderTools::ConverterFeature::DebugInfo)) { Error{} << "The -g option is set, but" << converterName << "doesn't support debug info"; return 10; } converter->setDebugInfoLevel(args.value("debug-info")); } if(args.isSet("link")) { arrayReserve(linkInputs, args.arrayValueCount("input")); for(std::size_t i = 0; i != args.arrayValueCount("input"); ++i) arrayAppend(linkInputs, Containers::InPlaceInit, ShaderTools::Stage::Unspecified, args.arrayValue("input", i)); } } converter->setFlags(flags); /* If validating, do it just with the first passed converter and then exit */ if(args.isSet("validate")) { /* The validation exits right after, so this branch shouldn't get re-entered again */ CORRADE_INTERNAL_ASSERT(i == 0); if(!(converter->features() & ShaderTools::ConverterFeature::ValidateFile)) { Error{} << converterName << "doesn't support file validation"; return 11; } std::pair out = converter->validateFile(ShaderTools::Stage::Unspecified, args.arrayValue("input", 0)); if(!out.first) { if(args.isSet("verbose")) Error{} << "Validation failed:"; if(!out.second.isEmpty()) Error{} << out.second; } else if(!out.second.isEmpty()) { if(args.isSet("verbose")) Warning{} << "Validation succeeded with warnings:"; if(!out.second.isEmpty()) Warning{} << out.second; } else if(args.isSet("verbose")) Debug{} << "Validation passed"; return out.first ? 0 : 12; } /** @todo ability to specify the stage (need a configurationvalue parser for this) */ /* This is the first *and* last --converter, go from a file to a file */ if(i == 0 && converterCount <= 1) { if(!(converter->features() & ShaderTools::ConverterFeature::ConvertFile)) { Error{} << converterName << "doesn't support file conversion"; return 13; } /* No verbose output for just one converter */ /* Linking */ if(args.isSet("link")) { if(!converter->linkFilesToFile(linkInputs, args.value("output"))) { Error{} << "Cannot link" << args.arrayValue("input", 0) << "and others to" << args.value("output"); return 14; } /* Converting */ } else { if(!converter->convertFileToFile(ShaderTools::Stage::Unspecified, args.arrayValue("input", 0), args.value("output"))) { Error{} << "Cannot convert" << args.arrayValue("input", 0) << "to" << args.value("output"); return 15; } } /* Otherwise we need to go through data */ } else { if(!(converter->features() & ShaderTools::ConverterFeature::ConvertData)) { Error{} << converterName << "doesn't support data conversion"; return 16; } /* This is the first --converter and there are more, go from a file to data */ if(i == 0 && converterCount > 1) { if(args.isSet("verbose")) Debug{} << "Processing (" << Debug::nospace << (i+1) << Debug::nospace << "/" << Debug::nospace << converterCount << Debug::nospace << ") with" << converterName << Debug::nospace << "..."; /* Linking */ if(args.isSet("link")) { if(!(data = converter->linkFilesToData(linkInputs))) { Error{} << "Cannot link" << args.arrayValue("input", 0) << "and others to" << args.value("output"); return 17; } /* Converting */ } else { if(!(data = converter->convertFileToData(ShaderTools::Stage::Unspecified, args.arrayValue("input", 0)))) { Error{} << "Cannot convert" << args.arrayValue("input", 0); return 18; } } /* This is neither first nor last --converter, go from data to data */ } else if(i > 0 && i + 1 < converterCount) { if(args.isSet("verbose")) Debug{} << "Processing (" << Debug::nospace << (i+1) << Debug::nospace << "/" << Debug::nospace << converterCount << Debug::nospace << ") with" << converterName << Debug::nospace << "..."; CORRADE_INTERNAL_ASSERT(data); /* Subsequent operations are always a conversion, not link */ if(!(data = converter->convertDataToData(ShaderTools::Stage::Unspecified, data))) { Error{} << "Cannot convert shader data"; return 19; } /* This is the last --converter, output to a file and exit the loop */ } else if(i + 1 >= converterCount) { if(args.isSet("verbose")) Debug{} << "Saving output with" << converterName << Debug::nospace << "..."; CORRADE_INTERNAL_ASSERT(data); /* Subsequent operations are always a conversion, not link */ if(!converter->convertDataToFile(ShaderTools::Stage::Unspecified, data, args.value("output"))) { Error{} << "Cannot save file" << args.value("output"); return 20; } } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); } } }