Add viewport stuff for caching rendering resources properly.

Content nowadays has a bunch of Overlays that all cache IRenderTextures for various funny operations. These are all broken in the face of multiple viewports, as they need to be cached *per viewport*.

This commit adds an ID field & an event to allow content to properly handle these resources.

Also adds some debug commands
This commit is contained in:
PJB3005
2025-09-07 00:38:55 +02:00
parent 745d0e5532
commit ebe4538d4c
6 changed files with 186 additions and 24 deletions

View File

@@ -41,6 +41,7 @@ END TEMPLATE-->
* `Control.OrderedChildCollection` (gotten from `.Children`) now implements `IReadOnlyList<Control>`, allowing it to be indexed directly.
* `System.WeakReference<T>` is now available in the sandbox.
* `IClydeViewport` now has an `Id` and `ClearCachedResources` event. Together, these allow you to properly cache rendering resources per viewport.
### Bugfixes
@@ -52,7 +53,7 @@ END TEMPLATE-->
### Internal
*None yet*
* Added some debug commands for debugging viewport resource management: `vp_clear_all_cached` & `vp_test_finalize`
## 267.0.0

View File

@@ -0,0 +1,44 @@
#if TOOLS
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Robust.Client.Console.Commands;
internal sealed class ViewportClearAllCachedCommand : IConsoleCommand
{
[Dependency] private readonly IClydeInternal _clyde = default!;
public string Command => "vp_clear_all_cached";
public string Description => "Fires IClydeViewport.ClearCachedResources on all viewports";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
_clyde.ViewportsClearAllCached();
}
}
internal sealed class ViewportTestFinalizeCommand : IConsoleCommand
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
public string Command => "vp_test_finalize";
public string Description => "Creates a viewport, renders it once, then leaks it (finalizes it).";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var vp = _clyde.CreateViewport(new Vector2i(1920, 1080), nameof(ViewportTestFinalizeCommand));
vp.Eye = _eyeManager.CurrentEye;
vp.Render();
// Leak it.
}
}
#endif // TOOLS

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.UserInterface.CustomControls;
@@ -15,10 +16,14 @@ namespace Robust.Client.Graphics.Clyde
private readonly Dictionary<ClydeHandle, WeakReference<Viewport>> _viewports =
new();
private long _nextViewportId = 1;
private readonly ConcurrentQueue<ViewportDisposeData> _viewportDisposeQueue = new();
private Viewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters = default, string? name = null)
{
var handle = AllocRid();
var viewport = new Viewport(handle, name, this)
var viewport = new Viewport(_nextViewportId++, handle, name, this)
{
Size = size,
RenderTarget = CreateRenderTarget(size,
@@ -59,28 +64,43 @@ namespace Robust.Client.Graphics.Clyde
private void FlushViewportDispose()
{
// Free of allocations unless a dead viewport is found.
List<ClydeHandle>? toRemove = null;
foreach (var (handle, viewportRef) in _viewports)
while (_viewportDisposeQueue.TryDequeue(out var data))
{
if (!viewportRef.TryGetTarget(out _))
{
toRemove ??= new List<ClydeHandle>();
toRemove.Add(handle);
}
}
if (toRemove == null)
{
return;
}
foreach (var remove in toRemove)
{
_viewports.Remove(remove);
DisposeViewport(data);
}
}
private void DisposeViewport(ViewportDisposeData disposeData)
{
_clydeSawmill.Warning($"Viewport {disposeData.Id} got leaked");
_viewports.Remove(disposeData.Handle);
if (disposeData.ClearEvent is not { } clearEvent)
return;
try
{
clearEvent(disposeData.ClearEventData);
}
catch (Exception ex)
{
_clydeSawmill.Error($"Caught exception while disposing viewport: {ex}");
}
}
#if TOOLS
public void ViewportsClearAllCached()
{
foreach (var vpRef in _viewports.Values)
{
if (!vpRef.TryGetTarget(out var vp))
continue;
vp.FireClear();
}
}
#endif // TOOLS
private sealed class Viewport : IClydeViewport
{
private readonly ClydeHandle _handle;
@@ -106,17 +126,20 @@ namespace Robust.Client.Graphics.Clyde
public string? Name { get; }
public Viewport(ClydeHandle handle, string? name, Clyde clyde)
public Viewport(long id, ClydeHandle handle, string? name, Clyde clyde)
{
Name = name;
_handle = handle;
_clyde = clyde;
Id = id;
}
public Vector2i Size { get; set; }
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
public Color? ClearColor { get; set; } = Color.Black;
public Vector2 RenderScale { get; set; } = Vector2.One;
public bool AutomaticRender { get; set; }
public long Id { get; }
void IClydeViewport.Render()
{
@@ -186,20 +209,56 @@ namespace Robust.Client.Graphics.Clyde
_clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpace, viewportBounds);
}
~Viewport()
{
_clyde._viewportDisposeQueue.Enqueue(DisposeData(referenceSelf: false));
}
public void Dispose()
{
GC.SuppressFinalize(this);
RenderTarget.Dispose();
LightRenderTarget.Dispose();
WallMaskRenderTarget.Dispose();
WallBleedIntermediateRenderTarget1.Dispose();
WallBleedIntermediateRenderTarget2.Dispose();
_clyde._viewports.Remove(_handle);
_clyde.DisposeViewport(DisposeData(referenceSelf: false));
}
private ViewportDisposeData DisposeData(bool referenceSelf)
{
return new ViewportDisposeData
{
Handle = _handle,
Id = Id,
ClearEvent = ClearCachedResources,
ClearEventData = MakeClearEvent(referenceSelf)
};
}
private ClearCachedViewportResourcesEvent MakeClearEvent(bool referenceSelf)
{
return new ClearCachedViewportResourcesEvent(Id, referenceSelf ? this : null);
}
public void FireClear()
{
ClearCachedResources?.Invoke(MakeClearEvent(referenceSelf: true));
}
IRenderTexture IClydeViewport.RenderTarget => RenderTarget;
IRenderTexture IClydeViewport.LightRenderTarget => LightRenderTarget;
public IEye? Eye { get; set; }
}
private sealed class ViewportDisposeData
{
public ClydeHandle Handle;
public long Id;
public Action<ClearCachedViewportResourcesEvent>? ClearEvent;
public ClearCachedViewportResourcesEvent ClearEventData;
}
}
}

