Skip to content

Commit d5a87e2

Browse files
Refactor WebGPU native surface and chunking
1 parent 0386d47 commit d5a87e2

10 files changed

Lines changed: 226 additions & 170 deletions

File tree

src/ImageSharp.Drawing.WebGPU/WebGPUDeviceContext.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,15 +252,15 @@ internal DrawingCanvas CreateCanvas(
252252
int height)
253253
{
254254
Rectangle bounds = new(0, 0, width, height);
255-
NativeSurface surface = this.CreateSurface(textureHandle, textureViewHandle, format, width, height);
255+
WebGPUNativeSurface surface = this.CreateSurface(textureHandle, textureViewHandle, format, width, height);
256256

257257
return WebGPUCanvasFactory.CreateCanvas(this.Configuration, options, this.Backend, bounds, surface, format);
258258
}
259259

260260
/// <summary>
261261
/// Creates the wrapped native surface over the supplied texture handles.
262262
/// </summary>
263-
private NativeSurface CreateSurface(
263+
private WebGPUNativeSurface CreateSurface(
264264
WebGPUTextureHandle textureHandle,
265265
WebGPUTextureViewHandle textureViewHandle,
266266
WebGPUTextureFormat format,

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,12 @@ 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))
36+
if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || nativeSurface is not WebGPUNativeSurface nativeTarget)
3737
{
3838
throw new NotSupportedException("The target does not expose a native surface for readback.");
3939
}
4040

41-
WebGPUNativeTarget nativeTarget = nativeSurface.GetNativeTarget<WebGPUNativeTarget>();
42-
if (nativeTarget.DeviceHandle.IsInvalid ||
43-
nativeTarget.QueueHandle.IsInvalid ||
44-
nativeTarget.TargetTextureHandle.IsInvalid)
41+
if (nativeTarget.DeviceHandle.IsInvalid || nativeTarget.QueueHandle.IsInvalid || nativeTarget.TargetTextureHandle.IsInvalid)
4542
{
4643
throw new NotSupportedException("The target does not expose valid WebGPU device, queue, and texture handles for readback.");
4744
}

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ public sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisp
4242
private WebGPUSceneSchedulingArena? cachedSchedulingArena;
4343
private WebGPUSceneResourceArena? cachedResourceArena;
4444

45+
// Advisory first-guess state for repeated oversized eager scenes. Parallel renders may
46+
// race to update it; every hinted chunk is still validated before dispatch, so a stale
47+
// or cross-scene value can only affect the first shrink attempt, not correctness.
48+
private int chunkHintBinding;
49+
private int chunkHintTargetWidth;
50+
private int chunkHintTargetHeight;
51+
private int chunkHintTileHeight;
52+
4553
private WebGPUSceneDispatch.BindingLimitBuffer lastChunkingBindingFailure;
4654
private bool isDisposed;
4755

@@ -117,10 +125,10 @@ public void RenderScene<TPixel>(
117125
throw new NotSupportedException($"The WebGPU backend does not support pixel format '{typeof(TPixel).Name}'.");
118126
}
119127

120-
// RenderScene only accepts native frames on the WebGPU path; unwrap the surface once
121-
// so staging does not repeat the generic frame capability checks.
128+
// RenderScene only accepts WebGPU native frames on this path, so cast once at the backend
129+
// boundary and keep staging focused on dispatch data.
122130
_ = nativeTarget.TryGetNativeSurface(out NativeSurface? nativeSurface);
123-
WebGPUNativeTarget webGPUTarget = nativeSurface!.GetNativeTarget<WebGPUNativeTarget>();
131+
WebGPUNativeSurface webGPUTarget = (WebGPUNativeSurface)nativeSurface!;
124132
TextureFormat textureFormat = WebGPUTextureFormatMapper.ToNative(webGPUTarget.TargetFormat);
125133

