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::setBatchableVertexShaderExtraInputLocation(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 |
Definitions
- QShaderBakerPrivate
- readFile
- QShaderBaker
- ~QShaderBaker
- setSourceFileName
- setSourceFileName
- setSourceDevice
- setSourceString
- setGeneratedShaders
- setGeneratedShaderVariants
- setPreamble
- setBatchableVertexShaderExtraInputLocation
- setPerTargetCompilation
- setBreakOnShaderTranslationError
- setTessellationMode
- setTessellationOutputVertexCount
- setMultiViewCount
- setSpirvOptions
- qHash
- compile
- perTargetDefines
- bake
Learn Advanced QML with KDAB
Find out more