Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qssgrenderskymaterialmanager.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include <QtQuick3DRuntimeRender/private/qssgrenderskymaterialmanager_p.h>
6
7#include <QtQuick3DRuntimeRender/private/qssgrenderskymaterial_p.h>
8#include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
9#include <QtQuick3DUtils/private/qssgassert_p.h>
10
14
15#include <QtCore/qmath.h>
16
17#include <cmath>
18
19using namespace Qt::StringLiterals;
20
21QT_BEGIN_NAMESPACE
22
23namespace {
24
25static constexpr QRhiTexture::Format cTextureFormat = QRhiTexture::RGBA16F;
26
27static int nearestPowerOfTwo(int v)
28{
29 if (v <= 1)
30 return 1;
31 int upper = qNextPowerOfTwo(v);
32 int lower = upper >> 1;
33 return (v - lower < upper - v) ? lower : upper;
34}
35
37{
38 auto lookAt = [](const QVector3D &eye, const QVector3D &center, const QVector3D &up) {
39 QMatrix4x4 viewMatrix;
40 viewMatrix.lookAt(eye, center, up);
41 return viewMatrix;
42 };
43
44 QVarLengthArray<QMatrix4x4, 6> views;
45 views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(1.0, 0.0, 0.0), QVector3D(0.0f, -1.0f, 0.0f)));
46 views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(-1.0, 0.0, 0.0), QVector3D(0.0f, -1.0f, 0.0f)));
47 if (rhi->isYUpInFramebuffer()) {
48 views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0f, 0.0f, 1.0f)));
49 views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, -1.0, 0.0), QVector3D(0.0f, 0.0f, -1.0f)));
50 } else {
51 views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, -1.0, 0.0), QVector3D(0.0f, 0.0f, -1.0f)));
52 views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0f, 0.0f, 1.0f)));
53 }
54 views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 0.0, 1.0), QVector3D(0.0f, -1.0f, 0.0f)));
55 views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 0.0, -1.0), QVector3D(0.0f, -1.0f, 0.0f)));
56 return views;
57}
58
59// Creates 6 face render targets for a texture, always at mip level 0.
60// preserveColorContents controls whether the render target loads existing
61// content before drawing (additive accumulation) or clears it (first slice).
62// Using level=0 unconditionally is intentional: on Android GLES some drivers
63// silently ignore the level parameter to glFramebufferTextureLayer for level>0,
64// so the accumulator is allocated as separate per-mip non-mipmapped textures
65// and each is always attached at level 0.
66static bool skyIblCreateFaceTargets(QRhi *rhi,
67 QRhiTexture *texture,
68 const QByteArray &namePrefix,
69 QSSGSkyIblFaceTargets *outTargets,
70 bool preserveColorContents = false)
71{
72 Q_ASSERT(outTargets);
73 const QRhiTextureRenderTarget::Flags rtFlags = preserveColorContents
74 ? QRhiTextureRenderTarget::Flags(QRhiTextureRenderTarget::PreserveColorContents)
75 : QRhiTextureRenderTarget::Flags();
76 for (const auto face : QSSGRenderTextureCubeFaces) {
77 QRhiColorAttachment att(texture);
78 att.setLayer(quint8(face));
79 // No setLevel() call — always attaches at mip level 0. Each per-mip
80 // accumulator texture has only one mip level, so this is correct.
81 QRhiTextureRenderTargetDescription rtDesc;
82 rtDesc.setColorAttachments({ att });
83 auto renderTarget = rhi->newTextureRenderTarget(rtDesc, rtFlags);
84 renderTarget->setName(namePrefix + "/"_ba + QSSGBaseTypeHelpers::displayName(face));
85 renderTarget->setDescription(rtDesc);
86 if (!outTargets->renderPassDesc)
87 outTargets->renderPassDesc = renderTarget->newCompatibleRenderPassDescriptor();
88 renderTarget->setRenderPassDescriptor(outTargets->renderPassDesc);
89 if (!renderTarget->create()) {
90 qWarning("Failed to build sky IBL env map render target");
91 return false;
92 }
93 outTargets->renderTargets << renderTarget;
94 }
95 return true;
96}
97
98static bool skyIblCreatePrefilterTargets(QRhi *rhi,
99 QRhiTexture *texture,
100 const QSize &environmentMapSize,
101 const QByteArray &namePrefix,
102 QSSGSkyIblPrefilterTargets *outTargets,
103 bool preserveColorContents = false)
104{
105 Q_ASSERT(outTargets);
106 const bool hasMips = texture->flags().testFlag(QRhiTexture::MipMapped);
107 outTargets->mipmapCount = hasMips ? qMin(rhi->mipLevelsForSize(environmentMapSize), 6) : 1;
108 outTargets->mipLevelSizes.resize(outTargets->mipmapCount);
109 outTargets->mipRenderTargetsMap.resize(outTargets->mipmapCount);
110
111 const QRhiTextureRenderTarget::Flags rtFlags = preserveColorContents
112 ? QRhiTextureRenderTarget::Flags(QRhiTextureRenderTarget::PreserveColorContents)
113 : QRhiTextureRenderTarget::Flags();
114
115 auto cleanup = [outTargets](QRhiTextureRenderTarget *failed, const QSSGSkyIblCubeFaceRenderTargets &partial) {
116 delete failed;
117 for (auto *rt : partial)
118 delete rt;
119 for (const QSSGSkyIblCubeFaceRenderTargets &rts : std::as_const(outTargets->mipRenderTargetsMap)) {
120 for (auto *rt : rts)
121 delete rt;
122 }
123 delete outTargets->renderPassDesc;
124 outTargets->renderPassDesc = nullptr;
125 outTargets->mipRenderTargetsMap.clear();
126 outTargets->mipLevelSizes.clear();
127 outTargets->mipmapCount = 0;
128 };
129
130 for (int mipLevel = 0; mipLevel < outTargets->mipmapCount; ++mipLevel) {
131 QSSGSkyIblCubeFaceRenderTargets renderTargets;
132 for (const auto face : QSSGRenderTextureCubeFaces) {
133 QRhiColorAttachment att(texture);
134 att.setLayer(quint8(face));
135 att.setLevel(mipLevel);
136 QRhiTextureRenderTargetDescription rtDesc;
137 rtDesc.setColorAttachments({ att });
138 auto renderTarget = rhi->newTextureRenderTarget(rtDesc, rtFlags);
139 renderTarget->setName(namePrefix + QByteArrayLiteral("/m") + QByteArray::number(mipLevel)
140 + QByteArrayLiteral("/") + QSSGBaseTypeHelpers::displayName(face));
141 renderTarget->setDescription(rtDesc);
142 if (!outTargets->renderPassDesc)
143 outTargets->renderPassDesc = renderTarget->newCompatibleRenderPassDescriptor();
144 renderTarget->setRenderPassDescriptor(outTargets->renderPassDesc);
145 if (!renderTarget->create()) {
146 qWarning("Failed to build sky IBL prefilter env map render target");
147 cleanup(renderTarget, renderTargets);
148 return false;
149 }
150 renderTargets << renderTarget;
151 }
152 const QSize levelSize(environmentMapSize.width() * std::pow(0.5, mipLevel),
153 environmentMapSize.height() * std::pow(0.5, mipLevel));
154 outTargets->mipLevelSizes[mipLevel] = levelSize;
155 outTargets->mipRenderTargetsMap[mipLevel] = renderTargets;
156 }
157 return true;
158}
159
160static void skyIblInitializeUnrenderedMips(QRhi *rhi,
161 QRhiCommandBuffer *cb,
162 QSSGRhiContext *context,
163 QRhiTexture *texture,
164 int firstMip,
165 int mipCountExclusive,
166 const QByteArray &debugObjectName)
167{
168 if (firstMip >= mipCountExclusive)
169 return;
170 QRhiRenderPassDescriptor *rpDesc = nullptr;
171 for (int mipLevel = firstMip; mipLevel < mipCountExclusive; ++mipLevel) {
172 for (const auto face : QSSGRenderTextureCubeFaces) {
173 QRhiColorAttachment att(texture);
174 att.setLayer(quint8(face));
175 att.setLevel(mipLevel);
176 QRhiTextureRenderTargetDescription rtDesc;
177 rtDesc.setColorAttachments({ att });
178 auto *rt = rhi->newTextureRenderTarget(rtDesc);
179 rt->setName(debugObjectName + "/init/m"_ba + QByteArray::number(mipLevel) + "/"_ba
180 + QSSGBaseTypeHelpers::displayName(face));
181 rt->setDescription(rtDesc);
182 if (!rpDesc)
183 rpDesc = rt->newCompatibleRenderPassDescriptor();
184 rt->setRenderPassDescriptor(rpDesc);
185 if (!rt->create()) {
186 qWarning("Failed to create sky IBL init render target");
187 delete rt;
188 continue;
189 }
190 rt->deleteLater();
191 cb->beginPass(rt, QColor(0, 0, 0, 1), { 1.0f, 0 }, nullptr, context->commonPassFlags());
192 cb->endPass();
193 }
194 }
195 if (rpDesc)
196 rpDesc->deleteLater();
197}
198
199static const float skyIblCubeVerts[] = {
200 -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f,
201 -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f,
202
203 -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,
204 -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f,
205
206 -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f,
207 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f,
208
209 -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f,
210 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f,
211
212 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f,
213 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f,
214
215 -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f,
216 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
217
218 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
219
220 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f,
221
222 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
223
224 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
225
226 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,
227
228 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,
229};
230
231static bool ensureDynamicUBuf(QRhi *rhi, QRhiBuffer *&dst, int size, const char *errorMessage)
232{
233 if (dst)
234 return true;
235 dst = rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, size);
236 if (!dst->create()) {
237 qWarning("%s", errorMessage);
238 delete dst;
239 dst = nullptr;
240 return false;
241 }
242 return true;
243}
244
245static bool ensureSrb(QRhi *rhi, QRhiShaderResourceBindings *&dst, std::initializer_list<QRhiShaderResourceBinding> bindings, const char *errorMessage)
246{
247 if (dst)
248 return true;
249 dst = rhi->newShaderResourceBindings();
250 dst->setBindings(bindings);
251 if (!dst->create()) {
252 qWarning("%s", errorMessage);
253 delete dst;
254 dst = nullptr;
255 return false;
256 }
257 return true;
258}
259
261{
262 QSSGRhiShaderPipeline *shader = nullptr;
265 bool additiveBlend = false;
266};
267
269 const QRhiVertexInputLayout &inputLayout,
270 const PrefilterPipelineConfig &cfg,
271 const char *errorMessage)
272{
273 auto *pipeline = rhi->newGraphicsPipeline();
274 pipeline->setCullMode(QRhiGraphicsPipeline::Front);
275 pipeline->setFrontFace(QRhiGraphicsPipeline::CCW);
276 pipeline->setDepthOp(QRhiGraphicsPipeline::LessOrEqual);
277 pipeline->setShaderStages({ *cfg.shader->vertexStage(), *cfg.shader->fragmentStage() });
278 pipeline->setVertexInputLayout(inputLayout);
279 pipeline->setShaderResourceBindings(cfg.srb);
280 pipeline->setRenderPassDescriptor(cfg.rpd);
281 pipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
282 if (cfg.additiveBlend) {
283 QRhiGraphicsPipeline::TargetBlend addBlend;
284 addBlend.enable = true;
285 addBlend.srcColor = QRhiGraphicsPipeline::One;
286 addBlend.dstColor = QRhiGraphicsPipeline::One;
287 addBlend.opColor = QRhiGraphicsPipeline::Add;
288 addBlend.srcAlpha = QRhiGraphicsPipeline::One;
289 addBlend.dstAlpha = QRhiGraphicsPipeline::One;
290 addBlend.opAlpha = QRhiGraphicsPipeline::Add;
291 pipeline->setTargetBlends({ addBlend });
292 }
293 if (!pipeline->create()) {
294 qWarning("%s", errorMessage);
295 delete pipeline;
296 return nullptr;
297 }
298 return pipeline;
299}
300
301static void drawCubeFace(QRhiCommandBuffer *cb,
302 QSSGRhiContext *ctx,
303 QRhiTextureRenderTarget *rt,
304 QSize viewport,
305 QRhiGraphicsPipeline *pipeline,
306 QRhiShaderResourceBindings *srb,
307 const QRhiCommandBuffer::VertexInput &vbufBinding,
308 const QVector<QPair<int, quint32>> &dynamicOffsets,
309 const QByteArray &profilerLabel,
310 const QByteArray &passDebugLabel,
311 const QColor &clearColor = QColor(0, 0, 0, 1))
312{
313 cb->beginPass(rt, clearColor, { 1.0f, 0 }, nullptr, ctx->commonPassFlags());
314 QSSGRHICTX_STAT(ctx, beginRenderPass(rt));
315 Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderPass);
316 cb->setViewport(QRhiViewport(0, 0, viewport.width(), viewport.height()));
317 cb->setScissor(QRhiScissor(0, 0, viewport.width(), viewport.height()));
318 cb->setGraphicsPipeline(pipeline);
319 cb->setShaderResources(srb, dynamicOffsets.size(), dynamicOffsets.constData());
320 cb->setVertexInput(0, 1, &vbufBinding);
321 Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderCall);
322 cb->draw(36);
323 QSSGRHICTX_STAT(ctx, draw(36, 1));
324 Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderCall, 36llu | (1llu << 32), profilerLabel);
325 cb->endPass();
326 QSSGRHICTX_STAT(ctx, endRenderPass());
327 Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderPass, 0, passDebugLabel);
328}
329
330} // namespace
331
332// Per-frame derived state passed between phase methods
333struct QSSGRenderSkyMaterialManager::FrameState
334{
335 // computeFrameState
336 QSize environmentMapSize;
337 int totalSamples = 0;
338 bool enableIBL = false;
339 bool needCreateEnv = false;
340 bool inProgressTimeSlice = false;
341 bool envContentDirty = false;
342 bool deferEnvRefresh = false;
343 bool needRenderEnv = false;
344
345 // ensureTextures
346 bool prefilteredJustCreated = false;
347 int prefilterTotalMipCount = 0;
348 int prefilterSpecularMipCount = 0;
349 int prefilterRoughnessDenom = 1;
350 float resolution = 0.0f;
351
352 // deriveCycleState
353 bool multiFrame = false;
354 bool prefilterIsConverged = false;
355 bool runPrefilterSlice = false;
356 int perFrameBudget = 0;
357 int sliceSamplesThisFrame = 0;
358 int sliceSampleStart = 0;
359 int sliceSampleEnd = 0;
360 bool sliceCompletesCycle = false;
361 bool writePrefilteredCubeThisFrame = false;
362 bool runIrradiancePass = false;
363 bool haveConvergedResultEntering = false;
364 bool isFirstSlice = false;
365
366 // ensureSharedResources
367 QRhiVertexInputLayout inputLayout;
368 QMatrix4x4 mvp;
369 QVarLengthArray<QMatrix4x4, 6> views;
370 int ubufElementSize = 0;
371 QRhiCommandBuffer::VertexInput vbufBinding { nullptr, 0 };
372};
373
374QSSGRenderSkyMaterialManager::QSSGRenderSkyMaterialManager(const QSSGRenderContextInterface &inContext)
375 : m_context(inContext)
376{
377}
378
379QSSGRenderSkyMaterialManager::~QSSGRenderSkyMaterialManager()
380{
381 releaseCachedResources();
382}
383
384void QSSGRenderSkyMaterialManager::clearPrefilterCache()
385{
386 auto releasePrefilterTargets = [](QSSGSkyIblPrefilterTargets &t) {
387 for (const QSSGSkyIblCubeFaceRenderTargets &renderTargets : std::as_const(t.mipRenderTargetsMap)) {
388 for (QRhiTextureRenderTarget *renderTarget : renderTargets)
389 delete renderTarget;
390 }
391 delete t.renderPassDesc;
392 t.renderPassDesc = nullptr;
393 t.mipRenderTargetsMap.clear();
394 t.mipLevelSizes.clear();
395 t.mipmapCount = 0;
396 };
397
398 auto releaseFaceTargets = [](QSSGSkyIblFaceTargets &t) {
399 for (QRhiTextureRenderTarget *rt : t.renderTargets)
400 delete rt;
401 delete t.renderPassDesc;
402 t.renderPassDesc = nullptr;
403 t.renderTargets.clear();
404 };
405
406 auto safeDelete = [](auto *&res) {
407 delete res;
408 res = nullptr;
409 };
410
411 safeDelete(m_cache.vertexBuffer);
412 safeDelete(m_cache.uBuf);
413 safeDelete(m_cache.uBufSlice);
414 safeDelete(m_cache.uBufNormalize);
415 safeDelete(m_cache.uBufIrradiance);
416
417 releaseFaceTargets(m_cache.envFaceTargets);
418 releasePrefilterTargets(m_cache.prefilterTargets);
419
420 // Release per-mip accumulator face targets (preserve and clear variants)
421 for (QSSGSkyIblFaceTargets &t : m_cache.accumPreserveFaceTargets)
422 releaseFaceTargets(t);
423 m_cache.accumPreserveFaceTargets.clear();
424
425 for (QSSGSkyIblFaceTargets &t : m_cache.accumClearFaceTargets)
426 releaseFaceTargets(t);
427 m_cache.accumClearFaceTargets.clear();
428
429 // Release per-mip normalize SRBs
430 for (QRhiShaderResourceBindings *srb : std::as_const(m_cache.normalizeSrbs))
431 delete srb;
432 m_cache.normalizeSrbs.clear();
433
434 safeDelete(m_cache.sliceSrb);
435 safeDelete(m_cache.irradianceSrb);
436
437 safeDelete(m_cache.envMapPipeline);
438 safeDelete(m_cache.slicePipeline);
439 safeDelete(m_cache.normalizeCubePipeline);
440 safeDelete(m_cache.irradiancePipeline);
441
442 m_cache.environmentMapSize = { };
443 m_cache.enableIBL = false;
444 m_cache.prefilterTotalMipCount = 0;
445 m_cache.envCubeMap = nullptr;
446 m_cache.prefilteredCubeMap = nullptr;
447 m_cache.prefilterAccumulators.clear();
448 m_cache.envShaderPipeline = nullptr;
449}
450
451void QSSGRenderSkyMaterialManager::releaseCachedResources()
452{
453 clearPrefilterCache();
454 if (auto rhiCtx = m_context.rhiContext().get(); QSSG_GUARD(rhiCtx && rhiCtx->isValid())) {
455 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
456 if (m_envCubeMap)
457 rhiCtxD->releaseTexture(m_envCubeMap);
458 if (m_prefilteredCubeMap)
459 rhiCtxD->releaseTexture(m_prefilteredCubeMap);
460 for (QRhiTexture *t : std::as_const(m_prefilterAccumulators))
461 rhiCtxD->releaseTexture(t);
462 }
463 m_envCubeMap = nullptr;
464 m_prefilteredCubeMap = nullptr;
465 m_prefilterAccumulators.clear();
466 m_skyIblTexture = { };
467 m_cubeMapSize = { };
468 m_prefilteredMipCount = 0;
469 m_accumulatedSamples = 0;
470 m_accumIblSampleCount = 0;
471 m_haveConvergedResult = false;
472 m_envTailMipsInitialized = false;
473 m_prefilteredTailMipsInitialized = false;
474}
475
476QSSGRenderImageTexture QSSGRenderSkyMaterialManager::resolve(QSSGRenderSkyMaterial *settings)
477{
478 const auto &rhiCtx = m_context.rhiContext();
479 if (!QSSG_GUARD(rhiCtx && rhiCtx->isValid() && rhiCtx->rhi()->isRecordingFrame()))
480 return { };
481
482 if (!ensureEnvironmentMap(settings)) {
483 return { };
484 }
485
486 const bool pendingAccumulation = settings->enableIBL && m_accumulatedSamples < settings->iblSampleCount;
487 settings->wantsMoreFrames = pendingAccumulation || settings->isDirty;
488
489 return m_skyIblTexture;
490}
491
492bool QSSGRenderSkyMaterialManager::ensureEnvironmentMap(QSSGRenderSkyMaterial *inSky)
493{
494 const auto &context = m_context.rhiContext();
495 if (!context->rhi()->isTextureFormatSupported(cTextureFormat)) {
496 static bool warningPrinted = false;
497 if (Q_UNLIKELY(!warningPrinted)) {
498 qWarning() << "SkyMaterial not supported due to missing RGBA16F texture format support.";
499 warningPrinted = true;
500 }
501 return false;
502 }
503
504 auto *cb = context->commandBuffer();
505
506 FrameState fs;
507 if (!computeFrameState(inSky, fs))
508 return false;
509
510 if (!ensureTextures(fs))
511 return false;
512
513 deriveCycleState(inSky, fs);
514
515 QSSGRhiShaderPipelinePtr shaderPipeline;
516 if (fs.needRenderEnv) {
517 shaderPipeline = inSky->ensurePipeline(m_context);
518 if (!shaderPipeline) {
519 return false;
520 }
521 }
522 QSSGRhiShaderPipeline *envShaderPipelineKey = fs.needRenderEnv ? shaderPipeline.get() : m_cache.envShaderPipeline;
523
524 validateAndUpdateCacheKey(fs, envShaderPipelineKey);
525
526 if (!ensureSharedResources(fs, cb))
527 return false;
528
529 auto *rub = context->rhi()->nextResourceUpdateBatch();
530 for (const auto face : QSSGRenderTextureCubeFaces) {
531 rub->updateDynamicBuffer(m_cache.uBuf, quint8(face) * fs.ubufElementSize, 64, fs.mvp.constData());
532 rub->updateDynamicBuffer(m_cache.uBuf, quint8(face) * fs.ubufElementSize + 64, 64, fs.views[quint8(face)].constData());
533 }
534
535 if (fs.runIrradiancePass) {
536 struct IrradianceData
537 {
538 float roughness;
539 float resolution;
540 float lodBias;
541 int sampleCount;
542 int distribution;
543 } irradianceData;
544 irradianceData.roughness = 0.0f;
545 irradianceData.resolution = fs.resolution;
546 irradianceData.lodBias = 0.0f;
547 irradianceData.distribution = 0;
548 irradianceData.sampleCount = qMax(int(fs.resolution / 4.0f), 1);
549 rub->updateDynamicBuffer(m_cache.uBufIrradiance, 0, sizeof(IrradianceData), &irradianceData);
550 }
551
552 if (fs.needRenderEnv) {
553 if (!renderEnvironmentCube(inSky, fs, shaderPipeline, cb, rub))
554 return false;
555 rub = nullptr;
556 }
557
558 if (fs.enableIBL && fs.needRenderEnv)
559 runEnvironmentMipChain(fs, cb);
560
561 if (!rub)
562 rub = context->rhi()->nextResourceUpdateBatch();
563
564 cb->debugMarkBegin("Sky IBL Pre-filtered Environment Cubemap Generation");
565 if (!runPrefilterCycle(inSky, fs, cb, rub))
566 return false;
567 if (rub) {
568 cb->resourceUpdate(rub);
569 rub = nullptr;
570 }
571 cb->debugMarkEnd();
572
573 m_prefilteredMipCount = m_cache.prefilterTargets.mipmapCount;
574 initializeTailMips(fs, cb);
575
576 m_skyIblTexture.m_texture = m_prefilteredCubeMap;
577 m_skyIblTexture.m_mipmapCount = m_prefilteredMipCount;
578 m_skyIblTexture.m_flags.setLinear(true);
579 m_skyIblTexture.m_flags.setRgbe8(false);
580 return true;
581}
582
583bool QSSGRenderSkyMaterialManager::computeFrameState(QSSGRenderSkyMaterial *inSky, FrameState &fs)
584{
585 fs.enableIBL = inSky->enableIBL;
586 fs.totalSamples = qBound(1, inSky->iblSampleCount, 1024);
587
588 const int radianceMapSize = qBound(8, nearestPowerOfTwo(inSky->radianceMapSize), 2048);
589 fs.environmentMapSize = QSize(radianceMapSize, radianceMapSize);
590
591 const bool envWantsMips = fs.enableIBL;
592 const bool envHasMips = m_envCubeMap && m_envCubeMap->flags().testFlag(QRhiTexture::MipMapped);
593 fs.needCreateEnv = !m_envCubeMap || m_cubeMapSize != fs.environmentMapSize || envHasMips != envWantsMips;
594
595 fs.inProgressTimeSlice = fs.enableIBL && m_accumulatedSamples > 0 && m_accumulatedSamples < m_accumIblSampleCount;
596 fs.envContentDirty = inSky->isDirty && !fs.needCreateEnv;
597 fs.deferEnvRefresh = fs.envContentDirty && fs.inProgressTimeSlice;
598 fs.needRenderEnv = fs.needCreateEnv || (fs.envContentDirty && !fs.deferEnvRefresh);
599 return true;
600}
601
602bool QSSGRenderSkyMaterialManager::ensureTextures(FrameState &fs)
603{
604 const auto &context = m_context.rhiContext();
605 auto *rhi = context->rhi();
606 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(context.get());
607
608 // --- Environment cube ---
609 if (fs.needCreateEnv) {
610 if (m_envCubeMap) {
611 rhiCtxD->releaseTexture(m_envCubeMap);
612 m_envCubeMap = nullptr;
613 }
614 // The prefiltered cube derives from m_envCubeMap and must be invalidated alongside.
615 if (m_prefilteredCubeMap) {
616 rhiCtxD->releaseTexture(m_prefilteredCubeMap);
617 m_prefilteredCubeMap = nullptr;
618 m_prefilteredMipCount = 0;
619 }
620 m_envTailMipsInitialized = false;
621 m_prefilteredTailMipsInitialized = false;
622
623 QRhiTexture::Flags envFlags = QRhiTexture::RenderTarget | QRhiTexture::CubeMap;
624 if (fs.enableIBL)
625 envFlags |= QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips;
626 m_envCubeMap = rhi->newTexture(cTextureFormat, fs.environmentMapSize, 1, envFlags);
627 if (!m_envCubeMap->create()) {
628 qWarning("Failed to create Sky IBL environment cube map");
629 delete m_envCubeMap;
630 m_envCubeMap = nullptr;
631 return false;
632 }
633 m_envCubeMap->setName("SkyMaterialLightProbe procEnvCube"_ba);
634 rhiCtxD->registerTexture(m_envCubeMap);
635 m_cubeMapSize = fs.environmentMapSize;
636 }
637
638 if (!m_prefilteredCubeMap) {
639 const QRhiTexture::Flags pfFlags = QRhiTexture::RenderTarget | QRhiTexture::CubeMap | QRhiTexture::MipMapped;
640 m_prefilteredCubeMap = rhi->newTexture(cTextureFormat, fs.environmentMapSize, 1, pfFlags);
641 if (!m_prefilteredCubeMap->create()) {
642 qWarning("Failed to create Sky IBL pre-filtered environment cube map");
643 delete m_prefilteredCubeMap;
644 m_prefilteredCubeMap = nullptr;
645 return false;
646 }
647 m_prefilteredCubeMap->setName("SkyMaterialLightProbe"_ba);
648 rhiCtxD->registerTexture(m_prefilteredCubeMap);
649 fs.prefilteredJustCreated = true;
650 m_haveConvergedResult = false;
651 m_prefilteredTailMipsInitialized = false;
652 }
653
654 fs.prefilterTotalMipCount = m_prefilteredCubeMap->flags().testFlag(QRhiTexture::MipMapped)
655 ? qMin(rhi->mipLevelsForSize(fs.environmentMapSize), 6)
656 : 1;
657 fs.prefilterSpecularMipCount = fs.enableIBL ? fs.prefilterTotalMipCount - 1 : 1;
658 fs.prefilterRoughnessDenom = qMax(fs.prefilterSpecularMipCount - 1, 1);
659 fs.resolution = float(fs.environmentMapSize.width());
660
661 // Determine whether the existing per-mip accumulator set is still valid.
662 // We need one texture per specular mip level, each sized for that mip.
663 const bool needCreateAccum = m_prefilterAccumulators.isEmpty() || m_prefilterAccumulators.size() != fs.prefilterSpecularMipCount
664 || (!m_prefilterAccumulators.isEmpty() && m_prefilterAccumulators[0]->pixelSize() != fs.environmentMapSize);
665
666 if (needCreateAccum) {
667 for (QRhiTexture *t : std::as_const(m_prefilterAccumulators))
668 rhiCtxD->releaseTexture(t);
669 m_prefilterAccumulators.clear();
670 m_accumulatedSamples = 0;
671
672 // Allocate one non-mipmapped 2D array texture per specular mip level.
673 // Each texture is sized for its mip level and only has mip 0.
674 //
675 // Rationale: on Android GLES (Adreno, Mali) glFramebufferTextureLayer
676 // with level > 0 is sometimes silently broken. The driver accepts the
677 // FBO as complete but writes to the wrong location or ignores the level
678 // entirely. By giving each mip level its own texture and always attaching
679 // at level 0, we avoid the broken code path entirely.
680 bool ok = true;
681 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
682 const QSize mipSize(qMax(1, fs.environmentMapSize.width() >> mip), qMax(1, fs.environmentMapSize.height() >> mip));
683 // No MipMapped flag — single level only, always attached at level 0.
684 auto *t = rhi->newTextureArray(cTextureFormat, 6, mipSize, 1, QRhiTexture::RenderTarget);
685 if (!t->create()) {
686 qWarning("Failed to create Sky IBL prefilter accumulator mip %d", mip);
687 delete t;
688 ok = false;
689 break;
690 }
691 t->setName("SkyMaterialLightProbe procEnvPfAccum m"_ba + QByteArray::number(mip));
692 rhiCtxD->registerTexture(t);
693 m_prefilterAccumulators.append(t);
694 }
695
696 if (!ok) {
697 for (QRhiTexture *t : std::as_const(m_prefilterAccumulators))
698 rhiCtxD->releaseTexture(t);
699 m_prefilterAccumulators.clear();
700 // Accumulator unavailable — fall back to single-frame prefilter.
701 } else {
702 // No tail-mip initialization needed: each texture has exactly one
703 // mip level (level 0), so there are no unwritten tail mips.
704 }
705 }
706
707 return true;
708}
709
710void QSSGRenderSkyMaterialManager::deriveCycleState(QSSGRenderSkyMaterial *inSky, FrameState &fs)
711{
712 fs.multiFrame = fs.enableIBL && inSky->iblSamplesPerFrame > 0 && inSky->iblSamplesPerFrame < fs.totalSamples;
713
714 // Restart the accumulator if anything stale: env content changed, prefiltered cube was
715 // re-created, or the sample-count target changed.
716 if (!m_prefilterAccumulators.isEmpty()) {
717 const bool resetAccumulation = fs.needRenderEnv || fs.prefilteredJustCreated || m_accumIblSampleCount != fs.totalSamples;
718 if (resetAccumulation)
719 m_accumulatedSamples = 0;
720 m_accumIblSampleCount = fs.totalSamples;
721 }
722
723 fs.prefilterIsConverged = !m_prefilterAccumulators.isEmpty() && m_accumulatedSamples >= fs.totalSamples;
724 fs.runPrefilterSlice = !m_prefilterAccumulators.isEmpty() && !fs.prefilterIsConverged;
725
726 // perFrameBudget = samples to integrate this frame.
727 // Single-frame (iblSamplesPerFrame <= 0) or !enableIBL: all remaining samples.
728 // Multi-frame: the requested budget.
729 fs.perFrameBudget = (inSky->iblSamplesPerFrame > 0 && fs.enableIBL) ? inSky->iblSamplesPerFrame
730 : (fs.totalSamples - m_accumulatedSamples);
731 fs.sliceSamplesThisFrame = fs.runPrefilterSlice ? qMin(fs.perFrameBudget, fs.totalSamples - m_accumulatedSamples) : 0;
732 fs.sliceCompletesCycle = fs.runPrefilterSlice && (m_accumulatedSamples + fs.sliceSamplesThisFrame >= fs.totalSamples);
733
734 fs.sliceSampleStart = m_accumulatedSamples;
735 fs.sliceSampleEnd = fs.runPrefilterSlice ? qMin(m_accumulatedSamples + fs.perFrameBudget, fs.totalSamples) : 0;
736 fs.isFirstSlice = m_accumulatedSamples == 0;
737 fs.haveConvergedResultEntering = m_haveConvergedResult;
738
739 // Seed cycle (no converged result yet) publishes to the cube every frame so the user
740 // sees progressive convergence. Established cycles only publish on cycle completion.
741 fs.writePrefilteredCubeThisFrame = fs.runPrefilterSlice && (fs.sliceCompletesCycle || !m_haveConvergedResult);
742 fs.runIrradiancePass = fs.enableIBL && fs.writePrefilteredCubeThisFrame;
743}
744
745void QSSGRenderSkyMaterialManager::validateAndUpdateCacheKey(const FrameState &fs, QSSGRhiShaderPipeline *envShaderPipelineKey)
746{
747 const bool cacheValid = m_cache.environmentMapSize == fs.environmentMapSize && m_cache.enableIBL == fs.enableIBL
748 && m_cache.prefilterTotalMipCount == fs.prefilterTotalMipCount && m_cache.envCubeMap == m_envCubeMap
749 && m_cache.prefilteredCubeMap == m_prefilteredCubeMap && m_cache.prefilterAccumulators == m_prefilterAccumulators
750 && m_cache.envShaderPipeline == envShaderPipelineKey;
751 if (cacheValid)
752 return;
753 clearPrefilterCache();
754 m_cache.environmentMapSize = fs.environmentMapSize;
755 m_cache.enableIBL = fs.enableIBL;
756 m_cache.prefilterTotalMipCount = fs.prefilterTotalMipCount;
757 m_cache.envCubeMap = m_envCubeMap;
758 m_cache.prefilteredCubeMap = m_prefilteredCubeMap;
759 m_cache.prefilterAccumulators = m_prefilterAccumulators;
760 m_cache.envShaderPipeline = envShaderPipelineKey;
761}
762
763bool QSSGRenderSkyMaterialManager::ensureSharedResources(FrameState &fs, QRhiCommandBuffer *cb)
764{
765 const auto &context = m_context.rhiContext();
766 auto *rhi = context->rhi();
767
768 fs.inputLayout.setBindings({ { 3 * sizeof(float) } });
769 fs.inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 } });
770
771 fs.mvp = rhi->clipSpaceCorrMatrix();
772 fs.mvp.perspective(90.0f, 1.0f, 0.1f, 10.0f);
773 fs.views = skyIblEnvironmentMapViews(rhi);
774
775 fs.ubufElementSize = rhi->ubufAligned(128);
776
777 if (!m_cache.vertexBuffer) {
778 m_cache.vertexBuffer = rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(skyIblCubeVerts));
779 if (!m_cache.vertexBuffer->create()) {
780 qWarning("Failed to create sky IBL vertex buffer");
781 delete m_cache.vertexBuffer;
782 m_cache.vertexBuffer = nullptr;
783 return false;
784 }
785 auto *initRub = rhi->nextResourceUpdateBatch();
786 initRub->uploadStaticBuffer(m_cache.vertexBuffer, skyIblCubeVerts);
787 cb->resourceUpdate(initRub);
788 }
789 if (!ensureDynamicUBuf(rhi, m_cache.uBuf, fs.ubufElementSize * 6, "Failed to create sky IBL view uniform buffer"))
790 return false;
791 fs.vbufBinding = QRhiCommandBuffer::VertexInput(m_cache.vertexBuffer, 0);
792
793 if (!m_cache.prefilterTargets.renderPassDesc) {
794 if (!skyIblCreatePrefilterTargets(rhi, m_prefilteredCubeMap, fs.environmentMapSize, "SkyMaterialLightProbe procEnvPf"_ba, &m_cache.prefilterTargets)) {
795 return false;
796 }
797 }
798
799 if (!ensureDynamicUBuf(rhi, m_cache.uBufIrradiance, rhi->ubufAligned(20), "Failed to create sky IBL irradiance uniform buffer"))
800 return false;
801
802 const QSSGRhiSamplerDescription samplerNoMipDesc { QRhiSampler::Linear, QRhiSampler::Linear,
803 QRhiSampler::None, QRhiSampler::ClampToEdge,
804 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
805 QRhiSampler *envMapCubeNoMipSampler = context->sampler(samplerNoMipDesc);
806
807 if (!ensureSrb(rhi,
808 m_cache.irradianceSrb,
809 { QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
810 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufIrradiance, 20),
811 QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_envCubeMap, envMapCubeNoMipSampler) },
812 "Failed to create sky IBL irradiance SRB"))
813 return false;
814
815 if (!m_cache.irradiancePipeline) {
816 const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhienvironmentmapPreFilterShader(false);
817 m_cache.irradiancePipeline = createPrefilterPipeline(rhi,
818 fs.inputLayout,
819 { shader.get(),
820 m_cache.irradianceSrb,
821 m_cache.prefilterTargets.renderPassDesc,
822 false },
823 "Failed to create sky IBL realtime irradiance pipeline "
824 "state");
825 if (!m_cache.irradiancePipeline)
826 return false;
827 }
828 return true;
829}
830
831bool QSSGRenderSkyMaterialManager::renderEnvironmentCube(QSSGRenderSkyMaterial *inSky,
832 const FrameState &fs,
833 const QSSGRhiShaderPipelinePtr &shaderPipeline,
834 QRhiCommandBuffer *cb,
835 QRhiResourceUpdateBatch *rub)
836{
837 const auto &context = m_context.rhiContext();
838 auto *rhi = context->rhi();
839 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(context.get());
840
841 const quint32 skyElementSize = inSky->updateUniforms(m_context, fs.mvp, fs.views);
842
843 if (!m_cache.envFaceTargets.renderPassDesc) {
844 if (!skyIblCreateFaceTargets(rhi, m_envCubeMap, "SkyMaterialLightProbe procEnvCube"_ba, &m_cache.envFaceTargets))
845 return false;
846 }
847
848 QRhiShaderResourceBindings *envSrb = rhiCtxD->srb(inSky->bindings);
849 Q_ASSERT(envSrb);
850
851 if (!m_cache.envMapPipeline) {
852 m_cache.envMapPipeline = createPrefilterPipeline(rhi,
853 fs.inputLayout,
854 { shaderPipeline.get(), envSrb, m_cache.envFaceTargets.renderPassDesc, false },
855 "Failed to create sky IBL env map pipeline state");
856 if (!m_cache.envMapPipeline)
857 return false;
858 }
859
860 cb->resourceUpdate(rub);
861
862 cb->debugMarkBegin("Sky IBL Procedural Environment Cubemap Generation");
863 for (const auto face : QSSGRenderTextureCubeFaces) {
864 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(skyElementSize * quint8(face)) } };
865 drawCubeFace(cb,
866 context.get(),
867 m_cache.envFaceTargets.renderTargets[quint8(face)],
868 fs.environmentMapSize,
869 m_cache.envMapPipeline,
870 envSrb,
871 fs.vbufBinding,
872 offsets,
873 QByteArrayLiteral("sky_ibl_procedural_environment_map"),
874 QSSG_RENDERPASS_NAME("sky_ibl_procedural_environment_map", 0, face));
875 }
876 cb->debugMarkEnd();
877
878 inSky->isDirty = false;
879 return true;
880}
881
882void QSSGRenderSkyMaterialManager::runEnvironmentMipChain(const FrameState &fs, QRhiCommandBuffer *cb)
883{
884 const auto &context = m_context.rhiContext();
885 auto *rhi = context->rhi();
886
887 if (!m_envTailMipsInitialized) {
888 const int envFullMipCount = rhi->mipLevelsForSize(fs.environmentMapSize);
889 skyIblInitializeUnrenderedMips(rhi, cb, context.get(), m_envCubeMap, 1, envFullMipCount, "SkyMaterialLightProbe procEnvCube"_ba);
890 m_envTailMipsInitialized = true;
891 }
892
893 auto *rubMip = rhi->nextResourceUpdateBatch();
894 rubMip->generateMips(m_envCubeMap);
895 cb->resourceUpdate(rubMip);
896}
897
898bool QSSGRenderSkyMaterialManager::runPrefilterCycle(QSSGRenderSkyMaterial *inSky,
899 const FrameState &fs,
900 QRhiCommandBuffer *cb,
901 QRhiResourceUpdateBatch *&rub)
902{
903 Q_UNUSED(inSky);
904 if (!fs.runPrefilterSlice) {
905 cb->resourceUpdate(rub);
906 rub = nullptr;
907 return true;
908 }
909
910 const auto &context = m_context.rhiContext();
911 auto *rhi = context->rhi();
912
913 // Ensure per-mip face targets for the accumulator.
914 //
915 // Each specular mip level has its own non-mipmapped 2D array texture
916 // (m_prefilterAccumulators[mip]), and we need two sets of face render
917 // targets for it: one that preserves existing content (for slices 2..N,
918 // additive blend) and one that clears (for the first slice).
919 //
920 // We always attach at level 0 because each accumulator texture has only
921 // one mip level. This sidesteps the Android GLES driver bug where
922 // glFramebufferTextureLayer ignores level > 0.
923 if (m_cache.accumPreserveFaceTargets.size() != fs.prefilterSpecularMipCount) {
924 // Release any stale targets (size mismatch after cache invalidation)
925 for (QSSGSkyIblFaceTargets &t : m_cache.accumPreserveFaceTargets) {
926 for (QRhiTextureRenderTarget *rt : t.renderTargets)
927 delete rt;
928 delete t.renderPassDesc;
929 }
930 m_cache.accumPreserveFaceTargets.clear();
931 m_cache.accumPreserveFaceTargets.resize(fs.prefilterSpecularMipCount);
932
933 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
934 if (!skyIblCreateFaceTargets(rhi,
935 m_prefilterAccumulators[mip],
936 "SkyMaterialLightProbe procEnvPfAccum/m"_ba + QByteArray::number(mip),
937 &m_cache.accumPreserveFaceTargets[mip],
938 true)) {
939 return false;
940 }
941 }
942 }
943
944 if (m_cache.accumClearFaceTargets.size() != fs.prefilterSpecularMipCount) {
945 for (QSSGSkyIblFaceTargets &t : m_cache.accumClearFaceTargets) {
946 for (QRhiTextureRenderTarget *rt : t.renderTargets)
947 delete rt;
948 delete t.renderPassDesc;
949 }
950 m_cache.accumClearFaceTargets.clear();
951 m_cache.accumClearFaceTargets.resize(fs.prefilterSpecularMipCount);
952
953 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
954 if (!skyIblCreateFaceTargets(rhi,
955 m_prefilterAccumulators[mip],
956 "SkyMaterialLightProbe procEnvPfAccumClear/m"_ba + QByteArray::number(mip),
957 &m_cache.accumClearFaceTargets[mip],
958 false)) {
959 return false;
960 }
961 }
962 }
963
964 QSSGSkyIblPrefilterTargets &prefilterTargets = m_cache.prefilterTargets;
965
966 constexpr int uBufSliceSize = 32;
967 constexpr int uBufNormalizeSize = 16;
968 const int uBufSliceElementSize = rhi->ubufAligned(uBufSliceSize);
969 const int uBufNormalizeElementSize = rhi->ubufAligned(uBufNormalizeSize);
970 const int uBufNormalizeEntryCount = qMax(fs.prefilterSpecularMipCount, 1) * 6;
971
972 if (!ensureDynamicUBuf(rhi, m_cache.uBufSlice, uBufSliceElementSize * qMax(fs.prefilterSpecularMipCount, 1), "Failed to create sky IBL slice uniform buffer"))
973 return false;
974 if (!ensureDynamicUBuf(rhi, m_cache.uBufNormalize, uBufNormalizeElementSize * uBufNormalizeEntryCount, "Failed to create sky IBL normalize uniform buffer"))
975 return false;
976
977 const QSSGRhiSamplerDescription mipSamplerDesc { QRhiSampler::Linear, QRhiSampler::Linear,
978 QRhiSampler::Linear, QRhiSampler::ClampToEdge,
979 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
980 const QSSGRhiSamplerDescription noMipLinearSamplerDesc { QRhiSampler::Linear, QRhiSampler::Linear,
981 QRhiSampler::None, QRhiSampler::ClampToEdge,
982 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
983 const QSSGRhiSamplerDescription nearestSamplerDesc { QRhiSampler::Nearest, QRhiSampler::Nearest,
984 QRhiSampler::None, QRhiSampler::ClampToEdge,
985 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
986 QRhiSampler *envMapCubeSampler = context->sampler(fs.enableIBL ? mipSamplerDesc : noMipLinearSamplerDesc);
987 QRhiSampler *accumReadSampler = context->sampler(nearestSamplerDesc);
988
989 if (!ensureSrb(rhi,
990 m_cache.sliceSrb,
991 { QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
992 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufSlice, uBufSliceSize),
993 QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_envCubeMap, envMapCubeSampler) },
994 "Failed to create sky IBL slice SRB"))
995 return false;
996
997 // Create one normalize SRB per specular mip level, each binding the
998 // corresponding per-mip accumulator texture. Since the accumulator textures
999 // are separate non-mipmapped arrays (one per mip), we always read from
1000 // level 0 of whichever texture is bound — the normalize shader passes 0
1001 // as the texelFetch lod (mipLevel in the UBO is set to 0 for all entries).
1002 if (m_cache.normalizeSrbs.size() != fs.prefilterSpecularMipCount) {
1003 for (QRhiShaderResourceBindings *srb : std::as_const(m_cache.normalizeSrbs))
1004 delete srb;
1005 m_cache.normalizeSrbs.clear();
1006 m_cache.normalizeSrbs.resize(fs.prefilterSpecularMipCount, nullptr);
1007 }
1008 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
1009 if (!ensureSrb(rhi,
1010 m_cache.normalizeSrbs[mip],
1011 { QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
1012 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufNormalize, uBufNormalizeSize),
1013 QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_prefilterAccumulators[mip], accumReadSampler) },
1014 "Failed to create sky IBL normalize SRB"))
1015 return false;
1016 }
1017
1018 if (!m_cache.slicePipeline) {
1019 const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhiSkyIblPreFilterShader();
1020 m_cache.slicePipeline = createPrefilterPipeline(rhi,
1021 fs.inputLayout,
1022 { shader.get(),
1023 m_cache.sliceSrb,
1024 m_cache.accumPreserveFaceTargets[0].renderPassDesc,
1025 true },
1026 "Failed to create sky IBL slice pipeline state");
1027 if (!m_cache.slicePipeline)
1028 return false;
1029 }
1030 if (!m_cache.normalizeCubePipeline) {
1031 // All normalize SRBs share the same layout; create the pipeline from SRB 0.
1032 const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhiSkyIblPreFilterNormalizeShader();
1033 m_cache.normalizeCubePipeline = createPrefilterPipeline(rhi,
1034 fs.inputLayout,
1035 { shader.get(),
1036 m_cache.normalizeSrbs[0],
1037 prefilterTargets.renderPassDesc,
1038 false },
1039 "Failed to create sky IBL normalize-to-cube pipeline "
1040 "state");
1041 if (!m_cache.normalizeCubePipeline)
1042 return false;
1043 }
1044
1045 for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
1046 struct SliceData
1047 {
1048 float roughness;
1049 float resolution;
1050 quint32 sampleStart;
1051 quint32 sampleEnd;
1052 quint32 totalSampleCount;
1053 quint32 _pad0;
1054 quint32 _pad1;
1055 quint32 _pad2;
1056 } sliceData;
1057 sliceData.roughness = float(mipLevel) / float(fs.prefilterRoughnessDenom);
1058 sliceData.resolution = fs.resolution;
1059 sliceData.sampleStart = quint32(fs.sliceSampleStart);
1060 sliceData.sampleEnd = quint32(fs.sliceSampleEnd);
1061 sliceData.totalSampleCount = quint32(fs.totalSamples);
1062 sliceData._pad0 = sliceData._pad1 = sliceData._pad2 = 0;
1063 rub->updateDynamicBuffer(m_cache.uBufSlice, mipLevel * uBufSliceElementSize, sizeof(SliceData), &sliceData);
1064
1065 for (const auto face : QSSGRenderTextureCubeFaces) {
1066 struct NormalizeData
1067 {
1068 qint32 faceIndex;
1069 qint32 _pad0 = 0;
1070 qint32 _pad1 = 0;
1071 qint32 _pad2 = 0;
1072 } normalizeData;
1073 normalizeData.faceIndex = quint8(face);
1074 const int entryIndex = mipLevel * 6 + quint8(face);
1075 rub->updateDynamicBuffer(m_cache.uBufNormalize, entryIndex * uBufNormalizeElementSize, sizeof(NormalizeData), &normalizeData);
1076 }
1077 }
1078
1079 cb->resourceUpdate(rub);
1080 rub = nullptr;
1081
1082 // Slice accumulation pass.
1083 //
1084 // On the first slice we use the clear-variant face targets (no PreserveColorContents)
1085 // so the render pass clears the accumulator before the additive draw. This replaces
1086 // the old pattern of a separate empty clear pass followed by a preserve pass, which
1087 // was fragile on tile-based GPUs (the load in the preserve pass could see stale tile
1088 // data from a prior clear pass that stored to the same surface).
1089 //
1090 // On subsequent slices we use the preserve-variant targets so the additive blend
1091 // accumulates on top of the existing content.
1092 for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
1093 QSSGSkyIblFaceTargets &sliceTargets = fs.isFirstSlice ? m_cache.accumClearFaceTargets[mipLevel]
1094 : m_cache.accumPreserveFaceTargets[mipLevel];
1095 const QSize mipSize(qMax(1, fs.environmentMapSize.width() >> mipLevel), qMax(1, fs.environmentMapSize.height() >> mipLevel));
1096
1097 for (const auto face : QSSGRenderTextureCubeFaces) {
1098 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) },
1099 { 2, quint32(uBufSliceElementSize * mipLevel) } };
1100 drawCubeFace(cb,
1101 context.get(),
1102 sliceTargets.renderTargets[quint8(face)],
1103 mipSize,
1104 m_cache.slicePipeline,
1105 m_cache.sliceSrb,
1106 fs.vbufBinding,
1107 offsets,
1108 QByteArrayLiteral("sky_ibl_prefilter_slice"),
1109 QSSG_RENDERPASS_NAME("sky_ibl_prefilter_slice", mipLevel, face),
1110 QColor(0, 0, 0, 0));
1111 }
1112 }
1113
1114 if (fs.sliceCompletesCycle || !fs.haveConvergedResultEntering) {
1115 for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
1116 for (const auto face : QSSGRenderTextureCubeFaces) {
1117 const int normalizeEntryIndex = mipLevel * 6 + quint8(face);
1118 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) },
1119 { 2, quint32(uBufNormalizeElementSize * normalizeEntryIndex) } };
1120 drawCubeFace(cb,
1121 context.get(),
1122 prefilterTargets.mipRenderTargetsMap[mipLevel][quint8(face)],
1123 prefilterTargets.mipLevelSizes[mipLevel],
1124 m_cache.normalizeCubePipeline,
1125 m_cache.normalizeSrbs[mipLevel],
1126 fs.vbufBinding,
1127 offsets,
1128 QByteArrayLiteral("sky_ibl_prefilter_normalize"),
1129 QSSG_RENDERPASS_NAME("sky_ibl_prefilter_normalize", mipLevel, face));
1130 }
1131 }
1132 }
1133
1134 if (fs.enableIBL && (fs.sliceCompletesCycle || !fs.haveConvergedResultEntering)) {
1135 const int irradianceMip = prefilterTargets.mipmapCount - 1;
1136 for (const auto face : QSSGRenderTextureCubeFaces) {
1137 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) }, { 2, 0u } };
1138 drawCubeFace(cb,
1139 context.get(),
1140 prefilterTargets.mipRenderTargetsMap[irradianceMip][quint8(face)],
1141 prefilterTargets.mipLevelSizes[irradianceMip],
1142 m_cache.irradiancePipeline,
1143 m_cache.irradianceSrb,
1144 fs.vbufBinding,
1145 offsets,
1146 QByteArrayLiteral("sky_ibl_irradiance"),
1147 QSSG_RENDERPASS_NAME("sky_ibl_irradiance", irradianceMip, face));
1148 }
1149 }
1150
1151 m_accumulatedSamples = fs.sliceSampleEnd;
1152 if (fs.sliceCompletesCycle)
1153 m_haveConvergedResult = true;
1154
1155 return true;
1156}
1157
1158void QSSGRenderSkyMaterialManager::initializeTailMips(const FrameState &fs, QRhiCommandBuffer *cb)
1159{
1160 if (!m_prefilteredCubeMap->flags().testFlag(QRhiTexture::MipMapped) || m_prefilteredTailMipsInitialized)
1161 return;
1162 const auto &context = m_context.rhiContext();
1163 auto *rhi = context->rhi();
1164 const int prefilteredFullMipCount = rhi->mipLevelsForSize(fs.environmentMapSize);
1165 // Mips actually written this lifetime depend on enableIBL:
1166 // * enableIBL=true: prefilter loop writes mips [0, prefilterMipCount), irradiance writes
1167 // the last (prefilterTargets.mipmapCount-1). So mips [0, prefilterTargets.mipmapCount)
1168 // are covered → init starts at prefilterTargets.mipmapCount.
1169 // * enableIBL=false: prefilter loop writes only mip 0, irradiance is skipped → init starts at 1.
1170 // The prefiltered cube is allocated mip-mapped in both modes for downstream sampler
1171 // completeness; we just need to clear the tail.
1172 const int firstUnwrittenMip = fs.enableIBL ? m_cache.prefilterTargets.mipmapCount : 1;
1173 skyIblInitializeUnrenderedMips(rhi, cb, context.get(), m_prefilteredCubeMap, firstUnwrittenMip, prefilteredFullMipCount, "SkyMaterialLightProbe"_ba);
1174 m_prefilteredTailMipsInitialized = true;
1175}
1176
1177QT_END_NAMESPACE
static bool skyIblCreatePrefilterTargets(QRhi *rhi, QRhiTexture *texture, const QSize &environmentMapSize, const QByteArray &namePrefix, QSSGSkyIblPrefilterTargets *outTargets, bool preserveColorContents=false)
static constexpr QRhiTexture::Format cTextureFormat
static bool ensureSrb(QRhi *rhi, QRhiShaderResourceBindings *&dst, std::initializer_list< QRhiShaderResourceBinding > bindings, const char *errorMessage)
static bool skyIblCreateFaceTargets(QRhi *rhi, QRhiTexture *texture, const QByteArray &namePrefix, QSSGSkyIblFaceTargets *outTargets, bool preserveColorContents=false)
static QRhiGraphicsPipeline * createPrefilterPipeline(QRhi *rhi, const QRhiVertexInputLayout &inputLayout, const PrefilterPipelineConfig &cfg, const char *errorMessage)
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))
static void skyIblInitializeUnrenderedMips(QRhi *rhi, QRhiCommandBuffer *cb, QSSGRhiContext *context, QRhiTexture *texture, int firstMip, int mipCountExclusive, const QByteArray &debugObjectName)
static bool ensureDynamicUBuf(QRhi *rhi, QRhiBuffer *&dst, int size, const char *errorMessage)
static QVarLengthArray< QMatrix4x4, 6 > skyIblEnvironmentMapViews(QRhi *rhi)
#define QSSGRHICTX_STAT(ctx, f)