Add Graphics.Shadows.Dump command for profiling shadow mapping

This commit is contained in:
2026-06-25 19:02:35 +02:00
parent 5c5641efc2
commit 1385d5282f
3 changed files with 152 additions and 0 deletions
+7
View File
@@ -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
/// <summary>
/// Dumps active shadow projections info to the log (the next frame). Can be used to inspect what lights are casting shadows (for optimization).
/// </summary>
API_FUNCTION(Attributes="DebugCommand") static void Dump();
#endif
};
// Motion Vectors rendering configuration.
+140
View File
@@ -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<Light>(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)
+5
View File
@@ -107,6 +107,11 @@ public:
return Width != 0;
}
int32 Count() const
{
return Nodes.Count() - FreeNodes.Count();
}
/// <summary>
/// Initializes the atlas of a given size. Clears any previously added nodes. This won't invoke OnFree for atlas tiles.
/// </summary>