View File

@@ -34,6 +34,7 @@ namespace Robust.Client.Graphics.Clyde
public bool IsFocused => true;
private readonly List<IClydeWindow> _windows = new();
private int _nextWindowId = 2;
private long _nextViewportId = 1;
public ShaderInstance InstanceShader(ShaderSourceResource handle, bool? light = null, ShaderBlendMode? blend = null)
{
@@ -240,7 +241,7 @@ namespace Robust.Client.Graphics.Clyde
public IClydeViewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters,
string? name = null)
{
return new Viewport(size);
return new Viewport(_nextViewportId++, size);
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
@@ -309,6 +310,13 @@ namespace Robust.Client.Graphics.Clyde
public bool VsyncEnabled { get; set; }
#if TOOLS
public void ViewportsClearAllCached()
{
throw new NotImplementedException();
}
#endif // TOOLS
private sealed class DummyCursor : ICursor
{
public void Dispose()
@@ -484,15 +492,19 @@ namespace Robust.Client.Graphics.Clyde
private sealed class Viewport : IClydeViewport
{
public Viewport(Vector2i size)
public Viewport(long id, Vector2i size)
{
Size = size;
Id = id;
}
public void Dispose()
{
ClearCachedResources?.Invoke(new ClearCachedViewportResourcesEvent(Id, null));
}
public long Id { get; }
public IRenderTexture RenderTarget { get; } =
new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One));
@@ -501,6 +513,7 @@ namespace Robust.Client.Graphics.Clyde
public IEye? Eye { get; set; }
public Vector2i Size { get; }
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
public Color? ClearColor { get; set; } = Color.Black;
public Vector2 RenderScale { get; set; }
public bool AutomaticRender { get; set; }

View File

@@ -74,5 +74,16 @@ namespace Robust.Client.Graphics
IFileDialogManagerImplementation? FileDialogImpl { get; }
bool VsyncEnabled { get; set; }
// Viewports
#if TOOLS
/// <summary>
/// Fires <see cref="IClydeViewport.ClearCachedResources"/> on all viewports. For debugging.
/// </summary>
void ViewportsClearAllCached();
#endif // TOOLS
}
}

View File

@@ -13,6 +13,11 @@ namespace Robust.Client.Graphics
/// </summary>
public interface IClydeViewport : IDisposable
{
/// <summary>
/// A unique ID for this viewport. No other viewport with this ID can ever exist in the app lifetime.
/// </summary>
long Id { get; }
/// <summary>
/// The render target that is rendered to when rendering this viewport.
/// </summary>
@@ -22,6 +27,16 @@ namespace Robust.Client.Graphics
IEye? Eye { get; set; }
Vector2i Size { get; }
/// <summary>
/// Raised when the viewport indicates that any cached rendering resources (e.g. render targets)
/// should be purged.
/// </summary>
/// <remarks>
/// This event is raised if the viewport is disposed (manually or via finalization).
/// However, code should expect this event to be raised at any time, even if the viewport is not disposed fully.
/// </remarks>
event Action<ClearCachedViewportResourcesEvent> ClearCachedResources;
/// <summary>
/// Color to clear the render target to before rendering. If null, no clearing will happen.
/// </summary>
@@ -85,4 +100,23 @@ namespace Robust.Client.Graphics
IViewportControl control,
in UIBox2i viewportBounds);
}
public struct ClearCachedViewportResourcesEvent
{
/// <summary>
/// The <see cref="IClydeViewport.Id"/> of the viewport.
/// </summary>
public readonly long ViewportId;
/// <summary>
/// The viewport itself. This is not available if the viewport was disposed.
/// </summary>
public readonly IClydeViewport? Viewport;
internal ClearCachedViewportResourcesEvent(long viewportId, IClydeViewport? viewport)
{
ViewportId = viewportId;
Viewport = viewport;
}
}
}