1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include <QtCore/qcoreapplication.h>
5#include <QtCore/qcommandlineparser.h>
6#include <QtCore/qtextstream.h>
7#include <QtCore/qfile.h>
8#include <QtCore/qdir.h>
9#include <QtCore/qtemporarydir.h>
10#include <QtCore/qdebug.h>
11#include <QtCore/qlibraryinfo.h>
12#include <QtGui/private/qshader_p.h>
13#include <rhi/qshaderbaker.h>
14
15#if QT_CONFIG(process)
16#include <QtCore/qprocess.h>
17#endif
18
19#include <cstdarg>
20#include <cstdio>
21
22// All qDebug must be guarded by !silent. For qWarnings, only the most
23// fatal ones should be unconditional; warnings from external tool
24// invocations must be guarded with !silent.
25static bool silent = false;
26
27enum class FileType
28{
29 Binary,
30 Text
31};
32
33static void printError(const char *msg, ...)
34{
35 va_list arglist;
36 va_start(arglist, msg);
37 vfprintf(stderr, format: msg, arg: arglist);
38 fputs(s: "\n", stderr);
39 va_end(arglist);
40}
41
42static bool writeToFile(const QByteArray &buf, const QString &filename, FileType fileType)
43{
44 QDir().mkpath(dirPath: QFileInfo(filename).path());
45 QFile f(filename);
46 QIODevice::OpenMode flags = QIODevice::WriteOnly | QIODevice::Truncate;
47 if (fileType == FileType::Text)
48 flags |= QIODevice::Text;
49 if (!f.open(flags)) {
50 printError(msg: "Failed to open %s for writing", qPrintable(filename));
51 return false;
52 }
53 f.write(data: buf);
54 return true;
55}
56
57static QByteArray readFile(const QString &filename, FileType fileType)
58{
59 QFile f(filename);
60 QIODevice::OpenMode flags = QIODevice::ReadOnly;
61 if (fileType == FileType::Text)
62 flags |= QIODevice::Text;
63 if (!f.open(flags)) {
64 printError(msg: "Failed to open %s", qPrintable(filename));
65 return QByteArray();
66 }
67 return f.readAll();
68}
69
70static QString writeTemp(const QTemporaryDir &tempDir, const QString &filename, const QShaderCode &s, FileType fileType)
71{
72 const QString fullPath = tempDir.path() + QLatin1String("/") + filename;
73 if (writeToFile(buf: s.shader(), filename: fullPath, fileType))
74 return fullPath;
75 else
76 return QString();
77}
78
79static bool runProcess(const QString &binary, const QStringList &arguments,
80 QByteArray *output, QByteArray *errorOutput)
81{
82#if QT_CONFIG(process)
83 QProcess p;
84 p.start(program: binary, arguments);
85 const QString cmd = binary + QLatin1Char(' ') + arguments.join(sep: QLatin1Char(' '));
86 if (!silent)
87 qDebug(msg: "%s", qPrintable(cmd));
88 if (!p.waitForStarted()) {
89 if (!silent)
90 printError(msg: "Failed to run %s: %s", qPrintable(cmd), qPrintable(p.errorString()));
91 return false;
92 }
93 if (!p.waitForFinished()) {
94 if (!silent)
95 printError(msg: "%s timed out", qPrintable(cmd));
96 return false;
97 }
98
99 if (p.exitStatus() == QProcess::CrashExit) {
100 if (!silent)
101 printError(msg: "%s crashed", qPrintable(cmd));
102 return false;
103 }
104
105 *output = p.readAllStandardOutput();
106 *errorOutput = p.readAllStandardError();
107
108 if (p.exitCode() != 0) {
109 if (!silent)
110 printError(msg: "%s returned non-zero error code %d", qPrintable(cmd), p.exitCode());
111 return false;
112 }
113
114 return true;
115#else
116 Q_UNUSED(binary);
117 Q_UNUSED(arguments);
118 Q_UNUSED(output);
119 *errorOutput = QByteArrayLiteral("QProcess not supported on this platform");
120 return false;
121#endif
122}
123
124static QString stageStr(QShader::Stage stage)
125{
126 switch (stage) {
127 case QShader::VertexStage:
128 return QStringLiteral("Vertex");
129 case QShader::TessellationControlStage:
130 return QStringLiteral("TessellationControl");
131 case QShader::TessellationEvaluationStage:
132 return QStringLiteral("TessellationEvaluation");
133 case QShader::GeometryStage:
134 return QStringLiteral("Geometry");
135 case QShader::FragmentStage:
136 return QStringLiteral("Fragment");
137 case QShader::ComputeStage:
138 return QStringLiteral("Compute");
139 default:
140 Q_UNREACHABLE();
141 }
142}
143
144static QString sourceStr(QShader::Source source)
145{
146 switch (source) {
147 case QShader::SpirvShader:
148 return QStringLiteral("SPIR-V");
149 case QShader::GlslShader:
150 return QStringLiteral("GLSL");
151 case QShader::HlslShader:
152 return QStringLiteral("HLSL");
153 case QShader::DxbcShader:
154 return QStringLiteral("DXBC");
155 case QShader::MslShader:
156 return QStringLiteral("MSL");
157 case QShader::DxilShader:
158 return QStringLiteral("DXIL");
159 case QShader::MetalLibShader:
160 return QStringLiteral("metallib");
161 case QShader::WgslShader:
162 return QStringLiteral("WGSL");
163 default:
164 Q_UNREACHABLE();
165 }
166}
167
168static QString sourceVersionStr(const QShaderVersion &v)
169{
170 QString s = v.version() ? QString::number(v.version()) : QString();
171 if (v.flags().testFlag(flag: QShaderVersion::GlslEs))
172 s += QLatin1String(" es");
173
174 return s;
175}
176
177static QString sourceVariantStr(const QShader::Variant &v)
178{
179 switch (v) {
180 case QShader::StandardShader:
181 return QLatin1String("Standard");
182 case QShader::BatchableVertexShader:
183 return QLatin1String("Batchable");
184 case QShader::UInt32IndexedVertexAsComputeShader:
185 return QLatin1String("UInt32IndexedVertexAsCompute");
186 case QShader::UInt16IndexedVertexAsComputeShader:
187 return QLatin1String("UInt16IndexedVertexAsCompute");
188 case QShader::NonIndexedVertexAsComputeShader:
189 return QLatin1String("NonIndexedVertexAsCompute");
190 default:
191 Q_UNREACHABLE();
192 }
193}
194
195static void dump(const QShader &bs)
196{
197 QTextStream ts(stdout);
198 ts << "Stage: " << stageStr(stage: bs.stage()) << "\n";
199 ts << "QSB_VERSION: " << QShaderPrivate::get(s: &bs)->qsbVersion << "\n";
200 const QList<QShaderKey> keys = bs.availableShaders();
201 ts << "Has " << keys.size() << " shaders:\n";
202 for (int i = 0; i < keys.size(); ++i) {
203 ts << " Shader " << i << ": " << sourceStr(source: keys[i].source())
204 << " " << sourceVersionStr(v: keys[i].sourceVersion())
205 << " [" << sourceVariantStr(v: keys[i].sourceVariant()) << "]\n";
206 }
207 ts << "\n";
208 ts << "Reflection info: " << bs.description().toJson() << "\n\n";
209 for (int i = 0; i < keys.size(); ++i) {
210 ts << "Shader " << i << ": " << sourceStr(source: keys[i].source())
211 << " " << sourceVersionStr(v: keys[i].sourceVersion())
212 << " [" << sourceVariantStr(v: keys[i].sourceVariant()) << "]\n";
213 QShaderCode shader = bs.shader(key: keys[i]);
214 if (!shader.entryPoint().isEmpty())
215 ts << "Entry point: " << shader.entryPoint() << "\n";
216 QShader::NativeResourceBindingMap nativeResMap = bs.nativeResourceBindingMap(key: keys[i]);
217 if (!nativeResMap.isEmpty()) {
218 ts << "Native resource binding map:\n";
219 for (auto mapIt = nativeResMap.cbegin(), mapItEnd = nativeResMap.cend(); mapIt != mapItEnd; ++mapIt)
220 ts << mapIt.key() << " -> [" << mapIt.value().first << ", " << mapIt.value().second << "]\n";
221 }
222 QShader::SeparateToCombinedImageSamplerMappingList samplerMapList = bs.separateToCombinedImageSamplerMappingList(key: keys[i]);
223 if (!samplerMapList.isEmpty()) {
224 ts << "Mapping table for auto-generated combined image samplers:\n";
225 for (auto listIt = samplerMapList.cbegin(), listItEnd = samplerMapList.cend(); listIt != listItEnd; ++listIt)
226 ts << "\"" << listIt->combinedSamplerName << "\" -> [" << listIt->textureBinding << ", " << listIt->samplerBinding << "]\n";
227 }
228 QShader::NativeShaderInfo shaderInfo = bs.nativeShaderInfo(key: keys[i]);
229 if (shaderInfo.flags)
230 ts << "Native shader info flags: " << shaderInfo.flags << "\n";
231 if (!shaderInfo.extraBufferBindings.isEmpty()) {
232 ts << "Native shader extra buffer bindings:\n";
233 for (auto mapIt = shaderInfo.extraBufferBindings.cbegin(), mapItEnd = shaderInfo.extraBufferBindings.cend();
234 mapIt != mapItEnd; ++mapIt)
235 {
236 static struct {
237 QShaderPrivate::MslNativeShaderInfoExtraBufferBindings key;
238 const char *str;
239 } ebbNames[] = {
240 { .key: QShaderPrivate::MslTessVertIndicesBufferBinding, .str: "tessellation(vert)-index-buffer-binding" },
241 { .key: QShaderPrivate::MslTessVertTescOutputBufferBinding, .str: "tessellation(vert/tesc)-output-buffer-binding" },
242 { .key: QShaderPrivate::MslTessTescTessLevelBufferBinding, .str: "tessellation(tesc)-level-buffer-binding" },
243 { .key: QShaderPrivate::MslTessTescPatchOutputBufferBinding, .str: "tessellation(tesc)-patch-output-buffer-binding" },
244 { .key: QShaderPrivate::MslTessTescParamsBufferBinding, .str: "tessellation(tesc)-params-buffer-binding" },
245 { .key: QShaderPrivate::MslTessTescInputBufferBinding, .str: "tessellation(tesc)-input-buffer-binding" },
246 { .key: QShaderPrivate::MslBufferSizeBufferBinding, .str: "buffer-size-buffer-binding" }
247 };
248 bool known = false;
249 for (size_t i = 0; i < sizeof(ebbNames) / sizeof(ebbNames[0]); ++i) {
250 if (ebbNames[i].key == mapIt.key()) {
251 ts << "[" << ebbNames[i].str << "] = " << mapIt.value() << "\n";
252 known = true;
253 break;
254 }
255 }
256 if (!known)
257 ts << "[" << mapIt.key() << "] = " << mapIt.value() << "\n";
258 }
259 }
260
261 ts << "Contents:\n";
262 switch (keys[i].source()) {
263 case QShader::SpirvShader:
264 case QShader::DxbcShader:
265 case QShader::DxilShader:
266 case QShader::MetalLibShader:
267 ts << "Binary of " << shader.shader().size() << " bytes\n\n";
268 break;
269 default:
270 ts << shader.shader() << "\n";
271 break;
272 }
273 ts << "\n************************************\n\n";
274 }
275}
276
277static QShaderKey shaderKeyFromWhatSpec(const QString &what, QShader::Variant variant)
278{
279 const QStringList typeAndVersion = what.split(sep: QLatin1Char(','), behavior: Qt::SkipEmptyParts);
280 if (typeAndVersion.size() < 2)
281 return {};
282
283 QShader::Source src;
284 if (typeAndVersion[0] == QLatin1String("spirv"))
285 src = QShader::SpirvShader;
286 else if (typeAndVersion[0] == QLatin1String("glsl"))
287 src = QShader::GlslShader;
288 else if (typeAndVersion[0] == QLatin1String("hlsl"))
289 src = QShader::HlslShader;
290 else if (typeAndVersion[0] == QLatin1String("msl"))
291 src = QShader::MslShader;
292 else if (typeAndVersion[0] == QLatin1String("dxbc"))
293 src = QShader::DxbcShader;
294 else if (typeAndVersion[0] == QLatin1String("dxil"))
295 src = QShader::DxilShader;
296 else if (typeAndVersion[0] == QLatin1String("metallib"))
297 src = QShader::MetalLibShader;
298 else if (typeAndVersion[0] == QLatin1String("wgsl"))
299 src = QShader::WgslShader;
300 else
301 return {};
302
303 QShaderVersion::Flags flags;
304 QString version = typeAndVersion[1];
305 if (version.endsWith(s: QLatin1String(" es"))) {
306 version = version.left(n: version.size() - 3);
307 flags |= QShaderVersion::GlslEs;
308 } else if (version.endsWith(s: QLatin1String("es"))) {
309 version = version.left(n: version.size() - 2);
310 flags |= QShaderVersion::GlslEs;
311 }
312 const int ver = version.toInt();
313
314 return { src, { ver, flags }, variant };
315}
316
317static bool extract(const QShader &bs, const QString &what, QShader::Variant variant, const QString &outfn)
318{
319 if (what == QLatin1String("reflect")) {
320 const QByteArray reflect = bs.description().toJson();
321 if (!writeToFile(buf: reflect, filename: outfn, fileType: FileType::Text))
322 return false;
323 if (!silent)
324 qDebug(msg: "Reflection data written to %s", qPrintable(outfn));
325 return true;
326 }
327
328 const QShaderKey key = shaderKeyFromWhatSpec(what, variant);
329 const QShaderCode code = bs.shader(key);
330 if (code.shader().isEmpty())
331 return false;
332 if (!writeToFile(buf: code.shader(), filename: outfn, fileType: FileType::Binary))
333 return false;
334 if (!silent) {
335 qDebug(msg: "%s %d%s code (variant %s) written to %s. Entry point is '%s'.",
336 qPrintable(sourceStr(key.source())),
337 key.sourceVersion().version(),
338 key.sourceVersion().flags().testFlag(flag: QShaderVersion::GlslEs) ? " es" : "",
339 qPrintable(sourceVariantStr(key.sourceVariant())),
340 qPrintable(outfn), code.entryPoint().constData());
341 }
342 return true;
343}
344
345static bool addOrReplace(const QShader &shaderPack,
346 const QStringList &whatList,
347 QShader::Variant variant,
348 const QString &outfn,
349 QShader::SerializedFormatVersion qsbVersion)
350{
351 QShader workShaderPack = shaderPack;
352 for (const QString &what : whatList) {
353 const QStringList spec = what.split(sep: QLatin1Char(','), behavior: Qt::SkipEmptyParts);
354 if (spec.size() < 3) {
355 printError(msg: "Invalid replace spec '%s'", qPrintable(what));
356 return false;
357 }
358
359 const QShaderKey key = shaderKeyFromWhatSpec(what, variant);
360 const QString fn = spec[2];
361
362 const QByteArray buf = readFile(filename: fn, fileType: FileType::Binary);
363 if (buf.isEmpty())
364 return false;
365
366 // Does not matter if 'key' was present before or not, we support both
367 // replacing and adding using the same qsb -r ... syntax.
368
369 const QShaderCode code(buf, QByteArrayLiteral("main"));
370 workShaderPack.setShader(key, shader: code);
371
372 if (!silent) {
373 qDebug(msg: "Replaced %s %d%s (variant %s) with %s. Entry point is 'main'.",
374 qPrintable(sourceStr(key.source())),
375 key.sourceVersion().version(),
376 key.sourceVersion().flags().testFlag(flag: QShaderVersion::GlslEs) ? " es" : "",
377 qPrintable(sourceVariantStr(key.sourceVariant())),
378 qPrintable(fn));
379 }
380 }
381 return writeToFile(buf: workShaderPack.serialized(version: qsbVersion), filename: outfn, fileType: FileType::Binary);
382}
383
384static bool remove(const QShader &shaderPack,
385 const QStringList &whatList,
386 QShader::Variant variant,
387 const QString &outfn,
388 QShader::SerializedFormatVersion qsbVersion)
389{
390 QShader workShaderPack = shaderPack;
391 for (const QString &what : whatList) {
392 const QShaderKey key = shaderKeyFromWhatSpec(what, variant);
393 if (!workShaderPack.availableShaders().contains(t: key))
394 continue;
395 workShaderPack.removeShader(key);
396 if (!silent) {
397 qDebug(msg: "Removed %s %d%s (variant %s).",
398 qPrintable(sourceStr(key.source())),
399 key.sourceVersion().version(),
400 key.sourceVersion().flags().testFlag(flag: QShaderVersion::GlslEs) ? " es" : "",
401 qPrintable(sourceVariantStr(key.sourceVariant())));
402 }
403 }
404 return writeToFile(buf: workShaderPack.serialized(version: qsbVersion), filename: outfn, fileType: FileType::Binary);
405}
406
407static QByteArray fxcProfile(const QShader &bs, const QShaderKey &k)
408{
409 QByteArray t;
410
411 switch (bs.stage()) {
412 case QShader::VertexStage:
413 t += QByteArrayLiteral("vs_");
414 break;
415 case QShader::TessellationControlStage:
416 t += QByteArrayLiteral("hs_");
417 break;
418 case QShader::TessellationEvaluationStage:
419 t += QByteArrayLiteral("ds_");
420 break;
421 case QShader::GeometryStage:
422 t += QByteArrayLiteral("gs_");
423 break;
424 case QShader::FragmentStage:
425 t += QByteArrayLiteral("ps_");
426 break;
427 case QShader::ComputeStage:
428 t += QByteArrayLiteral("cs_");
429 break;
430 default:
431 break;
432 }
433
434 const int major = k.sourceVersion().version() / 10;
435 const int minor = k.sourceVersion().version() % 10;
436 t += QByteArray::number(major);
437 t += '_';
438 t += QByteArray::number(minor);
439
440 return t;
441}
442
443static void replaceShaderContents(QShader *shaderPack,
444 const QShaderKey &originalKey,
445 QShader::Source newType,
446 const QByteArray &contents,
447 const QByteArray &entryPoint)
448{
449 QShaderKey newKey = originalKey;
450 newKey.setSource(newType);
451 QShaderCode shader(contents, entryPoint);
452 shaderPack->setShader(key: newKey, shader);
453 if (newKey != originalKey) {
454 shaderPack->setResourceBindingMap(key: newKey, map: shaderPack->nativeResourceBindingMap(key: originalKey));
455 shaderPack->removeResourceBindingMap(key: originalKey);
456 shaderPack->setSeparateToCombinedImageSamplerMappingList(key: newKey, list: shaderPack->separateToCombinedImageSamplerMappingList(key: originalKey));
457 shaderPack->removeSeparateToCombinedImageSamplerMappingList(key: originalKey);
458 shaderPack->removeShader(key: originalKey);
459 }
460}
461
462int main(int argc, char **argv)
463{
464 QCoreApplication app(argc, argv);
465
466 QCommandLineParser cmdLineParser;
467 const QString appDesc = QString::asprintf(format: "Qt Shader Baker (using QShader from Qt %s)", qVersion());
468 cmdLineParser.setApplicationDescription(appDesc);
469 app.setApplicationVersion(QLatin1String(QT_VERSION_STR));
470 cmdLineParser.addHelpOption();
471 cmdLineParser.addVersionOption();
472 cmdLineParser.addPositionalArgument(name: QLatin1String("file"),
473 description: QObject::tr(s: "Vulkan GLSL source file to compile. The file extension determines the shader stage, and can be one of "
474 ".vert, .tesc, .tese, .frag, .comp. "
475 "Note: Tessellation control/evaluation is not supported with HLSL, instead use -r to inject handcrafted hull/domain shaders. "
476 "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."
477 ),
478 syntax: QObject::tr(s: "file"));
479 QCommandLineOption batchableOption({ "b", "batchable" }, QObject::tr(s: "Also generates rewritten vertex shader for Qt Quick scene graph batching."));
480 cmdLineParser.addOption(commandLineOption: batchableOption);
481 QCommandLineOption batchLocOption("zorder-loc",
482 QObject::tr(s: "The extra vertex input location when rewriting for batching. Defaults to 7."),
483 QObject::tr(s: "location"));
484 cmdLineParser.addOption(commandLineOption: batchLocOption);
485 QCommandLineOption glslOption("glsl",
486 QObject::tr(s: "Comma separated list of GLSL versions to generate. (for example, \"100 es,120,330\")"),
487 QObject::tr(s: "versions"));
488 cmdLineParser.addOption(commandLineOption: glslOption);
489 QCommandLineOption hlslOption("hlsl",
490 QObject::tr(s: "Comma separated list of HLSL (Shader Model) versions to generate. F.ex. 50 is 5.0, 51 is 5.1."),
491 QObject::tr(s: "versions"));
492 cmdLineParser.addOption(commandLineOption: hlslOption);
493 QCommandLineOption mslOption("msl",
494 QObject::tr(s: "Comma separated list of Metal Shading Language versions to generate. F.ex. 12 is 1.2, 20 is 2.0."),
495 QObject::tr(s: "versions"));
496 cmdLineParser.addOption(commandLineOption: mslOption);
497 QCommandLineOption shortcutDefaultOption("qt6", QObject::tr(s: "Equivalent to --glsl \"100 es,120,150\" --hlsl 50 --msl 12. "
498 "This set is commonly used with shaders for Qt Quick materials and effects."));
499 cmdLineParser.addOption(commandLineOption: shortcutDefaultOption);
500 QCommandLineOption tessOption("msltess", QObject::tr(s: "Indicates that a vertex shader is going to be used in a pipeline with tessellation. "
501 "Mandatory for vertex shaders planned to be used with tessellation when targeting Metal (--msl)."));
502 cmdLineParser.addOption(commandLineOption: tessOption);
503 QCommandLineOption tessVertCountOption("tess-vertex-count", QObject::tr(s: "The output vertex count from the tessellation control stage. "
504 "Mandatory for tessellation evaluation shaders planned to be used with Metal. "
505 "The default value is 3. "
506 "If it does not match the tess.control stage, the generated MSL code will not function as expected."),
507 QObject::tr(s: "count"));
508 cmdLineParser.addOption(commandLineOption: tessVertCountOption);
509 QCommandLineOption tessModeOption("tess-mode", QObject::tr(s: "The tessellation mode: triangles or quads. Mandatory for tessellation control shaders planned to be used with Metal. "
510 "The default value is triangles. Isolines are not supported with Metal. "
511 "If it does not match the tess.evaluation stage, the generated MSL code will not function as expected."),
512 QObject::tr(s: "mode"));
513 cmdLineParser.addOption(commandLineOption: tessModeOption);
514 QCommandLineOption debugInfoOption("g", QObject::tr(s: "Generate full debug info for SPIR-V and DXBC"));
515 cmdLineParser.addOption(commandLineOption: debugInfoOption);
516 QCommandLineOption spirvOptOption("O", QObject::tr(s: "Invoke spirv-opt (external tool) to optimize SPIR-V for performance."));
517 cmdLineParser.addOption(commandLineOption: spirvOptOption);
518 QCommandLineOption outputOption({ "o", "output" },
519 QObject::tr(s: "Output file for the shader pack."),
520 QObject::tr(s: "filename"));
521 cmdLineParser.addOption(commandLineOption: outputOption);
522 QCommandLineOption qsbVersionOption("qsbversion",
523 QObject::tr(s: "QSB version to use for the output file. By default the latest version is automatically used, "
524 "use only to bake compatibility versions. F.ex. 64 is Qt 6.4."),
525 QObject::tr(s: "version"));
526 cmdLineParser.addOption(commandLineOption: qsbVersionOption);
527 QCommandLineOption fxcOption({ "c", "fxc" }, QObject::tr(s: "In combination with --hlsl invokes fxc to store DXBC instead of HLSL."));
528 cmdLineParser.addOption(commandLineOption: fxcOption);
529 QCommandLineOption mtllibOption({ "t", "metallib" },
530 QObject::tr(s: "In combination with --msl builds a Metal library with xcrun metal(lib) and stores that instead of the source. "
531 "Suitable only when targeting macOS, not iOS."));
532 cmdLineParser.addOption(commandLineOption: mtllibOption);
533 QCommandLineOption mtllibIosOption({ "T", "metallib-ios" },
534 QObject::tr(s: "In combination with --msl builds a Metal library with xcrun metal(lib) and stores that instead of the source. "
535 "Suitable only when targeting iOS, not macOS."));
536 cmdLineParser.addOption(commandLineOption: mtllibIosOption);
537 QCommandLineOption defineOption({ "D", "define" }, QObject::tr(s: "Define macro. This argument can be specified multiple times."), QObject::tr(s: "name[=value]"));
538 cmdLineParser.addOption(commandLineOption: defineOption);
539 QCommandLineOption perTargetCompileOption({ "p", "per-target" }, QObject::tr(s: "Enable per-target compilation. (instead of source->SPIRV->targets, do "
540 "source->SPIRV->target separately for each target)"));
541 cmdLineParser.addOption(commandLineOption: perTargetCompileOption);
542 QCommandLineOption dumpOption({ "d", "dump" }, QObject::tr(s: "Switches to dump mode. Input file is expected to be a shader pack."));
543 cmdLineParser.addOption(commandLineOption: dumpOption);
544 QCommandLineOption extractOption({ "x", "extract" }, QObject::tr(s: "Switches to extract mode. Input file is expected to be a shader pack. "
545 "Result is written to the output specified by -o. "
546 "Pass -b to choose the batchable variant. "
547 "<what>=reflect|spirv,<version>|glsl,<version>|..."),
548 QObject::tr(s: "what"));
549 cmdLineParser.addOption(commandLineOption: extractOption);
550 QCommandLineOption replaceOption({ "r", "replace" },
551 QObject::tr(s: "Switches to replace mode. Replaces the specified shader in the shader pack with the contents of a file. "
552 "This argument can be specified multiple times. "
553 "Pass -b to choose the batchable variant. "
554 "Also supports adding a shader for a target/variant that was not present before. "
555 "<what>=<target>,<filename> where <target>=spirv,<version>|glsl,<version>|..."),
556 QObject::tr(s: "what"));
557 cmdLineParser.addOption(commandLineOption: replaceOption);
558 QCommandLineOption eraseOption({ "e", "erase" },
559 QObject::tr(s: "Switches to erase mode. Removes the specified shader from the shader pack. "
560 "Pass -b to choose the batchable variant. "
561 "<what>=spirv,<version>|glsl,<version>|..."),
562 QObject::tr(s: "what"));
563 cmdLineParser.addOption(commandLineOption: eraseOption);
564 QCommandLineOption silentOption({ "s", "silent" }, QObject::tr(s: "Enables silent mode. Only fatal errors will be printed."));
565 cmdLineParser.addOption(commandLineOption: silentOption);
566
567 cmdLineParser.process(app);
568
569 if (cmdLineParser.positionalArguments().isEmpty()) {
570 cmdLineParser.showHelp();
571 return 0;
572 }
573
574 silent = cmdLineParser.isSet(option: silentOption);
575
576 QShaderBaker baker;
577 for (const QString &fn : cmdLineParser.positionalArguments()) {
578 auto qsbVersion = QShader::SerializedFormatVersion::Latest;
579 if (cmdLineParser.isSet(option: qsbVersionOption)) {
580 const QString qsbVersionString = cmdLineParser.value(option: qsbVersionOption);
581 if (qsbVersionString == QStringLiteral("64")) {
582 qsbVersion = QShader::SerializedFormatVersion::Qt_6_4;
583 } else if (qsbVersionString == QStringLiteral("65")) {
584 qsbVersion = QShader::SerializedFormatVersion::Qt_6_5;
585 } else if (qsbVersionString.toLower() != QStringLiteral("latest")) {
586 printError(msg: "Unknown Qt qsb version: %s", qPrintable(qsbVersionString));
587 printError(msg: "Available versions: 64, 65, latest");
588 return 1;
589 }
590 }
591 if (cmdLineParser.isSet(option: dumpOption)
592 || cmdLineParser.isSet(option: extractOption)
593 || cmdLineParser.isSet(option: replaceOption)
594 || cmdLineParser.isSet(option: eraseOption))
595 {
596 QByteArray buf = readFile(filename: fn, fileType: FileType::Binary);
597 if (!buf.isEmpty()) {
598 QShader bs = QShader::fromSerialized(data: buf);
599 if (bs.isValid()) {
600 const bool batchable = cmdLineParser.isSet(option: batchableOption);
601 const QShader::Variant variant = batchable ? QShader::BatchableVertexShader : QShader::StandardShader;
602 if (cmdLineParser.isSet(option: dumpOption)) {
603 dump(bs);
604 } else if (cmdLineParser.isSet(option: extractOption)) {
605 if (cmdLineParser.isSet(option: outputOption)) {
606 if (!extract(bs, what: cmdLineParser.value(option: extractOption), variant, outfn: cmdLineParser.value(option: outputOption)))
607 return 1;
608 } else {
609 printError(msg: "No output file specified");
610 }
611 } else if (cmdLineParser.isSet(option: replaceOption)) {
612 if (!addOrReplace(shaderPack: bs, whatList: cmdLineParser.values(option: replaceOption), variant, outfn: fn, qsbVersion))
613 return 1;
614 } else if (cmdLineParser.isSet(option: eraseOption)) {
615 if (!remove(shaderPack: bs, whatList: cmdLineParser.values(option: eraseOption), variant, outfn: fn, qsbVersion))
616 return 1;
617 }
618 } else {
619 printError(msg: "Failed to deserialize %s (or the shader pack is empty)", qPrintable(fn));
620 }
621 }
622 continue;
623 }
624
625 baker.setSourceFileName(fn);
626
627 baker.setPerTargetCompilation(cmdLineParser.isSet(option: perTargetCompileOption));
628
629 QShaderBaker::SpirvOptions spirvOptions;
630 // We either want full debug info, or none at all (so no variable names
631 // either - that too can be stripped after the SPIRV-Cross stage).
632 if (cmdLineParser.isSet(option: debugInfoOption))
633 spirvOptions |= QShaderBaker::SpirvOption::GenerateFullDebugInfo;
634 else
635 spirvOptions |= QShaderBaker::SpirvOption::StripDebugAndVarInfo;
636
637 baker.setSpirvOptions(spirvOptions);
638
639 QList<QShader::Variant> variants;
640 variants << QShader::StandardShader;
641 if (cmdLineParser.isSet(option: batchableOption)) {
642 variants << QShader::BatchableVertexShader;
643 if (cmdLineParser.isSet(option: batchLocOption))
644 baker.setBatchableVertexShaderExtraInputLocation(cmdLineParser.value(option: batchLocOption).toInt());
645 }
646 if (cmdLineParser.isSet(option: tessOption)) {
647 variants << QShader::UInt16IndexedVertexAsComputeShader
648 << QShader::UInt32IndexedVertexAsComputeShader
649 << QShader::NonIndexedVertexAsComputeShader;
650 }
651
652 if (cmdLineParser.isSet(option: tessModeOption)) {
653 const QString tessModeStr = cmdLineParser.value(option: tessModeOption).toLower();
654 if (tessModeStr == QLatin1String("triangles"))
655 baker.setTessellationMode(QShaderDescription::TrianglesTessellationMode);
656 else if (tessModeStr == QLatin1String("quads"))
657 baker.setTessellationMode(QShaderDescription::QuadTessellationMode);
658 else
659 qWarning(msg: "Unknown tessellation mode '%s'", qPrintable(tessModeStr));
660 }
661
662 if (cmdLineParser.isSet(option: tessVertCountOption))
663 baker.setTessellationOutputVertexCount(cmdLineParser.value(option: tessVertCountOption).toInt());
664
665 baker.setGeneratedShaderVariants(variants);
666
667 QList<QShaderBaker::GeneratedShader> genShaders;
668
669 genShaders << std::make_pair(x: QShader::SpirvShader, y: QShaderVersion(100));
670
671 if (cmdLineParser.isSet(option: glslOption)) {
672 const QStringList versions = cmdLineParser.value(option: glslOption).trimmed().split(sep: ',');
673 for (QString version : versions) {
674 QShaderVersion::Flags flags;
675 if (version.endsWith(s: QLatin1String(" es"))) {
676 version = version.left(n: version.size() - 3);
677 flags |= QShaderVersion::GlslEs;
678 } else if (version.endsWith(s: QLatin1String("es"))) {
679 version = version.left(n: version.size() - 2);
680 flags |= QShaderVersion::GlslEs;
681 }
682 bool ok = false;
683 int v = version.toInt(ok: &ok);
684 if (ok)
685 genShaders << std::make_pair(x: QShader::GlslShader, y: QShaderVersion(v, flags));
686 else
687 printError(msg: "Ignoring invalid GLSL version %s", qPrintable(version));
688 }
689 }
690
691 if (cmdLineParser.isSet(option: hlslOption)) {
692 const QStringList versions = cmdLineParser.value(option: hlslOption).trimmed().split(sep: ',');
693 for (QString version : versions) {
694 bool ok = false;
695 int v = version.toInt(ok: &ok);
696 if (ok) {
697 genShaders << std::make_pair(x: QShader::HlslShader, y: QShaderVersion(v));
698 } else {
699 printError(msg: "Ignoring invalid HLSL (Shader Model) version %s",
700 qPrintable(version));
701 }
702 }
703 }
704
705 if (cmdLineParser.isSet(option: mslOption)) {
706 const QStringList versions = cmdLineParser.value(option: mslOption).trimmed().split(sep: ',');
707 for (QString version : versions) {
708 bool ok = false;
709 int v = version.toInt(ok: &ok);
710 if (ok)
711 genShaders << std::make_pair(x: QShader::MslShader, y: QShaderVersion(v));
712 else
713 printError(msg: "Ignoring invalid MSL version %s", qPrintable(version));
714 }
715 }
716
717 if (cmdLineParser.isSet(option: shortcutDefaultOption)) {
718 for (const QShaderBaker::GeneratedShader &genShaderEntry :
719 {
720 std::make_pair(x: QShader::GlslShader, y: QShaderVersion(100, QShaderVersion::GlslEs)),
721 std::make_pair(x: QShader::GlslShader, y: QShaderVersion(120)),
722 std::make_pair(x: QShader::GlslShader, y: QShaderVersion(150)),
723 std::make_pair(x: QShader::HlslShader, y: QShaderVersion(50)),
724 std::make_pair(x: QShader::MslShader, y: QShaderVersion(12))
725 })
726 {
727 if (!genShaders.contains(t: genShaderEntry))
728 genShaders << genShaderEntry;
729 }
730 }
731
732 baker.setGeneratedShaders(genShaders);
733
734 if (cmdLineParser.isSet(option: defineOption)) {
735 QByteArray preamble;
736 const QStringList defines = cmdLineParser.values(option: defineOption);
737 for (const QString &def : defines) {
738 const QStringList defs = def.split(sep: QLatin1Char('='), behavior: Qt::SkipEmptyParts);
739 if (!defs.isEmpty()) {
740 preamble.append(s: "#define");
741 for (const QString &s : defs) {
742 preamble.append(c: ' ');
743 preamble.append(a: s.toUtf8());
744 }
745 preamble.append(c: '\n');
746 }
747 }
748 baker.setPreamble(preamble);
749 }
750
751 QShader bs = baker.bake();
752 if (!bs.isValid()) {
753 printError(msg: "Shader baking failed: %s", qPrintable(baker.errorMessage()));
754 return 1;
755 }
756
757 // post processing: run spirv-opt when requested for each entry with
758 // type SpirvShader and replace the contents. Not having spirv-opt
759 // available must not be a fatal error, skip it if the process fails.
760 if (cmdLineParser.isSet(option: spirvOptOption)) {
761 QTemporaryDir tempDir;
762 if (!tempDir.isValid()) {
763 printError(msg: "Failed to create temporary directory");
764 return 1;
765 }
766 auto skeys = bs.availableShaders();
767 for (QShaderKey &k : skeys) {
768 if (k.source() == QShader::SpirvShader) {
769 QShaderCode s = bs.shader(key: k);
770
771 const QString tmpIn = writeTemp(tempDir, filename: QLatin1String("qsb_spv_temp"), s, fileType: FileType::Binary);
772 const QString tmpOut = tempDir.path() + QLatin1String("/qsb_spv_temp_out");
773 if (tmpIn.isEmpty())
774 break;
775
776 const QStringList arguments({
777 QLatin1String("-O"),
778 QDir::toNativeSeparators(pathName: tmpIn),
779 QLatin1String("-o"), QDir::toNativeSeparators(pathName: tmpOut)
780 });
781 QByteArray output;
782 QByteArray errorOutput;
783 bool success = runProcess(binary: QLatin1String("spirv-opt"), arguments, output: &output, errorOutput: &errorOutput);
784 if (success) {
785 const QByteArray bytecode = readFile(filename: tmpOut, fileType: FileType::Binary);
786 if (!bytecode.isEmpty())
787 replaceShaderContents(shaderPack: &bs, originalKey: k, newType: QShader::SpirvShader, contents: bytecode, entryPoint: s.entryPoint());
788 } else {
789 if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
790 printError(msg: "%s\n%s",
791 qPrintable(output.constData()),
792 qPrintable(errorOutput.constData()));
793 }
794 }
795 }
796 }
797 }
798
799 // post processing: run fxc when requested for each entry with type
800 // HlslShader and add a new entry with type DxbcShader and remove the
801 // original HlslShader entry
802 if (cmdLineParser.isSet(option: fxcOption)) {
803 QTemporaryDir tempDir;
804 if (!tempDir.isValid()) {
805 printError(msg: "Failed to create temporary directory");
806 return 1;
807 }
808 auto skeys = bs.availableShaders();
809 for (QShaderKey &k : skeys) {
810 if (k.source() == QShader::HlslShader) {
811 QShaderCode s = bs.shader(key: k);
812
813 const QString tmpIn = writeTemp(tempDir, filename: QLatin1String("qsb_hlsl_temp"), s, fileType: FileType::Text);
814 const QString tmpOut = tempDir.path() + QLatin1String("/qsb_hlsl_temp_out");
815 if (tmpIn.isEmpty())
816 break;
817
818 const QByteArray typeArg = fxcProfile(bs, k);
819 QStringList arguments({
820 QLatin1String("/nologo"),
821 QLatin1String("/E"), QString::fromLocal8Bit(ba: s.entryPoint()),
822 QLatin1String("/T"), QString::fromLocal8Bit(ba: typeArg),
823 QLatin1String("/Fo"), QDir::toNativeSeparators(pathName: tmpOut)
824 });
825 if (cmdLineParser.isSet(option: debugInfoOption))
826 arguments << QLatin1String("/Od") << QLatin1String("/Zi");
827 arguments.append(t: QDir::toNativeSeparators(pathName: tmpIn));
828 QByteArray output;
829 QByteArray errorOutput;
830 bool success = runProcess(binary: QLatin1String("fxc"), arguments, output: &output, errorOutput: &errorOutput);
831 if (success) {
832 const QByteArray bytecode = readFile(filename: tmpOut, fileType: FileType::Binary);
833 if (!bytecode.isEmpty())
834 replaceShaderContents(shaderPack: &bs, originalKey: k, newType: QShader::DxbcShader, contents: bytecode, entryPoint: s.entryPoint());
835 } else {
836 if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
837 printError(msg: "%s\n%s",
838 qPrintable(output.constData()),
839 qPrintable(errorOutput.constData()));
840 }
841 }
842 }
843 }
844 }
845
846 // post processing: run xcrun metal and metallib when requested for
847 // each entry with type MslShader and add a new entry with type
848 // MetalLibShader and remove the original MslShader entry
849 if (cmdLineParser.isSet(option: mtllibOption) || cmdLineParser.isSet(option: mtllibIosOption)) {
850 const bool isIos = cmdLineParser.isSet(option: mtllibIosOption);
851 QTemporaryDir tempDir;
852 if (!tempDir.isValid()) {
853 printError(msg: "Failed to create temporary directory");
854 return 1;
855 }
856 auto skeys = bs.availableShaders();
857 for (const QShaderKey &k : skeys) {
858 if (k.source() == QShader::MslShader) {
859 QShaderCode s = bs.shader(key: k);
860
861 // having the .metal file extension may matter for the external tools here, so use that
862 const QString tmpIn = writeTemp(tempDir, filename: QLatin1String("qsb_msl_temp.metal"), s, fileType: FileType::Text);
863 const QString tmpInterm = tempDir.path() + QLatin1String("/qsb_msl_temp_air");
864 const QString tmpOut = tempDir.path() + QLatin1String("/qsb_msl_temp_out");
865 if (tmpIn.isEmpty())
866 break;
867
868 const QString binary = QLatin1String("xcrun");
869 const QStringList baseArguments = {
870 QLatin1String("-sdk"),
871 isIos ? QLatin1String("iphoneos") : QLatin1String("macosx")
872 };
873 QStringList arguments = baseArguments;
874 const QString langVerFmt = QLatin1String("-std=%1-metal%2.%3");
875 const QString langPlatform = isIos ? QLatin1String("ios") : QLatin1String("macos");
876 const int langMajor = k.sourceVersion().version() / 10;
877 const int langMinor = k.sourceVersion().version() % 10;
878 const QString langVer = langVerFmt.arg(a: langPlatform).arg(a: langMajor).arg(a: langMinor);
879 arguments.append(other: {
880 QLatin1String("metal"),
881 QLatin1String("-c"),
882 langVer,
883 QDir::toNativeSeparators(pathName: tmpIn),
884 QLatin1String("-o"),
885 QDir::toNativeSeparators(pathName: tmpInterm)
886 });
887 QByteArray output;
888 QByteArray errorOutput;
889 bool success = runProcess(binary, arguments, output: &output, errorOutput: &errorOutput);
890 if (success) {
891 arguments = baseArguments;
892 arguments.append(other: {QLatin1String("metallib"), QDir::toNativeSeparators(pathName: tmpInterm),
893 QLatin1String("-o"), QDir::toNativeSeparators(pathName: tmpOut)});
894 output.clear();
895 errorOutput.clear();
896 success = runProcess(binary, arguments, output: &output, errorOutput: &errorOutput);
897 if (success) {
898 const QByteArray bytecode = readFile(filename: tmpOut, fileType: FileType::Binary);
899 if (!bytecode.isEmpty())
900 replaceShaderContents(shaderPack: &bs, originalKey: k, newType: QShader::MetalLibShader, contents: bytecode, entryPoint: s.entryPoint());
901 } else {
902 if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
903 printError(msg: "%s\n%s",
904 qPrintable(output.constData()),
905 qPrintable(errorOutput.constData()));
906 }
907 }
908 } else {
909 if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
910 printError(msg: "%s\n%s",
911 qPrintable(output.constData()),
912 qPrintable(errorOutput.constData()));
913 }
914 }
915 }
916 }
917 }
918
919 if (cmdLineParser.isSet(option: outputOption))
920 writeToFile(buf: bs.serialized(version: qsbVersion), filename: cmdLineParser.value(option: outputOption), fileType: FileType::Binary);
921 }
922
923 return 0;
924}
925

source code of qtshadertools/tools/qsb/qsb.cpp