126134
if (webGPUTarget.TargetFormat != formatId)
@@ -178,13 +186,24 @@ public void RenderScene<TPixel>(
178186
bool renderSucceeded = WebGPUSceneDispatch.TryRenderStagedScene(
179187
ref stagedScene,
180188
ref schedulingArena,
189+
this.GetChunkTileHeightHint(stagedScene.BindingLimitFailure.Buffer, encodedScene.TargetSize),
181190
out bool requiresGrowth,
182191
out WebGPUSceneBumpSizes grownBumpSizes,
192+
out uint successfulChunkTileHeight,
183193
out string? error);
184194

185195
if (renderSucceeded)
186196
{
187197
currentBumpSizes = MaxBumpSizes(currentBumpSizes, grownBumpSizes);
198+
199+
if (successfulChunkTileHeight != 0)
200+
{
201+
this.UpdateChunkTileHeightHint(
202+
stagedScene.BindingLimitFailure.Buffer,
203+
encodedScene.TargetSize,
204+
successfulChunkTileHeight);
205+
}
206+
188207
renderCompleted = true;
189208
break;
190209
}
@@ -237,6 +256,50 @@ private static WebGPUSceneBumpSizes MaxBumpSizes(
237256
Math.Max(left.BlendSpill, right.BlendSpill),
238257
Math.Max(left.Ptcl, right.Ptcl));
239258

259+
/// <summary>
260+
/// Gets the last successful chunk height for a matching oversized render.
261+
/// </summary>
262+
/// <param name="binding">The binding category that selected chunked rendering.</param>
263+
/// <param name="targetSize">The target size being rendered.</param>
264+
/// <returns>The advisory chunk height, or <c>0</c> when no matching hint exists.</returns>
265+
private uint GetChunkTileHeightHint(WebGPUSceneDispatch.BindingLimitBuffer binding, Size targetSize)
266+
{
267+
int tileHeight = Volatile.Read(ref this.chunkHintTileHeight);
268+
if (tileHeight <= 0)
269+
{
270+
return 0;
271+
}
272+
273+
if (Volatile.Read(ref this.chunkHintBinding) != (int)binding ||
274+
Volatile.Read(ref this.chunkHintTargetWidth) != targetSize.Width ||
275+
Volatile.Read(ref this.chunkHintTargetHeight) != targetSize.Height)
276+
{
277+
return 0;
278+
}
279+
280+
return unchecked((uint)tileHeight);
281+
}
282+
283+
/// <summary>
284+
/// Stores the last successful chunk height for later eager renders on this backend.
285+
/// </summary>
286+
/// <param name="binding">The binding category that selected chunked rendering.</param>
287+
/// <param name="targetSize">The target size being rendered.</param>
288+
/// <param name="tileHeight">The successful chunk height.</param>
289+
private void UpdateChunkTileHeightHint(
290+
WebGPUSceneDispatch.BindingLimitBuffer binding,
291+
Size targetSize,
292+
uint tileHeight)
293+
{
294+
Volatile.Write(ref this.chunkHintBinding, (int)binding);
295+
Volatile.Write(ref this.chunkHintTargetWidth, targetSize.Width);
296+
Volatile.Write(ref this.chunkHintTargetHeight, targetSize.Height);
297+
298+
// Publish the height last so readers never observe a new non-zero hint before
299+
// its binding and target-size key have been written.
300+
Volatile.Write(ref this.chunkHintTileHeight, unchecked((int)tileHeight));
301+
}
302+
240303
/// <summary>
241304
/// Rents the cached scene resource arena for a render, leaving the backend cache empty.
242305
/// </summary>

src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,10 @@ public static WebGPUFlushContext Create<TPixel>(
170170
MemoryAllocator memoryAllocator)
171171
where TPixel : unmanaged, IPixel<TPixel>
172172
{
173-
// The native-frame overload is used after WebGPU target selection has already
174-
// succeeded, so this unwraps the known native surface instead of probing support.
173+
// The native-frame overload is used after WebGPU target selection has already succeeded,
174+
// so this casts once at the backend boundary and keeps the concrete target data together.
175175
_ = frame.TryGetNativeSurface(out NativeSurface? nativeSurface);
176-
WebGPUNativeTarget nativeTarget = nativeSurface!.GetNativeTarget<WebGPUNativeTarget>();
176+
WebGPUNativeSurface nativeTarget = (WebGPUNativeSurface)nativeSurface!;
177177
WebGPU api = WebGPURuntime.GetApi();
178178
TextureFormat textureFormat = WebGPUTextureFormatMapper.ToNative(nativeTarget.TargetFormat);
179179
Rectangle bounds = frame.Bounds;

src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,77 @@
66
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
77

88
/// <summary>
9-
/// WebGPU native drawing target exposed through the backend-agnostic <see cref="NativeSurface"/> contract.
9+
/// WebGPU native drawing surface exposed through the backend-agnostic <see cref="NativeSurface"/> base type.
1010
/// </summary>
1111
internal sealed class WebGPUNativeSurface : NativeSurface
1212
{
13-
private readonly WebGPUNativeTarget target;
14-
1513
/// <summary>
1614
/// Initializes a new instance of the <see cref="WebGPUNativeSurface"/> class.
1715
/// </summary>
18-
/// <param name="target">The WebGPU target exposed by this surface.</param>
19-
internal WebGPUNativeSurface(WebGPUNativeTarget target)
20-
=> this.target = target;
16+
/// <param name="deviceHandle">The wrapped device handle that owns the target texture.</param>
17+
/// <param name="queueHandle">The wrapped queue handle used to submit work against the target texture.</param>
18+
/// <param name="targetTextureHandle">The wrapped target texture handle.</param>
19+
/// <param name="targetTextureViewHandle">The wrapped target texture-view handle.</param>
20+
/// <param name="targetFormat">The target texture format.</param>
21+
/// <param name="width">The surface width in pixels.</param>
22+
/// <param name="height">The surface height in pixels.</param>
23+
public WebGPUNativeSurface(
24+
WebGPUDeviceHandle deviceHandle,
25+
WebGPUQueueHandle queueHandle,
26+
WebGPUTextureHandle targetTextureHandle,
27+
WebGPUTextureViewHandle targetTextureViewHandle,
28+
WebGPUTextureFormat targetFormat,
29+
int width,
30+
int height)
31+
{
32+
Guard.NotNull(deviceHandle, nameof(deviceHandle));
33+
Guard.NotNull(queueHandle, nameof(queueHandle));
34+
Guard.NotNull(targetTextureHandle, nameof(targetTextureHandle));
35+
Guard.NotNull(targetTextureViewHandle, nameof(targetTextureViewHandle));
36+
37+
this.DeviceHandle = deviceHandle;
38+
this.QueueHandle = queueHandle;
39+
this.TargetTextureHandle = targetTextureHandle;
40+
this.TargetTextureViewHandle = targetTextureViewHandle;
41+
this.TargetFormat = targetFormat;
42+
this.Width = width;
43+
this.Height = height;
44+
}
45+
46+
/// <summary>
47+
/// Gets the wrapped device handle that owns the target texture.
48+
/// </summary>
49+
public WebGPUDeviceHandle DeviceHandle { get; }
50+
51+
/// <summary>
52+
/// Gets the wrapped queue handle used to submit work against the target texture.
53+
/// </summary>
54+
public WebGPUQueueHandle QueueHandle { get; }
55+
56+
/// <summary>
57+
/// Gets the wrapped target texture handle.
58+
/// </summary>
59+
public WebGPUTextureHandle TargetTextureHandle { get; }
60+
61+
/// <summary>
62+
/// Gets the wrapped target texture-view handle.
63+
/// </summary>
64+
public WebGPUTextureViewHandle TargetTextureViewHandle { get; }
65+
66+
/// <summary>
67+
/// Gets the native render target texture format identifier.
68+
/// </summary>
69+
public WebGPUTextureFormat TargetFormat { get; }
70+
71+
/// <summary>
72+
/// Gets the surface width in pixels.
73+
/// </summary>
74+
public int Width { get; }
75+
76+
/// <summary>
77+
/// Gets the surface height in pixels.
78+
/// </summary>
79+
public int Height { get; }
2180

2281
/// <summary>
2382
/// Allocates a WebGPU render target and creates a native surface over the owned texture handles.
@@ -31,7 +90,7 @@ internal WebGPUNativeSurface(WebGPUNativeTarget target)
3190
/// <param name="textureHandle">Receives the allocated wrapped <c>WGPUTexture*</c> handle.</param>
3291
/// <param name="textureViewHandle">Receives the allocated wrapped <c>WGPUTextureView*</c> handle.</param>
3392
/// <returns>The native surface wrapping the allocated texture.</returns>
34-
internal static unsafe NativeSurface Create(
93+
internal static unsafe WebGPUNativeSurface Create(
3594
WebGPU api,
3695
WebGPUDeviceHandle deviceHandle,
3796
WebGPUQueueHandle queueHandle,
@@ -105,7 +164,7 @@ internal static unsafe NativeSurface Create(
105164
{
106165
createdTextureHandle = new WebGPUTextureHandle(api, (nint)texture, ownsHandle: true);
107166
createdTextureViewHandle = new WebGPUTextureViewHandle(api, (nint)textureView, ownsHandle: true);
108-
NativeSurface surface = Create(
167+
WebGPUNativeSurface surface = Create(
109168
deviceHandle,
110169
queueHandle,
111170
createdTextureHandle,
@@ -140,7 +199,7 @@ internal static unsafe NativeSurface Create(
140199
/// <summary>
141200
/// Creates a native surface over wrapped WebGPU texture handles.
142201
/// </summary>
143-
internal static NativeSurface Create(
202+
internal static WebGPUNativeSurface Create(
144203
WebGPUDeviceHandle deviceHandle,
145204
WebGPUQueueHandle queueHandle,
146205
WebGPUTextureHandle targetTextureHandle,
@@ -157,25 +216,13 @@ internal static NativeSurface Create(
157216
Guard.MustBeGreaterThan(width, 0, nameof(width));
158217
Guard.MustBeGreaterThan(height, 0, nameof(height));
159218

160-
return new WebGPUNativeSurface(new WebGPUNativeTarget(
219+
return new WebGPUNativeSurface(
161220
deviceHandle,
162221
queueHandle,
163222
targetTextureHandle,
164223
targetTextureViewHandle,
165224
targetFormat,
166225
width,
167-
height));
168-
}
169-
170-
/// <inheritdoc />
171-
public override TNativeTarget GetNativeTarget<TNativeTarget>()
172-
where TNativeTarget : class
173-
{
174-
if (this.target is TNativeTarget typed)
175-
{
176-
return typed;
177-
}
178-
179-
throw new NotSupportedException($"The native surface does not expose a native target of type '{typeof(TNativeTarget).Name}'.");
226+
height);
180227
}
181228
}

src/ImageSharp.Drawing.WebGPU/WebGPUNativeTarget.cs

Lines changed: 0 additions & 76 deletions
This file was deleted.

src/ImageSharp.Drawing.WebGPU/WebGPURenderTarget.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ private WebGPURenderTarget(
8585
deviceContext.ThrowIfDisposed();
8686

8787
WebGPU api = WebGPURuntime.GetApi();
88-
NativeSurface surface = WebGPUNativeSurface.Create(
88+
WebGPUNativeSurface surface = WebGPUNativeSurface.Create(
8989
api,
9090
deviceContext.DeviceHandle,
9191
deviceContext.QueueHandle,
@@ -121,7 +121,7 @@ private WebGPURenderTarget(
121121
/// Gets the native surface backing this render target.
122122
/// Most callers should use <see cref="CreateCanvas()"/> or <see cref="ReadbackImage()"/> instead.
123123
/// </summary>
124-
internal NativeSurface Surface { get; }
124+
internal WebGPUNativeSurface Surface { get; }
125125

126126
/// <summary>
127127
/// Gets the target width in pixels.

0 commit comments

Comments
 (0)