Skip to content

Commit 920b798

Browse files
Replace TryReadback API with Readback methods, remove Shutdown
1 parent 1290892 commit 920b798

File tree

5 files changed

+77
-91
lines changed

5 files changed

+77
-91
lines changed

samples/DrawingBackendBenchmark/WebGpuBenchmarkBackend.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,17 @@ public BenchmarkRenderResult Render(ReadOnlySpan<VisualLine> lines, int width, i
5252

5353
Image<Bgra32>? preview = null;
5454
string? readbackError = null;
55-
if (capturePreview && !renderTarget.TryReadback(out preview, out readbackError))
55+
if (capturePreview)
5656
{
57-
preview = null;
57+
try
58+
{
59+
preview = renderTarget.Readback();
60+
}
61+
catch (Exception ex)
62+
{
63+
preview = null;
64+
readbackError = ex.Message;
65+
}
5866
}
5967

6068
return new BenchmarkRenderResult(

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ public bool TryReadRegion<TPixel>(
6262
capability.Queue == 0 ||
6363
capability.TargetTexture == 0)
6464
{
65-
error = "Readback is only available for native WebGPU targets with valid device, queue, and texture handles.";
65+
error = "The target does not expose a native WebGPU surface with valid device, queue, and texture handles for readback.";
6666
return false;
6767
}
6868

6969
if (!TryGetCompositeTextureFormat<TPixel>(out WebGPUTextureFormatId expectedFormat, out FeatureName requiredFeature) ||
7070
expectedFormat != capability.TargetFormat)
7171
{
72-
error = $"Pixel type '{typeof(TPixel).Name}' is not compatible with target format '{capability.TargetFormat}'.";
72+
error = $"Pixel type '{typeof(TPixel).Name}' cannot be read back from target format '{capability.TargetFormat}'.";
7373
return false;
7474
}
7575

@@ -85,7 +85,7 @@ public bool TryReadRegion<TPixel>(
8585

8686
if (source.Width <= 0 || source.Height <= 0)
8787
{
88-
error = "The requested source rectangle does not intersect the target bounds.";
88+
error = "The requested readback rectangle does not intersect the target bounds.";
8989
return false;
9090
}
9191

@@ -96,7 +96,7 @@ public bool TryReadRegion<TPixel>(
9696
if (requiredFeature != FeatureName.Undefined
9797
&& !WebGPURuntime.GetOrCreateDeviceState(api, device).HasFeature(requiredFeature))
9898
{
99-
error = $"The target device does not support required feature '{requiredFeature}' for pixel type '{typeof(TPixel).Name}'.";
99+
error = $"The target device does not support WebGPU feature '{requiredFeature}' required to read back pixel type '{typeof(TPixel).Name}'.";
100100
return false;
101101
}
102102

@@ -124,15 +124,15 @@ public bool TryReadRegion<TPixel>(
124124
readbackBuffer = api.DeviceCreateBuffer(device, in bufferDescriptor);
125125
if (readbackBuffer is null)
126126
{
127-
error = "WebGPU.DeviceCreateBuffer returned null for readback.";
127+
error = "The WebGPU device could not create a readback buffer.";
128128
return false;
129129
}
130130

131131
CommandEncoderDescriptor encoderDescriptor = default;
132132
commandEncoder = api.DeviceCreateCommandEncoder(device, in encoderDescriptor);
133133
if (commandEncoder is null)
134134
{
135-
error = "WebGPU.DeviceCreateCommandEncoder returned null.";
135+
error = "The WebGPU device could not create a command encoder for readback.";
136136
return false;
137137
}
138138

@@ -163,7 +163,7 @@ public bool TryReadRegion<TPixel>(
163163
commandBuffer = api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor);
164164
if (commandBuffer is null)
165165
{
166-
error = "WebGPU.CommandEncoderFinish returned null.";
166+
error = "The WebGPU device could not finalize the readback command buffer.";
167167
return false;
168168
}
169169

@@ -187,15 +187,15 @@ void Callback(BufferMapAsyncStatus status, void* userData)
187187
api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)readbackByteCount, callback, null);
188188
if (!WaitForMapSignal(wgpuExtension, device, mapReady) || mapStatus != BufferMapAsyncStatus.Success)
189189
{
190-
error = $"WebGPU readback map failed with status '{mapStatus}'.";
190+
error = $"The WebGPU device could not map the readback buffer. Status: '{mapStatus}'.";
191191
return false;
192192
}
193193

194194
void* mapped = api.BufferGetConstMappedRange(readbackBuffer, 0, (nuint)readbackByteCount);
195195
if (mapped is null)
196196
{
197197
api.BufferUnmap(readbackBuffer);
198-
error = "WebGPU.BufferGetConstMappedRange returned null.";
198+
error = "The WebGPU device mapped the readback buffer but returned no readable data.";
199199
return false;
200200
}
201201

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

Lines changed: 22 additions & 53 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 Silk.NET.WebGPU;
65
using SixLabors.ImageSharp.Memory;
76
using SixLabors.ImageSharp.PixelFormats;
@@ -11,7 +10,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
1110
/// <summary>
1211
/// An offscreen WebGPU render target.
1312
/// Use this type when you want to render to a GPU-backed target and optionally read the result back with
14-
/// <see cref="TryReadback"/> or <see cref="TryReadbackInto(Image{TPixel}, out string?)"/>.
13+
/// <see cref="Readback"/> or <see cref="ReadbackInto(Image{TPixel})"/>.
1514
/// </summary>
1615
/// <typeparam name="TPixel">The canvas pixel format.</typeparam>
1716
public sealed class WebGPURenderTarget<TPixel> : IDisposable
@@ -185,52 +184,36 @@ public DrawingCanvas<TPixel> CreateCanvas(DrawingOptions options)
185184
}
186185

187186
/// <summary>
188-
/// Attempts to read the current GPU texture contents back into a new CPU image.
187+
/// Reads the current GPU texture contents back into a new CPU image.
189188
/// </summary>
190-
/// <param name="image">Receives the readback image on success.</param>
191-
/// <param name="error">Receives the failure reason when readback cannot complete.</param>
192-
/// <returns><see langword="true"/> when readback succeeds; otherwise <see langword="false"/>.</returns>
193-
public bool TryReadback([NotNullWhen(true)] out Image<TPixel>? image, [NotNullWhen(false)] out string? error)
189+
/// <returns>The readback image.</returns>
190+
public Image<TPixel> Readback()
194191
{
195-
if (this.isDisposed)
196-
{
197-
image = null;
198-
error = "Render target is disposed.";
199-
return false;
200-
}
192+
this.ThrowIfDisposed();
193+
this.Graphics.ThrowIfDisposed();
201194

195+
Image<TPixel> image = new(this.Width, this.Height);
202196
try
203197
{
204-
this.Graphics.ThrowIfDisposed();
205-
}
206-
catch (ObjectDisposedException ex)
207-
{
208-
image = null;
209-
error = ex.Message;
210-
return false;
198+
this.ReadbackInto(image);
199+
return image;
211200
}
212-
213-
image = new Image<TPixel>(this.Width, this.Height);
214-
if (!this.TryReadbackInto(image, out error))
201+
catch
215202
{
216203
image.Dispose();
217-
image = null;
218-
return false;
204+
throw;
219205
}
220-
221-
error = null;
222-
return true;
223206
}
224207

225208
/// <summary>
226-
/// Attempts to read the current GPU texture contents back into an existing CPU image.
209+
/// Reads the current GPU texture contents back into an existing CPU image.
227210
/// </summary>
228211
/// <param name="destination">The destination image that receives the readback pixels.</param>
229-
/// <param name="error">Receives the failure reason when readback cannot complete.</param>
230-
/// <returns><see langword="true"/> when readback succeeds; otherwise <see langword="false"/>.</returns>
231-
public bool TryReadbackInto(Image<TPixel> destination, [NotNullWhen(false)] out string? error)
212+
public void ReadbackInto(Image<TPixel> destination)
232213
{
233214
Guard.NotNull(destination, nameof(destination));
215+
this.ThrowIfDisposed();
216+
this.Graphics.ThrowIfDisposed();
234217

235218
if (destination.Width != this.Width || destination.Height != this.Height)
236219
{
@@ -239,35 +222,21 @@ public bool TryReadbackInto(Image<TPixel> destination, [NotNullWhen(false)] out
239222
nameof(destination));
240223
}
241224

242-
if (this.isDisposed)
243-
{
244-
error = "Render target is disposed.";
245-
return false;
246-
}
247-
248-
try
249-
{
250-
this.Graphics.ThrowIfDisposed();
251-
}
252-
catch (ObjectDisposedException ex)
253-
{
254-
error = ex.Message;
255-
return false;
256-
}
257-
258225
Buffer2DRegion<TPixel> region = new(destination.Frames.RootFrame.PixelBuffer, destination.Bounds);
259226
if (!this.Graphics.Backend.TryReadRegion(
260227
this.Graphics.Configuration,
261228
this.NativeFrame,
262229
new Rectangle(0, 0, this.Width, this.Height),
263230
region,
264-
out error))
231+
out string? error))
265232
{
266-
return false;
267-
}
233+
if (error is null)
234+
{
235+
throw new InvalidOperationException("The WebGPU render target readback failed without reporting a reason.");
236+
}
268237

269-
error = null;
270-
return true;
238+
throw new InvalidOperationException(error);
239+
}
271240
}
272241

