Skip to content

Commit ef29546

Browse files
Cleanup API and fix demo FPS display
1 parent 99c2bd4 commit ef29546

21 files changed

+222
-430
lines changed

ImageSharp.Drawing.Samples.sln

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 17
4-
VisualStudioVersion = 17.0.31903.59
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.4.11626.88 stable
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}"
77
EndProject
@@ -113,4 +113,8 @@ Global
113113
{AEFE7C59-5D1C-4F14-A83F-0FD665130FA3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
114114
{0AF23A97-CD73-409A-AA29-D214AA400AB0} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
115115
EndGlobalSection
116+
GlobalSection(SharedMSBuildProjectFiles) = preSolution
117+
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{042b144e-5444-4b88-b4f2-038e54fc25d0}*SharedItemsImports = 5
118+
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{439c7ccb-78f0-4f24-9d7f-6d6659495def}*SharedItemsImports = 5
119+
EndGlobalSection
116120
EndGlobal

samples/DrawingBackendBenchmark/WebGpuBenchmarkBackend.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,18 @@ public BenchmarkRenderResult Render(ReadOnlySpan<VisualLine> lines, int width, i
5050

5151
stopwatch.Stop();
5252

53-
Image<Bgra32>? preview = capturePreview ? renderTarget.Readback() : null;
53+
Image<Bgra32>? preview = null;
54+
string? readbackError = null;
55+
if (capturePreview && !renderTarget.TryReadback(out preview, out readbackError))
56+
{
57+
preview = null;
58+
}
59+
5460
return new BenchmarkRenderResult(
5561
stopwatch.Elapsed.TotalMilliseconds,
5662
preview,
5763
renderTarget.Graphics.Backend.DiagnosticLastFlushUsedGPU,
58-
renderTarget.Graphics.Backend.DiagnosticLastSceneFailure);
64+
readbackError ?? renderTarget.Graphics.Backend.DiagnosticLastSceneFailure);
5965
}
6066

6167
/// <summary>

samples/WebGPUWindowDemo/Program.cs

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

