Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qssgrendershadercache.cpp
Go to the documentation of this file.
1// Copyright (C) 2008-2012 NVIDIA Corporation.
2// Copyright (C) 2019 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
4// Qt-Security score:significant reason:default
5
6
7#include "graphobjects/qssgrendergraphobject_p.h"
10
11#include <QtQuick3DUtils/private/qssgutils_p.h>
12#include <QtQuick3DUtils/private/qquick3dprofiler_p.h>
13
14#include <QtQuick3DRuntimeRender/private/qssgruntimerenderlogging_p.h>
15#include <qtquick3d_tracepoints_p.h>
16
17#include <QCoreApplication>
18#include <QStandardPaths>
19#include <QString>
20#include <QFile>
21#include <QDir>
22
23#include <QtGui/qsurfaceformat.h>
24#if QT_CONFIG(opengl)
25# include <QtGui/qopenglcontext.h>
26#endif
27
28#ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS
29#include <rhi/qshaderbaker.h>
30#endif
31
32#include <QtCore/qmutex.h>
33
35
36Q_TRACE_POINT(qtquick3d, QSSG_loadShader_entry)
37Q_TRACE_POINT(qtquick3d, QSSG_loadShader_exit)
38
39static QtQuick3DEditorHelpers::ShaderBaker::StatusCallback s_statusCallback = nullptr;
41
42size_t qHash(QSSGShaderFeatures features) noexcept { return (features.flags & (~QSSGShaderFeatures::IndexMask)); }
43
44#ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS
45
46static QString dumpFilename(QShader::Stage stage)
47{
48 switch (stage) {
49 case QShader::VertexStage:
50 return QStringLiteral("failedvert.txt");
51 break;
52 case QShader::FragmentStage:
53 return QStringLiteral("failedfrag.txt");
54 break;
55 case QShader::ComputeStage:
56 return QStringLiteral("failedcomp.txt");
57 break;
58 default:
59 return QStringLiteral("failedshader.txt");
60 }
61}
62
63static void dumpShader(QShader::Stage stage, const QByteArray &code)
64{
65 switch (stage) {
66 case QShader::Stage::VertexStage:
67 qDebug("VERTEX SHADER:\n*****\n");
68 break;
69 case QShader::Stage::FragmentStage:
70 qDebug("FRAGMENT SHADER:\n*****\n");
71 break;
72 case QShader::Stage::ComputeStage:
73 qDebug("COMPUTE SHADER:\n*****\n");
74 break;
75 default:
76 qDebug("SHADER:\n*****\n");
77 break;
78 }
79 const auto lines = code.split('\n');
80 for (int i = 0; i < lines.size(); i++)
81 qDebug("%3d %s", i + 1, lines.at(i).constData());
82 qDebug("\n*****\n");
83}
84
85static void dumpShaderToFile(QShader::Stage stage, const QByteArray &data)
86{
87 QFile f(dumpFilename(stage));
88 if (f.open(QIODevice::WriteOnly | QIODevice::Text)) {
89 f.write(data);
90 } else {
91 qWarning("Failed to write to file: %s (%s)\n",
92 qPrintable(f.fileName()), qPrintable(f.errorString()));
93 }
94}
95#endif // QT_QUICK3D_HAS_RUNTIME_SHADERS
96
98{
99 const char *name = nullptr;
101};
102
103static constexpr DefineEntry DefineTable[] {
104 { "QSSG_ENABLE_LIGHT_PROBE", QSSGShaderFeatures::Feature::LightProbe },
105 { "QSSG_ENABLE_IBL_ORIENTATION", QSSGShaderFeatures::Feature::IblOrientation },
106 { "QSSG_ENABLE_SSM", QSSGShaderFeatures::Feature::Ssm },
107 { "QSSG_ENABLE_SSAO", QSSGShaderFeatures::Feature::Ssao },
108 { "QSSG_ENABLE_DEPTH_PASS", QSSGShaderFeatures::Feature::DepthPass },
109 { "QSSG_ENABLE_ORTHO_SHADOW_PASS", QSSGShaderFeatures::Feature::OrthoShadowPass },
110 { "QSSG_ENABLE_PERSPECTIVE_SHADOW_PASS", QSSGShaderFeatures::Feature::PerspectiveShadowPass },
111 { "QSSG_ENABLE_LINEAR_TONEMAPPING", QSSGShaderFeatures::Feature::LinearTonemapping },
112 { "QSSG_ENABLE_ACES_TONEMAPPING", QSSGShaderFeatures::Feature::AcesTonemapping },
113 { "QSSG_ENABLE_HEJLDAWSON_TONEMAPPING", QSSGShaderFeatures::Feature::HejlDawsonTonemapping },
114 { "QSSG_ENABLE_FILMIC_TONEMAPPING", QSSGShaderFeatures::Feature::FilmicTonemapping },
115 { "QSSG_ENABLE_RGBE_LIGHT_PROBE", QSSGShaderFeatures::Feature::RGBELightProbe },
116 { "QSSG_ENABLE_OPAQUE_DEPTH_PRE_PASS", QSSGShaderFeatures::Feature::OpaqueDepthPrePass },
117 { "QSSG_ENABLE_REFLECTION_PROBE", QSSGShaderFeatures::Feature::ReflectionProbe },
118 { "QSSG_REDUCE_MAX_NUM_LIGHTS", QSSGShaderFeatures::Feature::ReduceMaxNumLights },
119 { "QSSG_ENABLE_LIGHTMAP", QSSGShaderFeatures::Feature::Lightmap },
120 { "QSSG_DISABLE_MULTIVIEW", QSSGShaderFeatures::Feature::DisableMultiView },
121 { "QSSG_FORCE_IBL_EXPOSURE", QSSGShaderFeatures::Feature::ForceIblExposure },
122 { "QSSG_ENABLE_NORMAL_PASS", QSSGShaderFeatures::Feature::NormalPass },
123 { "QSSG_ENABLE_USER_RENDER_PASS", QSSGShaderFeatures::Feature::UserRenderPass},
124 { "QSSG_ENABLE_MOTION_VECTOR", QSSGShaderFeatures::Feature::MotionVector }
125};
126
127static_assert(std::size(DefineTable) == QSSGShaderFeatures::Count, "Missing feature define?");
128
129const char *QSSGShaderFeatures::asDefineString(QSSGShaderFeatures::Feature feature) { return DefineTable[static_cast<FlagType>(feature) & QSSGShaderFeatures::IndexMask].name; }
130QSSGShaderFeatures::Feature QSSGShaderFeatures::fromIndex(quint32 idx) { return DefineTable[idx].feature; }
131
132void QSSGShaderFeatures::set(QSSGShaderFeatures::Feature feature, bool val)
133{
134 if (val)
135 flags |= (static_cast<FlagType>(feature) & ~IndexMask);
136 else
137 flags &= ~(static_cast<FlagType>(feature) & ~IndexMask);
138}
139
140QList<QShaderVersion> QSSGShaderCache::glslTargetsForContext(const QSurfaceFormat &format, bool isGLESModule)
141{
142 QList<QShaderVersion> versions;
143
144 // QShaderVersion from QSurfaceFormat::version pair. Only valid from GLSL
145 // 330 and GLSL ES 300 up; below those the GLSL version does not follow the
146 // context version, but the callers below never get there.
147 const auto fromVersion = [](const std::pair<int, int> &version, const QShaderVersion::Flags flags = {}) -> QShaderVersion {
148 return QShaderVersion(version.first * 100 + version.second * 10, flags);
149 };
150
151 const auto version = format.version();
152
153 if (format.profile() == QSurfaceFormat::CoreProfile && version >= qMakePair(3, 3)) {
154 // Add the floor version so that silent translation errors do not leave the
155 // GLSL outputs empty
156 versions.append(QShaderVersion(330));
157 // Use actual version
158 if (version > qMakePair(3, 3))
159 versions.append(fromVersion(version));
160 } else if (format.renderableType() == QSurfaceFormat::OpenGLES || isGLESModule) {
161 if (format.majorVersion() >= 3) {
162 // Add the floor version so that silent translation errors do not leave the
163 // GLSL outputs empty
164 versions.append(QShaderVersion(300, QShaderVersion::GlslEs));
165 // Use actual version
166 if (version > qMakePair(3, 0))
167 versions.append(fromVersion(version, QShaderVersion::GlslEs));
168 } else {
169 versions.append(QShaderVersion(100, QShaderVersion::GlslEs)); // GLES 2.0
170 }
171 } else {
172 // Need to default to at least GLSL 130 (OpenGL 3.0), not 120.
173 // The difference is actually relevant when it comes to certain
174 // GLSL features (textureSize, unsigned integers, and with
175 // SPIRV-Cross even bool), and we do not have to care about
176 // pure OpenGL (non-ES) 2.x implementations in practice.
177
178 // For full feature set we need GLSL 140 (OpenGL 3.1), e.g.
179 // because of inverse() used for instancing.
180
181 // GLSL 130 should still be attempted, to support old Mesa
182 // llvmpipe that only gives us OpenGL 3.0. At the time of
183 // writing the opengl32sw.dll shipped with pre-built Qt is one
184 // of these still.
185
186 versions.append(QShaderVersion(version >= qMakePair(3, 1) ? 140 : 130));
187 if (version >= qMakePair(3, 3))
188 versions.append(fromVersion(version));
189 }
190
191 return versions;
192}
193
194#ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS
195static void initBakerForNonPersistentUse(QShaderBaker *baker, QRhi *rhi)
196{
197 QVector<QShaderBaker::GeneratedShader> outputs;
198 switch (rhi->backend()) {
199 case QRhi::D3D11:
200 outputs.append({ QShader::HlslShader, QShaderVersion(50) }); // Shader Model 5.0
201 break;
202 case QRhi::D3D12:
203 outputs.append({ QShader::HlslShader, QShaderVersion(61) }); // Shader Model 6.1 (includes multiview support)
204 break;
205 case QRhi::Metal:
206#if defined(Q_OS_VISIONOS)
207 outputs.append({ QShader::MslShader, QShaderVersion(21) }); // Metal 2.1 (multiview support)
208#else
209 outputs.append({ QShader::MslShader, QShaderVersion(12) }); // Metal 1.2
210#endif // Q_OS_VISIONOS
211 break;
212 case QRhi::OpenGLES2:
213 {
214 QSurfaceFormat format = QSurfaceFormat::defaultFormat();
215 bool isGLESModule = false;
216#if QT_CONFIG(opengl)
217 auto h = static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles());
218 if (h && h->context)
219 format = h->context->format();
220 isGLESModule = QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES;
221#endif
222 const QList<QShaderVersion> versions = QSSGShaderCache::glslTargetsForContext(format, isGLESModule);
223 outputs.reserve(outputs.size() + versions.size());
224 for (const QShaderVersion &version : versions)
225 outputs.append({ QShader::GlslShader, version });
226 }
227 break;
228 default: // Vulkan, Null
229 outputs.append({ QShader::SpirvShader, QShaderVersion(100) });
230 break;
231 }
232
233 // Do not fail baking if one of the target fails
234 // See comment on initBakerForPersistentUse
235 baker->setBreakOnShaderTranslationError(false);
236 baker->setGeneratedShaders(outputs);
237 baker->setGeneratedShaderVariants({ QShader::StandardShader });
238}
239
240void QSSGShaderCache::initBakerForPersistentUse(QShaderBaker *baker, QRhi *)
241{
242 QVector<QShaderBaker::GeneratedShader> outputs;
243 outputs.reserve(13);
244
245#ifndef Q_OS_WASM
246 outputs.append({ QShader::SpirvShader, QShaderVersion(100) });
247 outputs.append({ QShader::HlslShader, QShaderVersion(50) }); // Shader Model 5.0
248 outputs.append({ QShader::HlslShader, QShaderVersion(61) }); // Shader Model 6.1 (for multiview on d3d12)
249#if defined(Q_OS_VISIONOS)
250 outputs.append({ QShader::MslShader, QShaderVersion(21) }); // Metal 2.1 (multiview support)
251#else
252 outputs.append({ QShader::MslShader, QShaderVersion(12) }); // Metal 1.2
253#endif // Q_OS_VISIONOS
254
255 outputs.append({ QShader::GlslShader, QShaderVersion(430) }); // OpenGL 4.3+ (compute shaders, shader storage buffers)
256 outputs.append({ QShader::GlslShader, QShaderVersion(420) }); // OpenGL 4.2+
257 outputs.append({ QShader::GlslShader, QShaderVersion(330) }); // OpenGL 3.3+
258 outputs.append({ QShader::GlslShader, QShaderVersion(140) }); // OpenGL 3.1+
259 outputs.append({ QShader::GlslShader, QShaderVersion(130) }); // OpenGL 3.0+
260 outputs.append({ QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs) }); // GLES 2.0
261#endif
262 outputs.append({ QShader::GlslShader, QShaderVersion(300, QShaderVersion::GlslEs) }); // GLES 3.0+
263 outputs.append({ QShader::GlslShader, QShaderVersion(310, QShaderVersion::GlslEs) }); // GLES 3.1+
264 outputs.append({ QShader::GlslShader, QShaderVersion(320, QShaderVersion::GlslEs) }); // GLES 3.2+ (image atomics)
265
266 // If one of the above cannot be generated due to failing at the
267 // SPIRV-Cross translation stage, it will be skipped, but bake() will not
268 // fail. This is essential, because with the default fail if anything fails
269 // behavior many shaders could not be baked at all due to failing for e.g.
270 // GLSL ES 100. This is a non-issue when choosing the targets dynamically
271 // based on the current API/context, but here we need to ensure what we
272 // generate will work with a different RHI backend, graphics API, and
273 // perhaps even on a different platform (if the cache file is manually
274 // moved). So have to generate what we can, without breaking the
275 // application when the shader is not compatible with a target. (if that
276 // shader is not used at runtime, it's fine anyway, it it is, it won't work
277 // just as with the other, non-caching path)
278 baker->setBreakOnShaderTranslationError(false);
279
280 baker->setGeneratedShaders(outputs);
281 baker->setGeneratedShaderVariants({ QShader::StandardShader });
282}
283
284#else
285static void initBakerForNonPersistentUse(QShaderBaker *, QRhi *)
286{
287}
288
289void QSSGShaderCache::initBakerForPersistentUse(QShaderBaker *, QRhi *)
290{
291}
292#endif // QT_QUICK3D_HAS_RUNTIME_SHADERS
293
294static bool s_autoDiskCacheEnabled = true;
295
297{
298 // these three mirror QOpenGLShaderProgram/QQuickGraphicsConfiguration/QSGRhiSupport
299 static const bool diskCacheDisabled = qEnvironmentVariableIntValue("QT_DISABLE_SHADER_DISK_CACHE")
300 || qEnvironmentVariableIntValue("QSG_RHI_DISABLE_DISK_CACHE");
301 const bool attrDiskCacheDisabled = (qApp ? qApp->testAttribute(Qt::AA_DisableShaderDiskCache) : false);
302 return (!diskCacheDisabled && !attrDiskCacheDisabled && s_autoDiskCacheEnabled);
303
304}
305
306static inline bool ensureWritableDir(const QString &name)
307{
308 QDir::root().mkpath(name);
309 return QFileInfo(name).isWritable();
310}
311
313{
314 static bool checked = false;
315 static QString currentCacheDir;
316 static bool cacheWritable = false;
317
318 if (checked)
319 return cacheWritable ? currentCacheDir : QString();
320
321 checked = true;
322 const QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
323 const QString subPath = QLatin1String("/q3dshadercache-") + QSysInfo::buildAbi() + QLatin1Char('/');
324
325 if (!cachePath.isEmpty()) {
326 currentCacheDir = cachePath + subPath;
327 cacheWritable = ensureWritableDir(currentCacheDir);
328 }
329
330 return cacheWritable ? currentCacheDir : QString();
331}
332
334{
335 const QString cacheDir = persistentQsbcDir();
336 if (!cacheDir.isEmpty())
337 return cacheDir + QLatin1String("q3dshadercache.qsbc");
338
339 return QString();
340}
341
342QSSGShaderCache::QSSGShaderCache(QSSGRhiContext &ctx,
343 const InitBakerFunc initBakeFn)
344 : m_rhiContext(ctx),
345 m_initBaker(initBakeFn),
346 m_builtInShaders(*this)
347{
348 if (isAutoDiskCacheEnabled()) {
349 const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled();
350 m_persistentShaderStorageFileName = persistentQsbcFileName();
351 if (!m_persistentShaderStorageFileName.isEmpty()) {
352 const bool skipCacheFile = qEnvironmentVariableIntValue("QT_QUICK3D_NO_SHADER_CACHE_LOAD");
353 if (!skipCacheFile && QFileInfo::exists(m_persistentShaderStorageFileName)) {
354 if (shaderDebug)
355 qDebug("Attempting to seed material shader cache from %s", qPrintable(m_persistentShaderStorageFileName));
356 if (m_persistentShaderBakingCache.load(m_persistentShaderStorageFileName)) {
357 if (shaderDebug) {
358 const int count = m_persistentShaderBakingCache.availableEntries().count();
359 qDebug("Loaded %d shader pipelines into the material shader cache", count);
360 }
361 }
362 }
363 }
364 }
365
366 if (!m_initBaker) {
367 // It is important to generate all possible shader variants if the qsb
368 // collection is going to be stored on disk. Otherwise switching the
369 // rhi backend could break the application. This is however an overkill
370 // if we know that what we bake will not be reused in future runs of
371 // the application, so do not do it if the disk cache was disabled or
372 // the cache directory was not available (no file system, no
373 // permissions, etc.).
374 m_initBaker = m_persistentShaderStorageFileName.isEmpty() ? initBakerForNonPersistentUse
375 : QSSGShaderCache::initBakerForPersistentUse;
376 }
377}
378
379QSSGShaderCache::~QSSGShaderCache()
380{
381 if (!m_persistentShaderStorageFileName.isEmpty())
382 m_persistentShaderBakingCache.save(m_persistentShaderStorageFileName);
383}
384
385void QSSGShaderCache::releaseCachedResources()
386{
387 m_builtInShaders.releaseCachedResources();
388
389 m_rhiShaders.clear();
390
391 // m_persistentShaderBakingCache is not cleared, that is intentional,
392 // otherwise we would permanently lose what got loaded at startup.
393}
394
395QSSGRhiShaderPipelinePtr QSSGShaderCache::tryGetRhiShaderPipeline(const QByteArray &inKey,
396 const QSSGShaderFeatures &inFeatures)
397{
398 QSSGShaderCacheKey cacheKey(inKey);
399 cacheKey.m_features = inFeatures;
400 cacheKey.updateHashCode();
401 const auto theIter = m_rhiShaders.constFind(cacheKey);
402 if (theIter != m_rhiShaders.cend())
403 return theIter.value();
404 return nullptr;
405}
406
407
408void QSSGShaderCache::addShaderPreprocessor(QByteArray &str,
409 const QByteArray &inKey,
410 ShaderType shaderType,
411 const QSSGShaderFeatures &inFeatures,
412 const QSSGUserShaderAugmentation &shaderAugmentation,
413 int viewCount)
414{
415 m_insertStr.clear();
416
417 m_insertStr += "#version 440\n";
418
419 if (!inKey.isNull()) {
420 m_insertStr += "//Shader name -";
421 m_insertStr += inKey;
422 m_insertStr += "\n";
423 }
424
425 m_insertStr += "#define texture2D texture\n";
426
427 // match Qt Quick and QSGMaterial(Shader)
428 m_insertStr += "#define QSHADER_VIEW_COUNT ";
429 m_insertStr += QByteArray::number(viewCount);
430 m_insertStr += "\n";
431
432 str.insert(0, m_insertStr);
433 QString::size_type insertPos = int(m_insertStr.size());
434
435 m_insertStr.clear();
436 const bool fragOutputEnabled = (!inFeatures.isSet(QSSGShaderFeatures::Feature::DepthPass)) && shaderType == ShaderType::Fragment;
437 for (const auto &def : DefineTable) {
438 m_insertStr.append("#define ");
439 m_insertStr.append(def.name);
440 m_insertStr.append(" ");
441 m_insertStr.append(inFeatures.isSet(def.feature) ? "1" : "0");
442 m_insertStr.append("\n");
443 }
444
445 str.insert(insertPos, m_insertStr);
446 insertPos += int(m_insertStr.size());
447
448 m_insertStr.clear();
449 if (fragOutputEnabled) {
450 if (shaderAugmentation.outputs.size() != 0) {
451 const auto &outputs = shaderAugmentation.outputs;
452 for (int i = 0; i < outputs.size(); ++i) {
453 m_insertStr += "layout(location = ";
454 m_insertStr += QByteArray::number(i);
455 m_insertStr += ") out vec4 ";
456 m_insertStr += outputs[i];
457 m_insertStr += ";\n";
458 }
459 } else {
460 m_insertStr += "layout(location = 0) out vec4 fragOutput;\n";
461 }
462 }
463
464 str.insert(insertPos, m_insertStr);
465}
466
467QByteArray QSSGShaderCache::resourceFolder()
468{
469 return QByteArrayLiteral(":/res/rhishaders/");
470}
471
472QByteArray QSSGShaderCache::shaderCollectionFile()
473{
474 return QByteArrayLiteral("qtappshaders.qsbc");
475}
476
477QByteArray QSSGShaderCache::particleShaderCollectionFile()
478{
479 return QByteArrayLiteral("qtparticleshaders.qsbc");
480}
481
482QSSGRhiShaderPipelinePtr QSSGShaderCache::compileGraphicsPipeline(
483 const QByteArray &inKey, const QByteArray &inVert, const QByteArray &inFrag,
484 const QSSGShaderFeatures &inFeatures, QSSGRhiShaderPipeline::StageFlags stageFlags,
485 const QSSGUserShaderAugmentation &shaderAugmentation,
486 int viewCount, bool perTargetCompilation, const QByteArray &inCacheKey)
487{
488#ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS
489 // inKey is also used as the shader name in a comment in the generated
490 // source, so it may not contain everything needed to identify the
491 // shaders. When inCacheKey is set, that is what the caches are keyed on.
492 const QByteArray &cacheKeyString = inCacheKey.isEmpty() ? inKey : inCacheKey;
493
494 const QSSGRhiShaderPipelinePtr &rhiShaders = tryGetRhiShaderPipeline(cacheKeyString, inFeatures);
495 if (rhiShaders) {
496 QSSG_ASSERT(rhiShaders->vertexStage() && rhiShaders->fragmentStage(), return {});
497 return rhiShaders;
498 }
499
500 QSSGShaderCacheKey tempKey(cacheKeyString);
501 tempKey.m_features = inFeatures;
502 tempKey.updateHashCode();
503
504 QByteArray vertexCode = inVert;
505 QByteArray fragmentCode = inFrag;
506
507 if (!vertexCode.isEmpty())
508 addShaderPreprocessor(vertexCode, inKey, ShaderType::Vertex, inFeatures, shaderAugmentation, viewCount);
509
510 if (!fragmentCode.isEmpty())
511 addShaderPreprocessor(fragmentCode, inKey, ShaderType::Fragment, inFeatures, shaderAugmentation, viewCount);
512
513 // lo and behold the final shader strings are ready
514
515 QSSGRhiShaderPipelinePtr shaders;
516 QString vertErr, fragErr;
517
518 QShaderBaker baker;
519 m_initBaker(&baker, m_rhiContext.rhi());
520
521 // If requested, per-target compilation allows doing things like #if
522 // QSHADER_HLSL in the shader code, at the expense of spending more time in
523 // bake())
524 baker.setPerTargetCompilation(perTargetCompilation);
525
526 // This is in the shader key, but cannot query that here anymore now that it's serialized.
527 // So we get it as a dedicated argument.
528 baker.setMultiViewCount(viewCount);
529
530 // For fragment shaders for GLSL ES (but only ES) we can make the generated
531 // sources contain 'precision mediump float' instead of 'precision highp float'.
532 const bool mediumPrecision = qEnvironmentVariableIntValue("QT_QUICK3D_MEDIUM_PRECISION");
533 if (mediumPrecision)
534 baker.setGlslOptions(QShaderBaker::GlslOption::GlslEsFragDefaultFloatPrecisionMedium);
535
536 const bool editorMode = QSSGRhiContextPrivate::editorMode();
537 // Shader debug is disabled in editor mode
538 const bool shaderDebug = !editorMode && QSSGRhiContextPrivate::shaderDebuggingEnabled();
539
540 baker.setSourceString(vertexCode, QShader::VertexStage);
541 QShader vertexShader = baker.bake();
542 const auto vertShaderValid = vertexShader.isValid();
543 if (!vertShaderValid) {
544 vertErr = baker.errorMessage();
545 if (!editorMode) {
546 qWarning("Failed to compile vertex shader: %s\n", qPrintable(vertErr));
547 if (!shaderDebug)
548 qWarning() << inKey << '\n';
549 }
550 }
551
552 if (shaderDebug) {
553 dumpShader(QShader::Stage::VertexStage, vertexCode);
554 if (!vertShaderValid)
555 dumpShaderToFile(QShader::Stage::VertexStage, vertexCode);
556 }
557
558 baker.setSourceString(fragmentCode, QShader::FragmentStage);
559 QShader fragmentShader = baker.bake();
560 const bool fragShaderValid = fragmentShader.isValid();
561 if (!fragShaderValid) {
562 fragErr = baker.errorMessage();
563 if (!editorMode) {
564 qWarning("Failed to compile fragment shader: %s\n", qPrintable(fragErr));
565 if (!shaderDebug)
566 qWarning() << inKey << '\n';
567 }
568 }
569
570 if (shaderDebug) {
571 dumpShader(QShader::Stage::FragmentStage, fragmentCode);
572 if (!fragShaderValid)
573 dumpShaderToFile(QShader::Stage::FragmentStage, fragmentCode);
574 }
575
576 if (vertShaderValid && fragShaderValid) {
577 shaders = std::make_shared<QSSGRhiShaderPipeline>(m_rhiContext);
578 shaders->addStage(QRhiShaderStage(QRhiShaderStage::Vertex, vertexShader), stageFlags);
579 shaders->addStage(QRhiShaderStage(QRhiShaderStage::Fragment, fragmentShader), stageFlags);
580 if (shaderDebug)
581 qDebug("Compilation for vertex and fragment stages succeeded");
582 }
583
584 if (editorMode && s_statusCallback) {
585 using namespace QtQuick3DEditorHelpers::ShaderBaker;
586 const auto vertStatus = vertShaderValid ? Status::Success : Status::Error;
587 const auto fragStatus = fragShaderValid ? Status::Success : Status::Error;
588 QMutexLocker locker(&*s_statusMutex);
589 s_statusCallback(inKey, vertStatus, vertErr, QShader::VertexStage);
590 s_statusCallback(inKey, fragStatus, fragErr, QShader::FragmentStage);
591 }
592
593 auto result = m_rhiShaders.insert(tempKey, shaders).value();
594 if (result && result->vertexStage() && result->fragmentStage()) {
595 QQsbCollection::EntryDesc entryDesc = {
596 cacheKeyString,
597 QQsbCollection::toFeatureSet(inFeatures),
598 result->vertexStage()->shader(),
599 result->fragmentStage()->shader()
600 };
601 m_persistentShaderBakingCache.addEntry(entryDesc.generateSha(), entryDesc);
602 }
603 return result;
604
605#else
606 Q_UNUSED(inKey);
607 Q_UNUSED(inVert);
608 Q_UNUSED(inFrag);
609 Q_UNUSED(inFeatures);
610 Q_UNUSED(stageFlags);
611 Q_UNUSED(inCacheKey);
612 qWarning("Cannot compile and condition shaders at runtime because this build of Qt Quick 3D is not linking to Qt Shader Tools. "
613 "Only pre-processed materials are supported.");
614 return {};
615#endif
616}
617
618QSSGRhiShaderPipelinePtr QSSGShaderCache::compileComputePipeline(
619 const QByteArray &inKey, const QByteArray &inComp,
620 int viewCount, bool perTargetCompilation, const QByteArray &inCacheKey)
621{
622#ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS
623 const QByteArray &cacheKeyString = inCacheKey.isEmpty() ? inKey : inCacheKey;
624 const QSSGRhiShaderPipelinePtr &rhiShaders = tryGetRhiShaderPipeline(cacheKeyString, {});
625 if (rhiShaders) {
626 QSSG_ASSERT(rhiShaders->computeStage(), return {});
627 return rhiShaders;
628 }
629 QSSGShaderCacheKey tempKey(cacheKeyString);
630 tempKey.updateHashCode();
631
632 QShaderBaker baker;
633 m_initBaker(&baker, m_rhiContext.rhi());
634
635 QByteArray computeCode = "#version 440\n";
636
637 if (!inKey.isNull()) {
638 computeCode += "//Shader name -";
639 computeCode += inKey;
640 computeCode += "\n";
641 }
642
643 // match Qt Quick and QSGMaterial(Shader)
644 computeCode += "#define QSHADER_VIEW_COUNT ";
645 computeCode += QByteArray::number(viewCount);
646 computeCode += "\n";
647
648 computeCode += inComp;
649
650 // If requested, per-target compilation allows doing things like #if
651 // QSHADER_HLSL in the shader code, at the expense of spending more time in
652 // bake())
653 baker.setPerTargetCompilation(perTargetCompilation);
654 baker.setSourceString(computeCode, QShader::ComputeStage);
655 baker.setMultiViewCount(viewCount);
656
657 const bool editorMode = QSSGRhiContextPrivate::editorMode();
658 // Shader debug is disabled in editor mode
659 const bool shaderDebug = !editorMode && QSSGRhiContextPrivate::shaderDebuggingEnabled();
660
661 QShader computeShader = baker.bake();
662 QSSGRhiShaderPipelinePtr shaders;
663 const auto valid = computeShader.isValid();
664 if (shaderDebug) {
665 dumpShader(QShader::Stage::ComputeStage, computeCode);
666 if (!valid)
667 dumpShaderToFile(QShader::Stage::ComputeStage, computeCode);
668 }
669 if (valid) {
670 shaders = std::make_shared<QSSGRhiShaderPipeline>(m_rhiContext);
671 shaders->addStage(QRhiShaderStage(QRhiShaderStage::Compute, computeShader), {});
672 if (shaderDebug)
673 qDebug("Compilation for compute stage succeeded");
674 } else if (!editorMode) {
675 qWarning("Failed to compile compute shader: %s\n", qPrintable(baker.errorMessage()));
676 if (!shaderDebug)
677 qWarning() << inKey << '\n';
678 }
679 auto result = m_rhiShaders.insert(tempKey, shaders).value();
680 if (result && result->computeStage()) {
681 QQsbCollection::EntryDesc entryDesc = {
682 cacheKeyString,
683 {},
684 result->computeStage()->shader(),
685 {}
686 };
687 m_persistentShaderBakingCache.addEntry(entryDesc.generateSha(), entryDesc);
688 }
689 return result;
690#else
691 Q_UNUSED(inKey);
692 Q_UNUSED(inComp);
693 Q_UNUSED(viewCount);
694 Q_UNUSED(perTargetCompilation);
695 Q_UNUSED(inCacheKey);
696 qWarning("Cannot compile and condition shaders at runtime because this build of Qt Quick 3D is not linking to Qt Shader Tools. "
697 "Only pre-processed materials are supported.");
698 return {};
699#endif
700}
701
702QSSGRhiShaderPipelinePtr QSSGShaderCache::newPipelineFromPregenerated(const QByteArray &inKey,
703 const QSSGShaderFeatures &inFeatures,
704 QQsbCollection::Entry entry,
705 const QSSGRenderGraphObject &obj,
706 QSSGRhiShaderPipeline::StageFlags stageFlags)
707{
708 // No lookup in m_rhiShaders. It is up to the caller to do that, if they
709 // want to. We will insert into it at the end, but there is intentionally
710 // no lookup. The result from this function is always a new
711 // QSSGRhiShaderPipeline (it's just much faster to create than the
712 // full-blown generator). That is important for some clients (effect
713 // system) so returning an existing QSSGRhiShaderPipeline is _wrong_.
714
715 const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled();
716 if (shaderDebug)
717 qDebug("Loading pregenerated rhi shader(s)");
718
719 Q_TRACE_SCOPE(QSSG_loadShader);
720 Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DLoadShader);
721
722 // Note that we are required to return a non-null (but empty) shader set even if loading fails.
723 QSSGRhiShaderPipelinePtr shaders(new QSSGRhiShaderPipeline(m_rhiContext));
724
725 const QStringList collectionFiles
726 = {QString::fromLatin1(resourceFolder() + shaderCollectionFile()),
727 QString::fromLatin1(resourceFolder() + particleShaderCollectionFile())};
728
729 QQsbCollection::EntryDesc entryDesc;
730 QScopedPointer<QQsbIODeviceCollection> qsbc;
731 for (const auto &collectionFile : collectionFiles) {
732 QFileInfo info(collectionFile);
733 if (!info.exists())
734 continue;
735
736 qsbc.reset(new QQsbIODeviceCollection(collectionFile));
737 if (qsbc->map(QQsbIODeviceCollection::Read)) {
738 if (qsbc->extractEntry(entry, entryDesc))
739 break;
740 else
741 qsbc.reset();
742 }
743 }
744
745 if (qsbc.isNull())
746 qWarning("Failed to open entry %s", entry.key.constData());
747
748 if (entryDesc.vertShader.isValid() && entryDesc.fragShader.isValid()) {
749 shaders->addStage(QRhiShaderStage(QRhiShaderStage::Vertex, entryDesc.vertShader), stageFlags);
750 shaders->addStage(QRhiShaderStage(QRhiShaderStage::Fragment, entryDesc.fragShader), stageFlags);
751 if (shaderDebug)
752 qDebug("Loading of vertex and fragment stages succeeded");
753 }
754
755#if !QT_CONFIG(qml_debug)
756 Q_UNUSED(obj);
757#else
758 Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DLoadShader, 0, obj.profilingId);
759#endif
760
761 QSSGShaderCacheKey cacheKey(inKey);
762 cacheKey.m_features = inFeatures;
763 cacheKey.updateHashCode();
764
765 const auto inserted = m_rhiShaders.insert(cacheKey, shaders);
766 if (qsbc)
767 qsbc->unmap();
768 return inserted.value();
769}
770
771QSSGRhiShaderPipelinePtr QSSGShaderCache::tryNewPipelineFromPersistentCache(const QByteArray &qsbcKey,
772 const QByteArray &inKey,
773 const QSSGShaderFeatures &inFeatures,
774 QSSGRhiShaderPipeline::StageFlags stageFlags)
775{
776 // No lookup in m_rhiShaders. it is up to the caller to do that, if they
777 // want to. We will insert into it at the end, but there is intentionally
778 // no lookup. The result from this function is always a new
779 // QSSGRhiShaderPipeline (it's just much faster to create than the
780 // full-blown generator). That is important for some clients (effect
781 // system) so returning an existing QSSGRhiShaderPipeline is _wrong_.
782
783 QQsbCollection::EntryDesc entryDesc;
784
785 // Here we are allowed to return null to indicate that there is no such
786 // entry in this particular cache.
787 if (!m_persistentShaderBakingCache.extractEntry(QQsbCollection::Entry(qsbcKey), entryDesc))
788 return {};
789
790 if (entryDesc.vertShader.isValid() && entryDesc.fragShader.isValid()) {
791 const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled();
792 if (shaderDebug)
793 qDebug("Loading rhi shaders from disk cache for %s (%s)", qsbcKey.constData(), inKey.constData());
794
795 QSSGRhiShaderPipelinePtr shaders(new QSSGRhiShaderPipeline(m_rhiContext));
796 shaders->addStage(QRhiShaderStage(QRhiShaderStage::Vertex, entryDesc.vertShader), stageFlags);
797 shaders->addStage(QRhiShaderStage(QRhiShaderStage::Fragment, entryDesc.fragShader), stageFlags);
798 QSSGShaderCacheKey cacheKey(inKey);
799 cacheKey.m_features = inFeatures;
800 cacheKey.updateHashCode();
801 return m_rhiShaders.insert(cacheKey, shaders).value();
802 } else if (entryDesc.vertShader.isValid()) {
803 const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled();
804 if (shaderDebug)
805 qDebug("Loading rhi shaders from disk cache for %s (%s)", qsbcKey.constData(), inKey.constData());
806
807 if (entryDesc.vertShader.stage() != QShader::ComputeStage) {
808 qWarning("Failed to find compute stage while loading rhi shaders from disk cache for %s.", qsbcKey.constData());
809 return {};
810 }
811 QSSGRhiShaderPipelinePtr shaders(new QSSGRhiShaderPipeline(m_rhiContext));
812 shaders->addStage(QRhiShaderStage(QRhiShaderStage::Compute, entryDesc.vertShader), stageFlags);
813 QSSGShaderCacheKey cacheKey(inKey);
814 cacheKey.m_features = inFeatures;
815 cacheKey.updateHashCode();
816 return m_rhiShaders.insert(cacheKey, shaders).value();
817 }
818
819 return {};
820}
821
822QSSGRhiShaderPipelinePtr QSSGShaderCache::loadBuiltinUncached(const QByteArray &inKey, int viewCount,
823 QSSGRhiShaderPipeline::StageFlags vertexStageFlags)
824{
825 const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled();
826 if (shaderDebug)
827 qDebug("Loading builtin rhi shader: %s (view count: %d)", inKey.constData(), viewCount);
828
829 Q_TRACE_SCOPE(QSSG_loadShader);
830 Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DLoadShader);
831
832 // Note that we are required to return a non-null (but empty) shader set even if loading fails.
833 QSSGRhiShaderPipelinePtr shaders(new QSSGRhiShaderPipeline(m_rhiContext));
834
835 // inShaderName is a prefix of a .qsb file, so "abc" means we should
836 // look for abc.vert.qsb and abc.frag.qsb.
837
838 const QString prefix = QString::fromUtf8(resourceFolder() + inKey);
839 QString vertexFileName = prefix + QLatin1String(".vert.qsb");
840 QString fragmentFileName = prefix + QLatin1String(".frag.qsb");
841
842 // This must match QSGMaterial(Shader) in Qt Quick, in particular the
843 // QSGMaterialShader::setShaderFileName() overload taking a viewCount.
844 if (viewCount == 2) {
845 vertexFileName += QLatin1String(".mv2qsb");
846 fragmentFileName += QLatin1String(".mv2qsb");
847 }
848
849 QShader vertexShader;
850 QShader fragmentShader;
851
852 QFile f;
853 f.setFileName(vertexFileName);
854 if (f.open(QIODevice::ReadOnly)) {
855 const QByteArray vsData = f.readAll();
856 vertexShader = QShader::fromSerialized(vsData);
857 f.close();
858 } else {
859 qWarning("Failed to open %s", qPrintable(f.fileName()));
860 }
861 f.setFileName(fragmentFileName);
862 if (f.open(QIODevice::ReadOnly)) {
863 const QByteArray fsData = f.readAll();
864 fragmentShader = QShader::fromSerialized(fsData);
865 f.close();
866 } else {
867 qWarning("Failed to open %s", qPrintable(f.fileName()));
868 }
869
870 if (vertexShader.isValid() && fragmentShader.isValid()) {
871 shaders->addStage(QRhiShaderStage(QRhiShaderStage::Vertex, vertexShader), vertexStageFlags);
872 shaders->addStage(QRhiShaderStage(QRhiShaderStage::Fragment, fragmentShader));
873 if (shaderDebug)
874 qDebug("Loading of vertex and fragment stages succeeded");
875 }
876
877 Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DLoadShader, 0, inKey);
878
879 return shaders;
880}
881
884{
885 QMutexLocker locker(&*s_statusMutex);
886 s_statusCallback = cb;
887}
888
890{
891 s_autoDiskCacheEnabled = enable;
892}
893
898
899}
900
901QT_END_NAMESPACE
Combined button and popup list for selecting options.
Q_QUICK3DRUNTIMERENDER_EXPORT void setStatusCallback(StatusCallback cb)
void(*)(const QByteArray &descKey, Status status, const QString &err, QShader::Stage stage) StatusCallback
Q_QUICK3DRUNTIMERENDER_EXPORT void setAutomaticDiskCache(bool enable)
Q_QUICK3DRUNTIMERENDER_EXPORT bool isAutomaticDiskCacheEnabled()
Q_GLOBAL_STATIC(QReadWriteLock, g_updateMutex)
static QString persistentQsbcFileName()
static bool isAutoDiskCacheEnabled()
size_t qHash(QSSGShaderFeatures features) noexcept
static bool ensureWritableDir(const QString &name)
static bool s_autoDiskCacheEnabled
static QString persistentQsbcDir()
static void initBakerForNonPersistentUse(QShaderBaker *, QRhi *)
static constexpr DefineEntry DefineTable[]