273242
/// <summary>

src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
2020
/// <see cref="TryGetOrCreateDevice"/> to use the cached default device/queue pair.
2121
/// </para>
2222
/// <para>
23-
/// Runtime unload is explicit:
23+
/// Runtime cleanup happens automatically on process exit.
2424
/// </para>
25-
/// <list type="bullet">
26-
/// <item><description><see cref="Shutdown"/> for explicit teardown.</description></item>
27-
/// <item><description>Best-effort cleanup on process exit.</description></item>
28-
/// </list>
2925
/// </remarks>
3026
internal static unsafe partial class WebGPURuntime
3127
{
@@ -366,21 +362,6 @@ internal static int ProbeComputePipelineSupport()
366362
}
367363
}
368364

369-
/// <summary>
370-
/// Shuts down the process-level WebGPU runtime.
371-
/// </summary>
372-
/// <remarks>
373-
/// This call is intended for coordinated application shutdown. Runtime state can be
374-
/// reinitialized later by calling <see cref="GetApi"/> again.
375-
/// </remarks>
376-
public static void Shutdown()
377-
{
378-
lock (Sync)
379-
{
380-
DisposeRuntimeCore();
381-
}
382-
}
383-
384365
/// <summary>
385366
/// Process-exit cleanup callback.
386367
/// </summary>
@@ -390,7 +371,11 @@ private static void OnProcessExit(object? sender, EventArgs e)
390371
{
391372
_ = sender;
392373
_ = e;
393-
Shutdown();
374+
375+
lock (Sync)
376+
{
377+
DisposeRuntimeCore();
378+
}
394379
}
395380

