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

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