4+
using System.Diagnostics;
45
using System.Numerics;
56
using SixLabors.Fonts;
67
using SixLabors.ImageSharp;
@@ -41,14 +42,13 @@ public static void Main()
4142
private sealed class DemoApp
4243
{
4344
private const int BallCount = 1000;
45+
private static readonly TimeSpan FpsUpdateInterval = TimeSpan.FromSeconds(1);
4446

4547
private readonly WebGPUWindow<Bgra32> window;
4648
private readonly Random rng = new(42);
49+
private readonly Stopwatch fpsWindow = Stopwatch.StartNew();
4750
private Ball[] balls = [];
4851
private int frameCount;
49-
private double fpsElapsed;
50-
private double fpsSum;
51-
private double fpsSumSquares;
5252
private IPathCollection scrollPaths = new PathCollection();
5353
private float scrollOffset;
5454
private float scrollTextHeight;
@@ -126,11 +126,11 @@ private void InitializeScene()
126126
/// <summary>
127127
/// Advances the animation state for the next frame.
128128
/// </summary>
129-
/// <param name="deltaTime">Elapsed time since the previous update, in seconds.</param>
130-
private void OnUpdate(double deltaTime)
129+
/// <param name="deltaTime">Elapsed time since the previous update.</param>
130+
private void OnUpdate(TimeSpan deltaTime)
131131
{
132132
Size framebufferSize = this.window.FramebufferSize;
133-
float dt = (float)deltaTime;
133+
float dt = (float)deltaTime.TotalSeconds;
134134
for (int i = 0; i < this.balls.Length; i++)
135135
{
136136
this.balls[i].Update(dt, framebufferSize.Width, framebufferSize.Height);
@@ -152,32 +152,24 @@ private void OnRender(WebGPUWindowFrame<Bgra32> frame)
152152
DrawingCanvas<Bgra32> canvas = frame.Canvas;
153153
canvas.Fill(Brushes.Solid(Color.FromPixel(new Bgra32(30, 30, 40, 255))));
154154

155-
this.DrawScrollingText(canvas, frame.FramebufferSize.Width, frame.FramebufferSize.Height);
156-
157155
for (int i = 0; i < this.balls.Length; i++)
158156
{
159157
ref Ball ball = ref this.balls[i];
160158
EllipsePolygon ellipse = new(ball.X, ball.Y, ball.Radius);
161159
canvas.Fill(Brushes.Solid(ball.Color), ellipse);
162160
}
163161

162+
this.DrawScrollingText(canvas, frame.FramebufferSize.Width, frame.FramebufferSize.Height);
163+
164164
this.frameCount++;
165-
double frameSeconds = frame.DeltaTime.TotalSeconds;
166-
this.fpsElapsed += frameSeconds;
167-
double frameFps = frameSeconds > 0D ? 1D / frameSeconds : 0D;
168-
this.fpsSum += frameFps;
169-
this.fpsSumSquares += frameFps * frameFps;
170-
if (this.fpsElapsed >= 1D)
165+
TimeSpan elapsed = this.fpsWindow.Elapsed;
166+
if (elapsed >= FpsUpdateInterval)
171167
{
172-
double meanFps = this.fpsSum / this.frameCount;
173-
double variance = Math.Max(0D, (this.fpsSumSquares / this.frameCount) - (meanFps * meanFps));
174-
double stdDevFps = Math.Sqrt(variance);
175-
double frameTimeMs = frame.DeltaTime.TotalMilliseconds;
176-
this.window.Title = $"ImageSharp.Drawing WebGPU Demo - Current: {frameTimeMs:F1} ms / {frameFps:F1} FPS | Mean: {meanFps:F1} FPS | StdDev: {stdDevFps:F1}";
168+
double fps = this.frameCount / elapsed.TotalSeconds;
169+
double frameTimeMs = elapsed.TotalMilliseconds / this.frameCount;
170+
this.window.Title = $"ImageSharp.Drawing WebGPU Demo - {frameTimeMs:F1} ms / {fps:F1} FPS";
177171
this.frameCount = 0;
178-
this.fpsElapsed = 0;
179-
this.fpsSum = 0;
180-
this.fpsSumSquares = 0;
172+
this.fpsWindow.Restart();
181173
}
182174
}
183175

samples/WebGPUWindowDemo/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ The important pattern here is that text shaping is not done every frame. The sam
9292
this.window.Update += this.OnUpdate;
9393
```
9494

95-
`OnUpdate(double deltaTime)` performs simulation only:
95+
`OnUpdate(TimeSpan deltaTime)` performs simulation only:
9696

9797
- each ball advances by `velocity * dt`
9898
- each ball reflects off the framebuffer edges

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

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

44
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.Runtime.CompilerServices;
67
using System.Runtime.InteropServices;
78
using Silk.NET.WebGPU;
@@ -26,6 +27,28 @@ public bool TryReadRegion<TPixel>(
2627
Rectangle sourceRectangle,
2728
Buffer2DRegion<TPixel> destination)
2829
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>
2952
{
3053
this.ThrowIfDisposed();
3154
Guard.NotNull(configuration, nameof(configuration));
@@ -39,12 +62,14 @@ public bool TryReadRegion<TPixel>(
3962
capability.Queue == 0 ||
4063
capability.TargetTexture == 0)
4164
{
65+
error = "Readback is only available for native WebGPU targets with valid device, queue, and texture handles.";
4266
return false;
4367
}
4468

4569
if (!TryGetCompositeTextureFormat<TPixel>(out WebGPUTextureFormatId expectedFormat, out FeatureName requiredFeature) ||
4670
expectedFormat != capability.TargetFormat)
4771
{
72+
error = $"Pixel type '{typeof(TPixel).Name}' is not compatible with target format '{capability.TargetFormat}'.";
4873
return false;
4974
}
5075

@@ -60,6 +85,7 @@ public bool TryReadRegion<TPixel>(
6085

6186
if (source.Width <= 0 || source.Height <= 0)
6287
{
88+
error = "The requested source rectangle does not intersect the target bounds.";
6389
return false;
6490
}
6591

@@ -70,6 +96,7 @@ public bool TryReadRegion<TPixel>(
7096
if (requiredFeature != FeatureName.Undefined
7197
&& !WebGPURuntime.GetOrCreateDeviceState(api, device).HasFeature(requiredFeature))
7298
{
99+
error = $"The target device does not support required feature '{requiredFeature}' for pixel type '{typeof(TPixel).Name}'.";
73100
return false;
74101
}
75102

@@ -97,13 +124,15 @@ public bool TryReadRegion<TPixel>(
97124
readbackBuffer = api.DeviceCreateBuffer(device, in bufferDescriptor);
98125
if (readbackBuffer is null)
99126
{
127+
error = "WebGPU.DeviceCreateBuffer returned null for readback.";
100128
return false;
101129
}
102130

103131
CommandEncoderDescriptor encoderDescriptor = default;
104132
commandEncoder = api.DeviceCreateCommandEncoder(device, in encoderDescriptor);
105133
if (commandEncoder is null)
106134
{
135+
error = "WebGPU.DeviceCreateCommandEncoder returned null.";
107136
return false;
108137
}
109138

@@ -134,6 +163,7 @@ public bool TryReadRegion<TPixel>(
134163
commandBuffer = api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor);
135164
if (commandBuffer is null)
136165
{
166+
error = "WebGPU.CommandEncoderFinish returned null.";
137167
return false;
138168
}
139169

@@ -157,13 +187,15 @@ void Callback(BufferMapAsyncStatus status, void* userData)
157187
api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)readbackByteCount, callback, null);
158188
if (!WaitForMapSignal(lease.WgpuExtension, device, mapReady) || mapStatus != BufferMapAsyncStatus.Success)
159189
{
190+
error = $"WebGPU readback map failed with status '{mapStatus}'.";
160191
return false;
161192
}
162193

163194
void* mapped = api.BufferGetConstMappedRange(readbackBuffer, 0, (nuint)readbackByteCount);
164195
if (mapped is null)
165196
{
166197
api.BufferUnmap(readbackBuffer);
198+
error = "WebGPU.BufferGetConstMappedRange returned null.";
167199
return false;
168200
}
169201

@@ -181,6 +213,7 @@ void Callback(BufferMapAsyncStatus status, void* userData)
181213
.CopyTo(MemoryMarshal.AsBytes(destination.DangerousGetRowSpan(y)));
182214
}
183215

216+
error = null;
184217
return true;
185218
}
186219
finally

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
1414
/// <remarks>
1515
/// Scene composition uses a staged WebGPU raster path when the target surface and pixel format are supported,
1616
/// and falls back to <see cref="DefaultDrawingBackend"/> otherwise.
17+
/// Public diagnostic properties on this type describe only the most recent flush executed by this backend
18+
/// instance. They are lightweight inspection state for debugging, tests, and benchmarks, and they are
19+
/// overwritten by the next flush.
1720
/// </remarks>
1821
public sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable
1922
{
@@ -72,21 +75,39 @@ public WebGPUDrawingBackend()
7275
/// <summary>
7376
/// Gets a value indicating whether the last flush completed on the staged path.
7477
/// </summary>
78+
/// <remarks>
79+
/// This value describes only the most recent call to <see cref="FlushCompositions{TPixel}(Configuration, ICanvasFrame{TPixel}, CompositionScene)"/>
80+
/// on this backend instance. It is overwritten by the next flush.
81+
/// </remarks>
7582
public bool DiagnosticLastFlushUsedGPU => this.TestingLastFlushUsedGPU;
7683

7784
/// <summary>
7885
/// Gets the last staged-scene creation or dispatch failure that forced CPU fallback.
7986
/// </summary>
87+
/// <remarks>
88+
/// This value describes only the most recent call to <see cref="FlushCompositions{TPixel}(Configuration, ICanvasFrame{TPixel}, CompositionScene)"/>
89+
/// on this backend instance. It is reset at the start of each flush and overwritten by the next failure.
90+
/// A <see langword="null"/> value means no failure reason was recorded for the most recent flush.
91+
/// </remarks>
8092
public string? DiagnosticLastSceneFailure => this.TestingLastGPUInitializationFailure;
8193

8294
/// <summary>
8395
/// Gets a value indicating whether the last staged flush used the chunked oversized-scene path.
8496
/// </summary>
97+
/// <remarks>
98+
/// This value describes only the most recent call to <see cref="FlushCompositions{TPixel}(Configuration, ICanvasFrame{TPixel}, CompositionScene)"/>
99+
/// on this backend instance. It is overwritten by the next flush.
100+
/// </remarks>
85101
public bool DiagnosticLastFlushUsedChunking => this.TestingLastFlushUsedChunking;
86102

87103
/// <summary>
88104
/// Gets the chunkable binding-limit failure that selected the chunked oversized-scene path for the last staged flush.
89105
/// </summary>
106+
/// <remarks>
107+
/// This value describes only the most recent call to <see cref="FlushCompositions{TPixel}(Configuration, ICanvasFrame{TPixel}, CompositionScene)"/>
108+
/// on this backend instance. When the most recent flush did not use the chunked path, this property returns
109+
/// the default <c>None</c> value from <see cref="WebGPUSceneDispatch.BindingLimitBuffer"/>.
110+
/// </remarks>
90111
public string DiagnosticLastChunkingBindingFailure => this.TestingLastChunkingBindingFailure.ToString();
91112

92113
/// <inheritdoc />

src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public static NativeSurface Create<TPixel>(
3939
ValidateCommon(
4040
deviceHandle,
4141
queueHandle,
42+
targetTextureHandle,
4243
targetTextureViewHandle,
4344
width,
4445
height);
@@ -63,6 +64,7 @@ public static NativeSurface Create<TPixel>(
6364
private static void ValidateCommon(
6465
nint deviceHandle,
6566
nint queueHandle,
67+
nint targetTextureHandle,
6668
nint targetTextureViewHandle,
6769
int width,
6870
int height)
@@ -77,6 +79,11 @@ private static void ValidateCommon(
7779
throw new ArgumentOutOfRangeException(nameof(queueHandle), "Queue handle must be non-zero.");
7880
}
7981

82+
if (targetTextureHandle == 0)
83+
{
84+
throw new ArgumentOutOfRangeException(nameof(targetTextureHandle), "Texture handle must be non-zero.");
85+
}
86+
8087
if (targetTextureViewHandle == 0)
8188
{
8289
throw new ArgumentOutOfRangeException(nameof(targetTextureViewHandle), "Texture view handle must be non-zero.");

0 commit comments

Comments
 (0)