396381
private static void DisposeRuntimeCore()
@@ -421,10 +406,35 @@ private static void DisposeRuntimeCore()
421406
computePipelineProbeError = null;
422407
}
423408

424-
wgpuExtension?.Dispose();
425-
wgpuExtension = null;
426-
api?.Dispose();
427-
api = null;
409+
// By the time process-exit teardown reaches the shared loader wrappers, the runtime may
410+
// already be unwinding native loader state underneath Silk. The cached device/queue and
411+
// all runtime-owned GPU state have already been released above, so these dispose failures
412+
// no longer represent leaked WebGPU objects; they only mean the loader is already torn
413+
// down or no longer in a state where Silk can unload it cleanly. We must still null the
414+
// references so any later re-entry in the same process cannot observe stale wrappers.
415+
try
416+
{
417+
wgpuExtension?.Dispose();
418+
}
419+
catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException)
420+
{
421+
}
422+
finally
423+
{
424+
wgpuExtension = null;
425+
}
426+
427+
try
428+
{
429+
api?.Dispose();
430+
}
431+
catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException)
432+
{
433+
}
434+
finally
435+
{
436+
api = null;
437+
}
428438
}
429439

430440
private static void EnsureInitialized()

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ public void FillParis_ImageSharp_WebGPU()
6363
}
6464

6565
canvas.Flush();
66-
Assert.True(target.TryReadback(out Image<Rgba32> readback, out string error), error);
67-
using Image<Rgba32> readbackImage = readback!;
66+
using Image<Rgba32> readbackImage = target.Readback();
6867
Assert.True(ContainsNonDefaultPixel(readbackImage));
6968
}
7069

0 commit comments

Comments
 (0)