Files
sbox-public/engine/Sandbox.Engine/Systems/Render/Shadows/ShadowMapper.Debug.cs
Lorenz Junglas 2e8a208d0d Fix shadows vram leak (#4527)
* Add Texture Pool section to shadow debug

https://files.facepunch.com/lolleko/2026/April/13_16-21-UsedRook.png

Shows of orphaned textures (in flight stat keeps growing.

* Return shadow map to pool when light is destroyed

https://files.facepunch.com/lolleko/2026/April/13_16-24-ClutteredCooter.png

* Use Queue instead of Stack for shadow texture pool eviction

We want FIFO semantics instead of LIFO.
Stack (LIFO) buried old textures under newer ones, potentially preventing disposal

* Remove unused OnLightRemoved callback
2026-04-13 18:05:57 +02:00

192 lines
7.8 KiB
C#

namespace Sandbox.Rendering;
internal partial class ShadowMapper
{
internal static void Draw( ref Vector2 pos, HudPainter Hud )
{
var x = pos.x;
var y = pos.y;
var header = new TextRendering.Scope( "", Color.White, 12, "Roboto Mono", 700 );
header.Outline = new TextRendering.Outline { Color = Color.Black, Enabled = true, Size = 2 };
var scope = new TextRendering.Scope( "", Color.White, 11, "Roboto Mono", 600 );
scope.Outline = new TextRendering.Outline { Color = Color.Black, Enabled = true, Size = 2 };
var dimScope = new TextRendering.Scope( "", Color.White.WithAlpha( 0.5f ), 11, "Roboto Mono", 600 );
dimScope.Outline = new TextRendering.Outline { Color = Color.Black, Enabled = true, Size = 2 };
scope.Text = $"Shadow Memory Allocated: {MemorySize.FormatBytes()}";
scope.TextColor = new Color( 0.6f, 0.9f, 1f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
scope.Text = $"Directional Shadow Memory: {DirectionalShadowMemorySize.FormatBytes()}";
scope.TextColor = new Color( 0.6f, 0.9f, 1f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
scope.Text = $"Max Resolution: {ShadowMapper.MaxResolution}";
scope.TextColor = new Color( 0.6f, 0.9f, 1f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
scope.Text = $"Filter Quality: {ShadowMapper.ShadowFilter}";
scope.TextColor = new Color( 0.6f, 0.9f, 1f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
y += 14;
scope.Text = $"Shadow Maps In Cache: {ShadowMapper.Cache.Count()}";
scope.TextColor = new Color( 0.6f, 0.9f, 1f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
scope.Text = $"Shadow Maps Rendered This Frame: {ShadowMapper.Cache.Where( x => x.Value.LastFrame > (RealTime.Now - 0.01f) ).Count()}";
scope.TextColor = new Color( 0.6f, 0.9f, 1f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
scope.Text = $"Shadow Maps Used This Frame: {ShadowMapper.Cache.Where( x => x.Value.LastFrame > (RealTime.Now - 0.01f) ).Count()}";
scope.TextColor = new Color( 0.6f, 0.9f, 1f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
scope.Text = $"Projected Shadows Rendered: {ShadowMapper.ProjectedShadowsRenderedLastFrame}";
scope.TextColor = new Color( 0.6f, 1f, 0.6f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
scope.Text = $"Projected Shadows Screen Size Culled: {ShadowMapper.ProjectedShadowsCulledLastFrame}";
scope.TextColor = new Color( 1f, 0.6f, 0.6f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
y += 14;
// These stats track the health of shadow texture allocation.
int poolTextureCount = 0;
long poolMemorySize = 0;
foreach ( var (poolKey, stack) in ShadowMapper.TexturePool )
{
int count = stack.Count;
poolTextureCount += count;
// D16 = 2 bytes per pixel, cube maps have 6 faces
long bytesPerTexture = (long)poolKey.Resolution * poolKey.Resolution * 2 * (poolKey.IsCube ? 6 : 1);
poolMemorySize += bytesPerTexture * count;
}
header.Text = "Texture Pool";
header.TextColor = new Color( 1f, 0.85f, 0.4f );
Hud.DrawText( header, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
scope.Text = $"Pool Textures: {poolTextureCount} ({poolMemorySize.FormatBytes()})";
scope.TextColor = new Color( 0.6f, 0.9f, 1f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
// Total textures alive = Created - Disposed. These should be in cache, pool, or in-flight.
// If in-flight grows indefinitely, textures are being orphaned.
int cacheCount = ShadowMapper.Cache.Where( x => x.Value.ShadowMap is not null ).Count();
int alive = (int)(ShadowMapper.TotalTexturesCreated - ShadowMapper.TotalTexturesDisposed);
int inFlight = alive - cacheCount - poolTextureCount;
scope.Text = $"Alive: {alive} (cache: {cacheCount}, pool: {poolTextureCount}, in-flight: {inFlight})";
scope.TextColor = new Color( 0.6f, 0.9f, 1f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
y += 14;
header.Text = "Cache";
header.TextColor = new Color( 1f, 0.85f, 0.4f );
Hud.DrawText( header, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
scope.Text = $"In Cache";
scope.TextColor = new Color( 0.6f, 0.9f, 1f );
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
x += 14;
foreach ( var (k, v) in ShadowMapper.Cache.OrderByDescending( x => x.Value.LastFrame ).ThenBy( x => x.Value.ScreenSize ).Take( 20 ) )
{
Color textColor = Color.Yellow;
if ( v.LastFrame < RealTime.Now - 0.1f )
textColor = Color.White.Darken( 0.4f );
scope.Text = $"{k.GetType().Name} - {v.ShadowMap.Size.x}px {g_pRenderDevice.ComputeTextureMemorySize( v.ShadowMap.native ).FormatBytes()} (Screen Size: {v.ScreenSize * 100:F0}%)";
scope.TextColor = textColor;
Hud.DrawText( scope, new Vector2( x, y ), TextFlag.LeftTop );
y += 14;
}
pos.y = y;
// Draw world-space overlays on each local light with shadow info
var camera = Application.GetActiveScene()?.Camera;
if ( camera is not null )
{
const float previewSize = 96;
var labelScope = new TextRendering.Scope( "", Color.White, 10, "Roboto Mono", 600 );
labelScope.Outline = new TextRendering.Outline { Color = Color.Black, Enabled = true, Size = 2 };
foreach ( var (light, entry) in ShadowMapper.Cache )
{
if ( entry.ShadowMap is null ) continue;
if ( !light.IsValid() ) continue;
var screenPos = camera.PointToScreenPixels( light.Position, out bool isBehind );
if ( isBehind ) continue;
// Shadow map preview centered above the light
var texRect = new Rect( screenPos.x - previewSize * 0.5f, screenPos.y - previewSize - 8, previewSize, previewSize );
Hud.DrawTexture( entry.ShadowMap, texRect );
// Info text below the preview
bool active = entry.LastFrame > RealTime.Now - 0.1f;
var textColor = active ? Color.Yellow : Color.White.Darken( 0.4f );
labelScope.TextColor = textColor;
var textPos = new Vector2( screenPos.x, texRect.Bottom + 4 );
string lightType = light.lightNative.GetLightType() switch { 1 => "Point", 3 => "Spot", _ => "Light" };
var memSize = g_pRenderDevice.ComputeTextureMemorySize( entry.ShadowMap.native );
labelScope.Text = $"{lightType} {entry.CurrentResolution}px ({memSize.FormatBytes()})";
Hud.DrawText( labelScope, textPos, TextFlag.CenterTop );
textPos.y += 12;
labelScope.Text = $"R:{light.Radius:F0} Screen:{entry.ScreenSize * 100:F0}% Bias:{light.ShadowBias}";
Hud.DrawText( labelScope, textPos, TextFlag.CenterTop );
textPos.y += 12;
float halfAngle = light.lightNative.GetLightType() == 3 ? light.lightNative.GetPhi() : 45f;
float biasScale = ComputeBiasScale( halfAngle, light.Radius, entry.CurrentResolution );
labelScope.Text = $"BiasScale:{biasScale:F2} Const:{(int)(ShadowDepthBias * biasScale)} Slope:{ShadowSlopeScale * biasScale:F1}";
Hud.DrawText( labelScope, textPos, TextFlag.CenterTop );
}
}
// Draw CSM cascade textures at the bottom of the screen
if ( CascadeDebugCount > 0 )
{
var margin = 8;
var size = 160;
var texY = Screen.Height - size - margin;
for ( int i = 0; i < CascadeDebugCount; i++ )
{
var info = CascadeDebugInfos[i];
if ( info.DepthTexture is null ) continue;
var texX = margin + i * (size + margin);
Hud.DrawTexture( info.DepthTexture, new Rect( texX, texY, size, size ) );
Hud.DrawText( $"Cascade {i}", 11, Color.Yellow, new Vector2( texX, texY - 48 ), TextFlag.LeftTop );
Hud.DrawText( $"Depth: {info.Near:F0} to {info.Far:F0}", 11, Color.Yellow, new Vector2( texX, texY - 32 ), TextFlag.LeftTop );
Hud.DrawText( $"Rect: {info.Width:F0} x {info.Height:F0} units", 11, Color.Yellow, new Vector2( texX, texY - 16 ), TextFlag.LeftTop );
}
}
}
}