diff --git a/Source/Engine/Graphics/Graphics.h b/Source/Engine/Graphics/Graphics.h
index 45883bd69..73882b5e1 100644
--- a/Source/Engine/Graphics/Graphics.h
+++ b/Source/Engine/Graphics/Graphics.h
@@ -118,6 +118,13 @@ public:
// The minimum size in pixels of objects to cast shadows. Improves performance by skipping too small objects (eg. sub-pixel) from rendering into shadow maps.
API_FIELD() static float MinObjectPixelSize;
+
+#if COMPILE_WITH_PROFILER
+ ///
+ /// Dumps active shadow projections info to the log (the next frame). Can be used to inspect what lights are casting shadows (for optimization).
+ ///
+ API_FUNCTION(Attributes="DebugCommand") static void Dump();
+#endif
};
// Motion Vectors rendering configuration.
diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp
index 68fb56d7a..9b341d467 100644
--- a/Source/Engine/Renderer/ShadowsPass.cpp
+++ b/Source/Engine/Renderer/ShadowsPass.cpp
@@ -12,6 +12,7 @@
#include "Engine/Engine/Engine.h"
#include "Engine/Engine/Units.h"
#include "Engine/Graphics/RenderTools.h"
+#include "Engine/Level/Actors/PointLight.h"
#include "Engine/Level/Scene/SceneRendering.h"
#include "Engine/Scripting/Enums.h"
#include "Engine/Utilities/RectPack.h"
@@ -200,6 +201,15 @@ struct ShadowAtlasLight
Float4 CascadeSplits;
ShadowAtlasLightTile Tiles[SHADOWS_MAX_TILES];
ShadowAtlasLightCache Cache;
+#if COMPILE_WITH_PROFILER
+ enum Types
+ {
+ Point,
+ Spot,
+ Directional,
+ } Type;
+ float CachedUpdateRateInv[SHADOWS_MAX_TILES];
+#endif
ShadowAtlasLight()
{
@@ -467,6 +477,114 @@ void ShadowAtlasLightTile::FreeStatic(ShadowsCustomBuffer* buffer)
}
}
+#if COMPILE_WITH_PROFILER
+
+#include "Engine/Core/Utilities.h"
+#include "Engine/Scripting/Scripting.h"
+#include "Engine/Level/Actors/Light.h"
+
+uint64 DumpShadowsFrame = MAX_uint64;
+
+void Graphics::Shadows::Dump()
+{
+ DumpShadowsFrame = Engine::FrameCount + 1;
+}
+
+void UpdateDumpShadows(ShadowsCustomBuffer* shadows = nullptr)
+{
+ if (DumpShadowsFrame != Engine::FrameCount)
+ return;
+ if (!shadows)
+ {
+ LOG(Info, "No active shadows");
+ return;
+ }
+
+ LOG(Info, "Shadows atlas:");
+ if (shadows->Atlas.Width > 0)
+ {
+ float usage = (float)shadows->AtlasPixelsUsed / (shadows->Atlas.Width * shadows->Atlas.Height);
+ LOG(Info, " > Dynamic {}x{}, {}% used, {} tiles", shadows->Atlas.Width, shadows->Atlas.Height, (int32)(usage * 100), shadows->Atlas.Count());
+ }
+ if (shadows->StaticAtlas.Width > 0)
+ {
+ float usage = (float)shadows->StaticAtlasPixelsUsed / (shadows->StaticAtlas.Width * shadows->StaticAtlas.Height);
+ LOG(Info, " > Static {}x{}, {}% used, {} tiles", shadows->Atlas.Width, shadows->Atlas.Height, (int32)(usage * 100), shadows->StaticAtlas.Count());
+ }
+ LOG(Info, " > Buffer size: {}", Utilities::BytesToText(shadows->ShadowsBuffer.Data.Count()));
+ LOG(Info, " > Lights: {}", shadows->Lights.Count());
+
+ LOG(Info, "Shadows:");
+ for (const auto& e : shadows->Lights)
+ {
+ auto& atlasLight = e.Value;
+ const Char* type;
+ switch (atlasLight.Type)
+ {
+ case ShadowAtlasLight::Point:
+ type = TEXT("Point");
+ break;
+ case ShadowAtlasLight::Spot:
+ type = TEXT("Spot");
+ break;
+ case ShadowAtlasLight::Directional:
+ type = TEXT("Directional");
+ break;
+ }
+ auto lightActor = Scripting::TryFindObject(e.Key);
+ if (lightActor)
+ LOG(Info, " > {} Light, '{}', {}", type, lightActor->GetNamePath(), e.Key);
+ else
+ LOG(Info, " > {} Light, {}", type, e.Key);
+ LOG(Info, " Projections: {}", atlasLight.TilesCount);
+ LOG(Info, " Resolution: {}", atlasLight.Resolution);
+ if (atlasLight.CachedUpdateRateInv[0] > 0)
+ {
+ if (atlasLight.Type == ShadowAtlasLight::Directional)
+ {
+ String updateRates;
+ for (int32 i = 0; i < atlasLight.TilesCount; i++)
+ {
+ if (i != 0)
+ updateRates += TEXT(", ");
+ updateRates += StringUtils::ToString(1.0f / atlasLight.CachedUpdateRateInv[i]);
+ }
+ LOG(Info, " Cascade Update Rates: {}", updateRates);
+ }
+ else
+ LOG(Info, " Update Rate: {}", 1.0f / atlasLight.CachedUpdateRateInv[0]);
+ }
+
+ if (atlasLight.StaticState != ShadowAtlasLight::Unused)
+ {
+ const Char* staticState;
+ switch (atlasLight.StaticState)
+ {
+ case ShadowAtlasLight::WaitForGeometryCheck:
+ staticState = TEXT("WaitForGeometryCheck");
+ break;
+ case ShadowAtlasLight::UpdateStaticShadow:
+ staticState = TEXT("UpdateStaticShadow");
+ break;
+ case ShadowAtlasLight::CopyStaticShadow:
+ staticState = TEXT("CopyStaticShadow");
+ break;
+ case ShadowAtlasLight::NoStaticGeometry:
+ staticState = TEXT("NoStaticGeometry");
+ break;
+ case ShadowAtlasLight::FailedToInsertTiles:
+ staticState = TEXT("FailedToInsertTiles");
+ break;
+ }
+ LOG(Info, " Static State: {}", staticState);
+ LOG(Info, " Static Resolution: {}", atlasLight.StaticResolution);
+ LOG(Info, " Has Static Geometry: {}", atlasLight.HasStaticGeometry());
+ }
+ }
+}
+
+#endif
+
String ShadowsPass::ToString() const
{
return TEXT("ShadowsPass");
@@ -824,6 +942,9 @@ bool ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render
// Calculate update rate based on the distance to the view
bool freezeUpdate;
const float updateRateInv = atlasLight.CalculateUpdateRateInv(light, dstLightToView, freezeUpdate);
+#if COMPILE_WITH_PROFILER
+ atlasLight.CachedUpdateRateInv[0] = updateRateInv;
+#endif
float& framesToUpdate = atlasLight.Tiles[0].FramesToUpdate; // Use the first tile for all local light projections to be in sync
if ((framesToUpdate > 0.0f || freezeUpdate) && atlasLight.Cache.DynamicValid && !atlasLight.HasStaticShadowContext)
{
@@ -852,6 +973,9 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render
{
SetupLight(shadows, renderContext, renderContextBatch, (RenderLightData&)light, atlasLight);
+#if COMPILE_WITH_PROFILER
+ atlasLight.Type = ShadowAtlasLight::Directional;
+#endif
const int32 csmCount = atlasLight.TilesCount;
const auto shadowMapsSize = (float)atlasLight.Resolution;
atlasLight.BlendCSM = Graphics::AllowCSMBlending;
@@ -937,6 +1061,9 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render
float dstToCascade = cascadeIndex == 0 ? 0 : atlasLight.CascadeSplits.Raw[cascadeIndex - 1];
bool freezeUpdate;
const float updateRateInv = atlasLight.CalculateUpdateRateInv(light, dstToCascade, freezeUpdate, cascadeIndex != 0);
+#if COMPILE_WITH_PROFILER
+ atlasLight.CachedUpdateRateInv[cascadeIndex] = updateRateInv;
+#endif
auto& tile = atlasLight.Tiles[cascadeIndex];
if ((tile.FramesToUpdate > 0.0f || freezeUpdate) && atlasLight.Cache.DynamicValid)
{
@@ -1087,6 +1214,9 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render
Matrix borderScaleMatrix;
Matrix::Scaling(borderScale, borderScale, 1.0f, borderScaleMatrix);
+#if COMPILE_WITH_PROFILER
+ atlasLight.Type = ShadowAtlasLight::Point;
+#endif
atlasLight.ContextIndex = renderContextBatch.Contexts.Count();
atlasLight.ContextCount = atlasLight.HasStaticShadowContext ? 12 : 6;
renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount);
@@ -1123,6 +1253,9 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render
if (SetupLight(shadows, renderContext, renderContextBatch, (RenderLocalLightData&)light, atlasLight))
return;
+#if COMPILE_WITH_PROFILER
+ atlasLight.Type = ShadowAtlasLight::Spot;
+#endif
atlasLight.ContextIndex = renderContextBatch.Contexts.Count();
atlasLight.ContextCount = atlasLight.HasStaticShadowContext ? 2 : 1;
renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount);
@@ -1223,6 +1356,9 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch&
if (old->LastFrameUsed == currentFrame)
old->LastFrameUsed = 0;
}
+#if COMPILE_WITH_PROFILER
+ UpdateDumpShadows();
+#endif
return;
}
@@ -1535,6 +1671,10 @@ RETRY_ATLAS_SETUP:
GPUContext* context = GPUDevice::Instance->GetMainContext();
shadows.ShadowsBuffer.Flush(context);
shadows.ShadowsBufferView = shadows.ShadowsBuffer.GetBuffer()->View();
+
+#if COMPILE_WITH_PROFILER
+ UpdateDumpShadows(&shadows);
+#endif
}
void ShadowsPass::RenderShadowMaps(RenderContextBatch& renderContextBatch)
diff --git a/Source/Engine/Utilities/RectPack.h b/Source/Engine/Utilities/RectPack.h
index 7d381317a..af81a168c 100644
--- a/Source/Engine/Utilities/RectPack.h
+++ b/Source/Engine/Utilities/RectPack.h
@@ -107,6 +107,11 @@ public:
return Width != 0;
}
+ int32 Count() const
+ {
+ return Nodes.Count() - FreeNodes.Count();
+ }
+
///
/// Initializes the atlas of a given size. Clears any previously added nodes. This won't invoke OnFree for atlas tiles.
///