]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Fix render target caching in overlays (#40181)
authorPieter-Jan Briers <pieterjan.briers+git@gmail.com>
Sun, 21 Sep 2025 05:16:17 +0000 (07:16 +0200)
committerGitHub <noreply@github.com>
Sun, 21 Sep 2025 05:16:17 +0000 (17:16 +1200)
Many newer overlays use IRenderTextures that are sized to the rendered viewport. This was completely broken, because a single viewport can be rendered on multiple viewports in a single frame.

The end result of this was that in the better case, constant render targets were allocated and freed, which is  extremely inefficient. In the worse case, many of these overlays completely failed to Dispose() their render targets, leading to *extremely* swift VRAM OOMs.

This fixes all the overlays to properly cache resources per viewport. This uses new engine functionality, so it requires engine master.

This is still a pretty lousy way to do GPU resource management but, well, anything better needs a render graph, so...

12 files changed:
Content.Client/Graphics/OverlayResourceCache.cs [new file with mode: 0644]
Content.Client/Light/AfterLightTargetOverlay.cs
Content.Client/Light/AmbientOcclusionOverlay.cs
Content.Client/Light/BeforeLightTargetOverlay.cs
Content.Client/Light/LightBlurOverlay.cs
Content.Client/Light/RoofOverlay.cs
Content.Client/Light/SunShadowOverlay.cs
Content.Client/Light/TileEmissionOverlay.cs
Content.Client/Overlays/StencilOverlay.RestrictedRange.cs
Content.Client/Overlays/StencilOverlay.Weather.cs
Content.Client/Overlays/StencilOverlay.cs
Content.Client/Silicons/StationAi/StationAiOverlay.cs

diff --git a/Content.Client/Graphics/OverlayResourceCache.cs b/Content.Client/Graphics/OverlayResourceCache.cs
new file mode 100644 (file)
index 0000000..ef7ebfd
--- /dev/null
@@ -0,0 +1,90 @@
+using Robust.Client.Graphics;
+
+namespace Content.Client.Graphics;
+
+/// <summary>
+/// A cache for <see cref="Overlay"/>s to store per-viewport render resources, such as render targets.
+/// </summary>
+/// <typeparam name="T">The type of data stored in the cache.</typeparam>
+public sealed class OverlayResourceCache<T> : IDisposable where T : class, IDisposable
+{
+    private readonly Dictionary<long, CacheEntry> _cache = new();
+
+    /// <summary>
+    /// Get the data for a specific viewport, creating a new entry if necessary.
+    /// </summary>
+    /// <remarks>
+    /// The cached data may be cleared at any time if <see cref="IClydeViewport.ClearCachedResources"/> gets invoked.
+    /// </remarks>
+    /// <param name="viewport">The viewport for which to retrieve cached data.</param>
+    /// <param name="factory">A delegate used to create the cached data, if necessary.</param>
+    public T GetForViewport(IClydeViewport viewport, Func<IClydeViewport, T> factory)
+    {
+        return GetForViewport(viewport, out _, factory);
+    }
+
+    /// <summary>
+    /// Get the data for a specific viewport, creating a new entry if necessary.
+    /// </summary>
+    /// <remarks>
+    /// The cached data may be cleared at any time if <see cref="IClydeViewport.ClearCachedResources"/> gets invoked.
+    /// </remarks>
+    /// <param name="viewport">The viewport for which to retrieve cached data.</param>
+    /// <param name="wasCached">True if the data was pulled from cache, false if it was created anew.</param>
+    /// <param name="factory">A delegate used to create the cached data, if necessary.</param>
+    public T GetForViewport(IClydeViewport viewport, out bool wasCached, Func<IClydeViewport, T> factory)
+    {
+        if (_cache.TryGetValue(viewport.Id, out var entry))
+        {
+            wasCached = true;
+            return entry.Data;
+        }
+
+        wasCached = false;
+
+        entry = new CacheEntry
+        {
+            Data = factory(viewport),
+            Viewport = new WeakReference<IClydeViewport>(viewport),
+        };
+        _cache.Add(viewport.Id, entry);
+
+        viewport.ClearCachedResources += ViewportOnClearCachedResources;
+
+        return entry.Data;
+    }
+
+    private void ViewportOnClearCachedResources(ClearCachedViewportResourcesEvent ev)
+    {
+        if (!_cache.Remove(ev.ViewportId, out var entry))
+        {
+            // I think this could theoretically happen if you manually dispose the cache *after* a leaked viewport got
+            // GC'd, but before its ClearCachedResources got invoked.
+            return;
+        }
+
+        entry.Data.Dispose();
+
+        if (ev.Viewport != null)
+            ev.Viewport.ClearCachedResources -= ViewportOnClearCachedResources;
+    }
+
+    public void Dispose()
+    {
+        foreach (var entry in _cache)
+        {
+            if (entry.Value.Viewport.TryGetTarget(out var viewport))
+                viewport.ClearCachedResources -= ViewportOnClearCachedResources;
+
+            entry.Value.Data.Dispose();
+        }
+
+        _cache.Clear();
+    }
+
+    private struct CacheEntry
+    {
+        public T Data;
+        public WeakReference<IClydeViewport> Viewport;
+    }
+}
index 7856fd4ded07ed93a6351adfb57d0de3b4f6f9b0..8f19ce922dcb28616d820c97aff083a512708e30 100644 (file)
@@ -30,6 +30,7 @@ public sealed class AfterLightTargetOverlay : Overlay
             return;
 
         var lightOverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
+        var lightRes = lightOverlay.GetCachedForViewport(args.Viewport);
         var bounds = args.WorldBounds;
 
         // at 1-1 render scale it's mostly fine but at 4x4 it's way too fkn big
@@ -38,7 +39,7 @@ public sealed class AfterLightTargetOverlay : Overlay
 
         var localMatrix =
             viewport.LightRenderTarget.GetWorldToLocalMatrix(viewport.Eye, newScale);
-        var diff = (lightOverlay.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size);
+        var diff = (lightRes.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size);
         var halfDiff = diff / 2;
 
         // Pixels -> Metres -> Half distance.
@@ -53,7 +54,7 @@ public sealed class AfterLightTargetOverlay : Overlay
                     viewport.LightRenderTarget.Size.Y + halfDiff.Y);
 
                 worldHandle.SetTransform(localMatrix);
