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. |
26 | static bool silent = false; |
27 | |
28 | using namespace Qt::StringLiterals; |
29 | |
30 | enum class FileType |
31 | { |
32 | Binary, |
33 | Text |
34 | }; |
35 | |
36 | static 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 | |
45 | static 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 | |
60 | static 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 | |
73 | static 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 | |
82 | static 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 | |
127 | static 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 | |
147 | static 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 | |
171 | static 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 | |
180 | static 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 | |
198 | static 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 | |
281 | static 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 | |
321 | static bool (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 | |
349 | static 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 | |
388 | static 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 | |
411 | static 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 | |
447 | static 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 | |
466 | static 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 | |
551 | int 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 ({ "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 | |