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)
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)) {
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)
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, 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 const FrameState &fs,
866 const QSSGRhiShaderPipelinePtr &shaderPipeline,
867 QRhiCommandBuffer *cb,
868 QRhiResourceUpdateBatch *rub)
869{
870 const auto &context = m_context.rhiContext();
871 auto *rhi = context->rhi();
872 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(context.get());
873
874 const quint32 skyElementSize = inSky->updateUniforms(m_context, fs.mvp, fs.views);
875
876 if (!m_cache.envFaceTargets.renderPassDesc) {
877 if (!skyIblCreateFaceTargets(rhi, m_envCubeMap, "SkyMaterialLightProbe procEnvCube"_ba, &m_cache.envFaceTargets))
878 return false;
879 }
880
881 QRhiShaderResourceBindings *envSrb = rhiCtxD->srb(inSky->bindings);
882 Q_ASSERT(envSrb);
883
884 if (!m_cache.envMapPipeline) {
885 m_cache.envMapPipeline = createPrefilterPipeline(rhi,
886 fs.inputLayout,
887 { shaderPipeline.get(), envSrb, m_cache.envFaceTargets.renderPassDesc, false },
888 "Failed to create sky IBL env map pipeline state");
889 if (!m_cache.envMapPipeline)
890 return false;
891 }
892
893 cb->resourceUpdate(rub);
894
895 cb->debugMarkBegin("Sky IBL Procedural Environment Cubemap Generation");
896 for (const auto face : QSSGRenderTextureCubeFaces) {
897 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(skyElementSize * quint8(face)) } };
898 drawCubeFace(cb,
899 context.get(),
900 m_cache.envFaceTargets.renderTargets[quint8(face)],
901 fs.environmentMapSize,
902 m_cache.envMapPipeline,
903 envSrb,
904 fs.vbufBinding,
905 offsets,
906 QByteArrayLiteral("sky_ibl_procedural_environment_map"),
907 QSSG_RENDERPASS_NAME("sky_ibl_procedural_environment_map", 0, face));
908 }
909 cb->debugMarkEnd();
910
911 inSky->isDirty = false;
912 return true;
913}
914
915void QSSGRenderSkyMaterialManager::runEnvironmentMipChain(const FrameState &fs, QRhiCommandBuffer *cb)
916{
917 const auto &context = m_context.rhiContext();
918 auto *rhi = context->rhi();
919
920 if (!m_envTailMipsInitialized) {
921 const int envFullMipCount = rhi->mipLevelsForSize(fs.environmentMapSize);
922 skyIblInitializeUnrenderedMips(rhi, cb, context.get(), m_envCubeMap, 1, envFullMipCount, "SkyMaterialLightProbe procEnvCube"_ba);
923 m_envTailMipsInitialized = true;
924 }
925
926 auto *rubMip = rhi->nextResourceUpdateBatch();
927 rubMip->generateMips(m_envCubeMap);
928 cb->resourceUpdate(rubMip);
929}
930
931bool QSSGRenderSkyMaterialManager::runPrefilterCycle(QSSGRenderSkyMaterial *inSky,
932 const FrameState &fs,
933 QRhiCommandBuffer *cb,
934 QRhiResourceUpdateBatch *&rub)
935{
936 Q_UNUSED(inSky);
937 if (!fs.runPrefilterSlice && !fs.writePrefilteredCubeThisFrame) {
938 cb->resourceUpdate(rub);
939 rub = nullptr;
940 return true;
941 }
942
943 const auto &context = m_context.rhiContext();
944 auto *rhi = context->rhi();
945
946 // Ensure per-mip face targets for the accumulator.
947 //
948 // Each specular mip level has its own non-mipmapped 2D array texture
949 // (m_prefilterAccumulators[mip]), and we need two sets of face render
950 // targets for it: one that preserves existing content (for slices 2..N,
951 // additive blend) and one that clears (for the first slice).
952 //
953 // We always attach at level 0 because each accumulator texture has only
954 // one mip level. This sidesteps the Android GLES driver bug where
955 // glFramebufferTextureLayer ignores level > 0.
956 if (m_cache.accumPreserveFaceTargets.size() != fs.prefilterSpecularMipCount) {
957 // Release any stale targets (size mismatch after cache invalidation)
958 for (QSSGSkyIblFaceTargets &t : m_cache.accumPreserveFaceTargets) {
959 for (QRhiTextureRenderTarget *rt : t.renderTargets)
960 delete rt;
961 delete t.renderPassDesc;
962 }
963 m_cache.accumPreserveFaceTargets.clear();
964 m_cache.accumPreserveFaceTargets.resize(fs.prefilterSpecularMipCount);
965
966 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
967 if (!skyIblCreateFaceTargets(rhi,
968 m_prefilterAccumulators[mip],
969 "SkyMaterialLightProbe procEnvPfAccum/m"_ba + QByteArray::number(mip),
970 &m_cache.accumPreserveFaceTargets[mip],
971 true)) {
972 return false;
973 }
974 }
975 }
976
977 if (m_cache.accumClearFaceTargets.size() != fs.prefilterSpecularMipCount) {
978 for (QSSGSkyIblFaceTargets &t : m_cache.accumClearFaceTargets) {
979 for (QRhiTextureRenderTarget *rt : t.renderTargets)
980 delete rt;
981 delete t.renderPassDesc;
982 }
983 m_cache.accumClearFaceTargets.clear();
984 m_cache.accumClearFaceTargets.resize(fs.prefilterSpecularMipCount);
985
986 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
987 if (!skyIblCreateFaceTargets(rhi,
988 m_prefilterAccumulators[mip],
989 "SkyMaterialLightProbe procEnvPfAccumClear/m"_ba + QByteArray::number(mip),
990 &m_cache.accumClearFaceTargets[mip],
991 false)) {
992 return false;
993 }
994 }
995 }
996
997 QSSGSkyIblPrefilterTargets &prefilterTargets = m_cache.prefilterTargets;
998
999 constexpr int uBufSliceSize = 32;
1000 constexpr int uBufNormalizeSize = 16;
1001 const int uBufSliceElementSize = rhi->ubufAligned(uBufSliceSize);
1002 const int uBufNormalizeElementSize = rhi->ubufAligned(uBufNormalizeSize);
1003 const int uBufNormalizeEntryCount = qMax(fs.prefilterSpecularMipCount, 1) * 6;
1004
1005 if (!ensureDynamicUBuf(rhi, m_cache.uBufSlice, uBufSliceElementSize * qMax(fs.prefilterSpecularMipCount, 1), "Failed to create sky IBL slice uniform buffer"))
1006 return false;
1007 if (!ensureDynamicUBuf(rhi, m_cache.uBufNormalize, uBufNormalizeElementSize * uBufNormalizeEntryCount, "Failed to create sky IBL normalize uniform buffer"))
1008 return false;
1009
1010 const QSSGRhiSamplerDescription mipSamplerDesc { QRhiSampler::Linear, QRhiSampler::Linear,
1011 QRhiSampler::Linear, QRhiSampler::ClampToEdge,
1012 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
1013 const QSSGRhiSamplerDescription noMipLinearSamplerDesc { QRhiSampler::Linear, QRhiSampler::Linear,
1014 QRhiSampler::None, QRhiSampler::ClampToEdge,
1015 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
1016 const QSSGRhiSamplerDescription nearestSamplerDesc { QRhiSampler::Nearest, QRhiSampler::Nearest,
1017 QRhiSampler::None, QRhiSampler::ClampToEdge,
1018 QRhiSampler::ClampToEdge, QRhiSampler::Repeat };
1019 QRhiSampler *envMapCubeSampler = context->sampler(fs.enableIBL ? mipSamplerDesc : noMipLinearSamplerDesc);
1020 QRhiSampler *accumReadSampler = context->sampler(nearestSamplerDesc);
1021
1022 if (!ensureSrb(rhi,
1023 m_cache.sliceSrb,
1024 { QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
1025 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufSlice, uBufSliceSize),
1026 QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_envCubeMap, envMapCubeSampler) },
1027 "Failed to create sky IBL slice SRB"))
1028 return false;
1029
1030 // Create one normalize SRB per specular mip level, each binding the
1031 // corresponding per-mip accumulator texture. Since the accumulator textures
1032 // are separate non-mipmapped arrays (one per mip), we always read from
1033 // level 0 of whichever texture is bound — the normalize shader passes 0
1034 // as the texelFetch lod (mipLevel in the UBO is set to 0 for all entries).
1035 if (m_cache.normalizeSrbs.size() != fs.prefilterSpecularMipCount) {
1036 for (QRhiShaderResourceBindings *srb : std::as_const(m_cache.normalizeSrbs))
1037 delete srb;
1038 m_cache.normalizeSrbs.clear();
1039 m_cache.normalizeSrbs.resize(fs.prefilterSpecularMipCount, nullptr);
1040 }
1041 for (int mip = 0; mip < fs.prefilterSpecularMipCount; ++mip) {
1042 if (!ensureSrb(rhi,
1043 m_cache.normalizeSrbs[mip],
1044 { QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, m_cache.uBuf, 128),
1045 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, m_cache.uBufNormalize, uBufNormalizeSize),
1046 QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_prefilterAccumulators[mip], accumReadSampler) },
1047 "Failed to create sky IBL normalize SRB"))
1048 return false;
1049 }
1050
1051 if (!m_cache.slicePipeline) {
1052 const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhiSkyIblPreFilterShader();
1053 m_cache.slicePipeline = createPrefilterPipeline(rhi,
1054 fs.inputLayout,
1055 { shader.get(),
1056 m_cache.sliceSrb,
1057 m_cache.accumPreserveFaceTargets[0].renderPassDesc,
1058 true },
1059 "Failed to create sky IBL slice pipeline state");
1060 if (!m_cache.slicePipeline)
1061 return false;
1062 }
1063 if (!m_cache.normalizeCubePipeline) {
1064 // All normalize SRBs share the same layout; create the pipeline from SRB 0.
1065 const auto &shader = m_context.shaderCache()->getBuiltInRhiShaders().getRhiSkyIblPreFilterNormalizeShader();
1066 m_cache.normalizeCubePipeline = createPrefilterPipeline(rhi,
1067 fs.inputLayout,
1068 { shader.get(),
1069 m_cache.normalizeSrbs[0],
1070 prefilterTargets.renderPassDesc,
1071 false },
1072 "Failed to create sky IBL normalize-to-cube pipeline "
1073 "state");
1074 if (!m_cache.normalizeCubePipeline)
1075 return false;
1076 }
1077
1078 for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
1079 struct SliceData
1080 {
1081 float roughness;
1082 float resolution;
1083 quint32 sampleStart;
1084 quint32 sampleEnd;
1085 quint32 totalSampleCount;
1086 quint32 _pad0;
1087 quint32 _pad1;
1088 quint32 _pad2;
1089 } sliceData;
1090 sliceData.roughness = float(mipLevel) / float(fs.prefilterRoughnessDenom);
1091 sliceData.resolution = fs.resolution;
1092 sliceData.sampleStart = quint32(fs.sliceSampleStart);
1093 sliceData.sampleEnd = quint32(fs.sliceSampleEnd);
1094 sliceData.totalSampleCount = quint32(fs.totalSamples);
1095 sliceData._pad0 = sliceData._pad1 = sliceData._pad2 = 0;
1096 rub->updateDynamicBuffer(m_cache.uBufSlice, mipLevel * uBufSliceElementSize, sizeof(SliceData), &sliceData);
1097
1098 for (const auto face : QSSGRenderTextureCubeFaces) {
1099 struct NormalizeData
1100 {
1101 qint32 faceIndex;
1102 qint32 _pad0 = 0;
1103 qint32 _pad1 = 0;
1104 qint32 _pad2 = 0;
1105 } normalizeData;
1106 normalizeData.faceIndex = quint8(face);
1107 const int entryIndex = mipLevel * 6 + quint8(face);
1108 rub->updateDynamicBuffer(m_cache.uBufNormalize, entryIndex * uBufNormalizeElementSize, sizeof(NormalizeData), &normalizeData);
1109 }
1110 }
1111
1112 cb->resourceUpdate(rub);
1113 rub = nullptr;
1114
1115 // Slice accumulation pass.
1116 //
1117 // On the first slice we use the clear-variant face targets (no PreserveColorContents)
1118 // so the render pass clears the accumulator before the additive draw. This replaces
1119 // the old pattern of a separate empty clear pass followed by a preserve pass, which
1120 // was fragile on tile-based GPUs (the load in the preserve pass could see stale tile
1121 // data from a prior clear pass that stored to the same surface).
1122 //
1123 // On subsequent slices we use the preserve-variant targets so the additive blend
1124 // accumulates on top of the existing content.
1125 if (fs.runPrefilterSlice) {
1126 for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
1127 QSSGSkyIblFaceTargets &sliceTargets = fs.isFirstSlice ? m_cache.accumClearFaceTargets[mipLevel]
1128 : m_cache.accumPreserveFaceTargets[mipLevel];
1129 const QSize mipSize(qMax(1, fs.environmentMapSize.width() >> mipLevel),
1130 qMax(1, fs.environmentMapSize.height() >> mipLevel));
1131
1132 for (const auto face : QSSGRenderTextureCubeFaces) {
1133 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) },
1134 { 2, quint32(uBufSliceElementSize * mipLevel) } };
1135 drawCubeFace(cb,
1136 context.get(),
1137 sliceTargets.renderTargets[quint8(face)],
1138 mipSize,
1139 m_cache.slicePipeline,
1140 m_cache.sliceSrb,
1141 fs.vbufBinding,
1142 offsets,
1143 QByteArrayLiteral("sky_ibl_prefilter_slice"),
1144 QSSG_RENDERPASS_NAME("sky_ibl_prefilter_slice", mipLevel, face),
1145 QColor(0, 0, 0, 0));
1146 }
1147 }
1148 m_accumulatedSamples = fs.sliceSampleEnd;
1149 if (fs.sliceCompletesCycle) {
1150 if (inSky->iblRenderFrames >= 1) {
1151 // Defer normalize+irradiance to a dedicated finalize frame.
1152 // Set the flag here (after the slice actually ran) so the accumulators
1153 // are guaranteed to contain data when the finalize fires.
1154 m_finalizeIblPending = true;
1155 } else {
1156 // iblRenderFrames==0: normalize runs inline, mark converged now.
1157 m_haveConvergedResult = true;
1158 }
1159 }
1160 }
1161
1162 // Normalize accumulated samples into the prefiltered cube.
1163 // For iblRenderFrames==0: runs every frame until convergence (progressive).
1164 // For iblRenderFrames>=1: only runs in the dedicated finalize frame.
1165 if (fs.writePrefilteredCubeThisFrame) {
1166 for (int mipLevel = 0; mipLevel < fs.prefilterSpecularMipCount; ++mipLevel) {
1167 for (const auto face : QSSGRenderTextureCubeFaces) {
1168 const int normalizeEntryIndex = mipLevel * 6 + quint8(face);
1169 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) },
1170 { 2, quint32(uBufNormalizeElementSize * normalizeEntryIndex) } };
1171 drawCubeFace(cb,
1172 context.get(),
1173 prefilterTargets.mipRenderTargetsMap[mipLevel][quint8(face)],
1174 prefilterTargets.mipLevelSizes[mipLevel],
1175 m_cache.normalizeCubePipeline,
1176 m_cache.normalizeSrbs[mipLevel],
1177 fs.vbufBinding,
1178 offsets,
1179 QByteArrayLiteral("sky_ibl_prefilter_normalize"),
1180 QSSG_RENDERPASS_NAME("sky_ibl_prefilter_normalize", mipLevel, face));
1181 }
1182 }
1183 }
1184
1185 if (fs.runIrradiancePass) {
1186 const int irradianceMip = prefilterTargets.mipmapCount - 1;
1187 for (const auto face : QSSGRenderTextureCubeFaces) {
1188 const QVector<QPair<int, quint32>> offsets = { { 0, quint32(fs.ubufElementSize * quint8(face)) }, { 2, 0u } };
1189 drawCubeFace(cb,
1190 context.get(),
1191 prefilterTargets.mipRenderTargetsMap[irradianceMip][quint8(face)],
1192 prefilterTargets.mipLevelSizes[irradianceMip],
1193 m_cache.irradiancePipeline,
1194 m_cache.irradianceSrb,
1195 fs.vbufBinding,
1196 offsets,
1197 QByteArrayLiteral("sky_ibl_irradiance"),
1198 QSSG_RENDERPASS_NAME("sky_ibl_irradiance", irradianceMip, face));
1199 }
1200 }
1201
1202 return true;
1203}
1204
1205void QSSGRenderSkyMaterialManager::initializeTailMips(const FrameState &fs, QRhiCommandBuffer *cb)
1206{
1207 if (!m_prefilteredCubeMap->flags().testFlag(QRhiTexture::MipMapped) || m_prefilteredTailMipsInitialized)
1208 return;
1209 const auto &context = m_context.rhiContext();
1210 auto *rhi = context->rhi();
1211 const int prefilteredFullMipCount = rhi->mipLevelsForSize(fs.environmentMapSize);
1212 // Mips actually written this lifetime depend on enableIBL:
1213 // * enableIBL=true: prefilter loop writes mips [0, prefilterMipCount), irradiance writes
1214 // the last (prefilterTargets.mipmapCount-1). So mips [0, prefilterTargets.mipmapCount)
1215 // are covered → init starts at prefilterTargets.mipmapCount.
1216 // * enableIBL=false: prefilter loop writes only mip 0, irradiance is skipped → init starts at 1.
1217 // The prefiltered cube is allocated mip-mapped in both modes for downstream sampler
1218 // completeness; we just need to clear the tail.
1219 const int firstUnwrittenMip = fs.enableIBL ? m_cache.prefilterTargets.mipmapCount : 1;
1220 skyIblInitializeUnrenderedMips(rhi, cb, context.get(), m_prefilteredCubeMap, firstUnwrittenMip, prefilteredFullMipCount, "SkyMaterialLightProbe"_ba);
1221 m_prefilteredTailMipsInitialized = true;
1222}
1223
1224QT_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)