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. |
25 | static bool silent = false; |
26 | |
27 | enum class FileType |
28 | { |
29 | Binary, |
30 | Text |
31 | }; |
32 | |
33 | static 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 | |
42 | static 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 | |
57 | static 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 | |
70 | static 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 | |
79 | static 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 | |
124 | static 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 | |
144 | static 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 | |
168 | static 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 | |
177 | static 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 | |
195 | static 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 | |
277 | static 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 | |
317 | static bool (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 | |
345 | static 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 | |
384 | static 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 | |
407 | static 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 | |
443 | static 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 | |
462 | int 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 ({ "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 | |