Revision control

Copy as Markdown

// Copyright (C) 2008-2012 NVIDIA Corporation.
// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include <QtQuick3DRuntimeRender/private/qssgrenderlayer_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrendershadowmap_p.h>
#include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
#include "qssgrendercontextcore.h"
QT_BEGIN_NAMESPACE
namespace AtlasHelpers {
struct ShelfPacker {
struct Shelf {
int y;
int height;
int curX;
};
struct ShelfPage {
int pageIndex;
int width;
int height;
int curY;
std::vector<Shelf> shelves;
};
struct AtlasPlacement {
bool success;
int pageIndex;
int x;
int y;
};
const int pageWidth;
const int pageHeight;
std::vector<ShelfPage> pages;
ShelfPacker(int pageWidth, int pageHeight)
: pageWidth(pageWidth)
, pageHeight(pageHeight)
{
}
AtlasPlacement addRectangle(int sizeNeeded) {
// Try each page in turn
for (auto &page : pages) {
AtlasPlacement placement = placeOnPage(page, sizeNeeded);
if (placement.success)
return placement;
}
// If we get here, we need a new page
ShelfPage newPage;
newPage.pageIndex = int(pages.size());
newPage.width = pageWidth;
newPage.height = pageHeight;
newPage.curY = 0;
newPage.shelves.clear();
pages.push_back(newPage);
// now place in the new page
return placeOnPage(pages.back(), sizeNeeded);
}
AtlasPlacement placeOnPage(ShelfPage &page, int sizeNeeded)
{
AtlasPlacement result;
result.success = false;
// Iterate over shelves to see if we can place the rect
for (auto &shelf : page.shelves) {
// check if it fits horizontally
if (shelf.curX + sizeNeeded <= page.width) {
// also check if the shelf's height is enough
// but here we store the shelf's "height" as the max of the rectangles' heights
// If each rect is the same "sizeNeeded," we just need to see if there's room in page.height
// not enough vertical space
if (shelf.y + shelf.height >= page.height)
continue;
// place here
result.success = true;
result.pageIndex = page.pageIndex;
result.x = shelf.curX;
result.y = shelf.y;
// update shelf
shelf.curX += sizeNeeded;
if (sizeNeeded > shelf.height)
shelf.height = sizeNeeded;
return result;
}
}
// No existing shelf had space, so start a new shelf
int newShelfY = 0;
if (!page.shelves.empty()) {
const Shelf &lastShelf = page.shelves.back();
newShelfY = lastShelf.y + lastShelf.height;
}
// check if there's enough space left in the page vertically
if (newShelfY + sizeNeeded > page.height) {
// no space in this page
return result; // success = false
}
// create a new shelf
Shelf newShelf;
newShelf.y = newShelfY;
newShelf.height = sizeNeeded;
newShelf.curX = 0;
page.shelves.push_back(newShelf);
// place rect at top-left of this shelf
Shelf &shelfRef = page.shelves.back();
result.success = true;
result.pageIndex = page.pageIndex;
result.x = shelfRef.curX;
result.y = shelfRef.y;
shelfRef.curX += sizeNeeded;
return result;
}
int pagesNeeded() const { return int(pages.size()); }
};
}
static QRhiTexture *allocateRhiShadowTexture(QRhi *rhi, QRhiTexture::Format format, const QSize &size, quint32 numLayers, QRhiTexture::Flags flags)
{
auto texture = rhi->newTexture(format, size, 1, flags);
if (flags & QRhiTexture::TextureArray)
texture->setArraySize(numLayers);
if (!texture->create())
qWarning("Failed to create shadow map texture of size %dx%d", size.width(), size.height());
return texture;
}
static QRhiRenderBuffer *allocateRhiShadowRenderBuffer(QRhi *rhi, QRhiRenderBuffer::Type type, const QSize &size)
{
auto renderBuffer = rhi->newRenderBuffer(type, size, 1);
if (!renderBuffer->create())
qWarning("Failed to build depth-stencil buffer of size %dx%d", size.width(), size.height());
return renderBuffer;
}
static bool checkCompatibility(QSSGShadowMapEntry *entry, const QVector<QSSGShadowMapEntry::AtlasEntry> &atlasEntries) {
if (!entry)
return false;
for (int i = 0; i < atlasEntries.size(); ++i) {
const auto &atlasEntry = atlasEntries.at(i);
if (!(entry->m_atlasInfo[i].layerIndex == atlasEntry.layerIndex &&
qFuzzyCompare(entry->m_atlasInfo[i].uOffset, atlasEntry.uOffset) &&
qFuzzyCompare(entry->m_atlasInfo[i].vOffset, atlasEntry.vOffset) &&
qFuzzyCompare(entry->m_atlasInfo[i].uvScale, atlasEntry.uvScale)))
return false;
}
return true;
}
static QVector<QSSGShadowMapEntry::AtlasEntry> createAtlasEntries(const QVarLengthArray<AtlasHelpers::ShelfPacker::AtlasPlacement, 4> &atlasPlacements, int entrySize, int atlasPageSize) {
QVector<QSSGShadowMapEntry::AtlasEntry> atlasEntries;
atlasEntries.reserve(atlasPlacements.size());
for (int i = 0; i < atlasPlacements.size(); ++i) {
const auto &placement = atlasPlacements.at(i);
const float x = float(placement.x) / float(atlasPageSize);
const float y = float(placement.y) / float(atlasPageSize);
const float uvScale = float(entrySize) / float(atlasPageSize);
atlasEntries.append({placement.pageIndex, x, y, uvScale});
}
return atlasEntries;
}
QSSGRenderShadowMap::QSSGRenderShadowMap(const QSSGRenderContextInterface &inContext)
: m_context(inContext)
{
}
QSSGRenderShadowMap::~QSSGRenderShadowMap()
{
releaseCachedResources();
}
void QSSGRenderShadowMap::releaseCachedResources()
{
for (QSSGShadowMapEntry &entry : m_shadowMapList)
entry.destroyRhiResources();
if (m_shadowMapAtlasTexture)
m_shadowMapAtlasTexture.reset();
if (m_shadowMapBlueNoiseTexture)
m_shadowMapBlueNoiseTexture.reset();
qDeleteAll(m_layerDepthStencilBuffers);
m_layerDepthStencilBuffers.clear();
qDeleteAll(m_layerRenderTargets);
m_layerRenderTargets.clear();
qDeleteAll(m_layerRenderPassDescriptors);
m_layerRenderPassDescriptors.clear();
m_shadowMapList.clear();
}
void QSSGRenderShadowMap::addShadowMaps(const QSSGShaderLightList &renderableLights)
{
QRhi *rhi = m_context.rhiContext()->rhi();
// Bail out if there is no QRhi, since we can't add entries without it
if (!rhi)
return;
// Handle blue noise texture
if (!m_shadowMapBlueNoiseTexture) {
// The blue noise image is based on two random blue noise images with values v0 and v1.
// Then the cos and sin of these are stored to avoid trigonmoetric functions
// in the shadow mapping shader.
// R = cos(v0)
// G = sin(v0)
// B = cos(v1)
// A = sin(v1)
QImage blueNoiseImage(QStringLiteral(":/res/textures/blue_noise.png"));
if (blueNoiseImage.isNull()) {
qWarning("Failed to load blue noise texture!");
} else {
Q_ASSERT(blueNoiseImage.size() == QSize(64, 64));
Q_ASSERT(blueNoiseImage.format() == QImage::Format_ARGB32);
blueNoiseImage = blueNoiseImage.convertToFormat(QImage::Format_RGBA8888);
m_shadowMapBlueNoiseTexture.reset(allocateRhiShadowTexture(rhi, QRhiTexture::RGBA8, QSize(64, 64), 0, {}));
if (!m_shadowMapBlueNoiseTexture->create()) {
qWarning("Failed to create blue noise texture");
} else {
QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
QRhiTextureSubresourceUploadDescription subresDesc(blueNoiseImage);
rub->uploadTexture(m_shadowMapBlueNoiseTexture.get(),
QRhiTextureUploadDescription({ QRhiTextureUploadEntry(0, 0, subresDesc) }));
QRhiCommandBuffer *cb = m_context.rhiContext()->commandBuffer();
cb->resourceUpdate(rub);
}
}
}
const quint32 numLights = renderableLights.size();
qsizetype numShadows = 0;
const bool supports32BitTextures = rhi->isTextureFormatSupported(QRhiTexture::R32F);
QRhiTexture::Format format = QRhiTexture::R16F;
quint32 mapSize = 0;
QHash<quint32, QVector<QSSGShadowMapEntry::AtlasEntry>> lightIndexToAtlasEntries;
// Get format and maximum texture size needed
for (quint32 lightIndex = 0; lightIndex < numLights; ++lightIndex) {
const QSSGShaderLight &shaderLight = renderableLights.at(lightIndex);
if (!shaderLight.shadows)
continue;
// Force R32F format if any light requests it
if (shaderLight.light->m_use32BitShadowmap && supports32BitTextures)
format = QRhiTexture::R32F;
// Find the largest shadow map size needed
if (mapSize < shaderLight.light->m_shadowMapRes)
mapSize = shaderLight.light->m_shadowMapRes;
numShadows += 1;
}
auto atlasPacker = AtlasHelpers::ShelfPacker(mapSize, mapSize);
// Figure out the number of layers needed for the atlas
// including where everthing will go in the atlas
for (quint32 lightIndex = 0; lightIndex < numLights; ++lightIndex) {
const QSSGShaderLight &shaderLight = renderableLights.at(lightIndex);
if (!shaderLight.shadows)
continue;
quint8 mapsNeeded = 0;
if (shaderLight.light->type == QSSGRenderLight::Type::DirectionalLight)
mapsNeeded = shaderLight.light->m_csmNumSplits + 1;
else if (shaderLight.light->type == QSSGRenderLight::Type::SpotLight)
mapsNeeded = 1;
else if (shaderLight.light->type == QSSGRenderLight::Type::PointLight)
mapsNeeded = 2;
QVarLengthArray<AtlasHelpers::ShelfPacker::AtlasPlacement, 4> atlasPlacements;
for (quint8 i = 0; i < mapsNeeded; ++i) {
const int size = shaderLight.light->m_shadowMapRes;
auto result = atlasPacker.addRectangle(size);
if (result.success)
atlasPlacements.push_back(result);
}
lightIndexToAtlasEntries.insert(lightIndex, createAtlasEntries(atlasPlacements, shaderLight.light->m_shadowMapRes, mapSize));
}
const QSize texSize = QSize(mapSize, mapSize);
const quint32 layersNeeded = atlasPacker.pagesNeeded();
// Check if we need a rebuild
bool needsRebuild = numShadows != shadowMapEntryCount();
if (!m_shadowMapAtlasTexture ||
m_shadowMapAtlasTexture->pixelSize() != texSize ||
m_shadowMapAtlasTexture->arraySize() != int(layersNeeded) ||
m_shadowMapAtlasTexture->format() != format) {
// If we need a new texture as well
if (m_shadowMapAtlasTexture) {
m_shadowMapAtlasTexture.reset();
}
m_shadowMapAtlasTexture.reset(allocateRhiShadowTexture(rhi, format, texSize, layersNeeded, QRhiTexture::RenderTarget | QRhiTexture::TextureArray));
qDeleteAll(m_layerDepthStencilBuffers);
m_layerDepthStencilBuffers.clear();
qDeleteAll(m_layerRenderTargets);
m_layerRenderTargets.clear();
qDeleteAll(m_layerRenderPassDescriptors);
m_layerRenderPassDescriptors.clear();
for (quint32 i = 0; i < layersNeeded; ++i) {
// Recreate per layer RenderBuffers, TextureRenderTarget, and RenderPassDescriptors
QRhiRenderBuffer *depthStencilBuffer = allocateRhiShadowRenderBuffer(rhi, QRhiRenderBuffer::DepthStencil, texSize);
QRhiTextureRenderTargetDescription rtDesc;
QRhiColorAttachment attachment(m_shadowMapAtlasTexture.get());
attachment.setLayer(i);
rtDesc.setColorAttachments({ attachment });
rtDesc.setDepthStencilBuffer(depthStencilBuffer);
QRhiTextureRenderTarget *rt = rhi->newTextureRenderTarget(rtDesc);
rt->setDescription(rtDesc);
rt->setFlags(QRhiTextureRenderTarget::PreserveColorContents); // Don't clear between passes since this is an atlas
QRhiRenderPassDescriptor *rpDesc = rt->newCompatibleRenderPassDescriptor();
rt->setRenderPassDescriptor(rpDesc);
if (!rt->create())
qWarning("Failed to build shadow map render target");
rt->setName(QByteArrayLiteral("shadow map atlas layer") + QByteArray::number(i));
m_layerDepthStencilBuffers.append(depthStencilBuffer);
m_layerRenderTargets.append(rt);
m_layerRenderPassDescriptors.append(rpDesc);
}
needsRebuild = true;
}
// If we need to allocate the RHI resources
if (!m_sharedFrontCubeToAtlasUniformBuffer || !m_sharedBackCubeToAtlasUniformBuffer) {
const quint32 uniformValueFront = 0;
const quint32 uniformValueBack = 1;
QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
if (!m_sharedFrontCubeToAtlasUniformBuffer) {
m_sharedFrontCubeToAtlasUniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(uint32_t)));
m_sharedFrontCubeToAtlasUniformBuffer->create();
rub->updateDynamicBuffer(m_sharedFrontCubeToAtlasUniformBuffer.get(), 0, sizeof(quint32), &uniformValueFront);
}
if (!m_sharedBackCubeToAtlasUniformBuffer) {
m_sharedBackCubeToAtlasUniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(uint32_t)));
m_sharedBackCubeToAtlasUniformBuffer->create();
rub->updateDynamicBuffer(m_sharedBackCubeToAtlasUniformBuffer.get(), 0, sizeof(quint32), &uniformValueBack);
}
QRhiCommandBuffer *cb = m_context.rhiContext()->commandBuffer();
cb->resourceUpdate(rub);
}
if (!m_sharedCubeToAtlasSampler) {
m_sharedCubeToAtlasSampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
m_sharedCubeToAtlasSampler->create();
}
if (!m_shadowClearSrb) {
m_shadowClearSrb.reset(rhi->newShaderResourceBindings());
m_shadowClearSrb->create();
}
if (!needsRebuild) {
// Check if relevant shadow properties has changed
for (quint32 lightIndex = 0; lightIndex < numLights; ++lightIndex) {
const QSSGShaderLight &shaderLight = renderableLights.at(lightIndex);
if (!shaderLight.shadows)
continue;
QSSGShadowMapEntry *pEntry = shadowMapEntry(lightIndex);
if (!pEntry) {
needsRebuild = true;
break;
}
const auto &atlasEntires = lightIndexToAtlasEntries.value(lightIndex);
if (!checkCompatibility(pEntry, atlasEntires)) {
needsRebuild = true;
break;
}
}
}
if (!needsRebuild)
return;
// Rebuild then
for (QSSGShadowMapEntry &entry : m_shadowMapList)
entry.destroyRhiResources();
m_shadowMapList.clear();
for (quint32 lightIndex = 0; lightIndex < numLights; ++lightIndex) {
const QSSGShaderLight &shaderLight = renderableLights.at(lightIndex);
if (!shaderLight.shadows)
continue;
addShadowMap(lightIndex,
texSize,
lightIndexToAtlasEntries.value(lightIndex),
shaderLight.light->m_csmNumSplits,
format,
shaderLight.light->type == QSSGRenderLight::Type::PointLight,
shaderLight.light->debugObjectName);
}
}
QSSGShadowMapEntry *QSSGRenderShadowMap::addShadowMap(quint32 lightIdx,
QSize size,
QVector<QSSGShadowMapEntry::AtlasEntry> atlasEntries,
quint32 csmNumSplits,
QRhiTexture::Format rhiFormat,
bool isPointLight,
const QString &renderNodeObjName)
{
QRhi *rhi = m_context.rhiContext()->rhi();
QSSGShadowMapEntry *pEntry = shadowMapEntry(lightIdx);
Q_ASSERT(rhi);
Q_ASSERT(!pEntry);
Q_ASSERT(!atlasEntries.isEmpty());
m_shadowMapList.push_back(QSSGShadowMapEntry::withAtlas(lightIdx));
pEntry = &m_shadowMapList.back();
pEntry->m_csmNumSplits = csmNumSplits;
if (isPointLight) {
const QSize localSize = size * atlasEntries.first().uvScale;
// First pass renders to depth cube map
const QByteArray rtName = renderNodeObjName.toLatin1();
pEntry->m_rhiDepthCube = allocateRhiShadowTexture(rhi, rhiFormat, localSize, 0, QRhiTexture::RenderTarget | QRhiTexture::CubeMap);
pEntry->m_rhiDepthStencilCube = allocateRhiShadowRenderBuffer(rhi, QRhiRenderBuffer::DepthStencil, localSize);
for (const auto face : QSSGRenderTextureCubeFaces) {
QRhiTextureRenderTarget *&rt(pEntry->m_rhiRenderTargetCube[quint8(face)]);
Q_ASSERT(!rt);
QRhiColorAttachment att(pEntry->m_rhiDepthCube);
att.setLayer(quint8(face)); // 6 render targets, each referencing one face of the cubemap
QRhiTextureRenderTargetDescription rtDesc;
rtDesc.setColorAttachments({ att });
rtDesc.setDepthStencilBuffer(pEntry->m_rhiDepthStencilCube);
rt = rhi->newTextureRenderTarget(rtDesc);
rt->setDescription(rtDesc);
if (!pEntry->m_rhiRenderPassDescCube)
pEntry->m_rhiRenderPassDescCube = rt->newCompatibleRenderPassDescriptor();
rt->setRenderPassDescriptor(pEntry->m_rhiRenderPassDescCube);
if (!rt->create())
qWarning("Failed to build shadow map render target");
rt->setName(rtName + QByteArrayLiteral(" shadow cube face: ") + QSSGBaseTypeHelpers::displayName(face));
}
if (!pEntry->m_cubeToAtlasFrontSrb) {
pEntry->m_cubeToAtlasFrontSrb = rhi->newShaderResourceBindings();
pEntry->m_cubeToAtlasFrontSrb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::FragmentStage, m_sharedFrontCubeToAtlasUniformBuffer.get()),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, pEntry->m_rhiDepthCube, m_sharedCubeToAtlasSampler.get())
});
pEntry->m_cubeToAtlasFrontSrb->create();
}
if (!pEntry->m_cubeToAtlasBackSrb) {
pEntry->m_cubeToAtlasBackSrb = rhi->newShaderResourceBindings();
pEntry->m_cubeToAtlasBackSrb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::FragmentStage, m_sharedBackCubeToAtlasUniformBuffer.get()),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, pEntry->m_rhiDepthCube, m_sharedCubeToAtlasSampler.get())
});
pEntry->m_cubeToAtlasBackSrb->create();
}
}
// Additional graphics resources: samplers, render targets.
const quint32 entriesCount = atlasEntries.size();
for (quint32 splitIndex = 0; splitIndex < entriesCount; splitIndex++) {
pEntry->m_atlasInfo[splitIndex].layerIndex = atlasEntries.at(splitIndex).layerIndex;
pEntry->m_atlasInfo[splitIndex].uOffset = atlasEntries.at(splitIndex).uOffset;
pEntry->m_atlasInfo[splitIndex].vOffset = atlasEntries.at(splitIndex).vOffset;
pEntry->m_atlasInfo[splitIndex].uvScale = atlasEntries.at(splitIndex).uvScale;
quint32 layerId = atlasEntries.at(splitIndex).layerIndex;
pEntry->m_rhiRenderTargets[splitIndex] = m_layerRenderTargets[layerId];
pEntry->m_rhiRenderPassDesc[splitIndex] = m_layerRenderPassDescriptors[layerId];
}
pEntry->m_lightIndex = lightIdx;
return pEntry;
}
QSSGShadowMapEntry *QSSGRenderShadowMap::shadowMapEntry(int lightIdx)
{
Q_ASSERT(lightIdx >= 0);
for (int i = 0; i < m_shadowMapList.size(); i++) {
QSSGShadowMapEntry *pEntry = &m_shadowMapList[i];
if (pEntry->m_lightIndex == quint32(lightIdx))
return pEntry;
}
return nullptr;
}
QRhiTextureRenderTarget *QSSGRenderShadowMap::layerRenderTarget(int layerIndex)
{
if (layerIndex < 0 || layerIndex >= m_layerRenderTargets.size())
return nullptr;
return m_layerRenderTargets.at(layerIndex);
}
QRhiRenderPassDescriptor *QSSGRenderShadowMap::layerRenderPassDescriptor(int layerIndex)
{
if (layerIndex < 0 || layerIndex >= m_layerRenderPassDescriptors.size())
return nullptr;
return m_layerRenderPassDescriptors.at(layerIndex);
}
QRhiTexture *QSSGRenderShadowMap::shadowMapAtlasTexture() const
{
return m_shadowMapAtlasTexture.get();
}
QRhiTexture *QSSGRenderShadowMap::shadowMapBlueNoiseTexture() const
{
return m_shadowMapBlueNoiseTexture.get();
}
QSSGShadowMapEntry::QSSGShadowMapEntry()
: m_lightIndex(std::numeric_limits<quint32>::max())
{
}
QSSGShadowMapEntry QSSGShadowMapEntry::withAtlas(quint32 lightIdx)
{
QSSGShadowMapEntry e;
e.m_lightIndex = lightIdx;
return e;
}
void QSSGShadowMapEntry::destroyRhiResources()
{
delete m_rhiDepthCube;
m_rhiDepthCube = nullptr;
delete m_rhiDepthStencilCube;
m_rhiDepthStencilCube = nullptr;
qDeleteAll(m_rhiRenderTargetCube);
m_rhiRenderTargetCube.fill(nullptr);
delete m_rhiRenderPassDescCube;
m_rhiRenderPassDescCube = nullptr;
delete m_cubeToAtlasFrontSrb;
m_cubeToAtlasFrontSrb = nullptr;
delete m_cubeToAtlasBackSrb;
m_cubeToAtlasBackSrb = nullptr;
// un-owned references
m_rhiRenderTargets.fill(nullptr);
m_rhiRenderPassDesc.fill(nullptr);
}
QT_END_NAMESPACE