Revision control
Copy as Markdown
// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtCore/qcoreapplication.h>
#include <QtCore/qcommandlineparser.h>
#include <QtCore/qtextstream.h>
#include <QtCore/qfile.h>
#include <QtCore/qdir.h>
#include <QtCore/qset.h>
#include <QtCore/qtemporarydir.h>
#include <QtCore/qdebug.h>
#include <QtCore/qlibraryinfo.h>
#include <QtGui/private/qshader_p.h>
#include <rhi/qshaderbaker.h>
#if QT_CONFIG(process)
#include <QtCore/qprocess.h>
#endif
#include <cstdarg>
#include <algorithm>
#include <cstdio>
// All qDebug must be guarded by !silent. For qWarnings, only the most
// fatal ones should be unconditional; warnings from external tool
// invocations must be guarded with !silent.
static bool silent = false;
using namespace Qt::StringLiterals;
enum class FileType
{
Binary,
Text
};
static void printError(const char *msg, ...)
{
va_list arglist;
va_start(arglist, msg);
vfprintf(stderr, msg, arglist);
fputs("\n", stderr);
va_end(arglist);
}
static bool writeToFile(const QByteArray &buf, const QString &filename, FileType fileType)
{
QDir().mkpath(QFileInfo(filename).path());
QFile f(filename);
QIODevice::OpenMode flags = QIODevice::WriteOnly | QIODevice::Truncate;
if (fileType == FileType::Text)
flags |= QIODevice::Text;
if (!f.open(flags)) {
printError("Failed to open %s for writing", qPrintable(filename));
return false;
}
f.write(buf);
return true;
}
static QByteArray readFile(const QString &filename, FileType fileType)
{
QFile f(filename);
QIODevice::OpenMode flags = QIODevice::ReadOnly;
if (fileType == FileType::Text)
flags |= QIODevice::Text;
if (!f.open(flags)) {
printError("Failed to open %s", qPrintable(filename));
return QByteArray();
}
return f.readAll();
}
static QString writeTemp(const QTemporaryDir &tempDir, const QString &filename, const QShaderCode &s, FileType fileType)
{
const QString fullPath = tempDir.path() + QLatin1String("/") + filename;
if (writeToFile(s.shader(), fullPath, fileType))
return fullPath;
else
return QString();
}
static bool runProcess(const QString &binary, const QStringList &arguments,
QByteArray *output, QByteArray *errorOutput)
{
#if QT_CONFIG(process)
QProcess p;
p.start(binary, arguments);
const QString cmd = binary + QLatin1Char(' ') + arguments.join(QLatin1Char(' '));
if (!silent)
qDebug("%s", qPrintable(cmd));
if (!p.waitForStarted()) {
if (!silent)
printError("Failed to run %s: %s", qPrintable(cmd), qPrintable(p.errorString()));
return false;
}
if (!p.waitForFinished()) {
if (!silent)
printError("%s timed out", qPrintable(cmd));
return false;
}
if (p.exitStatus() == QProcess::CrashExit) {
if (!silent)
printError("%s crashed", qPrintable(cmd));
return false;
}
*output = p.readAllStandardOutput();
*errorOutput = p.readAllStandardError();
if (p.exitCode() != 0) {
if (!silent)
printError("%s returned non-zero error code %d", qPrintable(cmd), p.exitCode());
return false;
}
return true;
#else
Q_UNUSED(binary);
Q_UNUSED(arguments);
Q_UNUSED(output);
*errorOutput = QByteArrayLiteral("QProcess not supported on this platform");
return false;
#endif
}
static QString stageStr(QShader::Stage stage)
{
switch (stage) {
case QShader::VertexStage:
return QStringLiteral("Vertex");
case QShader::TessellationControlStage:
return QStringLiteral("TessellationControl");
case QShader::TessellationEvaluationStage:
return QStringLiteral("TessellationEvaluation");
case QShader::GeometryStage:
return QStringLiteral("Geometry");
case QShader::FragmentStage:
return QStringLiteral("Fragment");
case QShader::ComputeStage:
return QStringLiteral("Compute");
default:
Q_UNREACHABLE();
}
}
static QString sourceStr(QShader::Source source)
{
switch (source) {
case QShader::SpirvShader:
return QStringLiteral("SPIR-V");
case QShader::GlslShader:
return QStringLiteral("GLSL");
case QShader::HlslShader:
return QStringLiteral("HLSL");
case QShader::DxbcShader:
return QStringLiteral("DXBC");
case QShader::MslShader:
return QStringLiteral("MSL");
case QShader::DxilShader:
return QStringLiteral("DXIL");
case QShader::MetalLibShader:
return QStringLiteral("metallib");
case QShader::WgslShader:
return QStringLiteral("WGSL");
default:
Q_UNREACHABLE();
}
}
static QString sourceVersionStr(const QShaderVersion &v)
{
QString s = v.version() ? QString::number(v.version()) : QString();
if (v.flags().testFlag(QShaderVersion::GlslEs))
s += QLatin1String(" es");
return s;
}
static QString sourceVariantStr(const QShader::Variant &v)
{
switch (v) {
case QShader::StandardShader:
return QLatin1String("Standard");
case QShader::BatchableVertexShader:
return QLatin1String("Batchable");
case QShader::UInt32IndexedVertexAsComputeShader:
return QLatin1String("UInt32IndexedVertexAsCompute");
case QShader::UInt16IndexedVertexAsComputeShader:
return QLatin1String("UInt16IndexedVertexAsCompute");
case QShader::NonIndexedVertexAsComputeShader:
return QLatin1String("NonIndexedVertexAsCompute");
case QShader::ArgumentBufferShader:
return QLatin1String("ArgumentBuffer");
default:
Q_UNREACHABLE();
}
}
static void dump(const QShader &bs)
{
QTextStream ts(stdout);
ts << "Stage: " << stageStr(bs.stage()) << "\n";
ts << "QSB_VERSION: " << QShaderPrivate::get(&bs)->qsbVersion << "\n";
const QList<QShaderKey> keys = bs.availableShaders();
ts << "Has " << keys.size() << " shaders:\n";
for (int i = 0; i < keys.size(); ++i) {
ts << " Shader " << i << ": " << sourceStr(keys[i].source())
<< " " << sourceVersionStr(keys[i].sourceVersion())
<< " [" << sourceVariantStr(keys[i].sourceVariant()) << "]\n";
}
ts << "\n";
ts << "Reflection info: " << bs.description().toJson() << "\n\n";
for (int i = 0; i < keys.size(); ++i) {
ts << "Shader " << i << ": " << sourceStr(keys[i].source())
<< " " << sourceVersionStr(keys[i].sourceVersion())
<< " [" << sourceVariantStr(keys[i].sourceVariant()) << "]\n";
QShaderCode shader = bs.shader(keys[i]);
if (!shader.entryPoint().isEmpty())
ts << "Entry point: " << shader.entryPoint() << "\n";
QShader::NativeResourceBindingMap nativeResMap = bs.nativeResourceBindingMap(keys[i]);
if (!nativeResMap.isEmpty()) {
ts << "Native resource binding map:\n";
for (auto mapIt = nativeResMap.cbegin(), mapItEnd = nativeResMap.cend(); mapIt != mapItEnd; ++mapIt)
ts << mapIt.key() << " -> [" << mapIt.value().first << ", " << mapIt.value().second << "]\n";
}
QShader::SeparateToCombinedImageSamplerMappingList samplerMapList = bs.separateToCombinedImageSamplerMappingList(keys[i]);
if (!samplerMapList.isEmpty()) {
ts << "Mapping table for auto-generated combined image samplers:\n";
for (auto listIt = samplerMapList.cbegin(), listItEnd = samplerMapList.cend(); listIt != listItEnd; ++listIt)
ts << "\"" << listIt->combinedSamplerName << "\" -> [" << listIt->textureBinding << ", " << listIt->samplerBinding << "]\n";
}
QShader::NativeShaderInfo shaderInfo = bs.nativeShaderInfo(keys[i]);
if (shaderInfo.flags)
ts << "Native shader info flags: " << shaderInfo.flags << "\n";
if (!shaderInfo.extraBufferBindings.isEmpty()) {
ts << "Native shader extra buffer bindings:\n";
for (auto mapIt = shaderInfo.extraBufferBindings.cbegin(), mapItEnd = shaderInfo.extraBufferBindings.cend();
mapIt != mapItEnd; ++mapIt)
{
static struct {
QShaderPrivate::NativeShaderInfoExtraBufferBindings key;
const char *str;
} ebbNames[] = {
{ QShaderPrivate::MslTessVertIndicesBufferBinding, "tessellation(vert)-index-buffer-binding" },
{ QShaderPrivate::MslTessVertTescOutputBufferBinding, "tessellation(vert/tesc)-output-buffer-binding" },
{ QShaderPrivate::MslTessTescTessLevelBufferBinding, "tessellation(tesc)-level-buffer-binding" },
{ QShaderPrivate::MslTessTescPatchOutputBufferBinding, "tessellation(tesc)-patch-output-buffer-binding" },
{ QShaderPrivate::MslTessTescParamsBufferBinding, "tessellation(tesc)-params-buffer-binding" },
{ QShaderPrivate::MslTessTescInputBufferBinding, "tessellation(tesc)-input-buffer-binding" },
{ QShaderPrivate::MslBufferSizeBufferBinding, "buffer-size-buffer-binding" },
{ QShaderPrivate::MslMultiViewMaskBufferBinding, "view-mask-buffer-binding" },
{ QShaderPrivate::MslArgumentBufferBinding, "argument-buffer-binding" },
{ QShaderPrivate::MslPushConstantBufferBinding, "push-constant-buffer-binding" },
{ QShaderPrivate::HlslPushConstantBufferBinding, "push-constant-register" }
};
bool known = false;
for (size_t i = 0; i < sizeof(ebbNames) / sizeof(ebbNames[0]); ++i) {
if (ebbNames[i].key == mapIt.key()) {
ts << "[" << ebbNames[i].str << "] = " << mapIt.value() << "\n";
known = true;
break;
}
}
if (!known)
ts << "[" << mapIt.key() << "] = " << mapIt.value() << "\n";
}
}
ts << "Contents:\n";
switch (keys[i].source()) {
case QShader::SpirvShader:
case QShader::DxbcShader:
case QShader::DxilShader:
case QShader::MetalLibShader:
ts << "Binary of " << shader.shader().size() << " bytes\n\n";
break;
default:
ts << shader.shader() << "\n";
break;
}
ts << "\n************************************\n\n";
}
}
static QShaderKey shaderKeyFromWhatSpec(const QString &what, QShader::Variant variant)
{
const QStringList typeAndVersion = what.split(QLatin1Char(','), Qt::SkipEmptyParts);
if (typeAndVersion.size() < 2)
return {};
QShader::Source src;
if (typeAndVersion[0] == QLatin1String("spirv"))
src = QShader::SpirvShader;
else if (typeAndVersion[0] == QLatin1String("glsl"))
src = QShader::GlslShader;
else if (typeAndVersion[0] == QLatin1String("hlsl"))
src = QShader::HlslShader;
else if (typeAndVersion[0] == QLatin1String("msl"))
src = QShader::MslShader;
else if (typeAndVersion[0] == QLatin1String("dxbc"))
src = QShader::DxbcShader;
else if (typeAndVersion[0] == QLatin1String("dxil"))
src = QShader::DxilShader;
else if (typeAndVersion[0] == QLatin1String("metallib"))
src = QShader::MetalLibShader;
else if (typeAndVersion[0] == QLatin1String("wgsl"))
src = QShader::WgslShader;
else
return {};
QShaderVersion::Flags flags;
QString version = typeAndVersion[1];
if (version.endsWith(QLatin1String(" es"))) {
version = version.left(version.size() - 3);
flags |= QShaderVersion::GlslEs;
} else if (version.endsWith(QLatin1String("es"))) {
version = version.left(version.size() - 2);
flags |= QShaderVersion::GlslEs;
}
const int ver = version.toInt();
return { src, { ver, flags }, variant };
}
static bool extract(const QShader &bs, const QString &what, QShader::Variant variant, const QString &outfn)
{
if (what == QLatin1String("reflect")) {
const QByteArray reflect = bs.description().toJson();
if (!writeToFile(reflect, outfn, FileType::Text))
return false;
if (!silent)
qDebug("Reflection data written to %s", qPrintable(outfn));
return true;
}
const QShaderKey key = shaderKeyFromWhatSpec(what, variant);
const QShaderCode code = bs.shader(key);
if (code.shader().isEmpty())
return false;
if (!writeToFile(code.shader(), outfn, FileType::Binary))
return false;
if (!silent) {
qDebug("%s %d%s code (variant %s) written to %s. Entry point is '%s'.",
qPrintable(sourceStr(key.source())),
key.sourceVersion().version(),
key.sourceVersion().flags().testFlag(QShaderVersion::GlslEs) ? " es" : "",
qPrintable(sourceVariantStr(key.sourceVariant())),
qPrintable(outfn), code.entryPoint().constData());
}
return true;
}
static bool addOrReplace(const QShader &shaderPack,
const QStringList &whatList,
QShader::Variant variant,
const QString &outfn,
QShader::SerializedFormatVersion qsbVersion)
{
QShader workShaderPack = shaderPack;
for (const QString &what : whatList) {
const QStringList spec = what.split(QLatin1Char(','), Qt::SkipEmptyParts);
if (spec.size() < 3) {
printError("Invalid replace spec '%s'", qPrintable(what));
return false;
}
const QShaderKey key = shaderKeyFromWhatSpec(what, variant);
const QString fn = spec[2];
const QByteArray buf = readFile(fn, FileType::Binary);
if (buf.isEmpty())
return false;
// Does not matter if 'key' was present before or not, we support both
// replacing and adding using the same qsb -r ... syntax.
const QShaderCode code(buf, QByteArrayLiteral("main"));
workShaderPack.setShader(key, code);
if (!silent) {
qDebug("Replaced %s %d%s (variant %s) with %s. Entry point is 'main'.",
qPrintable(sourceStr(key.source())),
key.sourceVersion().version(),
key.sourceVersion().flags().testFlag(QShaderVersion::GlslEs) ? " es" : "",
qPrintable(sourceVariantStr(key.sourceVariant())),
qPrintable(fn));
}
}
return writeToFile(workShaderPack.serialized(qsbVersion), outfn, FileType::Binary);
}
static bool remove(const QShader &shaderPack,
const QStringList &whatList,
QShader::Variant variant,
const QString &outfn,
QShader::SerializedFormatVersion qsbVersion)
{
QShader workShaderPack = shaderPack;
for (const QString &what : whatList) {
const QShaderKey key = shaderKeyFromWhatSpec(what, variant);
if (!workShaderPack.availableShaders().contains(key))
continue;
workShaderPack.removeShader(key);
if (!silent) {
qDebug("Removed %s %d%s (variant %s).",
qPrintable(sourceStr(key.source())),
key.sourceVersion().version(),
key.sourceVersion().flags().testFlag(QShaderVersion::GlslEs) ? " es" : "",
qPrintable(sourceVariantStr(key.sourceVariant())));
}
}
return writeToFile(workShaderPack.serialized(qsbVersion), outfn, FileType::Binary);
}
static QByteArray fxcProfile(const QShader &bs, const QShaderKey &k)
{
QByteArray t;
switch (bs.stage()) {
case QShader::VertexStage:
t += QByteArrayLiteral("vs_");
break;
case QShader::TessellationControlStage:
t += QByteArrayLiteral("hs_");
break;
case QShader::TessellationEvaluationStage:
t += QByteArrayLiteral("ds_");
break;
case QShader::GeometryStage:
t += QByteArrayLiteral("gs_");
break;
case QShader::FragmentStage:
t += QByteArrayLiteral("ps_");
break;
case QShader::ComputeStage:
t += QByteArrayLiteral("cs_");
break;
default:
break;
}
const int major = k.sourceVersion().version() / 10;
const int minor = k.sourceVersion().version() % 10;
t += QByteArray::number(major);
t += '_';
t += QByteArray::number(minor);
return t;
}
static void replaceShaderContents(QShader *shaderPack,
const QShaderKey &originalKey,
QShader::Source newType,
const QByteArray &contents,
const QByteArray &entryPoint)
{
QShaderKey newKey = originalKey;
newKey.setSource(newType);
QShaderCode shader(contents, entryPoint);
shaderPack->setShader(newKey, shader);
if (newKey != originalKey) {
shaderPack->setResourceBindingMap(newKey, shaderPack->nativeResourceBindingMap(originalKey));
shaderPack->removeResourceBindingMap(originalKey);
shaderPack->setSeparateToCombinedImageSamplerMappingList(newKey, shaderPack->separateToCombinedImageSamplerMappingList(originalKey));
shaderPack->removeSeparateToCombinedImageSamplerMappingList(originalKey);
shaderPack->setNativeShaderInfo(newKey, shaderPack->nativeShaderInfo(originalKey));
shaderPack->removeNativeShaderInfo(originalKey);
shaderPack->removeShader(originalKey);
}
}
static bool generateDepfile(QFile &depfile, const QString &inputFilename, const QString &originalInputFilename,
const QString &outputFilename)
{
constexpr QByteArrayView includeKeyword("include");
// Assume that the minimalistic include statment should look as following: #include "x"
constexpr qsizetype minIncludeStatementSize = includeKeyword.size() + 5;
QFile inputFile(inputFilename);
if (!inputFile.open(QFile::ReadOnly)) {
printError("Unable to open input file: '%s'", qPrintable(inputFilename));
return false;
}
depfile.write(outputFilename.toUtf8());
depfile.write(": \\\n "_ba);
depfile.write(originalInputFilename.toUtf8());
enum { ParseHash, ParseInclude, ParseFilename } parserState = ParseHash;
QSet<QString> knownDeps;
QByteArray outputBuffer;
while (!inputFile.atEnd()) {
QByteArray line = inputFile.readLine();
if (line.size() < minIncludeStatementSize)
continue;
parserState = ParseHash;
for (auto it = line.constBegin(); it < line.constEnd(); ++it) {
const auto c = *it;
if (c == '\t' || c == ' ')
continue;
if (parserState == ParseHash) {
// Looking for #
if (c == '#')
parserState = ParseInclude;
else
break;
} else if (parserState == ParseInclude) {
// Looking for 'include'
if (includeKeyword == QByteArrayView(it, includeKeyword.size()))
parserState = ParseFilename;
else
break;
it += includeKeyword.size();
} else if (parserState == ParseFilename) {
// Looking for wrapping quotes
QString includeString = QString::fromUtf8(QByteArrayView(it, line.constEnd()));
QChar quoteCounterpart;
if (includeString.front() == '<') {
quoteCounterpart = '>';
} else if (includeString.front() == '"') {
quoteCounterpart = '"';
} else {
qWarning("Unrecognised include statement: '%s'", qPrintable(includeString));
break;
}
const auto filenameBegin = 1;
const auto filenameEnd = includeString.indexOf(quoteCounterpart, filenameBegin);
if (filenameEnd > filenameBegin) {
QString filename =
includeString.sliced(filenameBegin, filenameEnd - filenameBegin);
QFileInfo info(QFileInfo(inputFilename).absolutePath() + '/' + filename);
if (info.exists()) {
QString filePath = info.absoluteFilePath();
if (!knownDeps.contains(filePath)) {
outputBuffer.append(" \\\n "_ba + filePath.toUtf8());
knownDeps.insert(filePath);
}
} else {
qWarning("File '%s' included in '%s' doesn't exist. Skip adding "
"dependency.",
qPrintable(filename), qPrintable(inputFilename));
}
}
break;
}
}
}
if (!outputBuffer.isEmpty())
depfile.write(outputBuffer);
return true;
}
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QCommandLineParser cmdLineParser;
const QString appDesc = QString::asprintf("Qt Shader Baker (using QShader from Qt %s)", qVersion());
cmdLineParser.setApplicationDescription(appDesc);
app.setApplicationVersion(QLatin1String(QT_VERSION_STR));
cmdLineParser.addHelpOption();
cmdLineParser.addVersionOption();
cmdLineParser.addPositionalArgument(QLatin1String("file"),
QObject::tr("Vulkan GLSL source file to compile. The file extension determines the shader stage, and can be one of "
".vert, .tesc, .tese, .geom, .frag, .comp. "
"Note: Tessellation control/evaluation is not supported with HLSL, instead use -r to inject handcrafted hull/domain shaders. "
"Some targets may need special arguments to be set, e.g. MSL tessellation will likely need --msltess, --tess-vertex-count, --tess-mode, depending on the stage. "
"Geometry shaders are not supported with Metal."
),
QObject::tr("file"));
QCommandLineOption batchableOption({ "b", "batchable" }, QObject::tr("Also generates rewritten vertex shader for Qt Quick scene graph batching."));
cmdLineParser.addOption(batchableOption);
QCommandLineOption batchLocOption("zorder-loc",
QObject::tr("The extra vertex input location when rewriting for batching. Defaults to 7."),
QObject::tr("location"));
cmdLineParser.addOption(batchLocOption);
QCommandLineOption glslOption("glsl",
QObject::tr("Comma separated list of GLSL versions to generate. (for example, \"100 es,120,330\")"),
QObject::tr("versions"));
cmdLineParser.addOption(glslOption);
QCommandLineOption hlslOption("hlsl",
QObject::tr("Comma separated list of HLSL (Shader Model) versions to generate. F.ex. 50 is 5.0, 51 is 5.1."),
QObject::tr("versions"));
cmdLineParser.addOption(hlslOption);
QCommandLineOption mslOption("msl",
QObject::tr("Comma separated list of Metal Shading Language versions to generate. F.ex. 12 is 1.2, 20 is 2.0."),
QObject::tr("versions"));
cmdLineParser.addOption(mslOption);
QCommandLineOption shortcutDefaultOption("qt6", QObject::tr("Equivalent to --glsl \"100 es,120,150\" --hlsl 50 --msl 12. "
"This set is commonly used with shaders for Qt Quick materials and effects."));
cmdLineParser.addOption(shortcutDefaultOption);
QCommandLineOption mslArgBufOption("msl-argument-buffers",
QObject::tr("Also generates MSL 2.0+ shaders that access textures and samplers via a Metal "
"argument buffer. Needed for shaders used with indirect draw calls that source "
"the draw count from a buffer."));
cmdLineParser.addOption(mslArgBufOption);
QCommandLineOption tessOption("msltess", QObject::tr("Indicates that a vertex shader is going to be used in a pipeline with tessellation. "
"Mandatory for vertex shaders planned to be used with tessellation when targeting Metal (--msl)."));
cmdLineParser.addOption(tessOption);
QCommandLineOption tessVertCountOption("tess-vertex-count", QObject::tr("The output vertex count from the tessellation control stage. "
"Mandatory for tessellation evaluation shaders planned to be used with Metal. "
"The default value is 3. "
"If it does not match the tess.control stage, the generated MSL code will not function as expected."),
QObject::tr("count"));
cmdLineParser.addOption(tessVertCountOption);
QCommandLineOption tessModeOption("tess-mode", QObject::tr("The tessellation mode: triangles or quads. Mandatory for tessellation control shaders planned to be used with Metal. "
"The default value is triangles. Isolines are not supported with Metal. "
"If it does not match the tess.evaluation stage, the generated MSL code will not function as expected."),
QObject::tr("mode"));
cmdLineParser.addOption(tessModeOption);
QCommandLineOption multiViewCountOption("view-count", QObject::tr("The number of views the shader is used with. num_views must be >= 2. "
"Mandatory when multiview rendering is used (gl_ViewIndex). "
"Set only for vertex shaders that really do rely on multiview (as the resulting asset is tied to num_views). "
"Can be set for fragment shaders too, to get QSHADER_VIEW_COUNT auto-defined. (useful for ensuring uniform buffer layouts)"),
QObject::tr("num_views"));
cmdLineParser.addOption(multiViewCountOption);
QCommandLineOption debugInfoOption("g", QObject::tr("Generate full debug info for SPIR-V and DXBC"));
cmdLineParser.addOption(debugInfoOption);
QCommandLineOption spirvOptOption("O", QObject::tr("Invoke spirv-opt (external tool) to optimize SPIR-V for performance."));
cmdLineParser.addOption(spirvOptOption);
QCommandLineOption outputOption({ "o", "output" },
QObject::tr("Output file for the shader pack."),
QObject::tr("filename"));
cmdLineParser.addOption(outputOption);
QCommandLineOption origFnOption("orig-file",
QObject::tr("Filename to be be used for dependency tracking instead of <file>."),
QObject::tr("filename"));
cmdLineParser.addOption(origFnOption);
QCommandLineOption qsbVersionOption("qsbversion",
QObject::tr("QSB version to use for the output file. By default the latest version is automatically used, "
"use only to bake compatibility versions. F.ex. 64 is Qt 6.4."),
QObject::tr("version"));
cmdLineParser.addOption(qsbVersionOption);
QCommandLineOption fxcOption({ "c", "fxc" }, QObject::tr("In combination with --hlsl invokes fxc (SM 5.0/5.1) or dxc (SM 6.0+) to store DXBC or DXIL instead of HLSL."));
cmdLineParser.addOption(fxcOption);
QCommandLineOption mtllibOption({ "t", "metallib" },
QObject::tr("In combination with --msl builds a Metal library with xcrun metal(lib) and stores that instead of the source. "
"Suitable only when targeting macOS, not iOS."));
cmdLineParser.addOption(mtllibOption);
QCommandLineOption mtllibIosOption({ "T", "metallib-ios" },
QObject::tr("In combination with --msl builds a Metal library with xcrun metal(lib) and stores that instead of the source. "
"Suitable only when targeting iOS, not macOS."));
cmdLineParser.addOption(mtllibIosOption);
QCommandLineOption defineOption({ "D", "define" }, QObject::tr("Define macro. This argument can be specified multiple times."), QObject::tr("name[=value]"));
cmdLineParser.addOption(defineOption);
QCommandLineOption perTargetCompileOption({ "p", "per-target" }, QObject::tr("Enable per-target compilation. (instead of source->SPIRV->targets, do "
"source->SPIRV->target separately for each target)"));
cmdLineParser.addOption(perTargetCompileOption);
QCommandLineOption dumpOption({ "d", "dump" }, QObject::tr("Switches to dump mode. Input file is expected to be a shader pack."));
cmdLineParser.addOption(dumpOption);
QCommandLineOption extractOption({ "x", "extract" }, QObject::tr("Switches to extract mode. Input file is expected to be a shader pack. "
"Result is written to the output specified by -o. "
"Pass -b to choose the batchable variant. "
"<what>=reflect|spirv,<version>|glsl,<version>|..."),
QObject::tr("what"));
cmdLineParser.addOption(extractOption);
QCommandLineOption replaceOption({ "r", "replace" },
QObject::tr("Switches to replace mode. Replaces the specified shader in the shader pack with the contents of a file. "
"This argument can be specified multiple times. "
"Pass -b to choose the batchable variant. "
"Also supports adding a shader for a target/variant that was not present before. "
"<what>=<target>,<filename> where <target>=spirv,<version>|glsl,<version>|..."),
QObject::tr("what"));
cmdLineParser.addOption(replaceOption);
QCommandLineOption eraseOption({ "e", "erase" },
QObject::tr("Switches to erase mode. Removes the specified shader from the shader pack. "
"Pass -b to choose the batchable variant. "
"<what>=spirv,<version>|glsl,<version>|..."),
QObject::tr("what"));
cmdLineParser.addOption(eraseOption);
QCommandLineOption silentOption({ "s", "silent" }, QObject::tr("Enables silent mode. Only fatal errors will be printed."));
cmdLineParser.addOption(silentOption);
QCommandLineOption depfileOption("depfile",
QObject::tr("Enables generating the depfile for the input "
"shaders, using the #include statements."),
QObject::tr("depfile"));
QCommandLineOption mediumpOption("mediump", QObject::tr("Default to medium precision for floats in fragment shaders instead of high. "
"GLSL ES only; ignored for everything else, including GLSL."));
cmdLineParser.addOption(mediumpOption);
cmdLineParser.addOption(depfileOption);
cmdLineParser.process(app);
if (cmdLineParser.positionalArguments().isEmpty()) {
cmdLineParser.showHelp();
return 0;
}
silent = cmdLineParser.isSet(silentOption);
QShaderBaker baker;
QFile depfile;
if (const QString depfilePath = cmdLineParser.value(depfileOption); !depfilePath.isEmpty()) {
QDir().mkpath(QFileInfo(depfilePath).path());
depfile.setFileName(depfilePath);
if (!depfile.open(QFile::WriteOnly | QFile::Truncate)) {
printError("Unable to create DEPFILE: '%s'", qPrintable(depfilePath));
return 1;
}
}
const bool depfileRequired = depfile.isOpen();
for (const QString &fn : cmdLineParser.positionalArguments()) {
auto qsbVersion = QShader::SerializedFormatVersion::Latest;
if (cmdLineParser.isSet(qsbVersionOption)) {
const QString qsbVersionString = cmdLineParser.value(qsbVersionOption);
if (qsbVersionString == QStringLiteral("64")) {
qsbVersion = QShader::SerializedFormatVersion::Qt_6_4;
} else if (qsbVersionString == QStringLiteral("65")) {
qsbVersion = QShader::SerializedFormatVersion::Qt_6_5;
} else if (qsbVersionString.toLower() != QStringLiteral("latest")) {
printError("Unknown Qt qsb version: %s", qPrintable(qsbVersionString));
printError("Available versions: 64, 65, latest");
return 1;
}
}
if (cmdLineParser.isSet(dumpOption)
|| cmdLineParser.isSet(extractOption)
|| cmdLineParser.isSet(replaceOption)
|| cmdLineParser.isSet(eraseOption))
{
QByteArray buf = readFile(fn, FileType::Binary);
if (!buf.isEmpty()) {
QShader bs = QShader::fromSerialized(buf);
if (bs.isValid()) {
QShader::Variant variant = QShader::StandardShader;
if (cmdLineParser.isSet(batchableOption))
variant = QShader::BatchableVertexShader;
else if (cmdLineParser.isSet(mslArgBufOption))
variant = QShader::ArgumentBufferShader;
if (cmdLineParser.isSet(dumpOption)) {
dump(bs);
} else if (cmdLineParser.isSet(extractOption)) {
if (cmdLineParser.isSet(outputOption)) {
if (!extract(bs, cmdLineParser.value(extractOption), variant, cmdLineParser.value(outputOption)))
return 1;
} else {
printError("No output file specified");
}
} else if (cmdLineParser.isSet(replaceOption)) {
if (!addOrReplace(bs, cmdLineParser.values(replaceOption), variant, fn, qsbVersion))
return 1;
} else if (cmdLineParser.isSet(eraseOption)) {
if (!remove(bs, cmdLineParser.values(eraseOption), variant, fn, qsbVersion))
return 1;
}
} else {
printError("Failed to deserialize %s (or the shader pack is empty)", qPrintable(fn));
}
}
continue;
}
if (depfileRequired) {
QString originalFilename = fn;
if (cmdLineParser.isSet(origFnOption))
originalFilename = cmdLineParser.value(origFnOption);
if (!generateDepfile(depfile, fn, originalFilename, cmdLineParser.value(outputOption)))
return 1;
}
baker.setSourceFileName(fn);
baker.setPerTargetCompilation(cmdLineParser.isSet(perTargetCompileOption));
QShaderBaker::SpirvOptions spirvOptions;
// We either want full debug info, or none at all (so no variable names
// either - that too can be stripped after the SPIRV-Cross stage).
if (cmdLineParser.isSet(debugInfoOption))
spirvOptions |= QShaderBaker::SpirvOption::GenerateFullDebugInfo;
else
spirvOptions |= QShaderBaker::SpirvOption::StripDebugAndVarInfo;
baker.setSpirvOptions(spirvOptions);
QShaderBaker::GlslOptions glslOptions;
if (cmdLineParser.isSet(mediumpOption))
glslOptions |= QShaderBaker::GlslOption::GlslEsFragDefaultFloatPrecisionMedium;
baker.setGlslOptions(glslOptions);
QList<QShader::Variant> variants;
variants << QShader::StandardShader;
if (cmdLineParser.isSet(batchableOption)) {
variants << QShader::BatchableVertexShader;
if (cmdLineParser.isSet(batchLocOption))
baker.setBatchableVertexShaderExtraInputLocation(cmdLineParser.value(batchLocOption).toInt());
}
if (cmdLineParser.isSet(tessOption)) {
variants << QShader::UInt16IndexedVertexAsComputeShader
<< QShader::UInt32IndexedVertexAsComputeShader
<< QShader::NonIndexedVertexAsComputeShader;
}
if (cmdLineParser.isSet(mslArgBufOption))
variants << QShader::ArgumentBufferShader;
if (cmdLineParser.isSet(tessModeOption)) {
const QString tessModeStr = cmdLineParser.value(tessModeOption).toLower();
if (tessModeStr == QLatin1String("triangles"))
baker.setTessellationMode(QShaderDescription::TrianglesTessellationMode);
else if (tessModeStr == QLatin1String("quads"))
baker.setTessellationMode(QShaderDescription::QuadTessellationMode);
else
qWarning("Unknown tessellation mode '%s'", qPrintable(tessModeStr));
}
if (cmdLineParser.isSet(tessVertCountOption))
baker.setTessellationOutputVertexCount(cmdLineParser.value(tessVertCountOption).toInt());
if (cmdLineParser.isSet(multiViewCountOption))
baker.setMultiViewCount(cmdLineParser.value(multiViewCountOption).toInt());
baker.setGeneratedShaderVariants(variants);
QList<QShaderBaker::GeneratedShader> genShaders;
genShaders << std::make_pair(QShader::SpirvShader, QShaderVersion(100));
if (cmdLineParser.isSet(glslOption)) {
const QStringList versions = cmdLineParser.value(glslOption).trimmed().split(',');
for (QString version : versions) {
QShaderVersion::Flags flags;
if (version.endsWith(QLatin1String(" es"))) {
version = version.left(version.size() - 3);
flags |= QShaderVersion::GlslEs;
} else if (version.endsWith(QLatin1String("es"))) {
version = version.left(version.size() - 2);
flags |= QShaderVersion::GlslEs;
}
bool ok = false;
int v = version.toInt(&ok);
if (ok)
genShaders << std::make_pair(QShader::GlslShader, QShaderVersion(v, flags));
else
printError("Ignoring invalid GLSL version %s", qPrintable(version));
}
}
if (cmdLineParser.isSet(hlslOption)) {
const QStringList versions = cmdLineParser.value(hlslOption).trimmed().split(',');
for (QString version : versions) {
bool ok = false;
int v = version.toInt(&ok);
if (ok) {
genShaders << std::make_pair(QShader::HlslShader, QShaderVersion(v));
} else {
printError("Ignoring invalid HLSL (Shader Model) version %s",
qPrintable(version));
}
}
}
if (cmdLineParser.isSet(mslOption)) {
const QStringList versions = cmdLineParser.value(mslOption).trimmed().split(',');
for (QString version : versions) {
bool ok = false;
int v = version.toInt(&ok);
if (ok)
genShaders << std::make_pair(QShader::MslShader, QShaderVersion(v));
else
printError("Ignoring invalid MSL version %s", qPrintable(version));
}
}
if (cmdLineParser.isSet(shortcutDefaultOption)) {
for (const QShaderBaker::GeneratedShader &genShaderEntry :
{
std::make_pair(QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs)),
std::make_pair(QShader::GlslShader, QShaderVersion(120)),
std::make_pair(QShader::GlslShader, QShaderVersion(150)),
std::make_pair(QShader::HlslShader, QShaderVersion(50)),
std::make_pair(QShader::MslShader, QShaderVersion(12))
})
{
if (!genShaders.contains(genShaderEntry))
genShaders << genShaderEntry;
}
}
if (cmdLineParser.isSet(mslArgBufOption)) {
const bool haveMsl20 = std::any_of(genShaders.cbegin(), genShaders.cend(),
[](const QShaderBaker::GeneratedShader &g) {
return g.first == QShader::MslShader && g.second.version() >= 20;
});
if (!haveMsl20)
printError("--msl-argument-buffers needs an MSL version of at least 20");
}
baker.setGeneratedShaders(genShaders);
if (cmdLineParser.isSet(defineOption)) {
QByteArray preamble;
const QStringList defines = cmdLineParser.values(defineOption);
for (const QString &def : defines) {
const QStringList defs = def.split(QLatin1Char('='), Qt::SkipEmptyParts);
if (!defs.isEmpty()) {
preamble.append("#define");
for (const QString &s : defs) {
preamble.append(' ');
preamble.append(s.toUtf8());
}
preamble.append('\n');
}
}
baker.setPreamble(preamble);
}
QShader bs = baker.bake();
if (!bs.isValid()) {
printError("Shader baking failed: %s", qPrintable(baker.errorMessage()));
return 1;
}
// post processing: run spirv-opt when requested for each entry with
// type SpirvShader and replace the contents. Not having spirv-opt
// available must not be a fatal error, skip it if the process fails.
if (cmdLineParser.isSet(spirvOptOption)) {
QTemporaryDir tempDir;
if (!tempDir.isValid()) {
printError("Failed to create temporary directory");
return 1;
}
auto skeys = bs.availableShaders();
for (QShaderKey &k : skeys) {
if (k.source() == QShader::SpirvShader) {
QShaderCode s = bs.shader(k);
const QString tmpIn = writeTemp(tempDir, QLatin1String("qsb_spv_temp"), s, FileType::Binary);
const QString tmpOut = tempDir.path() + QLatin1String("/qsb_spv_temp_out");
if (tmpIn.isEmpty())
break;
const QStringList arguments({
QLatin1String("-O"),
QDir::toNativeSeparators(tmpIn),
QLatin1String("-o"), QDir::toNativeSeparators(tmpOut)
});
QByteArray output;
QByteArray errorOutput;
bool success = runProcess(QLatin1String("spirv-opt"), arguments, &output, &errorOutput);
if (success) {
const QByteArray bytecode = readFile(tmpOut, FileType::Binary);
if (!bytecode.isEmpty())
replaceShaderContents(&bs, k, QShader::SpirvShader, bytecode, s.entryPoint());
} else {
if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
printError("%s\n%s",
qPrintable(output.constData()),
qPrintable(errorOutput.constData()));
}
}
}
}
}
// post processing: run fxc/dxc when requested for each entry with type
// HlslShader and add a new entry with type DxbcShader/DxilShader and remove the
// original HlslShader entry
if (cmdLineParser.isSet(fxcOption)) {
QTemporaryDir tempDir;
if (!tempDir.isValid()) {
printError("Failed to create temporary directory");
return 1;
}
auto skeys = bs.availableShaders();
for (QShaderKey &k : skeys) {
if (k.source() == QShader::HlslShader) {
// For Shader Model 6.0 and higher, use dxc, fxc will not compile that anymore.
const bool useDxc = k.sourceVersion().version() >= 60;
QShaderCode s = bs.shader(k);
const QString tmpIn = writeTemp(tempDir, QLatin1String("qsb_hlsl_temp"), s, FileType::Text);
const QString tmpOut = tempDir.path() + QLatin1String("/qsb_hlsl_temp_out");
if (tmpIn.isEmpty())
break;
const QByteArray typeArg = fxcProfile(bs, k);
QStringList arguments({
QLatin1String("/nologo"),
QLatin1String("/E"), QString::fromLocal8Bit(s.entryPoint()),
QLatin1String("/T"), QString::fromLocal8Bit(typeArg),
QLatin1String("/Fo"), QDir::toNativeSeparators(tmpOut)
});
if (cmdLineParser.isSet(debugInfoOption))
arguments << QLatin1String("/Od") << QLatin1String("/Zi");
arguments.append(QDir::toNativeSeparators(tmpIn));
QByteArray output;
QByteArray errorOutput;
const QString compilerName = useDxc ? QLatin1String("dxc") : QLatin1String("fxc");
bool success = runProcess(compilerName, arguments, &output, &errorOutput);
if (success) {
const QByteArray bytecode = readFile(tmpOut, FileType::Binary);
if (!bytecode.isEmpty()) {
const QShader::Source bytecodeType = useDxc ? QShader::DxilShader : QShader::DxbcShader;
replaceShaderContents(&bs, k, bytecodeType, bytecode, s.entryPoint());
}
} else {
if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
printError("%s\n%s",
qPrintable(output.constData()),
qPrintable(errorOutput.constData()));
}
}
}
}
}
// post processing: run xcrun metal and metallib when requested for
// each entry with type MslShader and add a new entry with type
// MetalLibShader and remove the original MslShader entry
if (cmdLineParser.isSet(mtllibOption) || cmdLineParser.isSet(mtllibIosOption)) {
const bool isIos = cmdLineParser.isSet(mtllibIosOption);
QTemporaryDir tempDir;
if (!tempDir.isValid()) {
printError("Failed to create temporary directory");
return 1;
}
auto skeys = bs.availableShaders();
for (const QShaderKey &k : skeys) {
if (k.source() == QShader::MslShader) {
QShaderCode s = bs.shader(k);
// having the .metal file extension may matter for the external tools here, so use that
const QString tmpIn = writeTemp(tempDir, QLatin1String("qsb_msl_temp.metal"), s, FileType::Text);
const QString tmpInterm = tempDir.path() + QLatin1String("/qsb_msl_temp_air");
const QString tmpOut = tempDir.path() + QLatin1String("/qsb_msl_temp_out");
if (tmpIn.isEmpty())
break;
const QString binary = QLatin1String("xcrun");
const QStringList baseArguments = {
QLatin1String("-sdk"),
isIos ? QLatin1String("iphoneos") : QLatin1String("macosx")
};
QStringList arguments = baseArguments;
const QString langVerFmt = QLatin1String("-std=%1-metal%2.%3");
const QString langPlatform = isIos ? QLatin1String("ios") : QLatin1String("macos");
const int langMajor = k.sourceVersion().version() / 10;
const int langMinor = k.sourceVersion().version() % 10;
const QString langVer = langVerFmt.arg(langPlatform).arg(langMajor).arg(langMinor);
arguments.append({
QLatin1String("metal"),
QLatin1String("-c"),
langVer,
QDir::toNativeSeparators(tmpIn),
QLatin1String("-o"),
QDir::toNativeSeparators(tmpInterm)
});
QByteArray output;
QByteArray errorOutput;
bool success = runProcess(binary, arguments, &output, &errorOutput);
if (success) {
arguments = baseArguments;
arguments.append({QLatin1String("metallib"), QDir::toNativeSeparators(tmpInterm),
QLatin1String("-o"), QDir::toNativeSeparators(tmpOut)});
output.clear();
errorOutput.clear();
success = runProcess(binary, arguments, &output, &errorOutput);
if (success) {
const QByteArray bytecode = readFile(tmpOut, FileType::Binary);
if (!bytecode.isEmpty())
replaceShaderContents(&bs, k, QShader::MetalLibShader, bytecode, s.entryPoint());
} else {
if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
printError("%s\n%s",
qPrintable(output.constData()),
qPrintable(errorOutput.constData()));
}
}
} else {
if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
printError("%s\n%s",
qPrintable(output.constData()),
qPrintable(errorOutput.constData()));
}
}
}
}
}
if (cmdLineParser.isSet(outputOption))
writeToFile(bs.serialized(qsbVersion), cmdLineParser.value(outputOption), FileType::Binary);
}
return 0;
}