Skip to content

Commit 63dbd46

Browse files
Add ReleaseFrameResources and WebGPU CPU cache key
1 parent ea688af commit 63dbd46

8 files changed

Lines changed: 120 additions & 11 deletions

File tree

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,16 @@ public void FlushCompositions<TPixel>(
300300
compositionBounds);
301301
}
302302

303+
/// <inheritdoc />
304+
public void ReleaseFrameResources<TPixel>(
305+
Configuration configuration,
306+
ICanvasFrame<TPixel> target)
307+
where TPixel : unmanaged, IPixel<TPixel>
308+
{
309+
nint targetIdentity = (nint)RuntimeHelpers.GetHashCode(target);
310+
WebGPUFlushContext.ReleaseCpuTargetEntries(targetIdentity);
311+
}
312+
303313
/// <summary>
304314
/// Checks whether all scene commands are directly composable by WebGPU.
305315
/// </summary>

src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ private WebGPUFlushContext(
237237
}
238238

239239
context = new WebGPUFlushContext(lease, device, queue, in bounds, expectedTextureFormat, memoryAllocator, deviceState);
240-
context.InitializeCpuTarget(cpuRegion, pixelSizeInBytes, initialUploadBounds);
240+
nint targetIdentity = (nint)RuntimeHelpers.GetHashCode(frame);
241+
context.InitializeCpuTarget(cpuRegion, pixelSizeInBytes, targetIdentity, initialUploadBounds);
241242
return context;
242243
}
243244
catch
@@ -308,6 +309,18 @@ public static void ClearFallbackStagingCache()
308309
FallbackStagingCache.Clear();
309310
}
310311

312+
/// <summary>
313+
/// Releases all cached CPU target resources associated with the specified target identity.
314+
/// </summary>
315+
/// <param name="targetIdentity">The target frame identity whose cached resources should be released.</param>
316+
public static void ReleaseCpuTargetEntries(nint targetIdentity)
317+
{
318+
foreach (DeviceSharedState state in DeviceStateCache.Values)
319+
{
320+
state.ReleaseCpuTargetEntries(targetIdentity);
321+
}
322+
}
323+
311324
/// <summary>
312325
/// Clears all cached device-scoped shared state.
313326
/// </summary>
@@ -621,6 +634,7 @@ private void InitializeNativeTarget(WebGPUSurfaceCapability capability)
621634
private void InitializeCpuTarget<TPixel>(
622635
Buffer2DRegion<TPixel> cpuRegion,
623636
int pixelSizeInBytes,
637+
nint targetIdentity,
624638
Rectangle? initialUploadBounds)
625639
where TPixel : unmanaged
626640
{
@@ -630,7 +644,8 @@ private void InitializeCpuTarget<TPixel>(
630644
this.TextureFormat,
631645
width,
632646
height,
633-
pixelSizeInBytes);
647+
pixelSizeInBytes,
648+
targetIdentity);
634649
Texture* targetTexture = lease.TargetTexture;
635650
TextureView* targetView = lease.TargetView;
636651
WgpuBuffer* readbackBuffer = lease.ReadbackBuffer;
@@ -894,18 +909,36 @@ private static HashSet<FeatureName> EnumerateDeviceFeatures(WebGPU api, Device*
894909
/// <param name="width">The destination width.</param>
895910
/// <param name="height">The destination height.</param>
896911
/// <param name="pixelSizeInBytes">The destination pixel size in bytes.</param>
912+
/// <param name="targetIdentity">Identity of the target frame to prevent cache collisions.</param>
897913
/// <returns>A lease for staging resources.</returns>
898914
public CpuTargetLease RentCpuTarget(
899915
TextureFormat textureFormat,
900916
int width,
901917
int height,
902-
int pixelSizeInBytes)
918+
int pixelSizeInBytes,
919+
nint targetIdentity)
903920
{
904-
CpuTargetCacheKey key = new(textureFormat, width, height, pixelSizeInBytes);
921+
CpuTargetCacheKey key = new(textureFormat, width, height, pixelSizeInBytes, targetIdentity);
905922
CpuTargetEntry entry = this.cpuTargetCache.GetOrAdd(key, static _ => new CpuTargetEntry());
906923
return entry.Rent(this.Api, this.Device, in key);
907924
}
908925

