Skip to content

Commit ebe4538

Browse files
committed
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
1 parent 745d0e5 commit ebe4538

6 files changed

Lines changed: 180 additions & 18 deletions

File tree

RELEASE-NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ END TEMPLATE-->
4141

4242
* `Control.OrderedChildCollection` (gotten from `.Children`) now implements `IReadOnlyList<Control>`, allowing it to be indexed directly.
4343
* `System.WeakReference<T>` is now available in the sandbox.
44+
* `IClydeViewport` now has an `Id` and `ClearCachedResources` event. Together, these allow you to properly cache rendering resources per viewport.
4445

4546
### Bugfixes
4647

@@ -52,7 +53,7 @@ END TEMPLATE-->
5253

5354
### Internal
5455

55-
*None yet*
56+
* Added some debug commands for debugging viewport resource management: `vp_clear_all_cached` & `vp_test_finalize`
5657

5758

5859
## 267.0.0
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#if TOOLS
2+
3+
using Robust.Client.Graphics;
4+
using Robust.Shared.Console;
5+
using Robust.Shared.IoC;
6+
using Robust.Shared.Maths;
7+
8+
namespace Robust.Client.Console.Commands;
9+
10+
internal sealed class ViewportClearAllCachedCommand : IConsoleCommand
11+
{
12+
[Dependency] private readonly IClydeInternal _clyde = default!;
13+
14+
public string Command => "vp_clear_all_cached";
15+
public string Description => "Fires IClydeViewport.ClearCachedResources on all viewports";
16+
public string Help => "";
17+
18+
public void Execute(IConsoleShell shell, string argStr, string[] args)
19+
{
20+
_clyde.ViewportsClearAllCached();
21+
}
22+
}
23+
24+
internal sealed class ViewportTestFinalizeCommand : IConsoleCommand
25+
{
26+
[Dependency] private readonly IClyde _clyde = default!;
27+
[Dependency] private readonly IEyeManager _eyeManager = default!;
28+
29+
public string Command => "vp_test_finalize";
30+
public string Description => "Creates a viewport, renders it once, then leaks it (finalizes it).";
31+
public string Help => "";
32+
33+
public void Execute(IConsoleShell shell, string argStr, string[] args)
34+
{
35+
var vp = _clyde.CreateViewport(new Vector2i(1920, 1080), nameof(ViewportTestFinalizeCommand));
36+
vp.Eye = _eyeManager.CurrentEye;
37+
38+
vp.Render();
39+
40+
// Leak it.
41+
}
42+
}
43+
44+
#endif // TOOLS

Robust.Client/Graphics/Clyde/Clyde.Viewport.cs

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.Numerics;
45
using Robust.Client.UserInterface.CustomControls;
@@ -15,10 +16,14 @@ internal sealed partial class Clyde
1516
private readonly Dictionary<ClydeHandle, WeakReference<Viewport>> _viewports =
1617
new();
1718

