Revision control

Copy as Markdown

// 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 <QtQuick3DRuntimeRender/private/qssgrenderskymaterialmanager_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrenderskymaterial_p.h>
#include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
#include <QtQuick3DUtils/private/qssgassert_p.h>
#include "qssgrendercontextcore.h"
#include "qssgrendershadercache_p.h"
#include "qssgrhicontext_p.h"
#include <QtCore/qmath.h>
#include <cmath>
using namespace Qt::StringLiterals;
QT_BEGIN_NAMESPACE
namespace {
static constexpr QRhiTexture::Format cTextureFormat = QRhiTexture::RGBA16F;
static int nearestPowerOfTwo(int v)
{
if (v <= 1)
return 1;
int upper = qNextPowerOfTwo(v);
int lower = upper >> 1;
return (v - lower < upper - v) ? lower : upper;
}
static QVarLengthArray<QMatrix4x4, 6> skyIblEnvironmentMapViews(QRhi *rhi)
{
auto lookAt = [](const QVector3D &eye, const QVector3D &center, const QVector3D &up) {
QMatrix4x4 viewMatrix;
viewMatrix.lookAt(eye, center, up);
return viewMatrix;
};
QVarLengthArray<QMatrix4x4, 6> views;
views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(1.0, 0.0, 0.0), QVector3D(0.0f, -1.0f, 0.0f)));
views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(-1.0, 0.0, 0.0), QVector3D(0.0f, -1.0f, 0.0f)));
if (rhi->isYUpInFramebuffer()) {
views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0f, 0.0f, 1.0f)));
views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, -1.0, 0.0), QVector3D(0.0f, 0.0f, -1.0f)));
} else {
views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, -1.0, 0.0), QVector3D(0.0f, 0.0f, -1.0f)));
views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0f, 0.0f, 1.0f)));
}
views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 0.0, 1.0), QVector3D(0.0f, -1.0f, 0.0f)));
views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 0.0, -1.0), QVector3D(0.0f, -1.0f, 0.0f)));
return views;
}
// Creates 6 face render targets for a texture, always at mip level 0.
// preserveColorContents controls whether the render target loads existing
// content before drawing (additive accumulation) or clears it (first slice).
// Using level=0 unconditionally is intentional: on Android GLES some drivers
// silently ignore the level parameter to glFramebufferTextureLayer for level>0,
// so the accumulator is allocated as separate per-mip non-mipmapped textures
// and each is always attached at level 0.
static bool skyIblCreateFaceTargets(QRhi *rhi,
QRhiTexture *texture,
const QByteArray &namePrefix,
QSSGSkyIblFaceTargets *outTargets,
bool preserveColorContents = false)
{
Q_ASSERT(outTargets);
const QRhiTextureRenderTarget::Flags rtFlags = preserveColorContents
? QRhiTextureRenderTarget::Flags(QRhiTextureRenderTarget::PreserveColorContents)
: QRhiTextureRenderTarget::Flags();
for (const auto face : QSSGRenderTextureCubeFaces) {
QRhiColorAttachment att(texture);
att.setLayer(quint8(face));
// No setLevel() call — always attaches at mip level 0. Each per-mip
// accumulator texture has only one mip level, so this is correct.
QRhiTextureRenderTargetDescription rtDesc;
rtDesc.setColorAttachments({ att });
auto renderTarget = rhi->newTextureRenderTarget(rtDesc, rtFlags);
renderTarget->setName(namePrefix + "/"_ba + QSSGBaseTypeHelpers::displayName(face));
renderTarget->setDescription(rtDesc);
if (!outTargets->renderPassDesc)
outTargets->renderPassDesc = renderTarget->newCompatibleRenderPassDescriptor();
renderTarget->setRenderPassDescriptor(outTargets->renderPassDesc);
if (!renderTarget->create()) {
qWarning("Failed to build sky IBL env map render target");
return false;
}
outTargets->renderTargets << renderTarget;
}
return true;
}
static bool skyIblCreatePrefilterTargets(QRhi *rhi,
QRhiTexture *texture,
const QSize &environmentMapSize,
const QByteArray &namePrefix,
QSSGSkyIblPrefilterTargets *outTargets,
bool preserveColorContents = false)
{
Q_ASSERT(outTargets);
const bool hasMips = texture->flags().testFlag(QRhiTexture::MipMapped);
outTargets->mipmapCount = hasMips ? qMin(rhi->mipLevelsForSize(environmentMapSize), 6) : 1;
outTargets->mipLevelSizes.resize(outTargets->mipmapCount);
outTargets->mipRenderTargetsMap.resize(outTargets->mipmapCount);
const QRhiTextureRenderTarget::Flags rtFlags = preserveColorContents
? QRhiTextureRenderTarget::Flags(QRhiTextureRenderTarget::PreserveColorContents)
: QRhiTextureRenderTarget::Flags();
auto cleanup = [outTargets](QRhiTextureRenderTarget *failed, const QSSGSkyIblCubeFaceRenderTargets &partial) {
delete failed;
for (auto *rt : partial)
delete rt;
for (const QSSGSkyIblCubeFaceRenderTargets &rts : std::as_const(outTargets->mipRenderTargetsMap)) {
for (auto *rt : rts)
delete rt;
}
delete outTargets->renderPassDesc;
outTargets->renderPassDesc = nullptr;
outTargets->mipRenderTargetsMap.clear();
outTargets->mipLevelSizes.clear();
outTargets->mipmapCount = 0;
};
for (int mipLevel = 0; mipLevel < outTargets->mipmapCount; ++mipLevel) {
QSSGSkyIblCubeFaceRenderTargets renderTargets;
for (const auto face : QSSGRenderTextureCubeFaces) {
QRhiColorAttachment att(texture);
att.setLayer(quint8(face));
att.setLevel(mipLevel);
QRhiTextureRenderTargetDescription rtDesc;
rtDesc.setColorAttachments({ att });
auto renderTarget = rhi->newTextureRenderTarget(rtDesc, rtFlags);
renderTarget->setName(namePrefix + QByteArrayLiteral("/m") + QByteArray::number(mipLevel)
+ QByteArrayLiteral("/") + QSSGBaseTypeHelpers::displayName(face));
renderTarget->setDescription(rtDesc);
if (!outTargets->renderPassDesc)
outTargets->renderPassDesc = renderTarget->newCompatibleRenderPassDescriptor();
renderTarget->setRenderPassDescriptor(outTargets->renderPassDesc);
if (!renderTarget->create()) {
qWarning("Failed to build sky IBL prefilter env map render target");
cleanup(renderTarget, renderTargets);
return false;
}
renderTargets << renderTarget;
}
const QSize levelSize(environmentMapSize.width() * std::pow(0.5, mipLevel),
environmentMapSize.height() * std::pow(0.5, mipLevel));
outTargets->mipLevelSizes[mipLevel] = levelSize;
outTargets->mipRenderTargetsMap[mipLevel] = renderTargets;
}
return true;
}
static void skyIblInitializeUnrenderedMips(QRhi *rhi,
QRhiCommandBuffer *cb,
QSSGRhiContext *context,
QRhiTexture *texture,
int firstMip,
int mipCountExclusive,
const QByteArray &debugObjectName)
{
if (firstMip >= mipCountExclusive)
return;
QRhiRenderPassDescriptor *rpDesc = nullptr;
for (int mipLevel = firstMip; mipLevel < mipCountExclusive; ++mipLevel) {
for (const auto face : QSSGRenderTextureCubeFaces) {
QRhiColorAttachment att(texture);
att.setLayer(quint8(face));
att.setLevel(mipLevel);
QRhiTextureRenderTargetDescription rtDesc;
rtDesc.setColorAttachments({ att });
auto *rt = rhi->newTextureRenderTarget(rtDesc);
rt->setName(debugObjectName + "/init/m"_ba + QByteArray::number(mipLevel) + "/"_ba
+ QSSGBaseTypeHelpers::displayName(face));
rt->setDescription(rtDesc);
if (!rpDesc)
rpDesc = rt->newCompatibleRenderPassDescriptor();
rt->setRenderPassDescriptor(rpDesc);
if (!rt->create()) {
qWarning("Failed to create sky IBL init render target");
delete rt;
continue;
}
rt->deleteLater();
cb->beginPass(rt, QColor(0, 0, 0, 1), { 1.0f, 0 }, nullptr, context->commonPassFlags());
cb->endPass();
}
}
if (rpDesc)
rpDesc->deleteLater();
}
static const float skyIblCubeVerts[] = {
-1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f,
1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f,
1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f,
1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,
};
static bool ensureDynamicUBuf(QRhi *rhi, QRhiBuffer *&dst, int size, const char *errorMessage)
{
if (dst)
return true;
dst = rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, size);
if (!dst->create()) {
qWarning("%s", errorMessage);
delete dst;
dst = nullptr;
return false;
}
return true;
}
static bool ensureSrb(QRhi *rhi, QRhiShaderResourceBindings *&dst, std::initializer_list<QRhiShaderResourceBinding> bindings, const char *errorMessage)
{
if (dst)
return true;
dst = rhi->newShaderResourceBindings();
dst->setBindings(bindings);
if (!dst->create()) {
qWarning("%s", errorMessage);
delete dst;
dst = nullptr;
return false;
}
return true;
}
struct PrefilterPipelineConfig
{
QSSGRhiShaderPipeline *shader = nullptr;
QRhiShaderResourceBindings *srb = nullptr;
QRhiRenderPassDescriptor *rpd = nullptr;
bool additiveBlend = false;
};
static QRhiGraphicsPipeline *createPrefilterPipeline(QRhi *rhi,
const QRhiVertexInputLayout &inputLayout,
const PrefilterPipelineConfig &cfg,
const char *errorMessage)
{
auto *pipeline = rhi->newGraphicsPipeline();
pipeline->setCullMode(QRhiGraphicsPipeline::Front);
pipeline->setFrontFace(QRhiGraphicsPipeline::CCW);
pipeline->setDepthOp(QRhiGraphicsPipeline::LessOrEqual);
pipeline->setShaderStages({ *cfg.shader->vertexStage(), *cfg.shader->fragmentStage() });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(cfg.srb);
pipeline->setRenderPassDescriptor(cfg.rpd);
pipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
if (cfg.additiveBlend) {
QRhiGraphicsPipeline::TargetBlend addBlend;
addBlend.enable = true;
addBlend.srcColor = QRhiGraphicsPipeline::One;
addBlend.dstColor = QRhiGraphicsPipeline::One;
addBlend.opColor = QRhiGraphicsPipeline::Add;
addBlend.srcAlpha = QRhiGraphicsPipeline::One;
addBlend.dstAlpha = QRhiGraphicsPipeline::One;
addBlend.opAlpha = QRhiGraphicsPipeline::Add;
pipeline->setTargetBlends({ addBlend });
}
if (!pipeline->create()) {
qWarning("%s", errorMessage);
delete pipeline;
return nullptr;
}
return pipeline;
}
static void drawCubeFace(QRhiCommandBuffer *cb,
QSSGRhiContext *ctx,
QRhiTextureRenderTarget *rt,
QSize viewport,
QRhiGraphicsPipeline *pipeline,
QRhiShaderResourceBindings *srb,
const QRhiCommandBuffer::VertexInput &vbufBinding,
const QVector<QPair<int, quint32>> &dynamicOffsets,
const QByteArray &profilerLabel,
const QByteArray &passDebugLabel,
const QColor &clearColor = QColor(0, 0, 0, 1))
{
cb->beginPass(rt, clearColor, { 1.0f, 0 }, nullptr, ctx->commonPassFlags());
QSSGRHICTX_STAT(ctx, beginRenderPass(rt));
Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderPass);
cb->setViewport(QRhiViewport(0, 0, viewport.width(), viewport.height()));
cb->setScissor(QRhiScissor(0, 0, viewport.width(), viewport.height()));
cb->setGraphicsPipeline(pipeline);
cb->setShaderResources(srb, dynamicOffsets.size(), dynamicOffsets.constData());
cb->setVertexInput(0, 1, &vbufBinding);
Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderCall);
cb->draw(36);
QSSGRHICTX_STAT(ctx, draw(36, 1));
Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderCall, 36llu | (1llu << 32), profilerLabel);
cb->endPass();
QSSGRHICTX_STAT(ctx, endRenderPass());
Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderPass, 0, passDebugLabel);
}
} // namespace
// Per-frame derived state passed between phase methods
struct QSSGRenderSkyMaterialManager::FrameState
{
// computeFrameState
QSize environmentMapSize;
int totalSamples = 0;
bool enableIBL = false;
bool needCreateEnv = false;
bool inProgressTimeSlice = false;
bool envContentDirty = false;
bool deferEnvRefresh = false;
bool needRenderEnv = false;
// ensureTextures
bool prefilteredJustCreated = false;
int prefilterTotalMipCount = 0;
int prefilterSpecularMipCount = 0;
int prefilterRoughnessDenom = 1;
float resolution = 0.0f;
// deriveCycleState
bool multiFrame = false;
bool prefilterIsConverged = false;
bool runPrefilterSlice = false;
int perFrameBudget = 0;
int sliceSamplesThisFrame = 0;
int sliceSampleStart = 0;
int sliceSampleEnd = 0;
bool sliceCompletesCycle = false;
bool writePrefilteredCubeThisFrame = false;
bool runIrradiancePass = false;
bool haveConvergedResultEntering = false;
bool isFirstSlice = false;
// ensureSharedResources
QRhiVertexInputLayout inputLayout;
QMatrix4x4 mvp;
QVarLengthArray<QMatrix4x4, 6> views;
int ubufElementSize = 0;
QRhiCommandBuffer::VertexInput vbufBinding { nullptr, 0 };
};
QSSGRenderSkyMaterialManager::QSSGRenderSkyMaterialManager(const QSSGRenderContextInterface &inContext)
: m_context(inContext)
{
}
QSSGRenderSkyMaterialManager::~QSSGRenderSkyMaterialManager()
{
releaseCachedResources();
}
void QSSGRenderSkyMaterialManager::clearPrefilterCache()
{
auto releasePrefilterTargets = [](QSSGSkyIblPrefilterTargets &t) {
for (const QSSGSkyIblCubeFaceRenderTargets &renderTargets : std::as_const(t.mipRenderTargetsMap)) {
for (QRhiTextureRenderTarget *renderTarget : renderTargets)
delete renderTarget;
}
delete t.renderPassDesc;
t.renderPassDesc = nullptr;
t.mipRenderTargetsMap.clear();
t.mipLevelSizes.clear();
t.mipmapCount = 0;
};
auto releaseFaceTargets = [](QSSGSkyIblFaceTargets &t) {
for (QRhiTextureRenderTarget *rt : t.renderTargets)
delete rt;
delete t.renderPassDesc;
t.renderPassDesc = nullptr;
t.renderTargets.clear();
};
auto safeDelete = [](auto *&res) {
delete res;
res = nullptr;
};
safeDelete(m_cache.vertexBuffer);
safeDelete(m_cache.uBuf);
safeDelete(m_cache.uBufSlice);
safeDelete(m_cache.uBufNormalize);
safeDelete(m_cache.uBufIrradiance);
releaseFaceTargets(m_cache.envFaceTargets);
releasePrefilterTargets(m_cache.prefilterTargets);
// Release per-mip accumulator face targets (preserve and clear variants)
for (QSSGSkyIblFaceTargets &t : m_cache.accumPreserveFaceTargets)
releaseFaceTargets(t);
m_cache.accumPreserveFaceTargets.clear();
for (QSSGSkyIblFaceTargets &t : m_cache.accumClearFaceTargets)
releaseFaceTargets(t);
m_cache.accumClearFaceTargets.clear();
// Release per-mip normalize SRBs
for (QRhiShaderResourceBindings *srb : std::as_const(m_cache.normalizeSrbs))
delete srb;
m_cache.normalizeSrbs.clear();
safeDelete(m_cache.sliceSrb);
safeDelete(m_cache.irradianceSrb);
safeDelete(m_cache.envMapPipeline);
safeDelete(m_cache.slicePipeline);
safeDelete(m_cache.normalizeCubePipeline);
safeDelete(m_cache.irradiancePipeline);
m_cache.environmentMapSize = { };
m_cache.enableIBL = false;
m_cache.prefilterTotalMipCount = 0;
m_cache.envCubeMap = nullptr;
m_cache.prefilteredCubeMap = nullptr;
m_cache.prefilterAccumulators.clear();
m_cache.envShaderPipeline = nullptr;
}
void QSSGRenderSkyMaterialManager::releaseCachedResources()
{
clearPrefilterCache();
if (auto rhiCtx = m_context.rhiContext().get(); QSSG_GUARD(rhiCtx && rhiCtx->isValid())) {
QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
if (m_envCubeMap)
rhiCtxD->releaseTexture(m_envCubeMap);
if (m_prefilteredCubeMap)
rhiCtxD->releaseTexture(m_prefilteredCubeMap);
for (QRhiTexture *t : std::as_const(m_prefilterAccumulators))
rhiCtxD->releaseTexture(t);
}
m_envCubeMap = nullptr;
m_prefilteredCubeMap = nullptr;
m_prefilterAccumulators.clear();
m_skyIblTexture = { };
m_cubeMapSize = { };
m_prefilteredMipCount = 0;
m_accumulatedSamples = 0;
m_accumIblSampleCount = 0;
m_haveConvergedResult = false;
m_envTailMipsInitialized = false;
m_prefilteredTailMipsInitialized = false;
m_finalizeIblPending = false;
}
QSSGRenderImageTexture QSSGRenderSkyMaterialManager::resolve(QSSGRenderSkyMaterial *settings, QSSGPassKey passKey)
{
const auto &rhiCtx = m_context.rhiContext();
if (!QSSG_GUARD(rhiCtx && rhiCtx->isValid() && rhiCtx->rhi()->isRecordingFrame()))
return { };
if (!ensureEnvironmentMap(settings, passKey)) {
return { };
}
const bool pendingAccumulation = settings->enableIBL && m_accumulatedSamples < settings->iblSampleCount;
settings->wantsMoreFrames = pendingAccumulation || settings->isDirty || m_finalizeIblPending;
return m_skyIblTexture;
}
bool QSSGRenderSkyMaterialManager::ensureEnvironmentMap(QSSGRenderSkyMaterial *inSky, QSSGPassKey passKey)
{
const auto &context = m_context.rhiContext();
if (!context->rhi()->isTextureFormatSupported(cTextureFormat)) {
static bool warningPrinted = false;
if (Q_UNLIKELY(!warningPrinted)) {
qWarning() << "SkyMaterial not supported due to missing RGBA16F texture format support.";
warningPrinted = true;
}
return false;
}
auto *cb = context->commandBuffer();
FrameState fs;
if (!computeFrameState(inSky, fs))
return false;
if (!ensureTextures(fs))
return false;
deriveCycleState(inSky, fs);
QSSGRhiShaderPipelinePtr shaderPipeline;
if (fs.needRenderEnv) {
shaderPipeline = inSky->ensurePipeline(m_context);
if (!shaderPipeline) {
return false;
}
}
QSSGRhiShaderPipeline *envShaderPipelineKey = fs.needRenderEnv ? shaderPipeline.get() : m_cache.envShaderPipeline;
validateAndUpdateCacheKey(fs, envShaderPipelineKey);
if (!ensureSharedResources(fs, cb))
return false;
auto *rub = context->rhi()->nextResourceUpdateBatch();
for (const auto face : QSSGRenderTextureCubeFaces) {
rub->updateDynamicBuffer(m_cache.uBuf, quint8(face) * fs.ubufElementSize, 64, fs.mvp.constData());
rub->updateDynamicBuffer(m_cache.uBuf, quint8(face) * fs.ubufElementSize + 64, 64, fs.views[quint8(face)].constData());
}
if (fs.runIrradiancePass) {
struct IrradianceData
{
float roughness;
float resolution;
float lodBias;
int sampleCount;
int distribution;
} irradianceData;
irradianceData.roughness = 0.0f;
irradianceData.resolution = fs.resolution;
irradianceData.lodBias = 0.0f;
irradianceData.distribution = 0;
irradianceData.sampleCount = qMax(int(fs.resolution / 4.0f), 1);
rub->updateDynamicBuffer(m_cache.uBufIrradiance, 0, sizeof(IrradianceData), &irradianceData);
}
if (fs.needRenderEnv) {
if (!renderEnvironmentCube(inSky, passKey, fs, shaderPipeline, cb, rub))
return false;
rub = nullptr;
}
if (fs.enableIBL && fs.needRenderEnv)
runEnvironmentMipChain(fs, cb);
if (!rub)
rub = context->rhi()->nextResourceUpdateBatch();
cb->debugMarkBegin("Sky IBL Pre-filtered Environment Cubemap Generation");
if (!runPrefilterCycle(inSky, fs, cb, rub))
return false;
if (rub) {
cb->resourceUpdate(rub);
rub = nullptr;
}
cb->debugMarkEnd();
m_prefilteredMipCount = m_cache.prefilterTargets.mipmapCount;
initializeTailMips(fs, cb);
m_skyIblTexture.m_texture = m_prefilteredCubeMap;
m_skyIblTexture.m_mipmapCount = m_prefilteredMipCount;
m_skyIblTexture.m_flags.setLinear(true);
m_skyIblTexture.m_flags.setRgbe8(false);
return true;
}
bool QSSGRenderSkyMaterialManager::computeFrameState(QSSGRenderSkyMaterial *inSky, FrameState &fs)
{
fs.enableIBL = inSky->enableIBL;
fs.totalSamples = qBound(1, inSky->iblSampleCount, 1024);
const int radianceMapSize = qBound(8, nearestPowerOfTwo(inSky->radianceMapSize), 2048);
fs.environmentMapSize = QSize(radianceMapSize, radianceMapSize);
const bool envWantsMips = fs.enableIBL;
const bool envHasMips = m_envCubeMap && m_envCubeMap->flags().testFlag(QRhiTexture::MipMapped);
fs.needCreateEnv = !m_envCubeMap || m_cubeMapSize != fs.environmentMapSize || envHasMips != envWantsMips;
fs.inProgressTimeSlice = fs.enableIBL && m_accumulatedSamples > 0 && m_accumulatedSamples < m_accumIblSampleCount;
fs.envContentDirty = inSky->isDirty && !fs.needCreateEnv;
fs.deferEnvRefresh = fs.envContentDirty && (fs.inProgressTimeSlice || m_finalizeIblPending);
fs.needRenderEnv = fs.needCreateEnv || (fs.envContentDirty && !fs.deferEnvRefresh);
return true;
}
bool QSSGRenderSkyMaterialManager::ensureTextures(FrameState &fs)
{
const auto &context = m_context.rhiContext();
auto *rhi = context->rhi();
QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(context.get());
// --- Environment cube ---
if (fs.needCreateEnv) {
if (m_envCubeMap) {
rhiCtxD->releaseTexture(m_envCubeMap);
m_envCubeMap = nullptr;
}
// The prefiltered cube derives from m_envCubeMap and must be invalidated alongside.
if (m_prefilteredCubeMap) {
rhiCtxD->releaseTexture(m_prefilteredCubeMap);
m_prefilteredCubeMap = nullptr;
m_prefilteredMipCount = 0;
}
m_envTailMipsInitialized = false;
m_prefilteredTailMipsInitialized = false;
QRhiTexture::Flags envFlags = QRhiTexture::RenderTarget | QRhiTexture::CubeMap;
if (fs.enableIBL)
envFlags |= QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips;
m_envCubeMap = rhi->newTexture(cTextureFormat, fs.environmentMapSize, 1, envFlags);
if (!m_envCubeMap->create()) {
qWarning("Failed to create Sky IBL environment cube map");
delete m_envCubeMap;
m_envCubeMap = nullptr;
return false;
}
m_envCubeMap->setName("SkyMaterialLightProbe procEnvCube"_ba);
rhiCtxD->registerTexture(m_envCubeMap);
m_cubeMapSize = fs.environmentMapSize;
}
if (!m_prefilteredCubeMap) {
const QRhiTexture::Flags pfFlags = QRhiTexture::RenderTarget | QRhiTexture::CubeMap | QRhiTexture::MipMapped;
m_prefilteredCubeMap = rhi->newTexture(cTextureFormat, fs.environmentMapSize, 1, pfFlags);
if (!m_prefilteredCubeMap->create()) {
qWarning("Failed to create Sky IBL pre-filtered environment cube map");
delete m_prefilteredCubeMap;
m_prefilteredCubeMap = nullptr;
return false;
}
m_prefilteredCubeMap->setName("SkyMaterialLightProbe"_ba);
rhiCtxD->registerTexture(m_prefilteredCubeMap);
fs.prefilteredJustCreated = true;
m_haveConvergedResult = false;
m_prefilteredTailMipsInitialized = false;
}
fs.prefilterTotalMipCount = m_prefilteredCubeMap->flags().testFlag(QRhiTexture::MipMapped)
? qMin(rhi->mipLevelsForSize(fs.environmentMapSize), 6)
: 1;
fs.prefilterSpecularMipCount = fs.enableIBL ? fs.prefilterTotalMipCount - 1 : 1;
fs.prefilterRoughnessDenom = qMax(fs.prefilterSpecularMipCount - 1, 1);
fs.resolution = float(fs.environmentMapSize.width());
// Determine whether the existing per-mip accumulator set is still valid.
// We need one texture per specular mip level, each sized for that mip.
const bool needCreateAccum = m_prefilterAccumulators.isEmpty() || m_prefilterAccumulators.size() != fs.prefilterSpecularMipCount
|| (!m_prefilterAccumulators.isEmpty() && m_prefilterAccumulators[0]->pixelSize() != fs.environmentMapSize);
if (needCreateAccum) {
for (QRhiTexture *t : std::as_const(m_prefilterAccumulators))
rhiCtxD->releaseTexture(t);
m_prefilterAccumulators.clear();
m_accumulatedSamples = 0;
// Allocate one non-mipmapped 2D array texture per specular mip level.
// Each texture is sized for its mip level and only has mip 0.
//
// Rationale: on Android GLES (Adreno, Mali) glFramebufferTextureLayer
// with level > 0 is sometimes silently broken. The driver accepts the
// FBO as complete but writes to the wrong location or ignores the level
// entirely. By giving each mip level its own texture and always attaching
// at level 0, we avoid the broken code path entirely.
bool ok = true;
for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
const QSize mipSize(qMax(1, fs.environmentMapSize.width() >> mip), qMax(1, fs.environmentMapSize.height() >> mip));
// No MipMapped flag — single level only, always attached at level 0.
auto *t = rhi->newTextureArray(cTextureFormat, 6, mipSize, 1, QRhiTexture::RenderTarget);
if (!t->create()) {
qWarning("Failed to create Sky IBL prefilter accumulator mip %d", mip);
delete t;
ok = false;
break;
}
t->setName("SkyMaterialLightProbe procEnvPfAccum m"_ba + QByteArray::number(mip));
rhiCtxD->registerTexture(t);
m_prefilterAccumulators.append(t);
}
if (!ok) {
for (QRhiTexture *t : std::as_const(m_prefilterAccumulators))
rhiCtxD->releaseTexture(t);
m_prefilterAccumulators.clear();
// Accumulator unavailable — fall back to single-frame prefilter.
} else {
// No tail-mip initialization needed: each texture has exactly one
// mip level (level 0), so there are no unwritten tail mips.
}
}
return true;
}
void QSSGRenderSkyMaterialManager::deriveCycleState(QSSGRenderSkyMaterial *inSky, FrameState &fs)
{
// Finalize frame: all slicing is complete, run normalize + irradiance now.
if (m_finalizeIblPending && !fs.needRenderEnv && !fs.prefilteredJustCreated && m_accumIblSampleCount == fs.totalSamples) {
m_finalizeIblPending = false;
m_haveConvergedResult = true;
fs.runPrefilterSlice = false;
fs.writePrefilteredCubeThisFrame = true;
fs.runIrradiancePass = fs.enableIBL;
fs.sliceSamplesThisFrame = 0;
fs.sliceSampleStart = m_accumulatedSamples;
fs.sliceSampleEnd = m_accumulatedSamples;
fs.isFirstSlice = false;
fs.sliceCompletesCycle = false;
fs.haveConvergedResultEntering = false; // was not converged before this frame
fs.multiFrame = false;
return;
}
fs.multiFrame = fs.enableIBL && inSky->iblSamplesPerFrame > 0 && inSky->iblSamplesPerFrame < fs.totalSamples;
// Restart the accumulator if anything stale: env content changed, prefiltered cube was
// re-created, or the sample-count target changed.
if (!m_prefilterAccumulators.isEmpty()) {
const bool resetAccumulation = fs.needRenderEnv || fs.prefilteredJustCreated || m_accumIblSampleCount != fs.totalSamples;
if (resetAccumulation) {
m_accumulatedSamples = 0;
// Any finalize pending from the previous cycle is now stale — the accumulators
// have been reset so running normalize would write garbage to the prefiltered cube.
m_finalizeIblPending = false;
}
m_accumIblSampleCount = fs.totalSamples;
}
fs.prefilterIsConverged = !m_prefilterAccumulators.isEmpty() && m_accumulatedSamples >= fs.totalSamples;
fs.runPrefilterSlice = !m_prefilterAccumulators.isEmpty() && !fs.prefilterIsConverged;
// perFrameBudget = samples to integrate this frame.
// Single-frame (iblSamplesPerFrame <= 0) or !enableIBL: all remaining samples.
// Multi-frame: the requested budget.
fs.perFrameBudget = (inSky->iblSamplesPerFrame > 0 && fs.enableIBL) ? inSky->iblSamplesPerFrame
: (fs.totalSamples - m_accumulatedSamples);
fs.sliceSamplesThisFrame = fs.runPrefilterSlice ? qMin(fs.perFrameBudget, fs.totalSamples - m_accumulatedSamples) : 0;
fs.sliceCompletesCycle = fs.runPrefilterSlice && (m_accumulatedSamples + fs.sliceSamplesThisFrame >= fs.totalSamples);
fs.sliceSampleStart = m_accumulatedSamples;
fs.sliceSampleEnd = fs.runPrefilterSlice ? qMin(m_accumulatedSamples + fs.perFrameBudget, fs.totalSamples) : 0;
fs.isFirstSlice = m_accumulatedSamples == 0;
fs.haveConvergedResultEntering = m_haveConvergedResult;
if (inSky->iblRenderFrames >= 1) {
// iblRenderFrames >= 1: normalize and irradiance are deferred to a dedicated frame
// after all slicing completes, so the last slice frame never pays both costs.
// m_finalizeIblPending is set in runPrefilterCycle (not here) so it only fires
// once the accumulators actually contain data. Setting it here would trigger a
// spurious finalize when the prefilter was blocked by canPrefilter==false (e.g.
// skyRenderFrames >= 1 on the env-render frame), normalising empty accumulators.
fs.writePrefilteredCubeThisFrame = false;
fs.runIrradiancePass = false;
} else {
// iblRenderFrames == 0: normalize runs every frame so the user sees progressive
// convergence while slicing, then fully on the cycle-completion frame.
fs.writePrefilteredCubeThisFrame = fs.runPrefilterSlice && (fs.sliceCompletesCycle || !m_haveConvergedResult);
fs.runIrradiancePass = fs.enableIBL && fs.writePrefilteredCubeThisFrame;
}
}
void QSSGRenderSkyMaterialManager::validateAndUpdateCacheKey(const FrameState &fs, QSSGRhiShaderPipeline *envShaderPipelineKey)
{
const bool cacheValid = m_cache.environmentMapSize == fs.environmentMapSize && m_cache.enableIBL == fs.enableIBL
&& m_cache.prefilterTotalMipCount == fs.prefilterTotalMipCount && m_cache.envCubeMap == m_envCubeMap
&& m_cache.prefilteredCubeMap == m_prefilteredCubeMap && m_cache.prefilterAccumulators == m_prefilterAccumulators
&& m_cache.envShaderPipeline == envShaderPipelineKey;
if (cacheValid)
return;
clearPrefilterCache();
m_cache.environmentMapSize = fs.environmentMapSize;
m_cache.enableIBL = fs.enableIBL;
m_cache.prefilterTotalMipCount = fs.prefilterTotalMipCount;
m_cache.envCubeMap = m_envCubeMap;
m_cache.prefilteredCubeMap = m_prefilteredCubeMap;
m_cache.prefilterAccumulators = m_prefilterAccumulators;
m_cache.envShaderPipeline = envShaderPipelineKey;
}
bool QSSGRenderSkyMaterialManager::ensureSharedResources(FrameState &fs, QRhiCommandBuffer *cb)
{
const auto &context = m_context.rhiContext();
auto *rhi = context->rhi();
fs.inputLayout.setBindings({ { 3 * sizeof(float) } });
fs.inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 } });
fs.mvp = rhi->clipSpaceCorrMatrix();
fs.mvp.perspective(90.0f, 1.0f, 0.1f, 10.0f);
fs.views = skyIblEnvironmentMapViews(rhi);
fs.ubufElementSize = rhi->ubufAligned(128);
if (!m_cache.vertexBuffer) {
m_cache.vertexBuffer = rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(skyIblCubeVerts));
if (!m_cache.vertexBuffer->create()) {
qWarning("Failed to create sky IBL vertex buffer");
delete m_cache.vertexBuffer;
m_cache.vertexBuffer = nullptr;
return false;
}
auto *initRub = rhi->nextResourceUpdateBatch();
initRub->uploadStaticBuffer(m_cache.vertexBuffer, skyIblCubeVerts);
cb->resourceUpdate(initRub);
}
if (!ensureDynamicUBuf(rhi, m_cache.uBuf, fs.ubufElementSize * 6, "Failed to create sky IBL view uniform buffer"))
return false;
fs.vbufBinding = QRhiCommandBuffer::VertexInput(m_cache.vertexBuffer, 0);
if (!m_cache.prefilterTargets.renderPassDesc) {
if (!skyIblCreatePrefilterTargets(rhi, m_prefilteredCubeMap, fs.environmentMapSize, "SkyMaterialLightProbe procEnvPf"_ba, &m_cache.prefilterTargets)) {
return false;
}
}
if (!ensureDynamicUBuf(rhi, m_cache.uBufIrradiance, rhi->ubufAligned(20), "Failed to create sky IBL irradiance uniform buffer"))
return false;
const QSSGRhiSamplerDescription samplerNoMipDesc { QRhiSampler::Linear, QRhiSampler::Linear,
QRhiSampler::None, QRhiSampler::ClampToEdge,
QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
QRhiSampler *envMapCubeNoMipSampler = context->sampler(samplerNoMipDesc);
if (!ensureSrb(rhi,
m_cache.irradianceSrb,
{ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufIrradiance, 20),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_envCubeMap, envMapCubeNoMipSampler) },
"Failed to create sky IBL irradiance SRB"))
return false;
if (!m_cache.irradiancePipeline) {
const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhienvironmentmapPreFilterShader(false);
m_cache.irradiancePipeline = createPrefilterPipeline(rhi,
fs.inputLayout,
{ shader.get(),
m_cache.irradianceSrb,
m_cache.prefilterTargets.renderPassDesc,
false },
"Failed to create sky IBL realtime irradiance pipeline "
"state");
if (!m_cache.irradiancePipeline)
return false;
}
return true;
}
bool QSSGRenderSkyMaterialManager::renderEnvironmentCube(QSSGRenderSkyMaterial *inSky,
QSSGPassKey passKey,
const FrameState &fs,
const QSSGRhiShaderPipelinePtr &shaderPipeline,
QRhiCommandBuffer *cb,
QRhiResourceUpdateBatch *rub)
{
const auto &context = m_context.rhiContext();
auto *rhi = context->rhi();
QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(context.get());
const quint32 skyElementSize = inSky->updateUniforms(m_context, passKey, fs.mvp, fs.views);
if (!m_cache.envFaceTargets.renderPassDesc) {
if (!skyIblCreateFaceTargets(rhi, m_envCubeMap, "SkyMaterialLightProbe procEnvCube"_ba, &m_cache.envFaceTargets))
return false;
}
QRhiShaderResourceBindings *envSrb = rhiCtxD->srb(inSky->bindings);
Q_ASSERT(envSrb);
if (!m_cache.envMapPipeline) {
m_cache.envMapPipeline = createPrefilterPipeline(rhi,
fs.inputLayout,
{ shaderPipeline.get(), envSrb, m_cache.envFaceTargets.renderPassDesc, false },
"Failed to create sky IBL env map pipeline state");
if (!m_cache.envMapPipeline)
return false;
}
cb->resourceUpdate(rub);
cb->debugMarkBegin("Sky IBL Procedural Environment Cubemap Generation");
for (const auto face : QSSGRenderTextureCubeFaces) {
const QVector<QPair<int, quint32>> offsets = { { 0, quint32(skyElementSize * quint8(face)) } };
drawCubeFace(cb,
context.get(),
m_cache.envFaceTargets.renderTargets[quint8(face)],
fs.environmentMapSize,
m_cache.envMapPipeline,
envSrb,
fs.vbufBinding,
offsets,
QByteArrayLiteral("sky_ibl_procedural_environment_map"),
QSSG_RENDERPASS_NAME("sky_ibl_procedural_environment_map", 0, face));
}
cb->debugMarkEnd();
inSky->isDirty = false;
return true;
}
void QSSGRenderSkyMaterialManager::runEnvironmentMipChain(const FrameState &fs, QRhiCommandBuffer *cb)
{
const auto &context = m_context.rhiContext();
auto *rhi = context->rhi();
if (!m_envTailMipsInitialized) {
const int envFullMipCount = rhi->mipLevelsForSize(fs.environmentMapSize);
skyIblInitializeUnrenderedMips(rhi, cb, context.get(), m_envCubeMap, 1, envFullMipCount, "SkyMaterialLightProbe procEnvCube"_ba);
m_envTailMipsInitialized = true;
}
auto *rubMip = rhi->nextResourceUpdateBatch();
rubMip->generateMips(m_envCubeMap);
cb->resourceUpdate(rubMip);
}
bool QSSGRenderSkyMaterialManager::runPrefilterCycle(QSSGRenderSkyMaterial *inSky,
const FrameState &fs,
QRhiCommandBuffer *cb,
QRhiResourceUpdateBatch *&rub)
{
Q_UNUSED(inSky);
if (!fs.runPrefilterSlice && !fs.writePrefilteredCubeThisFrame) {
cb->resourceUpdate(rub);
rub = nullptr;
return true;
}
const auto &context = m_context.rhiContext();
auto *rhi = context->rhi();
// Ensure per-mip face targets for the accumulator.
//
// Each specular mip level has its own non-mipmapped 2D array texture
// (m_prefilterAccumulators[mip]), and we need two sets of face render
// targets for it: one that preserves existing content (for slices 2..N,
// additive blend) and one that clears (for the first slice).
//
// We always attach at level 0 because each accumulator texture has only
// one mip level. This sidesteps the Android GLES driver bug where
// glFramebufferTextureLayer ignores level > 0.
if (m_cache.accumPreserveFaceTargets.size() != fs.prefilterSpecularMipCount) {
// Release any stale targets (size mismatch after cache invalidation)
for (QSSGSkyIblFaceTargets &t : m_cache.accumPreserveFaceTargets) {
for (QRhiTextureRenderTarget *rt : t.renderTargets)
delete rt;
delete t.renderPassDesc;
}
m_cache.accumPreserveFaceTargets.clear();
m_cache.accumPreserveFaceTargets.resize(fs.prefilterSpecularMipCount);
for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
if (!skyIblCreateFaceTargets(rhi,
m_prefilterAccumulators[mip],
"SkyMaterialLightProbe procEnvPfAccum/m"_ba + QByteArray::number(mip),
&m_cache.accumPreserveFaceTargets[mip],
true)) {
return false;
}
}
}
if (m_cache.accumClearFaceTargets.size() != fs.prefilterSpecularMipCount) {
for (QSSGSkyIblFaceTargets &t : m_cache.accumClearFaceTargets) {
for (QRhiTextureRenderTarget *rt : t.renderTargets)
delete rt;
delete t.renderPassDesc;
}
m_cache.accumClearFaceTargets.clear();
m_cache.accumClearFaceTargets.resize(fs.prefilterSpecularMipCount);
for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
if (!skyIblCreateFaceTargets(rhi,
m_prefilterAccumulators[mip],
"SkyMaterialLightProbe procEnvPfAccumClear/m"_ba + QByteArray::number(mip),
&m_cache.accumClearFaceTargets[mip],
false)) {
return false;
}
}
}
QSSGSkyIblPrefilterTargets &prefilterTargets = m_cache.prefilterTargets;
constexpr int uBufSliceSize = 32;
constexpr int uBufNormalizeSize = 16;
const int uBufSliceElementSize = rhi->ubufAligned(uBufSliceSize);
const int uBufNormalizeElementSize = rhi->ubufAligned(uBufNormalizeSize);
const int uBufNormalizeEntryCount = qMax(fs.prefilterSpecularMipCount, 1) * 6;
if (!ensureDynamicUBuf(rhi, m_cache.uBufSlice, uBufSliceElementSize * qMax(fs.prefilterSpecularMipCount, 1), "Failed to create sky IBL slice uniform buffer"))
return false;
if (!ensureDynamicUBuf(rhi, m_cache.uBufNormalize, uBufNormalizeElementSize * uBufNormalizeEntryCount, "Failed to create sky IBL normalize uniform buffer"))
return false;
const QSSGRhiSamplerDescription mipSamplerDesc { QRhiSampler::Linear, QRhiSampler::Linear,
QRhiSampler::Linear, QRhiSampler::ClampToEdge,
QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
const QSSGRhiSamplerDescription noMipLinearSamplerDesc { QRhiSampler::Linear, QRhiSampler::Linear,
QRhiSampler::None, QRhiSampler::ClampToEdge,
QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
const QSSGRhiSamplerDescription nearestSamplerDesc { QRhiSampler::Nearest, QRhiSampler::Nearest,
QRhiSampler::None, QRhiSampler::ClampToEdge,
QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
QRhiSampler *envMapCubeSampler = context->sampler(fs.enableIBL ? mipSamplerDesc : noMipLinearSamplerDesc);
QRhiSampler *accumReadSampler = context->sampler(nearestSamplerDesc);
if (!ensureSrb(rhi,
m_cache.sliceSrb,
{ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufSlice, uBufSliceSize),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_envCubeMap, envMapCubeSampler) },
"Failed to create sky IBL slice SRB"))
return false;
// Create one normalize SRB per specular mip level, each binding the
// corresponding per-mip accumulator texture. Since the accumulator textures
// are separate non-mipmapped arrays (one per mip), we always read from
// level 0 of whichever texture is bound — the normalize shader passes 0
// as the texelFetch lod (mipLevel in the UBO is set to 0 for all entries).
if (m_cache.normalizeSrbs.size() != fs.prefilterSpecularMipCount) {
for (QRhiShaderResourceBindings *srb : std::as_const(m_cache.normalizeSrbs))
delete srb;
m_cache.normalizeSrbs.clear();
m_cache.normalizeSrbs.resize(fs.prefilterSpecularMipCount, nullptr);
}
for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
if (!ensureSrb(rhi,
m_cache.normalizeSrbs[mip],
{ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufNormalize, uBufNormalizeSize),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_prefilterAccumulators[mip], accumReadSampler) },
"Failed to create sky IBL normalize SRB"))
return false;
}
if (!m_cache.slicePipeline) {
const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhiSkyIblPreFilterShader();
m_cache.slicePipeline = createPrefilterPipeline(rhi,
fs.inputLayout,
{ shader.get(),
m_cache.sliceSrb,
m_cache.accumPreserveFaceTargets[0].renderPassDesc,
true },
"Failed to create sky IBL slice pipeline state");
if (!m_cache.slicePipeline)
return false;
}
if (!m_cache.normalizeCubePipeline) {
// All normalize SRBs share the same layout; create the pipeline from SRB 0.
const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhiSkyIblPreFilterNormalizeShader();
m_cache.normalizeCubePipeline = createPrefilterPipeline(rhi,
fs.inputLayout,
{ shader.get(),
m_cache.normalizeSrbs[0],
prefilterTargets.renderPassDesc,
false },
"Failed to create sky IBL normalize-to-cube pipeline "
"state");
if (!m_cache.normalizeCubePipeline)
return false;
}
for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
struct SliceData
{
float roughness;
float resolution;
quint32 sampleStart;
quint32 sampleEnd;
quint32 totalSampleCount;
quint32 _pad0;
quint32 _pad1;
quint32 _pad2;
} sliceData;
sliceData.roughness = float(mipLevel) / float(fs.prefilterRoughnessDenom);
sliceData.resolution = fs.resolution;
sliceData.sampleStart = quint32(fs.sliceSampleStart);
sliceData.sampleEnd = quint32(fs.sliceSampleEnd);
sliceData.totalSampleCount = quint32(fs.totalSamples);
sliceData._pad0 = sliceData._pad1 = sliceData._pad2 = 0;
rub->updateDynamicBuffer(m_cache.uBufSlice, mipLevel * uBufSliceElementSize, sizeof(SliceData), &sliceData);
for (const auto face : QSSGRenderTextureCubeFaces) {
struct NormalizeData
{
qint32 faceIndex;
qint32 _pad0 = 0;
qint32 _pad1 = 0;
qint32 _pad2 = 0;
} normalizeData;
normalizeData.faceIndex = quint8(face);
const int entryIndex = mipLevel * 6 + quint8(face);
rub->updateDynamicBuffer(m_cache.uBufNormalize, entryIndex * uBufNormalizeElementSize, sizeof(NormalizeData), &normalizeData);
}
}
cb->resourceUpdate(rub);
rub = nullptr;
// Slice accumulation pass.
//
// On the first slice we use the clear-variant face targets (no PreserveColorContents)
// so the render pass clears the accumulator before the additive draw. This replaces
// the old pattern of a separate empty clear pass followed by a preserve pass, which
// was fragile on tile-based GPUs (the load in the preserve pass could see stale tile
// data from a prior clear pass that stored to the same surface).
//
// On subsequent slices we use the preserve-variant targets so the additive blend
// accumulates on top of the existing content.
if (fs.runPrefilterSlice) {
for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
QSSGSkyIblFaceTargets &sliceTargets = fs.isFirstSlice ? m_cache.accumClearFaceTargets[mipLevel]
: m_cache.accumPreserveFaceTargets[mipLevel];
const QSize mipSize(qMax(1, fs.environmentMapSize.width() >> mipLevel),
qMax(1, fs.environmentMapSize.height() >> mipLevel));
for (const auto face : QSSGRenderTextureCubeFaces) {
const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) },
{ 2, quint32(uBufSliceElementSize * mipLevel) } };
drawCubeFace(cb,
context.get(),
sliceTargets.renderTargets[quint8(face)],
mipSize,
m_cache.slicePipeline,
m_cache.sliceSrb,
fs.vbufBinding,
offsets,
QByteArrayLiteral("sky_ibl_prefilter_slice"),
QSSG_RENDERPASS_NAME("sky_ibl_prefilter_slice", mipLevel, face),
QColor(0, 0, 0, 0));
}
}
m_accumulatedSamples = fs.sliceSampleEnd;
if (fs.sliceCompletesCycle) {
if (inSky->iblRenderFrames >= 1) {
// Defer normalize+irradiance to a dedicated finalize frame.
// Set the flag here (after the slice actually ran) so the accumulators
// are guaranteed to contain data when the finalize fires.
m_finalizeIblPending = true;
} else {
// iblRenderFrames==0: normalize runs inline, mark converged now.
m_haveConvergedResult = true;
}
}
}
// Normalize accumulated samples into the prefiltered cube.
// For iblRenderFrames==0: runs every frame until convergence (progressive).
// For iblRenderFrames>=1: only runs in the dedicated finalize frame.
if (fs.writePrefilteredCubeThisFrame) {
for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
for (const auto face : QSSGRenderTextureCubeFaces) {
const int normalizeEntryIndex = mipLevel * 6 + quint8(face);
const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) },
{ 2, quint32(uBufNormalizeElementSize * normalizeEntryIndex) } };
drawCubeFace(cb,
context.get(),
prefilterTargets.mipRenderTargetsMap[mipLevel][quint8(face)],
prefilterTargets.mipLevelSizes[mipLevel],
m_cache.normalizeCubePipeline,
m_cache.normalizeSrbs[mipLevel],
fs.vbufBinding,
offsets,
QByteArrayLiteral("sky_ibl_prefilter_normalize"),
QSSG_RENDERPASS_NAME("sky_ibl_prefilter_normalize", mipLevel, face));
}
}
}
if (fs.runIrradiancePass) {
const int irradianceMip = prefilterTargets.mipmapCount - 1;
for (const auto face : QSSGRenderTextureCubeFaces) {
const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) }, { 2, 0u } };
drawCubeFace(cb,
context.get(),
prefilterTargets.mipRenderTargetsMap[irradianceMip][quint8(face)],
prefilterTargets.mipLevelSizes[irradianceMip],
m_cache.irradiancePipeline,
m_cache.irradianceSrb,
fs.vbufBinding,
offsets,
QByteArrayLiteral("sky_ibl_irradiance"),
QSSG_RENDERPASS_NAME("sky_ibl_irradiance", irradianceMip, face));
}
}
return true;
}
void QSSGRenderSkyMaterialManager::initializeTailMips(const FrameState &fs, QRhiCommandBuffer *cb)
{
if (!m_prefilteredCubeMap->flags().testFlag(QRhiTexture::MipMapped) || m_prefilteredTailMipsInitialized)
return;
const auto &context = m_context.rhiContext();
auto *rhi = context->rhi();
const int prefilteredFullMipCount = rhi->mipLevelsForSize(fs.environmentMapSize);
// Mips actually written this lifetime depend on enableIBL:
// * enableIBL=true: prefilter loop writes mips [0, prefilterMipCount), irradiance writes
// the last (prefilterTargets.mipmapCount-1). So mips [0, prefilterTargets.mipmapCount)
// are covered → init starts at prefilterTargets.mipmapCount.
// * enableIBL=false: prefilter loop writes only mip 0, irradiance is skipped → init starts at 1.
// The prefiltered cube is allocated mip-mapped in both modes for downstream sampler
// completeness; we just need to clear the tail.
const int firstUnwrittenMip = fs.enableIBL ? m_cache.prefilterTargets.mipmapCount : 1;
skyIblInitializeUnrenderedMips(rhi, cb, context.get(), m_prefilteredCubeMap, firstUnwrittenMip, prefilteredFullMipCount, "SkyMaterialLightProbe"_ba);
m_prefilteredTailMipsInitialized = true;
}
QT_END_NAMESPACE