926+
/// <summary>
927+
/// Releases and removes all CPU target cache entries matching the specified target identity.
928+
/// </summary>
929+
/// <param name="targetIdentity">The target frame identity whose entries should be released.</param>
930+
public void ReleaseCpuTargetEntries(nint targetIdentity)
931+
{
932+
foreach (KeyValuePair<CpuTargetCacheKey, CpuTargetEntry> pair in this.cpuTargetCache)
933+
{
934+
if (pair.Key.TargetIdentity == targetIdentity &&
935+
this.cpuTargetCache.TryRemove(pair.Key, out CpuTargetEntry? entry))
936+
{
937+
entry.Dispose(this.Api);
938+
}
939+
}
940+
}
941+
909942
/// <summary>
910943
/// Gets or creates a graphics pipeline used for composite rendering.
911944
/// </summary>
@@ -1438,7 +1471,8 @@ internal readonly struct CpuTargetCacheKey(
14381471
TextureFormat textureFormat,
14391472
int width,
14401473
int height,
1441-
int pixelSizeInBytes) : IEquatable<CpuTargetCacheKey>
1474+
int pixelSizeInBytes,
1475+
nint targetIdentity) : IEquatable<CpuTargetCacheKey>
14421476
{
14431477
/// <summary>
14441478
/// Gets the texture format for the cached CPU target.
@@ -1460,22 +1494,29 @@ internal readonly struct CpuTargetCacheKey(
14601494
/// </summary>
14611495
public int PixelSizeInBytes { get; } = pixelSizeInBytes;
14621496

1497+
/// <summary>
1498+
/// Gets the identity of the target frame to prevent different targets
1499+
/// with the same dimensions from sharing GPU texture content.
1500+
/// </summary>
1501+
public nint TargetIdentity { get; } = targetIdentity;
1502+
14631503
/// <summary>
14641504
/// Determines whether this key equals another CPU target cache key.
14651505
/// </summary>
14661506
/// <param name="other">The key to compare.</param>
1467-
/// <returns><see langword="true"/> if all dimensions and format match; otherwise <see langword="false"/>.</returns>
1507+
/// <returns><see langword="true"/> if all fields match; otherwise <see langword="false"/>.</returns>
14681508
public bool Equals(CpuTargetCacheKey other)
14691509
=> this.TextureFormat == other.TextureFormat &&
14701510
this.Width == other.Width &&
14711511
this.Height == other.Height &&
1472-
this.PixelSizeInBytes == other.PixelSizeInBytes;
1512+
this.PixelSizeInBytes == other.PixelSizeInBytes &&
1513+
this.TargetIdentity == other.TargetIdentity;
14731514

14741515
/// <inheritdoc/>
14751516
public override bool Equals(object? obj) => obj is CpuTargetCacheKey other && this.Equals(other);
14761517

14771518
/// <inheritdoc/>
1478-
public override int GetHashCode() => HashCode.Combine((int)this.TextureFormat, this.Width, this.Height, this.PixelSizeInBytes);
1519+
public override int GetHashCode() => HashCode.Combine((int)this.TextureFormat, this.Width, this.Height, this.PixelSizeInBytes, this.TargetIdentity);
14791520
}
14801521

14811522
/// <summary>

src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ public void FlushCompositions<TPixel>(
7171
}
7272
}
7373

74+
/// <inheritdoc />
75+
public void ReleaseFrameResources<TPixel>(
76+
Configuration configuration,
77+
ICanvasFrame<TPixel> target)
78+
where TPixel : unmanaged, IPixel<TPixel>
79+
{
80+
// No cached resources to release for CPU-only backend.
81+
}
82+
7483
/// <inheritdoc />
7584
public bool TryReadRegion<TPixel>(
7685
Configuration configuration,

src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,15 @@ public bool TryReadRegion<TPixel>(
4040
Rectangle sourceRectangle,
4141
[NotNullWhen(true)] out Image<TPixel>? image)
4242
where TPixel : unmanaged, IPixel<TPixel>;
43+
44+
/// <summary>
45+
/// Releases any backend resources cached against the specified target frame.
46+
/// </summary>
47+
/// <typeparam name="TPixel">The pixel format.</typeparam>
48+
/// <param name="configuration">Active processing configuration.</param>
49+
/// <param name="target">The target frame whose resources should be released.</param>
50+
public void ReleaseFrameResources<TPixel>(
51+
Configuration configuration,
52+
ICanvasFrame<TPixel> target)
53+
where TPixel : unmanaged, IPixel<TPixel>;
4354
}

