Skip to content

Commit

Permalink
Add support for an Atlas allocator for shadow maps
Browse files Browse the repository at this point in the history
This feature is controled by a feature flag and is turned off at this point.
This CL shouldn't change the existing behavior of shadowmap allocation as
long as the atlas feature is not enanled.

When enabled, shadowmap allocations are not limited to layers of the
shadowmap texture, instead, an atlas is used meaning a layer can
be shared by multiple shadowmaps. At the very least this can save
a lot of memory as smaller shadowmaps can be packed together.

Currently this feature breaks some VSM features:
- mipmapping
- blurring
  • Loading branch information
pixelflinger committed Jan 9, 2025
1 parent 1af9f0e commit 79b4ad3
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 50 deletions.
5 changes: 3 additions & 2 deletions filament/src/ShadowMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -340,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 @@ -352,9 +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
119 changes: 96 additions & 23 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.setAllocation(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.setAllocation(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 Down
9 changes: 6 additions & 3 deletions filament/src/ShadowMapManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ class ShadowMapManager {
static void terminate(FEngine& engine,
std::unique_ptr<ShadowMapManager>& shadowMapManager);

size_t getMaxShadowMapCount() const noexcept;

// Updates all the shadow maps and performs culling.
// Returns true if any of the shadow maps have visible shadows.
ShadowMapManager::ShadowTechnique update(Builder const& builder,
Expand Down Expand Up @@ -227,17 +229,18 @@ class ShadowMapManager {

// Inline storage for all our ShadowMap objects, we can't easily use a std::array<> directly.
// Because ShadowMap doesn't have a default ctor, and we avoid out-of-line allocations.
// Each ShadowMap is currently 40 bytes (total of 2.5KB for 64 shadow maps)
using ShadowMapStorage = std::aligned_storage<sizeof(ShadowMap), alignof(ShadowMap)>::type;
// Each ShadowMap is currently 88 bytes (total of ~12KB for 128 shadow maps)
using ShadowMapStorage = std::aligned_storage<sizeof(ShadowMap)>::type;
using ShadowMapCacheContainer = std::array<ShadowMapStorage, CONFIG_MAX_SHADOWMAPS>;
ShadowMapCacheContainer mShadowMapCache;
uint32_t mDirectionalShadowMapCount = 0;
uint32_t mSpotShadowMapCount = 0;
bool const mIsDepthClampSupported;
bool mInitialized = false;
bool mFeatureShadowAllocator = false;

ShadowMap& getShadowMap(size_t index) noexcept {
assert_invariant(index < CONFIG_MAX_SHADOWMAPS);
assert_invariant(index < mShadowMapCache.size());
return *std::launder(reinterpret_cast<ShadowMap*>(&mShadowMapCache[index]));
}

Expand Down
34 changes: 23 additions & 11 deletions filament/src/components/LightManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,26 @@
#include "components/LightManager.h"

#include "details/Engine.h"
#include "utils/ostream.h"

#include <filament/LightManager.h>

#include <utils/compiler.h>
#include <utils/debug.h>
#include <utils/Log.h>
#include <utils/ostream.h>

#include <math/fast.h>
#include <math/scalar.h>
#include <math/vec2.h>
#include <math/vec3.h>

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

#include <algorithm>
#include <cmath>
#include <utility>

using namespace filament::math;
using namespace utils;
Expand Down Expand Up @@ -58,12 +70,12 @@ struct LightManager::BuilderDetails {
};

using BuilderType = LightManager;
BuilderType::Builder::Builder(Type type) noexcept: BuilderBase<LightManager::BuilderDetails>(type) {}
BuilderType::Builder::Builder(Type type) noexcept: BuilderBase<BuilderDetails>(type) {}
BuilderType::Builder::~Builder() noexcept = default;
BuilderType::Builder::Builder(BuilderType::Builder const& rhs) noexcept = default;
BuilderType::Builder::Builder(BuilderType::Builder&& rhs) noexcept = default;
BuilderType::Builder& BuilderType::Builder::operator=(BuilderType::Builder const& rhs) noexcept = default;
BuilderType::Builder& BuilderType::Builder::operator=(BuilderType::Builder&& rhs) noexcept = default;
BuilderType::Builder::Builder(Builder const& rhs) noexcept = default;
BuilderType::Builder::Builder(Builder&& rhs) noexcept = default;
BuilderType::Builder& BuilderType::Builder::operator=(Builder const& rhs) noexcept = default;
BuilderType::Builder& BuilderType::Builder::operator=(Builder&& rhs) noexcept = default;

LightManager::Builder& LightManager::Builder::castShadows(bool enable) noexcept {
mImpl->mCastShadows = enable;
Expand Down Expand Up @@ -167,7 +179,7 @@ FLightManager::~FLightManager() {
void FLightManager::init(FEngine&) noexcept {
}

void FLightManager::create(const FLightManager::Builder& builder, utils::Entity entity) {
void FLightManager::create(const Builder& builder, Entity entity) {
auto& manager = mManager;

if (UTILS_UNLIKELY(manager.hasComponent(entity))) {
Expand Down Expand Up @@ -206,7 +218,7 @@ void FLightManager::create(const FLightManager::Builder& builder, utils::Entity
void FLightManager::prepare(backend::DriverApi&) const noexcept {
}

void FLightManager::destroy(utils::Entity e) noexcept {
void FLightManager::destroy(Entity e) noexcept {
Instance const i = getInstance(e);
if (i) {
auto& manager = mManager;
Expand All @@ -227,13 +239,13 @@ void FLightManager::terminate() noexcept {
}
}
}
void FLightManager::gc(utils::EntityManager& em) noexcept {
void FLightManager::gc(EntityManager& em) noexcept {
mManager.gc(em, [this](Entity e) {
destroy(e);
});
}

void FLightManager::setShadowOptions(Instance i, ShadowOptions const& options) noexcept {
void FLightManager::setShadowOptions(Instance const i, ShadowOptions const& options) noexcept {
ShadowParams& params = mManager[i].shadowParams;
params.options = options;
params.options.mapSize = clamp(options.mapSize, 8u, 2048u);
Expand Down Expand Up @@ -294,7 +306,7 @@ void FLightManager::setIntensity(Instance i, float intensity, IntensityUnit unit
if (i) {
Type const type = getLightType(i).type;
float luminousPower = intensity;
float luminousIntensity;
float luminousIntensity = 0.0f;
switch (type) {
case Type::SUN:
case Type::DIRECTIONAL:
Expand Down Expand Up @@ -335,7 +347,7 @@ void FLightManager::setIntensity(Instance i, float intensity, IntensityUnit unit
luminousIntensity = luminousPower * f::ONE_OVER_PI;
} else {
assert_invariant(unit == IntensityUnit::CANDELA);
// intensity specified directly in candela, no conversion needed
// intensity specified directly in Candela, no conversion needed
luminousIntensity = luminousPower;
}
break;
Expand Down
Loading

0 comments on commit 79b4ad3

Please sign in to comment.