Skip to content

Commit e48c02c

Browse files
Refactor WebGPU handles and render targets
1 parent 8ebefc2 commit e48c02c

File tree

8 files changed

+81
-220
lines changed

8 files changed

+81
-220
lines changed

src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -529,10 +529,10 @@ private bool InitializeNativeTarget()
529529
this.queueReference = this.queueHandle.AcquireReference();
530530
this.targetTextureReference = this.targetTextureHandle.AcquireReference();
531531
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;
532+
this.Device = (Device*)this.deviceReference.Value.Handle;
533+
this.Queue = (Queue*)this.queueReference.Value.Handle;
534+
this.TargetTexture = (Texture*)this.targetTextureReference.Value.Handle;
535+
this.TargetView = (TextureView*)this.targetTextureViewReference.Value.Handle;
536536
this.ownsTargetTexture = false;
537537
this.ownsTargetView = false;
538538
return true;

src/ImageSharp.Drawing.WebGPU/WebGPUHandle.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,11 @@ internal HandleReference AcquireReference()
7777
/// <remarks>
7878
/// This token exists so callers can use a normal <c>using</c> scope around raw WebGPU pointers
7979
/// without carrying a separate success flag beside the handle. Disposing it releases the
80-
/// reference acquired by <see cref="AcquireReference"/>.
80+
/// reference acquired by <see cref="AcquireReference"/>. Modeled after
81+
/// <see cref="System.Buffers.MemoryHandle"/>: a lightweight scoped value that should not be
82+
/// copied or stored beyond the immediate native-call scope.
8183
/// </remarks>
82-
internal sealed class HandleReference : IDisposable
84+
internal struct HandleReference : IDisposable
8385
{
8486
private WebGPUHandle? owner;
8587

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

Lines changed: 54 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,6 @@ public sealed class WebGPURenderTarget<TPixel> : IDisposable
1919
private readonly bool ownsGraphics;
2020
private bool isDisposed;
2121

22-
private WebGPURenderTarget(
23-
WebGPUDeviceContext<TPixel> graphics,
24-
bool ownsGraphics,
25-
WebGPUTextureHandle textureHandle,
26-
WebGPUTextureViewHandle textureViewHandle,
27-
NativeSurface surface,
28-
WebGPUTextureFormatId format,
29-
Rectangle bounds)
30-
{
31-
this.Graphics = graphics;
32-
this.ownsGraphics = ownsGraphics;
33-
this.TextureHandle = textureHandle;
34-
this.TextureViewHandle = textureViewHandle;
35-
this.Surface = surface;
36-
this.Format = format;
37-
this.Bounds = bounds;
38-
this.NativeFrame = new NativeCanvasFrame<TPixel>(bounds, surface);
39-
}
40-
4122
/// <summary>
4223
/// Initializes a new instance of the <see cref="WebGPURenderTarget{TPixel}"/> class using the shared process-level device.
4324
/// </summary>
@@ -55,31 +36,57 @@ public WebGPURenderTarget(int width, int height)
5536
/// <param name="width">The target width in pixels.</param>
5637
/// <param name="height">The target height in pixels.</param>
5738
public WebGPURenderTarget(Configuration configuration, int width, int height)
58-
: this(AllocateOwnedTarget(configuration, width, height))
39+
: this(new WebGPUDeviceContext<TPixel>(configuration), ownsGraphics: true, width, height)
5940
{
6041
}
6142

62-
private WebGPURenderTarget(OwnedTarget ownedTarget)
63-
: this(
64-
ownedTarget.Graphics,
65-
ownsGraphics: true,
66-
ownedTarget.TextureHandle,
67-
ownedTarget.TextureViewHandle,
68-
ownedTarget.Surface,
69-
ownedTarget.Format,
70-
ownedTarget.Bounds)
43+
private WebGPURenderTarget(WebGPUDeviceContext<TPixel> graphics, bool ownsGraphics, int width, int height)
7144
{
72-
}
45+
this.Graphics = graphics;
46+
this.ownsGraphics = ownsGraphics;
7347

74-
/// <summary>
75-
/// Gets the owned wrapped texture handle behind this render target.
76-
/// </summary>
77-
internal WebGPUTextureHandle TextureHandle { get; }
48+
try
49+
{
50+
graphics.ThrowIfDisposed();
7851

79-
/// <summary>
80-
/// Gets the owned wrapped texture-view handle bound when this render target is used as a native surface.
81-
/// </summary>
82-
internal WebGPUTextureViewHandle TextureViewHandle { get; }
52+
WebGPU api = WebGPURuntime.GetApi();
53+
if (!WebGPURenderTargetAllocation.TryCreateRenderTarget<TPixel>(
54+
api,
55+
graphics.DeviceHandle,
56+
graphics.QueueHandle,
57+
width,
58+
height,
59+
out NativeSurface surface,
60+
out WebGPUTextureHandle? textureHandle,
61+
out WebGPUTextureViewHandle? textureViewHandle,
62+
out WebGPUTextureFormatId format,
63+
out string allocationError))
64+
{
65+
throw new InvalidOperationException(allocationError);
66+
}
67+
68+
if (textureHandle is null || textureViewHandle is null)
69+
{
70+
throw new InvalidOperationException("WebGPU render-target allocation succeeded without returning both owned texture handles.");
71+
}
72+
73+
this.TextureHandle = textureHandle;
74+
this.TextureViewHandle = textureViewHandle;
75+
this.Surface = surface;
76+
this.Format = format;
77+
this.Bounds = new Rectangle(0, 0, width, height);
78+
this.NativeFrame = new NativeCanvasFrame<TPixel>(this.Bounds, surface);
79+
}
80+
catch
81+
{
82+
if (ownsGraphics)
83+
{
84+
graphics.Dispose();
85+
}
86+
87+
throw;
88+
}
89+
}
8390

