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