Skip to content

Commit 1eec802

Browse files
Use native frames for WebGPU staging and flush
1 parent 5db4e95 commit 1eec802

4 files changed

Lines changed: 54 additions & 270 deletions

File tree

ImageSharp.Drawing.All.sln

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,16 +336,15 @@ EndProject
336336
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.ManualBenchmarks", "tests\ImageSharp.Drawing.ManualBenchmarks\ImageSharp.Drawing.ManualBenchmarks.csproj", "{70193989-E587-451A-AF34-4C74FEE5DA5A}"
337337
EndProject
338338
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}"
339-
ProjectSection(SolutionItems) = preProject
340-
samples\DrawingBackendBenchmark\DrawingBackendBenchmark.csproj = samples\DrawingBackendBenchmark\DrawingBackendBenchmark.csproj
341-
EndProjectSection
342339
EndProject
343340
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUWindowDemo", "samples\WebGPUWindowDemo\WebGPUWindowDemo.csproj", "{999D90DE-6940-4693-9F80-701F2D5DB77A}"
344341
EndProject
345342
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawShapesWithImageSharp", "samples\DrawShapesWithImageSharp\DrawShapesWithImageSharp.csproj", "{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7}"
346343
EndProject
347344
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUExternalSurfaceDemo", "samples\WebGPUExternalSurfaceDemo\WebGPUExternalSurfaceDemo.csproj", "{F0AAE591-A44E-48A5-8C74-395BDF98F646}"
348345
EndProject
346+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawingBackendBenchmark", "samples\DrawingBackendBenchmark\DrawingBackendBenchmark.csproj", "{B5CF5531-624F-9F49-46BD-937FF4062F54}"
347+
EndProject
349348
Global
350349
GlobalSection(SolutionConfigurationPlatforms) = preSolution
351350
Debug|Any CPU = Debug|Any CPU
@@ -464,6 +463,18 @@ Global
464463
{F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x64.Build.0 = Release|Any CPU
465464
{F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x86.ActiveCfg = Release|Any CPU
466465
{F0AAE591-A44E-48A5-8C74-395BDF98F646}.Release|x86.Build.0 = Release|Any CPU
466+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
467+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|Any CPU.Build.0 = Debug|Any CPU
468+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|x64.ActiveCfg = Debug|Any CPU
469+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|x64.Build.0 = Debug|Any CPU
470+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|x86.ActiveCfg = Debug|Any CPU
471+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Debug|x86.Build.0 = Debug|Any CPU
472+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|Any CPU.ActiveCfg = Release|Any CPU
473+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|Any CPU.Build.0 = Release|Any CPU
474+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|x64.ActiveCfg = Release|Any CPU
475+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|x64.Build.0 = Release|Any CPU
476+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|x86.ActiveCfg = Release|Any CPU
477+
{B5CF5531-624F-9F49-46BD-937FF4062F54}.Release|x86.Build.0 = Release|Any CPU
467478
EndGlobalSection
468479
GlobalSection(SolutionProperties) = preSolution
469480
HideSolutionNode = FALSE
@@ -496,6 +507,7 @@ Global
496507
{999D90DE-6940-4693-9F80-701F2D5DB77A} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
497508
{43A3CA46-2675-4AD4-88FC-DDC2E657F0B7} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
498509
{F0AAE591-A44E-48A5-8C74-395BDF98F646} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
510+
{B5CF5531-624F-9F49-46BD-937FF4062F54} = {CC2DDE4D-58D9-4333-BBEE-B4F36E0A4070}
499511
EndGlobalSection
500512
GlobalSection(ExtensibilityGlobals) = postSolution
501513
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 20 additions & 82 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.Runtime.CompilerServices;
5-
using System.Threading;
65
using Silk.NET.WebGPU;
76
using SixLabors.ImageSharp.PixelFormats;
87

@@ -106,6 +105,8 @@ public void RenderScene<TPixel>(
106105
{
107106
this.ThrowIfDisposed();
108107

108+
NativeCanvasFrame<TPixel> nativeTarget = (NativeCanvasFrame<TPixel>)target;
109+
109110
if (scene is not WebGPUDrawingBackendScene webGPUScene)
110111
{
111112
throw new InvalidOperationException("The retained scene is not a WebGPU drawing backend scene.");
@@ -116,17 +117,18 @@ public void RenderScene<TPixel>(
116117
throw new NotSupportedException($"The WebGPU backend does not support pixel format '{typeof(TPixel).Name}'.");
117118
}
118119

119-
if (!TryGetSceneTarget(
120-
target,
121-
formatId,
122-
requiredFeature,
123-
out TextureFormat textureFormat,
124-
out Rectangle targetBounds))
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.
122+
_ = nativeTarget.TryGetNativeSurface(out NativeSurface? nativeSurface);
123+
WebGPUNativeTarget webGPUTarget = nativeSurface!.GetNativeTarget<WebGPUNativeTarget>();
124+
TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(webGPUTarget.TargetFormat);
125+
126+
if (webGPUTarget.TargetFormat != formatId)
125127
{
126-
throw new NotSupportedException("The target cannot create a WebGPU flush context.");
128+
throw new InvalidOperationException("The target texture format does not match the retained WebGPU scene pixel format.");
127129
}
128130

129-
if (targetBounds != webGPUScene.Bounds)
131+
if (nativeTarget.Bounds != webGPUScene.Bounds)
130132
{
131133
throw new InvalidOperationException("The target bounds do not match the retained WebGPU scene bounds.");
132134
}
@@ -137,7 +139,6 @@ public void RenderScene<TPixel>(
137139
WebGPUSceneBumpSizes currentBumpSizes = webGPUScene.BumpSizes;
138140
WebGPUSceneResourceArena? resourceArena = null;
139141
WebGPUSceneSchedulingArena? schedulingArena = null;
140-
string? error;
141142

142143
try
143144
{
@@ -157,24 +158,14 @@ public void RenderScene<TPixel>(
157158
// rediscovering the same growth.
158159
for (int attempt = 0; attempt < MaxDynamicGrowthAttempts; attempt++)
159160
{
160-
if (!WebGPUSceneDispatch.TryCreateStagedScene(
161-
configuration,
162-
target,
163-
encodedScene,
164-
textureFormat,
165-
requiredFeature,
166-
currentBumpSizes,
167-
ref resourceArena,
168-
out bool exceedsBindingLimit,
169-
out _,
170-
out WebGPUStagedScene stagedScene,
171-
out error))
172-
{
173-
throw new InvalidOperationException(
174-
exceedsBindingLimit
175-
? error ?? "The staged WebGPU scene exceeded the current binding limits."
176-
: error ?? "Failed to create the staged WebGPU scene.");
177-
}
161+
WebGPUStagedScene stagedScene = WebGPUSceneDispatch.CreateStagedScene(
162+
configuration,
163+
nativeTarget,
164+
encodedScene,
165+
textureFormat,
166+
requiredFeature,
167+
currentBumpSizes,
168+
ref resourceArena);
178169

179170
try
180171
{
@@ -189,7 +180,7 @@ public void RenderScene<TPixel>(
189180
ref schedulingArena,
190181
out bool requiresGrowth,
191182
out WebGPUSceneBumpSizes grownBumpSizes,
192-
out error);
183+
out string? error);
193184

194185
if (renderSucceeded)
195186
{
@@ -227,59 +218,6 @@ public void RenderScene<TPixel>(
227218
}
228219
}
229220

230-
/// <summary>
231-
/// Validates the target information needed to render a retained WebGPU scene.
232-
/// </summary>
233-
/// <typeparam name="TPixel">The pixel format.</typeparam>
234-
/// <param name="target">The target frame.</param>
235-
/// <param name="expectedFormat">The expected WebGPU texture format.</param>
236-
/// <param name="requiredFeature">The optional feature required by the texture format.</param>
237-
/// <param name="textureFormat">Receives the native WebGPU texture format.</param>
238-
/// <param name="targetBounds">Receives the target bounds.</param>
239-
/// <returns><see langword="true"/> when the target can accept a WebGPU scene.</returns>
240-
private static bool TryGetSceneTarget<TPixel>(
241-
ICanvasFrame<TPixel> target,
242-
WebGPUTextureFormat expectedFormat,
243-
FeatureName requiredFeature,
244-
out TextureFormat textureFormat,
245-
out Rectangle targetBounds)
246-
where TPixel : unmanaged, IPixel<TPixel>
247-
{
248-
textureFormat = default;
249-
targetBounds = default;
250-
251-
if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface))
252-
{
253-
return false;
254-
}
255-
256-
WebGPUNativeTarget nativeTarget = nativeSurface.GetNativeTarget<WebGPUNativeTarget>();
257-
if (nativeTarget.DeviceHandle.IsInvalid ||
258-
nativeTarget.QueueHandle.IsInvalid ||
259-
nativeTarget.TargetTextureViewHandle.IsInvalid ||
260-
nativeTarget.TargetFormat != expectedFormat)
261-
{
262-
return false;
263-
}
264-
265-
targetBounds = target.Bounds;
266-
Rectangle nativeBounds = new(0, 0, nativeTarget.Width, nativeTarget.Height);
267-
if (!nativeBounds.Contains(targetBounds))
268-
{
269-
return false;
270-
}
271-
272-
WebGPU api = WebGPURuntime.GetApi();
273-
WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(api, nativeTarget.DeviceHandle);
274-
if (requiredFeature != FeatureName.Undefined && !deviceState.HasFeature(requiredFeature))
275-
{
276-
return false;
277-
}
278-
279-
textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeTarget.TargetFormat);
280-
return true;
281-
}
282-
283221
/// <summary>
284222
/// Computes the maximum scratch capacities observed across render attempts.
285223
/// </summary>

src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,6 @@ private WebGPUFlushContext(
154154

155155
/// <summary>
156156
/// Creates a flush context for a native WebGPU surface.
157-
/// Returns <see langword="null"/> when the frame does not expose a native surface
158-
/// or the device lacks the required feature.
159157
/// </summary>
160158
/// <param name="frame">The target frame.</param>
161159
/// <param name="expectedTextureFormat">The expected GPU texture format.</param>
@@ -164,20 +162,18 @@ private WebGPUFlushContext(
164162
/// <see cref="FeatureName.Undefined"/> when no special feature is needed.
165163
/// </param>
166164
/// <param name="memoryAllocator">The memory allocator for staging buffers.</param>
167-
/// <returns>The flush context, or <see langword="null"/> when GPU execution is unavailable.</returns>
168-
public static WebGPUFlushContext? Create<TPixel>(
169-
ICanvasFrame<TPixel> frame,
165+
/// <returns>The flush context.</returns>
166+
public static WebGPUFlushContext Create<TPixel>(
167+
NativeCanvasFrame<TPixel> frame,
170168
TextureFormat expectedTextureFormat,
171169
FeatureName requiredFeature,
172170
MemoryAllocator memoryAllocator)
173171
where TPixel : unmanaged, IPixel<TPixel>
174172
{
175-
if (!frame.TryGetNativeSurface(out NativeSurface? nativeSurface))
176-
{
177-
return null;
178-
}
179-
180-
WebGPUNativeTarget nativeTarget = nativeSurface.GetNativeTarget<WebGPUNativeTarget>();
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.
175+
_ = frame.TryGetNativeSurface(out NativeSurface? nativeSurface);
176+
WebGPUNativeTarget nativeTarget = nativeSurface!.GetNativeTarget<WebGPUNativeTarget>();
181177
WebGPU api = WebGPURuntime.GetApi();
182178
TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeTarget.TargetFormat);
183179
Rectangle bounds = frame.Bounds;
@@ -189,20 +185,20 @@ private WebGPUFlushContext(
189185
nativeTarget.TargetTextureViewHandle.IsInvalid ||
190186
textureFormat != expectedTextureFormat)
191187
{
192-
return null;
188+
throw new InvalidOperationException("The native WebGPU target does not match the flush context requirements.");
193189
}
194190

195191
if (requiredFeature != FeatureName.Undefined && !deviceState.HasFeature(requiredFeature))
196192
{
197-
return null;
193+
throw new NotSupportedException($"The WebGPU device does not support required feature '{requiredFeature}'.");
198194
}
199195

200196
// Region frames expose bounds relative to their parent target. The flush context must preserve
201197
// that absolute slice so later scene encoding, dispatch planning, and texture copies address
202198
// the correct sub-rectangle of the native surface instead of silently expanding back to full-frame.
203199
if (!nativeBounds.Contains(bounds))
204200
{
205-
return null;
201+
throw new InvalidOperationException("The native WebGPU target bounds do not contain the flush bounds.");
206202
}
207203

208204
return new WebGPUFlushContext(

0 commit comments

Comments
 (0)