Skip to content

Commit e79e82a

Browse files
Replace TryReadRegion with ReadRegion (throws)
1 parent 681b5a4 commit e79e82a

13 files changed

Lines changed: 63 additions & 108 deletions

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public WebGPURenderTarget<TPixel> CreateRenderTarget(int width, int height)
167167
/// <returns>A canvas frame backed by the external WebGPU texture.</returns>
168168
/// <remarks>
169169
/// The caller retains ownership of the texture and view; this context does not release them.
170-
/// The texture must have been created with <c>RenderAttachment | TextureBinding</c> usage.
170+
/// The texture must have been created with <c>RenderAttachment | CopySrc | CopyDst | TextureBinding</c> usage.
171171
/// Both handles are typically valid only for the current frame: dispose any consumer of the returned frame
172172
/// before the host calls <c>wgpuSurfacePresent</c>, then re-acquire on the next frame.
173173
/// </remarks>
@@ -191,7 +191,7 @@ public NativeCanvasFrame<TPixel> CreateFrame(
191191
/// <returns>A drawing canvas targeting the external texture.</returns>
192192
/// <remarks>
193193
/// The caller retains ownership of the texture and view; this context does not release them.
194-
/// The texture must have been created with <c>RenderAttachment | TextureBinding</c> usage.
194+
/// The texture must have been created with <c>RenderAttachment | CopySrc | CopyDst | TextureBinding</c> usage.
195195
/// Dispose the returned canvas before the host calls <c>wgpuSurfacePresent</c>, then create a new canvas on the next frame.
196196
/// </remarks>
197197
public DrawingCanvas<TPixel> CreateCanvas(
@@ -221,7 +221,7 @@ public DrawingCanvas<TPixel> CreateCanvas(
221221
/// <returns>A drawing canvas targeting the external texture.</returns>
222222
/// <remarks>
223223
/// The caller retains ownership of the texture and view; this context does not release them.
224-
/// The texture must have been created with <c>RenderAttachment | TextureBinding</c> usage.
224+
/// The texture must have been created with <c>RenderAttachment | CopySrc | CopyDst | TextureBinding</c> usage.
225225
/// Dispose the returned canvas before the host calls <c>wgpuSurfacePresent</c>, then create a new canvas on the next frame.
226226
/// </remarks>
227227
public DrawingCanvas<TPixel> CreateCanvas(

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

Lines changed: 11 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Diagnostics;
5-
using System.Diagnostics.CodeAnalysis;
65
using System.Runtime.CompilerServices;
76
using System.Runtime.InteropServices;
87
using Silk.NET.WebGPU;
@@ -21,34 +20,12 @@ public sealed unsafe partial class WebGPUDrawingBackend
2120
private const int ReadbackCallbackTimeoutMilliseconds = 5000;
2221

2322
/// <inheritdoc />
24-
public bool TryReadRegion<TPixel>(
23+
public void ReadRegion<TPixel>(
2524
Configuration configuration,
2625
ICanvasFrame<TPixel> target,
2726
Rectangle sourceRectangle,
2827
Buffer2DRegion<TPixel> destination)
2928
where TPixel : unmanaged, IPixel<TPixel>
30-
=> this.TryReadRegion(configuration, target, sourceRectangle, destination, out _);
31-
32-
/// <summary>
33-
/// Attempts to read source pixels from the target into a caller-provided buffer.
34-
/// </summary>
35-
/// <typeparam name="TPixel">The pixel format.</typeparam>
36-
/// <param name="configuration">The active processing configuration.</param>
37-
/// <param name="target">The target frame.</param>
38-
/// <param name="sourceRectangle">Source rectangle in target-local coordinates.</param>
39-
/// <param name="destination">
40-
/// The caller-allocated region to receive the pixel data.
41-
/// Must be at least as large as <paramref name="sourceRectangle"/> (clamped to target bounds).
42-
/// </param>
43-
/// <param name="error">Receives the failure reason when readback cannot complete.</param>
44-
/// <returns><see langword="true"/> when readback succeeds; otherwise <see langword="false"/>.</returns>
45-
public bool TryReadRegion<TPixel>(
46-
Configuration configuration,
47-
ICanvasFrame<TPixel> target,
48-
Rectangle sourceRectangle,
49-
Buffer2DRegion<TPixel> destination,
50-
[NotNullWhen(false)] out string? error)
51-
where TPixel : unmanaged, IPixel<TPixel>
5229
{
5330
this.ThrowIfDisposed();
5431
Guard.NotNull(configuration, nameof(configuration));
@@ -62,15 +39,13 @@ public bool TryReadRegion<TPixel>(
6239
capability.QueueHandle.IsInvalid ||
6340
capability.TargetTextureHandle.IsInvalid)
6441
{
65-
error = "The target does not expose a native WebGPU surface with valid device, queue, and texture handles for readback.";
66-
return false;
42+
throw new NotSupportedException("The target does not expose a native WebGPU surface with valid device, queue, and texture handles for readback.");
6743
}
6844

6945
if (!TryGetCompositeTextureFormat<TPixel>(out WebGPUTextureFormatId expectedFormat, out FeatureName requiredFeature) ||
7046
expectedFormat != capability.TargetFormat)
7147
{
72-
error = $"Pixel type '{typeof(TPixel).Name}' cannot be read back from target format '{capability.TargetFormat}'.";
73-
return false;
48+
throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' cannot be read back from target format '{capability.TargetFormat}'.");
7449
}
7550

7651
// Convert canvas-local source coordinates to absolute native-surface coordinates.
@@ -85,8 +60,7 @@ public bool TryReadRegion<TPixel>(
8560

8661
if (source.Width <= 0 || source.Height <= 0)
8762
{
88-
error = "The requested readback rectangle does not intersect the target bounds.";
89-
return false;
63+
throw new ArgumentException("The requested readback rectangle does not intersect the target bounds.", nameof(sourceRectangle));
9064
}
9165

9266
WebGPU api = WebGPURuntime.GetApi();
@@ -100,8 +74,7 @@ public bool TryReadRegion<TPixel>(
10074
if (requiredFeature != FeatureName.Undefined
10175
&& !WebGPURuntime.GetOrCreateDeviceState(api, capability.DeviceHandle).HasFeature(requiredFeature))
10276
{
103-
error = $"The target device does not support WebGPU feature '{requiredFeature}' required to read back pixel type '{typeof(TPixel).Name}'.";
104-
return false;
77+
throw new NotSupportedException($"The target device does not support WebGPU feature '{requiredFeature}' required to read back pixel type '{typeof(TPixel).Name}'.");
10578
}
10679

10780
Queue* queue = (Queue*)queueReference.Handle;
@@ -129,16 +102,14 @@ public bool TryReadRegion<TPixel>(
129102
readbackBuffer = api.DeviceCreateBuffer(device, in bufferDescriptor);
130103
if (readbackBuffer is null)
131104
{
132-
error = "The WebGPU device could not create a readback buffer.";
133-
return false;
105+
throw new InvalidOperationException("The WebGPU device could not create a readback buffer.");
134106
}
135107

136108
CommandEncoderDescriptor encoderDescriptor = default;
137109
commandEncoder = api.DeviceCreateCommandEncoder(device, in encoderDescriptor);
138110
if (commandEncoder is null)
139111
{
140-
error = "The WebGPU device could not create a command encoder for readback.";
141-
return false;
112+
throw new InvalidOperationException("The WebGPU device could not create a command encoder for readback.");
142113
}
143114

144115
// Copy only the requested source rect from the target texture into the readback buffer.
@@ -168,8 +139,7 @@ public bool TryReadRegion<TPixel>(
168139
commandBuffer = api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor);
169140
if (commandBuffer is null)
170141
{
171-
error = "The WebGPU device could not finalize the readback command buffer.";
172-
return false;
142+
throw new InvalidOperationException("The WebGPU device could not finalize the readback command buffer.");
173143
}
174144

175145
api.QueueSubmit(queue, 1, ref commandBuffer);
@@ -192,16 +162,14 @@ void Callback(BufferMapAsyncStatus status, void* userData)
192162
api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)readbackByteCount, callback, null);
193163
if (!WaitForMapSignal(wgpuExtension, device, mapReady) || mapStatus != BufferMapAsyncStatus.Success)
194164
{
195-
error = $"The WebGPU device could not map the readback buffer. Status: '{mapStatus}'.";
196-
return false;
165+
throw new InvalidOperationException($"The WebGPU device could not map the readback buffer. Status: '{mapStatus}'.");
197166
}
198167

199168
void* mapped = api.BufferGetConstMappedRange(readbackBuffer, 0, (nuint)readbackByteCount);
200169
if (mapped is null)
201170
{
202171
api.BufferUnmap(readbackBuffer);
203-
error = "The WebGPU device mapped the readback buffer but returned no readable data.";
204-
return false;
172+
throw new InvalidOperationException("The WebGPU device mapped the readback buffer but returned no readable data.");
205173
}
206174

207175
try
@@ -218,8 +186,7 @@ void Callback(BufferMapAsyncStatus status, void* userData)
218186
.CopyTo(MemoryMarshal.AsBytes(destination.DangerousGetRowSpan(y)));
219187
}
220188

221-
error = null;
222-
return true;
189+
return;
223190
}
224191
finally
225192
{

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -217,14 +217,11 @@ private void FlushCompositionsFallback<TPixel>(
217217
Buffer2DRegion<TPixel> stagingRegion = new(stagingBuffer, targetBounds);
218218
ICanvasFrame<TPixel> stagingFrame = new MemoryCanvasFrame<TPixel>(stagingRegion);
219219

220-
if (!this.TryReadRegion(
221-
configuration,
222-
target,
223-
new Rectangle(0, 0, targetBounds.Width, targetBounds.Height),
224-
stagingRegion))
225-
{
226-
throw new NotSupportedException("WebGPU fallback requires readback from the native target.");
227-
}
220+
this.ReadRegion(
221+
configuration,
222+
target,
223+
new Rectangle(0, 0, targetBounds.Width, targetBounds.Height),
224+
stagingRegion);
228225

229226
this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositionScene);
230227

src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static class WebGPUNativeSurfaceFactory
3030
/// <returns>A configured <see cref="NativeSurface"/> instance.</returns>
3131
/// <remarks>
3232
/// These handles must originate from the same process WebGPU runtime used by ImageSharp.Drawing.WebGPU.
33-
/// The target texture must support render attachment and texture binding usage.
33+
/// The target texture must support render attachment, copy source, copy destination, and texture binding usage.
3434
/// </remarks>
3535
public static NativeSurface Create<TPixel>(
3636
nint deviceHandle,

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,11 @@ public void ReadbackInto(Image<TPixel> destination)
199199
nameof(destination));
200200
}
201201

202-
if (!this.Graphics.Backend.TryReadRegion(
203-
this.Graphics.Configuration,
204-
this.NativeFrame,
205-
new Rectangle(0, 0, this.Width, this.Height),
206-
new Buffer2DRegion<TPixel>(destination.Frames.RootFrame.PixelBuffer),
207-
out string? error))
208-
{
209-
throw new InvalidOperationException(error);
210-
}
202+
this.Graphics.Backend.ReadRegion(
203+
this.Graphics.Configuration,
204+
this.NativeFrame,
205+
new Rectangle(0, 0, this.Width, this.Height),
206+
new Buffer2DRegion<TPixel>(destination.Frames.RootFrame.PixelBuffer));
211207
}
212208

213209
/// <summary>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ private void ConfigureSurfaceCore(WebGPUPresentMode presentMode, Size framebuffe
246246

247247
SurfaceConfiguration surfaceConfiguration = new()
248248
{
249-
Usage = TextureUsage.RenderAttachment | TextureUsage.CopyDst | TextureUsage.TextureBinding,
249+
Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding,
250250
Format = WebGPUTextureFormatMapper.ToSilk(this.Format),
251251
PresentMode = (SilkPresentMode)(int)presentMode,
252252
Width = (uint)framebufferSize.Width,

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ public static void ComposeLayer<TPixel>(
430430
}
431431

432432
/// <inheritdoc />
433-
public bool TryReadRegion<TPixel>(
433+
public void ReadRegion<TPixel>(
434434
Configuration configuration,
435435
ICanvasFrame<TPixel> target,
436436
Rectangle sourceRectangle,
@@ -443,7 +443,7 @@ public bool TryReadRegion<TPixel>(
443443
// CPU backend readback is available only when the target exposes CPU pixels.
444444
if (!target.TryGetCpuRegion(out Buffer2DRegion<TPixel> sourceRegion))
445445
{
446-
return false;
446+
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for readback.");
447447
}
448448

449449
// Clamp the request to the target region to avoid out-of-range row slicing.
@@ -453,18 +453,17 @@ public bool TryReadRegion<TPixel>(
453453

454454
if (clipped.Width <= 0 || clipped.Height <= 0)
455455
{
456-
return false;
456+
throw new ArgumentException("The requested readback rectangle does not intersect the target bounds.", nameof(sourceRectangle));
457457
}
458458

459459
int copyWidth = Math.Min(clipped.Width, destination.Width);
460460
int copyHeight = Math.Min(clipped.Height, destination.Height);
461+
461462
for (int y = 0; y < copyHeight; y++)
462463
{
463464
sourceRegion.DangerousGetRowSpan(clipped.Y + y)
464465
.Slice(clipped.X, copyWidth)
465466
.CopyTo(destination.DangerousGetRowSpan(y));
466467
}
467-
468-
return true;
469468
}
470469
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public void FlushCompositions<TPixel>(
2929
where TPixel : unmanaged, IPixel<TPixel>;
3030

3131
/// <summary>
32-
/// Attempts to read source pixels from the target into a caller-provided buffer.
32+
/// Reads source pixels from the target into a caller-provided buffer.
3333
/// </summary>
3434
/// <typeparam name="TPixel">The pixel format.</typeparam>
3535
/// <param name="configuration">The active processing configuration.</param>
@@ -39,8 +39,7 @@ public void FlushCompositions<TPixel>(
3939
/// The caller-allocated region to receive the pixel data.
4040
/// Must be at least as large as <paramref name="sourceRectangle"/> (clamped to target bounds).
4141
/// </param>
42-
/// <returns><see langword="true"/> when readback succeeds; otherwise <see langword="false"/>.</returns>
43-
public bool TryReadRegion<TPixel>(
42+
public void ReadRegion<TPixel>(
4443
Configuration configuration,
4544
ICanvasFrame<TPixel> target,
4645
Rectangle sourceRectangle,

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

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Diagnostics.CodeAnalysis;
54
using System.Numerics;
65
using SixLabors.Fonts;
76
using SixLabors.Fonts.Rendering;
@@ -352,11 +351,7 @@ public void Apply(IPath path, Action<IImageProcessingContext> operation)
352351
return;
353352
}
354353

355-
if (!this.TryCreateProcessSourceImage(sourceRect, out Image<TPixel>? sourceImage))
356-
{
357-
throw new NotSupportedException("Canvas process operations require access to the current target pixels.");
358-
}
359-
354+
Image<TPixel> sourceImage = this.CreateProcessSourceImage(sourceRect);
360355
sourceImage.Mutate(operation);
361356

362357
Point brushOffset = new(
@@ -1009,26 +1004,30 @@ private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList<IPa
10091004
}
10101005

10111006
/// <summary>
1012-
/// Attempts to create a source image for process-in-path operations.
1007+
/// Creates a source image for process-in-path operations.
10131008
/// </summary>
10141009
/// <param name="sourceRect">Source rectangle in local canvas coordinates.</param>
1015-
/// <param name="sourceImage">The readback image when available.</param>
1016-
/// <returns><see langword="true"/> when source pixels were resolved.</returns>
1017-
private bool TryCreateProcessSourceImage(Rectangle sourceRect, [NotNullWhen(true)] out Image<TPixel>? sourceImage)
1010+
/// <returns>The readback image.</returns>
1011+
private Image<TPixel> CreateProcessSourceImage(Rectangle sourceRect)
10181012
{
1019-
sourceImage = new Image<TPixel>(this.configuration, sourceRect.Width, sourceRect.Height);
1020-
if (!this.backend.TryReadRegion(
1013+
Image<TPixel> sourceImage = new(this.configuration, sourceRect.Width, sourceRect.Height);
1014+
1015+
try
1016+
{
1017+
this.backend.ReadRegion(
10211018
this.configuration,
10221019
this.targetFrame,
10231020
sourceRect,
1024-
new Buffer2DRegion<TPixel>(sourceImage.Frames.RootFrame.PixelBuffer)))
1021+
new Buffer2DRegion<TPixel>(sourceImage.Frames.RootFrame.PixelBuffer));
1022+
1023+
return sourceImage;
1024+
}
1025+
catch
10251026
{
1027+
// Ownership only transfers to the caller after readback succeeds.
10261028
sourceImage.Dispose();
1027-
sourceImage = null;
1028-
return false;
1029+
throw;
10291030
}
1030-
1031-
return true;
10321031
}
10331032

10341033
/// <inheritdoc />

tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDeviceContextTests.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,10 @@ public void Dispose_Context_DoesNotReleaseOwnedTargetHandles()
8888
using WebGPUDrawingBackend backend = new();
8989
using Image<Rgba32> image = new(12, 10);
9090

91-
Assert.True(
92-
backend.TryReadRegion(
93-
Configuration.Default,
94-
target.NativeFrame,
95-
target.Bounds,
96-
new Buffer2DRegion<Rgba32>(image.Frames.RootFrame.PixelBuffer),
97-
out string readError),
98-
readError);
91+
backend.ReadRegion(
92+
Configuration.Default,
93+
target.NativeFrame,
94+
target.Bounds,
95+
new Buffer2DRegion<Rgba32>(image.Frames.RootFrame.PixelBuffer));
9996
}
10097
}

0 commit comments

Comments
 (0)