Skip to content

Commit 654b96b

Browse files
Use WebGPUNativeTarget and refactor native surface
1 parent 93d4c77 commit 654b96b

8 files changed

Lines changed: 66 additions & 101 deletions

File tree

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

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,23 @@ public void ReadRegion<TPixel>(
3333
Guard.NotNull(destination.Buffer, nameof(destination));
3434

3535
// Readback is only available for native WebGPU targets with valid interop handles.
36-
if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) ||
37-
!nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability) ||
38-
capability.DeviceHandle.IsInvalid ||
39-
capability.QueueHandle.IsInvalid ||
40-
capability.TargetTextureHandle.IsInvalid)
36+
if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface))
4137
{
42-
throw new NotSupportedException("The target does not expose a native WebGPU surface with valid device, queue, and texture handles for readback.");
38+
throw new NotSupportedException("The target does not expose a native surface for readback.");
39+
}
40+
41+
WebGPUNativeTarget nativeTarget = nativeSurface.GetNativeTarget<WebGPUNativeTarget>();
42+
if (nativeTarget.DeviceHandle.IsInvalid ||
43+
nativeTarget.QueueHandle.IsInvalid ||
44+
nativeTarget.TargetTextureHandle.IsInvalid)
45+
{
46+
throw new NotSupportedException("The target does not expose valid WebGPU device, queue, and texture handles for readback.");
4347
}
4448

4549
if (!TryGetCompositeTextureFormat<TPixel>(out WebGPUTextureFormatId expectedFormat, out FeatureName requiredFeature) ||
46-
expectedFormat != capability.TargetFormat)
50+
expectedFormat != nativeTarget.TargetFormat)
4751
{
48-
throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' cannot be read back from target format '{capability.TargetFormat}'.");
52+
throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' cannot be read back from target format '{nativeTarget.TargetFormat}'.");
4953
}
5054

5155
// Convert canvas-local source coordinates to absolute native-surface coordinates.
@@ -55,7 +59,7 @@ public void ReadRegion<TPixel>(
5559
sourceRectangle.Width,
5660
sourceRectangle.Height);
5761

58-
Rectangle surfaceBounds = new(0, 0, capability.Width, capability.Height);
62+
Rectangle surfaceBounds = new(0, 0, nativeTarget.Width, nativeTarget.Height);
5963
Rectangle source = Rectangle.Intersect(surfaceBounds, absoluteSource);
6064

6165
if (source.Width <= 0 || source.Height <= 0)
@@ -65,14 +69,14 @@ public void ReadRegion<TPixel>(
6569

6670
WebGPU api = WebGPURuntime.GetApi();
6771
Wgpu wgpuExtension = WebGPURuntime.GetWgpuExtension();
68-
using WebGPUHandle.HandleReference deviceReference = capability.DeviceHandle.AcquireReference();
69-
using WebGPUHandle.HandleReference queueReference = capability.QueueHandle.AcquireReference();
70-
using WebGPUHandle.HandleReference textureReference = capability.TargetTextureHandle.AcquireReference();
72+
using WebGPUHandle.HandleReference deviceReference = nativeTarget.DeviceHandle.AcquireReference();
73+
using WebGPUHandle.HandleReference queueReference = nativeTarget.QueueHandle.AcquireReference();
74+
using WebGPUHandle.HandleReference textureReference = nativeTarget.TargetTextureHandle.AcquireReference();
7175

7276
Device* device = (Device*)deviceReference.Handle;
7377

7478
if (requiredFeature != FeatureName.Undefined
75-
&& !WebGPURuntime.GetOrCreateDeviceState(api, capability.DeviceHandle).HasFeature(requiredFeature))
79+
&& !WebGPURuntime.GetOrCreateDeviceState(api, nativeTarget.DeviceHandle).HasFeature(requiredFeature))
7680
{
7781
throw new NotSupportedException($"The target device does not support WebGPU feature '{requiredFeature}' required to read back pixel type '{typeof(TPixel).Name}'.");
7882
}

src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs

