| 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 | |