-                worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
+                worldHandle.DrawTextureRectRegion(lightRes.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
             }, Color.Transparent);
     }
 }
index 4caf65449494a46cced71735d20065c70c617723..aa8c3b52a199b6c535e354bbc8b9219d771e9a3e 100644 (file)
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Client.Graphics;
 using Content.Shared.CCVar;
 using Content.Shared.Maps;
 using Robust.Client.Graphics;
@@ -27,11 +28,7 @@ public sealed class AmbientOcclusionOverlay : Overlay
 
     public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
 
-    private IRenderTexture? _aoTarget;
-    private IRenderTexture? _aoBlurBuffer;
-
-    // Couldn't figure out a way to avoid this so if you can then please do.
-    private IRenderTexture? _aoStencilTarget;
+    private readonly OverlayResourceCache<CachedResources> _resources = new ();
 
     public AmbientOcclusionOverlay()
     {
@@ -69,30 +66,32 @@ public sealed class AmbientOcclusionOverlay : Overlay
         var turfSystem = _entManager.System<TurfSystem>();
         var invMatrix = args.Viewport.GetWorldToLocalMatrix();
 
-        if (_aoTarget?.Texture.Size != target.Size)
+        var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
+        if (res.AOTarget?.Texture.Size != target.Size)
         {
-            _aoTarget?.Dispose();
-            _aoTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target");
+            res.AOTarget?.Dispose();
+            res.AOTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target");
         }
 
-        if (_aoBlurBuffer?.Texture.Size != target.Size)
+        if (res.AOBlurBuffer?.Texture.Size != target.Size)
         {
-            _aoBlurBuffer?.Dispose();
-            _aoBlurBuffer = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target");
+            res.AOBlurBuffer?.Dispose();
+            res.AOBlurBuffer = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target");
         }
 
-        if (_aoStencilTarget?.Texture.Size != target.Size)
+        if (res.AOStencilTarget?.Texture.Size != target.Size)
         {
-            _aoStencilTarget?.Dispose();
-            _aoStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target");
+            res.AOStencilTarget?.Dispose();
+            res.AOStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target");
         }
 
         // Draw the texture data to the texture.
-        args.WorldHandle.RenderInRenderTarget(_aoTarget,
+        args.WorldHandle.RenderInRenderTarget(res.AOTarget,
             () =>
             {
                 worldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
-                var invMatrix = _aoTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
+                var invMatrix = res.AOTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
 
                 foreach (var entry in query.QueryAabb(mapId, worldBounds))
                 {
@@ -106,11 +105,11 @@ public sealed class AmbientOcclusionOverlay : Overlay
                 }
             }, Color.Transparent);
 
-        _clyde.BlurRenderTarget(viewport, _aoTarget, _aoBlurBuffer, viewport.Eye!, 14f);
+        _clyde.BlurRenderTarget(viewport, res.AOTarget, res.AOBlurBuffer, viewport.Eye!, 14f);
 
         // Need to do stencilling after blur as it will nuke it.
         // Draw stencil for the grid so we don't draw in space.
-        args.WorldHandle.RenderInRenderTarget(_aoStencilTarget,
+        args.WorldHandle.RenderInRenderTarget(res.AOStencilTarget,
             () =>
             {
                 // Don't want lighting affecting it.
@@ -136,13 +135,36 @@ public sealed class AmbientOcclusionOverlay : Overlay
 
         // Draw the stencil texture to depth buffer.
         worldHandle.UseShader(_proto.Index(StencilMaskShader).Instance());
-        worldHandle.DrawTextureRect(_aoStencilTarget!.Texture, worldBounds);
+        worldHandle.DrawTextureRect(res.AOStencilTarget!.Texture, worldBounds);
 
         // Draw the Blurred AO texture finally.
         worldHandle.UseShader(_proto.Index(StencilEqualDrawShader).Instance());
-        worldHandle.DrawTextureRect(_aoTarget!.Texture, worldBounds, color);
+        worldHandle.DrawTextureRect(res.AOTarget!.Texture, worldBounds, color);
 
         args.WorldHandle.SetTransform(Matrix3x2.Identity);
         args.WorldHandle.UseShader(null);
     }
+
+    protected override void DisposeBehavior()
+    {
+        _resources.Dispose();
+
+        base.DisposeBehavior();
+    }
+
+    private sealed class CachedResources : IDisposable
+    {
+        public IRenderTexture? AOTarget;
+        public IRenderTexture? AOBlurBuffer;
+
+        // Couldn't figure out a way to avoid this so if you can then please do.
+        public IRenderTexture? AOStencilTarget;
+
+        public void Dispose()
+        {
+            AOTarget?.Dispose();
+            AOBlurBuffer?.Dispose();
+            AOStencilTarget?.Dispose();
+        }
+    }
 }
index 8f1bd0e5276c1ce244486bcb781e33ad7ad46141..6afaebc1465ccc4cc041e13029c62114ee930b04 100644 (file)
@@ -1,4 +1,4 @@
-using System.Numerics;
+using Content.Client.Graphics;
 using Robust.Client.Graphics;
 using Robust.Shared.Enums;
 
@@ -13,7 +13,8 @@ public sealed class BeforeLightTargetOverlay : Overlay
 
     [Dependency] private readonly IClyde _clyde = default!;
 
-    public IRenderTexture EnlargedLightTarget = default!;
+    private readonly OverlayResourceCache<CachedResources> _resources = new();
+
     public Box2Rotated EnlargedBounds;
 
     /// <summary>
@@ -36,16 +37,42 @@ public sealed class BeforeLightTargetOverlay : Overlay
         var size = args.Viewport.LightRenderTarget.Size + (int) (_skirting * EyeManager.PixelsPerMeter);
         EnlargedBounds = args.WorldBounds.Enlarged(_skirting / 2f);
 
+        var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
         // This just exists to copy the lightrendertarget and write back to it.
-        if (EnlargedLightTarget?.Size != size)
+        if (res.EnlargedLightTarget?.Size != size)
         {
-            EnlargedLightTarget = _clyde
+            res.EnlargedLightTarget = _clyde
                 .CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-copy");
         }
 
-        args.WorldHandle.RenderInRenderTarget(EnlargedLightTarget,
+        args.WorldHandle.RenderInRenderTarget(res.EnlargedLightTarget,
             () =>
             {
             }, _clyde.GetClearColor(args.MapUid));
     }
+
+    internal CachedResources GetCachedForViewport(IClydeViewport viewport)
+    {
+        return _resources.GetForViewport(viewport,
+            static _ => throw new InvalidOperationException(
+                "Expected BeforeLightTargetOverlay to have created its resources"));
+    }
+
+    protected override void DisposeBehavior()
+    {
+        _resources.Dispose();
+
+        base.DisposeBehavior();
+    }
+
+    internal sealed class CachedResources : IDisposable
+    {
+        public IRenderTexture EnlargedLightTarget = default!;
+
+        public void Dispose()
+        {
+            EnlargedLightTarget?.Dispose();
+        }
+    }
 }
index 4ce80946aa5aa782a3d19e4d7ced6d815aead801..eab4a95c07faf7928fa7e85b393752000366d2d4 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Client.Graphics;
 using Robust.Client.Graphics;
 using Robust.Shared.Enums;
 
@@ -15,7 +16,7 @@ public sealed class LightBlurOverlay : Overlay
 
     public const int ContentZIndex = TileEmissionOverlay.ContentZIndex + 1;
 
-    private IRenderTarget? _blurTarget;
+    private readonly OverlayResourceCache<CachedResources> _resources = new();
 
     public LightBlurOverlay()
     {
@@ -29,16 +30,36 @@ public sealed class LightBlurOverlay : Overlay
             return;
 
         var beforeOverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
-        var size = beforeOverlay.EnlargedLightTarget.Size;
+        var beforeLightRes = beforeOverlay.GetCachedForViewport(args.Viewport);
+        var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
 
-        if (_blurTarget?.Size != size)
+        var size = beforeLightRes.EnlargedLightTarget.Size;
+
+        if (res.BlurTarget?.Size != size)
         {
-            _blurTarget = _clyde
+            res.BlurTarget = _clyde
                 .CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-blur");
         }
 
-        var target = beforeOverlay.EnlargedLightTarget;
+        var target = beforeLightRes.EnlargedLightTarget;
         // Yeah that's all this does keep walkin.
-        _clyde.BlurRenderTarget(args.Viewport, target, _blurTarget, args.Viewport.Eye, 14f * 5f);
+        _clyde.BlurRenderTarget(args.Viewport, target, res.BlurTarget, args.Viewport.Eye, 14f * 5f);
+    }
+
+    protected override void DisposeBehavior()
+    {
+        _resources.Dispose();
+
+        base.DisposeBehavior();
+    }
+
+    private sealed class CachedResources : IDisposable
+    {
+        public IRenderTarget? BlurTarget;
+
+        public void Dispose()
+        {
+            BlurTarget?.Dispose();
+        }
     }
 }
index 9be4bfe4c4d961d58c796b89d3186f28354b7f1f..01e9bf09616a34fe6ff4183f7281c542d7061510 100644 (file)
@@ -51,8 +51,9 @@ public sealed class RoofOverlay : Overlay
 
         var worldHandle = args.WorldHandle;
         var lightoverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
+        var lightRes = lightoverlay.GetCachedForViewport(args.Viewport);
         var bounds = lightoverlay.EnlargedBounds;
-        var target = lightoverlay.EnlargedLightTarget;
+        var target = lightRes.EnlargedLightTarget;
 
         _grids.Clear();
         _mapManager.FindGridsIntersecting(args.MapId, bounds, ref _grids, approx: true, includeMap: true);
index f30f4c0409bf3c7646b51e5ce970607cde78da28..59ac0a5efb494dff30034d9673a2f55679b32d2e 100644 (file)
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Client.Graphics;
 using Content.Shared.Light.Components;
 using Robust.Client.Graphics;
 using Robust.Shared.Enums;
@@ -24,8 +25,7 @@ public sealed class SunShadowOverlay : Overlay
 
     private readonly HashSet<Entity<SunShadowCastComponent>> _shadows = new();
 
-    private IRenderTexture? _blurTarget;
-    private IRenderTexture? _target;
+    private readonly OverlayResourceCache<CachedResources> _resources = new();
 
     public SunShadowOverlay()
     {
@@ -55,16 +55,18 @@ public sealed class SunShadowOverlay : Overlay
         var worldBounds = args.WorldBounds;
         var targetSize = viewport.LightRenderTarget.Size;
 
-        if (_target?.Size != targetSize)
+        var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
+        if (res.Target?.Size != targetSize)
         {
-            _target = _clyde
+            res.Target = _clyde
                 .CreateRenderTarget(targetSize,
                     new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
                     name: "sun-shadow-target");
 
-            if (_blurTarget?.Size != targetSize)
+            if (res.BlurTarget?.Size != targetSize)
             {
-                _blurTarget = _clyde
+                res.BlurTarget = _clyde
                     .CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
             }
         }
@@ -93,11 +95,11 @@ public sealed class SunShadowOverlay : Overlay
             _shadows.Clear();
 
             // Draw shadow polys to stencil
-            args.WorldHandle.RenderInRenderTarget(_target,
+            args.WorldHandle.RenderInRenderTarget(res.Target,
                 () =>
                 {
                     var invMatrix =
-                        _target.GetWorldToLocalMatrix(eye, scale);
+                        res.Target.GetWorldToLocalMatrix(eye, scale);
                     var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
 
                     // Go through shadows in range.
@@ -142,7 +144,7 @@ public sealed class SunShadowOverlay : Overlay
                 Color.Transparent);
 
             // Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
-            _clyde.BlurRenderTarget(viewport, _target, _blurTarget!, eye, 1f);
+            _clyde.BlurRenderTarget(viewport, res.Target, res.BlurTarget!, eye, 1f);
 
             // Draw stencil (see roofoverlay).
             args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
@@ -155,8 +157,27 @@ public sealed class SunShadowOverlay : Overlay
                     var maskShader = _protoManager.Index(MixShader).Instance();
                     worldHandle.UseShader(maskShader);
 
-                    worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
+                    worldHandle.DrawTextureRect(res.Target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
                 }, null);
         }
     }
+
+    protected override void DisposeBehavior()
+    {
+        _resources.Dispose();
+
+        base.DisposeBehavior();
+    }
+
+    private sealed class CachedResources : IDisposable
+    {
+        public IRenderTexture? BlurTarget;
+        public IRenderTexture? Target;
+
+        public void Dispose()
+        {
+            BlurTarget?.Dispose();
+            Target?.Dispose();
+        }
+    }
 }