src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ public sealed class DrawingCanvas<TPixel> : IDrawingCanvas
4747
/// </summary>
4848
private readonly List<Image<TPixel>> pendingImageResources = [];
4949

50+
/// <summary>
51+
/// Indicates whether this canvas is the root owner of the target frame.
52+
/// Only the root canvas releases backend resources on dispose.
53+
/// </summary>
54+
private readonly bool isRoot;
55+
5056
/// <summary>
5157
/// Tracks whether this instance has already been disposed.
5258
/// </summary>
@@ -108,7 +114,8 @@ internal DrawingCanvas(
108114
backend,
109115
targetFrame,
110116
new DrawingCanvasBatcher<TPixel>(configuration, backend, targetFrame),
111-
new DrawingCanvasState(options, clipPaths))
117+
new DrawingCanvasState(options, clipPaths),
118+
isRoot: true)
112119
{
113120
}
114121

@@ -121,12 +128,14 @@ internal DrawingCanvas(
121128
/// <param name="targetFrame">The destination frame.</param>
122129
/// <param name="batcher">The command batcher used for deferred composition.</param>
123130
/// <param name="defaultState">The default state used when no scoped state is active.</param>
131+
/// <param name="isRoot">Whether this canvas is the root owner of the target frame.</param>
124132
private DrawingCanvas(
125133
Configuration configuration,
126134
IDrawingBackend backend,
127135
ICanvasFrame<TPixel> targetFrame,
128136
DrawingCanvasBatcher<TPixel> batcher,
129-
DrawingCanvasState defaultState)
137+
DrawingCanvasState defaultState,
138+
bool isRoot)
130139
{
131140
Guard.NotNull(configuration, nameof(configuration));
132141
Guard.NotNull(backend, nameof(backend));
@@ -143,6 +152,7 @@ private DrawingCanvas(
143152
this.backend = backend;
144153
this.targetFrame = targetFrame;
145154
this.batcher = batcher;
155+
this.isRoot = isRoot;
146156

147157
// Canvas coordinates are local to the current frame; origin stays at (0,0).
148158
this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height);
@@ -277,7 +287,8 @@ public DrawingCanvas<TPixel> CreateRegion(Rectangle region)
277287
this.backend,
278288
childFrame,
279289
this.batcher,
280-
this.ResolveState());
290+
this.ResolveState(),
291+
isRoot: false);
281292
}
282293

283294
/// <inheritdoc />
@@ -1046,6 +1057,12 @@ public void Dispose()
10461057
finally
10471058
{
10481059
this.DisposePendingImageResources();
1060+
1061+
if (this.isRoot)
1062+
{
1063+
this.backend.ReleaseFrameResources(this.configuration, this.targetFrame);
1064+
}
1065+
10491066
this.isDisposed = true;
10501067
}
10511068
}

tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,5 +173,12 @@ public bool TryReadRegion<TPixel>(
173173
image = null;
174174
return false;
175175
}
176+
177+
public void ReleaseFrameResources<TPixel>(
178+
Configuration configuration,
179+
ICanvasFrame<TPixel> target)
180+
where TPixel : unmanaged, IPixel<TPixel>
181+
{
182+
}
176183
}
177184
}

tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,12 @@ public bool TryReadRegion<TTargetPixel>(
193193
image = cropped.CloneAs<TTargetPixel>();
194194
return true;
195195
}
196+
197+
public void ReleaseFrameResources<TTargetPixel>(
198+
Configuration configuration,
199+
ICanvasFrame<TTargetPixel> target)
200+
where TTargetPixel : unmanaged, IPixel<TTargetPixel>
201+
{
202+
}
196203
}
197204
}

tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,12 @@ public bool TryReadRegion<TPixel>(
6565
image = null;
6666
return false;
6767
}
68+
69+
public void ReleaseFrameResources<TPixel>(
70+
Configuration configuration,
71+
ICanvasFrame<TPixel> target)
72+
where TPixel : unmanaged, IPixel<TPixel>
73+
{
74+
}
6875
}
6976
}

0 commit comments

Comments
 (0)