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, .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 ),
567 syntax: QObject::tr(s: "file"));
568 QCommandLineOption batchableOption({ "b", "batchable" }, QObject::tr(s: "Also generates rewritten vertex shader for Qt Quick scene graph batching."));
569 cmdLineParser.addOption(commandLineOption: batchableOption);
570 QCommandLineOption batchLocOption("zorder-loc",
571 QObject::tr(s: "The extra vertex input location when rewriting for batching. Defaults to 7."),
572 QObject::tr(s: "location"));
573 cmdLineParser.addOption(commandLineOption: batchLocOption);
574 QCommandLineOption glslOption("glsl",
575 QObject::tr(s: "Comma separated list of GLSL versions to generate. (for example, \"100 es,120,330\")"),
576 QObject::tr(s: "versions"));
577 cmdLineParser.addOption(commandLineOption: glslOption);
578 QCommandLineOption hlslOption("hlsl",
579 QObject::tr(s: "Comma separated list of HLSL (Shader Model) versions to generate. F.ex. 50 is 5.0, 51 is 5.1."),
580 QObject::tr(s: "versions"));
581 cmdLineParser.addOption(commandLineOption: hlslOption);
582 QCommandLineOption mslOption("msl",
583 QObject::tr(s: "Comma separated list of Metal Shading Language versions to generate. F.ex. 12 is 1.2, 20 is 2.0."),
584 QObject::tr(s: "versions"));
585 cmdLineParser.addOption(commandLineOption: mslOption);
586 QCommandLineOption shortcutDefaultOption("qt6", QObject::tr(s: "Equivalent to --glsl \"100 es,120,150\" --hlsl 50 --msl 12. "
587 "This set is commonly used with shaders for Qt Quick materials and effects."));
588 cmdLineParser.addOption(commandLineOption: shortcutDefaultOption);
589 QCommandLineOption tessOption("msltess", QObject::tr(s: "Indicates that a vertex shader is going to be used in a pipeline with tessellation. "
590 "Mandatory for vertex shaders planned to be used with tessellation when targeting Metal (--msl)."));
591 cmdLineParser.addOption(commandLineOption: tessOption);
592 QCommandLineOption tessVertCountOption("tess-vertex-count", QObject::tr(s: "The output vertex count from the tessellation control stage. "
593 "Mandatory for tessellation evaluation shaders planned to be used with Metal. "
594 "The default value is 3. "
595 "If it does not match the tess.control stage, the generated MSL code will not function as expected."),
596 QObject::tr(s: "count"));
597 cmdLineParser.addOption(commandLineOption: tessVertCountOption);
598 QCommandLineOption tessModeOption("tess-mode", QObject::tr(s: "The tessellation mode: triangles or quads. Mandatory for tessellation control shaders planned to be used with Metal. "
599 "The default value is triangles. Isolines are not supported with Metal. "
600 "If it does not match the tess.evaluation stage, the generated MSL code will not function as expected."),
601 QObject::tr(s: "mode"));
602 cmdLineParser.addOption(commandLineOption: tessModeOption);
603 QCommandLineOption multiViewCountOption("view-count", QObject::tr(s: "The number of views the shader is used with. num_views must be >= 2. "
604 "Mandatory when multiview rendering is used (gl_ViewIndex). "
605 "Set only for vertex shaders that really do rely on multiview (as the resulting asset is tied to num_views). "
606 "Can be set for fragment shaders too, to get QSHADER_VIEW_COUNT auto-defined. (useful for ensuring uniform buffer layouts)"),
607 QObject::tr(s: "num_views"));
608 cmdLineParser.addOption(commandLineOption: multiViewCountOption);
609 QCommandLineOption debugInfoOption("g", QObject::tr(s: "Generate full debug info for SPIR-V and DXBC"));
610 cmdLineParser.addOption(commandLineOption: debugInfoOption);
611 QCommandLineOption spirvOptOption("O", QObject::tr(s: "Invoke spirv-opt (external tool) to optimize SPIR-V for performance."));
612 cmdLineParser.addOption(commandLineOption: spirvOptOption);
613 QCommandLineOption outputOption({ "o", "output" },
614 QObject::tr(s: "Output file for the shader pack."),
615 QObject::tr(s: "filename"));
616 cmdLineParser.addOption(commandLineOption: outputOption);
617 QCommandLineOption qsbVersionOption("qsbversion",
618 QObject::tr(s: "QSB version to use for the output file. By default the latest version is automatically used, "
619 "use only to bake compatibility versions. F.ex. 64 is Qt 6.4."),
620 QObject::tr(s: "version"));
621 cmdLineParser.addOption(commandLineOption: qsbVersionOption);
622 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."));
623 cmdLineParser.addOption(commandLineOption: fxcOption);
624 QCommandLineOption mtllibOption({ "t", "metallib" },
625 QObject::tr(s: "In combination with --msl builds a Metal library with xcrun metal(lib) and stores that instead of the source. "
626 "Suitable only when targeting macOS, not iOS."));
627 cmdLineParser.addOption(commandLineOption: mtllibOption);
628 QCommandLineOption mtllibIosOption({ "T", "metallib-ios" },
629 QObject::tr(s: "In combination with --msl builds a Metal library with xcrun metal(lib) and stores that instead of the source. "
630 "Suitable only when targeting iOS, not macOS."));
631 cmdLineParser.addOption(commandLineOption: mtllibIosOption);
632 QCommandLineOption defineOption({ "D", "define" }, QObject::tr(s: "Define macro. This argument can be specified multiple times."), QObject::tr(s: "name[=value]"));
633 cmdLineParser.addOption(commandLineOption: defineOption);
634 QCommandLineOption perTargetCompileOption({ "p", "per-target" }, QObject::tr(s: "Enable per-target compilation. (instead of source->SPIRV->targets, do "
635 "source->SPIRV->target separately for each target)"));
636 cmdLineParser.addOption(commandLineOption: perTargetCompileOption);
637 QCommandLineOption dumpOption({ "d", "dump" }, QObject::tr(s: "Switches to dump mode. Input file is expected to be a shader pack."));
638 cmdLineParser.addOption(commandLineOption: dumpOption);
639 QCommandLineOption extractOption({ "x", "extract" }, QObject::tr(s: "Switches to extract mode. Input file is expected to be a shader pack. "
640 "Result is written to the output specified by -o. "
641 "Pass -b to choose the batchable variant. "
642 "<what>=reflect|spirv,<version>|glsl,<version>|..."),
643 QObject::tr(s: "what"));
644 cmdLineParser.addOption(commandLineOption: extractOption);
645 QCommandLineOption replaceOption({ "r", "replace" },
646 QObject::tr(s: "Switches to replace mode. Replaces the specified shader in the shader pack with the contents of a file. "
647 "This argument can be specified multiple times. "
648 "Pass -b to choose the batchable variant. "
649 "Also supports adding a shader for a target/variant that was not present before. "
650 "<what>=<target>,<filename> where <target>=spirv,<version>|glsl,<version>|..."),
651 QObject::tr(s: "what"));
652 cmdLineParser.addOption(commandLineOption: replaceOption);
653 QCommandLineOption eraseOption({ "e", "erase" },
654 QObject::tr(s: "Switches to erase mode. Removes the specified shader from the shader pack. "
655 "Pass -b to choose the batchable variant. "
656 "<what>=spirv,<version>|glsl,<version>|..."),
657 QObject::tr(s: "what"));
658 cmdLineParser.addOption(commandLineOption: eraseOption);
659 QCommandLineOption silentOption({ "s", "silent" }, QObject::tr(s: "Enables silent mode. Only fatal errors will be printed."));
660 cmdLineParser.addOption(commandLineOption: silentOption);
661
662 QCommandLineOption depfileOption("depfile",
663 QObject::tr(s: "Enables generating the depfile for the input "
664 "shaders, using the #include statements."),
665 QObject::tr(s: "depfile"));
666 cmdLineParser.addOption(commandLineOption: depfileOption);
667
668 cmdLineParser.process(app);
669
670 if (cmdLineParser.positionalArguments().isEmpty()) {
671 cmdLineParser.showHelp();
672 return 0;
673 }
674
675 silent = cmdLineParser.isSet(option: silentOption);
676
677 QShaderBaker baker;
678
679 QFile depfile;
680 if (const QString depfilePath = cmdLineParser.value(option: depfileOption); !depfilePath.isEmpty()) {
681 QDir().mkpath(dirPath: QFileInfo(depfilePath).path());
682 depfile.setFileName(depfilePath);
683 if (!depfile.open(flags: QFile::WriteOnly | QFile::Truncate)) {
684 printError(msg: "Unable to create DEPFILE: '%s'", qPrintable(depfilePath));
685 return 1;
686 }
687 }
688
689 const bool depfileRequired = depfile.isOpen();
690
691 for (const QString &fn : cmdLineParser.positionalArguments()) {
692 auto qsbVersion = QShader::SerializedFormatVersion::Latest;
693 if (cmdLineParser.isSet(option: qsbVersionOption)) {
694 const QString qsbVersionString = cmdLineParser.value(option: qsbVersionOption);
695 if (qsbVersionString == QStringLiteral("64")) {
696 qsbVersion = QShader::SerializedFormatVersion::Qt_6_4;
697 } else if (qsbVersionString == QStringLiteral("65")) {
698 qsbVersion = QShader::SerializedFormatVersion::Qt_6_5;
699 } else if (qsbVersionString.toLower() != QStringLiteral("latest")) {
700 printError(msg: "Unknown Qt qsb version: %s", qPrintable(qsbVersionString));
701 printError(msg: "Available versions: 64, 65, latest");
702 return 1;
703 }
704 }
705
706 if (cmdLineParser.isSet(option: dumpOption)
707 || cmdLineParser.isSet(option: extractOption)
708 || cmdLineParser.isSet(option: replaceOption)
709 || cmdLineParser.isSet(option: eraseOption))
710 {
711 QByteArray buf = readFile(filename: fn, fileType: FileType::Binary);
712 if (!buf.isEmpty()) {
713 QShader bs = QShader::fromSerialized(data: buf);
714 if (bs.isValid()) {
715 const bool batchable = cmdLineParser.isSet(option: batchableOption);
716 const QShader::Variant variant = batchable ? QShader::BatchableVertexShader : QShader::StandardShader;
717 if (cmdLineParser.isSet(option: dumpOption)) {
718 dump(bs);
719 } else if (cmdLineParser.isSet(option: extractOption)) {
720 if (cmdLineParser.isSet(option: outputOption)) {
721 if (!extract(bs, what: cmdLineParser.value(option: extractOption), variant, outfn: cmdLineParser.value(option: outputOption)))
722 return 1;
723 } else {
724 printError(msg: "No output file specified");
725 }
726 } else if (cmdLineParser.isSet(option: replaceOption)) {
727 if (!addOrReplace(shaderPack: bs, whatList: cmdLineParser.values(option: replaceOption), variant, outfn: fn, qsbVersion))
728 return 1;
729 } else if (cmdLineParser.isSet(option: eraseOption)) {
730 if (!remove(shaderPack: bs, whatList: cmdLineParser.values(option: eraseOption), variant, outfn: fn, qsbVersion))
731 return 1;
732 }
733 } else {
734 printError(msg: "Failed to deserialize %s (or the shader pack is empty)", qPrintable(fn));
735 }
736 }
737 continue;
738 }
739
740 if (depfileRequired && !generateDepfile(depfile, inputFilename: fn, outputFilename: cmdLineParser.value(option: outputOption)))
741 return 1;
742
743 baker.setSourceFileName(fn);
744
745 baker.setPerTargetCompilation(cmdLineParser.isSet(option: perTargetCompileOption));
746
747 QShaderBaker::SpirvOptions spirvOptions;
748 // We either want full debug info, or none at all (so no variable names
749 // either - that too can be stripped after the SPIRV-Cross stage).
750 if (cmdLineParser.isSet(option: debugInfoOption))
751 spirvOptions |= QShaderBaker::SpirvOption::GenerateFullDebugInfo;
752 else
753 spirvOptions |= QShaderBaker::SpirvOption::StripDebugAndVarInfo;
754
755 baker.setSpirvOptions(spirvOptions);
756
757 QList<QShader::Variant> variants;
758 variants << QShader::StandardShader;
759 if (cmdLineParser.isSet(option: batchableOption)) {
760 variants << QShader::BatchableVertexShader;
761 if (cmdLineParser.isSet(option: batchLocOption))
762 baker.setBatchableVertexShaderExtraInputLocation(cmdLineParser.value(option: batchLocOption).toInt());
763 }
764 if (cmdLineParser.isSet(option: tessOption)) {
765 variants << QShader::UInt16IndexedVertexAsComputeShader
766 << QShader::UInt32IndexedVertexAsComputeShader
767 << QShader::NonIndexedVertexAsComputeShader;
768 }
769
770 if (cmdLineParser.isSet(option: tessModeOption)) {
771 const QString tessModeStr = cmdLineParser.value(option: tessModeOption).toLower();
772 if (tessModeStr == QLatin1String("triangles"))
773 baker.setTessellationMode(QShaderDescription::TrianglesTessellationMode);
774 else if (tessModeStr == QLatin1String("quads"))
775 baker.setTessellationMode(QShaderDescription::QuadTessellationMode);
776 else
777 qWarning(msg: "Unknown tessellation mode '%s'", qPrintable(tessModeStr));
778 }
779
780 if (cmdLineParser.isSet(option: tessVertCountOption))
781 baker.setTessellationOutputVertexCount(cmdLineParser.value(option: tessVertCountOption).toInt());
782
783 if (cmdLineParser.isSet(option: multiViewCountOption))
784 baker.setMultiViewCount(cmdLineParser.value(option: multiViewCountOption).toInt());
785
786 baker.setGeneratedShaderVariants(variants);
787
788 QList<QShaderBaker::GeneratedShader> genShaders;
789
790 genShaders << std::make_pair(x: QShader::SpirvShader, y: QShaderVersion(100));
791
792 if (cmdLineParser.isSet(option: glslOption)) {
793 const QStringList versions = cmdLineParser.value(option: glslOption).trimmed().split(sep: ',');
794 for (QString version : versions) {
795 QShaderVersion::Flags flags;
796 if (version.endsWith(s: QLatin1String(" es"))) {
797 version = version.left(n: version.size() - 3);
798 flags |= QShaderVersion::GlslEs;
799 } else if (version.endsWith(s: QLatin1String("es"))) {
800 version = version.left(n: version.size() - 2);
801 flags |= QShaderVersion::GlslEs;
802 }
803 bool ok = false;
804 int v = version.toInt(ok: &ok);
805 if (ok)
806 genShaders << std::make_pair(x: QShader::GlslShader, y: QShaderVersion(v, flags));
807 else
808 printError(msg: "Ignoring invalid GLSL version %s", qPrintable(version));
809 }
810 }
811
812 if (cmdLineParser.isSet(option: hlslOption)) {
813 const QStringList versions = cmdLineParser.value(option: hlslOption).trimmed().split(sep: ',');
814 for (QString version : versions) {
815 bool ok = false;
816 int v = version.toInt(ok: &ok);
817 if (ok) {
818 genShaders << std::make_pair(x: QShader::HlslShader, y: QShaderVersion(v));
819 } else {
820 printError(msg: "Ignoring invalid HLSL (Shader Model) version %s",
821 qPrintable(version));
822 }
823 }
824 }
825
826 if (cmdLineParser.isSet(option: mslOption)) {
827 const QStringList versions = cmdLineParser.value(option: mslOption).trimmed().split(sep: ',');
828 for (QString version : versions) {
829 bool ok = false;
830 int v = version.toInt(ok: &ok);
831 if (ok)
832 genShaders << std::make_pair(x: QShader::MslShader, y: QShaderVersion(v));
833 else
834 printError(msg: "Ignoring invalid MSL version %s", qPrintable(version));
835 }
836 }
837
838 if (cmdLineParser.isSet(option: shortcutDefaultOption)) {
839 for (const QShaderBaker::GeneratedShader &genShaderEntry :
840 {
841 std::make_pair(x: QShader::GlslShader, y: QShaderVersion(100, QShaderVersion::GlslEs)),
842 std::make_pair(x: QShader::GlslShader, y: QShaderVersion(120)),
843 std::make_pair(x: QShader::GlslShader, y: QShaderVersion(150)),
844 std::make_pair(x: QShader::HlslShader, y: QShaderVersion(50)),
845 std::make_pair(x: QShader::MslShader, y: QShaderVersion(12))
846 })
847 {
848 if (!genShaders.contains(t: genShaderEntry))
849 genShaders << genShaderEntry;
850 }
851 }
852
853 baker.setGeneratedShaders(genShaders);
854
855 if (cmdLineParser.isSet(option: defineOption)) {
856 QByteArray preamble;
857 const QStringList defines = cmdLineParser.values(option: defineOption);
858 for (const QString &def : defines) {
859 const QStringList defs = def.split(sep: QLatin1Char('='), behavior: Qt::SkipEmptyParts);
860 if (!defs.isEmpty()) {
861 preamble.append(s: "#define");
862 for (const QString &s : defs) {
863 preamble.append(c: ' ');
864 preamble.append(a: s.toUtf8());
865 }
866 preamble.append(c: '\n');
867 }
868 }
869 baker.setPreamble(preamble);
870 }
871
872 QShader bs = baker.bake();
873 if (!bs.isValid()) {
874 printError(msg: "Shader baking failed: %s", qPrintable(baker.errorMessage()));
875 return 1;
876 }
877
878 // post processing: run spirv-opt when requested for each entry with
879 // type SpirvShader and replace the contents. Not having spirv-opt
880 // available must not be a fatal error, skip it if the process fails.
881 if (cmdLineParser.isSet(option: spirvOptOption)) {
882 QTemporaryDir tempDir;
883 if (!tempDir.isValid()) {
884 printError(msg: "Failed to create temporary directory");
885 return 1;
886 }
887 auto skeys = bs.availableShaders();
888 for (QShaderKey &k : skeys) {
889 if (k.source() == QShader::SpirvShader) {
890 QShaderCode s = bs.shader(key: k);
891
892 const QString tmpIn = writeTemp(tempDir, filename: QLatin1String("qsb_spv_temp"), s, fileType: FileType::Binary);
893 const QString tmpOut = tempDir.path() + QLatin1String("/qsb_spv_temp_out");
894 if (tmpIn.isEmpty())
895 break;
896
897 const QStringList arguments({
898 QLatin1String("-O"),
899 QDir::toNativeSeparators(pathName: tmpIn),
900 QLatin1String("-o"), QDir::toNativeSeparators(pathName: tmpOut)
901 });
902 QByteArray output;
903 QByteArray errorOutput;
904 bool success = runProcess(binary: QLatin1String("spirv-opt"), arguments, output: &output, errorOutput: &errorOutput);
905 if (success) {
906 const QByteArray bytecode = readFile(filename: tmpOut, fileType: FileType::Binary);
907 if (!bytecode.isEmpty())
908 replaceShaderContents(shaderPack: &bs, originalKey: k, newType: QShader::SpirvShader, contents: bytecode, entryPoint: s.entryPoint());
909 } else {
910 if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
911 printError(msg: "%s\n%s",
912 qPrintable(output.constData()),
913 qPrintable(errorOutput.constData()));
914 }
915 }
916 }
917 }
918 }
919
920 // post processing: run fxc/dxc when requested for each entry with type
921 // HlslShader and add a new entry with type DxbcShader/DxilShader and remove the
922 // original HlslShader entry
923 if (cmdLineParser.isSet(option: fxcOption)) {
924 QTemporaryDir tempDir;
925 if (!tempDir.isValid()) {
926 printError(msg: "Failed to create temporary directory");
927 return 1;
928 }
929 auto skeys = bs.availableShaders();
930 for (QShaderKey &k : skeys) {
931 if (k.source() == QShader::HlslShader) {
932 // For Shader Model 6.0 and higher, use dxc, fxc will not compile that anymore.
933 const bool useDxc = k.sourceVersion().version() >= 60;
934 QShaderCode s = bs.shader(key: k);
935
936 const QString tmpIn = writeTemp(tempDir, filename: QLatin1String("qsb_hlsl_temp"), s, fileType: FileType::Text);
937 const QString tmpOut = tempDir.path() + QLatin1String("/qsb_hlsl_temp_out");
938 if (tmpIn.isEmpty())
939 break;
940
941 const QByteArray typeArg = fxcProfile(bs, k);
942 QStringList arguments({
943 QLatin1String("/nologo"),
944 QLatin1String("/E"), QString::fromLocal8Bit(ba: s.entryPoint()),
945 QLatin1String("/T"), QString::fromLocal8Bit(ba: typeArg),
946 QLatin1String("/Fo"), QDir::toNativeSeparators(pathName: tmpOut)
947 });
948 if (cmdLineParser.isSet(option: debugInfoOption))
949 arguments << QLatin1String("/Od") << QLatin1String("/Zi");
950 arguments.append(t: QDir::toNativeSeparators(pathName: tmpIn));
951 QByteArray output;
952 QByteArray errorOutput;
953 const QString compilerName = useDxc ? QLatin1String("dxc") : QLatin1String("fxc");
954 bool success = runProcess(binary: compilerName, arguments, output: &output, errorOutput: &errorOutput);
955 if (success) {
956 const QByteArray bytecode = readFile(filename: tmpOut, fileType: FileType::Binary);
957 if (!bytecode.isEmpty()) {
958 const QShader::Source bytecodeType = useDxc ? QShader::DxilShader : QShader::DxbcShader;
959 replaceShaderContents(shaderPack: &bs, originalKey: k, newType: bytecodeType, contents: bytecode, entryPoint: s.entryPoint());
960 }
961 } else {
962 if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
963 printError(msg: "%s\n%s",
964 qPrintable(output.constData()),
965 qPrintable(errorOutput.constData()));
966 }
967 }
968 }
969 }
970 }
971
972 // post processing: run xcrun metal and metallib when requested for
973 // each entry with type MslShader and add a new entry with type
974 // MetalLibShader and remove the original MslShader entry
975 if (cmdLineParser.isSet(option: mtllibOption) || cmdLineParser.isSet(option: mtllibIosOption)) {
976 const bool isIos = cmdLineParser.isSet(option: mtllibIosOption);
977 QTemporaryDir tempDir;
978 if (!tempDir.isValid()) {
979 printError(msg: "Failed to create temporary directory");
980 return 1;
981 }
982 auto skeys = bs.availableShaders();
983 for (const QShaderKey &k : skeys) {
984 if (k.source() == QShader::MslShader) {
985 QShaderCode s = bs.shader(key: k);
986
987 // having the .metal file extension may matter for the external tools here, so use that
988 const QString tmpIn = writeTemp(tempDir, filename: QLatin1String("qsb_msl_temp.metal"), s, fileType: FileType::Text);
989 const QString tmpInterm = tempDir.path() + QLatin1String("/qsb_msl_temp_air");
990 const QString tmpOut = tempDir.path() + QLatin1String("/qsb_msl_temp_out");
991 if (tmpIn.isEmpty())
992 break;
993
994 const QString binary = QLatin1String("xcrun");
995 const QStringList baseArguments = {
996 QLatin1String("-sdk"),
997 isIos ? QLatin1String("iphoneos") : QLatin1String("macosx")
998 };
999 QStringList arguments = baseArguments;
1000 const QString langVerFmt = QLatin1String("-std=%1-metal%2.%3");
1001 const QString langPlatform = isIos ? QLatin1String("ios") : QLatin1String("macos");
1002 const int langMajor = k.sourceVersion().version() / 10;
1003 const int langMinor = k.sourceVersion().version() % 10;
1004 const QString langVer = langVerFmt.arg(a: langPlatform).arg(a: langMajor).arg(a: langMinor);
1005 arguments.append(other: {
1006 QLatin1String("metal"),
1007 QLatin1String("-c"),
1008 langVer,
1009 QDir::toNativeSeparators(pathName: tmpIn),
1010 QLatin1String("-o"),
1011 QDir::toNativeSeparators(pathName: tmpInterm)
1012 });
1013 QByteArray output;
1014 QByteArray errorOutput;
1015 bool success = runProcess(binary, arguments, output: &output, errorOutput: &errorOutput);
1016 if (success) {
1017 arguments = baseArguments;
1018 arguments.append(other: {QLatin1String("metallib"), QDir::toNativeSeparators(pathName: tmpInterm),
1019 QLatin1String("-o"), QDir::toNativeSeparators(pathName: tmpOut)});
1020 output.clear();
1021 errorOutput.clear();
1022 success = runProcess(binary, arguments, output: &output, errorOutput: &errorOutput);
1023 if (success) {
1024 const QByteArray bytecode = readFile(filename: tmpOut, fileType: FileType::Binary);
1025 if (!bytecode.isEmpty())
1026 replaceShaderContents(shaderPack: &bs, originalKey: k, newType: QShader::MetalLibShader, contents: bytecode, entryPoint: s.entryPoint());
1027 } else {
1028 if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
1029 printError(msg: "%s\n%s",
1030 qPrintable(output.constData()),
1031 qPrintable(errorOutput.constData()));
1032 }
1033 }
1034 } else {
1035 if ((!output.isEmpty() || !errorOutput.isEmpty()) && !silent) {
1036 printError(msg: "%s\n%s",
1037 qPrintable(output.constData()),
1038 qPrintable(errorOutput.constData()));
1039 }
1040 }
1041 }
1042 }
1043 }
1044
1045 if (cmdLineParser.isSet(option: outputOption))
1046 writeToFile(buf: bs.serialized(version: qsbVersion), filename: cmdLineParser.value(option: outputOption), fileType: FileType::Binary);
1047 }
1048
1049 return 0;
1050}
1051

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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