Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for an Atlas allocator for shadow maps #8345

Merged
merged 3 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions filament/src/AtlasAllocator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ AtlasAllocator::NodeId AtlasAllocator::allocateInLayer(size_t maxHeight) noexcep
}
} else if (candidate.l < int8_t(QuadTree::height())) {
// we need to create the hierarchy down to the level we need

if (candidate.l > 0) {
// first thing to do is to update our parent's children count (the first node
// doesn't have a parent).
size_t const pi = QuadTreeUtils::parent(candidate.l, candidate.code);
Node& parentNode = mQuadTree[pi];
assert_invariant(!parentNode.isAllocated());
assert_invariant(!parentNode.hasAllChildren());
parentNode.children++;
}

NodeId found{ -1, 0 };
QuadTree::traverse(candidate.l, candidate.code,
[this, n, &found](NodeId const& curr) -> QuadTree::TraversalResult {
Expand Down
9 changes: 6 additions & 3 deletions filament/src/AtlasAllocator.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@

#include <private/filament/EngineEnums.h>

#include <stdint.h>
#include <stddef.h>

class AtlasAllocator_AllocateFirstLevel_Test;
class AtlasAllocator_AllocateSecondLevel_Test;
class AtlasAllocator_AllocateMixed0_Test;
Expand All @@ -45,11 +48,11 @@ class AtlasAllocator {
* track which children though.
*/
struct Node {
// whether this node is allocated. Implies no children.
// Whether this node is allocated. Implies no children.
constexpr bool isAllocated() const noexcept { return allocated; }
// whether this node has children. Implies it's not allocated.
// Whether this node has children. Implies it's not allocated.
constexpr bool hasChildren() const noexcept { return children != 0; }
// whether this node has all four children. Implies hasChildren().
// Whether this node has all four children. Implies hasChildren().
constexpr bool hasAllChildren() const noexcept { return children == 4; }
bool allocated : 1; // true / false
uint8_t children : 3; // 0, 1, 2, 3, 4
Expand Down
14 changes: 9 additions & 5 deletions filament/src/ShadowMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,10 @@ void ShadowMap::updateSceneInfoSpot(mat4f const& Mv, FScene const& scene,
}
);
}
void ShadowMap::setAllocation(uint8_t layer, backend::Viewport viewport) noexcept {
mLayer = layer;
mOffset = { viewport.left, viewport.bottom };
}

backend::Viewport ShadowMap::getViewport() const noexcept {
// We set a viewport with a 1-texel border for when we index outside the texture.
Expand All @@ -1305,7 +1309,7 @@ backend::Viewport ShadowMap::getViewport() const noexcept {
// can work properly if the shadowmap is in an atlas (and we can't rely on h/w clamp).
const uint32_t dim = mOptions->mapSize;
const uint16_t border = 1u;
return { border, border, dim - 2u * border, dim - 2u * border };
return { mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border };
}

backend::Viewport ShadowMap::getScissor() const noexcept {
Expand All @@ -1319,10 +1323,10 @@ backend::Viewport ShadowMap::getScissor() const noexcept {
const uint16_t border = 1u;
switch (mShadowType) {
case ShadowType::DIRECTIONAL:
return { border, border, dim - 2u * border, dim - 2u * border };
return { mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border };
case ShadowType::SPOT:
case ShadowType::POINT:
return { 0, 0, dim, dim };
return { mOffset.x, mOffset.y, dim, dim };
}
}

Expand All @@ -1345,8 +1349,8 @@ math::float4 ShadowMap::getClampToEdgeCoords(ShadowMapInfo const& shadowMapInfo)

float const texel = 1.0f / float(shadowMapInfo.atlasDimension);
float const dim = float(mOptions->mapSize);
float const l = border;
float const b = border;
float const l = float(mOffset.x) + border;
float const b = float(mOffset.y) + border;
float const w = dim - 2.0f * border;
float const h = dim - 2.0f * border;
float4 const v = float4{ l, b, l + w, b + h } * texel;
Expand Down
9 changes: 6 additions & 3 deletions filament/src/ShadowMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ class ShadowMap {
LightManager::ShadowOptions const* getShadowOptions() const noexcept { return mOptions; }
size_t getLightIndex() const { return mLightIndex; }
uint16_t getShadowIndex() const { return mShadowIndex; }
void setLayer(uint8_t layer) noexcept { mLayer = layer; }
void setAllocation(uint8_t layer, backend::Viewport viewport) noexcept;

uint8_t getLayer() const noexcept { return mLayer; }
backend::Viewport getViewport() const noexcept;
backend::Viewport getScissor() const noexcept;
Expand Down Expand Up @@ -339,7 +340,7 @@ class ShadowMap {
{ 2, 6, 7, 3 }, // top
};

mutable ShadowMapDescriptorSet mPerShadowMapUniforms; // 4
mutable ShadowMapDescriptorSet mPerShadowMapUniforms; // 48

FCamera* mCamera = nullptr; // 8
FCamera* mDebugCamera = nullptr; // 8
Expand All @@ -351,8 +352,10 @@ class ShadowMap {
uint16_t mShadowIndex = 0; // our index in the shadowMap vector // 2
uint8_t mLayer = 0; // our layer in the shadowMap texture // 1
ShadowType mShadowType : 2; // :2
bool mHasVisibleShadows : 2; // :2
bool mHasVisibleShadows : 1; // :1
uint8_t mFace : 3; // :3
math::ushort2 mOffset{}; // 4
UTILS_UNUSED uint8_t reserved[4]; // 4
};

} // namespace filament
Expand Down
131 changes: 98 additions & 33 deletions filament/src/ShadowMapManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
*/

#include "ShadowMapManager.h"
#include "AtlasAllocator.h"
#include "RenderPass.h"
#include "ShadowMap.h"

#include <filament/Frustum.h>
#include <filament/LightManager.h>
#include <filament/Options.h>

#include <iterator>
#include <private/filament/EngineEnums.h>

#include "components/RenderableManager.h"
Expand Down Expand Up @@ -54,6 +56,7 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <functional>
#include <limits>
#include <new>
#include <memory>
Expand All @@ -75,6 +78,8 @@ ShadowMapManager::ShadowMapManager(FEngine& engine)
&engine.debug.shadowmap.disable_light_frustum_align);
debugRegistry.registerProperty("d.shadowmap.depth_clamp",
&engine.debug.shadowmap.depth_clamp);

mFeatureShadowAllocator = engine.features.engine.shadows.use_shadow_atlas;
}

ShadowMapManager::~ShadowMapManager() {
Expand All @@ -101,6 +106,10 @@ void ShadowMapManager::terminate(FEngine& engine,
}
}

size_t ShadowMapManager::getMaxShadowMapCount() const noexcept {
return mFeatureShadowAllocator ? CONFIG_MAX_SHADOWMAPS : CONFIG_MAX_SHADOW_LAYERS;
}

void ShadowMapManager::terminate(FEngine& engine) {
if (UTILS_UNLIKELY(mInitialized)) {
DriverApi& driver = engine.getDriverApi();
Expand Down Expand Up @@ -232,7 +241,7 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
// -------------------------------------------------------------------------------------------

struct PrepareShadowPassData {
struct ShadowPass {
struct ShadowPass { // 112 bytes
mutable RenderPass::Executor executor;
ShadowMap* shadowMap;
utils::Range<uint32_t> range;
Expand All @@ -248,7 +257,7 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG

auto& prepareShadowPass = fg.addPass<PrepareShadowPassData>("Prepare Shadow Pass",
[&](FrameGraph::Builder& builder, auto& data) {
data.passList.reserve(CONFIG_MAX_SHADOWMAPS);
data.passList.reserve(getMaxShadowMapCount());
data.shadows = builder.createTexture("Shadowmap", {
.width = textureRequirements.size, .height = textureRequirements.size,
.depth = textureRequirements.layers,
Expand Down Expand Up @@ -308,7 +317,18 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
}
}

assert_invariant(passList.size() <= textureRequirements.layers);
assert_invariant(mFeatureShadowAllocator ||
passList.size() <= textureRequirements.layers);

if (mFeatureShadowAllocator) {
// sort shadow passes by layer so that we can update all the shadow maps of
// a layer in one render pass.
std::sort(passList.begin(), passList.end(), [](
PrepareShadowPassData::ShadowPass const& lhs,
PrepareShadowPassData::ShadowPass const& rhs) {
return lhs.shadowMap->getLayer() < rhs.shadowMap->getLayer();
});
}

// This pass must be declared as having a side effect because it never gets a
// "read" from one of its resource (only writes), so the FrameGraph culls it.
Expand Down Expand Up @@ -336,7 +356,8 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
// same command buffer for all shadow map, but then we'd generate
// a lot of unneeded draw calls.
// To do this efficiently, we'd need a way to cull draw calls already
// recorded in the command buffer, per shadow map.
// recorded in the command buffer, per shadow map. Maybe this could
// be done with indirect draw calls.

// Note: the output of culling below is stored in scene->getRenderableData()

Expand Down Expand Up @@ -429,13 +450,26 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
};

auto const& passList = prepareShadowPass.getData().passList;
for (auto const& entry: passList) {
auto first = passList.begin();
while (first != passList.end()) {
auto const& entry = *first;

const uint8_t layer = entry.shadowMap->getLayer();
const auto* options = entry.shadowMap->getShadowOptions();
const auto msaaSamples = textureRequirements.msaaSamples;
const bool blur = entry.shadowMap->hasVisibleShadows() &&
view.hasVSM() && options->vsm.blurWidth > 0.0f;

auto last = first;
// loop over each shadow pass to find its layer range
while (last != passList.end() && last->shadowMap->getLayer() == layer) {
++last;
}

assert_invariant(mFeatureShadowAllocator ||
std::distance(first, last) == 1);

// And render all shadow pass of a given layer as a single render pass
auto& shadowPass = fg.addPass<ShadowPassData>("Shadow Pass",
[&](FrameGraph::Builder& builder, auto& data) {

Expand Down Expand Up @@ -507,7 +541,7 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
// blurring.
data.rt = blur ? data.rt : rt;
},
[=, &engine, &entry](FrameGraphResources const& resources,
[=, &engine](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {

// Note: we capture entry by reference here. That's actually okay because
Expand All @@ -520,22 +554,31 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
auto rt = resources.getRenderPassInfo(data.rt);

driver.beginRenderPass(rt.target, rt.params);
// if we know there are no visible shadows, we can skip rendering, but
// we need the render-pass to clear/initialize the shadow-map
// Note: this is always true for directional/cascade shadows.
if (entry.shadowMap->hasVisibleShadows()) {
entry.shadowMap->bind(driver);
entry.executor.overrideScissor(entry.shadowMap->getScissor());
entry.executor.execute(engine, driver);

for (auto curr = first; curr != last; curr++) {
// if we know there are no visible shadows, we can skip rendering, but
// we need the render-pass to clear/initialize the shadow-map
// Note: this is always true for directional/cascade shadows.
if (curr->shadowMap->hasVisibleShadows()) {
curr->shadowMap->bind(driver);
curr->executor.overrideScissor(curr->shadowMap->getScissor());
curr->executor.execute(engine, driver);
}
}

driver.endRenderPass();
});

first = last;

// now emit the blurring passes if needed
if (UTILS_UNLIKELY(blur)) {
auto& ppm = engine.getPostProcessManager();

// FIXME: this `options` is for the first shadowmap in the list, but it applies to
// the whole layer. Blurring should happen per shadowmap, not for the whole
// layer.

const float blurWidth = options->vsm.blurWidth;
if (blurWidth > 0.0f) {
const float sigma = (blurWidth + 1.0f) / 6.0f;
Expand All @@ -547,6 +590,9 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
false, kernelWidth, sigma);
}

// FIXME: mipmapping here is broken because it'll access texels from adjacent
// shadow maps.

// If the shadow texture has more than one level, mipmapping was requested, either directly
// or indirectly via anisotropic filtering.
// So generate the mipmaps for each layer
Expand Down Expand Up @@ -968,32 +1014,59 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine
void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view,
FScene::LightSoa const&) noexcept {

// Lay out the shadow maps. For now, we take the largest requested dimension and allocate a
// texture of that size. Each cascade / shadow map gets its own layer in the array texture.
// The directional shadow cascades start on layer 0, followed by spotlights.
uint8_t layer = 0;
uint32_t maxDimension = 0;
bool elvsm = false;
for (ShadowMap& shadowMap : getCascadedShadowMap()) {

for (ShadowMap const& shadowMap : getCascadedShadowMap()) {
// Shadow map size should be the same for all cascades.
auto const& options = shadowMap.getShadowOptions();
maxDimension = std::max(maxDimension, options->mapSize);
elvsm = elvsm || options->vsm.elvsm;
shadowMap.setLayer(layer++);
}
for (ShadowMap& shadowMap : getSpotShadowMaps()) {

for (ShadowMap const& shadowMap : getSpotShadowMaps()) {
auto const& options = shadowMap.getShadowOptions();
maxDimension = std::max(maxDimension, options->mapSize);
elvsm = elvsm || options->vsm.elvsm;
shadowMap.setLayer(layer++);
}

const uint8_t layersNeeded = layer;
uint8_t layersNeeded = 0;

std::function const allocateFromAtlas =
[&layersNeeded, allocator = AtlasAllocator{ maxDimension }](
ShadowMap* pShadowMap) mutable {
// Allocate shadowmap from our Atlas Allocator
auto const& options = pShadowMap->getShadowOptions();
auto [layer, pos] = allocator.allocate(options->mapSize);
assert_invariant(layer >= 0);
assert_invariant(!pos.empty());
pShadowMap->setAllocation(layer, pos);
layersNeeded = std::max(uint8_t(layer + 1), layersNeeded);
};

std::function const allocateFromTextureArray =
[&layersNeeded, layer = 0](ShadowMap* pShadowMap) mutable {
// Layout the shadow maps. For now, we take the largest requested dimension and allocate a
// texture of that size. Each cascade / shadow map gets its own layer in the array texture.
// The directional shadow cascades start on layer 0, followed by spotlights.
pShadowMap->setAllocation(layer, {});
layersNeeded = ++layer;
};

auto& allocateShadowmapTexture = mFeatureShadowAllocator ?
allocateFromAtlas : allocateFromTextureArray;

for (ShadowMap& shadowMap : getCascadedShadowMap()) {
allocateShadowmapTexture(&shadowMap);
}
for (ShadowMap& shadowMap : getSpotShadowMaps()) {
allocateShadowmapTexture(&shadowMap);
}

// Generate mipmaps for VSM when anisotropy is enabled or when requested
auto const& vsmShadowOptions = view.getVsmShadowOptions();
const bool useMipmapping = view.hasVSM() &&
((vsmShadowOptions.anisotropy > 0) || vsmShadowOptions.mipmapping);
((vsmShadowOptions.anisotropy > 0) || vsmShadowOptions.mipmapping);

uint8_t msaaSamples = vsmShadowOptions.msaaSamples;
if (engine.getDriverApi().isWorkaroundNeeded(Workaround::DISABLE_BLIT_INTO_TEXTURE_ARRAY)) {
Expand All @@ -1003,17 +1076,9 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
TextureFormat format = TextureFormat::DEPTH16;
if (view.hasVSM()) {
if (vsmShadowOptions.highPrecision) {
if (elvsm) {
format = TextureFormat::RGBA32F;
} else {
format = TextureFormat::RG32F;
}
format = elvsm ? TextureFormat::RGBA32F : TextureFormat::RG32F;
} else {
if (elvsm) {
format = TextureFormat::RGBA16F;
} else {
format = TextureFormat::RG16F;
}
format = elvsm ? TextureFormat::RGBA16F : TextureFormat::RG16F;
}
}

Expand Down
Loading
Loading