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