8491
/// <summary>
8592
/// Gets the graphics device context used by this target.
@@ -116,48 +123,15 @@ private WebGPURenderTarget(OwnedTarget ownedTarget)
116123
/// </summary>
117124
public WebGPUTextureFormatId Format { get; }
118125

119-
private static OwnedTarget AllocateOwnedTarget(Configuration configuration, int width, int height)
120-
{
121-
WebGPUDeviceContext<TPixel> graphics = new(configuration);
122-
try
123-
{
124-
WebGPU api = WebGPURuntime.GetApi();
125-
if (!WebGPURenderTargetAllocation.TryCreateRenderTarget<TPixel>(
126-
api,
127-
graphics.DeviceHandle,
128-
graphics.QueueHandle,
129-
width,
130-
height,
131-
out NativeSurface surface,
132-
out WebGPUTextureHandle? textureHandle,
133-
out WebGPUTextureViewHandle? textureViewHandle,
134-
out WebGPUTextureFormatId format,
135-
out string allocationError))
136-
{
137-
graphics.Dispose();
138-
throw new InvalidOperationException(allocationError);
139-
}
140-
141-
if (textureHandle is null || textureViewHandle is null)
142-
{
143-
graphics.Dispose();
144-
throw new InvalidOperationException("WebGPU render-target allocation succeeded without returning both owned texture handles.");
145-
}
126+
/// <summary>
127+
/// Gets the owned wrapped texture handle behind this render target.
128+
/// </summary>
129+
internal WebGPUTextureHandle TextureHandle { get; }
146130

147-
return new OwnedTarget(
148-
graphics,
149-
textureHandle,
150-
textureViewHandle,
151-
surface,
152-
format,
153-
new Rectangle(0, 0, width, height));
154-
}
155-
catch
156-
{
157-
graphics.Dispose();
158-
throw;
159-
}
160-
}
131+
/// <summary>
132+
/// Gets the owned wrapped texture-view handle bound when this render target is used as a native surface.
133+
/// </summary>
134+
internal WebGPUTextureViewHandle TextureViewHandle { get; }
161135

