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 m_finalizeIblPending = false;
475}
476
477QSSGRenderImageTexture QSSGRenderSkyMaterialManager::resolve(QSSGRenderSkyMaterial *settings, QSSGPassKey passKey)
478{
479 const auto &rhiCtx = m_context.rhiContext();
480 if (!QSSG_GUARD(rhiCtx && rhiCtx->isValid() && rhiCtx->rhi()->isRecordingFrame()))
481 return { };
482
483 if (!ensureEnvironmentMap(settings, passKey)) {
484 return { };
485 }
486
487 const bool pendingAccumulation = settings->enableIBL && m_accumulatedSamples < settings->iblSampleCount;
488 settings->wantsMoreFrames = pendingAccumulation || settings->isDirty || m_finalizeIblPending;
489
490 return m_skyIblTexture;
491}
492
493bool QSSGRenderSkyMaterialManager::ensureEnvironmentMap(QSSGRenderSkyMaterial *inSky, QSSGPassKey passKey)
494{
495 const auto &context = m_context.rhiContext();
496 if (!context->rhi()->isTextureFormatSupported(cTextureFormat)) {
497 static bool warningPrinted = false;
498 if (Q_UNLIKELY(!warningPrinted)) {
499 qWarning() << "SkyMaterial not supported due to missing RGBA16F texture format support.";
500 warningPrinted = true;
501 }
502 return false;
503 }
504
505 auto *cb = context->commandBuffer();
506
507 FrameState fs;
508 if (!computeFrameState(inSky, fs))
509 return false;
510
511 if (!ensureTextures(fs))
512 return false;
513
514 deriveCycleState(inSky, fs);
515
516 QSSGRhiShaderPipelinePtr shaderPipeline;
517 if (fs.needRenderEnv) {
518 shaderPipeline = inSky->ensurePipeline(m_context);
519 if (!shaderPipeline) {
520 return false;
521 }
522 }
523 QSSGRhiShaderPipeline *envShaderPipelineKey = fs.needRenderEnv ? shaderPipeline.get() : m_cache.envShaderPipeline;
524
525 validateAndUpdateCacheKey(fs, envShaderPipelineKey);
526
527 if (!ensureSharedResources(fs, cb))
528 return false;
529
530 auto *rub = context->rhi()->nextResourceUpdateBatch();
531 for (const auto face : QSSGRenderTextureCubeFaces) {
532 rub->updateDynamicBuffer(m_cache.uBuf, quint8(face) * fs.ubufElementSize, 64, fs.mvp.constData());
533 rub->updateDynamicBuffer(m_cache.uBuf, quint8(face) * fs.ubufElementSize + 64, 64, fs.views[quint8(face)].constData());
534 }
535
536 if (fs.runIrradiancePass) {
537 struct IrradianceData
538 {
539 float roughness;
540 float resolution;
541 float lodBias;
542 int sampleCount;
543 int distribution;
544 } irradianceData;
545 irradianceData.roughness = 0.0f;
546 irradianceData.resolution = fs.resolution;
547 irradianceData.lodBias = 0.0f;
548 irradianceData.distribution = 0;
549 irradianceData.sampleCount = qMax(int(fs.resolution / 4.0f), 1);
550 rub->updateDynamicBuffer(m_cache.uBufIrradiance, 0, sizeof(IrradianceData), &irradianceData);
551 }
552
553 if (fs.needRenderEnv) {
554 if (!renderEnvironmentCube(inSky, passKey, fs, shaderPipeline, cb, rub)) {
555 // The batch is only submitted on success, return it to the pool
556 rub->release();
557 return false;
558 }
559 rub = nullptr;
560 }
561
562 if (fs.enableIBL && fs.needRenderEnv)
563 runEnvironmentMipChain(fs, cb);
564
565 if (!rub)
566 rub = context->rhi()->nextResourceUpdateBatch();
567
568 cb->debugMarkBegin("Sky IBL Pre-filtered Environment Cubemap Generation");
569 if (!runPrefilterCycle(inSky, fs, cb, rub)) {
570 if (rub)
571 rub->release();
572 cb->debugMarkEnd();
573 return false;
574 }
575 if (rub) {
576 cb->resourceUpdate(rub);
577 rub = nullptr;
578 }
579 cb->debugMarkEnd();
580
581 m_prefilteredMipCount = m_cache.prefilterTargets.mipmapCount;
582 initializeTailMips(fs, cb);
583
584 m_skyIblTexture.m_texture = m_prefilteredCubeMap;
585 m_skyIblTexture.m_mipmapCount = m_prefilteredMipCount;
586 m_skyIblTexture.m_flags.setLinear(true);
587 m_skyIblTexture.m_flags.setRgbe8(false);
588 return true;
589}
590
591bool QSSGRenderSkyMaterialManager::computeFrameState(QSSGRenderSkyMaterial *inSky, FrameState &fs)
592{
593 fs.enableIBL = inSky->enableIBL;
594 fs.totalSamples = qBound(1, inSky->iblSampleCount, 1024);
595
596 const int radianceMapSize = qBound(8, nearestPowerOfTwo(inSky->radianceMapSize), 2048);
597 fs.environmentMapSize = QSize(radianceMapSize, radianceMapSize);
598
599 const bool envWantsMips = fs.enableIBL;
600 const bool envHasMips = m_envCubeMap && m_envCubeMap->flags().testFlag(QRhiTexture::MipMapped);
601 fs.needCreateEnv = !m_envCubeMap || m_cubeMapSize != fs.environmentMapSize || envHasMips != envWantsMips;
602
603 fs.inProgressTimeSlice = fs.enableIBL && m_accumulatedSamples > 0 && m_accumulatedSamples < m_accumIblSampleCount;
604 fs.envContentDirty = inSky->isDirty && !fs.needCreateEnv;
605 fs.deferEnvRefresh = fs.envContentDirty && (fs.inProgressTimeSlice || m_finalizeIblPending);
606 fs.needRenderEnv = fs.needCreateEnv || (fs.envContentDirty && !fs.deferEnvRefresh);
607 return true;
608}
609
610bool QSSGRenderSkyMaterialManager::ensureTextures(FrameState &fs)
611{
612 const auto &context = m_context.rhiContext();
613 auto *rhi = context->rhi();
614 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(context.get());
615
616 // --- Environment cube ---
617 if (fs.needCreateEnv) {
618 if (m_envCubeMap) {
619 rhiCtxD->releaseTexture(m_envCubeMap);
620 m_envCubeMap = nullptr;
621 }
622 // The prefiltered cube derives from m_envCubeMap and must be invalidated alongside.
623 if (m_prefilteredCubeMap) {
624 rhiCtxD->releaseTexture(m_prefilteredCubeMap);
625 m_prefilteredCubeMap = nullptr;
626 m_prefilteredMipCount = 0;
627 }
628 m_envTailMipsInitialized = false;
629 m_prefilteredTailMipsInitialized = false;
630
631 QRhiTexture::Flags envFlags = QRhiTexture::RenderTarget | QRhiTexture::CubeMap;
632 if (fs.enableIBL)
633 envFlags |= QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips;
634 m_envCubeMap = rhi->newTexture(cTextureFormat, fs.environmentMapSize, 1, envFlags);
635 if (!m_envCubeMap->create()) {
636 qWarning("Failed to create Sky IBL environment cube map");
637 delete m_envCubeMap;
638 m_envCubeMap = nullptr;
639 return false;
640 }
641 m_envCubeMap->setName("SkyMaterialLightProbe procEnvCube"_ba);
642 rhiCtxD->registerTexture(m_envCubeMap);
643 m_cubeMapSize = fs.environmentMapSize;
644 }
645
646 if (!m_prefilteredCubeMap) {
647 const QRhiTexture::Flags pfFlags = QRhiTexture::RenderTarget | QRhiTexture::CubeMap | QRhiTexture::MipMapped;
648 m_prefilteredCubeMap = rhi->newTexture(cTextureFormat, fs.environmentMapSize, 1, pfFlags);
649 if (!m_prefilteredCubeMap->create()) {
650 qWarning("Failed to create Sky IBL pre-filtered environment cube map");
651 delete m_prefilteredCubeMap;
652 m_prefilteredCubeMap = nullptr;
653 return false;
654 }
655 m_prefilteredCubeMap->setName("SkyMaterialLightProbe"_ba);
656 rhiCtxD->registerTexture(m_prefilteredCubeMap);
657 fs.prefilteredJustCreated = true;
658 m_haveConvergedResult = false;
659 m_prefilteredTailMipsInitialized = false;
660 }
661
662 fs.prefilterTotalMipCount = m_prefilteredCubeMap->flags().testFlag(QRhiTexture::MipMapped)
663 ? qMin(rhi->mipLevelsForSize(fs.environmentMapSize), 6)
664 : 1;
665 fs.prefilterSpecularMipCount = fs.enableIBL ? fs.prefilterTotalMipCount - 1 : 1;
666 fs.prefilterRoughnessDenom = qMax(fs.prefilterSpecularMipCount - 1, 1);
667 fs.resolution = float(fs.environmentMapSize.width());
668
669 // Determine whether the existing per-mip accumulator set is still valid.
670 // We need one texture per specular mip level, each sized for that mip.
671 const bool needCreateAccum = m_prefilterAccumulators.isEmpty() || m_prefilterAccumulators.size() != fs.prefilterSpecularMipCount
672 || (!m_prefilterAccumulators.isEmpty() && m_prefilterAccumulators[0]->pixelSize() != fs.environmentMapSize);
673
674 if (needCreateAccum) {
675 for (QRhiTexture *t : std::as_const(m_prefilterAccumulators))
676 rhiCtxD->releaseTexture(t);
677 m_prefilterAccumulators.clear();
678 m_accumulatedSamples = 0;
679
680 // Allocate one non-mipmapped 2D array texture per specular mip level.
681 // Each texture is sized for its mip level and only has mip 0.
682 //
683 // Rationale: on Android GLES (Adreno, Mali) glFramebufferTextureLayer
684 // with level > 0 is sometimes silently broken. The driver accepts the
685 // FBO as complete but writes to the wrong location or ignores the level
686 // entirely. By giving each mip level its own texture and always attaching
687 // at level 0, we avoid the broken code path entirely.
688 bool ok = true;
689 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
690 const QSize mipSize(qMax(1, fs.environmentMapSize.width() >> mip), qMax(1, fs.environmentMapSize.height() >> mip));
691 // No MipMapped flag — single level only, always attached at level 0.
692 auto *t = rhi->newTextureArray(cTextureFormat, 6, mipSize, 1, QRhiTexture::RenderTarget);
693 if (!t->create()) {
694 qWarning("Failed to create Sky IBL prefilter accumulator mip %d", mip);
695 delete t;
696 ok = false;
697 break;
698 }
699 t->setName("SkyMaterialLightProbe procEnvPfAccum m"_ba + QByteArray::number(mip));
700 rhiCtxD->registerTexture(t);
701 m_prefilterAccumulators.append(t);
702 }
703
704 if (!ok) {
705 for (QRhiTexture *t : std::as_const(m_prefilterAccumulators))
706 rhiCtxD->releaseTexture(t);
707 m_prefilterAccumulators.clear();
708 // Accumulator unavailable — fall back to single-frame prefilter.
709 } else {
710 // No tail-mip initialization needed: each texture has exactly one
711 // mip level (level 0), so there are no unwritten tail mips.
712 }
713 }
714
715 return true;
716}
717
718void QSSGRenderSkyMaterialManager::deriveCycleState(QSSGRenderSkyMaterial *inSky, FrameState &fs)
719{
720 // Finalize frame: all slicing is complete, run normalize + irradiance now.
721 if (m_finalizeIblPending && !fs.needRenderEnv && !fs.prefilteredJustCreated && m_accumIblSampleCount == fs.totalSamples) {
722 m_finalizeIblPending = false;
723 m_haveConvergedResult = true;
724 fs.runPrefilterSlice = false;
725 fs.writePrefilteredCubeThisFrame = true;
726 fs.runIrradiancePass = fs.enableIBL;
727 fs.sliceSamplesThisFrame = 0;
728 fs.sliceSampleStart = m_accumulatedSamples;
729 fs.sliceSampleEnd = m_accumulatedSamples;
730 fs.isFirstSlice = false;
731 fs.sliceCompletesCycle = false;
732 fs.haveConvergedResultEntering = false; // was not converged before this frame
733 fs.multiFrame = false;
734 return;
735 }
736
737 fs.multiFrame = fs.enableIBL && inSky->iblSamplesPerFrame > 0 && inSky->iblSamplesPerFrame < fs.totalSamples;
738
739 // Restart the accumulator if anything stale: env content changed, prefiltered cube was
740 // re-created, or the sample-count target changed.
741 if (!m_prefilterAccumulators.isEmpty()) {
742 const bool resetAccumulation = fs.needRenderEnv || fs.prefilteredJustCreated || m_accumIblSampleCount != fs.totalSamples;
743 if (resetAccumulation) {
744 m_accumulatedSamples = 0;
745 // Any finalize pending from the previous cycle is now stale — the accumulators
746 // have been reset so running normalize would write garbage to the prefiltered cube.
747 m_finalizeIblPending = false;
748 }
749 m_accumIblSampleCount = fs.totalSamples;
750 }
751
752 fs.prefilterIsConverged = !m_prefilterAccumulators.isEmpty() && m_accumulatedSamples >= fs.totalSamples;
753 fs.runPrefilterSlice = !m_prefilterAccumulators.isEmpty() && !fs.prefilterIsConverged;
754
755 // perFrameBudget = samples to integrate this frame.
756 // Single-frame (iblSamplesPerFrame <= 0) or !enableIBL: all remaining samples.
757 // Multi-frame: the requested budget.
758 fs.perFrameBudget = (inSky->iblSamplesPerFrame > 0 && fs.enableIBL) ? inSky->iblSamplesPerFrame
759 : (fs.totalSamples - m_accumulatedSamples);
760 fs.sliceSamplesThisFrame = fs.runPrefilterSlice ? qMin(fs.perFrameBudget, fs.totalSamples - m_accumulatedSamples) : 0;
761 fs.sliceCompletesCycle = fs.runPrefilterSlice && (m_accumulatedSamples + fs.sliceSamplesThisFrame >= fs.totalSamples);
762
763 fs.sliceSampleStart = m_accumulatedSamples;
764 fs.sliceSampleEnd = fs.runPrefilterSlice ? qMin(m_accumulatedSamples + fs.perFrameBudget, fs.totalSamples) : 0;
765 fs.isFirstSlice = m_accumulatedSamples == 0;
766 fs.haveConvergedResultEntering = m_haveConvergedResult;
767
768 if (inSky->iblRenderFrames >= 1) {
769 // iblRenderFrames >= 1: normalize and irradiance are deferred to a dedicated frame
770 // after all slicing completes, so the last slice frame never pays both costs.
771 // m_finalizeIblPending is set in runPrefilterCycle (not here) so it only fires
772 // once the accumulators actually contain data. Setting it here would trigger a
773 // spurious finalize when the prefilter was blocked by canPrefilter==false (e.g.
774 // skyRenderFrames >= 1 on the env-render frame), normalising empty accumulators.
775 fs.writePrefilteredCubeThisFrame = false;
776 fs.runIrradiancePass = false;
777 } else {
778 // iblRenderFrames == 0: normalize runs every frame so the user sees progressive
779 // convergence while slicing, then fully on the cycle-completion frame.
780 fs.writePrefilteredCubeThisFrame = fs.runPrefilterSlice && (fs.sliceCompletesCycle || !m_haveConvergedResult);
781 fs.runIrradiancePass = fs.enableIBL && fs.writePrefilteredCubeThisFrame;
782 }
783}
784
785void QSSGRenderSkyMaterialManager::validateAndUpdateCacheKey(const FrameState &fs, QSSGRhiShaderPipeline *envShaderPipelineKey)
786{
787 const bool cacheValid = m_cache.environmentMapSize == fs.environmentMapSize && m_cache.enableIBL == fs.enableIBL
788 && m_cache.prefilterTotalMipCount == fs.prefilterTotalMipCount && m_cache.envCubeMap == m_envCubeMap
789 && m_cache.prefilteredCubeMap == m_prefilteredCubeMap && m_cache.prefilterAccumulators == m_prefilterAccumulators
790 && m_cache.envShaderPipeline == envShaderPipelineKey;
791 if (cacheValid)
792 return;
793 clearPrefilterCache();
794 m_cache.environmentMapSize = fs.environmentMapSize;
795 m_cache.enableIBL = fs.enableIBL;
796 m_cache.prefilterTotalMipCount = fs.prefilterTotalMipCount;
797 m_cache.envCubeMap = m_envCubeMap;
798 m_cache.prefilteredCubeMap = m_prefilteredCubeMap;
799 m_cache.prefilterAccumulators = m_prefilterAccumulators;
800 m_cache.envShaderPipeline = envShaderPipelineKey;
801}
802
803bool QSSGRenderSkyMaterialManager::ensureSharedResources(FrameState &fs, QRhiCommandBuffer *cb)
804{
805 const auto &context = m_context.rhiContext();
806 auto *rhi = context->rhi();
807
808 fs.inputLayout.setBindings({ { 3 * sizeof(float) } });
809 fs.inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 } });
810
811 fs.mvp = rhi->clipSpaceCorrMatrix();
812 fs.mvp.perspective(90.0f, 1.0f, 0.1f, 10.0f);
813 fs.views = skyIblEnvironmentMapViews(rhi);
814
815 fs.ubufElementSize = rhi->ubufAligned(128);
816
817 if (!m_cache.vertexBuffer) {
818 m_cache.vertexBuffer = rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(skyIblCubeVerts));
819 if (!m_cache.vertexBuffer->create()) {
820 qWarning("Failed to create sky IBL vertex buffer");
821 delete m_cache.vertexBuffer;
822 m_cache.vertexBuffer = nullptr;
823 return false;
824 }
825 auto *initRub = rhi->nextResourceUpdateBatch();
826 initRub->uploadStaticBuffer(m_cache.vertexBuffer, skyIblCubeVerts);
827 cb->resourceUpdate(initRub);
828 }
829 if (!ensureDynamicUBuf(rhi, m_cache.uBuf, fs.ubufElementSize * 6, "Failed to create sky IBL view uniform buffer"))
830 return false;
831 fs.vbufBinding = QRhiCommandBuffer::VertexInput(m_cache.vertexBuffer, 0);
832
833 if (!m_cache.prefilterTargets.renderPassDesc) {
834 if (!skyIblCreatePrefilterTargets(rhi, m_prefilteredCubeMap, fs.environmentMapSize, "SkyMaterialLightProbe procEnvPf"_ba, &m_cache.prefilterTargets)) {
835 return false;
836 }
837 }
838
839 if (!ensureDynamicUBuf(rhi, m_cache.uBufIrradiance, rhi->ubufAligned(20), "Failed to create sky IBL irradiance uniform buffer"))
840 return false;
841
842 const QSSGRhiSamplerDescription samplerNoMipDesc { QRhiSampler::Linear, QRhiSampler::Linear,
843 QRhiSampler::None, QRhiSampler::ClampToEdge,
844 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
845 QRhiSampler *envMapCubeNoMipSampler = context->sampler(samplerNoMipDesc);
846
847 if (!ensureSrb(rhi,
848 m_cache.irradianceSrb,
849 { QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
850 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufIrradiance, 20),
851 QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_envCubeMap, envMapCubeNoMipSampler) },
852 "Failed to create sky IBL irradiance SRB"))
853 return false;
854
855 if (!m_cache.irradiancePipeline) {
856 const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhienvironmentmapPreFilterShader(false);
857 m_cache.irradiancePipeline = createPrefilterPipeline(rhi,
858 fs.inputLayout,
859 { shader.get(),
860 m_cache.irradianceSrb,
861 m_cache.prefilterTargets.renderPassDesc,
862 false },
863 "Failed to create sky IBL realtime irradiance pipeline "
864 "state");
865 if (!m_cache.irradiancePipeline)
866 return false;
867 }
868 return true;
869}
870
871bool QSSGRenderSkyMaterialManager::renderEnvironmentCube(QSSGRenderSkyMaterial *inSky,
872 QSSGPassKey passKey,
873 const FrameState &fs,
874 const QSSGRhiShaderPipelinePtr &shaderPipeline,
875 QRhiCommandBuffer *cb,
876 QRhiResourceUpdateBatch *rub)
877{
878 const auto &context = m_context.rhiContext();
879 auto *rhi = context->rhi();
880 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(context.get());
881
882 const quint32 skyElementSize = inSky->updateUniforms(m_context, passKey, fs.mvp, fs.views);
883
884 if (!m_cache.envFaceTargets.renderPassDesc) {
885 if (!skyIblCreateFaceTargets(rhi, m_envCubeMap, "SkyMaterialLightProbe procEnvCube"_ba, &m_cache.envFaceTargets))
886 return false;
887 }
888
889 QRhiShaderResourceBindings *envSrb = rhiCtxD->srb(inSky->bindings);
890 Q_ASSERT(envSrb);
891
892 if (!m_cache.envMapPipeline) {
893 m_cache.envMapPipeline = createPrefilterPipeline(rhi,
894 fs.inputLayout,
895 { shaderPipeline.get(), envSrb, m_cache.envFaceTargets.renderPassDesc, false },
896 "Failed to create sky IBL env map pipeline state");
897 if (!m_cache.envMapPipeline)
898 return false;
899 }
900
901 cb->resourceUpdate(rub);
902
903 cb->debugMarkBegin("Sky IBL Procedural Environment Cubemap Generation");
904 for (const auto face : QSSGRenderTextureCubeFaces) {
905 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(skyElementSize * quint8(face)) } };
906 drawCubeFace(cb,
907 context.get(),
908 m_cache.envFaceTargets.renderTargets[quint8(face)],
909 fs.environmentMapSize,
910 m_cache.envMapPipeline,
911 envSrb,
912 fs.vbufBinding,
913 offsets,
914 QByteArrayLiteral("sky_ibl_procedural_environment_map"),
915 QSSG_RENDERPASS_NAME("sky_ibl_procedural_environment_map", 0, face));
916 }
917 cb->debugMarkEnd();
918
919 inSky->isDirty = false;
920 return true;
921}
922
923void QSSGRenderSkyMaterialManager::runEnvironmentMipChain(const FrameState &fs, QRhiCommandBuffer *cb)
924{
925 const auto &context = m_context.rhiContext();
926 auto *rhi = context->rhi();
927
928 if (!m_envTailMipsInitialized) {
929 const int envFullMipCount = rhi->mipLevelsForSize(fs.environmentMapSize);
930 skyIblInitializeUnrenderedMips(rhi, cb, context.get(), m_envCubeMap, 1, envFullMipCount, "SkyMaterialLightProbe procEnvCube"_ba);
931 m_envTailMipsInitialized = true;
932 }
933
934 auto *rubMip = rhi->nextResourceUpdateBatch();
935 rubMip->generateMips(m_envCubeMap);
936 cb->resourceUpdate(rubMip);
937}
938
939bool QSSGRenderSkyMaterialManager::runPrefilterCycle(QSSGRenderSkyMaterial *inSky,
940 const FrameState &fs,
941 QRhiCommandBuffer *cb,
942 QRhiResourceUpdateBatch *&rub)
943{
944 Q_UNUSED(inSky);
945 if (!fs.runPrefilterSlice && !fs.writePrefilteredCubeThisFrame) {
946 cb->resourceUpdate(rub);
947 rub = nullptr;
948 return true;
949 }
950
951 const auto &context = m_context.rhiContext();
952 auto *rhi = context->rhi();
953
954 // Ensure per-mip face targets for the accumulator.
955 //
956 // Each specular mip level has its own non-mipmapped 2D array texture
957 // (m_prefilterAccumulators[mip]), and we need two sets of face render
958 // targets for it: one that preserves existing content (for slices 2..N,
959 // additive blend) and one that clears (for the first slice).
960 //
961 // We always attach at level 0 because each accumulator texture has only
962 // one mip level. This sidesteps the Android GLES driver bug where
963 // glFramebufferTextureLayer ignores level > 0.
964 if (m_cache.accumPreserveFaceTargets.size() != fs.prefilterSpecularMipCount) {
965 // Release any stale targets (size mismatch after cache invalidation)
966 for (QSSGSkyIblFaceTargets &t : m_cache.accumPreserveFaceTargets) {
967 for (QRhiTextureRenderTarget *rt : t.renderTargets)
968 delete rt;
969 delete t.renderPassDesc;
970 }
971 m_cache.accumPreserveFaceTargets.clear();
972 m_cache.accumPreserveFaceTargets.resize(fs.prefilterSpecularMipCount);
973
974 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
975 if (!skyIblCreateFaceTargets(rhi,
976 m_prefilterAccumulators[mip],
977 "SkyMaterialLightProbe procEnvPfAccum/m"_ba + QByteArray::number(mip),
978 &m_cache.accumPreserveFaceTargets[mip],
979 true)) {
980 return false;
981 }
982 }
983 }
984
985 if (m_cache.accumClearFaceTargets.size() != fs.prefilterSpecularMipCount) {
986 for (QSSGSkyIblFaceTargets &t : m_cache.accumClearFaceTargets) {
987 for (QRhiTextureRenderTarget *rt : t.renderTargets)
988 delete rt;
989 delete t.renderPassDesc;
990 }
991 m_cache.accumClearFaceTargets.clear();
992 m_cache.accumClearFaceTargets.resize(fs.prefilterSpecularMipCount);
993
994 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
995 if (!skyIblCreateFaceTargets(rhi,
996 m_prefilterAccumulators[mip],
997 "SkyMaterialLightProbe procEnvPfAccumClear/m"_ba + QByteArray::number(mip),
998 &m_cache.accumClearFaceTargets[mip],
999 false)) {
1000 return false;
1001 }
1002 }
1003 }
1004
1005 QSSGSkyIblPrefilterTargets &prefilterTargets = m_cache.prefilterTargets;
1006
1007 constexpr int uBufSliceSize = 32;
1008 constexpr int uBufNormalizeSize = 16;
1009 const int uBufSliceElementSize = rhi->ubufAligned(uBufSliceSize);
1010 const int uBufNormalizeElementSize = rhi->ubufAligned(uBufNormalizeSize);
1011 const int uBufNormalizeEntryCount = qMax(fs.prefilterSpecularMipCount, 1) * 6;
1012
1013 if (!ensureDynamicUBuf(rhi, m_cache.uBufSlice, uBufSliceElementSize * qMax(fs.prefilterSpecularMipCount, 1), "Failed to create sky IBL slice uniform buffer"))
1014 return false;
1015 if (!ensureDynamicUBuf(rhi, m_cache.uBufNormalize, uBufNormalizeElementSize * uBufNormalizeEntryCount, "Failed to create sky IBL normalize uniform buffer"))
1016 return false;
1017
1018 const QSSGRhiSamplerDescription mipSamplerDesc { QRhiSampler::Linear, QRhiSampler::Linear,
1019 QRhiSampler::Linear, QRhiSampler::ClampToEdge,
1020 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
1021 const QSSGRhiSamplerDescription noMipLinearSamplerDesc { QRhiSampler::Linear, QRhiSampler::Linear,
1022 QRhiSampler::None, QRhiSampler::ClampToEdge,
1023 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
1024 const QSSGRhiSamplerDescription nearestSamplerDesc { QRhiSampler::Nearest, QRhiSampler::Nearest,
1025 QRhiSampler::None, QRhiSampler::ClampToEdge,
1026 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
1027 QRhiSampler *envMapCubeSampler = context->sampler(fs.enableIBL ? mipSamplerDesc : noMipLinearSamplerDesc);
1028 QRhiSampler *accumReadSampler = context->sampler(nearestSamplerDesc);
1029
1030 if (!ensureSrb(rhi,
1031 m_cache.sliceSrb,
1032 { QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
1033 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufSlice, uBufSliceSize),
1034 QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_envCubeMap, envMapCubeSampler) },
1035 "Failed to create sky IBL slice SRB"))
1036 return false;
1037
1038 // Create one normalize SRB per specular mip level, each binding the
1039 // corresponding per-mip accumulator texture. Since the accumulator textures
1040 // are separate non-mipmapped arrays (one per mip), we always read from
1041 // level 0 of whichever texture is bound — the normalize shader passes 0
1042 // as the texelFetch lod (mipLevel in the UBO is set to 0 for all entries).
1043 if (m_cache.normalizeSrbs.size() != fs.prefilterSpecularMipCount) {
1044 for (QRhiShaderResourceBindings *srb : std::as_const(m_cache.normalizeSrbs))
1045 delete srb;
1046 m_cache.normalizeSrbs.clear();
1047 m_cache.normalizeSrbs.resize(fs.prefilterSpecularMipCount, nullptr);
1048 }
1049 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
1050 if (!ensureSrb(rhi,
1051 m_cache.normalizeSrbs[mip],
1052 { QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
1053 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufNormalize, uBufNormalizeSize),
1054 QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_prefilterAccumulators[mip], accumReadSampler) },
1055 "Failed to create sky IBL normalize SRB"))
1056 return false;
1057 }
1058
1059 if (!m_cache.slicePipeline) {
1060 const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhiSkyIblPreFilterShader();
1061 m_cache.slicePipeline = createPrefilterPipeline(rhi,
1062 fs.inputLayout,
1063 { shader.get(),
1064 m_cache.sliceSrb,
1065 m_cache.accumPreserveFaceTargets[0].renderPassDesc,
1066 true },
1067 "Failed to create sky IBL slice pipeline state");
1068 if (!m_cache.slicePipeline)
1069 return false;
1070 }
1071 if (!m_cache.normalizeCubePipeline) {
1072 // All normalize SRBs share the same layout; create the pipeline from SRB 0.
1073 const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhiSkyIblPreFilterNormalizeShader();
1074 m_cache.normalizeCubePipeline = createPrefilterPipeline(rhi,
1075 fs.inputLayout,
1076 { shader.get(),
1077 m_cache.normalizeSrbs[0],
1078 prefilterTargets.renderPassDesc,
1079 false },
1080 "Failed to create sky IBL normalize-to-cube pipeline "
1081 "state");
1082 if (!m_cache.normalizeCubePipeline)
1083 return false;
1084 }
1085
1086 for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
1087 struct SliceData
1088 {
1089 float roughness;
1090 float resolution;
1091 quint32 sampleStart;
1092 quint32 sampleEnd;
1093 quint32 totalSampleCount;
1094 quint32 _pad0;
1095 quint32 _pad1;
1096 quint32 _pad2;
1097 } sliceData;
1098 sliceData.roughness = float(mipLevel) / float(fs.prefilterRoughnessDenom);
1099 sliceData.resolution = fs.resolution;
1100 sliceData.sampleStart = quint32(fs.sliceSampleStart);
1101 sliceData.sampleEnd = quint32(fs.sliceSampleEnd);
1102 sliceData.totalSampleCount = quint32(fs.totalSamples);
1103 sliceData._pad0 = sliceData._pad1 = sliceData._pad2 = 0;
1104 rub->updateDynamicBuffer(m_cache.uBufSlice, mipLevel * uBufSliceElementSize, sizeof(SliceData), &sliceData);
1105
1106 for (const auto face : QSSGRenderTextureCubeFaces) {
1107 struct NormalizeData
1108 {
1109 qint32 faceIndex;
1110 qint32 _pad0 = 0;
1111 qint32 _pad1 = 0;
1112 qint32 _pad2 = 0;
1113 } normalizeData;
1114 normalizeData.faceIndex = quint8(face);
1115 const int entryIndex = mipLevel * 6 + quint8(face);
1116 rub->updateDynamicBuffer(m_cache.uBufNormalize, entryIndex * uBufNormalizeElementSize, sizeof(NormalizeData), &normalizeData);
1117 }
1118 }
1119
1120 cb->resourceUpdate(rub);
1121 rub = nullptr;
1122
1123 // Slice accumulation pass.
1124 //
1125 // On the first slice we use the clear-variant face targets (no PreserveColorContents)
1126 // so the render pass clears the accumulator before the additive draw. This replaces
1127 // the old pattern of a separate empty clear pass followed by a preserve pass, which
1128 // was fragile on tile-based GPUs (the load in the preserve pass could see stale tile
1129 // data from a prior clear pass that stored to the same surface).
1130 //
1131 // On subsequent slices we use the preserve-variant targets so the additive blend
1132 // accumulates on top of the existing content.
1133 if (fs.runPrefilterSlice) {
1134 for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
1135 QSSGSkyIblFaceTargets &sliceTargets = fs.isFirstSlice ? m_cache.accumClearFaceTargets[mipLevel]
1136 : m_cache.accumPreserveFaceTargets[mipLevel];
1137 const QSize mipSize(qMax(1, fs.environmentMapSize.width() >> mipLevel),
1138 qMax(1, fs.environmentMapSize.height() >> mipLevel));
1139
1140 for (const auto face : QSSGRenderTextureCubeFaces) {
1141 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) },
1142 { 2, quint32(uBufSliceElementSize * mipLevel) } };
1143 drawCubeFace(cb,
1144 context.get(),
1145 sliceTargets.renderTargets[quint8(face)],
1146 mipSize,
1147 m_cache.slicePipeline,
1148 m_cache.sliceSrb,
1149 fs.vbufBinding,
1150 offsets,
1151 QByteArrayLiteral("sky_ibl_prefilter_slice"),
1152 QSSG_RENDERPASS_NAME("sky_ibl_prefilter_slice", mipLevel, face),
1153 QColor(0, 0, 0, 0));
1154 }
1155 }
1156 m_accumulatedSamples = fs.sliceSampleEnd;
1157 if (fs.sliceCompletesCycle) {
1158 if (inSky->iblRenderFrames >= 1) {
1159 // Defer normalize+irradiance to a dedicated finalize frame.
1160 // Set the flag here (after the slice actually ran) so the accumulators
1161 // are guaranteed to contain data when the finalize fires.
1162 m_finalizeIblPending = true;
1163 } else {
1164 // iblRenderFrames==0: normalize runs inline, mark converged now.
1165 m_haveConvergedResult = true;
1166 }
1167 }
1168 }
1169
1170 // Normalize accumulated samples into the prefiltered cube.
1171 // For iblRenderFrames==0: runs every frame until convergence (progressive).
1172 // For iblRenderFrames>=1: only runs in the dedicated finalize frame.
1173 if (fs.writePrefilteredCubeThisFrame) {
1174 for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
1175 for (const auto face : QSSGRenderTextureCubeFaces) {
1176 const int normalizeEntryIndex = mipLevel * 6 + quint8(face);
1177 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) },
1178 { 2, quint32(uBufNormalizeElementSize * normalizeEntryIndex) } };
1179 drawCubeFace(cb,
1180 context.get(),
1181 prefilterTargets.mipRenderTargetsMap[mipLevel][quint8(face)],
1182 prefilterTargets.mipLevelSizes[mipLevel],
1183 m_cache.normalizeCubePipeline,
1184 m_cache.normalizeSrbs[mipLevel],
1185 fs.vbufBinding,
1186 offsets,
1187 QByteArrayLiteral("sky_ibl_prefilter_normalize"),
1188 QSSG_RENDERPASS_NAME("sky_ibl_prefilter_normalize", mipLevel, face));
1189 }
1190 }
1191 }
1192
1193 if (fs.runIrradiancePass) {
1194 const int irradianceMip = prefilterTargets.mipmapCount - 1;
1195 for (const auto face : QSSGRenderTextureCubeFaces) {
1196 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) }, { 2, 0u } };
1197 drawCubeFace(cb,
1198 context.get(),
1199 prefilterTargets.mipRenderTargetsMap[irradianceMip][quint8(face)],
1200 prefilterTargets.mipLevelSizes[irradianceMip],
1201 m_cache.irradiancePipeline,
1202 m_cache.irradianceSrb,
1203 fs.vbufBinding,
1204 offsets,
1205 QByteArrayLiteral("sky_ibl_irradiance"),
1206 QSSG_RENDERPASS_NAME("sky_ibl_irradiance", irradianceMip, face));
1207 }
1208 }
1209
1210 return true;
1211}
1212
1213void QSSGRenderSkyMaterialManager::initializeTailMips(const FrameState &fs, QRhiCommandBuffer *cb)
1214{
1215 if (!m_prefilteredCubeMap->flags().testFlag(QRhiTexture::MipMapped) || m_prefilteredTailMipsInitialized)
1216 return;
1217 const auto &context = m_context.rhiContext();
1218 auto *rhi = context->rhi();
1219 const int prefilteredFullMipCount = rhi->mipLevelsForSize(fs.environmentMapSize);
1220 // Mips actually written this lifetime depend on enableIBL:
1221 // * enableIBL=true: prefilter loop writes mips [0, prefilterMipCount), irradiance writes
1222 // the last (prefilterTargets.mipmapCount-1). So mips [0, prefilterTargets.mipmapCount)
1223 // are covered → init starts at prefilterTargets.mipmapCount.
1224 // * enableIBL=false: prefilter loop writes only mip 0, irradiance is skipped → init starts at 1.
1225 // The prefiltered cube is allocated mip-mapped in both modes for downstream sampler
1226 // completeness; we just need to clear the tail.
1227 const int firstUnwrittenMip = fs.enableIBL ? m_cache.prefilterTargets.mipmapCount : 1;
1228 skyIblInitializeUnrenderedMips(rhi, cb, context.get(), m_prefilteredCubeMap, firstUnwrittenMip, prefilteredFullMipCount, "SkyMaterialLightProbe"_ba);
1229 m_prefilteredTailMipsInitialized = true;
1230}
1231
1232QT_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)