| 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 | |