From 37fd83af665b61202b10ab2d1672c2dcc3e99b82 Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Tue, 7 Jan 2025 14:27:42 -0800 Subject: [PATCH] Add support for an Atlas allocator for shadow maps 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 --- filament/src/ShadowMap.h | 5 +- filament/src/ShadowMapManager.cpp | 119 ++++++++++++++---- filament/src/ShadowMapManager.h | 9 +- filament/src/components/LightManager.cpp | 34 +++-- filament/src/details/Engine.cpp | 6 + filament/src/details/Engine.h | 16 ++- filament/src/details/View.cpp | 10 +- .../include/private/filament/EngineEnums.h | 9 +- 8 files changed, 158 insertions(+), 50 deletions(-) diff --git a/filament/src/ShadowMap.h b/filament/src/ShadowMap.h index 1da8b10e2b1..e997dce14cf 100644 --- a/filament/src/ShadowMap.h +++ b/filament/src/ShadowMap.h @@ -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 @@ -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 diff --git a/filament/src/ShadowMapManager.cpp b/filament/src/ShadowMapManager.cpp index 25e8815280d..b6ae94ab82d 100644 --- a/filament/src/ShadowMapManager.cpp +++ b/filament/src/ShadowMapManager.cpp @@ -15,6 +15,7 @@ */ #include "ShadowMapManager.h" +#include "AtlasAllocator.h" #include "RenderPass.h" #include "ShadowMap.h" @@ -22,6 +23,7 @@ #include #include +#include #include #include "components/RenderableManager.h" @@ -54,6 +56,7 @@ #include #include #include +#include #include #include #include @@ -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() { @@ -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(); @@ -232,7 +241,7 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG // ------------------------------------------------------------------------------------------- struct PrepareShadowPassData { - struct ShadowPass { + struct ShadowPass { // 112 bytes mutable RenderPass::Executor executor; ShadowMap* shadowMap; utils::Range range; @@ -248,7 +257,7 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG auto& prepareShadowPass = fg.addPass("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, @@ -308,7 +317,18 @@ FrameGraphId 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. @@ -336,7 +356,8 @@ FrameGraphId 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() @@ -429,13 +450,26 @@ FrameGraphId 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("Shadow Pass", [&](FrameGraph::Builder& builder, auto& data) { @@ -507,7 +541,7 @@ FrameGraphId 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 @@ -520,22 +554,31 @@ FrameGraphId 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; @@ -547,6 +590,9 @@ FrameGraphId 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 @@ -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)) { diff --git a/filament/src/ShadowMapManager.h b/filament/src/ShadowMapManager.h index 964be0d1593..8450c69573e 100644 --- a/filament/src/ShadowMapManager.h +++ b/filament/src/ShadowMapManager.h @@ -119,6 +119,8 @@ class ShadowMapManager { static void terminate(FEngine& engine, std::unique_ptr& 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, @@ -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::type; + // Each ShadowMap is currently 88 bytes (total of ~12KB for 128 shadow maps) + using ShadowMapStorage = std::aligned_storage::type; using ShadowMapCacheContainer = std::array; 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(&mShadowMapCache[index])); } diff --git a/filament/src/components/LightManager.cpp b/filament/src/components/LightManager.cpp index 1b173ef1ffa..cc9a539413f 100644 --- a/filament/src/components/LightManager.cpp +++ b/filament/src/components/LightManager.cpp @@ -19,14 +19,26 @@ #include "components/LightManager.h" #include "details/Engine.h" +#include "utils/ostream.h" #include +#include #include #include +#include #include #include +#include +#include + +#include +#include + +#include +#include +#include using namespace filament::math; using namespace utils; @@ -58,12 +70,12 @@ struct LightManager::BuilderDetails { }; using BuilderType = LightManager; -BuilderType::Builder::Builder(Type type) noexcept: BuilderBase(type) {} +BuilderType::Builder::Builder(Type type) noexcept: BuilderBase(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; @@ -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))) { @@ -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; @@ -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); @@ -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: @@ -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; diff --git a/filament/src/details/Engine.cpp b/filament/src/details/Engine.cpp index 576e0e081a8..ac2f7cfff86 100644 --- a/filament/src/details/Engine.cpp +++ b/filament/src/details/Engine.cpp @@ -39,6 +39,7 @@ #include #include +#include #include @@ -1305,6 +1306,11 @@ size_t FEngine::getSkyboxeCount() const noexcept { return mSkyboxes.size(); } size_t FEngine::getColorGradingCount() const noexcept { return mColorGradings.size(); } size_t FEngine::getRenderTargetCount() const noexcept { return mRenderTargets.size(); } +size_t FEngine::getMaxShadowMapCount() const noexcept { + return features.engine.shadows.use_shadow_atlas ? + CONFIG_MAX_SHADOWMAPS : CONFIG_MAX_SHADOW_LAYERS; +} + void* FEngine::streamAlloc(size_t size, size_t alignment) noexcept { // we allow this only for small allocations if (size > 65536) { diff --git a/filament/src/details/Engine.h b/filament/src/details/Engine.h index 1f21849051c..7788c41e596 100644 --- a/filament/src/details/Engine.h +++ b/filament/src/details/Engine.h @@ -247,6 +247,8 @@ class FEngine : public Engine { return mPlatform; } + size_t getMaxShadowMapCount() const noexcept; + // Return a vector of shader languages, in order of preference. utils::FixedCapacityVector getShaderLanguage() const noexcept { switch (mBackend) { @@ -677,6 +679,11 @@ class FEngine : public Engine { } debug; struct { + struct { + struct { + bool use_shadow_atlas = false; + } shadows; + } engine; struct { struct { bool assert_native_window_is_valid = false; @@ -686,7 +693,7 @@ class FEngine : public Engine { } backend; } features; - std::array const mFeatures{{ + std::array const mFeatures{{ { "backend.disable_parallel_shader_compile", "Disable parallel shader compilation in GL and Metal backends.", &features.backend.disable_parallel_shader_compile, true }, @@ -695,10 +702,13 @@ class FEngine : public Engine { &features.backend.disable_handle_use_after_free_check, true }, { "backend.opengl.assert_native_window_is_valid", "Asserts that the ANativeWindow is valid when rendering starts.", - &features.backend.opengl.assert_native_window_is_valid, true } + &features.backend.opengl.assert_native_window_is_valid, true }, + { "engine.shadows.use_shadow_atlas", + "Uses an array of atlases to store shadow maps.", + &features.engine.shadows.use_shadow_atlas, true } }}; - utils::Slice getFeatureFlags() const noexcept { + utils::Slice getFeatureFlags() const noexcept { return { mFeatures.data(), mFeatures.size() }; } diff --git a/filament/src/details/View.cpp b/filament/src/details/View.cpp index 36e38f0153e..24b203e48d2 100644 --- a/filament/src/details/View.cpp +++ b/filament/src/details/View.cpp @@ -364,21 +364,23 @@ void FView::prepareShadowing(FEngine& engine, FScene::RenderableSoa& renderableD if (UTILS_LIKELY(!lcm.isShadowCaster(li))) { // Because we early exit here, we need to make sure we mark the light as non-casting. // See `ShadowMapManager::updateSpotShadowMaps` for const_cast<> justification. - const_cast( - lightData.elementAt(l)).castsShadows = false; + auto& shadowInfo = const_cast( + lightData.elementAt(l)); + shadowInfo.castsShadows = false; continue; // doesn't cast shadows } const bool spotLight = lcm.isSpotLight(li); + const size_t maxShadowMapCount = engine.getMaxShadowMapCount(); const size_t shadowMapCountNeeded = spotLight ? 1 : 6; - if (shadowMapCount + shadowMapCountNeeded <= CONFIG_MAX_SHADOWMAPS) { + if (shadowMapCount + shadowMapCountNeeded <= maxShadowMapCount) { shadowMapCount += shadowMapCountNeeded; const auto& shadowOptions = lcm.getShadowOptions(li); builder.shadowMap(l, spotLight, &shadowOptions); } - if (shadowMapCount >= CONFIG_MAX_SHADOWMAPS) { + if (shadowMapCount >= maxShadowMapCount) { break; // we ran out of spotlight shadow casting } } diff --git a/libs/filabridge/include/private/filament/EngineEnums.h b/libs/filabridge/include/private/filament/EngineEnums.h index c2dd12f6cbd..2ba000c5db0 100644 --- a/libs/filabridge/include/private/filament/EngineEnums.h +++ b/libs/filabridge/include/private/filament/EngineEnums.h @@ -100,11 +100,11 @@ constexpr size_t CONFIG_MAX_LIGHT_INDEX = CONFIG_MAX_LIGHT_COUNT - 1; // Updating this value necessitates a material version bump. constexpr size_t CONFIG_MAX_RESERVED_SPEC_CONSTANTS = 16; -// The maximum number of shadowmaps. -// There is currently a maximum limit of 128 shadowmaps. +// The maximum number of shadow maps possible. +// There is currently a maximum limit of 128 shadow maps. // Factors contributing to this limit: // - minspec for UBOs is 16KiB, which currently can hold a maximum of 128 entries -constexpr size_t CONFIG_MAX_SHADOWMAPS = 64; +constexpr size_t CONFIG_MAX_SHADOWMAPS = 128; // The maximum number of shadow layers. // There is currently a maximum limit of 255 layers. @@ -112,7 +112,8 @@ constexpr size_t CONFIG_MAX_SHADOWMAPS = 64; // - minspec for 2d texture arrays layer is 256 // - we're using uint8_t to store the number of layers (255 max) // - nonsensical to be larger than the number of shadowmaps -constexpr size_t CONFIG_MAX_SHADOW_LAYERS = CONFIG_MAX_SHADOWMAPS; +// - AtlasAllocator depth limits it to 64 +constexpr size_t CONFIG_MAX_SHADOW_LAYERS = 64; // The maximum number of shadow cascades that can be used for directional lights. constexpr size_t CONFIG_MAX_SHADOW_CASCADES = 4;