| 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 | QSpirvCompiler compiler; |
| 142 | QString errorMessage; |
| 143 | }; |
| 144 | |
| 145 | bool QShaderBakerPrivate::readFile(const QString &fn) |
| 146 | { |
| 147 | QFile f(fn); |
| 148 | if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) { |
| 149 | qWarning(msg: "QShaderBaker: Failed to open %s" , qPrintable(fn)); |
| 150 | return false; |
| 151 | } |
| 152 | source = f.readAll(); |
| 153 | sourceFileName = fn; |
| 154 | return true; |
| 155 | } |
| 156 | |
| 157 | /*! |
| 158 | Constructs a new QShaderBaker. |
| 159 | */ |
| 160 | QShaderBaker::QShaderBaker() |
| 161 | : d(new QShaderBakerPrivate) |
| 162 | { |
| 163 | } |
| 164 | |
| 165 | /*! |
| 166 | Destructor. |
| 167 | */ |
| 168 | QShaderBaker::~QShaderBaker() |
| 169 | { |
| 170 | delete d; |
| 171 | } |
| 172 | |
| 173 | /*! |
| 174 | Sets the name of the shader source file to \a fileName. This is the file |
| 175 | that will be read when calling bake(). The shader stage is deduced |
| 176 | automatically from the file extension. When this is not desired or not |
| 177 | possible, use the overload with the stage argument instead. |
| 178 | |
| 179 | The supported file extensions are: |
| 180 | \list |
| 181 | \li \c{.vert} - vertex shader |
| 182 | \li \c{.frag} - fragment (pixel) shader |
| 183 | \li \c{.tesc} - tessellation control (hull) shader |
| 184 | \li \c{.tese} - tessellation evaluation (domain) shader |
| 185 | \li \c{.geom} - geometry shader |
| 186 | \li \c{.comp} - compute shader |
| 187 | \endlist |
| 188 | */ |
| 189 | void QShaderBaker::setSourceFileName(const QString &fileName) |
| 190 | { |
| 191 | if (!d->readFile(fn: fileName)) |
| 192 | return; |
| 193 | |
| 194 | const QString suffix = QFileInfo(fileName).suffix(); |
| 195 | if (suffix == QStringLiteral("vert" )) { |
| 196 | d->stage = QShader::VertexStage; |
| 197 | } else if (suffix == QStringLiteral("frag" )) { |
| 198 | d->stage = QShader::FragmentStage; |
| 199 | } else if (suffix == QStringLiteral("tesc" )) { |
| 200 | d->stage = QShader::TessellationControlStage; |
| 201 | } else if (suffix == QStringLiteral("tese" )) { |
| 202 | d->stage = QShader::TessellationEvaluationStage; |
| 203 | } else if (suffix == QStringLiteral("geom" )) { |
| 204 | d->stage = QShader::GeometryStage; |
| 205 | } else if (suffix == QStringLiteral("comp" )) { |
| 206 | d->stage = QShader::ComputeStage; |
| 207 | } else { |
| 208 | qWarning(msg: "QShaderBaker: Unknown shader stage, defaulting to vertex" ); |
| 209 | d->stage = QShader::VertexStage; |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | /*! |
| 214 | Sets the name of the shader source file to \a fileName. This is the file |
| 215 | that will be read when calling bake(). The shader stage is specified by \a |
| 216 | stage. |
| 217 | */ |
| 218 | void QShaderBaker::setSourceFileName(const QString &fileName, QShader::Stage stage) |
| 219 | { |
| 220 | if (d->readFile(fn: fileName)) |
| 221 | d->stage = stage; |
| 222 | } |
| 223 | |
| 224 | /*! |
| 225 | Sets the source \a device. This allows using any QIODevice instead of just |
| 226 | files. \a stage specifies the shader stage, while the optional \a fileName |
| 227 | contains a filename that is used in the error messages. |
| 228 | */ |
| 229 | void QShaderBaker::setSourceDevice(QIODevice *device, QShader::Stage stage, const QString &fileName) |
| 230 | { |
| 231 | setSourceString(sourceString: device->readAll(), stage, fileName); |
| 232 | } |
| 233 | |
| 234 | /*! |
| 235 | Sets the input shader \a sourceString. \a stage specified the shader stage, |
| 236 | while the optional \a fileName contains a filename that is used in the |
| 237 | error messages. |
| 238 | */ |
| 239 | void QShaderBaker::setSourceString(const QByteArray &sourceString, QShader::Stage stage, const QString &fileName) |
| 240 | { |
| 241 | d->sourceFileName = fileName; // for error messages, include handling, etc. |
| 242 | d->source = sourceString; |
| 243 | d->stage = stage; |
| 244 | } |
| 245 | |
| 246 | /*! |
| 247 | \typedef QShaderBaker::GeneratedShader |
| 248 | |
| 249 | Synonym for QPair<QShader::Source, QShaderVersion>. |
| 250 | */ |
| 251 | |
| 252 | /*! |
| 253 | Specifies what kind of shaders to compile or translate to. Nothing is |
| 254 | generated by default so calling this function before bake() is mandatory |
| 255 | |
| 256 | \note when this function is not called or \a v is empty or contains only invalid |
| 257 | entries, the resulting QShader will be empty and thus invalid. |
| 258 | |
| 259 | For example, the minimal possible baking target is SPIR-V, without any |
| 260 | additional translations to other languages. To request this, do: |
| 261 | |
| 262 | \badcode |
| 263 | baker.setGeneratedShaders({ QShader::SpirvShader, QShaderVersion(100) }); |
| 264 | \endcode |
| 265 | |
| 266 | \note QShaderBaker only handles the SPIR-V and human-readable source |
| 267 | targets. Further compilation into API-specific intermediate formats, such |
| 268 | as QShader::DxbcShader or QShader::MetalLibShader is implemented by the |
| 269 | \c qsb command-line tool, and is not part of the QShaderBaker runtime API. |
| 270 | */ |
| 271 | void QShaderBaker::setGeneratedShaders(const QList<GeneratedShader> &v) |
| 272 | { |
| 273 | d->reqVersions = v; |
| 274 | } |
| 275 | |
| 276 | /*! |
| 277 | Specifies which shader variants are generated. Each shader version can have |
| 278 | multiple variants in the resulting QShader. |
| 279 | |
| 280 | In most cases \a v contains a single entry, QShader::StandardShader. |
| 281 | |
| 282 | \note when no variants are set, the resulting QShader will be empty and |
| 283 | thus invalid. |
| 284 | */ |
| 285 | void QShaderBaker::setGeneratedShaderVariants(const QList<QShader::Variant> &v) |
| 286 | { |
| 287 | d->variants = v; |
| 288 | } |
| 289 | |
| 290 | /*! |
| 291 | Specifies a custom \a preamble that is processed before the normal shader |
| 292 | code. |
| 293 | |
| 294 | This is more than just prepending to the source string: the validity of the |
| 295 | GLSL version directive, which is required to be placed before everything |
| 296 | else, is not affected. Line numbers in the reported error messages also |
| 297 | remain unchanged, ignoring the contents given in the \a preamble. |
| 298 | |
| 299 | One use case for preambles is to transparently insert dynamically generated |
| 300 | \c{#define} statements. |
| 301 | */ |
| 302 | void QShaderBaker::setPreamble(const QByteArray &preamble) |
| 303 | { |
| 304 | d->preamble = preamble; |
| 305 | } |
| 306 | |
| 307 | /*! |
| 308 | When generating a QShader::BatchableVertexShader variant, \a location |
| 309 | specifies the input location for the inserted vertex input. The value is by |
| 310 | default 7 and needs to be overridden only if the vertex shader already uses |
| 311 | input location 7. |
| 312 | */ |
| 313 | void QShaderBaker::(int location) |
| 314 | { |
| 315 | d->batchLoc = location; |
| 316 | } |
| 317 | |
| 318 | /*! |
| 319 | Sets per-target compilation to \a enable. By default this is disabled, |
| 320 | meaning that the Vulkan/GLSL source is compiled to SPIR-V once per variant. |
| 321 | (so once by default, twice if it is a vertex shader and the Batchable |
| 322 | variant as requested as well). The resulting SPIR-V is then translated to |
| 323 | the various target languages (GLSL, HLSL, MSL). |
| 324 | |
| 325 | In per-target compilation mode, there is a separate GLSL to SPIR-V |
| 326 | compilation step for each target, meaning for each GLSL/HLSL/MSL version |
| 327 | requested via setGeneratedShaders(). The input source is the same, but with |
| 328 | target-specific preprocessor defines inserted. This is significantly more |
| 329 | time consuming, but allows applications to provide a single shader and use |
| 330 | \c{#ifdef} blocks to differentiate. When this mode is disabled, the only |
| 331 | way to achieve the same is to provide multiple versions of the shader file, |
| 332 | process each separately, ship {.qsb} files for each, and choose the right |
| 333 | file based on run time logic. |
| 334 | |
| 335 | The following macros will be automatically defined in this mode. Note that |
| 336 | the macros are always tied to shading languages, not graphics APIs. |
| 337 | |
| 338 | \list |
| 339 | |
| 340 | \li \c{QSHADER_SPIRV} - defined when targeting SPIR-V (to be consumed, |
| 341 | typically, by Vulkan). |
| 342 | |
| 343 | \li \c{QSHADER_SPIRV_VERSION} - the targeted SPIR-V version number, such as |
| 344 | \c 100. |
| 345 | |
| 346 | \li \c{QSHADER_GLSL} - defined when targeting GLSL or GLSL ES (to be |
| 347 | consumed, typically, by OpenGL or OpenGL ES) |
| 348 | |
| 349 | \li \c{QSHADER_GLSL_VERSION} - the targeted GLSL or GLSL ES version number, |
| 350 | such as \c 100, \c 300, or \c 330. |
| 351 | |
| 352 | \li \c{QSHADER_GLSL_ES} - defined only when targeting GLSL ES |
| 353 | |
| 354 | \li \c{QSHADER_HLSL} - defined when targeting HLSL (to be consumed, |
| 355 | typically, by Direct 3D) |
| 356 | |
| 357 | \li \c{QSHADER_HLSL_VERSION} - the targeted HLSL shader model version, such |
| 358 | as \c 50 |
| 359 | |
| 360 | \li \c{QSHADER_MSL} - defined when targeting the Metal Shading Language (to |
| 361 | be consumed, typically, by Metal) |
| 362 | |
| 363 | \li \c{QSHADER_MSL_VERSION} - the targeted MSL version, such as \c 12 or |
| 364 | \c 20. |
| 365 | |
| 366 | \endlist |
| 367 | |
| 368 | This allows writing shader code like the following. |
| 369 | |
| 370 | \badcode |
| 371 | #if QSHADER_HLSL || QSHADER_MSL |
| 372 | vec2 uv = vec2(uv_coord.x, 1.0 - uv_coord.y); |
| 373 | #else |
| 374 | vec2 uv = uv_coord; |
| 375 | #endif |
| 376 | \endcode |
| 377 | |
| 378 | \note Version numbers follow the GLSL-inspired QShaderVersion syntax and |
| 379 | thus are a single integer always. |
| 380 | |
| 381 | \note There is only one QShaderDescription per QShader, no matter how many |
| 382 | individual targets there are. Therefore members of uniform blocks, vertex |
| 383 | inputs, etc. must not be made conditional using the macros described above. |
| 384 | |
| 385 | \warning Be aware of the differences between the concepts of graphics APIs |
| 386 | and shading languages. QShaderBaker and the related tools work strictly |
| 387 | with the concept of shading languages, ignoring how the results are |
| 388 | consumed afterwards. Therefore, if the higher layers in the Qt graphics |
| 389 | stack one day start using SPIR-V also for an API other than Vulkan, the |
| 390 | assumption that QSHADER_SPIRV implies Vulkan will no longer hold. |
| 391 | */ |
| 392 | void QShaderBaker::setPerTargetCompilation(bool enable) |
| 393 | { |
| 394 | d->perTargetEnabled = enable; |
| 395 | } |
| 396 | |
| 397 | /*! |
| 398 | Controls the behavior when shader translation (from SPIR-V to |
| 399 | GLSL/HLSL/MSL) fails. By default this setting is true, which will cause |
| 400 | bake() to return with an error if a requested shader cannot be generated. |
| 401 | If that is not desired, and the intention is to generate what we can but |
| 402 | silently skip the rest, then set \a enable to false. |
| 403 | |
| 404 | Targeting multiple GLSL versions can lead to errors when a feature is not |
| 405 | translatable to a given version. For example, attempting to translate a |
| 406 | shader using textureSize() to GLSL ES 100 would fail the entire bake() call |
| 407 | with the error message "textureSize is not supported in ESSL 100". If it is |
| 408 | acceptable to not have a GLSL ES 100 shader in the result, even though it |
| 409 | was requested, then setting this flag to false makes bake() to succeed. |
| 410 | */ |
| 411 | void QShaderBaker::setBreakOnShaderTranslationError(bool enable) |
| 412 | { |
| 413 | d->breakOnShaderTranslationError = enable; |
| 414 | } |
| 415 | |
| 416 | /*! |
| 417 | When generating MSL shader code for a tessellation control shader, the |
| 418 | tessellation \a mode (triangles or quads) must be known upfront. In GLSL |
| 419 | this is declared in the tessellation evaluation shader typically, but for |
| 420 | Metal it must be known also when generating the compute shader from the |
| 421 | tessellation control shader. |
| 422 | |
| 423 | When not set, the default is triangles. |
| 424 | */ |
| 425 | void QShaderBaker::setTessellationMode(QShaderDescription::TessellationMode mode) |
| 426 | { |
| 427 | d->tessInfo.infoForTesc.mode = mode; |
| 428 | } |
| 429 | |
| 430 | /*! |
| 431 | When generating MSL shader code for a tessellation evaluation shader, the |
| 432 | output vertex \a count of the tessellation control shader must be known |
| 433 | upfront. in GLSL this would be declared in the tessellation control shader |
| 434 | typically, but for Metal it must be known also when generating the vertex |
| 435 | shader from the teselation evaluation shader. |
| 436 | |
| 437 | When not set, the default value is 3. |
| 438 | */ |
| 439 | void QShaderBaker::setTessellationOutputVertexCount(int count) |
| 440 | { |
| 441 | d->tessInfo.infoForTese.vertexCount = count; |
| 442 | } |
| 443 | |
| 444 | /*! |
| 445 | When transpiling shaders using multiview (e.g. a vertex shader using |
| 446 | gl_ViewIndex for a renderer relying on GL_OVR_multiview2, VK_KHR_multiview, |
| 447 | etc.), for some of the targets it is necessary to declare the number of |
| 448 | views in the shader. This is not done in the Vulkan-style GLSL code, and is |
| 449 | not relevant for targets such as SPIR-V or HLSL, but is required for OpenGL |
| 450 | and GLSL, and so the value has to be provided as additional metadata. |
| 451 | |
| 452 | By default the value is 0, which disables injecting the \c{num_views} |
| 453 | statement. Setting 1 is not useful since that is the default \c{num_views} |
| 454 | regardless. Therefore \a count should be >= 2 to make an effect. When set |
| 455 | to, for example, 2, the generated GLSL shader will contain a |
| 456 | \c{layout(num_views = 2) in;} statement. |
| 457 | |
| 458 | Setting a \a count of 2 or greater also injects some preprocessor |
| 459 | statements: |
| 460 | \c{QSHADER_VIEW_COUNT} is set to \a count, whereas the |
| 461 | \c GL_EXT_multiview extension is enabled automatically. Therefore, setting |
| 462 | the appropriate |
| 463 | \a count can be relevant with other types of shaders as well, e.g. when |
| 464 | sharing a uniform buffer between the vertex and fragment shader and both |
| 465 | shaders have to be able to write something like |
| 466 | \c{#if QSHADER_VIEW_COUNT >= 2}. |
| 467 | |
| 468 | \since 6.7 |
| 469 | */ |
| 470 | void QShaderBaker::setMultiViewCount(int count) |
| 471 | { |
| 472 | d->multiViewInfo.viewCount = count >= 2 ? count : 0; |
| 473 | } |
| 474 | |
| 475 | void QShaderBaker::setSpirvOptions(SpirvOptions options) |
| 476 | { |
| 477 | d->spirvOptions = options; |
| 478 | } |
| 479 | |
| 480 | inline size_t qHash(const QShaderBaker::GeneratedShader &k, size_t seed = 0) |
| 481 | { |
| 482 | return qHash(e: k.first, seed) ^ k.second.version(); |
| 483 | } |
| 484 | |
| 485 | QPair<QByteArray, QByteArray> QShaderBakerPrivate::compile() |
| 486 | { |
| 487 | QSpirvCompiler::Flags flags; |
| 488 | if (spirvOptions.testFlag(flag: QShaderBaker::SpirvOption::GenerateFullDebugInfo)) |
| 489 | flags |= QSpirvCompiler::FullDebugInfo; |
| 490 | |
| 491 | compiler.setFlags(flags); |
| 492 | const QByteArray spirvBin = compiler.compileToSpirv(); |
| 493 | if (spirvBin.isEmpty()) { |
| 494 | errorMessage = compiler.errorMessage(); |
| 495 | return {}; |
| 496 | } |
| 497 | QByteArray batchableSpirvBin; |
| 498 | if (stage == QShader::VertexStage && variants.contains(t: QShader::BatchableVertexShader)) { |
| 499 | compiler.setFlags(flags | QSpirvCompiler::RewriteToMakeBatchableForSG); |
| 500 | compiler.setSGBatchingVertexInputLocation(batchLoc); |
| 501 | batchableSpirvBin = compiler.compileToSpirv(); |
| 502 | if (batchableSpirvBin.isEmpty()) { |
| 503 | errorMessage = compiler.errorMessage(); |
| 504 | return {}; |
| 505 | } |
| 506 | } |
| 507 | return { spirvBin, batchableSpirvBin }; |
| 508 | } |
| 509 | |
| 510 | QByteArray QShaderBakerPrivate::perTargetDefines(const QShaderBaker::GeneratedShader &key) |
| 511 | { |
| 512 | QByteArray preamble; |
| 513 | switch (key.first) { |
| 514 | case QShader::SpirvShader: |
| 515 | preamble += QByteArrayLiteral("\n#define QSHADER_SPIRV 1\n#define QSHADER_SPIRV_VERSION " ); |
| 516 | preamble += QByteArray::number(key.second.version()); |
| 517 | preamble += QByteArrayLiteral("\n" ); |
| 518 | break; |
| 519 | case QShader::GlslShader: |
| 520 | preamble += QByteArrayLiteral("\n#define QSHADER_GLSL 1\n#define QSHADER_GLSL_VERSION " ); |
| 521 | preamble += QByteArray::number(key.second.version()); |
| 522 | if (key.second.flags().testFlag(flag: QShaderVersion::GlslEs)) |
| 523 | preamble += QByteArrayLiteral("\n#define QSHADER_GLSL_ES 1" ); |
| 524 | preamble += QByteArrayLiteral("\n" ); |
| 525 | break; |
| 526 | case QShader::HlslShader: |
| 527 | preamble += QByteArrayLiteral("\n#define QSHADER_HLSL 1\n#define QSHADER_HLSL_VERSION " ); |
| 528 | preamble += QByteArray::number(key.second.version()); |
| 529 | preamble += QByteArrayLiteral("\n" ); |
| 530 | break; |
| 531 | case QShader::MslShader: |
| 532 | preamble += QByteArrayLiteral("\n#define QSHADER_MSL 1\n#define QSHADER_MSL_VERSION " ); |
| 533 | preamble += QByteArray::number(key.second.version()); |
| 534 | preamble += QByteArrayLiteral("\n" ); |
| 535 | break; |
| 536 | default: |
| 537 | Q_UNREACHABLE(); |
| 538 | } |
| 539 | return preamble; |
| 540 | } |
| 541 | |
| 542 | /*! |
| 543 | Runs the compilation and translation process. |
| 544 | |
| 545 | \return a QShader instance. To check if the process was successful, |
| 546 | call QShader::isValid(). When that indicates \c false, call |
| 547 | errorMessage() to retrieve the log. |
| 548 | |
| 549 | This is an expensive operation. When calling this from applications, it can |
| 550 | be advisable to do it on a separate thread. |
| 551 | |
| 552 | \note QShaderBaker instances are reusable: after calling bake(), the same |
| 553 | instance can be used with different inputs again. However, a QShaderBaker |
| 554 | instance should only be used on one single thread during its lifetime. |
| 555 | */ |
| 556 | QShader QShaderBaker::bake() |
| 557 | { |
| 558 | d->errorMessage.clear(); |
| 559 | |
| 560 | if (d->source.isEmpty()) { |
| 561 | d->errorMessage = QLatin1String("QShaderBaker: No source specified" ); |
| 562 | return QShader(); |
| 563 | } |
| 564 | |
| 565 | d->compiler.setSourceString(sourceString: d->source, stage: d->stage, fileName: d->sourceFileName); |
| 566 | |
| 567 | // Normally one entry, for QShader::SpirvShader only. However, in |
| 568 | // compile-per-target mode there is a separate SPIR-V binary generated for |
| 569 | // each target (so for each GLSL/HLSL/MSL version requested). |
| 570 | QHash<GeneratedShader, QByteArray> spirv; |
| 571 | QHash<GeneratedShader, QByteArray> batchableSpirv; |
| 572 | const auto compileSpirvAndBatchable = [this, &spirv, &batchableSpirv](const GeneratedShader &key) { |
| 573 | const QPair<QByteArray, QByteArray> bin = d->compile(); |
| 574 | if (bin.first.isEmpty()) |
| 575 | return false; |
| 576 | spirv.insert(key, value: bin.first); |
| 577 | if (!bin.second.isEmpty()) |
| 578 | batchableSpirv.insert(key, value: bin.second); |
| 579 | return true; |
| 580 | }; |
| 581 | |
| 582 | QByteArray preamble = d->preamble; |
| 583 | if (d->multiViewInfo.viewCount >= 2) { |
| 584 | if (d->stage == QShader::VertexStage) |
| 585 | preamble += QByteArrayLiteral("\n#extension GL_EXT_multiview : require\n" ); |
| 586 | preamble += QByteArrayLiteral("\n#define QSHADER_VIEW_COUNT " ); |
| 587 | preamble += QByteArray::number(d->multiViewInfo.viewCount); |
| 588 | preamble += QByteArrayLiteral("\n" ); |
| 589 | } |
| 590 | |
| 591 | if (!d->perTargetEnabled) { |
| 592 | d->compiler.setPreamble(preamble); |
| 593 | if (!compileSpirvAndBatchable({ QShader::SpirvShader, {} })) |
| 594 | return QShader(); |
| 595 | } else { |
| 596 | // per-target compilation. the value here comes from the varying |
| 597 | // preamble (and so preprocessor defines) |
| 598 | for (GeneratedShader req: d->reqVersions) { |
| 599 | d->compiler.setPreamble(preamble + d->perTargetDefines(key: req)); |
| 600 | if (!compileSpirvAndBatchable(req)) |
| 601 | return QShader(); |
| 602 | } |
| 603 | } |
| 604 | |
| 605 | // Now spirv, and, if in use, batchableSpirv, contain at least one, |
| 606 | // optionally more SPIR-V binaries. |
| 607 | Q_ASSERT(!spirv.isEmpty() && (d->perTargetEnabled || spirv.size() == 1)); |
| 608 | |
| 609 | QShader bs; |
| 610 | bs.setStage(d->stage); |
| 611 | |
| 612 | QSpirvShader spirvShader; |
| 613 | QSpirvShader batchableSpirvShader; |
| 614 | // The QShaderDescription can be different for variants (we just have a |
| 615 | // hardcoded rule to pick one), but cannot differ for targets (in |
| 616 | // per-target mode, hence we can just pick the first SPIR-V binary and |
| 617 | // generate the reflection data based on that) |
| 618 | spirvShader.setSpirvBinary(spirv: spirv.constKeyValueBegin()->second, stage: d->stage); |
| 619 | if (batchableSpirv.isEmpty()) { |
| 620 | bs.setDescription(spirvShader.shaderDescription()); |
| 621 | } else { |
| 622 | batchableSpirvShader.setSpirvBinary(spirv: batchableSpirv.constKeyValueBegin()->second, stage: d->stage); |
| 623 | // prefer the batchable's reflection info with _qt_order and such present |
| 624 | bs.setDescription(batchableSpirvShader.shaderDescription()); |
| 625 | } |
| 626 | |
| 627 | for (const GeneratedShader &req: d->reqVersions) { |
| 628 | for (const QShader::Variant &v : d->variants) { |
| 629 | if (d->stage != QShader::VertexStage) { |
| 630 | if (v == QShader::BatchableVertexShader |
| 631 | || v == QShader::UInt32IndexedVertexAsComputeShader |
| 632 | || v == QShader::UInt16IndexedVertexAsComputeShader |
| 633 | || v == QShader::NonIndexedVertexAsComputeShader) |
| 634 | { |
| 635 | continue; |
| 636 | } |
| 637 | } |
| 638 | if (req.first != QShader::MslShader && req.first != QShader::MetalLibShader) { |
| 639 | if (v == QShader::UInt32IndexedVertexAsComputeShader |
| 640 | || v == QShader::UInt16IndexedVertexAsComputeShader |
| 641 | || v == QShader::NonIndexedVertexAsComputeShader) |
| 642 | { |
| 643 | continue; |
| 644 | } |
| 645 | } |
| 646 | |
| 647 | QSpirvShader *currentSpirvShader = nullptr; |
| 648 | if (d->perTargetEnabled) { |
| 649 | // This is expensive too, in addition to the multiple |
| 650 | // compilation rounds, but opting in to per-target mode is a |
| 651 | // careful, conscious choice (hopefully), so it's fine. |
| 652 | if (v == QShader::BatchableVertexShader) |
| 653 | batchableSpirvShader.setSpirvBinary(spirv: batchableSpirv[req], stage: d->stage); |
| 654 | else |
| 655 | spirvShader.setSpirvBinary(spirv: spirv[req], stage: d->stage); |
| 656 | } |
| 657 | if (v == QShader::BatchableVertexShader) |
| 658 | currentSpirvShader = &batchableSpirvShader; |
| 659 | else |
| 660 | currentSpirvShader = &spirvShader; |
| 661 | Q_ASSERT(currentSpirvShader); |
| 662 | Q_ASSERT(!currentSpirvShader->spirvBinary().isEmpty()); |
| 663 | const QShaderKey key(req.first, req.second, v); |
| 664 | QShaderCode shader; |
| 665 | shader.setEntryPoint(QByteArrayLiteral("main" )); |
| 666 | switch (req.first) { |
| 667 | case QShader::SpirvShader: |
| 668 | if (d->spirvOptions.testFlag(flag: QShaderBaker::SpirvOption::StripDebugAndVarInfo)) { |
| 669 | QString errorMsg; |
| 670 | const QByteArray strippedSpirv = currentSpirvShader->remappedSpirvBinary(flags: QSpirvShader::RemapFlag::StripOnly, errorMessage: &errorMsg); |
| 671 | if (strippedSpirv.isEmpty()) { |
| 672 | d->errorMessage = errorMsg; |
| 673 | return QShader(); |
| 674 | } |
| 675 | shader.setShader(strippedSpirv); |
| 676 | } else { |
| 677 | shader.setShader(currentSpirvShader->spirvBinary()); |
| 678 | } |
| 679 | break; |
| 680 | case QShader::GlslShader: |
| 681 | { |
| 682 | QSpirvShader::GlslFlags flags; |
| 683 | if (req.second.flags().testFlag(flag: QShaderVersion::GlslEs)) |
| 684 | flags |= QSpirvShader::GlslFlag::GlslEs; |
| 685 | QVector<QSpirvShader::SeparateToCombinedImageSamplerMapping> separateToCombinedImageSamplerMappings; |
| 686 | shader.setShader(currentSpirvShader->translateToGLSL(version: req.second.version(), |
| 687 | flags, |
| 688 | stage: d->stage, |
| 689 | multiViewInfo: d->multiViewInfo, |
| 690 | separateToCombinedImageSamplerMappings: &separateToCombinedImageSamplerMappings)); |
| 691 | if (shader.shader().isEmpty()) { |
| 692 | if (d->breakOnShaderTranslationError) { |
| 693 | d->errorMessage = currentSpirvShader->translationErrorMessage(); |
| 694 | return QShader(); |
| 695 | } else { |
| 696 | d->errorMessage += QLatin1String(" " ) + currentSpirvShader->translationErrorMessage(); |
| 697 | continue; |
| 698 | } |
| 699 | } |
| 700 | if (!separateToCombinedImageSamplerMappings.isEmpty()) { |
| 701 | const QShaderDescription desc = bs.description(); |
| 702 | QVector<QShaderDescription::InOutVariable> separateImages = desc.separateImages(); |
| 703 | QVector<QShaderDescription::InOutVariable> separateSamplers = desc.separateSamplers(); |
| 704 | QShader::SeparateToCombinedImageSamplerMappingList result; |
| 705 | for (const QSpirvShader::SeparateToCombinedImageSamplerMapping &mapping : separateToCombinedImageSamplerMappings) { |
| 706 | int textureBinding = -1; |
| 707 | int samplerBinding = -1; |
| 708 | for (int i = 0, count = separateImages.size(); i < count; ++i) { |
| 709 | if (separateImages[i].name == mapping.textureName) { |
| 710 | textureBinding = separateImages[i].binding; |
| 711 | break; |
| 712 | } |
| 713 | } |
| 714 | for (int i = 0, count = separateSamplers.size(); i < count; ++i) { |
| 715 | if (separateSamplers[i].name == mapping.samplerName) { |
| 716 | samplerBinding = separateSamplers[i].binding; |
| 717 | break; |
| 718 | } |
| 719 | } |
| 720 | result.append(t: { .combinedSamplerName: mapping.combinedSamplerName, .textureBinding: textureBinding, .samplerBinding: samplerBinding }); |
| 721 | } |
| 722 | bs.setSeparateToCombinedImageSamplerMappingList(key, list: result); |
| 723 | } |
| 724 | } |
| 725 | break; |
| 726 | case QShader::HlslShader: |
| 727 | { |
| 728 | QShader::NativeResourceBindingMap nativeBindings; |
| 729 | shader.setShader(currentSpirvShader->translateToHLSL(version: req.second.version(), nativeBindings: &nativeBindings)); |
| 730 | if (shader.shader().isEmpty()) { |
| 731 | if (d->breakOnShaderTranslationError) { |
| 732 | d->errorMessage = currentSpirvShader->translationErrorMessage(); |
| 733 | return QShader(); |
| 734 | } else { |
| 735 | d->errorMessage += QLatin1String(" " ) + currentSpirvShader->translationErrorMessage(); |
| 736 | continue; |
| 737 | } |
| 738 | } |
| 739 | bs.setResourceBindingMap(key, map: nativeBindings); |
| 740 | } |
| 741 | break; |
| 742 | case QShader::MslShader: |
| 743 | { |
| 744 | QShader::NativeResourceBindingMap nativeBindings; |
| 745 | QShader::NativeShaderInfo shaderInfo; |
| 746 | QSpirvShader::MslFlags flags; |
| 747 | if (d->stage == QShader::VertexStage) { |
| 748 | switch (v) { |
| 749 | case QShader::UInt16IndexedVertexAsComputeShader: |
| 750 | flags |= QSpirvShader::MslFlag::VertexAsCompute; |
| 751 | flags |= QSpirvShader::MslFlag::WithUInt16Index; |
| 752 | break; |
| 753 | case QShader::UInt32IndexedVertexAsComputeShader: |
| 754 | flags |= QSpirvShader::MslFlag::VertexAsCompute; |
| 755 | flags |= QSpirvShader::MslFlag::WithUInt32Index; |
| 756 | break; |
| 757 | case QShader::NonIndexedVertexAsComputeShader: |
| 758 | flags |= QSpirvShader::MslFlag::VertexAsCompute; |
| 759 | break; |
| 760 | default: |
| 761 | break; |
| 762 | } |
| 763 | } |
| 764 | shader.setShader(currentSpirvShader->translateToMSL(version: req.second.version(), |
| 765 | flags, |
| 766 | stage: d->stage, |
| 767 | nativeBindings: &nativeBindings, |
| 768 | shaderInfo: &shaderInfo, |
| 769 | multiViewInfo: d->multiViewInfo, |
| 770 | tessInfo: d->tessInfo)); |
| 771 | if (shader.shader().isEmpty()) { |
| 772 | if (d->breakOnShaderTranslationError) { |
| 773 | d->errorMessage = currentSpirvShader->translationErrorMessage(); |
| 774 | return QShader(); |
| 775 | } else { |
| 776 | d->errorMessage += QLatin1String(" " ) + currentSpirvShader->translationErrorMessage(); |
| 777 | continue; |
| 778 | } |
| 779 | } |
| 780 | shader.setEntryPoint(QByteArrayLiteral("main0" )); |
| 781 | bs.setResourceBindingMap(key, map: nativeBindings); |
| 782 | bs.setNativeShaderInfo(key, info: shaderInfo); |
| 783 | } |
| 784 | break; |
| 785 | default: |
| 786 | Q_UNREACHABLE(); |
| 787 | } |
| 788 | bs.setShader(key, shader); |
| 789 | } |
| 790 | } |
| 791 | |
| 792 | return bs; |
| 793 | } |
| 794 | |
| 795 | /*! |
| 796 | \return the error message from the last bake() run, or an empty string if |
| 797 | there was no error. |
| 798 | |
| 799 | \note Errors include file read errors, compilation, and translation |
| 800 | failures. Not requesting any targets or variants does not count as an error |
| 801 | even though the resulting QShader is invalid. |
| 802 | */ |
| 803 | QString QShaderBaker::errorMessage() const |
| 804 | { |
| 805 | return d->errorMessage; |
| 806 | } |
| 807 | |
| 808 | QT_END_NAMESPACE |
| 809 | |