Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qssgrendercontextcore.h"
++ #include <QtQuick3DRuntimeRender/private/qssgrenderskymaterial_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendererimplshaders_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrhieffectsystem_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgshadermaterialadapter_p.h>
++
++ #include <QtCore/qhash.h>
++ #include <QtCore/qspan.h>
++ #include <QtGui/qvector4d.h>
++ #include <rhi/qshaderbaker.h>
++
++ #include <algorithm>
++
++ using namespace Qt::StringLiterals;
++
++ QT_BEGIN_NAMESPACE
++
++ static const char *vertexShaderStr = R"(
++ void main()
++ {
++ vec4 qt_vertPosition = vec4(attr_pos, 1.0);
++ qt_eyeDir = qt_vertPosition.xyz;
++ gl_Position = qt_projectionMatrix * qt_viewMatrix * vec4(qt_eyeDir, 1.0);
++ }
++
++ )";
++
++ // Screen-space variant: the sky shader is evaluated directly on a fullscreen quad
++ // for the visible background (no cubemap round-trip). The per-pixel world-space view
++ // direction is reconstructed exactly like skybox.vert: inverse-project the NDC position
++ // into view space, then rotate into world space. qt_viewMatrix here carries the combined
++ // orientation * world-rotation (translation-free), so a single matrix suffices.
++ static const char *vertexShaderStrScreen = R"(
++ void main()
++ {
++ gl_Position = vec4(attr_pos, 1.0);
++ #if QSHADER_VIEW_COUNT >= 2
++ vec3 qt_unprojected = (qt_inverseProjection[gl_ViewIndex] * gl_Position).xyz;
++ qt_eyeDir = (qt_viewMatrix[gl_ViewIndex] * vec4(qt_unprojected, 0.0)).xyz;
++ #else
++ vec3 qt_unprojected = (qt_inverseProjection * gl_Position).xyz;
++ qt_eyeDir = (qt_viewMatrix * vec4(qt_unprojected, 0.0)).xyz;
++ #endif
++ gl_Position.y *= qt_adjustY;
++ }
++
++ )";
++
++ static const char *mainFragmentSnippet = R"(
++
++ void main()
++ {
++ qt_customMain();
++ }
++
++ )";
++
++ // Screen-space (background) fragment main: the user shader writes linear HDR into
++ // FRAGCOLOR, so we apply exposure + tonemapping here, exactly as skybox.frag does when
++ // it samples the IBL cube. qt_exposure / qt_tonemap are no-ops unless a tonemapping
++ // feature is compiled in (TonemapMode::None/Custom -> passthrough), matching the skybox.
++ static const char *mainFragmentSnippetScreen = R"(
++
++ void main()
++ {
++ qt_customMain();
++ FRAGCOLOR = vec4(qt_tonemap(qt_exposure(FRAGCOLOR.rgb, qt_skyExposure)), 1.0);
++ }
++
++ )";
++
++ static const char *debugFragStr = R"(
++ // Helper: outer square border of a face
++ float line(vec2 uv, float width)
++ {
++ vec2 d = abs(uv - 0.5);
++ return step(max(d.x, d.y), 0.5) - step(max(d.x, d.y), 0.5 - width);
++ }
++
++ // Simple plus/minus symbol
++ float drawPlus(vec2 uv)
++ {
++ float h = step(abs(uv.y - 0.5), 0.05) * step(abs(uv.x - 0.5), 0.2);
++ float v = step(abs(uv.x - 0.5), 0.05) * step(abs(uv.y - 0.5), 0.2);
++ return max(h, v);
++ }
++
++ float drawMinus(vec2 uv)
++ {
++ return step(abs(uv.y - 0.5), 0.05) * step(abs(uv.x - 0.5), 0.2);
++ }
++
++ void MAIN()
++ {
++ vec3 d = normalize(qt_eyeDir);
++ vec3 ad = abs(d);
++
++ vec2 uv;
++ vec3 faceColor;
++ float label = 0.0;
++
++ // Determine dominant axis (cubemap face)
++ if (ad.x > ad.y && ad.x > ad.z)
++ {
++ uv = d.zy / ad.x * 0.5 + 0.5;
++ if (d.x > 0.0)
++ {
++ faceColor = vec3(1.0, 0.0, 0.0); // +X red
++ label = drawPlus(uv);
++ }
++ else
++ {
++ faceColor = vec3(0.5, 0.0, 0.0); // -X dark red
++ label = drawMinus(uv);
++ }
++ }
++ else if (ad.y > ad.x && ad.y > ad.z)
++ {
++ uv = d.xz / ad.y * 0.5 + 0.5;
++ if (d.y > 0.0)
++ {
++ faceColor = vec3(0.0, 1.0, 0.0); // +Y green
++ label = drawPlus(uv);
++ }
++ else
++ {
++ faceColor = vec3(0.0, 0.5, 0.0); // -Y dark green
++ label = drawMinus(uv);
++ }
++ }
++ else
++ {
++ uv = d.xy / ad.z * 0.5 + 0.5;
++ if (d.z > 0.0)
++ {
++ faceColor = vec3(0.0, 0.0, 1.0); // +Z blue
++ label = drawPlus(uv);
++ }
++ else
++ {
++ faceColor = vec3(0.0, 0.0, 0.5); // -Z dark blue
++ label = drawMinus(uv);
++ }
++ }
++
++ // Outer border
++ float border = line(uv, 0.02);
++
++ // Combine
++ vec3 color = faceColor;
++ color = mix(color, vec3(1.0), border); // white border
++ color = mix(color, vec3(1.0), label); // white + or - label
++
++ FRAGCOLOR = vec4(color, 1.0);
++ }
++ )";
++
++ QSSGRenderSkyMaterial::QSSGRenderSkyMaterial()
++ : QSSGRenderGraphObject(QSSGRenderGraphObject::Type::SkyMaterial, FlagT(Flags::HasGraphicsResources))
++ {
++ }
++
++ QSSGRenderSkyMaterial::~QSSGRenderSkyMaterial() = default;
++
++ // Returns false if any custom-property texture is not ready yet (compilation must wait
++ // until next frame, since the GLSL sampler type depends on the actual texture: textures
++ // not created yet are assumed to be sampler2DArray, which could cause compile failures).
++ static bool skyShaderTexturesReady(const QList<QSSGBaseTypeProperty> &propertyUniforms, QSSGBufferManager &bufferManager)
++ {
++ for (const auto &u : std::as_const(propertyUniforms)) {
++ if (u.shaderDataType == QSSGRenderShaderValue::Texture) {
++ QSSGRenderImage *image = u.value.value<QSSGRenderImage *>();
++ const QSSGRenderImageTexture texture = image ? bufferManager.loadRenderImage(image) : QSSGRenderImageTexture { };
++ if (!image || !texture.m_texture)
++ return false;
++ }
++ }
++ return true;
++ }
++
++ // Appends the user shader's custom-property texture bindings to an SRB list. Shared by the
++ // cube (updateUniforms) and screen-space (updateBackgroundUniforms) paths.
++ static void appendCustomTextureBindings(QSSGRhiShaderPipeline *pipeline,
++ QSSGRhiShaderResourceBindingList &bindings,
++ QSSGRhiContext *rhiCtx)
++ {
++ int maxSamplerBinding = -1;
++ QVector<QShaderDescription::InOutVariable> samplerVars =
++ pipeline->fragmentStage()->shader().description().combinedImageSamplers();
++ for (const QShaderDescription::InOutVariable &var :
++ pipeline->vertexStage()->shader().description().combinedImageSamplers()) {
++ auto it = std::find_if(samplerVars.cbegin(), samplerVars.cend(), [&var](const QShaderDescription::InOutVariable &v) {
++ return var.binding == v.binding;
++ });
++ if (it == samplerVars.cend())
++ samplerVars.append(var);
++ }
++ for (const QShaderDescription::InOutVariable &var : std::as_const(samplerVars))
++ maxSamplerBinding = qMax(maxSamplerBinding, var.binding);
++
++ if (maxSamplerBinding < 0)
++ return;
++
++ const int customTexCount = pipeline->extraTextureCount();
++ for (int i = 0; i < customTexCount; ++i) {
++ const QSSGRhiTexture &t(pipeline->extraTextureAt(i));
++ const int samplerBinding = pipeline->bindingForTexture(t.name);
++ if (samplerBinding >= 0) {
++ QRhiSampler *sampler = rhiCtx->sampler(t.samplerDesc);
++ bindings.addTexture(samplerBinding, QRhiShaderResourceBinding::FragmentStage, t.texture, sampler);
++ }
++ }
++ }
++
++ // Compiles a sky pipeline from the shared user fragment shader and a given vertex
++ // variant. The user fragment is identical for the cube (IBL) and screen (background)
++ // paths since it only reads qt_eyeDir; the stages differ in the vertex code and uniforms
++ // and in the fragment main snippet (the screen path tonemaps the linear output for
++ // display). cacheKeyTag keeps the variants from aliasing in the shader cache.
++ QSSGRhiShaderPipelinePtr QSSGRenderSkyMaterial::buildPipeline(const QSSGRenderContextInterface &sgContext,
++ QByteArray vertexShader,
++ const QSSGShaderCustomMaterialAdapter::StringPairList &vertexViewDependentUniforms,
++ const QSSGShaderCustomMaterialAdapter::StringPairList &vertexUniforms,
++ const QByteArray &fragmentMainSnippet,
++ const QSSGShaderCustomMaterialAdapter::StringPairList &fragmentUniforms,
++ const QSSGShaderFeatures &features,
++ int viewCount,
++ const QByteArray &cacheKeyTag)
++ {
++ const bool multiViewCompatible = viewCount >= 2;
++
++ QByteArray fragmentShader = (!fragmentShaderSource.isEmpty() ? fragmentShaderSource : QByteArray(debugFragStr)) + fragmentMainSnippet;
++
++ QSSGShaderCustomMaterialAdapter::StringPairList propertyBaseUniforms;
++ for (const auto &u : std::as_const(propertyUniforms))
++ propertyBaseUniforms.append({ u.typeName, u.name });
++
++ QSSGShaderCustomMaterialAdapter::StringPairList inputOutputs;
++ inputOutputs.append({ "vec3", "qt_eyeDir"_ba });
++
++ {
++ QSSGShaderCustomMaterialAdapter::StringPairList vertexBaseUniforms = propertyBaseUniforms;
++ vertexBaseUniforms.append(vertexUniforms.constData(), vertexUniforms.size());
++
++ QSSGShaderCustomMaterialAdapter::ShaderCodeAndMetaData result;
++ QByteArray buf;
++ QSSGShaderCustomMaterialAdapter::CustomShaderPrepWorkData scratch;
++ QSSGShaderCustomMaterialAdapter::beginPrepareCustomShader(&scratch, &result, vertexShader, QSSGShaderCache::ShaderType::Vertex, multiViewCompatible);
++ QSSGShaderCustomMaterialAdapter::finishPrepareCustomShader(&buf,
++ scratch,
++ result,
++ QSSGShaderCache::ShaderType::Vertex,
++ multiViewCompatible,
++ vertexBaseUniforms,
++ { },
++ inputOutputs,
++ { },
++ vertexViewDependentUniforms);
++ vertexShader = result.first;
++ vertexShader.append(buf);
++ }
++
++ {
++ // Fragment uniforms (e.g. qt_skyExposure) are view-independent.
++ QSSGShaderCustomMaterialAdapter::StringPairList fragmentBaseUniforms = propertyBaseUniforms;
++ fragmentBaseUniforms.append(fragmentUniforms.constData(), fragmentUniforms.size());
++
++ QSSGShaderCustomMaterialAdapter::ShaderCodeAndMetaData result;
++ QByteArray buf;
++ QSSGShaderCustomMaterialAdapter::CustomShaderPrepWorkData scratch;
++ QSSGShaderCustomMaterialAdapter::beginPrepareCustomShader(&scratch, &result, fragmentShader, QSSGShaderCache::ShaderType::Fragment, multiViewCompatible);
++ QSSGShaderCustomMaterialAdapter::finishPrepareCustomShader(&buf,
++ scratch,
++ result,
++ QSSGShaderCache::ShaderType::Fragment,
++ multiViewCompatible,
++ fragmentBaseUniforms,
++ inputOutputs,
++ { },
++ { },
++ { });
++ fragmentShader = result.first;
++ fragmentShader.append(buf);
++ }
++
++ auto generator = sgContext.shaderProgramGenerator().get();
++ auto shaderLib = sgContext.shaderLibraryManager().get();
++ auto shaderCache = sgContext.shaderCache().get();
++
++ generator->beginProgram();
++ auto vertex = generator->getStage(QSSGShaderGeneratorStage::Vertex);
++ vertex->addIncoming("attr_pos"_ba, "vec3"_ba);
++ vertex->append(vertexShader);
++
++ auto fragment = generator->getStage(QSSGShaderGeneratorStage::Fragment);
++ fragment->addInclude("tonemapping.glsllib"_ba);
++ fragment->append(fragmentShader);
++
++ const QByteArray key = shaderPathKey + cacheKeyTag + ':'
++ + QCryptographicHash::hash(QByteArray(vertexShader + fragmentShader), QCryptographicHash::Algorithm::Sha1).toHex();
++ return generator->compileGeneratedRhiShader(key, features, *shaderLib, *shaderCache, QSSGRhiShaderPipeline::UsedWithoutIa, { }, viewCount, false);
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGRenderSkyMaterial::ensurePipeline(const QSSGRenderContextInterface &sgContext)
++ {
++ if (iblPassPipeline && !isFragmentShaderDirty)
++ return iblPassPipeline;
++
++ if (!skyShaderTexturesReady(propertyUniforms, *sgContext.bufferManager().get()))
++ return nullptr;
++
++ QSSGShaderCustomMaterialAdapter::StringPairList vertexViewDependentUniforms;
++ vertexViewDependentUniforms.append({ "mat4"_ba, "qt_projectionMatrix"_ba });
++ vertexViewDependentUniforms.append({ "mat4"_ba, "qt_viewMatrix"_ba });
++
++ // The cube render stores linear radiance for IBL, so no tonemapping is applied
++ // (AcesTonemapping is set only to satisfy the tonemapping.glsllib include; the cube
++ // fragment never calls qt_tonemap).
++ QSSGShaderFeatures features;
++ features.set(QSSGShaderFeatures::Feature::AcesTonemapping, true);
++
++ iblPassPipeline = buildPipeline(sgContext, vertexShaderStr, vertexViewDependentUniforms, { }, mainFragmentSnippet, { }, features, 1, QByteArrayLiteral(":cube"));
++
++ isFragmentShaderDirty = false;
++
++ return iblPassPipeline;
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGRenderSkyMaterial::ensureBackgroundPipeline(const QSSGRenderContextInterface &sgContext,
++ const QSSGShaderFeatures &tonemapFeatures,
++ quint32 tonemapKey,
++ int viewCount)
++ {
++ if (backgroundPipeline && !isBackgroundShaderDirty && m_backgroundTonemapKey == tonemapKey
++ && m_backgroundViewCount == viewCount) {
++ return backgroundPipeline;
++ }
++
++ if (!skyShaderTexturesReady(propertyUniforms, *sgContext.bufferManager().get()))
++ return nullptr;
++
++ QSSGShaderCustomMaterialAdapter::StringPairList vertexViewDependentUniforms;
++ vertexViewDependentUniforms.append({ "mat4"_ba, "qt_inverseProjection"_ba });
++ vertexViewDependentUniforms.append({ "mat4"_ba, "qt_viewMatrix"_ba });
++
++ QSSGShaderCustomMaterialAdapter::StringPairList vertexUniforms;
++ vertexUniforms.append({ "float"_ba, "qt_adjustY"_ba });
++
++ QSSGShaderCustomMaterialAdapter::StringPairList fragmentUniforms;
++ fragmentUniforms.append({ "float"_ba, "qt_skyExposure"_ba });
++
++ backgroundPipeline = buildPipeline(sgContext, vertexShaderStrScreen, vertexViewDependentUniforms, vertexUniforms,
++ mainFragmentSnippetScreen, fragmentUniforms, tonemapFeatures, viewCount,
++ QByteArrayLiteral(":screen:") + QByteArray::number(tonemapKey)
++ + ':' + QByteArray::number(viewCount));
++
++ m_backgroundTonemapKey = tonemapKey;
++ m_backgroundViewCount = viewCount;
++ isBackgroundShaderDirty = false;
++
++ return backgroundPipeline;
++ }
++
++ quint32 QSSGRenderSkyMaterial::updateUniforms(const QSSGRenderContextInterface &sgContext,
++ QSSGPassKey passKey,
++ const QMatrix4x4 &mvp,
++ const QVarLengthArray<QMatrix4x4, 6> views)
++ {
++ constexpr int cMatrixSize = 64;
++
++ if (!iblPassPipeline)
++ return 0;
++
++ QSSGRhiContext *rhiCtx = sgContext.rhiContext().get();
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
++ // Keyed on the driving pass (the layer's sky material pass) and the
++ // material; the material must be the key's resource so that
++ // cleanupDrawCallDataForResource() finds the entry when it is destroyed.
++ QSSGRhiDrawCallData *dcd = &rhiCtxD->drawCallData({ passKey, nullptr, this, nullptr, 0 });
++
++ const int uniformStride = rhiCtx->rhi()->ubufAligned(iblPassPipeline->ub0Size());
++ const int totalBufferSize = uniformStride * 6;
++
++ if (!dcd->ubuf) {
++ dcd->ubuf = rhiCtx->rhi()->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, totalBufferSize);
++ dcd->ubuf->create();
++ }
++
++ char *ubufData = dcd->ubuf->beginFullDynamicBufferUpdateForCurrentFrame();
++ auto bufferManager = sgContext.bufferManager().get();
++
++ iblPassPipeline->resetExtraTextures();
++
++ for (const auto &u : std::as_const(propertyUniforms))
++ iblPassPipeline->setShaderResources(ubufData, *bufferManager, u.name, u.value, u.shaderDataType);
++
++ // same for all faces
++ iblPassPipeline->setShaderResources(ubufData, *bufferManager, "qt_projectionMatrix"_ba, mvp, QSSGRenderShaderValue::Type::Matrix4x4);
++ iblPassPipeline->setShaderResources(ubufData, *bufferManager, "qt_viewMatrix"_ba, views[0], QSSGRenderShaderValue::Type::Matrix4x4);
++
++ // Kinda hacky way of filling out all the views
++ // Copy first properties then clone these six times (each face)
++ // view matrix is the only unique property per face
++ const int viewMatrixOffset = iblPassPipeline->offsetOfUniform("qt_viewMatrix"_ba);
++
++ Q_ASSERT(ubufData != nullptr);
++ Q_ASSERT(totalBufferSize >= 6 * uniformStride); // buffer must hold all 6 faces
++ Q_ASSERT(uniformStride >= cMatrixSize); // view matrix fits
++ Q_ASSERT(viewMatrixOffset >= 0);
++ Q_ASSERT(viewMatrixOffset + cMatrixSize <= uniformStride);
++
++ auto buffer = QSpan<char>(ubufData, totalBufferSize);
++ auto firstFace = buffer.first(uniformStride);
++
++ for (int face = 1; face < 6; ++face) {
++ auto dst = buffer.sliced(face * uniformStride, uniformStride);
++
++ // Copy the entire first face block
++ std::copy(firstFace.begin(), firstFace.end(), dst.begin());
++
++ // Copy only the per-face view matrix
++ std::copy_n(reinterpret_cast<const char *>(views[face].constData()), cMatrixSize, dst.data() + viewMatrixOffset);
++ }
++ dcd->ubuf->endFullDynamicBufferUpdateForCurrentFrame();
++
++ rhiCtxD->releaseCachedSrb(bindings);
++ bindings = QSSGRhiShaderResourceBindingList();
++ bindings.addUniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, dcd->ubuf, 0, uniformStride, true);
++ appendCustomTextureBindings(iblPassPipeline.get(), bindings, rhiCtx);
++
++ return uniformStride;
++ }
++
++ void QSSGRenderSkyMaterial::updateBackgroundUniforms(const QSSGRenderContextInterface &sgContext,
++ QSSGPassKey passKey,
++ const QVarLengthArray<QMatrix4x4, 2> &inverseProjections,
++ const QVarLengthArray<QMatrix4x4, 2> &viewRotations,
++ float adjustY,
++ float exposure)
++ {
++ if (!backgroundPipeline)
++ return;
++
++ QSSGRhiContext *rhiCtx = sgContext.rhiContext().get();
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
++ // Keyed on the driving pass (the layer's background pass) and the
++ // material; the material must be the key's resource so that
++ // cleanupDrawCallDataForResource() finds the entry when it is destroyed.
++ QSSGRhiDrawCallData *dcd = &rhiCtxD->drawCallData({ passKey, nullptr, this, nullptr, 0 });
++
++ const int bufferSize = rhiCtx->rhi()->ubufAligned(backgroundPipeline->ub0Size());
++
++ if (!dcd->ubuf || int(dcd->ubuf->size()) != bufferSize) {
++ delete dcd->ubuf;
++ dcd->ubuf = rhiCtx->rhi()->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, bufferSize);
++ dcd->ubuf->create();
++ }
++
++ char *ubufData = dcd->ubuf->beginFullDynamicBufferUpdateForCurrentFrame();
++ auto bufferManager = sgContext.bufferManager().get();
++
++ backgroundPipeline->resetExtraTextures();
++
++ for (const auto &u : std::as_const(propertyUniforms))
++ backgroundPipeline->setShaderResources(ubufData, *bufferManager, u.name, u.value, u.shaderDataType);
++
++ backgroundPipeline->setShaderResources(ubufData, *bufferManager, "qt_adjustY"_ba, adjustY, QSSGRenderShaderValue::Type::Float);
++ backgroundPipeline->setShaderResources(ubufData, *bufferManager, "qt_skyExposure"_ba, exposure, QSSGRenderShaderValue::Type::Float);
++
++ constexpr int matSize = 64;
++ const int invProjOffset = backgroundPipeline->offsetOfUniform("qt_inverseProjection"_ba);
++ const int viewMatOffset = backgroundPipeline->offsetOfUniform("qt_viewMatrix"_ba);
++ const int viewCount = int(inverseProjections.size());
++ for (int v = 0; v < viewCount; ++v) {
++ if (invProjOffset >= 0)
++ std::copy_n(reinterpret_cast<const char *>(inverseProjections[v].constData()), matSize, ubufData + invProjOffset + v * matSize);
++ if (viewMatOffset >= 0)
++ std::copy_n(reinterpret_cast<const char *>(viewRotations[v].constData()), matSize, ubufData + viewMatOffset + v * matSize);
++ }
++
++ dcd->ubuf->endFullDynamicBufferUpdateForCurrentFrame();
++
++ rhiCtxD->releaseCachedSrb(backgroundBindings);
++ backgroundBindings = QSSGRhiShaderResourceBindingList();
++ backgroundBindings.addUniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, dcd->ubuf);
++ appendCustomTextureBindings(backgroundPipeline.get(), backgroundBindings, rhiCtx);
++ }
++
++ QT_END_NAMESPACE