162136
/// <summary>
163137
/// Creates a native-only frame over this render target.
@@ -274,81 +248,8 @@ public void Dispose()
274248
/// <param name="height">The target height in pixels.</param>
275249
/// <returns>The created render target.</returns>
276250
internal static WebGPURenderTarget<TPixel> CreateFromContext(WebGPUDeviceContext<TPixel> graphics, int width, int height)
277-
=> CreateCore(graphics, ownsGraphics: false, width, height);
278-
279-
private static WebGPURenderTarget<TPixel> CreateCore(
280-
WebGPUDeviceContext<TPixel> graphics,
281-
bool ownsGraphics,
282-
int width,
283-
int height)
284-
{
285-
graphics.ThrowIfDisposed();
286-
287-
WebGPU api = WebGPURuntime.GetApi();
288-
if (!WebGPURenderTargetAllocation.TryCreateRenderTarget<TPixel>(
289-
api,
290-
graphics.DeviceHandle,
291-
graphics.QueueHandle,
292-
width,
293-
height,
294-
out NativeSurface surface,
295-
out WebGPUTextureHandle? textureHandle,
296-
out WebGPUTextureViewHandle? textureViewHandle,
297-
out WebGPUTextureFormatId format,
298-
out string error))
299-
{
300-
throw new InvalidOperationException(error);
301-
}
251+
=> new(graphics, ownsGraphics: false, width, height);
302252

303-
if (textureHandle is null || textureViewHandle is null)
304-
{
305-
throw new InvalidOperationException("WebGPU render-target allocation succeeded without returning both owned texture handles.");
306-
}
307-
308-
return new WebGPURenderTarget<TPixel>(
309-
graphics,
310-
ownsGraphics,
311-
textureHandle,
312-
textureViewHandle,
313-
surface,
314-
format,
315-
new Rectangle(0, 0, width, height));
316-
}
317-
318-
/// <summary>
319-
/// Throws when this render target is disposed.
320-
/// </summary>
321253
private void ThrowIfDisposed()
322254
=> ObjectDisposedException.ThrowIf(this.isDisposed, this);
323-
324-
private sealed class OwnedTarget
325-
{
326-
public OwnedTarget(
327-
WebGPUDeviceContext<TPixel> graphics,
328-
WebGPUTextureHandle textureHandle,
329-
WebGPUTextureViewHandle textureViewHandle,
330-
NativeSurface surface,
331-
WebGPUTextureFormatId format,
332-
Rectangle bounds)
333-
{
334-
this.Graphics = graphics;
335-
this.TextureHandle = textureHandle;
336-
this.TextureViewHandle = textureViewHandle;
337-
this.Surface = surface;
338-
this.Format = format;
339-
this.Bounds = bounds;
340-
}
341-
342-
public WebGPUDeviceContext<TPixel> Graphics { get; }
343-
344-
public WebGPUTextureHandle TextureHandle { get; }
345-
346-
public WebGPUTextureViewHandle TextureViewHandle { get; }
347-
348-
public NativeSurface Surface { get; }
349-
350-
public WebGPUTextureFormatId Format { get; }
351-
352-
public Rectangle Bounds { get; }
353-
}
354255
}

src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ internal sealed class DeviceSharedState : IDisposable
5858
private readonly ConcurrentDictionary<string, CompositeComputePipelineInfrastructure> compositeComputePipelines = new(StringComparer.Ordinal);
5959
private readonly ConcurrentDictionary<string, SharedBufferInfrastructure> sharedBuffers = new(StringComparer.Ordinal);
6060
private readonly HashSet<FeatureName> deviceFeatures;
61-
private readonly WebGPUHandle.HandleReference deviceReference;
61+
private WebGPUHandle.HandleReference deviceReference;
6262
private bool disposed;
6363

