Skip to content

Commit 7ed6fbf

Browse files
Introduce safe WebGPU handle wrappers
1 parent 920b798 commit 7ed6fbf

27 files changed

+1310
-457
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using Silk.NET.WebGPU;
5+
6+
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
7+
8+
/// <summary>
9+
/// Safe-handle wrapper for a WebGPU adapter handle.
10+
/// </summary>
11+
internal sealed unsafe class WebGPUAdapterHandle : WebGPUHandle
12+
{
13+
private readonly WebGPU? api;
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="WebGPUAdapterHandle"/> class.
17+
/// </summary>
18+
/// <param name="adapterHandle">The WebGPU adapter handle value.</param>
19+
/// <param name="ownsHandle">
20+
/// <see langword="true"/> when this wrapper owns the adapter and must release it;
21+
/// <see langword="false"/> when the caller retains ownership.
22+
/// </param>
23+
internal WebGPUAdapterHandle(nint adapterHandle, bool ownsHandle)
24+
: this(ownsHandle ? WebGPURuntime.GetApi() : null, adapterHandle, ownsHandle)
25+
{
26+
}
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="WebGPUAdapterHandle"/> class.
30+
/// </summary>
31+
/// <param name="api">
32+
/// The WebGPU API facade used to release the handle when this wrapper owns it,
33+
/// or <see langword="null"/> when the wrapper is non-owning.
34+
/// </param>
35+
/// <param name="adapterHandle">The WebGPU adapter handle value.</param>
36+
/// <param name="ownsHandle">
37+
/// <see langword="true"/> when this wrapper owns the adapter and must release it;
38+
/// <see langword="false"/> when the caller retains ownership.
39+
/// </param>
40+
internal WebGPUAdapterHandle(WebGPU? api, nint adapterHandle, bool ownsHandle)
41+
: base(adapterHandle, ownsHandle)
42+
=> this.api = api;
43+
44+
/// <inheritdoc />
45+
protected override bool ReleaseHandle()
46+
{
47+
try
48+
{
49+
this.api?.AdapterRelease((Adapter*)this.handle);
50+
return true;
51+
}
52+
catch
53+
{
54+
return false;
55+
}
56+
finally
57+
{
58+
this.handle = IntPtr.Zero;
59+
}
60+
}
61+
}

src/ImageSharp.Drawing.WebGPU/WebGPUDeviceContext{TPixel}.cs

