| 1 | // Copyright (C) 2023 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include "qshaderbaker.h" |
| 5 | #include "qspirvcompiler_p.h" |
| 6 | #include "qspirvshader_p.h" |
| 7 | #include <QFileInfo> |
| 8 | #include <QFile> |
| 9 | #include <QDebug> |
| 10 | |
| 11 | QT_BEGIN_NAMESPACE |
| 12 | |
| 13 | /*! |
| 14 | \class QShaderBaker |
| 15 | \inmodule QtShaderTools |
| 16 | \since 6.6 |
| 17 | |
| 18 | \brief Compiles a GLSL/Vulkan shader into SPIR-V, translates into other |
| 19 | shading languages, and gathers reflection metadata. |
| 20 | |
| 21 | \warning QShaderBaker, just like the QRhi family of classes in the Qt Gui |
| 22 | module, including QShader and QShaderDescription, offers limited |
| 23 | compatibility guarantees. There are no source or binary compatibility |
| 24 | guarantees for these classes, meaning the API is only guaranteed to work |
| 25 | with the Qt version the application was developed against. Source |
| 26 | incompatible changes are however aimed to be kept at a minimum and will only |
| 27 | be made in minor releases (6.7, 6.8, and so on). To use this class in an |
| 28 | application, link to \c{Qt::ShaderToolsPrivate} (if using CMake), and |
| 29 | include the headers with the \c rhi prefix, for example |
| 30 | \c{#include <rhi/qshaderbaker.h>}. |
| 31 | |
| 32 | QShaderBaker takes a graphics (vertex, fragment, etc.) or compute shader, |
| 33 | and produces multiple - either source or bytecode - variants of it, |
| 34 | together with reflection information. The results are represented by a |
| 35 | QShader instance, which also provides simple and fast serialization |
| 36 | and deserialization. |
| 37 | |
| 38 | \note Applications and libraries are recommended to avoid using this class |
| 39 | directly. Rather, all Qt users are encouraged to rely on offline compilation |
| 40 | by invoking the \c qsb command-line tool at build time via CMake. The \c qsb |
| 41 | tool uses QShaderBaker and writes the serialized version of the generated |
| 42 | QShader into a file. The usage of this class should be restricted to cases |
| 43 | where run time compilation cannot be avoided, such as when working with |
| 44 | user-provided or dynamically generated shader source strings. |
| 45 | |
| 46 | The input format is always assumed to be Vulkan-flavored GLSL at the |
| 47 | moment. See the |
| 48 | \l{https://github.com/KhronosGroup/GLSL/blob/master/extensions/khr/GL_KHR_vulkan_glsl.txt}{GL_KHR_vulkan_glsl |
| 49 | specification} for an overview, keeping in mind that the Qt Shader Tools |
| 50 | module is meant to be used in combination with the QRhi classes from Qt |
| 51 | Rendering Hardware Interface module, and therefore a number of concepts and |
| 52 | constructs (push constants, storage buffers, subpasses, etc.) are not |
| 53 | applicable at the moment. Additional options may be introduced in the |
| 54 | future, for example, by enabling |
| 55 | \l{https://docs.microsoft.com/en-us/windows/desktop/direct3dhlsl/dx-graphics-hlsl}{HLSL} |
| 56 | as a source format, once HLSL to SPIR-V compilation is deemed suitable. |
| 57 | |
| 58 | The reflection metadata is retrievable from the resulting QShader by |
| 59 | calling QShader::description(). This is essential when having to |
| 60 | discover what set of vertex inputs and shader resources a shader expects, |
| 61 | and what the layouts of those are, as many modern graphics APIs offer no |
| 62 | built-in shader reflection capabilities. |
| 63 | |
| 64 | \section2 Typical Workflow |
| 65 | |
| 66 | Let's assume an application has a vertex and fragment shader like the following: |
| 67 | |
| 68 | Vertex shader: |
| 69 | \snippet color.vert 0 |
| 70 | |
| 71 | Fragment shader: |
| 72 | \snippet color.frag 0 |
| 73 | |
| 74 | To get QShader instances that can be passed as-is to a |
| 75 | QRhiGraphicsPipeline, there are two options: doing the shader pack |
| 76 | generation off line, or at run time. |
| 77 | |
| 78 | The former involves running the \c qsb tool: |
| 79 | |
| 80 | \badcode |
| 81 | qsb --glsl "100 es,120" --hlsl 50 --msl 12 color.vert -o color.vert.qsb |
| 82 | qsb --glsl "100 es,120" --hlsl 50 --msl 12 color.frag -o color.frag.qsb |
| 83 | \endcode |
| 84 | |
| 85 | The example uses the translation targets as appropriate for QRhi. This |
| 86 | means GLSL/ES 100, GLSL 120, HLSL Shader Model 5.0, and Metal Shading |
| 87 | Language 1.2. |
| 88 | |
| 89 | Note how the command line options correspond to what can be specified via |
| 90 | setGeneratedShaders(). Once the resulting files are available, they can be |
| 91 | shipped with the application (typically embedded into the executable the |
| 92 | the Qt Resource System), and can be loaded and passed to |
| 93 | QShader::fromSerialized() at run time. |
| 94 | |
| 95 | While not shown here, \c qsb can do more: it is also able to invoke \c fxc |
| 96 | on Windows or the appropriate XCode tools on macOS to compile the generated |
| 97 | HLSL or Metal shader code into bytecode and include the compiled versions |
| 98 | in the QShader. After a baked shader pack is written into a file, its |
| 99 | contents can be examined by running \c{qsb -d} on it. Run \c qsb with |
| 100 | \c{--help} for more information. |
| 101 | |
| 102 | The alternative approach is to perform the same at run time. This involves |
| 103 | creating a QShaderBaker instance, calling setSourceFileName(), and then |
| 104 | setting up the translation targets via setGeneratedShaders(): |
| 105 | |
| 106 | \badcode |
| 107 | baker.setGeneratedShaderVariants({ QShader::StandardShader }); |
| 108 | QList<QShaderBaker::GeneratedShader> targets; |
| 109 | targets.append({ QShader::SpirvShader, QShaderVersion(100) }); |
| 110 | targets.append({ QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs) }); |
| 111 | targets.append({ QShader::SpirvShader, QShaderVersion(120) }); |
| 112 | targets.append({ QShader::HlslShader, QShaderVersion(50) }); |
| 113 | targets.append({ QShader::MslShader, QShaderVersion(12) }); |
| 114 | baker.setGeneratedShaders(targets); |
| 115 | QShader shaders = baker.bake(); |
| 116 | if (!shaders.isValid()) |
| 117 | qWarning() << baker.errorMessage(); |
| 118 | \endcode |
| 119 | |
| 120 | \sa QShader |
| 121 | */ |
| 122 | |
| 123 | struct QShaderBakerPrivate |
| 124 | { |
| 125 | bool readFile(const QString &fn); |
| 126 | QPair<QByteArray, QByteArray> compile(); |
| 127 | QByteArray perTargetDefines(const QShaderBaker::GeneratedShader &key); |
| 128 | |
| 129 | QString sourceFileName; |
| 130 | QByteArray source; |
| 131 | QShader::Stage stage; |
| 132 | QList<QShaderBaker::GeneratedShader> reqVersions; |
| 133 | QList<QShader::Variant> variants; |
| 134 | QByteArray preamble; |
| 135 | int batchLoc = 7; |
| 136 | bool perTargetEnabled = false; |
| 137 | bool breakOnShaderTranslationError = true; |
| 138 | QSpirvShader::TessellationInfo tessInfo; |
| 139 | QSpirvShader::MultiViewInfo multiViewInfo; |
| 140 | QShaderBaker::SpirvOptions spirvOptions; |
| 141 | QShaderBaker::GlslOptions glslOptions; |
| 142 | QSpirvCompiler compiler; |
| 143 | QString errorMessage; |
| 144 | }; |
| 145 | |
| 146 | bool QShaderBakerPrivate::readFile(const QString &fn) |
| 147 | { |
| 148 | QFile f(fn); |
| 149 | if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) { |
| 150 | qWarning(msg: "QShaderBaker: Failed to open %s" , qPrintable(fn)); |
| 151 | return false; |
| 152 | } |
| 153 | source = f.readAll(); |
| 154 | |
| 155 | // Remove UTF-8 BOM, if present. glslang fails otherwise, since the BOM is |
| 156 | // removed in glslangValidator (the command-line frontend), not in the |
| 157 | // compiler itself. Replicate that here. |
| 158 | if (source.size() >= 3) { |
| 159 | const unsigned char *p = reinterpret_cast<const unsigned char *>(source.constData()); |
| 160 | if (p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF) |
| 161 | source.remove(index: 0, len: 3); |
| 162 | } |
| 163 | |
| 164 | sourceFileName = fn; |
| 165 | return true; |
| 166 | } |
| 167 | |
| 168 | /*! |
| 169 | Constructs a new QShaderBaker. |
| 170 | */ |
| 171 | QShaderBaker::QShaderBaker() |
| 172 | : d(new QShaderBakerPrivate) |
| 173 | { |
| 174 | } |
| 175 | |
| 176 | /*! |
| 177 | Destructor. |
| 178 | */ |
| 179 | QShaderBaker::~QShaderBaker() |
| 180 | { |
| 181 | delete d; |
| 182 | } |
| 183 | |
| 184 | /*! |
| 185 | Sets the name of the shader source file to \a fileName. This is the file |
| 186 | that will be read when calling bake(). The shader stage is deduced |
| 187 | automatically from the file extension. When this is not desired or not |
| 188 | possible, use the overload with the stage argument instead. |
| 189 | |
| 190 | The supported file extensions are: |
| 191 | \list |
| 192 | \li \c{.vert} - vertex shader |
| 193 | \li \c{.frag} - fragment (pixel) shader |
| 194 | \li \c{.tesc} - tessellation control (hull) shader |
| 195 | \li \c{.tese} - tessellation evaluation (domain) shader |
| 196 | \li \c{.geom} - geometry shader |
| 197 | \li \c{.comp} - compute shader |
| 198 | \endlist |
| 199 | |
| 200 | \warning \a fileName is expected to contain trusted content. Application |
| 201 | developers are advised to carefully consider the potential implications |
| 202 | before passing in user-provided source files that are not part of the |
| 203 | application. |
| 204 | */ |
| 205 | void QShaderBaker::setSourceFileName(const QString &fileName) |
| 206 | { |
| 207 | if (!d->readFile(fn: fileName)) |
| 208 | return; |
| 209 | |
| 210 | const QString suffix = QFileInfo(fileName).suffix(); |
| 211 | if (suffix == QStringLiteral("vert" )) { |
| 212 | d->stage = QShader::VertexStage; |
| 213 | } else if (suffix == QStringLiteral("frag" )) { |
| 214 | d->stage = QShader::FragmentStage; |
| 215 | } else if (suffix == QStringLiteral("tesc" )) { |
| 216 | d->stage = QShader::TessellationControlStage; |
| 217 | } else if (suffix == QStringLiteral("tese" )) { |
| 218 | d->stage = QShader::TessellationEvaluationStage; |
| 219 | } else if (suffix == QStringLiteral("geom" )) { |
| 220 | d->stage = QShader::GeometryStage; |
| 221 | } else if (suffix == QStringLiteral("comp" )) { |
| 222 | d->stage = QShader::ComputeStage; |
| 223 | } else { |
| 224 | qWarning(msg: "QShaderBaker: Unknown shader stage, defaulting to vertex" ); |
| 225 | d->stage = QShader::VertexStage; |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | /*! |
| 230 | Sets the name of the shader source file to \a fileName. This is the file |
| 231 | that will be read when calling bake(). The shader stage is specified by \a |
| 232 | stage. |
| 233 | |
| 234 | \warning \a fileName is expected to contain trusted content. Application |
| 235 | developers are advised to carefully consider the potential implications |
| 236 | before passing in user-provided source files that are not part of the |
| 237 | application. |
| 238 | */ |
| 239 | void QShaderBaker::setSourceFileName(const QString &fileName, QShader::Stage stage) |
| 240 | { |
| 241 | if (d->readFile(fn: fileName)) |
| 242 | d->stage = stage; |
| 243 | } |
| 244 | |
| 245 | /*! |
| 246 | Sets the source \a device. This allows using any QIODevice instead of just |
| 247 | files. \a stage specifies the shader stage, while the optional \a fileName |
| 248 | contains a filename that is used in the error messages. |
| 249 | |
| 250 | \warning \a device is expected to contain trusted content. Application |
| 251 | developers are advised to carefully consider the potential implications |
| 252 | before passing in user-provided data from sources that are not under the |
| 253 | application's control. |
| 254 | */ |
| 255 | void QShaderBaker::setSourceDevice(QIODevice *device, QShader::Stage stage, const QString &fileName) |
| 256 | { |
| 257 | setSourceString(sourceString: device->readAll(), stage, fileName); |
| 258 | } |
| 259 | |
| 260 | /*! |
| 261 | Sets the input shader \a sourceString. \a stage specified the shader stage, |
| 262 | while the optional \a fileName contains a filename that is used in the |
| 263 | error messages. |
| 264 | |
| 265 | \warning \a sourceString is expected to contain trusted content. Application |
| 266 | developers are advised to carefully consider the potential implications |
| 267 | before passing in user-provided data from sources that are not under the |
| 268 | application's control. |
| 269 | */ |
| 270 | void QShaderBaker::setSourceString(const QByteArray &sourceString, QShader::Stage stage, const QString &fileName) |
| 271 | { |
| 272 | d->sourceFileName = fileName; // for error messages, include handling, etc. |
| 273 | d->source = sourceString; |
| 274 | d->stage = stage; |
| 275 | } |
| 276 | |
| 277 | /*! |
| 278 | \typedef QShaderBaker::GeneratedShader |
| 279 | |
| 280 | Synonym for QPair<QShader::Source, QShaderVersion>. |
| 281 | */ |
| 282 | |
| 283 | /*! |
| 284 | Specifies what kind of shaders to compile or translate to. Nothing is |
| 285 | generated by default so calling this function before bake() is mandatory |
| 286 | |
| 287 | \note when this function is not called or \a v is empty or contains only invalid |
| 288 | entries, the resulting QShader will be empty and thus invalid. |
| 289 | |
| 290 | For example, the minimal possible baking target is SPIR-V, without any |
| 291 | additional translations to other languages. To request this, do: |
| 292 | |
| 293 | \badcode |
| 294 | baker.setGeneratedShaders({ QShader::SpirvShader, QShaderVersion(100) }); |
| 295 | \endcode |
| 296 | |
| 297 | \note QShaderBaker only handles the SPIR-V and human-readable source |
| 298 | targets. Further compilation into API-specific intermediate formats, such |
| 299 | as QShader::DxbcShader or QShader::MetalLibShader is implemented by the |
| 300 | \c qsb command-line tool, and is not part of the QShaderBaker runtime API. |
| 301 | */ |
| 302 | void QShaderBaker::setGeneratedShaders(const QList<GeneratedShader> &v) |
| 303 | { |
| 304 | d->reqVersions = v; |
| 305 | } |
| 306 | |
| 307 | /*! |
| 308 | Specifies which shader variants are generated. Each shader version can have |
| 309 | multiple variants in the resulting QShader. |
| 310 | |
| 311 | In most cases \a v contains a single entry, QShader::StandardShader. |
| 312 | |
| 313 | \note when no variants are set, the resulting QShader will be empty and |
| 314 | thus invalid. |
| 315 | */ |
| 316 | void QShaderBaker::setGeneratedShaderVariants(const QList<QShader::Variant> &v) |
| 317 | { |
| 318 | d->variants = v; |
| 319 | } |
| 320 | |
| 321 | /*! |
| 322 | Specifies a custom \a preamble that is processed before the normal shader |
| 323 | code. |
| 324 | |
| 325 | This is more than just prepending to the source string: the validity of the |
| 326 | GLSL version directive, which is required to be placed before everything |
| 327 | else, is not affected. Line numbers in the reported error messages also |
| 328 | remain unchanged, ignoring the contents given in the \a preamble. |
| 329 | |
| 330 | One use case for preambles is to transparently insert dynamically generated |
| 331 | \c{#define} statements. |
| 332 | */ |
| 333 | void QShaderBaker::setPreamble(const QByteArray &preamble) |
| 334 | { |
| 335 | d->preamble = preamble; |
| 336 | } |
| 337 | |
| 338 | /*! |
| 339 | When generating a QShader::BatchableVertexShader variant, \a location |
| 340 | specifies the input location for the inserted vertex input. The value is by |
| 341 | default 7 and needs to be overridden only if the vertex shader already uses |
| 342 | input location 7. |
| 343 | */ |
| 344 | void QShaderBaker::(int location) |
| 345 | { |
| 346 | d->batchLoc = location; |
| 347 | } |
| 348 | |
| 349 | /*! |
| 350 | Sets per-target compilation to \a enable. By default this is disabled, |
| 351 | meaning that the Vulkan/GLSL source is compiled to SPIR-V once per variant. |
| 352 | (so once by default, twice if it is a vertex shader and the Batchable |
| 353 | variant as requested as well). The resulting SPIR-V is then translated to |
| 354 | the various target languages (GLSL, HLSL, MSL). |
| 355 | |
| 356 | In per-target compilation mode, there is a separate GLSL to SPIR-V |
| 357 | compilation step for each target, meaning for each GLSL/HLSL/MSL version |
| 358 | requested via setGeneratedShaders(). The input source is the same, but with |
| 359 | target-specific preprocessor defines inserted. This is significantly more |
| 360 | time consuming, but allows applications to provide a single shader and use |
| 361 | \c{#ifdef} blocks to differentiate. When this mode is disabled, the only |
| 362 | way to achieve the same is to provide multiple versions of the shader file, |
| 363 | process each separately, ship {.qsb} files for each, and choose the right |
| 364 | file based on run time logic. |
| 365 | |
| 366 | The following macros will be automatically defined in this mode. Note that |
| 367 | the macros are always tied to shading languages, not graphics APIs. |
| 368 | |
| 369 | \list |
| 370 | |
| 371 | \li \c{QSHADER_SPIRV} - defined when targeting SPIR-V (to be consumed, |
| 372 | typically, by Vulkan). |
| 373 | |
| 374 | \li \c{QSHADER_SPIRV_VERSION} - the targeted SPIR-V version number, such as |
| 375 | \c 100. |
| 376 | |
| 377 | \li \c{QSHADER_GLSL} - defined when targeting GLSL or GLSL ES (to be |
| 378 | consumed, typically, by OpenGL or OpenGL ES) |
| 379 | |
| 380 | \li \c{QSHADER_GLSL_VERSION} - the targeted GLSL or GLSL ES version number, |
| 381 | such as \c 100, \c 300, or \c 330. |
| 382 | |
| 383 | \li \c{QSHADER_GLSL_ES} - defined only when targeting GLSL ES |
| 384 | |
| 385 | \li \c{QSHADER_HLSL} - defined when targeting HLSL (to be consumed, |
| 386 | typically, by Direct 3D) |
| 387 | |
| 388 | \li \c{QSHADER_HLSL_VERSION} - the targeted HLSL shader model version, such |
| 389 | as \c 50 |
| 390 | |
| 391 | \li \c{QSHADER_MSL} - defined when targeting the Metal Shading Language (to |
| 392 | be consumed, typically, by Metal) |
| 393 | |
| 394 | \li \c{QSHADER_MSL_VERSION} - the targeted MSL version, such as \c 12 or |
| 395 | \c 20. |
| 396 | |
| 397 | \endlist |
| 398 | |
| 399 | This allows writing shader code like the following. |
| 400 | |
| 401 | \badcode |
| 402 | #if QSHADER_HLSL || QSHADER_MSL |
| 403 | vec2 uv = vec2(uv_coord.x, 1.0 - uv_coord.y); |
| 404 | #else |
| 405 | vec2 uv = uv_coord; |
| 406 | #endif |
| 407 | \endcode |
| 408 | |
| 409 | \note Version numbers follow the GLSL-inspired QShaderVersion syntax and |
| 410 | thus are a single integer always. |
| 411 | |
| 412 | \note There is only one QShaderDescription per QShader, no matter how many |
| 413 | individual targets there are. Therefore members of uniform blocks, vertex |
| 414 | inputs, etc. must not be made conditional using the macros described above. |
| 415 | |
| 416 | \warning Be aware of the differences between the concepts of graphics APIs |
| 417 | and shading languages. QShaderBaker and the related tools work strictly |
| 418 | with the concept of shading languages, ignoring how the results are |
| 419 | consumed afterwards. Therefore, if the higher layers in the Qt graphics |
| 420 | stack one day start using SPIR-V also for an API other than Vulkan, the |
| 421 | assumption that QSHADER_SPIRV implies Vulkan will no longer hold. |
| 422 | */ |
| 423 | void QShaderBaker::setPerTargetCompilation(bool enable) |
| 424 | { |
| 425 | d->perTargetEnabled = enable; |
| 426 | } |
| 427 | |
| 428 | /*! |
| 429 | Controls the behavior when shader translation (from SPIR-V to |
| 430 | GLSL/HLSL/MSL) fails. By default this setting is true, which will cause |
| 431 | bake() to return with an error if a requested shader cannot be generated. |
| 432 | If that is not desired, and the intention is to generate what we can but |
| 433 | silently skip the rest, then set \a enable to false. |
| 434 | |
| 435 | Targeting multiple GLSL versions can lead to errors when a feature is not |
| 436 | translatable to a given version. For example, attempting to translate a |
| 437 | shader using textureSize() to GLSL ES 100 would fail the entire bake() call |
| 438 | with the error message "textureSize is not supported in ESSL 100". If it is |
| 439 | acceptable to not have a GLSL ES 100 shader in the result, even though it |
| 440 | was requested, then setting this flag to false makes bake() to succeed. |
| 441 | */ |
| 442 | void QShaderBaker::setBreakOnShaderTranslationError(bool enable) |
| 443 | { |
| 444 | d->breakOnShaderTranslationError = enable; |
| 445 | } |
| 446 | |
| 447 | /*! |
| 448 | When generating MSL shader code for a tessellation control shader, the |
| 449 | tessellation \a mode (triangles or quads) must be known upfront. In GLSL |
| 450 | this is declared in the tessellation evaluation shader typically, but for |
| 451 | Metal it must be known also when generating the compute shader from the |
| 452 | tessellation control shader. |
| 453 | |
| 454 | When not set, the default is triangles. |
| 455 | */ |
| 456 | void QShaderBaker::setTessellationMode(QShaderDescription::TessellationMode mode) |
| 457 | { |
| 458 | d->tessInfo.infoForTesc.mode = mode; |
| 459 | } |
| 460 | |
| 461 | /*! |
| 462 | When generating MSL shader code for a tessellation evaluation shader, the |
| 463 | output vertex \a count of the tessellation control shader must be known |
| 464 | upfront. in GLSL this would be declared in the tessellation control shader |
| 465 | typically, but for Metal it must be known also when generating the vertex |
| 466 | shader from the teselation evaluation shader. |
| 467 | |
| 468 | When not set, the default value is 3. |
| 469 | */ |
| 470 | void QShaderBaker::setTessellationOutputVertexCount(int count) |
| 471 | { |
| 472 | d->tessInfo.infoForTese.vertexCount = count; |
| 473 | } |
| 474 | |
| 475 | /*! |
| 476 | When transpiling shaders using multiview (e.g. a vertex shader using |
| 477 | gl_ViewIndex for a renderer relying on GL_OVR_multiview2, VK_KHR_multiview, |
| 478 | etc.), for some of the targets it is necessary to declare the number of |
| 479 | views in the shader. This is not done in the Vulkan-style GLSL code, and is |
| 480 | not relevant for targets such as SPIR-V or HLSL, but is required for OpenGL |
| 481 | and GLSL, and so the value has to be provided as additional metadata. |
| 482 | |
| 483 | By default the value is 0, which disables injecting the \c{num_views} |
| 484 | statement. Setting 1 is not useful since that is the default \c{num_views} |
| 485 | regardless. Therefore \a count should be >= 2 to make an effect. When set |
| 486 | to, for example, 2, the generated GLSL shader will contain a |
| 487 | \c{layout(num_views = 2) in;} statement. |
| 488 | |
| 489 | Setting a \a count of 2 or greater also injects some preprocessor |
| 490 | statements: |
| 491 | \c{QSHADER_VIEW_COUNT} is set to \a count, whereas the |
| 492 | \c GL_EXT_multiview extension is enabled automatically. Therefore, setting |
| 493 | the appropriate |
| 494 | \a count can be relevant with other types of shaders as well, e.g. when |
| 495 | sharing a uniform buffer between the vertex and fragment shader and both |
| 496 | shaders have to be able to write something like |
| 497 | \c{#if QSHADER_VIEW_COUNT >= 2}. |
| 498 | |
| 499 | \since 6.7 |
| 500 | */ |
| 501 | void QShaderBaker::setMultiViewCount(int count) |
| 502 | { |
| 503 | d->multiViewInfo.viewCount = count >= 2 ? count : 0; |
| 504 | } |
| 505 | |
| 506 | /*! |
| 507 | \enum QShaderBaker::SpirvOption |
| 508 | \value GenerateFullDebugInfo Generate and store additional debug information in the SPIR-V binary. |
| 509 | \value StripDebugAndVarInfo Strip all debug and variable name information from the SPIR-V binary. |
| 510 | */ |
| 511 | |
| 512 | /*! |
| 513 | Sets additional \a options for the generated SPIR-V binary. |
| 514 | By default no flags are set. |
| 515 | */ |
| 516 | void QShaderBaker::setSpirvOptions(SpirvOptions options) |
| 517 | { |
| 518 | d->spirvOptions = options; |
| 519 | } |
| 520 | |
| 521 | /*! |
| 522 | \enum QShaderBaker::GlslOption |
| 523 | \value GlslEsFragDefaultFloatPrecisionMedium Emit \c{precision mediump float;} in fragment shaders for GLSL ES. |
| 524 | */ |
| 525 | |
| 526 | /*! |
| 527 | Sets additional \a options for the generated GLSL and GLSL ES sources. |
| 528 | By default no flags are set. |
| 529 | \since 6.9 |
| 530 | */ |
| 531 | void QShaderBaker::setGlslOptions(GlslOptions options) |
| 532 | { |
| 533 | d->glslOptions = options; |
| 534 | } |
| 535 | |
| 536 | inline size_t qHash(const QShaderBaker::GeneratedShader &k, size_t seed = 0) |
| 537 | { |
| 538 | return qHash(e: k.first, seed) ^ k.second.version(); |
| 539 | } |
| 540 | |
| 541 | QPair<QByteArray, QByteArray> QShaderBakerPrivate::compile() |
| 542 | { |
| 543 | QSpirvCompiler::Flags flags; |
| 544 | if (spirvOptions.testFlag(flag: QShaderBaker::SpirvOption::GenerateFullDebugInfo)) |
| 545 | flags |= QSpirvCompiler::FullDebugInfo; |
| 546 | |
| 547 | compiler.setFlags(flags); |
| 548 | const QByteArray spirvBin = compiler.compileToSpirv(); |
| 549 | if (spirvBin.isEmpty()) { |
| 550 | errorMessage = compiler.errorMessage(); |
| 551 | return {}; |
| 552 | } |
| 553 | QByteArray batchableSpirvBin; |
| 554 | if (stage == QShader::VertexStage && variants.contains(t: QShader::BatchableVertexShader)) { |
| 555 | compiler.setFlags(flags | QSpirvCompiler::RewriteToMakeBatchableForSG); |
| 556 | compiler.setSGBatchingVertexInputLocation(batchLoc); |
| 557 | batchableSpirvBin = compiler.compileToSpirv(); |
| 558 | if (batchableSpirvBin.isEmpty()) { |
| 559 | errorMessage = compiler.errorMessage(); |
| 560 | return {}; |
| 561 | } |
| 562 | } |
| 563 | return { spirvBin, batchableSpirvBin }; |
| 564 | } |
| 565 | |
| 566 | QByteArray QShaderBakerPrivate::perTargetDefines(const QShaderBaker::GeneratedShader &key) |
| 567 | { |
| 568 | QByteArray preamble; |
| 569 | switch (key.first) { |
| 570 | case QShader::SpirvShader: |
| 571 | preamble += QByteArrayLiteral("\n#define QSHADER_SPIRV 1\n#define QSHADER_SPIRV_VERSION " ); |
| 572 | preamble += QByteArray::number(key.second.version()); |
| 573 | preamble += QByteArrayLiteral("\n" ); |
| 574 | break; |
| 575 | case QShader::GlslShader: |
| 576 | preamble += QByteArrayLiteral("\n#define QSHADER_GLSL 1\n#define QSHADER_GLSL_VERSION " ); |
| 577 | preamble += QByteArray::number(key.second.version()); |
| 578 | if (key.second.flags().testFlag(flag: QShaderVersion::GlslEs)) |
| 579 | preamble += QByteArrayLiteral("\n#define QSHADER_GLSL_ES 1" ); |
| 580 | preamble += QByteArrayLiteral("\n" ); |
| 581 | break; |
| 582 | case QShader::HlslShader: |
| 583 | preamble += QByteArrayLiteral("\n#define QSHADER_HLSL 1\n#define QSHADER_HLSL_VERSION " ); |
| 584 | preamble += QByteArray::number(key.second.version()); |
| 585 | preamble += QByteArrayLiteral("\n" ); |
| 586 | break; |
| 587 | case QShader::MslShader: |
| 588 | preamble += QByteArrayLiteral("\n#define QSHADER_MSL 1\n#define QSHADER_MSL_VERSION " ); |
| 589 | preamble += QByteArray::number(key.second.version()); |
| 590 | preamble += QByteArrayLiteral("\n" ); |
| 591 | break; |
| 592 | default: |
| 593 | Q_UNREACHABLE(); |
| 594 | } |
| 595 | return preamble; |
| 596 | } |
| 597 | |
| 598 | /*! |
| 599 | Runs the compilation and translation process. |
| 600 | |
| 601 | \return a QShader instance. To check if the process was successful, |
| 602 | call QShader::isValid(). When that indicates \c false, call |
| 603 | errorMessage() to retrieve the log. |
| 604 | |
| 605 | This is an expensive operation. When calling this from applications, it can |
| 606 | be advisable to do it on a separate thread. |
| 607 | |
| 608 | \note QShaderBaker instances are reusable: after calling bake(), the same |
| 609 | instance can be used with different inputs again. However, a QShaderBaker |
| 610 | instance should only be used on one single thread during its lifetime. |
| 611 | */ |
| 612 | QShader QShaderBaker::bake() |
| 613 | { |
| 614 | d->errorMessage.clear(); |
| 615 | |
| 616 | if (d->source.isEmpty()) { |
| 617 | d->errorMessage = QLatin1String("QShaderBaker: No source specified" ); |
| 618 | return QShader(); |
| 619 | } |
| 620 | |
| 621 | d->compiler.setSourceString(sourceString: d->source, stage: d->stage, fileName: d->sourceFileName); |
| 622 | |
| 623 | // Normally one entry, for QShader::SpirvShader only. However, in |
| 624 | // compile-per-target mode there is a separate SPIR-V binary generated for |
| 625 | // each target (so for each GLSL/HLSL/MSL version requested). |
| 626 | QHash<GeneratedShader, QByteArray> spirv; |
| 627 | QHash<GeneratedShader, QByteArray> batchableSpirv; |
| 628 | const auto compileSpirvAndBatchable = [this, &spirv, &batchableSpirv](const GeneratedShader &key) { |
| 629 | const QPair<QByteArray, QByteArray> bin = d->compile(); |
| 630 | if (bin.first.isEmpty()) |
| 631 | return false; |
| 632 | spirv.insert(key, value: bin.first); |
| 633 | if (!bin.second.isEmpty()) |
| 634 | batchableSpirv.insert(key, value: bin.second); |
| 635 | return true; |
| 636 | }; |
| 637 | |
| 638 | QByteArray preamble = d->preamble; |
| 639 | if (d->multiViewInfo.viewCount >= 2) { |
| 640 | if (d->stage == QShader::VertexStage) |
| 641 | preamble += QByteArrayLiteral("\n#extension GL_EXT_multiview : require\n" ); |
| 642 | preamble += QByteArrayLiteral("\n#define QSHADER_VIEW_COUNT " ); |
| 643 | preamble += QByteArray::number(d->multiViewInfo.viewCount); |
| 644 | preamble += QByteArrayLiteral("\n" ); |
| 645 | } |
| 646 | |
| 647 | if (!d->perTargetEnabled) { |
| 648 | d->compiler.setPreamble(preamble); |
| 649 | if (!compileSpirvAndBatchable({ QShader::SpirvShader, {} })) |
| 650 | return QShader(); |
| 651 | } else { |
| 652 | // per-target compilation. the value here comes from the varying |
| 653 | // preamble (and so preprocessor defines) |
| 654 | for (GeneratedShader req: d->reqVersions) { |
| 655 | d->compiler.setPreamble(preamble + d->perTargetDefines(key: req)); |
| 656 | if (!compileSpirvAndBatchable(req)) |
| 657 | return QShader(); |
| 658 | } |
| 659 | } |
| 660 | |
| 661 | // Now spirv, and, if in use, batchableSpirv, contain at least one, |
| 662 | // optionally more SPIR-V binaries. |
| 663 | Q_ASSERT(!spirv.isEmpty() && (d->perTargetEnabled || spirv.size() == 1)); |
| 664 | |
| 665 | QShader bs; |
| 666 | bs.setStage(d->stage); |
| 667 | |
| 668 | QSpirvShader spirvShader; |
| 669 | QSpirvShader batchableSpirvShader; |
| 670 | // The QShaderDescription can be different for variants (we just have a |
| 671 | // hardcoded rule to pick one), but cannot differ for targets (in |
| 672 | // per-target mode, hence we can just pick the first SPIR-V binary and |
| 673 | // generate the reflection data based on that) |
| 674 | spirvShader.setSpirvBinary(spirv: spirv.constKeyValueBegin()->second, stage: d->stage); |
| 675 | if (batchableSpirv.isEmpty()) { |
| 676 | bs.setDescription(spirvShader.shaderDescription()); |
| 677 | } else { |
| 678 | batchableSpirvShader.setSpirvBinary(spirv: batchableSpirv.constKeyValueBegin()->second, stage: d->stage); |
| 679 | // prefer the batchable's reflection info with _qt_order and such present |
| 680 | bs.setDescription(batchableSpirvShader.shaderDescription()); |
| 681 | } |
| 682 | |
| 683 | for (const GeneratedShader &req: d->reqVersions) { |
| 684 | for (const QShader::Variant &v : d->variants) { |
| 685 | if (d->stage != QShader::VertexStage) { |
| 686 | if (v == QShader::BatchableVertexShader |
| 687 | || v == QShader::UInt32IndexedVertexAsComputeShader |
| 688 | || v == QShader::UInt16IndexedVertexAsComputeShader |
| 689 | || v == QShader::NonIndexedVertexAsComputeShader) |
| 690 | { |
| 691 | continue; |
| 692 | } |
| 693 | } |
| 694 | if (req.first != QShader::MslShader && req.first != QShader::MetalLibShader) { |
| 695 | if (v == QShader::UInt32IndexedVertexAsComputeShader |
| 696 | || v == QShader::UInt16IndexedVertexAsComputeShader |
| 697 | || v == QShader::NonIndexedVertexAsComputeShader) |
| 698 | { |
| 699 | continue; |
| 700 | } |
| 701 | } |
| 702 | |
| 703 | QSpirvShader *currentSpirvShader = nullptr; |
| 704 | if (d->perTargetEnabled) { |
| 705 | // This is expensive too, in addition to the multiple |
| 706 | // compilation rounds, but opting in to per-target mode is a |
| 707 | // careful, conscious choice (hopefully), so it's fine. |
| 708 | if (v == QShader::BatchableVertexShader) |
| 709 | batchableSpirvShader.setSpirvBinary(spirv: batchableSpirv[req], stage: d->stage); |
| 710 | else |
| 711 | spirvShader.setSpirvBinary(spirv: spirv[req], stage: d->stage); |
| 712 | } |
| 713 | if (v == QShader::BatchableVertexShader) |
| 714 | currentSpirvShader = &batchableSpirvShader; |
| 715 | else |
| 716 | currentSpirvShader = &spirvShader; |
| 717 | Q_ASSERT(currentSpirvShader); |
| 718 | Q_ASSERT(!currentSpirvShader->spirvBinary().isEmpty()); |
| 719 | const QShaderKey key(req.first, req.second, v); |
| 720 | QShaderCode shader; |
| 721 | shader.setEntryPoint(QByteArrayLiteral("main" )); |
| 722 | switch (req.first) { |
| 723 | case QShader::SpirvShader: |
| 724 | if (d->spirvOptions.testFlag(flag: QShaderBaker::SpirvOption::StripDebugAndVarInfo)) { |
| 725 | QString errorMsg; |
| 726 | const QByteArray strippedSpirv = currentSpirvShader->remappedSpirvBinary(flags: QSpirvShader::RemapFlag::StripOnly, errorMessage: &errorMsg); |
| 727 | if (strippedSpirv.isEmpty()) { |
| 728 | d->errorMessage = errorMsg; |
| 729 | return QShader(); |
| 730 | } |
| 731 | shader.setShader(strippedSpirv); |
| 732 | } else { |
| 733 | shader.setShader(currentSpirvShader->spirvBinary()); |
| 734 | } |
| 735 | break; |
| 736 | case QShader::GlslShader: |
| 737 | { |
| 738 | QSpirvShader::GlslFlags flags; |
| 739 | if (req.second.flags().testFlag(flag: QShaderVersion::GlslEs)) { |
| 740 | flags |= QSpirvShader::GlslFlag::GlslEs; |
| 741 | if (d->glslOptions.testFlag(flag: GlslOption::GlslEsFragDefaultFloatPrecisionMedium)) |
| 742 | flags |= QSpirvShader::GlslFlag::EsFragDefaultFloatPrecisionMedium; |
| 743 | } |
| 744 | QVector<QSpirvShader::SeparateToCombinedImageSamplerMapping> separateToCombinedImageSamplerMappings; |
| 745 | shader.setShader(currentSpirvShader->translateToGLSL(version: req.second.version(), |
| 746 | flags, |
| 747 | stage: d->stage, |
| 748 | multiViewInfo: d->multiViewInfo, |
| 749 | separateToCombinedImageSamplerMappings: &separateToCombinedImageSamplerMappings)); |
| 750 | if (shader.shader().isEmpty()) { |
| 751 | if (d->breakOnShaderTranslationError) { |
| 752 | d->errorMessage = currentSpirvShader->translationErrorMessage(); |
| 753 | return QShader(); |
| 754 | } else { |
| 755 | d->errorMessage += QLatin1String(" " ) + currentSpirvShader->translationErrorMessage(); |
| 756 | continue; |
| 757 | } |
| 758 | } |
| 759 | if (!separateToCombinedImageSamplerMappings.isEmpty()) { |
| 760 | const QShaderDescription desc = bs.description(); |
| 761 | QVector<QShaderDescription::InOutVariable> separateImages = desc.separateImages(); |
| 762 | QVector<QShaderDescription::InOutVariable> separateSamplers = desc.separateSamplers(); |
| 763 | QShader::SeparateToCombinedImageSamplerMappingList result; |
| 764 | for (const QSpirvShader::SeparateToCombinedImageSamplerMapping &mapping : separateToCombinedImageSamplerMappings) { |
| 765 | int textureBinding = -1; |
| 766 | int samplerBinding = -1; |
| 767 | for (int i = 0, count = separateImages.size(); i < count; ++i) { |
| 768 | if (separateImages[i].name == mapping.textureName) { |
| 769 | textureBinding = separateImages[i].binding; |
| 770 | break; |
| 771 | } |
| 772 | } |
| 773 | for (int i = 0, count = separateSamplers.size(); i < count; ++i) { |
| 774 | if (separateSamplers[i].name == mapping.samplerName) { |
| 775 | samplerBinding = separateSamplers[i].binding; |
| 776 | break; |
| 777 | } |
| 778 | } |
| 779 | result.append(t: { .combinedSamplerName: mapping.combinedSamplerName, .textureBinding: textureBinding, .samplerBinding: samplerBinding }); |
| 780 | } |
| 781 | bs.setSeparateToCombinedImageSamplerMappingList(key, list: result); |
| 782 | } |
| 783 | } |
| 784 | break; |
| 785 | case QShader::HlslShader: |
| 786 | { |
| 787 | QShader::NativeResourceBindingMap nativeBindings; |
| 788 | shader.setShader(currentSpirvShader->translateToHLSL(version: req.second.version(), nativeBindings: &nativeBindings)); |
| 789 | if (shader.shader().isEmpty()) { |
| 790 | if (d->breakOnShaderTranslationError) { |
| 791 | d->errorMessage = currentSpirvShader->translationErrorMessage(); |
| 792 | return QShader(); |
| 793 | } else { |
| 794 | d->errorMessage += QLatin1String(" " ) + currentSpirvShader->translationErrorMessage(); |
| 795 | continue; |
| 796 | } |
| 797 | } |
| 798 | bs.setResourceBindingMap(key, map: nativeBindings); |
| 799 | } |
| 800 | break; |
| 801 | case QShader::MslShader: |
| 802 | { |
| 803 | QShader::NativeResourceBindingMap nativeBindings; |
| 804 | QShader::NativeShaderInfo shaderInfo; |
| 805 | QSpirvShader::MslFlags flags; |
| 806 | if (d->stage == QShader::VertexStage) { |
| 807 | switch (v) { |
| 808 | case QShader::UInt16IndexedVertexAsComputeShader: |
| 809 | flags |= QSpirvShader::MslFlag::VertexAsCompute; |
| 810 | flags |= QSpirvShader::MslFlag::WithUInt16Index; |
| 811 | break; |
| 812 | case QShader::UInt32IndexedVertexAsComputeShader: |
| 813 | flags |= QSpirvShader::MslFlag::VertexAsCompute; |
| 814 | flags |= QSpirvShader::MslFlag::WithUInt32Index; |
| 815 | break; |
| 816 | case QShader::NonIndexedVertexAsComputeShader: |
| 817 | flags |= QSpirvShader::MslFlag::VertexAsCompute; |
| 818 | break; |
| 819 | default: |
| 820 | break; |
| 821 | } |
| 822 | } |
| 823 | shader.setShader(currentSpirvShader->translateToMSL(version: req.second.version(), |
| 824 | flags, |
| 825 | stage: d->stage, |
| 826 | nativeBindings: &nativeBindings, |
| 827 | shaderInfo: &shaderInfo, |
| 828 | multiViewInfo: d->multiViewInfo, |
| 829 | tessInfo: d->tessInfo)); |
| 830 | if (shader.shader().isEmpty()) { |
| 831 | if (d->breakOnShaderTranslationError) { |
| 832 | d->errorMessage = currentSpirvShader->translationErrorMessage(); |
| 833 | return QShader(); |
| 834 | } else { |
| 835 | d->errorMessage += QLatin1String(" " ) + currentSpirvShader->translationErrorMessage(); |
| 836 | continue; |
| 837 | } |
| 838 | } |
| 839 | shader.setEntryPoint(QByteArrayLiteral("main0" )); |
| 840 | bs.setResourceBindingMap(key, map: nativeBindings); |
| 841 | bs.setNativeShaderInfo(key, info: shaderInfo); |
| 842 | } |
| 843 | break; |
| 844 | default: |
| 845 | Q_UNREACHABLE(); |
| 846 | } |
| 847 | bs.setShader(key, shader); |
| 848 | } |
| 849 | } |
| 850 | |
| 851 | return bs; |
| 852 | } |
| 853 | |
| 854 | /*! |
| 855 | \return the error message from the last bake() run, or an empty string if |
| 856 | there was no error. |
| 857 | |
| 858 | \note Errors include file read errors, compilation, and translation |
| 859 | failures. Not requesting any targets or variants does not count as an error |
| 860 | even though the resulting QShader is invalid. |
| 861 | */ |
| 862 | QString QShaderBaker::errorMessage() const |
| 863 | { |
| 864 | return d->errorMessage; |
| 865 | } |
| 866 | |
| 867 | QT_END_NAMESPACE |
| 868 | |