summaryrefslogtreecommitdiffstats
path: root/src/shadertools/qshaderbaker.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/shadertools/qshaderbaker.cpp')
-rw-r--r--src/shadertools/qshaderbaker.cpp421
1 files changed, 421 insertions, 0 deletions
diff --git a/src/shadertools/qshaderbaker.cpp b/src/shadertools/qshaderbaker.cpp
new file mode 100644
index 0000000..fbb0a2b
--- /dev/null
+++ b/src/shadertools/qshaderbaker.cpp
@@ -0,0 +1,421 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the Qt Shader Tools module
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qshaderbaker.h"
+#include "qspirvcompiler_p.h"
+#include "qspirvshader_p.h"
+#include <QFileInfo>
+#include <QFile>
+#include <QDebug>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \class QShaderBaker
+ \inmodule QtShaderTools
+
+ \brief Compiles a GLSL/Vulkan shader into SPIR-V, translates into other
+ shading languages, and gathers reflection metadata.
+
+ 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
+ QRhiShader 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. This
+ tool uses QShaderBaker itself and writes the serialized version of the
+ generated QRhiShader 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 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
+ \l{https://docs.microsoft.com/en-us/windows/desktop/direct3dhlsl/dx-graphics-hlsl}{HLSL}
+ as a source format, once HLSL to SPIR-V compilation is deemed suitable.
+
+ The reflection metadata is retrievable from the resulting QRhiShader by
+ calling QRhiShader::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 QRhiShader 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
+ QRhiShader::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 QRhiShader. 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({ QRhiShaderKey::StandardShader });
+ QVector<QShaderBaker::GeneratedShader> targets;
+ targets.append({ QRhiShaderKey::SpirvShader, QRhiShaderVersion(100) });
+ targets.append({ QRhiShaderKey::GlslShader, QRhiShaderVersion(100, QRhiShaderVersion::GlslEs) });
+ targets.append({ QRhiShaderKey::SpirvShader, QRhiShaderVersion(120) });
+ targets.append({ QRhiShaderKey::HlslShader, QRhiShaderVersion(50) });
+ targets.append({ QRhiShaderKey::MslShader, QRhiShaderVersion(12) });
+ baker.setGeneratedShaders(targets);
+ QRhiShader shaders = baker.bake();
+ if (!shaders.isValid())
+ qWarning() << baker.errorMessage();
+ \endcode
+
+ \sa QRhiShader
+ */
+
+struct QShaderBakerPrivate
+{
+ bool readFile(const QString &fn);
+
+ QString sourceFileName;
+ QByteArray source;
+ QRhiShader::ShaderStage stage;
+ QVector<QShaderBaker::GeneratedShader> reqVersions;
+ QVector<QRhiShaderKey::ShaderVariant> variants;
+ 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();
+ 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
+ */
+void QShaderBaker::setSourceFileName(const QString &fileName)
+{
+ if (!d->readFile(fileName))
+ return;
+
+ const QString suffix = QFileInfo(fileName).suffix();
+ if (suffix == QStringLiteral("vert")) {
+ d->stage = QRhiShader::VertexStage;
+ } else if (suffix == QStringLiteral("frag")) {
+ d->stage = QRhiShader::FragmentStage;
+ } else if (suffix == QStringLiteral("tesc")) {
+ d->stage = QRhiShader::TessControlStage;
+ } else if (suffix == QStringLiteral("tese")) {
+ d->stage = QRhiShader::TessEvaluationStage;
+ } else if (suffix == QStringLiteral("geom")) {
+ d->stage = QRhiShader::GeometryStage;
+ } else if (suffix == QStringLiteral("comp")) {
+ d->stage = QRhiShader::ComputeStage;
+ } else {
+ qWarning("QShaderBaker: Unknown shader stage, defaulting to vertex");
+ d->stage = QRhiShader::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.
+ */
+void QShaderBaker::setSourceFileName(const QString &fileName, QRhiShader::ShaderStage 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.
+ */
+void QShaderBaker::setSourceDevice(QIODevice *device, QRhiShader::ShaderStage 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.
+ */
+void QShaderBaker::setSourceString(const QByteArray &sourceString, QRhiShader::ShaderStage stage, const QString &fileName)
+{
+ d->sourceFileName = fileName; // for error messages, include handling, etc.
+ d->source = sourceString;
+ d->stage = stage;
+}
+
+/*!
+ \typedef QShaderBaker::GeneratedShader
+
+ Synonym for QPair<QRhiShaderKey::ShaderSource, QRhiShaderVersion>.
+*/
+
+/*!
+ 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 QRhiShader 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({ QRhiShaderKey::SpirvShader, QRhiShaderVersion(100) });
+ \endcode
+ */
+void QShaderBaker::setGeneratedShaders(const QVector<GeneratedShader> &v)
+{
+ d->reqVersions = v;
+}
+
+/*!
+ Specifies which shader variants are genetated. Each shader version can have
+ multiple variants in the resulting QRhiShader.
+
+ In most cases \a v contains a single entry, QRhiShaderKey::StandardShader.
+
+ \note when no variants are set, the resulting QRhiShader will be empty and
+ thus invalid.
+ */
+void QShaderBaker::setGeneratedShaderVariants(const QVector<QRhiShaderKey::ShaderVariant> &v)
+{
+ d->variants = v;
+}
+
+/*!
+ Runs the compilation and translation process.
+
+ \return a QRhiShader instance. To check if the process was successful,
+ call QRhiShader::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.
+ */
+QRhiShader QShaderBaker::bake()
+{
+ d->errorMessage.clear();
+
+ if (d->source.isEmpty()) {
+ d->errorMessage = QLatin1String("QShaderBaker: No source specified");
+ return QRhiShader();
+ }
+
+ d->compiler.setSourceString(d->source, d->stage, d->sourceFileName);
+ d->compiler.setFlags(0);
+ QByteArray spirv = d->compiler.compileToSpirv();
+ if (spirv.isEmpty()) {
+ d->errorMessage = d->compiler.errorMessage();
+ return QRhiShader();
+ }
+
+ QByteArray batchableSpirv;
+ if (d->stage == QRhiShader::VertexStage && d->variants.contains(QRhiShaderKey::BatchableVertexShader)) {
+ d->compiler.setFlags(QSpirvCompiler::RewriteToMakeBatchableForSG);
+ batchableSpirv = d->compiler.compileToSpirv();
+ if (batchableSpirv.isEmpty()) {
+ d->errorMessage = d->compiler.errorMessage();
+ return QRhiShader();
+ }
+ }
+
+ QRhiShader bs;
+ bs.setStage(d->stage);
+
+ QSpirvShader spirvShader;
+ spirvShader.setSpirvBinary(spirv);
+
+ QSpirvShader batchableSpirvShader;
+ if (!batchableSpirv.isEmpty()) {
+ batchableSpirvShader.setSpirvBinary(batchableSpirv);
+ bs.setDescription(batchableSpirvShader.shaderDescription());
+ } else {
+ bs.setDescription(spirvShader.shaderDescription());
+ }
+
+ for (const GeneratedShader &req: d->reqVersions) {
+ for (const QRhiShaderKey::ShaderVariant &v : d->variants) {
+ QByteArray *currentSpirv = &spirv;
+ QSpirvShader *currentSpirvShader = &spirvShader;
+ if (v == QRhiShaderKey::BatchableVertexShader) {
+ if (!batchableSpirv.isEmpty()) {
+ currentSpirv = &batchableSpirv;
+ currentSpirvShader = &batchableSpirvShader;
+ } else {
+ continue;
+ }
+ }
+ const QRhiShaderKey key(req.first, req.second, v);
+ QRhiShaderCode shader;
+ shader.setEntryPoint(QByteArrayLiteral("main"));
+ switch (req.first) {
+ case QRhiShaderKey::SpirvShader:
+ shader.setShader(*currentSpirv);
+ break;
+ case QRhiShaderKey::GlslShader:
+ {
+ QSpirvShader::GlslFlags flags = 0;
+ if (req.second.flags().testFlag(QRhiShaderVersion::GlslEs))
+ flags |= QSpirvShader::GlslEs;
+ shader.setShader(currentSpirvShader->translateToGLSL(req.second.version(), flags));
+ if (shader.shader().isEmpty()) {
+ d->errorMessage = currentSpirvShader->translationErrorMessage();
+ return QRhiShader();
+ }
+ }
+ break;
+ case QRhiShaderKey::HlslShader:
+ shader.setShader(currentSpirvShader->translateToHLSL(req.second.version()));
+ if (shader.shader().isEmpty()) {
+ d->errorMessage = currentSpirvShader->translationErrorMessage();
+ return QRhiShader();
+ }
+ break;
+ case QRhiShaderKey::MslShader:
+ shader.setShader(currentSpirvShader->translateToMSL(req.second.version()));
+ if (shader.shader().isEmpty()) {
+ d->errorMessage = currentSpirvShader->translationErrorMessage();
+ return QRhiShader();
+ }
+ shader.setEntryPoint(QByteArrayLiteral("main0"));
+ 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 QRhiShader is invalid.
+ */
+QString QShaderBaker::errorMessage() const
+{
+ return d->errorMessage;
+}
+
+QT_END_NAMESPACE