Lines changed: 145 additions & 70 deletions
Large diffs are not rendered by default.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using Silk.NET.WebGPU;
5+
6+
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
7+
8+
/// <summary>
9+
/// Safe-handle wrapper for a WebGPU device handle.
10+
/// </summary>
11+
internal sealed unsafe class WebGPUDeviceHandle : WebGPUHandle
12+
{
13+
private readonly WebGPU? api;
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="WebGPUDeviceHandle"/> class.
17+
/// </summary>
18+
/// <param name="deviceHandle">The WebGPU device handle value.</param>
19+
/// <param name="ownsHandle">
20+
/// <see langword="true"/> when this wrapper owns the device and must release it;
21+
/// <see langword="false"/> when the caller retains ownership.
22+
/// </param>
23+
internal WebGPUDeviceHandle(nint deviceHandle, bool ownsHandle)
24+
: this(ownsHandle ? WebGPURuntime.GetApi() : null, deviceHandle, ownsHandle)
25+
{
26+
}
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="WebGPUDeviceHandle"/> class.
30+
/// </summary>
31+
/// <param name="api">
32+
/// The WebGPU API facade used to release the handle when this wrapper owns it,
33+
/// or <see langword="null"/> when the wrapper is non-owning.
34+
/// </param>
35+
/// <param name="deviceHandle">The WebGPU device handle value.</param>
36+
/// <param name="ownsHandle">
37+
/// <see langword="true"/> when this wrapper owns the device and must release it;
38+
/// <see langword="false"/> when the caller retains ownership.
39+
/// </param>
40+
internal WebGPUDeviceHandle(WebGPU? api, nint deviceHandle, bool ownsHandle)
41+
: base(deviceHandle, ownsHandle)
42+
=> this.api = api;
43+
44+
/// <inheritdoc />
45+
protected override bool ReleaseHandle()
46+
{
47+
try
48+
{
49+
this.api?.DeviceRelease((Device*)this.handle);
50+
return true;
51+
}
52+
catch
53+
{
54+
return false;
55+
}
56+
finally
57+
{
58+
this.handle = IntPtr.Zero;
59+
}
60+
}
61+
}

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ public bool TryReadRegion<TPixel>(
5858
// Readback is only available for native WebGPU targets with valid interop handles.
5959
if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) ||
6060
!nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability) ||
61-
capability.Device == 0 ||
62-
capability.Queue == 0 ||
63-
capability.TargetTexture == 0)
61+
capability.DeviceHandle.IsInvalid ||
62+
capability.QueueHandle.IsInvalid ||
63+
capability.TargetTextureHandle.IsInvalid)
6464
{
6565
error = "The target does not expose a native WebGPU surface with valid device, queue, and texture handles for readback.";
6666
return false;
@@ -91,16 +91,21 @@ public bool TryReadRegion<TPixel>(
9191

9292
WebGPU api = WebGPURuntime.GetApi();
9393
Wgpu wgpuExtension = WebGPURuntime.GetWgpuExtension();
94-
Device* device = (Device*)capability.Device;
94+
using WebGPUHandle.HandleReference deviceReference = capability.DeviceHandle.AcquireReference();
95+
using WebGPUHandle.HandleReference queueReference = capability.QueueHandle.AcquireReference();
96+
using WebGPUHandle.HandleReference textureReference = capability.TargetTextureHandle.AcquireReference();
97+
98+
Device* device = (Device*)deviceReference.Handle;
9599

96100
if (requiredFeature != FeatureName.Undefined
97-
&& !WebGPURuntime.GetOrCreateDeviceState(api, device).HasFeature(requiredFeature))
101+
&& !WebGPURuntime.GetOrCreateDeviceState(api, capability.DeviceHandle).HasFeature(requiredFeature))
98102
{
99103
error = $"The target device does not support WebGPU feature '{requiredFeature}' required to read back pixel type '{typeof(TPixel).Name}'.";
100104
return false;
101105
}
102106

103-
Queue* queue = (Queue*)capability.Queue;
107+
Queue* queue = (Queue*)queueReference.Handle;
108+
Texture* texture = (Texture*)textureReference.Handle;
104109

105110
int pixelSizeInBytes = Unsafe.SizeOf<TPixel>();
106111
int packedRowBytes = checked(source.Width * pixelSizeInBytes);
@@ -139,7 +144,7 @@ public bool TryReadRegion<TPixel>(
139144
// Copy only the requested source rect from the target texture into the readback buffer.
140145
ImageCopyTexture sourceCopy = new()
141146
{
142-
Texture = (Texture*)capability.TargetTexture,
147+
Texture = texture,
143148
MipLevel = 0,
144149
Origin = new Origin3D((uint)source.X, (uint)source.Y, 0),
145150
Aspect = TextureAspect.All

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,11 @@ private void FlushCompositionsFallback<TPixel>(
236236
Rectangle? compositionBounds)
237237
where TPixel : unmanaged, IPixel<TPixel>
238238
{
239-
_ = target.TryGetNativeSurface(out NativeSurface? nativeSurface);
240-
_ = nativeSurface!.TryGetCapability(out WebGPUSurfaceCapability? capability);
239+
if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) ||
240+
!nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability))
241+
{
242+
return;
243+
}
241244

242245
Rectangle targetBounds = target.Bounds;
243246
using Buffer2D<TPixel> stagingBuffer =
@@ -258,17 +261,20 @@ private void FlushCompositionsFallback<TPixel>(
258261
this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositionScene);
259262

260263
WebGPU api = WebGPURuntime.GetApi();
264+
using WebGPUHandle.HandleReference queueReference = capability.QueueHandle.AcquireReference();
265+
261266
Buffer2DRegion<TPixel> uploadRegion = compositionBounds is Rectangle cb && cb.Width > 0 && cb.Height > 0
262267
? stagingRegion.GetSubRegion(cb)
263268
: stagingRegion;
264269

265270
uint destX = compositionBounds is Rectangle cbx ? (uint)cbx.X : 0;
266271
uint destY = compositionBounds is Rectangle cby ? (uint)cby.Y : 0;
267272

273+
using WebGPUHandle.HandleReference textureReference = capability.TargetTextureHandle.AcquireReference();
268274
WebGPUFlushContext.UploadTextureFromRegion(
269275
api,
270-
(Queue*)capability!.Queue,
271-
(Texture*)capability.TargetTexture,
276+
(Queue*)queueReference.Handle,
277+
(Texture*)textureReference.Handle,
272278
uploadRegion,
273279
configuration.MemoryAllocator,
274280
destX,

src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable
3131
private bool disposed;
3232
private bool ownsTargetTexture;
3333
private bool ownsTargetView;
34+
private readonly WebGPUDeviceHandle deviceHandle;
35+
private readonly WebGPUQueueHandle queueHandle;
36+
private readonly WebGPUTextureHandle targetTextureHandle;
37+
private readonly WebGPUTextureViewHandle targetTextureViewHandle;
38+
private WebGPUHandle.HandleReference? deviceReference;
39+
private WebGPUHandle.HandleReference? queueReference;
40+
private WebGPUHandle.HandleReference? targetTextureReference;
41+
private WebGPUHandle.HandleReference? targetTextureViewReference;
3442
private readonly List<nint> transientBindGroups = [];
3543
private readonly List<nint> transientBuffers = [];
3644
private readonly List<nint> transientTextureViews = [];
@@ -44,17 +52,21 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable
4452
private WebGPUFlushContext(
4553
WebGPU api,
4654
Wgpu wgpuExtension,
47-
Device* device,
48-
Queue* queue,
55+
WebGPUDeviceHandle deviceHandle,
56+
WebGPUQueueHandle queueHandle,
57+
WebGPUTextureHandle targetTextureHandle,
58+
WebGPUTextureViewHandle targetTextureViewHandle,
4959
in Rectangle targetBounds,
5060
TextureFormat textureFormat,
5161
MemoryAllocator memoryAllocator,
5262
WebGPURuntime.DeviceSharedState deviceState)
5363
{
5464
this.Api = api;
5565
this.WgpuExtension = wgpuExtension;
56-
this.Device = device;
57-
this.Queue = queue;
66+
this.deviceHandle = deviceHandle;
67+
this.queueHandle = queueHandle;
68+
this.targetTextureHandle = targetTextureHandle;
69+
this.targetTextureViewHandle = targetTextureViewHandle;
5870
this.TargetBounds = targetBounds;
5971
this.TextureFormat = textureFormat;
6072
this.MemoryAllocator = memoryAllocator;
@@ -74,12 +86,12 @@ private WebGPUFlushContext(
7486
/// <summary>
7587
/// Gets the device used to create and execute GPU resources.
7688
/// </summary>
77-
public Device* Device { get; }
89+
public Device* Device { get; private set; }
7890

7991
/// <summary>
8092
/// Gets the queue used to submit GPU work.
8193
/// </summary>
82-
public Queue* Queue { get; }
94+
public Queue* Queue { get; private set; }
8395

8496
/// <summary>
8597
/// Gets the target bounds for this flush context.
@@ -168,12 +180,10 @@ private WebGPUFlushContext(
168180
}
169181

170182
WebGPU api = WebGPURuntime.GetApi();
171-
Device* device = (Device*)nativeCapability.Device;
172-
Queue* queue = (Queue*)nativeCapability.Queue;
173183
TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat);
174184
Rectangle bounds = frame.Bounds;
175185
Rectangle nativeBounds = new(0, 0, nativeCapability.Width, nativeCapability.Height);
176-
WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(api, device);
186+
WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(api, nativeCapability.DeviceHandle);
177187

178188
if (requiredFeature != FeatureName.Undefined && !deviceState.HasFeature(requiredFeature))
179189
{
@@ -191,13 +201,28 @@ private WebGPUFlushContext(
191201
WebGPUFlushContext context = new(
192202
api,
193203
WebGPURuntime.GetWgpuExtension(),
194-
device,
195-
queue,
204+
nativeCapability.DeviceHandle,
205+
nativeCapability.QueueHandle,
206+
nativeCapability.TargetTextureHandle,
207+
nativeCapability.TargetTextureViewHandle,
196208
in bounds,
197209
textureFormat,
198210
memoryAllocator,
199211
deviceState);
200-
context.InitializeNativeTarget(nativeCapability);
212+
try
213+
{
214+
if (!context.InitializeNativeTarget())
215+
{
216+
context.Dispose();
217+
return null;
218+
}
219+
}
220+
catch
221+
{
222+
context.Dispose();
223+
throw;
224+
}
225+
201226
return context;
202227
}
203228

@@ -486,20 +511,46 @@ public void Dispose()
486511
this.TargetTexture = null;
487512
this.ownsTargetView = false;
488513
this.ownsTargetTexture = false;
514+
this.DisposeNativeHandleReferences();
489515

490516
this.disposed = true;
491517
}
492518

493519
/// <summary>
494520
/// Adopts the texture and texture view provided by a native WebGPU surface capability.
495521
/// </summary>
496-
/// <param name="capability">The native surface capability describing the externally owned target.</param>
497-
private void InitializeNativeTarget(WebGPUSurfaceCapability capability)
522+
/// <returns><see langword="true"/> when all required native handles were acquired successfully; otherwise, <see langword="false"/>.</returns>
523+
private bool InitializeNativeTarget()
498524
{
499-
this.TargetTexture = (Texture*)capability.TargetTexture;
500-
this.TargetView = (TextureView*)capability.TargetTextureView;
525+
// The flush context caches raw native pointers and reuses them across the full flush,
526+
// so each safe handle must stay add-ref'd until the context is disposed. These fields are
527+
// assigned immediately so Dispose can unwind partial initialization if a later acquire throws.
528+
this.deviceReference = this.deviceHandle.AcquireReference();
529+
this.queueReference = this.queueHandle.AcquireReference();
530+
this.targetTextureReference = this.targetTextureHandle.AcquireReference();
531+
this.targetTextureViewReference = this.targetTextureViewHandle.AcquireReference();
532+
this.Device = (Device*)this.deviceReference.Handle;
533+
this.Queue = (Queue*)this.queueReference.Handle;
534+
this.TargetTexture = (Texture*)this.targetTextureReference.Handle;
535+
this.TargetView = (TextureView*)this.targetTextureViewReference.Handle;
501536
this.ownsTargetTexture = false;
502537
this.ownsTargetView = false;
538+
return true;
539+
}
540+
541+
/// <summary>
542+
/// Releases the scoped safe-handle references that keep the cached raw pointers valid for this flush.
543+
/// </summary>
544+
private void DisposeNativeHandleReferences()
545+
{
546+
this.targetTextureViewReference?.Dispose();
547+
this.targetTextureViewReference = null;
548+
this.targetTextureReference?.Dispose();
549+
this.targetTextureReference = null;
550+
this.queueReference?.Dispose();
551+
this.queueReference = null;
552+
this.deviceReference?.Dispose();
553+
this.deviceReference = null;
503554
}
504555

505556
/// <summary>
@@ -518,9 +569,9 @@ private void InitializeNativeTarget(WebGPUSurfaceCapability capability)
518569
return null;
519570
}
520571

521-
if (capability.Device == 0 ||
522-
capability.Queue == 0 ||
523-
capability.TargetTextureView == 0 ||
572+
if (capability.DeviceHandle.IsInvalid ||
573+
capability.QueueHandle.IsInvalid ||
574+
capability.TargetTextureViewHandle.IsInvalid ||
524575
WebGPUTextureFormatMapper.ToSilk(capability.TargetFormat) != expectedTextureFormat)
525576
{
526577
return null;

0 commit comments

Comments
 (0)