Lines changed: 17 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,25 @@ private WebGPUFlushContext(
172172
MemoryAllocator memoryAllocator)
173173
where TPixel : unmanaged, IPixel<TPixel>
174174
{
175-
WebGPUSurfaceCapability? nativeCapability = TryGetNativeSurfaceCapability(frame, expectedTextureFormat);
176-
if (nativeCapability is null)
175+
if (!frame.TryGetNativeSurface(out NativeSurface? nativeSurface))
177176
{
178177
return null;
179178
}
180179

180+
WebGPUNativeTarget nativeTarget = nativeSurface.GetNativeTarget<WebGPUNativeTarget>();
181181
WebGPU api = WebGPURuntime.GetApi();
182-
TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat);
182+
TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeTarget.TargetFormat);
183183
Rectangle bounds = frame.Bounds;
184-
Rectangle nativeBounds = new(0, 0, nativeCapability.Width, nativeCapability.Height);
185-
WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(api, nativeCapability.DeviceHandle);
184+
Rectangle nativeBounds = new(0, 0, nativeTarget.Width, nativeTarget.Height);
185+
WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(api, nativeTarget.DeviceHandle);
186+
187+
if (nativeTarget.DeviceHandle.IsInvalid ||
188+
nativeTarget.QueueHandle.IsInvalid ||
189+
nativeTarget.TargetTextureViewHandle.IsInvalid ||
190+
textureFormat != expectedTextureFormat)
191+
{
192+
return null;
193+
}
186194