19+
private long _nextViewportId = 1;
20+
21+
private readonly ConcurrentQueue<ViewportDisposeData> _viewportDisposeQueue = new();
22+
1823
private Viewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters = default, string? name = null)
1924
{
2025
var handle = AllocRid();
21-
var viewport = new Viewport(handle, name, this)
26+
var viewport = new Viewport(_nextViewportId++, handle, name, this)
2227
{
2328
Size = size,
2429
RenderTarget = CreateRenderTarget(size,
@@ -59,27 +64,42 @@ private static Vector2 ScreenToMap(Vector2 point, Viewport vp)
5964

6065
private void FlushViewportDispose()
6166
{
62-
// Free of allocations unless a dead viewport is found.
63-
List<ClydeHandle>? toRemove = null;
64-
foreach (var (handle, viewportRef) in _viewports)
67+
while (_viewportDisposeQueue.TryDequeue(out var data))
6568
{
66-
if (!viewportRef.TryGetTarget(out _))
67-
{
68-
toRemove ??= new List<ClydeHandle>();
69-
toRemove.Add(handle);
70-
}
69+
DisposeViewport(data);
7170
}
71+
}
7272

73-
if (toRemove == null)
74-
{
73+
private void DisposeViewport(ViewportDisposeData disposeData)
74+
{
75+
_clydeSawmill.Warning($"Viewport {disposeData.Id} got leaked");
76+
77+
_viewports.Remove(disposeData.Handle);
78+
if (disposeData.ClearEvent is not { } clearEvent)
7579
return;
80+
81+
try
82+
{
83+
clearEvent(disposeData.ClearEventData);
84+
}
85+
catch (Exception ex)
86+
{
87+
_clydeSawmill.Error($"Caught exception while disposing viewport: {ex}");
7688
}
89+
}
7790

78-
foreach (var remove in toRemove)
91+
#if TOOLS
92+
public void ViewportsClearAllCached()
93+
{
94+
foreach (var vpRef in _viewports.Values)
7995
{
80-
_viewports.Remove(remove);
96+
if (!vpRef.TryGetTarget(out var vp))
97+
continue;
98+
99+
vp.FireClear();
81100
}
82101
}
102+
#endif // TOOLS
83103

84104
private sealed class Viewport : IClydeViewport
85105
{
@@ -106,17 +126,20 @@ private sealed class Viewport : IClydeViewport
106126

107127
public string? Name { get; }
108128

109-
public Viewport(ClydeHandle handle, string? name, Clyde clyde)
129+
public Viewport(long id, ClydeHandle handle, string? name, Clyde clyde)
110130
{
111131
Name = name;
112132
_handle = handle;
113133
_clyde = clyde;
134+
Id = id;
114135
}
115136

116137
public Vector2i Size { get; set; }
138+
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
117139
public Color? ClearColor { get; set; } = Color.Black;
118140
public Vector2 RenderScale { get; set; } = Vector2.One;
119141
public bool AutomaticRender { get; set; }
142+
public long Id { get; }
120143

121144
void IClydeViewport.Render()
122145
{
@@ -186,20 +209,56 @@ public void RenderScreenOverlaysAbove(
186209
_clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpace, viewportBounds);
187210
}
188211

212+
~Viewport()
213+
{
214+
_clyde._viewportDisposeQueue.Enqueue(DisposeData(referenceSelf: false));
215+
}
216+
189217
public void Dispose()
190218
{
219+
GC.SuppressFinalize(this);
220+
191221
RenderTarget.Dispose();
192222
LightRenderTarget.Dispose();
193223
WallMaskRenderTarget.Dispose();
194224
WallBleedIntermediateRenderTarget1.Dispose();
195225
WallBleedIntermediateRenderTarget2.Dispose();
196226

197-
_clyde._viewports.Remove(_handle);
227+
_clyde.DisposeViewport(DisposeData(referenceSelf: false));
228+
}
229+
230+
private ViewportDisposeData DisposeData(bool referenceSelf)
231+
{
232+
return new ViewportDisposeData
233+
{
234+
Handle = _handle,
235+
Id = Id,
236+
ClearEvent = ClearCachedResources,
237+
ClearEventData = MakeClearEvent(referenceSelf)
238+
};
239+
}
240+
241+
private ClearCachedViewportResourcesEvent MakeClearEvent(bool referenceSelf)
242+
{
243+
return new ClearCachedViewportResourcesEvent(Id, referenceSelf ? this : null);
244+
}
245+
246+
public void FireClear()
247+
{
248+
ClearCachedResources?.Invoke(MakeClearEvent(referenceSelf: true));
198249
}
199250

200251
IRenderTexture IClydeViewport.RenderTarget => RenderTarget;
201252
IRenderTexture IClydeViewport.LightRenderTarget => LightRenderTarget;
202253
public IEye? Eye { get; set; }
203254
}
255+
256+
private sealed class ViewportDisposeData
257+
{
258+
public ClydeHandle Handle;
259+
public long Id;
260+
public Action<ClearCachedViewportResourcesEvent>? ClearEvent;
261+
public ClearCachedViewportResourcesEvent ClearEventData;
262+
}
204263
}
205264
}

Robust.Client/Graphics/Clyde/ClydeHeadless.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ internal sealed class ClydeHeadless : IClydeInternal
3434
public bool IsFocused => true;
3535
private readonly List<IClydeWindow> _windows = new();
3636
private int _nextWindowId = 2;
37+
private long _nextViewportId = 1;
3738

3839
public ShaderInstance InstanceShader(ShaderSourceResource handle, bool? light = null, ShaderBlendMode? blend = null)
3940
{
@@ -240,7 +241,7 @@ public void Screenshot(ScreenshotType type, CopyPixelsDelegate<Rgb24> callback,
240241
public IClydeViewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters,
241242
string? name = null)
242243
{
243-
return new Viewport(size);
244+
return new Viewport(_nextViewportId++, size);
244245
}
245246

246247
public IEnumerable<IClydeMonitor> EnumerateMonitors()
@@ -309,6 +310,13 @@ public void RunOnWindowThread(Action action)
309310

310311
public bool VsyncEnabled { get; set; }
311312

313+
#if TOOLS
314+
public void ViewportsClearAllCached()
315+
{
316+
throw new NotImplementedException();
317+
}
318+
#endif // TOOLS
319+
312320
private sealed class DummyCursor : ICursor
313321
{
314322
public void Dispose()
@@ -484,15 +492,19 @@ private sealed class DummyDebugInfo : IClydeDebugInfo
484492

485493
private sealed class Viewport : IClydeViewport
486494
{
487-
public Viewport(Vector2i size)
495+
public Viewport(long id, Vector2i size)
488496
{
489497
Size = size;
498+
Id = id;
490499
}
491500

492501
public void Dispose()
493502
{
503+
ClearCachedResources?.Invoke(new ClearCachedViewportResourcesEvent(Id, null));
494504
}
495505

506+
public long Id { get; }
507+
496508
public IRenderTexture RenderTarget { get; } =
497509
new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One));
498510

@@ -501,6 +513,7 @@ public void Dispose()
501513

502514
public IEye? Eye { get; set; }
503515
public Vector2i Size { get; }
516+
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
504517
public Color? ClearColor { get; set; } = Color.Black;
505518
public Vector2 RenderScale { get; set; }
506519
public bool AutomaticRender { get; set; }

Robust.Client/Graphics/IClydeInternal.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,16 @@ internal interface IClydeInternal : IClyde, IClipboardManager
7474
IFileDialogManagerImplementation? FileDialogImpl { get; }
7575

7676
bool VsyncEnabled { get; set; }
77+
78+
// Viewports
79+
80+
#if TOOLS
81+
82+
/// <summary>
83+
/// Fires <see cref="IClydeViewport.ClearCachedResources"/> on all viewports. For debugging.
84+
/// </summary>
85+
void ViewportsClearAllCached();
86+
87+
#endif // TOOLS
7788
}
7889
}

Robust.Client/Graphics/IClydeViewport.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ namespace Robust.Client.Graphics
1313
/// </summary>
1414
public interface IClydeViewport : IDisposable
1515
{
16+
/// <summary>
17+
/// A unique ID for this viewport. No other viewport with this ID can ever exist in the app lifetime.
18+
/// </summary>
19+
long Id { get; }
20+
1621
/// <summary>
1722
/// The render target that is rendered to when rendering this viewport.
1823
/// </summary>
@@ -22,6 +27,16 @@ public interface IClydeViewport : IDisposable
2227
IEye? Eye { get; set; }
2328
Vector2i Size { get; }
2429

30+
/// <summary>
31+
/// Raised when the viewport indicates that any cached rendering resources (e.g. render targets)
32+
/// should be purged.
33+
/// </summary>
34+
/// <remarks>
35+
/// This event is raised if the viewport is disposed (manually or via finalization).
36+
/// However, code should expect this event to be raised at any time, even if the viewport is not disposed fully.
37+
/// </remarks>
38+
event Action<ClearCachedViewportResourcesEvent> ClearCachedResources;
39+
2540
/// <summary>
2641
/// Color to clear the render target to before rendering. If null, no clearing will happen.
2742
/// </summary>
@@ -85,4 +100,23 @@ public void RenderScreenOverlaysAbove(
85100
IViewportControl control,
86101
in UIBox2i viewportBounds);
87102
}
103+
104+
public struct ClearCachedViewportResourcesEvent
105+
{
106+
/// <summary>
107+
/// The <see cref="IClydeViewport.Id"/> of the viewport.
108+
/// </summary>
109+
public readonly long ViewportId;
110+
111+
/// <summary>
112+
/// The viewport itself. This is not available if the viewport was disposed.
113+
/// </summary>
114+
public readonly IClydeViewport? Viewport;
115+
116+
internal ClearCachedViewportResourcesEvent(long viewportId, IClydeViewport? viewport)
117+
{
118+
ViewportId = viewportId;
119+
Viewport = viewport;
120+
}
121+
}
88122
}

0 commit comments

Comments
 (0)