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 | |
5 | #include "graphobjects/qssgrendergraphobject_p.h" |
6 | #include "qssgrendershadercache_p.h" |
7 | #include "qssgrendercontextcore.h" |
8 | |
9 | #include <QtQuick3DUtils/private/qssgutils_p.h> |
10 | #include <QtQuick3DUtils/private/qquick3dprofiler_p.h> |
11 | |
12 | #include <QtQuick3DRuntimeRender/private/qssgruntimerenderlogging_p.h> |
13 | #include <qtquick3d_tracepoints_p.h> |
14 | |
15 | #include <QCoreApplication> |
16 | #include <QStandardPaths> |
17 | #include <QString> |
18 | #include <QFile> |
19 | #include <QDir> |
20 | |
21 | #include <QtGui/qsurfaceformat.h> |
22 | #if QT_CONFIG(opengl) |
23 | # include <QtGui/qopenglcontext.h> |
24 | #endif |
25 | |
26 | #ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS |
27 | #include <rhi/qshaderbaker.h> |
28 | #endif |
29 | |
30 | #include <QtCore/qmutex.h> |
31 | |
32 | QT_BEGIN_NAMESPACE |
33 | |
34 | Q_TRACE_POINT(qtquick3d, QSSG_loadShader_entry) |
35 | Q_TRACE_POINT(qtquick3d, QSSG_loadShader_exit) |
36 | |
37 | static QtQuick3DEditorHelpers::ShaderBaker::StatusCallback s_statusCallback = nullptr; |
38 | Q_GLOBAL_STATIC(QMutex, s_statusMutex); |
39 | |
40 | size_t qHash(QSSGShaderFeatures features) noexcept { return (features.flags & (~QSSGShaderFeatures::IndexMask)); } |
41 | |
42 | static QString dumpFilename(QShader::Stage stage) |
43 | { |
44 | switch (stage) { |
45 | case QShader::VertexStage: |
46 | return QStringLiteral("failedvert.txt"); |
47 | break; |
48 | case QShader::FragmentStage: |
49 | return QStringLiteral("failedfrag.txt"); |
50 | break; |
51 | default: |
52 | return QStringLiteral("failedshader.txt"); |
53 | } |
54 | } |
55 | |
56 | struct DefineEntry |
57 | { |
58 | const char *name = nullptr; |
59 | QSSGShaderFeatures::Feature feature {}; |
60 | }; |
61 | |
62 | static constexpr DefineEntry DefineTable[] { |
63 | { .name: "QSSG_ENABLE_LIGHT_PROBE", .feature: QSSGShaderFeatures::Feature::LightProbe }, |
64 | { .name: "QSSG_ENABLE_IBL_ORIENTATION", .feature: QSSGShaderFeatures::Feature::IblOrientation }, |
65 | { .name: "QSSG_ENABLE_SSM", .feature: QSSGShaderFeatures::Feature::Ssm }, |
66 | { .name: "QSSG_ENABLE_SSAO", .feature: QSSGShaderFeatures::Feature::Ssao }, |
67 | { .name: "QSSG_ENABLE_DEPTH_PASS", .feature: QSSGShaderFeatures::Feature::DepthPass }, |
68 | { .name: "QSSG_ENABLE_ORTHO_SHADOW_PASS", .feature: QSSGShaderFeatures::Feature::OrthoShadowPass }, |
69 | { .name: "QSSG_ENABLE_PERSPECTIVE_SHADOW_PASS", .feature: QSSGShaderFeatures::Feature::PerspectiveShadowPass }, |
70 | { .name: "QSSG_ENABLE_LINEAR_TONEMAPPING", .feature: QSSGShaderFeatures::Feature::LinearTonemapping }, |
71 | { .name: "QSSG_ENABLE_ACES_TONEMAPPING", .feature: QSSGShaderFeatures::Feature::AcesTonemapping }, |
72 | { .name: "QSSG_ENABLE_HEJLDAWSON_TONEMAPPING", .feature: QSSGShaderFeatures::Feature::HejlDawsonTonemapping }, |
73 | { .name: "QSSG_ENABLE_FILMIC_TONEMAPPING", .feature: QSSGShaderFeatures::Feature::FilmicTonemapping }, |
74 | { .name: "QSSG_ENABLE_RGBE_LIGHT_PROBE", .feature: QSSGShaderFeatures::Feature::RGBELightProbe }, |
75 | { .name: "QSSG_ENABLE_OPAQUE_DEPTH_PRE_PASS", .feature: QSSGShaderFeatures::Feature::OpaqueDepthPrePass }, |
76 | { .name: "QSSG_ENABLE_REFLECTION_PROBE", .feature: QSSGShaderFeatures::Feature::ReflectionProbe }, |
77 | { .name: "QSSG_REDUCE_MAX_NUM_LIGHTS", .feature: QSSGShaderFeatures::Feature::ReduceMaxNumLights }, |
78 | { .name: "QSSG_ENABLE_LIGHTMAP", .feature: QSSGShaderFeatures::Feature::Lightmap }, |
79 | { .name: "QSSG_DISABLE_MULTIVIEW", .feature: QSSGShaderFeatures::Feature::DisableMultiView }, |
80 | { .name: "QSSG_FORCE_IBL_EXPOSURE", .feature: QSSGShaderFeatures::Feature::ForceIblExposure }, |
81 | }; |
82 | |
83 | static_assert(std::size(DefineTable) == QSSGShaderFeatures::Count, "Missing feature define?"); |
84 | |
85 | const char *QSSGShaderFeatures::asDefineString(QSSGShaderFeatures::Feature feature) { return DefineTable[static_cast<FlagType>(feature) & QSSGShaderFeatures::IndexMask].name; } |
86 | QSSGShaderFeatures::Feature QSSGShaderFeatures::fromIndex(quint32 idx) { return DefineTable[idx].feature; } |
87 | |
88 | void QSSGShaderFeatures::set(QSSGShaderFeatures::Feature feature, bool val) |
89 | { |
90 | if (val) |
91 | flags |= (static_cast<FlagType>(feature) & ~IndexMask); |
92 | else |
93 | flags &= ~(static_cast<FlagType>(feature) & ~IndexMask); |
94 | } |
95 | |
96 | #ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS |
97 | static void initBakerForNonPersistentUse(QShaderBaker *baker, QRhi *rhi) |
98 | { |
99 | QVector<QShaderBaker::GeneratedShader> outputs; |
100 | switch (rhi->backend()) { |
101 | case QRhi::D3D11: |
102 | outputs.append(t: { QShader::HlslShader, QShaderVersion(50) }); // Shader Model 5.0 |
103 | break; |
104 | case QRhi::D3D12: |
105 | outputs.append(t: { QShader::HlslShader, QShaderVersion(61) }); // Shader Model 6.1 (includes multiview support) |
106 | break; |
107 | case QRhi::Metal: |
108 | #if defined(Q_OS_VISIONOS) |
109 | outputs.append({ QShader::MslShader, QShaderVersion(21) }); // Metal 2.1 (multiview support) |
110 | #else |
111 | outputs.append(t: { QShader::MslShader, QShaderVersion(12) }); // Metal 1.2 |
112 | #endif // Q_OS_VISIONOS |
113 | break; |
114 | case QRhi::OpenGLES2: |
115 | { |
116 | QSurfaceFormat format = QSurfaceFormat::defaultFormat(); |
117 | #if QT_CONFIG(opengl) |
118 | auto h = static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles()); |
119 | if (h && h->context) |
120 | format = h->context->format(); |
121 | #endif |
122 | if (format.profile() == QSurfaceFormat::CoreProfile && format.version() >= qMakePair(value1: 3, value2: 3)) { |
123 | outputs.append(t: { QShader::GlslShader, QShaderVersion(330) }); // OpenGL 3.3+ |
124 | } else { |
125 | bool isGLESModule = false; |
126 | #if QT_CONFIG(opengl) |
127 | isGLESModule = QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES; |
128 | #endif |
129 | if (format.renderableType() == QSurfaceFormat::OpenGLES || isGLESModule) { |
130 | if (format.majorVersion() >= 3) |
131 | outputs.append(t: { QShader::GlslShader, QShaderVersion(300, QShaderVersion::GlslEs) }); // GLES 3.0+ |
132 | else |
133 | outputs.append(t: { QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs) }); // GLES 2.0 |
134 | } else { |
135 | // Need to default to at least GLSL 130 (OpenGL 3.0), not 120. |
136 | // The difference is actually relevant when it comes to certain |
137 | // GLSL features (textureSize, unsigned integers, and with |
138 | // SPIRV-Cross even bool), and we do not have to care about |
139 | // pure OpenGL (non-ES) 2.x implementations in practice. |
140 | |
141 | // For full feature set we need GLSL 140 (OpenGL 3.1), e.g. |
142 | // because of inverse() used for instancing. |
143 | |
144 | // GLSL 130 should still be attempted, to support old Mesa |
145 | // llvmpipe that only gives us OpenGL 3.0. At the time of |
146 | // writing the opengl32sw.dll shipped with pre-built Qt is one |
147 | // of these still. |
148 | |
149 | if (format.version() >= qMakePair(value1: 3, value2: 1)) |
150 | outputs.append(t: { QShader::GlslShader, QShaderVersion(140) }); // OpenGL 3.1+ |
151 | else |
152 | outputs.append(t: { QShader::GlslShader, QShaderVersion(130) }); // OpenGL 3.0+ |
153 | } |
154 | } |
155 | } |
156 | break; |
157 | default: // Vulkan, Null |
158 | outputs.append(t: { QShader::SpirvShader, QShaderVersion(100) }); |
159 | break; |
160 | } |
161 | |
162 | baker->setGeneratedShaders(outputs); |
163 | baker->setGeneratedShaderVariants({ QShader::StandardShader }); |
164 | } |
165 | |
166 | static void initBakerForPersistentUse(QShaderBaker *baker, QRhi *) |
167 | { |
168 | QVector<QShaderBaker::GeneratedShader> outputs; |
169 | outputs.reserve(asize: 8); |
170 | |
171 | #ifndef Q_OS_WASM |
172 | outputs.append(t: { QShader::SpirvShader, QShaderVersion(100) }); |
173 | outputs.append(t: { QShader::HlslShader, QShaderVersion(50) }); // Shader Model 5.0 |
174 | outputs.append(t: { QShader::HlslShader, QShaderVersion(61) }); // Shader Model 6.1 (for multiview on d3d12) |
175 | #if defined(Q_OS_VISIONOS) |
176 | outputs.append({ QShader::MslShader, QShaderVersion(21) }); // Metal 2.1 (multiview support) |
177 | #else |
178 | outputs.append(t: { QShader::MslShader, QShaderVersion(12) }); // Metal 1.2 |
179 | #endif // Q_OS_VISIONOS |
180 | outputs.append(t: { QShader::GlslShader, QShaderVersion(330) }); // OpenGL 3.3+ |
181 | outputs.append(t: { QShader::GlslShader, QShaderVersion(140) }); // OpenGL 3.1+ |
182 | outputs.append(t: { QShader::GlslShader, QShaderVersion(130) }); // OpenGL 3.0+ |
183 | outputs.append(t: { QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs) }); // GLES 2.0 |
184 | #endif |
185 | outputs.append(t: { QShader::GlslShader, QShaderVersion(300, QShaderVersion::GlslEs) }); // GLES 3.0+ |
186 | |
187 | // If one of the above cannot be generated due to failing at the |
188 | // SPIRV-Cross translation stage, it will be skipped, but bake() will not |
189 | // fail. This is essential, because with the default fail if anything fails |
190 | // behavior many shaders could not be baked at all due to failing for e.g. |
191 | // GLSL ES 100. This is a non-issue when choosing the targets dynamically |
192 | // based on the current API/context, but here we need to ensure what we |
193 | // generate will work with a different RHI backend, graphics API, and |
194 | // perhaps even on a different platform (if the cache file is manually |
195 | // moved). So have to generate what we can, without breaking the |
196 | // application when the shader is not compatible with a target. (if that |
197 | // shader is not used at runtime, it's fine anyway, it it is, it won't work |
198 | // just as with the other, non-caching path) |
199 | baker->setBreakOnShaderTranslationError(false); |
200 | |
201 | baker->setGeneratedShaders(outputs); |
202 | baker->setGeneratedShaderVariants({ QShader::StandardShader }); |
203 | } |
204 | |
205 | #else |
206 | static void initBakerForNonPersistentUse(QShaderBaker *, QRhi *) |
207 | { |
208 | } |
209 | |
210 | static void initBakerForPersistentUse(QShaderBaker *, QRhi *) |
211 | { |
212 | } |
213 | #endif // QT_QUICK3D_HAS_RUNTIME_SHADERS |
214 | |
215 | static bool s_autoDiskCacheEnabled = true; |
216 | |
217 | static bool isAutoDiskCacheEnabled() |
218 | { |
219 | // these three mirror QOpenGLShaderProgram/QQuickGraphicsConfiguration/QSGRhiSupport |
220 | static const bool diskCacheDisabled = qEnvironmentVariableIntValue(varName: "QT_DISABLE_SHADER_DISK_CACHE") |
221 | || qEnvironmentVariableIntValue(varName: "QSG_RHI_DISABLE_DISK_CACHE"); |
222 | const bool attrDiskCacheDisabled = (qApp ? qApp->testAttribute(attribute: Qt::AA_DisableShaderDiskCache) : false); |
223 | return (!diskCacheDisabled && !attrDiskCacheDisabled && s_autoDiskCacheEnabled); |
224 | |
225 | } |
226 | |
227 | static inline bool ensureWritableDir(const QString &name) |
228 | { |
229 | QDir::root().mkpath(dirPath: name); |
230 | return QFileInfo(name).isWritable(); |
231 | } |
232 | |
233 | static QString persistentQsbcDir() |
234 | { |
235 | static bool checked = false; |
236 | static QString currentCacheDir; |
237 | static bool cacheWritable = false; |
238 | |
239 | if (checked) |
240 | return cacheWritable ? currentCacheDir : QString(); |
241 | |
242 | checked = true; |
243 | const QString cachePath = QStandardPaths::writableLocation(type: QStandardPaths::CacheLocation); |
244 | const QString subPath = QLatin1String("/q3dshadercache-") + QSysInfo::buildAbi() + QLatin1Char('/'); |
245 | |
246 | if (!cachePath.isEmpty()) { |
247 | currentCacheDir = cachePath + subPath; |
248 | cacheWritable = ensureWritableDir(name: currentCacheDir); |
249 | } |
250 | |
251 | return cacheWritable ? currentCacheDir : QString(); |
252 | } |
253 | |
254 | static inline QString persistentQsbcFileName() |
255 | { |
256 | const QString cacheDir = persistentQsbcDir(); |
257 | if (!cacheDir.isEmpty()) |
258 | return cacheDir + QLatin1String("q3dshadercache.qsbc"); |
259 | |
260 | return QString(); |
261 | } |
262 | |
263 | QSSGShaderCache::QSSGShaderCache(QSSGRhiContext &ctx, |
264 | const InitBakerFunc initBakeFn) |
265 | : m_rhiContext(ctx), |
266 | m_initBaker(initBakeFn), |
267 | m_builtInShaders(*this) |
268 | { |
269 | if (isAutoDiskCacheEnabled()) { |
270 | const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled(); |
271 | m_persistentShaderStorageFileName = persistentQsbcFileName(); |
272 | if (!m_persistentShaderStorageFileName.isEmpty()) { |
273 | const bool skipCacheFile = qEnvironmentVariableIntValue(varName: "QT_QUICK3D_NO_SHADER_CACHE_LOAD"); |
274 | if (!skipCacheFile && QFileInfo::exists(file: m_persistentShaderStorageFileName)) { |
275 | if (shaderDebug) |
276 | qDebug(msg: "Attempting to seed material shader cache from %s", qPrintable(m_persistentShaderStorageFileName)); |
277 | if (m_persistentShaderBakingCache.load(filename: m_persistentShaderStorageFileName)) { |
278 | if (shaderDebug) { |
279 | const int count = m_persistentShaderBakingCache.availableEntries().count(); |
280 | qDebug(msg: "Loaded %d shader pipelines into the material shader cache", count); |
281 | } |
282 | } |
283 | } |
284 | } |
285 | } |
286 | |
287 | if (!m_initBaker) { |
288 | // It is important to generate all possible shader variants if the qsb |
289 | // collection is going to be stored on disk. Otherwise switching the |
290 | // rhi backend could break the application. This is however an overkill |
291 | // if we know that what we bake will not be reused in future runs of |
292 | // the application, so do not do it if the disk cache was disabled or |
293 | // the cache directory was not available (no file system, no |
294 | // permissions, etc.). |
295 | m_initBaker = m_persistentShaderStorageFileName.isEmpty() ? initBakerForNonPersistentUse |
296 | : initBakerForPersistentUse; |
297 | } |
298 | } |
299 | |
300 | QSSGShaderCache::~QSSGShaderCache() |
301 | { |
302 | if (!m_persistentShaderStorageFileName.isEmpty()) |
303 | m_persistentShaderBakingCache.save(filename: m_persistentShaderStorageFileName); |
304 | } |
305 | |
306 | void QSSGShaderCache::releaseCachedResources() |
307 | { |
308 | m_builtInShaders.releaseCachedResources(); |
309 | |
310 | m_rhiShaders.clear(); |
311 | |
312 | // m_persistentShaderBakingCache is not cleared, that is intentional, |
313 | // otherwise we would permanently lose what got loaded at startup. |
314 | } |
315 | |
316 | QSSGRhiShaderPipelinePtr QSSGShaderCache::tryGetRhiShaderPipeline(const QByteArray &inKey, |
317 | const QSSGShaderFeatures &inFeatures) |
318 | { |
319 | QSSGShaderCacheKey cacheKey(inKey); |
320 | cacheKey.m_features = inFeatures; |
321 | cacheKey.updateHashCode(); |
322 | const auto theIter = m_rhiShaders.constFind(key: cacheKey); |
323 | if (theIter != m_rhiShaders.cend()) |
324 | return theIter.value(); |
325 | return nullptr; |
326 | } |
327 | |
328 | |
329 | void QSSGShaderCache::addShaderPreprocessor(QByteArray &str, |
330 | const QByteArray &inKey, |
331 | ShaderType shaderType, |
332 | const QSSGShaderFeatures &inFeatures, |
333 | int viewCount) |
334 | { |
335 | m_insertStr.clear(); |
336 | |
337 | m_insertStr += "#version 440\n"; |
338 | |
339 | if (!inKey.isNull()) { |
340 | m_insertStr += "//Shader name -"; |
341 | m_insertStr += inKey; |
342 | m_insertStr += "\n"; |
343 | } |
344 | |
345 | m_insertStr += "#define texture2D texture\n"; |
346 | |
347 | // match Qt Quick and QSGMaterial(Shader) |
348 | m_insertStr += "#define QSHADER_VIEW_COUNT "; |
349 | m_insertStr += QByteArray::number(viewCount); |
350 | m_insertStr += "\n"; |
351 | |
352 | str.insert(i: 0, data: m_insertStr); |
353 | QString::size_type insertPos = int(m_insertStr.size()); |
354 | |
355 | m_insertStr.clear(); |
356 | const bool fragOutputEnabled = (!inFeatures.isSet(feature: QSSGShaderFeatures::Feature::DepthPass)) && shaderType == ShaderType::Fragment; |
357 | for (const auto &def : DefineTable) { |
358 | m_insertStr.append(s: "#define "); |
359 | m_insertStr.append(s: def.name); |
360 | m_insertStr.append(s: " "); |
361 | m_insertStr.append(s: inFeatures.isSet(feature: def.feature) ? "1": "0"); |
362 | m_insertStr.append(s: "\n"); |
363 | } |
364 | |
365 | str.insert(i: insertPos, data: m_insertStr); |
366 | insertPos += int(m_insertStr.size()); |
367 | |
368 | m_insertStr.clear(); |
369 | if (fragOutputEnabled) |
370 | m_insertStr += "layout(location = 0) out vec4 fragOutput;\n"; |
371 | |
372 | str.insert(i: insertPos, data: m_insertStr); |
373 | } |
374 | |
375 | QByteArray QSSGShaderCache::resourceFolder() |
376 | { |
377 | return QByteArrayLiteral(":/res/rhishaders/"); |
378 | } |
379 | |
380 | QByteArray QSSGShaderCache::shaderCollectionFile() |
381 | { |
382 | return QByteArrayLiteral("qtappshaders.qsbc"); |
383 | } |
384 | |
385 | QSSGRhiShaderPipelinePtr QSSGShaderCache::compileForRhi(const QByteArray &inKey, const QByteArray &inVert, const QByteArray &inFrag, |
386 | const QSSGShaderFeatures &inFeatures, QSSGRhiShaderPipeline::StageFlags stageFlags, |
387 | int viewCount, |
388 | bool perTargetCompilation) |
389 | { |
390 | #ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS |
391 | const QSSGRhiShaderPipelinePtr &rhiShaders = tryGetRhiShaderPipeline(inKey, inFeatures); |
392 | if (rhiShaders) |
393 | return rhiShaders; |
394 | |
395 | QSSGShaderCacheKey tempKey(inKey); |
396 | tempKey.m_features = inFeatures; |
397 | tempKey.updateHashCode(); |
398 | |
399 | QByteArray vertexCode = inVert; |
400 | QByteArray fragmentCode = inFrag; |
401 | |
402 | if (!vertexCode.isEmpty()) |
403 | addShaderPreprocessor(str&: vertexCode, inKey, shaderType: ShaderType::Vertex, inFeatures, viewCount); |
404 | |
405 | if (!fragmentCode.isEmpty()) |
406 | addShaderPreprocessor(str&: fragmentCode, inKey, shaderType: ShaderType::Fragment, inFeatures, viewCount); |
407 | |
408 | // lo and behold the final shader strings are ready |
409 | |
410 | QSSGRhiShaderPipelinePtr shaders; |
411 | QString vertErr, fragErr; |
412 | |
413 | QShaderBaker baker; |
414 | m_initBaker(&baker, m_rhiContext.rhi()); |
415 | |
416 | // If requested, per-target compilation allows doing things like #if |
417 | // QSHADER_HLSL in the shader code, at the expense of spending more time in |
418 | // bake()) |
419 | baker.setPerTargetCompilation(perTargetCompilation); |
420 | |
421 | // This is in the shader key, but cannot query that here anymore now that it's serialized. |
422 | // So we get it as a dedicated argument. |
423 | baker.setMultiViewCount(viewCount); |
424 | |
425 | const bool editorMode = QSSGRhiContextPrivate::editorMode(); |
426 | // Shader debug is disabled in editor mode |
427 | const bool shaderDebug = !editorMode && QSSGRhiContextPrivate::shaderDebuggingEnabled(); |
428 | |
429 | static auto dumpShader = [](QShader::Stage stage, const QByteArray &code) { |
430 | switch (stage) { |
431 | case QShader::Stage::VertexStage: |
432 | qDebug(msg: "VERTEX SHADER:\n*****\n"); |
433 | break; |
434 | case QShader::Stage::FragmentStage: |
435 | qDebug(msg: "FRAGMENT SHADER:\n*****\n"); |
436 | break; |
437 | default: |
438 | qDebug(msg: "SHADER:\n*****\n"); |
439 | break; |
440 | } |
441 | const auto lines = code.split(sep: '\n'); |
442 | for (int i = 0; i < lines.size(); i++) |
443 | qDebug(msg: "%3d %s", i + 1, lines.at(i).constData()); |
444 | qDebug(msg: "\n*****\n"); |
445 | }; |
446 | |
447 | static auto dumpShaderToFile = [](QShader::Stage stage, const QByteArray &data) { |
448 | QFile f(dumpFilename(stage)); |
449 | f.open(flags: QIODevice::WriteOnly | QIODevice::Text); |
450 | f.write(data); |
451 | f.close(); |
452 | }; |
453 | |
454 | baker.setSourceString(sourceString: vertexCode, stage: QShader::VertexStage); |
455 | QShader vertexShader = baker.bake(); |
456 | const auto vertShaderValid = vertexShader.isValid(); |
457 | if (!vertShaderValid) { |
458 | vertErr = baker.errorMessage(); |
459 | if (!editorMode) { |
460 | qWarning(msg: "Failed to compile vertex shader: %s\n", qPrintable(vertErr)); |
461 | if (!shaderDebug) |
462 | qWarning() << inKey << '\n'; |
463 | } |
464 | } |
465 | |
466 | if (shaderDebug) { |
467 | dumpShader(QShader::Stage::VertexStage, vertexCode); |
468 | if (!vertShaderValid) |
469 | dumpShaderToFile(QShader::Stage::VertexStage, vertexCode); |
470 | } |
471 | |
472 | baker.setSourceString(sourceString: fragmentCode, stage: QShader::FragmentStage); |
473 | QShader fragmentShader = baker.bake(); |
474 | const bool fragShaderValid = fragmentShader.isValid(); |
475 | if (!fragShaderValid) { |
476 | fragErr = baker.errorMessage(); |
477 | if (!editorMode) { |
478 | qWarning(msg: "Failed to compile fragment shader: %s\n", qPrintable(fragErr)); |
479 | if (!shaderDebug) |
480 | qWarning() << inKey << '\n'; |
481 | } |
482 | } |
483 | |
484 | if (shaderDebug) { |
485 | dumpShader(QShader::Stage::FragmentStage, fragmentCode); |
486 | if (!fragShaderValid) |
487 | dumpShaderToFile(QShader::Stage::FragmentStage, fragmentCode); |
488 | } |
489 | |
490 | if (vertShaderValid && fragShaderValid) { |
491 | shaders = std::make_shared<QSSGRhiShaderPipeline>(args&: m_rhiContext); |
492 | shaders->addStage(stage: QRhiShaderStage(QRhiShaderStage::Vertex, vertexShader), flags: stageFlags); |
493 | shaders->addStage(stage: QRhiShaderStage(QRhiShaderStage::Fragment, fragmentShader), flags: stageFlags); |
494 | if (shaderDebug) |
495 | qDebug(msg: "Compilation for vertex and fragment stages succeeded"); |
496 | } |
497 | |
498 | if (editorMode && s_statusCallback) { |
499 | using namespace QtQuick3DEditorHelpers::ShaderBaker; |
500 | const auto vertStatus = vertShaderValid ? Status::Success : Status::Error; |
501 | const auto fragStatus = fragShaderValid ? Status::Success : Status::Error; |
502 | QMutexLocker locker(&*s_statusMutex); |
503 | s_statusCallback(inKey, vertStatus, vertErr, QShader::VertexStage); |
504 | s_statusCallback(inKey, fragStatus, fragErr, QShader::FragmentStage); |
505 | } |
506 | |
507 | auto result = m_rhiShaders.insert(key: tempKey, value: shaders).value(); |
508 | if (result && result->vertexStage() && result->fragmentStage()) { |
509 | QQsbCollection::EntryDesc entryDesc = { |
510 | .materialKey: inKey, |
511 | .featureSet: QQsbCollection::toFeatureSet(ssgFeatureSet: inFeatures), |
512 | .vertShader: result->vertexStage()->shader(), |
513 | .fragShader: result->fragmentStage()->shader() |
514 | }; |
515 | m_persistentShaderBakingCache.addEntry(key: entryDesc.generateSha(), entryDesc); |
516 | } |
517 | return result; |
518 | |
519 | #else |
520 | Q_UNUSED(inKey); |
521 | Q_UNUSED(inVert); |
522 | Q_UNUSED(inFrag); |
523 | Q_UNUSED(inFeatures); |
524 | Q_UNUSED(stageFlags); |
525 | qWarning("Cannot compile and condition shaders at runtime because this build of Qt Quick 3D is not linking to Qt Shader Tools. " |
526 | "Only pre-processed materials are supported."); |
527 | return {}; |
528 | #endif |
529 | } |
530 | |
531 | QSSGRhiShaderPipelinePtr QSSGShaderCache::newPipelineFromPregenerated(const QByteArray &inKey, |
532 | const QSSGShaderFeatures &inFeatures, |
533 | QQsbCollection::Entry entry, |
534 | const QSSGRenderGraphObject &obj, |
535 | QSSGRhiShaderPipeline::StageFlags stageFlags) |
536 | { |
537 | // No lookup in m_rhiShaders. It is up to the caller to do that, if they |
538 | // want to. We will insert into it at the end, but there is intentionally |
539 | // no lookup. The result from this function is always a new |
540 | // QSSGRhiShaderPipeline (it's just much faster to create than the |
541 | // full-blown generator). That is important for some clients (effect |
542 | // system) so returning an existing QSSGRhiShaderPipeline is _wrong_. |
543 | |
544 | const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled(); |
545 | if (shaderDebug) |
546 | qDebug(msg: "Loading pregenerated rhi shader(s)"); |
547 | |
548 | Q_TRACE_SCOPE(QSSG_loadShader); |
549 | Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DLoadShader); |
550 | |
551 | // Note that we are required to return a non-null (but empty) shader set even if loading fails. |
552 | QSSGRhiShaderPipelinePtr shaders(new QSSGRhiShaderPipeline(m_rhiContext)); |
553 | |
554 | const QString collectionFile = QString::fromLatin1(ba: resourceFolder() + shaderCollectionFile()); |
555 | |
556 | QQsbIODeviceCollection qsbc(collectionFile); |
557 | QQsbCollection::EntryDesc entryDesc; |
558 | if (qsbc.map(mode: QQsbIODeviceCollection::Read)) |
559 | qsbc.extractEntry(entry, entryDesc); |
560 | else |
561 | qWarning(msg: "Failed to open entry %s", entry.key.constData()); |
562 | |
563 | if (entryDesc.vertShader.isValid() && entryDesc.fragShader.isValid()) { |
564 | shaders->addStage(stage: QRhiShaderStage(QRhiShaderStage::Vertex, entryDesc.vertShader), flags: stageFlags); |
565 | shaders->addStage(stage: QRhiShaderStage(QRhiShaderStage::Fragment, entryDesc.fragShader), flags: stageFlags); |
566 | if (shaderDebug) |
567 | qDebug(msg: "Loading of vertex and fragment stages succeeded"); |
568 | } |
569 | |
570 | #if !QT_CONFIG(qml_debug) |
571 | Q_UNUSED(obj); |
572 | #else |
573 | Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DLoadShader, 0, obj.profilingId); |
574 | #endif |
575 | |
576 | QSSGShaderCacheKey cacheKey(inKey); |
577 | cacheKey.m_features = inFeatures; |
578 | cacheKey.updateHashCode(); |
579 | |
580 | const auto inserted = m_rhiShaders.insert(key: cacheKey, value: shaders); |
581 | qsbc.unmap(); |
582 | return inserted.value(); |
583 | } |
584 | |
585 | QSSGRhiShaderPipelinePtr QSSGShaderCache::tryNewPipelineFromPersistentCache(const QByteArray &qsbcKey, |
586 | const QByteArray &inKey, |
587 | const QSSGShaderFeatures &inFeatures, |
588 | QSSGRhiShaderPipeline::StageFlags stageFlags) |
589 | { |
590 | // No lookup in m_rhiShaders. it is up to the caller to do that, if they |
591 | // want to. We will insert into it at the end, but there is intentionally |
592 | // no lookup. The result from this function is always a new |
593 | // QSSGRhiShaderPipeline (it's just much faster to create than the |
594 | // full-blown generator). That is important for some clients (effect |
595 | // system) so returning an existing QSSGRhiShaderPipeline is _wrong_. |
596 | |
597 | QQsbCollection::EntryDesc entryDesc; |
598 | |
599 | // Here we are allowed to return null to indicate that there is no such |
600 | // entry in this particular cache. |
601 | if (!m_persistentShaderBakingCache.extractEntry(entry: QQsbCollection::Entry(qsbcKey), entryDesc)) |
602 | return {}; |
603 | |
604 | if (entryDesc.vertShader.isValid() && entryDesc.fragShader.isValid()) { |
605 | const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled(); |
606 | if (shaderDebug) |
607 | qDebug(msg: "Loading rhi shaders from disk cache for %s (%s)", qsbcKey.constData(), inKey.constData()); |
608 | |
609 | QSSGRhiShaderPipelinePtr shaders(new QSSGRhiShaderPipeline(m_rhiContext)); |
610 | shaders->addStage(stage: QRhiShaderStage(QRhiShaderStage::Vertex, entryDesc.vertShader), flags: stageFlags); |
611 | shaders->addStage(stage: QRhiShaderStage(QRhiShaderStage::Fragment, entryDesc.fragShader), flags: stageFlags); |
612 | QSSGShaderCacheKey cacheKey(inKey); |
613 | cacheKey.m_features = inFeatures; |
614 | cacheKey.updateHashCode(); |
615 | return m_rhiShaders.insert(key: cacheKey, value: shaders).value(); |
616 | } |
617 | |
618 | return {}; |
619 | } |
620 | |
621 | QSSGRhiShaderPipelinePtr QSSGShaderCache::loadBuiltinUncached(const QByteArray &inKey, int viewCount) |
622 | { |
623 | const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled(); |
624 | if (shaderDebug) |
625 | qDebug(msg: "Loading builtin rhi shader: %s (view count: %d)", inKey.constData(), viewCount); |
626 | |
627 | Q_TRACE_SCOPE(QSSG_loadShader); |
628 | Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DLoadShader); |
629 | |
630 | // Note that we are required to return a non-null (but empty) shader set even if loading fails. |
631 | QSSGRhiShaderPipelinePtr shaders(new QSSGRhiShaderPipeline(m_rhiContext)); |
632 | |
633 | // inShaderName is a prefix of a .qsb file, so "abc" means we should |
634 | // look for abc.vert.qsb and abc.frag.qsb. |
635 | |
636 | const QString prefix = QString::fromUtf8(ba: resourceFolder() + inKey); |
637 | QString vertexFileName = prefix + QLatin1String(".vert.qsb"); |
638 | QString fragmentFileName = prefix + QLatin1String(".frag.qsb"); |
639 | |
640 | // This must match QSGMaterial(Shader) in Qt Quick, in particular the |
641 | // QSGMaterialShader::setShaderFileName() overload taking a viewCount. |
642 | if (viewCount == 2) { |
643 | vertexFileName += QLatin1String(".mv2qsb"); |
644 | fragmentFileName += QLatin1String(".mv2qsb"); |
645 | } |
646 | |
647 | QShader vertexShader; |
648 | QShader fragmentShader; |
649 | |
650 | QFile f; |
651 | f.setFileName(vertexFileName); |
652 | if (f.open(flags: QIODevice::ReadOnly)) { |
653 | const QByteArray vsData = f.readAll(); |
654 | vertexShader = QShader::fromSerialized(data: vsData); |
655 | f.close(); |
656 | } else { |
657 | qWarning(msg: "Failed to open %s", qPrintable(f.fileName())); |
658 | } |
659 | f.setFileName(fragmentFileName); |
660 | if (f.open(flags: QIODevice::ReadOnly)) { |
661 | const QByteArray fsData = f.readAll(); |
662 | fragmentShader = QShader::fromSerialized(data: fsData); |
663 | f.close(); |
664 | } else { |
665 | qWarning(msg: "Failed to open %s", qPrintable(f.fileName())); |
666 | } |
667 | |
668 | if (vertexShader.isValid() && fragmentShader.isValid()) { |
669 | shaders->addStage(stage: QRhiShaderStage(QRhiShaderStage::Vertex, vertexShader), flags: QSSGRhiShaderPipeline::UsedWithoutIa); |
670 | shaders->addStage(stage: QRhiShaderStage(QRhiShaderStage::Fragment, fragmentShader)); |
671 | if (shaderDebug) |
672 | qDebug(msg: "Loading of vertex and fragment stages succeeded"); |
673 | } |
674 | |
675 | Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DLoadShader, 0, inKey); |
676 | |
677 | return shaders; |
678 | } |
679 | |
680 | namespace QtQuick3DEditorHelpers { |
681 | void ShaderBaker::setStatusCallback(StatusCallback cb) |
682 | { |
683 | QMutexLocker locker(&*s_statusMutex); |
684 | s_statusCallback = cb; |
685 | } |
686 | |
687 | void ShaderCache::setAutomaticDiskCache(bool enable) |
688 | { |
689 | s_autoDiskCacheEnabled = enable; |
690 | } |
691 | |
692 | bool ShaderCache::isAutomaticDiskCacheEnabled() |
693 | { |
694 | return ::isAutoDiskCacheEnabled(); |
695 | } |
696 | |
697 | } |
698 | |
699 | QT_END_NAMESPACE |
700 |
Definitions
- s_statusCallback
- s_statusMutex
- qHash
- dumpFilename
- DefineEntry
- DefineTable
- asDefineString
- fromIndex
- set
- initBakerForNonPersistentUse
- initBakerForPersistentUse
- s_autoDiskCacheEnabled
- isAutoDiskCacheEnabled
- ensureWritableDir
- persistentQsbcDir
- persistentQsbcFileName
- QSSGShaderCache
- ~QSSGShaderCache
- releaseCachedResources
- tryGetRhiShaderPipeline
- addShaderPreprocessor
- resourceFolder
- shaderCollectionFile
- compileForRhi
- newPipelineFromPregenerated
- tryNewPipelineFromPersistentCache
- loadBuiltinUncached
- setStatusCallback
- setAutomaticDiskCache
Start learning QML with our Intro Training
Find out more