index 2f4a1390ff6368437234806661cb0a6f81294506..2acb0ee6092932ca3fb548bbfad7c48a24eac0b6 100644 (file)
@@ -47,7 +47,7 @@ public sealed class TileEmissionOverlay : Overlay
         var worldHandle = args.WorldHandle;
         var lightoverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
         var bounds = lightoverlay.EnlargedBounds;
-        var target = lightoverlay.EnlargedLightTarget;
+        var target = lightoverlay.GetCachedForViewport(args.Viewport).EnlargedLightTarget;
         var viewport = args.Viewport;
         _grids.Clear();
         _mapManager.FindGridsIntersecting(mapId, bounds, ref _grids, approx: true);
index a5efacc16c43227e9c5f5df207a6475f9687e018..7218e16da1729f732aaf13991056240cf30bbd9f 100644 (file)
@@ -7,7 +7,11 @@ namespace Content.Client.Overlays;
 
 public sealed partial class StencilOverlay
 {
-    private void DrawRestrictedRange(in OverlayDrawArgs args, RestrictedRangeComponent rangeComp, Matrix3x2 invMatrix)
+    private void DrawRestrictedRange(
+        in OverlayDrawArgs args,
+        CachedResources res,
+        RestrictedRangeComponent rangeComp,
+        Matrix3x2 invMatrix)
     {
         var worldHandle = args.WorldHandle;
         var renderScale = args.Viewport.RenderScale.X;
@@ -38,7 +42,7 @@ public sealed partial class StencilOverlay
         // Cut out the irrelevant bits via stencil
         // This is why we don't just use parallax; we might want specific tiles to get drawn over
         // particularly for planet maps or stations.
-        worldHandle.RenderInRenderTarget(_blep!, () =>
+        worldHandle.RenderInRenderTarget(res.Blep!, () =>
         {
             worldHandle.UseShader(_shader);
             worldHandle.DrawRect(localAABB, Color.White);
@@ -46,7 +50,7 @@ public sealed partial class StencilOverlay
 
         worldHandle.SetTransform(Matrix3x2.Identity);
         worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
-        worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
+        worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
         var curTime = _timing.RealTime;
         var sprite = _sprite.GetFrame(new SpriteSpecifier.Texture(new ResPath("/Textures/Parallaxes/noise.png")), curTime);
 
index 509b946ad41d51ce937f9fa1c43686202d968936..66a6a799a7609d0849dd1c63c6b5876eec3ef2f1 100644 (file)
@@ -11,7 +11,12 @@ public sealed partial class StencilOverlay
 {
     private List<Entity<MapGridComponent>> _grids = new();
 
-    private void DrawWeather(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha, Matrix3x2 invMatrix)
+    private void DrawWeather(
+        in OverlayDrawArgs args,
+        CachedResources res,
+        WeatherPrototype weatherProto,
+        float alpha,
+        Matrix3x2 invMatrix)
     {
         var worldHandle = args.WorldHandle;
         var mapId = args.MapId;
@@ -22,7 +27,7 @@ public sealed partial class StencilOverlay
         // Cut out the irrelevant bits via stencil
         // This is why we don't just use parallax; we might want specific tiles to get drawn over
         // particularly for planet maps or stations.
-        worldHandle.RenderInRenderTarget(_blep!, () =>
+        worldHandle.RenderInRenderTarget(res.Blep!, () =>
         {
             var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
             _grids.Clear();
@@ -56,7 +61,7 @@ public sealed partial class StencilOverlay
 
         worldHandle.SetTransform(Matrix3x2.Identity);
         worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
-        worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
+        worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
         var curTime = _timing.RealTime;
         var sprite = _sprite.GetFrame(weatherProto.Sprite, curTime);
 
index 55cb1811a51359bf4a52046a58e6bb3d7b8c6cc7..276181468bc7975f6ce72e505a6920a8b93ce912 100644 (file)
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Client.Graphics;
 using Content.Client.Parallax;
 using Content.Client.Weather;
 using Content.Shared.Salvage;
@@ -34,7 +35,7 @@ public sealed partial class StencilOverlay : Overlay
 
     public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
 
-    private IRenderTexture? _blep;
+    private readonly OverlayResourceCache<CachedResources> _resources = new();
 
     private readonly ShaderInstance _shader;
 
@@ -55,10 +56,12 @@ public sealed partial class StencilOverlay : Overlay
         var mapUid = _map.GetMapOrInvalid(args.MapId);
         var invMatrix = args.Viewport.GetWorldToLocalMatrix();
 
-        if (_blep?.Texture.Size != args.Viewport.Size)
+        var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
+        if (res.Blep?.Texture.Size != args.Viewport.Size)
         {
-            _blep?.Dispose();
-            _blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
+            res.Blep?.Dispose();
+            res.Blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
         }
 
         if (_entManager.TryGetComponent<WeatherComponent>(mapUid, out var comp))
@@ -69,16 +72,33 @@ public sealed partial class StencilOverlay : Overlay
                     continue;
 
                 var alpha = _weather.GetPercent(weather, mapUid);
-                DrawWeather(args, weatherProto, alpha, invMatrix);
+                DrawWeather(args, res, weatherProto, alpha, invMatrix);
             }
         }
 
         if (_entManager.TryGetComponent<RestrictedRangeComponent>(mapUid, out var restrictedRangeComponent))
         {
-            DrawRestrictedRange(args, restrictedRangeComponent, invMatrix);
+            DrawRestrictedRange(args, res, restrictedRangeComponent, invMatrix);
         }
 
         args.WorldHandle.UseShader(null);
         args.WorldHandle.SetTransform(Matrix3x2.Identity);
     }
+
+    protected override void DisposeBehavior()
+    {
+        _resources.Dispose();
+
+        base.DisposeBehavior();
+    }
+
+    private sealed class CachedResources : IDisposable
+    {
+        public IRenderTexture? Blep;
+
+        public void Dispose()
+        {
+            Blep?.Dispose();
+        }
+    }
 }
index 5c84ce0c9389ce52affc217b9d092c8b0678fdd2..76577447025e72547b69529234b780d313e7fe90 100644 (file)
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Client.Graphics;
 using Content.Shared.Silicons.StationAi;
 using Robust.Client.Graphics;
 using Robust.Client.Player;
@@ -26,8 +27,7 @@ public sealed class StationAiOverlay : Overlay
 
     private readonly HashSet<Vector2i> _visibleTiles = new();
 
-    private IRenderTexture? _staticTexture;
-    private IRenderTexture? _stencilTexture;
+    private readonly OverlayResourceCache<CachedResources> _resources = new();
 
     private float _updateRate = 1f / 30f;
     private float _accumulator;
@@ -39,12 +39,14 @@ public sealed class StationAiOverlay : Overlay
 
     protected override void Draw(in OverlayDrawArgs args)
     {
-        if (_stencilTexture?.Texture.Size != args.Viewport.Size)
+        var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
+
+        if (res.StencilTexture?.Texture.Size != args.Viewport.Size)
         {
-            _staticTexture?.Dispose();
-            _stencilTexture?.Dispose();
-            _stencilTexture = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "station-ai-stencil");
-            _staticTexture = _clyde.CreateRenderTarget(args.Viewport.Size,
+            res.StaticTexture?.Dispose();
+            res.StencilTexture?.Dispose();
+            res.StencilTexture = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "station-ai-stencil");
+            res.StaticTexture = _clyde.CreateRenderTarget(args.Viewport.Size,
                 new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
                 name: "station-ai-static");
         }
@@ -78,7 +80,7 @@ public sealed class StationAiOverlay : Overlay
             var matty =  Matrix3x2.Multiply(gridMatrix, invMatrix);
 
             // Draw visible tiles to stencil
-            worldHandle.RenderInRenderTarget(_stencilTexture!, () =>
+            worldHandle.RenderInRenderTarget(res.StencilTexture!, () =>
             {
                 worldHandle.SetTransform(matty);
 
@@ -91,7 +93,7 @@ public sealed class StationAiOverlay : Overlay
             Color.Transparent);
 
             // Once this is gucci optimise rendering.
-            worldHandle.RenderInRenderTarget(_staticTexture!,
+            worldHandle.RenderInRenderTarget(res.StaticTexture!,
             () =>
             {
                 worldHandle.SetTransform(invMatrix);
@@ -104,12 +106,12 @@ public sealed class StationAiOverlay : Overlay
         // Not on a grid
         else
         {
-            worldHandle.RenderInRenderTarget(_stencilTexture!, () =>
+            worldHandle.RenderInRenderTarget(res.StencilTexture!, () =>
             {
             },
             Color.Transparent);
 
-            worldHandle.RenderInRenderTarget(_staticTexture!,
+            worldHandle.RenderInRenderTarget(res.StaticTexture!,
             () =>
             {
                 worldHandle.SetTransform(Matrix3x2.Identity);
@@ -119,14 +121,33 @@ public sealed class StationAiOverlay : Overlay
 
         // Use the lighting as a mask
         worldHandle.UseShader(_proto.Index(StencilMaskShader).Instance());
-        worldHandle.DrawTextureRect(_stencilTexture!.Texture, worldBounds);
+        worldHandle.DrawTextureRect(res.StencilTexture!.Texture, worldBounds);
 
         // Draw the static
         worldHandle.UseShader(_proto.Index(StencilDrawShader).Instance());
-        worldHandle.DrawTextureRect(_staticTexture!.Texture, worldBounds);
+        worldHandle.DrawTextureRect(res.StaticTexture!.Texture, worldBounds);
 
         worldHandle.SetTransform(Matrix3x2.Identity);
         worldHandle.UseShader(null);
 
     }
+
+    protected override void DisposeBehavior()
+    {
+        _resources.Dispose();
+
+        base.DisposeBehavior();
+    }
+
+    private sealed class CachedResources : IDisposable
+    {
+        public IRenderTexture? StaticTexture;
+        public IRenderTexture? StencilTexture;
+
+        public void Dispose()
+        {
+            StaticTexture?.Dispose();
+            StencilTexture?.Dispose();
+        }
+    }
 }