187195
if (requiredFeature != FeatureName.Undefined && !deviceState.HasFeature(requiredFeature))
188196
{
@@ -200,10 +208,10 @@ private WebGPUFlushContext(
200208
return new WebGPUFlushContext(
201209
api,
202210
WebGPURuntime.GetWgpuExtension(),
203-
nativeCapability.DeviceHandle,
204-
nativeCapability.QueueHandle,
205-
nativeCapability.TargetTextureHandle,
206-
nativeCapability.TargetTextureViewHandle,
211+
nativeTarget.DeviceHandle,
212+
nativeTarget.QueueHandle,
213+
nativeTarget.TargetTextureHandle,
214+
nativeTarget.TargetTextureViewHandle,
207215
in bounds,
208216
textureFormat,
209217
memoryAllocator,
@@ -486,42 +494,6 @@ public void Dispose()
486494
this.disposed = true;
487495
}
488496

489-
/// <summary>
490-
/// Tries to obtain a native WebGPU surface capability from the canvas frame.
491-
/// </summary>
492-
/// <typeparam name="TPixel">The pixel type of the canvas frame.</typeparam>
493-
/// <param name="frame">The frame being flushed.</param>
494-
/// <param name="expectedTextureFormat">The texture format required by the current WebGPU path.</param>
495-
/// <returns>The compatible surface capability, or <see langword="null"/> when the frame cannot expose one.</returns>
496-
private static WebGPUSurfaceCapability? TryGetNativeSurfaceCapability<TPixel>(ICanvasFrame<TPixel> frame, TextureFormat expectedTextureFormat)
497-
where TPixel : unmanaged, IPixel<TPixel>
498-
{
499-
if (!frame.TryGetNativeSurface(out NativeSurface? nativeSurface) ||
500-
!nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability))
501-
{
502-
return null;
503-
}
504-
505-
if (capability.DeviceHandle.IsInvalid ||
506-
capability.QueueHandle.IsInvalid ||
507-
capability.TargetTextureViewHandle.IsInvalid ||
508-
WebGPUTextureFormatMapper.ToSilk(capability.TargetFormat) != expectedTextureFormat)
509-
{
510-
return null;
511-
}
512-
513-
Rectangle bounds = frame.Bounds;
514-
if (bounds.X < 0 ||
515-
bounds.Y < 0 ||
516-
bounds.Right > capability.Width ||
517-
bounds.Bottom > capability.Height)
518-
{
519-
return null;
520-
}
521-
522-
return capability;
523-
}
524-
525497
/// <summary>
526498
/// Uploads a source region into the destination texture starting at the origin.
527499
/// </summary>
Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,31 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Diagnostics.CodeAnalysis;
5-
64
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
75

86
/// <summary>
97
/// WebGPU native drawing target exposed through the backend-agnostic <see cref="NativeSurface"/> contract.
108
/// </summary>
119
internal sealed class WebGPUNativeSurface : NativeSurface
1210
{
13-
private readonly WebGPUSurfaceCapability capability;
11+
private readonly WebGPUNativeTarget target;
1412

1513
/// <summary>
1614
/// Initializes a new instance of the <see cref="WebGPUNativeSurface"/> class.
1715
/// </summary>
18-
/// <param name="capability">The WebGPU capability exposed by this surface.</param>
19-
internal WebGPUNativeSurface(WebGPUSurfaceCapability capability)
20-
=> this.capability = capability;
16+
/// <param name="target">The WebGPU target exposed by this surface.</param>
17+
internal WebGPUNativeSurface(WebGPUNativeTarget target)
18+
=> this.target = target;
2119

2220
/// <inheritdoc />
23-
public override bool TryGetCapability<TCapability>([NotNullWhen(true)] out TCapability? capability)
24-
where TCapability : class
21+
public override TNativeTarget GetNativeTarget<TNativeTarget>()
22+
where TNativeTarget : class
2523
{
26-
if (this.capability is TCapability typed)
24+
if (this.target is TNativeTarget typed)
2725
{
28-
capability = typed;
29-
return true;
26+
return typed;
3027
}
3128

32-
capability = null;
33-
return false;
29+
throw new NotSupportedException($"The native surface does not expose a native target of type '{typeof(TNativeTarget).Name}'.");
3430
}
3531
}

src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ internal static NativeSurface Create<TPixel>(
6969
Guard.MustBeGreaterThan(height, 0, nameof(height));
7070
ValidatePixelCompatibility<TPixel>(targetFormat);
7171

72-
return new WebGPUNativeSurface(new WebGPUSurfaceCapability(
72+
return new WebGPUNativeSurface(new WebGPUNativeTarget(
7373
deviceHandle,
7474
queueHandle,
7575
targetTextureHandle,

src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs renamed to src/ImageSharp.Drawing.WebGPU/WebGPUNativeTarget.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
55

66
/// <summary>
7-
/// Native WebGPU surface capability exposed by a <see cref="NativeSurface"/>.
7+
/// Native WebGPU target exposed by a <see cref="NativeSurface"/>.
88
/// </summary>
99
/// <remarks>
1010
/// The backing WebGPU device, queue, texture, and texture view must remain valid while canvases target this surface.
1111
/// </remarks>
12-
internal sealed class WebGPUSurfaceCapability
12+
internal sealed class WebGPUNativeTarget
1313
{
1414
private readonly WebGPUDeviceHandle deviceHandle;
1515
private readonly WebGPUQueueHandle queueHandle;
1616
private readonly WebGPUTextureHandle targetTextureHandle;
1717
private readonly WebGPUTextureViewHandle targetTextureViewHandle;
1818

19-
internal WebGPUSurfaceCapability(
19+
public WebGPUNativeTarget(
2020
WebGPUDeviceHandle deviceHandle,
2121
WebGPUQueueHandle queueHandle,
2222
WebGPUTextureHandle targetTextureHandle,
@@ -42,35 +42,35 @@ internal WebGPUSurfaceCapability(
4242
/// <summary>
4343
/// Gets the wrapped device handle that owns the target texture.
4444
/// </summary>
45-
internal WebGPUDeviceHandle DeviceHandle => this.deviceHandle;
45+
public WebGPUDeviceHandle DeviceHandle => this.deviceHandle;
4646

4747
/// <summary>
4848
/// Gets the wrapped queue handle used to submit work against the target texture.
4949
/// </summary>
50-
internal WebGPUQueueHandle QueueHandle => this.queueHandle;
50+
public WebGPUQueueHandle QueueHandle => this.queueHandle;
5151

5252
/// <summary>
53-
/// Gets the wrapped target texture handle exposed by this surface capability.
53+
/// Gets the wrapped target texture handle.
5454
/// </summary>
55-
internal WebGPUTextureHandle TargetTextureHandle => this.targetTextureHandle;
55+
public WebGPUTextureHandle TargetTextureHandle => this.targetTextureHandle;
5656

5757
/// <summary>
58-
/// Gets the wrapped target texture-view handle exposed by this surface capability.
58+
/// Gets the wrapped target texture-view handle.
5959
/// </summary>
60-
internal WebGPUTextureViewHandle TargetTextureViewHandle => this.targetTextureViewHandle;
60+
public WebGPUTextureViewHandle TargetTextureViewHandle => this.targetTextureViewHandle;
6161

6262
/// <summary>
6363
/// Gets the native render target texture format identifier.
6464
/// </summary>
65-
internal WebGPUTextureFormatId TargetFormat { get; }
65+
public WebGPUTextureFormatId TargetFormat { get; }
6666

6767
/// <summary>
6868
/// Gets the surface width in pixels.
6969
/// </summary>
70-
internal int Width { get; }
70+
public int Width { get; }
7171

7272
/// <summary>
7373
/// Gets the surface height in pixels.
7474
/// </summary>
75-
internal int Height { get; }
75+
public int Height { get; }
7676
}
Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Diagnostics.CodeAnalysis;
5-
64
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
75

86
/// <summary>
@@ -18,11 +16,10 @@ protected NativeSurface()
1816
}
1917

2018
/// <summary>
21-
/// Attempts to get a backend capability by type.
19+
/// Gets a backend-specific native target by type.
2220
/// </summary>
23-
/// <typeparam name="TCapability">Capability type.</typeparam>
24-
/// <param name="capability">Capability instance when available.</param>
25-
/// <returns><see langword="true"/> when found.</returns>
26-
public abstract bool TryGetCapability<TCapability>([NotNullWhen(true)] out TCapability? capability)
27-
where TCapability : class;
21+
/// <typeparam name="TNativeTarget">Native target type.</typeparam>
22+
/// <returns>The native target.</returns>
23+
public abstract TNativeTarget GetNativeTarget<TNativeTarget>()
24+
where TNativeTarget : class;
2825
}

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public void Process_Path_MatchesReference<TPixel>(TestImageProvider<TPixel> prov
6464

6565
[Theory]
6666
[WithBlankImage(220, 160, PixelTypes.Rgba32)]
67-
public void Process_NoCpuFrame_WithReadbackCapability_MatchesReference<TPixel>(TestImageProvider<TPixel> provider)
67+
public void Process_NoCpuFrame_UsesBackendReadback_MatchesReference<TPixel>(TestImageProvider<TPixel> provider)
6868
where TPixel : unmanaged, IPixel<TPixel>
6969
{
7070
using Image<TPixel> target = provider.GetImage();
@@ -74,7 +74,7 @@ public void Process_NoCpuFrame_WithReadbackCapability_MatchesReference<TPixel>(T
7474
MemoryCanvasFrame<TPixel> proxyFrame = new(new Buffer2DRegion<TPixel>(target.Frames.RootFrame.PixelBuffer));
7575
MirroringCpuReadbackTestBackend<TPixel> mirroringBackend = new(proxyFrame, target);
7676

77-
NativeSurface nativeSurface = new NoCapabilityNativeSurface();
77+
NativeSurface nativeSurface = new UnsupportedNativeSurface();
7878
Configuration configuration = provider.Configuration.Clone();
7979
configuration.SetDrawingBackend(mirroringBackend);
8080

@@ -158,14 +158,10 @@ private static IPath CreatePixelateTrianglePath()
158158
return pathBuilder.Build();
159159
}
160160

161-
private sealed class NoCapabilityNativeSurface : NativeSurface
161+
private sealed class UnsupportedNativeSurface : NativeSurface
162162
{
163-
public override bool TryGetCapability<TCapability>(out TCapability capability)
164-
where TCapability : class
165-
{
166-
capability = null!;
167-
return false;
168-
}
163+
public override TNativeTarget GetNativeTarget<TNativeTarget>()
164+
=> throw new NotSupportedException();
169165
}
170166

171167
/// <summary>

tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png renamed to tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_UsesBackendReadback_MatchesReference_Rgba32.png

File renamed without changes.

0 commit comments

Comments
 (0)