Revision control
++ // Copyright (C) 2023 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++
++ #include "qshaderbaker.h"
++ #include "qspirvcompiler_p.h"
++ #include "qspirvshader_p.h"
++ #include <QFileInfo>
++ #include <QFile>
++ #include <QDebug>
++ #include <QMap>
++
++ QT_BEGIN_NAMESPACE
++
++ /*!
++ \class QShaderBaker
++ \inmodule QtShaderTools
++ \since 6.6
++
++ \brief Compiles a GLSL/Vulkan shader into SPIR-V, translates into other
++ shading languages, and gathers reflection metadata.
++
++ \warning QShaderBaker, just like the QRhi family of classes in the Qt Gui
++ module, including QShader and QShaderDescription, offers limited
++ compatibility guarantees. There are no source or binary compatibility
++ guarantees for these classes, meaning the API is only guaranteed to work
++ with the Qt version the application was developed against. Source
++ incompatible changes are however aimed to be kept at a minimum and will only
++ be made in minor releases (6.7, 6.8, and so on). To use this class in an
++ application, link to \c{Qt::ShaderToolsPrivate} (if using CMake), and
++ include the headers with the \c rhi prefix, for example
++ \c{#include <rhi/qshaderbaker.h>}.
++
++ QShaderBaker takes a graphics (vertex, fragment, etc.) or compute shader,
++ and produces multiple - either source or bytecode - variants of it,
++ together with reflection information. The results are represented by a
++ QShader instance, which also provides simple and fast serialization
++ and deserialization.
++
++ \note Applications and libraries are recommended to avoid using this class
++ directly. Rather, all Qt users are encouraged to rely on offline compilation
++ by invoking the \c qsb command-line tool at build time via CMake. The \c qsb
++ tool uses QShaderBaker and writes the serialized version of the generated
++ QShader into a file. The usage of this class should be restricted to cases
++ where run time compilation cannot be avoided, such as when working with
++ user-provided or dynamically generated shader source strings.
++
++ The input format is always assumed to be Vulkan-flavored GLSL at the
++ moment. See the
++ \l{https://github.com/KhronosGroup/GLSL/blob/master/extensions/khr/GL_KHR_vulkan_glsl.txt}{GL_KHR_vulkan_glsl
++ specification} for an overview, keeping in mind that the Qt Shader Tools
++ module is meant to be used in combination with the QRhi classes from Qt
++ Rendering Hardware Interface module, and therefore a number of concepts and
++ constructs (push constants, storage buffers, subpasses, etc.) are not
++ applicable at the moment. Additional options may be introduced in the
++ future, for example, by enabling
++ as a source format, once HLSL to SPIR-V compilation is deemed suitable.
++
++ The reflection metadata is retrievable from the resulting QShader by
++ calling QShader::description(). This is essential when having to
++ discover what set of vertex inputs and shader resources a shader expects,
++ and what the layouts of those are, as many modern graphics APIs offer no
++ built-in shader reflection capabilities.
++
++ \section2 Typical Workflow
++
++ Let's assume an application has a vertex and fragment shader like the following:
++
++ Vertex shader:
++ \snippet color.vert 0
++
++ Fragment shader:
++ \snippet color.frag 0
++
++ To get QShader instances that can be passed as-is to a
++ QRhiGraphicsPipeline, there are two options: doing the shader pack
++ generation off line, or at run time.
++
++ The former involves running the \c qsb tool:
++
++ \badcode
++ qsb --glsl "100 es,120" --hlsl 50 --msl 12 color.vert -o color.vert.qsb
++ qsb --glsl "100 es,120" --hlsl 50 --msl 12 color.frag -o color.frag.qsb
++ \endcode
++
++ The example uses the translation targets as appropriate for QRhi. This
++ means GLSL/ES 100, GLSL 120, HLSL Shader Model 5.0, and Metal Shading
++ Language 1.2.
++
++ Note how the command line options correspond to what can be specified via
++ setGeneratedShaders(). Once the resulting files are available, they can be
++ shipped with the application (typically embedded into the executable the
++ the Qt Resource System), and can be loaded and passed to
++ QShader::fromSerialized() at run time.
++
++ While not shown here, \c qsb can do more: it is also able to invoke \c fxc
++ on Windows or the appropriate XCode tools on macOS to compile the generated
++ HLSL or Metal shader code into bytecode and include the compiled versions
++ in the QShader. After a baked shader pack is written into a file, its
++ contents can be examined by running \c{qsb -d} on it. Run \c qsb with
++ \c{--help} for more information.
++
++ The alternative approach is to perform the same at run time. This involves
++ creating a QShaderBaker instance, calling setSourceFileName(), and then
++ setting up the translation targets via setGeneratedShaders():
++
++ \badcode
++ baker.setGeneratedShaderVariants({ QShader::StandardShader });
++ QList<QShaderBaker::GeneratedShader> targets;
++ targets.append({ QShader::SpirvShader, QShaderVersion(100) });
++ targets.append({ QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs) });
++ targets.append({ QShader::SpirvShader, QShaderVersion(120) });
++ targets.append({ QShader::HlslShader, QShaderVersion(50) });
++ targets.append({ QShader::MslShader, QShaderVersion(12) });
++ baker.setGeneratedShaders(targets);
++ QShader shaders = baker.bake();
++ if (!shaders.isValid())
++ qWarning() << baker.errorMessage();
++ \endcode
++
++ \sa QShader
++ */
++
++ struct QShaderBakerPrivate
++ {
++ bool readFile(const QString &fn);
++ std::pair<QByteArray, QByteArray> compile();
++ QByteArray perTargetDefines(const QShaderBaker::GeneratedShader &key);
++
++ QString sourceFileName;
++ QByteArray source;
++ QShader::Stage stage;
++ QList<QShaderBaker::GeneratedShader> reqVersions;
++ QList<QShader::Variant> variants;
++ QByteArray preamble;
++ int batchLoc = 7;
++ bool perTargetEnabled = false;
++ bool breakOnShaderTranslationError = true;
++ QSpirvShader::TessellationInfo tessInfo;
++ QSpirvShader::MultiViewInfo multiViewInfo;
++ QShaderBaker::SpirvOptions spirvOptions;
++ QShaderBaker::GlslOptions glslOptions;
++ QSpirvCompiler compiler;
++ QString errorMessage;
++ };
++
++ bool QShaderBakerPrivate::readFile(const QString &fn)
++ {
++ QFile f(fn);
++ if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
++ qWarning("QShaderBaker: Failed to open %s", qPrintable(fn));
++ return false;
++ }
++ source = f.readAll();
++
++ // Remove UTF-8 BOM, if present. glslang fails otherwise, since the BOM is
++ // removed in glslangValidator (the command-line frontend), not in the
++ // compiler itself. Replicate that here.
++ if (source.size() >= 3) {
++ const unsigned char *p = reinterpret_cast<const unsigned char *>(source.constData());
++ if (p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF)
++ source.remove(0, 3);
++ }
++
++ sourceFileName = fn;
++ return true;
++ }
++
++ /*!
++ Constructs a new QShaderBaker.
++ */
++ QShaderBaker::QShaderBaker()
++ : d(new QShaderBakerPrivate)
++ {
++ }
++
++ /*!
++ Destructor.
++ */
++ QShaderBaker::~QShaderBaker()
++ {
++ delete d;
++ }
++
++ /*!
++ Sets the name of the shader source file to \a fileName. This is the file
++ that will be read when calling bake(). The shader stage is deduced
++ automatically from the file extension. When this is not desired or not
++ possible, use the overload with the stage argument instead.
++
++ The supported file extensions are:
++ \list
++ \li \c{.vert} - vertex shader
++ \li \c{.frag} - fragment (pixel) shader
++ \li \c{.tesc} - tessellation control (hull) shader
++ \li \c{.tese} - tessellation evaluation (domain) shader
++ \li \c{.geom} - geometry shader
++ \li \c{.comp} - compute shader
++ \endlist
++
++ \warning \a fileName is expected to contain trusted content. Application
++ developers are advised to carefully consider the potential implications
++ before passing in user-provided source files that are not part of the
++ application.
++ */
++ void QShaderBaker::setSourceFileName(const QString &fileName)
++ {
++ if (!d->readFile(fileName))
++ return;
++
++ const QString suffix = QFileInfo(fileName).suffix();
++ if (suffix == QStringLiteral("vert")) {
++ d->stage = QShader::VertexStage;
++ } else if (suffix == QStringLiteral("frag")) {
++ d->stage = QShader::FragmentStage;
++ } else if (suffix == QStringLiteral("tesc")) {
++ d->stage = QShader::TessellationControlStage;
++ } else if (suffix == QStringLiteral("tese")) {
++ d->stage = QShader::TessellationEvaluationStage;
++ } else if (suffix == QStringLiteral("geom")) {
++ d->stage = QShader::GeometryStage;
++ } else if (suffix == QStringLiteral("comp")) {
++ d->stage = QShader::ComputeStage;
++ } else {
++ qWarning("QShaderBaker: Unknown shader stage, defaulting to vertex");
++ d->stage = QShader::VertexStage;
++ }
++ }
++
++ /*!
++ Sets the name of the shader source file to \a fileName. This is the file
++ that will be read when calling bake(). The shader stage is specified by \a
++ stage.
++
++ \warning \a fileName is expected to contain trusted content. Application
++ developers are advised to carefully consider the potential implications
++ before passing in user-provided source files that are not part of the
++ application.
++ */
++ void QShaderBaker::setSourceFileName(const QString &fileName, QShader::Stage stage)
++ {
++ if (d->readFile(fileName))
++ d->stage = stage;
++ }
++
++ /*!
++ Sets the source \a device. This allows using any QIODevice instead of just
++ files. \a stage specifies the shader stage, while the optional \a fileName
++ contains a filename that is used in the error messages.
++
++ \warning \a device is expected to contain trusted content. Application
++ developers are advised to carefully consider the potential implications
++ before passing in user-provided data from sources that are not under the
++ application's control.
++ */
++ void QShaderBaker::setSourceDevice(QIODevice *device, QShader::Stage stage, const QString &fileName)
++ {
++ setSourceString(device->readAll(), stage, fileName);
++ }
++
++ /*!
++ Sets the input shader \a sourceString. \a stage specified the shader stage,
++ while the optional \a fileName contains a filename that is used in the
++ error messages.
++
++ \warning \a sourceString is expected to contain trusted content. Application
++ developers are advised to carefully consider the potential implications
++ before passing in user-provided data from sources that are not under the
++ application's control.
++ */
++ void QShaderBaker::setSourceString(const QByteArray &sourceString, QShader::Stage stage, const QString &fileName)
++ {
++ d->sourceFileName = fileName; // for error messages, include handling, etc.
++ d->source = sourceString;
++ d->stage = stage;
++ }
++
++ /*!
++ \typedef QShaderBaker::GeneratedShader
++
++ Synonym for std::pair<QShader::Source, QShaderVersion>.
++ */
++
++ /*!
++ Specifies what kind of shaders to compile or translate to. Nothing is
++ generated by default so calling this function before bake() is mandatory
++
++ \note when this function is not called or \a v is empty or contains only invalid
++ entries, the resulting QShader will be empty and thus invalid.
++
++ For example, the minimal possible baking target is SPIR-V, without any
++ additional translations to other languages. To request this, do:
++
++ \badcode
++ baker.setGeneratedShaders({ QShader::SpirvShader, QShaderVersion(100) });
++ \endcode
++
++ \note QShaderBaker only handles the SPIR-V and human-readable source
++ targets. Further compilation into API-specific intermediate formats, such
++ as QShader::DxbcShader or QShader::MetalLibShader is implemented by the
++ \c qsb command-line tool, and is not part of the QShaderBaker runtime API.
++ */
++ void QShaderBaker::setGeneratedShaders(const QList<GeneratedShader> &v)
++ {
++ d->reqVersions = v;
++ }
++
++ /*!
++ Specifies which shader variants are generated. Each shader version can have
++ multiple variants in the resulting QShader.
++
++ In most cases \a v contains a single entry, QShader::StandardShader.
++
++ \note when no variants are set, the resulting QShader will be empty and
++ thus invalid.
++ */
++ void QShaderBaker::setGeneratedShaderVariants(const QList<QShader::Variant> &v)
++ {
++ d->variants = v;
++ }
++
++ /*!
++ Specifies a custom \a preamble that is processed before the normal shader
++ code.
++
++ This is more than just prepending to the source string: the validity of the
++ GLSL version directive, which is required to be placed before everything
++ else, is not affected. Line numbers in the reported error messages also
++ remain unchanged, ignoring the contents given in the \a preamble.
++
++ One use case for preambles is to transparently insert dynamically generated
++ \c{#define} statements.
++ */
++ void QShaderBaker::setPreamble(const QByteArray &preamble)
++ {
++ d->preamble = preamble;
++ }
++
++ /*!
++ When generating a QShader::BatchableVertexShader variant, \a location
++ specifies the input location for the inserted vertex input. The value is by
++ default 7 and needs to be overridden only if the vertex shader already uses
++ input location 7.
++ */
++ void QShaderBaker::setBatchableVertexShaderExtraInputLocation(int location)
++ {
++ d->batchLoc = location;
++ }
++
++ /*!
++ Sets per-target compilation to \a enable. By default this is disabled,
++ meaning that the Vulkan/GLSL source is compiled to SPIR-V once per variant.
++ (so once by default, twice if it is a vertex shader and the Batchable
++ variant as requested as well). The resulting SPIR-V is then translated to
++ the various target languages (GLSL, HLSL, MSL).
++
++ In per-target compilation mode, there is a separate GLSL to SPIR-V
++ compilation step for each target, meaning for each GLSL/HLSL/MSL version
++ requested via setGeneratedShaders(). The input source is the same, but with
++ target-specific preprocessor defines inserted. This is significantly more
++ time consuming, but allows applications to provide a single shader and use
++ \c{#ifdef} blocks to differentiate. When this mode is disabled, the only
++ way to achieve the same is to provide multiple versions of the shader file,
++ process each separately, ship {.qsb} files for each, and choose the right
++ file based on run time logic.
++
++ The following macros will be automatically defined in this mode. Note that
++ the macros are always tied to shading languages, not graphics APIs.
++
++ \list
++
++ \li \c{QSHADER_SPIRV} - defined when targeting SPIR-V (to be consumed,
++ typically, by Vulkan).
++
++ \li \c{QSHADER_SPIRV_VERSION} - the targeted SPIR-V version number, such as
++ \c 100.
++
++ \li \c{QSHADER_GLSL} - defined when targeting GLSL or GLSL ES (to be
++ consumed, typically, by OpenGL or OpenGL ES)
++
++ \li \c{QSHADER_GLSL_VERSION} - the targeted GLSL or GLSL ES version number,
++ such as \c 100, \c 300, or \c 330.
++
++ \li \c{QSHADER_GLSL_ES} - defined only when targeting GLSL ES
++
++ \li \c{QSHADER_HLSL} - defined when targeting HLSL (to be consumed,
++ typically, by Direct 3D)
++
++ \li \c{QSHADER_HLSL_VERSION} - the targeted HLSL shader model version, such
++ as \c 50
++
++ \li \c{QSHADER_MSL} - defined when targeting the Metal Shading Language (to
++ be consumed, typically, by Metal)
++
++ \li \c{QSHADER_MSL_VERSION} - the targeted MSL version, such as \c 12 or
++ \c 20.
++
++ \endlist
++
++ This allows writing shader code like the following.
++
++ \badcode
++ #if QSHADER_HLSL || QSHADER_MSL
++ vec2 uv = vec2(uv_coord.x, 1.0 - uv_coord.y);
++ #else
++ vec2 uv = uv_coord;
++ #endif
++ \endcode
++
++ \note Version numbers follow the GLSL-inspired QShaderVersion syntax and
++ thus are a single integer always.
++
++ \note There is only one QShaderDescription per QShader, no matter how many
++ individual targets there are. Therefore members of uniform blocks, vertex
++ inputs, etc. must not be made conditional using the macros described above.
++
++ \warning Be aware of the differences between the concepts of graphics APIs
++ and shading languages. QShaderBaker and the related tools work strictly
++ with the concept of shading languages, ignoring how the results are
++ consumed afterwards. Therefore, if the higher layers in the Qt graphics
++ stack one day start using SPIR-V also for an API other than Vulkan, the
++ assumption that QSHADER_SPIRV implies Vulkan will no longer hold.
++ */
++ void QShaderBaker::setPerTargetCompilation(bool enable)
++ {
++ d->perTargetEnabled = enable;
++ }
++
++ /*!
++ Controls the behavior when shader translation (from SPIR-V to
++ GLSL/HLSL/MSL) fails. By default this setting is true, which will cause
++ bake() to return with an error if a requested shader cannot be generated.
++ If that is not desired, and the intention is to generate what we can but
++ silently skip the rest, then set \a enable to false.
++
++ Targeting multiple GLSL versions can lead to errors when a feature is not
++ translatable to a given version. For example, attempting to translate a
++ shader using textureSize() to GLSL ES 100 would fail the entire bake() call
++ with the error message "textureSize is not supported in ESSL 100". If it is
++ acceptable to not have a GLSL ES 100 shader in the result, even though it
++ was requested, then setting this flag to false makes bake() to succeed.
++ */
++ void QShaderBaker::setBreakOnShaderTranslationError(bool enable)
++ {
++ d->breakOnShaderTranslationError = enable;
++ }
++
++ /*!
++ When generating MSL shader code for a tessellation control shader, the
++ tessellation \a mode (triangles or quads) must be known upfront. In GLSL
++ this is declared in the tessellation evaluation shader typically, but for
++ Metal it must be known also when generating the compute shader from the
++ tessellation control shader.
++
++ When not set, the default is triangles.
++ */
++ void QShaderBaker::setTessellationMode(QShaderDescription::TessellationMode mode)
++ {
++ d->tessInfo.infoForTesc.mode = mode;
++ }
++
++ /*!
++ When generating MSL shader code for a tessellation evaluation shader, the
++ output vertex \a count of the tessellation control shader must be known
++ upfront. in GLSL this would be declared in the tessellation control shader
++ typically, but for Metal it must be known also when generating the vertex
++ shader from the teselation evaluation shader.
++
++ When not set, the default value is 3.
++ */
++ void QShaderBaker::setTessellationOutputVertexCount(int count)
++ {
++ d->tessInfo.infoForTese.vertexCount = count;
++ }
++
++ /*!
++ When transpiling shaders using multiview (e.g. a vertex shader using
++ gl_ViewIndex for a renderer relying on GL_OVR_multiview2, VK_KHR_multiview,
++ etc.), for some of the targets it is necessary to declare the number of
++ views in the shader. This is not done in the Vulkan-style GLSL code, and is
++ not relevant for targets such as SPIR-V or HLSL, but is required for OpenGL
++ and GLSL, and so the value has to be provided as additional metadata.
++
++ By default the value is 0, which disables injecting the \c{num_views}
++ statement. Setting 1 is not useful since that is the default \c{num_views}
++ regardless. Therefore \a count should be >= 2 to make an effect. When set
++ to, for example, 2, the generated GLSL shader will contain a
++ \c{layout(num_views = 2) in;} statement.
++
++ Setting a \a count of 2 or greater also injects some preprocessor
++ statements:
++ \c{QSHADER_VIEW_COUNT} is set to \a count, whereas the
++ \c GL_EXT_multiview extension is enabled automatically. Therefore, setting
++ the appropriate
++ \a count can be relevant with other types of shaders as well, e.g. when
++ sharing a uniform buffer between the vertex and fragment shader and both
++ shaders have to be able to write something like
++ \c{#if QSHADER_VIEW_COUNT >= 2}.
++
++ \since 6.7
++ */
++ void QShaderBaker::setMultiViewCount(int count)
++ {
++ d->multiViewInfo.viewCount = count >= 2 ? count : 0;
++ }
++
++ /*!
++ \enum QShaderBaker::SpirvOption
++ \value GenerateFullDebugInfo Generate and store additional debug information in the SPIR-V binary.
++ \value StripDebugAndVarInfo Strip all debug and variable name information from the SPIR-V binary.
++ */
++
++ /*!
++ Sets additional \a options for the generated SPIR-V binary.
++ By default no flags are set.
++ */
++ void QShaderBaker::setSpirvOptions(SpirvOptions options)
++ {
++ d->spirvOptions = options;
++ }
++
++ /*!
++ \enum QShaderBaker::GlslOption
++ \value GlslEsFragDefaultFloatPrecisionMedium Emit \c{precision mediump float;} in fragment shaders for GLSL ES.
++ */
++
++ /*!
++ Sets additional \a options for the generated GLSL and GLSL ES sources.
++ By default no flags are set.
++ \since 6.9
++ */
++ void QShaderBaker::setGlslOptions(GlslOptions options)
++ {
++ d->glslOptions = options;
++ }
++
++ #ifndef Q_OS_INTEGRITY // QTBUG-145990
++ #ifndef QSHADERVERSION_IS_HASHABLE
++ inline size_t qHash(const QShaderBaker::GeneratedShader &k, size_t seed = 0)
++ {
++ return qHash(k.first, seed) ^ k.second.version();
++ }
++ #endif
++ #endif
++
++ std::pair<QByteArray, QByteArray> QShaderBakerPrivate::compile()
++ {
++ QSpirvCompiler::Flags flags;
++ if (spirvOptions.testFlag(QShaderBaker::SpirvOption::GenerateFullDebugInfo))
++ flags |= QSpirvCompiler::FullDebugInfo;
++
++ compiler.setFlags(flags);
++ const QByteArray spirvBin = compiler.compileToSpirv();
++ if (spirvBin.isEmpty()) {
++ errorMessage = compiler.errorMessage();
++ return {};
++ }
++ QByteArray batchableSpirvBin;
++ if (stage == QShader::VertexStage && variants.contains(QShader::BatchableVertexShader)) {
++ compiler.setFlags(flags | QSpirvCompiler::RewriteToMakeBatchableForSG);
++ compiler.setSGBatchingVertexInputLocation(batchLoc);
++ batchableSpirvBin = compiler.compileToSpirv();
++ if (batchableSpirvBin.isEmpty()) {
++ errorMessage = compiler.errorMessage();
++ return {};
++ }
++ }
++ return { spirvBin, batchableSpirvBin };
++ }
++
++ QByteArray QShaderBakerPrivate::perTargetDefines(const QShaderBaker::GeneratedShader &key)
++ {
++ QByteArray preamble;
++ switch (key.first) {
++ case QShader::SpirvShader:
++ preamble += QByteArrayLiteral("\n#define QSHADER_SPIRV 1\n#define QSHADER_SPIRV_VERSION ");
++ preamble += QByteArray::number(key.second.version());
++ preamble += QByteArrayLiteral("\n");
++ break;
++ case QShader::GlslShader:
++ preamble += QByteArrayLiteral("\n#define QSHADER_GLSL 1\n#define QSHADER_GLSL_VERSION ");
++ preamble += QByteArray::number(key.second.version());
++ if (key.second.flags().testFlag(QShaderVersion::GlslEs))
++ preamble += QByteArrayLiteral("\n#define QSHADER_GLSL_ES 1");
++ preamble += QByteArrayLiteral("\n");
++ break;
++ case QShader::HlslShader:
++ preamble += QByteArrayLiteral("\n#define QSHADER_HLSL 1\n#define QSHADER_HLSL_VERSION ");
++ preamble += QByteArray::number(key.second.version());
++ preamble += QByteArrayLiteral("\n");
++ break;
++ case QShader::MslShader:
++ preamble += QByteArrayLiteral("\n#define QSHADER_MSL 1\n#define QSHADER_MSL_VERSION ");
++ preamble += QByteArray::number(key.second.version());
++ preamble += QByteArrayLiteral("\n");
++ break;
++ default:
++ Q_UNREACHABLE();
++ }
++ return preamble;
++ }
++
++ /*!
++ Runs the compilation and translation process.
++
++ \return a QShader instance. To check if the process was successful,
++ call QShader::isValid(). When that indicates \c false, call
++ errorMessage() to retrieve the log.
++
++ This is an expensive operation. When calling this from applications, it can
++ be advisable to do it on a separate thread.
++
++ \note QShaderBaker instances are reusable: after calling bake(), the same
++ instance can be used with different inputs again. However, a QShaderBaker
++ instance should only be used on one single thread during its lifetime.
++ */
++ QShader QShaderBaker::bake()
++ {
++ d->errorMessage.clear();
++
++ if (d->source.isEmpty()) {
++ d->errorMessage = QLatin1String("QShaderBaker: No source specified");
++ return QShader();
++ }
++
++ d->compiler.setSourceString(d->source, d->stage, d->sourceFileName);
++
++ // Normally one entry, for QShader::SpirvShader only. However, in
++ // compile-per-target mode there is a separate SPIR-V binary generated for
++ // each target (so for each GLSL/HLSL/MSL version requested).
++ // These are ordered maps, not hash tables, on purpose: the reflection info
++ // is generated from whichever binary comes first, and that must not depend
++ // on the (per-process randomized) QHash seed, otherwise the resulting
++ // QShader would not be reproducible from one run to another.
++ QMap<GeneratedShader, QByteArray> spirv;
++ QMap<GeneratedShader, QByteArray> batchableSpirv;
++ const auto compileSpirvAndBatchable = [this, &spirv, &batchableSpirv](const GeneratedShader &key) {
++ const std::pair<QByteArray, QByteArray> bin = d->compile();
++ if (bin.first.isEmpty())
++ return false;
++ spirv.insert(key, bin.first);
++ if (!bin.second.isEmpty())
++ batchableSpirv.insert(key, bin.second);
++ return true;
++ };
++
++ QByteArray preamble = d->preamble;
++ if (d->multiViewInfo.viewCount >= 2) {
++ if (d->stage == QShader::VertexStage)
++ preamble += QByteArrayLiteral("\n#extension GL_EXT_multiview : require\n");
++ preamble += QByteArrayLiteral("\n#define QSHADER_VIEW_COUNT ");
++ preamble += QByteArray::number(d->multiViewInfo.viewCount);
++ preamble += QByteArrayLiteral("\n");
++ }
++
++ if (!d->perTargetEnabled) {
++ d->compiler.setPreamble(preamble);
++ if (!compileSpirvAndBatchable({ QShader::SpirvShader, {} }))
++ return QShader();
++ } else {
++ // per-target compilation. the value here comes from the varying
++ // preamble (and so preprocessor defines)
++ for (GeneratedShader req: std::as_const(d->reqVersions)) {
++ d->compiler.setPreamble(preamble + d->perTargetDefines(req));
++ if (!compileSpirvAndBatchable(req))
++ return QShader();
++ }
++ }
++
++ // Now spirv, and, if in use, batchableSpirv, contain at least one,
++ // optionally more SPIR-V binaries.
++ Q_ASSERT(!spirv.isEmpty() && (d->perTargetEnabled || spirv.size() == 1));
++
++ QShader bs;
++ bs.setStage(d->stage);
++
++ QSpirvShader spirvShader;
++ QSpirvShader batchableSpirvShader;
++ // The QShaderDescription can be different for variants (we just have a
++ // hardcoded rule to pick one), but cannot differ for targets (in
++ // per-target mode, hence we can just pick the first SPIR-V binary and
++ // generate the reflection data based on that)
++ spirvShader.setSpirvBinary(spirv.constKeyValueBegin()->second, d->stage);
++ if (batchableSpirv.isEmpty()) {
++ bs.setDescription(spirvShader.shaderDescription());
++ } else {
++ batchableSpirvShader.setSpirvBinary(batchableSpirv.constKeyValueBegin()->second, d->stage);
++ // prefer the batchable's reflection info with _qt_order and such present
++ bs.setDescription(batchableSpirvShader.shaderDescription());
++ }
++
++ for (const GeneratedShader &req: std::as_const(d->reqVersions)) {
++ for (const QShader::Variant &v : std::as_const(d->variants)) {
++ if (d->stage != QShader::VertexStage) {
++ if (v == QShader::BatchableVertexShader
++ || v == QShader::UInt32IndexedVertexAsComputeShader
++ || v == QShader::UInt16IndexedVertexAsComputeShader
++ || v == QShader::NonIndexedVertexAsComputeShader)
++ {
++ continue;
++ }
++ }
++ if (req.first != QShader::MslShader && req.first != QShader::MetalLibShader) {
++ if (v == QShader::UInt32IndexedVertexAsComputeShader
++ || v == QShader::UInt16IndexedVertexAsComputeShader
++ || v == QShader::NonIndexedVertexAsComputeShader)
++ {
++ continue;
++ }
++ }
++ if (v == QShader::ArgumentBufferShader) {
++ // MSL only, needs 2.0, and would be a no-op without textures or samplers
++ const QShaderDescription desc = bs.description();
++ if (req.first != QShader::MslShader
++ || req.second.version() < 20
++ || (desc.combinedImageSamplers().isEmpty()
++ && desc.separateImages().isEmpty()
++ && desc.separateSamplers().isEmpty()
++ && desc.storageImages().isEmpty()))
++ {
++ continue;
++ }
++ }
++
++ QSpirvShader *currentSpirvShader = nullptr;
++ if (d->perTargetEnabled) {
++ // This is expensive too, in addition to the multiple
++ // compilation rounds, but opting in to per-target mode is a
++ // careful, conscious choice (hopefully), so it's fine.
++ if (v == QShader::BatchableVertexShader)
++ batchableSpirvShader.setSpirvBinary(batchableSpirv[req], d->stage);
++ else
++ spirvShader.setSpirvBinary(spirv[req], d->stage);
++ }
++ if (v == QShader::BatchableVertexShader)
++ currentSpirvShader = &batchableSpirvShader;
++ else
++ currentSpirvShader = &spirvShader;
++ Q_ASSERT(currentSpirvShader);
++ Q_ASSERT(!currentSpirvShader->spirvBinary().isEmpty());
++ const QShaderKey key(req.first, req.second, v);
++ QShaderCode shader;
++ shader.setEntryPoint(QByteArrayLiteral("main"));
++ switch (req.first) {
++ case QShader::SpirvShader:
++ if (d->spirvOptions.testFlag(QShaderBaker::SpirvOption::StripDebugAndVarInfo)) {
++ QString errorMsg;
++ const QByteArray strippedSpirv = currentSpirvShader->remappedSpirvBinary(QSpirvShader::RemapFlag::StripOnly, &errorMsg);
++ if (strippedSpirv.isEmpty()) {
++ d->errorMessage = errorMsg;
++ return QShader();
++ }
++ shader.setShader(strippedSpirv);
++ } else {
++ shader.setShader(currentSpirvShader->spirvBinary());
++ }
++ break;
++ case QShader::GlslShader:
++ {
++ QSpirvShader::GlslFlags flags;
++ if (req.second.flags().testFlag(QShaderVersion::GlslEs)) {
++ flags |= QSpirvShader::GlslFlag::GlslEs;
++ if (d->glslOptions.testFlag(GlslOption::GlslEsFragDefaultFloatPrecisionMedium))
++ flags |= QSpirvShader::GlslFlag::EsFragDefaultFloatPrecisionMedium;
++ }
++ QVector<QSpirvShader::SeparateToCombinedImageSamplerMapping> separateToCombinedImageSamplerMappings;
++ shader.setShader(currentSpirvShader->translateToGLSL(req.second.version(),
++ flags,
++ d->stage,
++ d->multiViewInfo,
++ &separateToCombinedImageSamplerMappings));
++ if (shader.shader().isEmpty()) {
++ if (d->breakOnShaderTranslationError) {
++ d->errorMessage = currentSpirvShader->translationErrorMessage();
++ return QShader();
++ } else {
++ d->errorMessage += QLatin1String(" ") + currentSpirvShader->translationErrorMessage();
++ continue;
++ }
++ }
++ if (!separateToCombinedImageSamplerMappings.isEmpty()) {
++ const QShaderDescription desc = bs.description();
++ QVector<QShaderDescription::InOutVariable> separateImages = desc.separateImages();
++ QVector<QShaderDescription::InOutVariable> separateSamplers = desc.separateSamplers();
++ QShader::SeparateToCombinedImageSamplerMappingList result;
++ for (const QSpirvShader::SeparateToCombinedImageSamplerMapping &mapping : std::as_const(separateToCombinedImageSamplerMappings)) {
++ int textureBinding = -1;
++ int samplerBinding = -1;
++ for (int i = 0, count = separateImages.size(); i < count; ++i) {
++ if (separateImages[i].name == mapping.textureName) {
++ textureBinding = separateImages[i].binding;
++ break;
++ }
++ }
++ for (int i = 0, count = separateSamplers.size(); i < count; ++i) {
++ if (separateSamplers[i].name == mapping.samplerName) {
++ samplerBinding = separateSamplers[i].binding;
++ break;
++ }
++ }
++ result.append({ mapping.combinedSamplerName, textureBinding, samplerBinding });
++ }
++ bs.setSeparateToCombinedImageSamplerMappingList(key, result);
++ }
++ }
++ break;
++ case QShader::HlslShader:
++ {
++ QShader::NativeResourceBindingMap nativeBindings;
++ QShader::NativeShaderInfo shaderInfo;
++ shader.setShader(currentSpirvShader->translateToHLSL(req.second.version(), &nativeBindings, &shaderInfo));
++ if (shader.shader().isEmpty()) {
++ if (d->breakOnShaderTranslationError) {
++ d->errorMessage = currentSpirvShader->translationErrorMessage();
++ return QShader();
++ } else {
++ d->errorMessage += QLatin1String(" ") + currentSpirvShader->translationErrorMessage();
++ continue;
++ }
++ }
++ bs.setResourceBindingMap(key, nativeBindings);
++ if (!shaderInfo.extraBufferBindings.isEmpty())
++ bs.setNativeShaderInfo(key, shaderInfo);
++ }
++ break;
++ case QShader::MslShader:
++ {
++ QShader::NativeResourceBindingMap nativeBindings;
++ QShader::NativeShaderInfo shaderInfo;
++ QSpirvShader::MslFlags flags;
++ if (v == QShader::ArgumentBufferShader)
++ flags |= QSpirvShader::MslFlag::ArgumentBuffers;
++ if (d->stage == QShader::VertexStage) {
++ switch (v) {
++ case QShader::UInt16IndexedVertexAsComputeShader:
++ flags |= QSpirvShader::MslFlag::VertexAsCompute;
++ flags |= QSpirvShader::MslFlag::WithUInt16Index;
++ break;
++ case QShader::UInt32IndexedVertexAsComputeShader:
++ flags |= QSpirvShader::MslFlag::VertexAsCompute;
++ flags |= QSpirvShader::MslFlag::WithUInt32Index;
++ break;
++ case QShader::NonIndexedVertexAsComputeShader:
++ flags |= QSpirvShader::MslFlag::VertexAsCompute;
++ break;
++ default:
++ break;
++ }
++ }
++ shader.setShader(currentSpirvShader->translateToMSL(req.second.version(),
++ flags,
++ d->stage,
++ &nativeBindings,
++ &shaderInfo,
++ d->multiViewInfo,
++ d->tessInfo));
++ if (shader.shader().isEmpty()) {
++ if (d->breakOnShaderTranslationError) {
++ d->errorMessage = currentSpirvShader->translationErrorMessage();
++ return QShader();
++ } else {
++ d->errorMessage += QLatin1String(" ") + currentSpirvShader->translationErrorMessage();
++ continue;
++ }
++ }
++ shader.setEntryPoint(QByteArrayLiteral("main0"));
++ bs.setResourceBindingMap(key, nativeBindings);
++ bs.setNativeShaderInfo(key, shaderInfo);
++ }
++ break;
++ default:
++ Q_UNREACHABLE();
++ }
++ bs.setShader(key, shader);
++ }
++ }
++
++ return bs;
++ }
++
++ /*!
++ \return the error message from the last bake() run, or an empty string if
++ there was no error.
++
++ \note Errors include file read errors, compilation, and translation
++ failures. Not requesting any targets or variants does not count as an error
++ even though the resulting QShader is invalid.
++ */
++ QString QShaderBaker::errorMessage() const
++ {
++ return d->errorMessage;
++ }
++
++ QT_END_NAMESPACE