6464
internal DeviceSharedState(WebGPU api, WebGPUDeviceHandle deviceHandle)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public sealed unsafe class WebGPUWindowFrame<TPixel> : IDisposable
1515
where TPixel : unmanaged, IPixel<TPixel>
1616
{
1717
private readonly WebGPU api;
18-
private readonly WebGPUHandle.HandleReference surfaceReference;
18+
private WebGPUHandle.HandleReference surfaceReference;
1919
private readonly WebGPUTextureHandle textureHandle;
2020
private readonly WebGPUTextureViewHandle textureViewHandle;
2121
private bool isDisposed;

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

Lines changed: 7 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -51,25 +51,16 @@ public WebGPUWindow(WebGPUWindowOptions options)
5151
/// <param name="configuration">The configuration instance to bind to the created backend.</param>
5252
/// <param name="options">The window creation options.</param>
5353
public WebGPUWindow(Configuration configuration, WebGPUWindowOptions options)
54-
: this(CreateConstruction(configuration, options))
5554
{
56-
}
57-
58-
private WebGPUWindow(WindowConstruction construction)
59-
: this(construction.Window, construction.Configuration, construction.Format, construction.PresentMode)
60-
{
61-
}
55+
if (!WebGPUDrawingBackend.TryGetCompositeTextureFormat<TPixel>(out WebGPUTextureFormatId expectedFormat))
56+
{
57+
throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' is not supported by the WebGPU backend.");
58+
}
6259

63-
private WebGPUWindow(
64-
IWindow window,
65-
Configuration configuration,
66-
WebGPUTextureFormatId format,
67-
WebGPUPresentMode presentMode)
68-
{
69-
this.window = window;
60+
this.window = Window.Create(CreateSilkOptions(options));
7061
this.Configuration = configuration;
71-
this.Format = format;
72-
this.presentMode = presentMode;
62+
this.Format = expectedFormat;
63+
this.presentMode = options.PresentMode;
7364

7465
try
7566
{
@@ -588,17 +579,6 @@ private WindowResources CreateResources()
588579
}
589580
}
590581

591-
private static WindowConstruction CreateConstruction(Configuration configuration, WebGPUWindowOptions options)
592-
{
593-
if (!WebGPUDrawingBackend.TryGetCompositeTextureFormat<TPixel>(out WebGPUTextureFormatId expectedFormat))
594-
{
595-
throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' is not supported by the WebGPU backend.");
596-
}
597-
598-
IWindow window = Window.Create(CreateSilkOptions(options));
599-
return new WindowConstruction(window, configuration, expectedFormat, options.PresentMode);
600-
}
601-
602582
private bool TryAcquireFrameCore(
603583
TimeSpan deltaTime,
604584
[NotNullWhen(true)] out WebGPUWindowFrame<TPixel>? frame,
@@ -919,27 +899,4 @@ public void Dispose()
919899
this.InstanceHandle.Dispose();
920900
}
921901
}
922-
923-
private sealed class WindowConstruction
924-
{
925-
public WindowConstruction(
926-
IWindow window,
927-
Configuration configuration,
928-
WebGPUTextureFormatId format,
929-
WebGPUPresentMode presentMode)
930-
{
931-
this.Window = window;
932-
this.Configuration = configuration;
933-
this.Format = format;
934-
this.PresentMode = presentMode;
935-
}
936-
937-
public IWindow Window { get; }
938-
939-
public Configuration Configuration { get; }
940-
941-
public WebGPUTextureFormatId Format { get; }
942-
943-
public WebGPUPresentMode PresentMode { get; }
944-
}
945902
}

src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ The command remains relatively close to the original user request. It may hold t
7676

7777
### Preparation
7878

79-
Preparation is the flush-time normalization step. In code this lives in `CompositionCommand.Prepare(...)` and the surrounding `CompositionCommandPreparer`.
79+
Preparation is the flush-time normalization step that turns recorded intent into backend-ready geometry and metadata. It is split across two stages:
8080

81-
Preparation turns recorded intent into backend-ready geometry and metadata. That is where path transforms are applied, strokes become outlines, clip paths are applied, prepared geometry is built or reused, and raster interest is recomputed.
81+
1. `DrawingCanvasBatcher<TPixel>.PrepareCommands(...)` runs first and only when needed. It applies command transforms, expands strokes to fill paths, and applies clip paths so that clipped commands reach the backend as ordinary fills. It also expands dashed strokes when a stroke pattern is present.
82+
2. The CPU backend then calls `FlushScene.Create(...)`, which lowers each command into a row-oriented retained representation through `TryPrepareFillPath` / `TryPrepareStrokePath`. That is where prepared rasterizable geometry is built, brushes are bound to the prepared coordinate space, and bounds and raster interest are recomputed.
8283

8384
Preparation is where the architecture moves from "what the caller asked for" to "what the backend can execute".
8485

@@ -363,12 +364,11 @@ If you want to move from the architecture into the code, this is the best order.
363364
2. `DrawingCanvasExtensions.cs`
364365
3. `DrawingCanvasBatcher{TPixel}.cs`
365366
4. `CompositionCommand.cs`
366-
5. `CompositionCommandPreparer.cs`
367-
6. `DefaultDrawingBackend.cs`
368-
7. `FlushScene.cs`
369-
8. `WebGPUEnvironment.cs`
370-
9. `WebGPUWindow{TPixel}.cs`, `WebGPURenderTarget{TPixel}.cs`, and `WebGPUDeviceContext{TPixel}.cs`
371-
10. `WebGPUDrawingBackend` and its scene/dispatch types
367+
5. `DefaultDrawingBackend.cs`
368+
6. `FlushScene.cs`
369+
7. `WebGPUEnvironment.cs`
370+
8. `WebGPUWindow{TPixel}.cs`, `WebGPURenderTarget{TPixel}.cs`, and `WebGPUDeviceContext{TPixel}.cs`
371+
9. `WebGPUDrawingBackend` and its scene/dispatch types
372372

373373
That path follows the real runtime flow:
374374

0 commit comments

Comments
 (0)