Revision control
++ // Copyright (C) 2008-2012 NVIDIA Corporation.
++ // Copyright (C) 2019 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++
++ #include "graphobjects/qssgrendergraphobject_p.h"
++ #include "qssgrendershadercache_p.h"
++ #include "qssgrendercontextcore.h"
++
++ #include <QtQuick3DUtils/private/qssgutils_p.h>
++ #include <QtQuick3DUtils/private/qquick3dprofiler_p.h>
++
++ #include <QtQuick3DRuntimeRender/private/qssgruntimerenderlogging_p.h>
++ #include <qtquick3d_tracepoints_p.h>
++
++ #include <QCoreApplication>
++ #include <QStandardPaths>
++ #include <QString>
++ #include <QFile>
++ #include <QDir>
++
++ #include <QtGui/qsurfaceformat.h>
++ #if QT_CONFIG(opengl)
++ # include <QtGui/qopenglcontext.h>
++ #endif
++
++ #ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS
++ #include <rhi/qshaderbaker.h>
++ #endif
++
++ #include <QtCore/qmutex.h>
++
++ QT_BEGIN_NAMESPACE
++
++ Q_TRACE_POINT(qtquick3d, QSSG_loadShader_entry)
++ Q_TRACE_POINT(qtquick3d, QSSG_loadShader_exit)
++
++ static QtQuick3DEditorHelpers::ShaderBaker::StatusCallback s_statusCallback = nullptr;
++ Q_GLOBAL_STATIC(QMutex, s_statusMutex);
++
++ size_t qHash(QSSGShaderFeatures features) noexcept { return (features.flags & (~QSSGShaderFeatures::IndexMask)); }
++
++ static QString dumpFilename(QShader::Stage stage)
++ {
++ switch (stage) {
++ case QShader::VertexStage:
++ return QStringLiteral("failedvert.txt");
++ break;
++ case QShader::FragmentStage:
++ return QStringLiteral("failedfrag.txt");
++ break;
++ default:
++ return QStringLiteral("failedshader.txt");
++ }
++ }
++
++ struct DefineEntry
++ {
++ const char *name = nullptr;
++ QSSGShaderFeatures::Feature feature {};
++ };
++
++ static constexpr DefineEntry DefineTable[] {
++ { "QSSG_ENABLE_LIGHT_PROBE", QSSGShaderFeatures::Feature::LightProbe },
++ { "QSSG_ENABLE_IBL_ORIENTATION", QSSGShaderFeatures::Feature::IblOrientation },
++ { "QSSG_ENABLE_SSM", QSSGShaderFeatures::Feature::Ssm },
++ { "QSSG_ENABLE_SSAO", QSSGShaderFeatures::Feature::Ssao },
++ { "QSSG_ENABLE_DEPTH_PASS", QSSGShaderFeatures::Feature::DepthPass },
++ { "QSSG_ENABLE_ORTHO_SHADOW_PASS", QSSGShaderFeatures::Feature::OrthoShadowPass },
++ { "QSSG_ENABLE_PERSPECTIVE_SHADOW_PASS", QSSGShaderFeatures::Feature::PerspectiveShadowPass },
++ { "QSSG_ENABLE_LINEAR_TONEMAPPING", QSSGShaderFeatures::Feature::LinearTonemapping },
++ { "QSSG_ENABLE_ACES_TONEMAPPING", QSSGShaderFeatures::Feature::AcesTonemapping },
++ { "QSSG_ENABLE_HEJLDAWSON_TONEMAPPING", QSSGShaderFeatures::Feature::HejlDawsonTonemapping },
++ { "QSSG_ENABLE_FILMIC_TONEMAPPING", QSSGShaderFeatures::Feature::FilmicTonemapping },
++ { "QSSG_ENABLE_RGBE_LIGHT_PROBE", QSSGShaderFeatures::Feature::RGBELightProbe },
++ { "QSSG_ENABLE_OPAQUE_DEPTH_PRE_PASS", QSSGShaderFeatures::Feature::OpaqueDepthPrePass },
++ { "QSSG_ENABLE_REFLECTION_PROBE", QSSGShaderFeatures::Feature::ReflectionProbe },
++ { "QSSG_REDUCE_MAX_NUM_LIGHTS", QSSGShaderFeatures::Feature::ReduceMaxNumLights },
++ { "QSSG_ENABLE_LIGHTMAP", QSSGShaderFeatures::Feature::Lightmap },
++ { "QSSG_DISABLE_MULTIVIEW", QSSGShaderFeatures::Feature::DisableMultiView },
++ { "QSSG_FORCE_IBL_EXPOSURE", QSSGShaderFeatures::Feature::ForceIblExposure },
++ { "QSSG_ENABLE_NORMAL_PASS", QSSGShaderFeatures::Feature::NormalPass },
++ { "QSSG_ENABLE_USER_RENDER_PASS", QSSGShaderFeatures::Feature::UserRenderPass},
++ { "QSSG_ENABLE_MOTION_VECTOR", QSSGShaderFeatures::Feature::MotionVector }
++ };
++
++ static_assert(std::size(DefineTable) == QSSGShaderFeatures::Count, "Missing feature define?");
++
++ const char *QSSGShaderFeatures::asDefineString(QSSGShaderFeatures::Feature feature) { return DefineTable[static_cast<FlagType>(feature) & QSSGShaderFeatures::IndexMask].name; }
++ QSSGShaderFeatures::Feature QSSGShaderFeatures::fromIndex(quint32 idx) { return DefineTable[idx].feature; }
++
++ void QSSGShaderFeatures::set(QSSGShaderFeatures::Feature feature, bool val)
++ {
++ if (val)
++ flags |= (static_cast<FlagType>(feature) & ~IndexMask);
++ else
++ flags &= ~(static_cast<FlagType>(feature) & ~IndexMask);
++ }
++
++ QList<QShaderVersion> QSSGShaderCache::glslTargetsForContext(const QSurfaceFormat &format, bool isGLESModule)
++ {
++ QList<QShaderVersion> versions;
++
++ // QShaderVersion from QSurfaceFormat::version pair. Only valid from GLSL
++ // 330 and GLSL ES 300 up; below those the GLSL version does not follow the
++ // context version, but the callers below never get there.
++ const auto fromVersion = [](const std::pair<int, int> &version, const QShaderVersion::Flags flags = {}) -> QShaderVersion {
++ return QShaderVersion(version.first * 100 + version.second * 10, flags);
++ };
++
++ const auto version = format.version();
++
++ if (format.profile() == QSurfaceFormat::CoreProfile && version >= qMakePair(3, 3)) {
++ // Add the floor version so that silent translation errors do not leave the
++ // GLSL outputs empty
++ versions.append(QShaderVersion(330));
++ // Use actual version
++ if (version > qMakePair(3, 3))
++ versions.append(fromVersion(version));
++ } else if (format.renderableType() == QSurfaceFormat::OpenGLES || isGLESModule) {
++ if (format.majorVersion() >= 3) {
++ // Add the floor version so that silent translation errors do not leave the
++ // GLSL outputs empty
++ versions.append(QShaderVersion(300, QShaderVersion::GlslEs));
++ // Use actual version
++ if (version > qMakePair(3, 0))
++ versions.append(fromVersion(version, QShaderVersion::GlslEs));
++ } else {
++ versions.append(QShaderVersion(100, QShaderVersion::GlslEs)); // GLES 2.0
++ }
++ } else {
++ // Need to default to at least GLSL 130 (OpenGL 3.0), not 120.
++ // The difference is actually relevant when it comes to certain
++ // GLSL features (textureSize, unsigned integers, and with
++ // SPIRV-Cross even bool), and we do not have to care about
++ // pure OpenGL (non-ES) 2.x implementations in practice.
++
++ // For full feature set we need GLSL 140 (OpenGL 3.1), e.g.
++ // because of inverse() used for instancing.
++
++ // GLSL 130 should still be attempted, to support old Mesa
++ // llvmpipe that only gives us OpenGL 3.0. At the time of
++ // writing the opengl32sw.dll shipped with pre-built Qt is one
++ // of these still.
++
++ versions.append(QShaderVersion(version >= qMakePair(3, 1) ? 140 : 130));
++ if (version >= qMakePair(3, 3))
++ versions.append(fromVersion(version));
++ }
++
++ return versions;
++ }
++
++ #ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS
++ static void initBakerForNonPersistentUse(QShaderBaker *baker, QRhi *rhi)
++ {
++ QVector<QShaderBaker::GeneratedShader> outputs;
++ switch (rhi->backend()) {
++ case QRhi::D3D11:
++ outputs.append({ QShader::HlslShader, QShaderVersion(50) }); // Shader Model 5.0
++ break;
++ case QRhi::D3D12:
++ outputs.append({ QShader::HlslShader, QShaderVersion(61) }); // Shader Model 6.1 (includes multiview support)
++ break;
++ case QRhi::Metal:
++ #if defined(Q_OS_VISIONOS)
++ outputs.append({ QShader::MslShader, QShaderVersion(21) }); // Metal 2.1 (multiview support)
++ #else
++ outputs.append({ QShader::MslShader, QShaderVersion(12) }); // Metal 1.2
++ #endif // Q_OS_VISIONOS
++ break;
++ case QRhi::OpenGLES2:
++ {
++ QSurfaceFormat format = QSurfaceFormat::defaultFormat();
++ bool isGLESModule = false;
++ #if QT_CONFIG(opengl)
++ auto h = static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles());
++ if (h && h->context)
++ format = h->context->format();
++ isGLESModule = QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES;
++ #endif
++ const QList<QShaderVersion> versions = QSSGShaderCache::glslTargetsForContext(format, isGLESModule);
++ outputs.reserve(outputs.size() + versions.size());
++ for (const QShaderVersion &version : versions)
++ outputs.append({ QShader::GlslShader, version });
++ }
++ break;
++ default: // Vulkan, Null
++ outputs.append({ QShader::SpirvShader, QShaderVersion(100) });
++ break;
++ }
++
++ // Do not fail baking if one of the target fails
++ // See comment on initBakerForPersistentUse
++ baker->setBreakOnShaderTranslationError(false);
++ baker->setGeneratedShaders(outputs);
++ baker->setGeneratedShaderVariants({ QShader::StandardShader });
++ }
++
++ void QSSGShaderCache::initBakerForPersistentUse(QShaderBaker *baker, QRhi *)
++ {
++ QVector<QShaderBaker::GeneratedShader> outputs;
++ outputs.reserve(13);
++
++ #ifndef Q_OS_WASM
++ outputs.append({ QShader::SpirvShader, QShaderVersion(100) });
++ outputs.append({ QShader::HlslShader, QShaderVersion(50) }); // Shader Model 5.0
++ outputs.append({ QShader::HlslShader, QShaderVersion(61) }); // Shader Model 6.1 (for multiview on d3d12)
++ #if defined(Q_OS_VISIONOS)
++ outputs.append({ QShader::MslShader, QShaderVersion(21) }); // Metal 2.1 (multiview support)
++ #else
++ outputs.append({ QShader::MslShader, QShaderVersion(12) }); // Metal 1.2
++ #endif // Q_OS_VISIONOS
++
++ outputs.append({ QShader::GlslShader, QShaderVersion(430) }); // OpenGL 4.3+ (compute shaders, shader storage buffers)
++ outputs.append({ QShader::GlslShader, QShaderVersion(420) }); // OpenGL 4.2+
++ outputs.append({ QShader::GlslShader, QShaderVersion(330) }); // OpenGL 3.3+
++ outputs.append({ QShader::GlslShader, QShaderVersion(140) }); // OpenGL 3.1+
++ outputs.append({ QShader::GlslShader, QShaderVersion(130) }); // OpenGL 3.0+
++ outputs.append({ QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs) }); // GLES 2.0
++ #endif
++ outputs.append({ QShader::GlslShader, QShaderVersion(300, QShaderVersion::GlslEs) }); // GLES 3.0+
++ outputs.append({ QShader::GlslShader, QShaderVersion(310, QShaderVersion::GlslEs) }); // GLES 3.1+
++ outputs.append({ QShader::GlslShader, QShaderVersion(320, QShaderVersion::GlslEs) }); // GLES 3.2+ (image atomics)
++
++ // If one of the above cannot be generated due to failing at the
++ // SPIRV-Cross translation stage, it will be skipped, but bake() will not
++ // fail. This is essential, because with the default fail if anything fails
++ // behavior many shaders could not be baked at all due to failing for e.g.
++ // GLSL ES 100. This is a non-issue when choosing the targets dynamically
++ // based on the current API/context, but here we need to ensure what we
++ // generate will work with a different RHI backend, graphics API, and
++ // perhaps even on a different platform (if the cache file is manually
++ // moved). So have to generate what we can, without breaking the
++ // application when the shader is not compatible with a target. (if that
++ // shader is not used at runtime, it's fine anyway, it it is, it won't work
++ // just as with the other, non-caching path)
++ baker->setBreakOnShaderTranslationError(false);
++
++ baker->setGeneratedShaders(outputs);
++ baker->setGeneratedShaderVariants({ QShader::StandardShader });
++ }
++
++ #else
++ static void initBakerForNonPersistentUse(QShaderBaker *, QRhi *)
++ {
++ }
++
++ void QSSGShaderCache::initBakerForPersistentUse(QShaderBaker *, QRhi *)
++ {
++ }
++ #endif // QT_QUICK3D_HAS_RUNTIME_SHADERS
++
++ static bool s_autoDiskCacheEnabled = true;
++
++ static bool isAutoDiskCacheEnabled()
++ {
++ // these three mirror QOpenGLShaderProgram/QQuickGraphicsConfiguration/QSGRhiSupport
++ static const bool diskCacheDisabled = qEnvironmentVariableIntValue("QT_DISABLE_SHADER_DISK_CACHE")
++ || qEnvironmentVariableIntValue("QSG_RHI_DISABLE_DISK_CACHE");
++ const bool attrDiskCacheDisabled = (qApp ? qApp->testAttribute(Qt::AA_DisableShaderDiskCache) : false);
++ return (!diskCacheDisabled && !attrDiskCacheDisabled && s_autoDiskCacheEnabled);
++
++ }
++
++ static inline bool ensureWritableDir(const QString &name)
++ {
++ QDir::root().mkpath(name);
++ return QFileInfo(name).isWritable();
++ }
++
++ static QString persistentQsbcDir()
++ {
++ static bool checked = false;
++ static QString currentCacheDir;
++ static bool cacheWritable = false;
++
++ if (checked)
++ return cacheWritable ? currentCacheDir : QString();
++
++ checked = true;
++ const QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
++ const QString subPath = QLatin1String("/q3dshadercache-") + QSysInfo::buildAbi() + QLatin1Char('/');
++
++ if (!cachePath.isEmpty()) {
++ currentCacheDir = cachePath + subPath;
++ cacheWritable = ensureWritableDir(currentCacheDir);
++ }
++
++ return cacheWritable ? currentCacheDir : QString();
++ }
++
++ static inline QString persistentQsbcFileName()
++ {
++ const QString cacheDir = persistentQsbcDir();
++ if (!cacheDir.isEmpty())
++ return cacheDir + QLatin1String("q3dshadercache.qsbc");
++
++ return QString();
++ }
++
++ QSSGShaderCache::QSSGShaderCache(QSSGRhiContext &ctx,
++ const InitBakerFunc initBakeFn)
++ : m_rhiContext(ctx),
++ m_initBaker(initBakeFn),
++ m_builtInShaders(*this)
++ {
++ if (isAutoDiskCacheEnabled()) {
++ const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled();
++ m_persistentShaderStorageFileName = persistentQsbcFileName();
++ if (!m_persistentShaderStorageFileName.isEmpty()) {
++ const bool skipCacheFile = qEnvironmentVariableIntValue("QT_QUICK3D_NO_SHADER_CACHE_LOAD");
++ if (!skipCacheFile && QFileInfo::exists(m_persistentShaderStorageFileName)) {
++ if (shaderDebug)
++ qDebug("Attempting to seed material shader cache from %s", qPrintable(m_persistentShaderStorageFileName));
++ if (m_persistentShaderBakingCache.load(m_persistentShaderStorageFileName)) {
++ if (shaderDebug) {
++ const int count = m_persistentShaderBakingCache.availableEntries().count();
++ qDebug("Loaded %d shader pipelines into the material shader cache", count);
++ }
++ }
++ }
++ }
++ }
++
++ if (!m_initBaker) {
++ // It is important to generate all possible shader variants if the qsb
++ // collection is going to be stored on disk. Otherwise switching the
++ // rhi backend could break the application. This is however an overkill
++ // if we know that what we bake will not be reused in future runs of
++ // the application, so do not do it if the disk cache was disabled or
++ // the cache directory was not available (no file system, no
++ // permissions, etc.).
++ m_initBaker = m_persistentShaderStorageFileName.isEmpty() ? initBakerForNonPersistentUse
++ : QSSGShaderCache::initBakerForPersistentUse;
++ }
++ }
++
++ QSSGShaderCache::~QSSGShaderCache()
++ {
++ if (!m_persistentShaderStorageFileName.isEmpty())
++ m_persistentShaderBakingCache.save(m_persistentShaderStorageFileName);
++ }
++
++ void QSSGShaderCache::releaseCachedResources()
++ {
++ m_builtInShaders.releaseCachedResources();
++
++ m_rhiShaders.clear();
++
++ // m_persistentShaderBakingCache is not cleared, that is intentional,
++ // otherwise we would permanently lose what got loaded at startup.
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGShaderCache::tryGetRhiShaderPipeline(const QByteArray &inKey,
++ const QSSGShaderFeatures &inFeatures)
++ {
++ QSSGShaderCacheKey cacheKey(inKey);
++ cacheKey.m_features = inFeatures;
++ cacheKey.updateHashCode();
++ const auto theIter = m_rhiShaders.constFind(cacheKey);
++ if (theIter != m_rhiShaders.cend())
++ return theIter.value();
++ return nullptr;
++ }
++
++
++ void QSSGShaderCache::addShaderPreprocessor(QByteArray &str,
++ const QByteArray &inKey,
++ ShaderType shaderType,
++ const QSSGShaderFeatures &inFeatures,
++ const QSSGUserShaderAugmentation &shaderAugmentation,
++ int viewCount)
++ {
++ m_insertStr.clear();
++
++ m_insertStr += "#version 440\n";
++
++ if (!inKey.isNull()) {
++ m_insertStr += "//Shader name -";
++ m_insertStr += inKey;
++ m_insertStr += "\n";
++ }
++
++ m_insertStr += "#define texture2D texture\n";
++
++ // match Qt Quick and QSGMaterial(Shader)
++ m_insertStr += "#define QSHADER_VIEW_COUNT ";
++ m_insertStr += QByteArray::number(viewCount);
++ m_insertStr += "\n";
++
++ str.insert(0, m_insertStr);
++ QString::size_type insertPos = int(m_insertStr.size());
++
++ m_insertStr.clear();
++ const bool fragOutputEnabled = (!inFeatures.isSet(QSSGShaderFeatures::Feature::DepthPass)) && shaderType == ShaderType::Fragment;
++ for (const auto &def : DefineTable) {
++ m_insertStr.append("#define ");
++ m_insertStr.append(def.name);
++ m_insertStr.append(" ");
++ m_insertStr.append(inFeatures.isSet(def.feature) ? "1" : "0");
++ m_insertStr.append("\n");
++ }
++
++ str.insert(insertPos, m_insertStr);
++ insertPos += int(m_insertStr.size());
++
++ m_insertStr.clear();
++ if (fragOutputEnabled) {
++ if (shaderAugmentation.outputs.size() != 0) {
++ const auto &outputs = shaderAugmentation.outputs;
++ for (int i = 0; i < outputs.size(); ++i) {
++ m_insertStr += "layout(location = ";
++ m_insertStr += QByteArray::number(i);
++ m_insertStr += ") out vec4 ";
++ m_insertStr += outputs[i];
++ m_insertStr += ";\n";
++ }
++ } else {
++ m_insertStr += "layout(location = 0) out vec4 fragOutput;\n";
++ }
++ }
++
++ str.insert(insertPos, m_insertStr);
++ }
++
++ QByteArray QSSGShaderCache::resourceFolder()
++ {
++ return QByteArrayLiteral(":/res/rhishaders/");
++ }
++
++ QByteArray QSSGShaderCache::shaderCollectionFile()
++ {
++ return QByteArrayLiteral("qtappshaders.qsbc");
++ }
++
++ QByteArray QSSGShaderCache::particleShaderCollectionFile()
++ {
++ return QByteArrayLiteral("qtparticleshaders.qsbc");
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGShaderCache::compileForRhi(const QByteArray &inKey, const QByteArray &inVert, const QByteArray &inFrag,
++ const QSSGShaderFeatures &inFeatures, QSSGRhiShaderPipeline::StageFlags stageFlags,
++ const QSSGUserShaderAugmentation &shaderAugmentation,
++ int viewCount,
++ bool perTargetCompilation,
++ const QByteArray &inCacheKey)
++ {
++ #ifdef QT_QUICK3D_HAS_RUNTIME_SHADERS
++ // inKey is also used as the shader name in a comment in the generated
++ // source, so it may not contain everything needed to identify the
++ // shaders. When inCacheKey is set, that is what the caches are keyed on.
++ const QByteArray &cacheKeyString = inCacheKey.isEmpty() ? inKey : inCacheKey;
++
++ const QSSGRhiShaderPipelinePtr &rhiShaders = tryGetRhiShaderPipeline(cacheKeyString, inFeatures);
++ if (rhiShaders)
++ return rhiShaders;
++
++ QSSGShaderCacheKey tempKey(cacheKeyString);
++ tempKey.m_features = inFeatures;
++ tempKey.updateHashCode();
++
++ QByteArray vertexCode = inVert;
++ QByteArray fragmentCode = inFrag;
++
++ if (!vertexCode.isEmpty())
++ addShaderPreprocessor(vertexCode, inKey, ShaderType::Vertex, inFeatures, shaderAugmentation, viewCount);
++
++ if (!fragmentCode.isEmpty())
++ addShaderPreprocessor(fragmentCode, inKey, ShaderType::Fragment, inFeatures, shaderAugmentation, viewCount);
++
++ // lo and behold the final shader strings are ready
++
++ QSSGRhiShaderPipelinePtr shaders;
++ QString vertErr, fragErr;
++
++ QShaderBaker baker;
++ m_initBaker(&baker, m_rhiContext.rhi());
++
++ // If requested, per-target compilation allows doing things like #if
++ // QSHADER_HLSL in the shader code, at the expense of spending more time in
++ // bake())
++ baker.setPerTargetCompilation(perTargetCompilation);
++
++ // This is in the shader key, but cannot query that here anymore now that it's serialized.
++ // So we get it as a dedicated argument.
++ baker.setMultiViewCount(viewCount);
++
++ // For fragment shaders for GLSL ES (but only ES) we can make the generated
++ // sources contain 'precision mediump float' instead of 'precision highp float'.
++ const bool mediumPrecision = qEnvironmentVariableIntValue("QT_QUICK3D_MEDIUM_PRECISION");
++ if (mediumPrecision)
++ baker.setGlslOptions(QShaderBaker::GlslOption::GlslEsFragDefaultFloatPrecisionMedium);
++
++ const bool editorMode = QSSGRhiContextPrivate::editorMode();
++ // Shader debug is disabled in editor mode
++ const bool shaderDebug = !editorMode && QSSGRhiContextPrivate::shaderDebuggingEnabled();
++
++ static auto dumpShader = [](QShader::Stage stage, const QByteArray &code) {
++ switch (stage) {
++ case QShader::Stage::VertexStage:
++ qDebug("VERTEX SHADER:\n*****\n");
++ break;
++ case QShader::Stage::FragmentStage:
++ qDebug("FRAGMENT SHADER:\n*****\n");
++ break;
++ default:
++ qDebug("SHADER:\n*****\n");
++ break;
++ }
++ const auto lines = code.split('\n');
++ for (int i = 0; i < lines.size(); i++)
++ qDebug("%3d %s", i + 1, lines.at(i).constData());
++ qDebug("\n*****\n");
++ };
++
++ static auto dumpShaderToFile = [](QShader::Stage stage, const QByteArray &data) {
++ QFile f(dumpFilename(stage));
++ if (f.open(QIODevice::WriteOnly | QIODevice::Text)) {
++ f.write(data);
++ } else {
++ qWarning("Failed to write to file: %s (%s)\n",
++ qPrintable(f.fileName()), qPrintable(f.errorString()));
++ }
++ };
++
++ baker.setSourceString(vertexCode, QShader::VertexStage);
++ QShader vertexShader = baker.bake();
++ const auto vertShaderValid = vertexShader.isValid();
++ if (!vertShaderValid) {
++ vertErr = baker.errorMessage();
++ if (!editorMode) {
++ qWarning("Failed to compile vertex shader: %s\n", qPrintable(vertErr));
++ if (!shaderDebug)
++ qWarning() << inKey << '\n';
++ }
++ }
++
++ if (shaderDebug) {
++ dumpShader(QShader::Stage::VertexStage, vertexCode);
++ if (!vertShaderValid)
++ dumpShaderToFile(QShader::Stage::VertexStage, vertexCode);
++ }
++
++ baker.setSourceString(fragmentCode, QShader::FragmentStage);
++ QShader fragmentShader = baker.bake();
++ const bool fragShaderValid = fragmentShader.isValid();
++ if (!fragShaderValid) {
++ fragErr = baker.errorMessage();
++ if (!editorMode) {
++ qWarning("Failed to compile fragment shader: %s\n", qPrintable(fragErr));
++ if (!shaderDebug)
++ qWarning() << inKey << '\n';
++ }
++ }
++
++ if (shaderDebug) {
++ dumpShader(QShader::Stage::FragmentStage, fragmentCode);
++ if (!fragShaderValid)
++ dumpShaderToFile(QShader::Stage::FragmentStage, fragmentCode);
++ }
++
++ if (vertShaderValid && fragShaderValid) {
++ shaders = std::make_shared<QSSGRhiShaderPipeline>(m_rhiContext);
++ shaders->addStage(QRhiShaderStage(QRhiShaderStage::Vertex, vertexShader), stageFlags);
++ shaders->addStage(QRhiShaderStage(QRhiShaderStage::Fragment, fragmentShader), stageFlags);
++ if (shaderDebug)
++ qDebug("Compilation for vertex and fragment stages succeeded");
++ }
++
++ if (editorMode && s_statusCallback) {
++ using namespace QtQuick3DEditorHelpers::ShaderBaker;
++ const auto vertStatus = vertShaderValid ? Status::Success : Status::Error;
++ const auto fragStatus = fragShaderValid ? Status::Success : Status::Error;
++ QMutexLocker locker(&*s_statusMutex);
++ s_statusCallback(inKey, vertStatus, vertErr, QShader::VertexStage);
++ s_statusCallback(inKey, fragStatus, fragErr, QShader::FragmentStage);
++ }
++
++ auto result = m_rhiShaders.insert(tempKey, shaders).value();
++ if (result && result->vertexStage() && result->fragmentStage()) {
++ QQsbCollection::EntryDesc entryDesc = {
++ cacheKeyString,
++ QQsbCollection::toFeatureSet(inFeatures),
++ result->vertexStage()->shader(),
++ result->fragmentStage()->shader()
++ };
++ m_persistentShaderBakingCache.addEntry(entryDesc.generateSha(), entryDesc);
++ }
++ return result;
++
++ #else
++ Q_UNUSED(inKey);
++ Q_UNUSED(inVert);
++ Q_UNUSED(inFrag);
++ Q_UNUSED(inFeatures);
++ Q_UNUSED(stageFlags);
++ Q_UNUSED(inCacheKey);
++ qWarning("Cannot compile and condition shaders at runtime because this build of Qt Quick 3D is not linking to Qt Shader Tools. "
++ "Only pre-processed materials are supported.");
++ return {};
++ #endif
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGShaderCache::newPipelineFromPregenerated(const QByteArray &inKey,
++ const QSSGShaderFeatures &inFeatures,
++ QQsbCollection::Entry entry,
++ const QSSGRenderGraphObject &obj,
++ QSSGRhiShaderPipeline::StageFlags stageFlags)
++ {
++ // No lookup in m_rhiShaders. It is up to the caller to do that, if they
++ // want to. We will insert into it at the end, but there is intentionally
++ // no lookup. The result from this function is always a new
++ // QSSGRhiShaderPipeline (it's just much faster to create than the
++ // full-blown generator). That is important for some clients (effect
++ // system) so returning an existing QSSGRhiShaderPipeline is _wrong_.
++
++ const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled();
++ if (shaderDebug)
++ qDebug("Loading pregenerated rhi shader(s)");
++
++ Q_TRACE_SCOPE(QSSG_loadShader);
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DLoadShader);
++
++ // Note that we are required to return a non-null (but empty) shader set even if loading fails.
++ QSSGRhiShaderPipelinePtr shaders(new QSSGRhiShaderPipeline(m_rhiContext));
++
++ const QStringList collectionFiles
++ = {QString::fromLatin1(resourceFolder() + shaderCollectionFile()),
++ QString::fromLatin1(resourceFolder() + particleShaderCollectionFile())};
++
++ QQsbCollection::EntryDesc entryDesc;
++ QScopedPointer<QQsbIODeviceCollection> qsbc;
++ for (const auto &collectionFile : collectionFiles) {
++ QFileInfo info(collectionFile);
++ if (!info.exists())
++ continue;
++
++ qsbc.reset(new QQsbIODeviceCollection(collectionFile));
++ if (qsbc->map(QQsbIODeviceCollection::Read)) {
++ if (qsbc->extractEntry(entry, entryDesc))
++ break;
++ else
++ qsbc.reset();
++ }
++ }
++
++ if (qsbc.isNull())
++ qWarning("Failed to open entry %s", entry.key.constData());
++
++ if (entryDesc.vertShader.isValid() && entryDesc.fragShader.isValid()) {
++ shaders->addStage(QRhiShaderStage(QRhiShaderStage::Vertex, entryDesc.vertShader), stageFlags);
++ shaders->addStage(QRhiShaderStage(QRhiShaderStage::Fragment, entryDesc.fragShader), stageFlags);
++ if (shaderDebug)
++ qDebug("Loading of vertex and fragment stages succeeded");
++ }
++
++ #if !QT_CONFIG(qml_debug)
++ Q_UNUSED(obj);
++ #else
++ Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DLoadShader, 0, obj.profilingId);
++ #endif
++
++ QSSGShaderCacheKey cacheKey(inKey);
++ cacheKey.m_features = inFeatures;
++ cacheKey.updateHashCode();
++
++ const auto inserted = m_rhiShaders.insert(cacheKey, shaders);
++ if (qsbc)
++ qsbc->unmap();
++ return inserted.value();
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGShaderCache::tryNewPipelineFromPersistentCache(const QByteArray &qsbcKey,
++ const QByteArray &inKey,
++ const QSSGShaderFeatures &inFeatures,
++ QSSGRhiShaderPipeline::StageFlags stageFlags)
++ {
++ // No lookup in m_rhiShaders. it is up to the caller to do that, if they
++ // want to. We will insert into it at the end, but there is intentionally
++ // no lookup. The result from this function is always a new
++ // QSSGRhiShaderPipeline (it's just much faster to create than the
++ // full-blown generator). That is important for some clients (effect
++ // system) so returning an existing QSSGRhiShaderPipeline is _wrong_.
++
++ QQsbCollection::EntryDesc entryDesc;
++
++ // Here we are allowed to return null to indicate that there is no such
++ // entry in this particular cache.
++ if (!m_persistentShaderBakingCache.extractEntry(QQsbCollection::Entry(qsbcKey), entryDesc))
++ return {};
++
++ if (entryDesc.vertShader.isValid() && entryDesc.fragShader.isValid()) {
++ const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled();
++ if (shaderDebug)
++ qDebug("Loading rhi shaders from disk cache for %s (%s)", qsbcKey.constData(), inKey.constData());
++
++ QSSGRhiShaderPipelinePtr shaders(new QSSGRhiShaderPipeline(m_rhiContext));
++ shaders->addStage(QRhiShaderStage(QRhiShaderStage::Vertex, entryDesc.vertShader), stageFlags);
++ shaders->addStage(QRhiShaderStage(QRhiShaderStage::Fragment, entryDesc.fragShader), stageFlags);
++ QSSGShaderCacheKey cacheKey(inKey);
++ cacheKey.m_features = inFeatures;
++ cacheKey.updateHashCode();
++ return m_rhiShaders.insert(cacheKey, shaders).value();
++ }
++
++ return {};
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGShaderCache::loadBuiltinUncached(const QByteArray &inKey, int viewCount,
++ QSSGRhiShaderPipeline::StageFlags vertexStageFlags)
++ {
++ const bool shaderDebug = !QSSGRhiContextPrivate::editorMode() && QSSGRhiContextPrivate::shaderDebuggingEnabled();
++ if (shaderDebug)
++ qDebug("Loading builtin rhi shader: %s (view count: %d)", inKey.constData(), viewCount);
++
++ Q_TRACE_SCOPE(QSSG_loadShader);
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DLoadShader);
++
++ // Note that we are required to return a non-null (but empty) shader set even if loading fails.
++ QSSGRhiShaderPipelinePtr shaders(new QSSGRhiShaderPipeline(m_rhiContext));
++
++ // inShaderName is a prefix of a .qsb file, so "abc" means we should
++ // look for abc.vert.qsb and abc.frag.qsb.
++
++ const QString prefix = QString::fromUtf8(resourceFolder() + inKey);
++ QString vertexFileName = prefix + QLatin1String(".vert.qsb");
++ QString fragmentFileName = prefix + QLatin1String(".frag.qsb");
++
++ // This must match QSGMaterial(Shader) in Qt Quick, in particular the
++ // QSGMaterialShader::setShaderFileName() overload taking a viewCount.
++ if (viewCount == 2) {
++ vertexFileName += QLatin1String(".mv2qsb");
++ fragmentFileName += QLatin1String(".mv2qsb");
++ }
++
++ QShader vertexShader;
++ QShader fragmentShader;
++
++ QFile f;
++ f.setFileName(vertexFileName);
++ if (f.open(QIODevice::ReadOnly)) {
++ const QByteArray vsData = f.readAll();
++ vertexShader = QShader::fromSerialized(vsData);
++ f.close();
++ } else {
++ qWarning("Failed to open %s", qPrintable(f.fileName()));
++ }
++ f.setFileName(fragmentFileName);
++ if (f.open(QIODevice::ReadOnly)) {
++ const QByteArray fsData = f.readAll();
++ fragmentShader = QShader::fromSerialized(fsData);
++ f.close();
++ } else {
++ qWarning("Failed to open %s", qPrintable(f.fileName()));
++ }
++
++ if (vertexShader.isValid() && fragmentShader.isValid()) {
++ shaders->addStage(QRhiShaderStage(QRhiShaderStage::Vertex, vertexShader), vertexStageFlags);
++ shaders->addStage(QRhiShaderStage(QRhiShaderStage::Fragment, fragmentShader));
++ if (shaderDebug)
++ qDebug("Loading of vertex and fragment stages succeeded");
++ }
++
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DLoadShader, 0, inKey);
++
++ return shaders;
++ }
++
++ namespace QtQuick3DEditorHelpers {
++ void ShaderBaker::setStatusCallback(StatusCallback cb)
++ {
++ QMutexLocker locker(&*s_statusMutex);
++ s_statusCallback = cb;
++ }
++
++ void ShaderCache::setAutomaticDiskCache(bool enable)
++ {
++ s_autoDiskCacheEnabled = enable;
++ }
++
++ bool ShaderCache::isAutomaticDiskCacheEnabled()
++ {
++ return ::isAutoDiskCacheEnabled();
++ }
++
++ }
++
++ QT_END_NAMESPACE