Skip to content

Commit 5eac1ee

Browse files
Simplify tests and update docs
1 parent 5553d94 commit 5eac1ee

File tree

13 files changed

+436
-123
lines changed

13 files changed

+436
-123
lines changed

samples/DrawingBackendBenchmark/BenchmarkForm.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,14 @@ private void UpdatePreview(BenchmarkRenderResult result, bool capturePreview)
249249
/// Formats one status line describing the current sample, running statistics, and backend outcome.
250250
/// </summary>
251251
private static string FormatStatusText(
252-
string backendName,
252+
string? backendName,
253253
BenchmarkRenderResult result,
254254
int lineCount,
255255
int iteration,
256256
int totalIterations,
257257
BenchmarkStatistics statistics)
258258
{
259-
string backendStatus = GetBackendStatusText(backendName, result);
259+
string backendStatus = GetBackendStatusText(backendName ?? string.Empty, result);
260260
string backendFailure = result.BackendFailure is not null ? $" | {result.BackendFailure}" : string.Empty;
261261

262262
return

samples/DrawingBackendBenchmark/CpuBenchmarkBackend.cs

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

44
using System.Diagnostics;
@@ -15,6 +15,7 @@ namespace DrawingBackendBenchmark;
1515
internal sealed class CpuBenchmarkBackend : IBenchmarkBackend
1616
{
1717
private readonly Configuration configuration;
18+
private Image<Bgra32>? image;
1819

1920
/// <summary>
2021
/// Initializes a new instance of the <see cref="CpuBenchmarkBackend"/> class.
@@ -26,7 +27,7 @@ internal sealed class CpuBenchmarkBackend : IBenchmarkBackend
2627
/// </summary>
2728
public BenchmarkRenderResult Render(ReadOnlySpan<VisualLine> lines, int width, int height, bool capturePreview)
2829
{
29-
using Image<Bgra32> image = new(width, height);
30+
Image<Bgra32> image = this.EnsureImage(width, height);
3031
Buffer2DRegion<Bgra32> region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds);
3132

3233
Stopwatch stopwatch = Stopwatch.StartNew();
@@ -46,4 +47,28 @@ public BenchmarkRenderResult Render(ReadOnlySpan<VisualLine> lines, int width, i
4647
/// Gets the name of this backend.
4748
/// </summary>
4849
public override string ToString() => "CPU";
50+
51+
/// <inheritdoc />
52+
public void Dispose()
53+
{
54+
this.image?.Dispose();
55+
this.image = null;
56+
}
57+
58+
/// <summary>
59+
/// Gets the cached CPU render target image for the benchmark backend.
60+
/// </summary>
61+
/// <param name="width">The image width.</param>
62+
/// <param name="height">The image height.</param>
63+
/// <returns>The cached image.</returns>
64+
private Image<Bgra32> EnsureImage(int width, int height)
65+
{
66+
if (this.image is not null)
67+
{
68+
return this.image;
69+
}
70+
71+
this.image = new Image<Bgra32>(width, height);
72+
return this.image;
73+
}
4974
}

samples/DrawingBackendBenchmark/IBenchmarkBackend.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace DrawingBackendBenchmark;
66
/// <summary>
77
/// Base interface for benchmark backends exposing their shared render method.
88
/// </summary>
9-
internal interface IBenchmarkBackend
9+
internal interface IBenchmarkBackend : IDisposable
1010
{
1111
/// <summary>
1212
/// Renders the benchmark scene and returns timing and optional preview data.
@@ -16,5 +16,5 @@ internal interface IBenchmarkBackend
1616
/// <param name="height">The height of the render target.</param>
1717
/// <param name="capturePreview">Whether to capture a preview image of the final frame.</param>
1818
/// <returns>The benchmark render result including timing and diagnostics.</returns>
19-
BenchmarkRenderResult Render(ReadOnlySpan<VisualLine> lines, int width, int height, bool capturePreview);
19+
public BenchmarkRenderResult Render(ReadOnlySpan<VisualLine> lines, int width, int height, bool capturePreview);
2020
}

samples/DrawingBackendBenchmark/Program.cs

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

4-
using Color = SixLabors.ImageSharp.Color;
5-
using PointF = SixLabors.ImageSharp.PointF;
6-
74
namespace DrawingBackendBenchmark;
85

96
/// <summary>
Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,93 @@
11
# Drawing Backend Benchmark
22

3-
A Windows sample based on the original `ImageSharpBenchmark` workload from the `Csharp-Data-Visualization` repo.
3+
`DrawingBackendBenchmark` is a small Windows sample that renders the same randomized line scene through several drawing backends and shows the last rendered frame alongside timing statistics.
4+
5+
It is based on the original ImageSharp benchmark workload from the `Csharp-Data-Visualization` repo:
46

57
https://swharden.com/csdv/platforms/compare/
68

7-
It renders a randomized line scene repeatedly and lets you compare:
9+
## What it compares
10+
11+
Depending on what the machine can initialize, the sample can show:
12+
13+
- `CPU`: the default `ImageSharp.Drawing` CPU backend
14+
- `SkiaSharp (CPU)`: Skia rasterized in CPU mode
15+
- `SkiaSharp (GPU)`: Skia using a GPU-backed `GRContext` when the hidden GL host is available
16+
- `WebGPU`: the offscreen `ImageSharp.Drawing.WebGPU` backend
817

9-
- `CPU` via the default `ImageSharp.Drawing` backend
10-
- `WebGPU` via the offscreen `ImageSharp.Drawing.WebGPU` backend
18+
The sample always starts with the CPU and Skia CPU paths. The WebGPU backend is added only when `WebGPUEnvironment.TryProbeComputePipelineSupport(...)` succeeds. The Skia GPU backend is added once the hidden `SKGLControl` has produced a usable `GRContext`.
1119

12-
The sample shows:
20+
## What the UI shows
1321

14-
- a preview of the last rendered frame
15-
- the most recent render time
16-
- running mean
17-
- standard deviation
22+
The benchmark window contains:
23+
24+
- a backend selector
25+
- an iteration count selector
26+
- preset buttons for `10`, `1k`, `10k`, and `100k` lines
27+
- a preview of the final rendered frame
28+
- current render time, running mean, and standard deviation
29+
30+
The fixed benchmark surface is `600x400` pixels.
1831

1932
## Running
2033

2134
```powershell
2235
dotnet run --project samples/DrawingBackendBenchmark -c Release
2336
```
2437

25-
The WebGPU path renders to an offscreen native texture and reads the final frame back for preview. The reported benchmark time measures scene rendering and flush time only, not the preview readback.
38+
## How one benchmark run works
39+
40+
For each iteration the form:
41+
42+
1. generates a deterministic random line set for the requested line count
43+
2. renders that scene through the selected backend
44+
3. records the elapsed render time
45+
4. updates the running mean and standard deviation
46+
5. captures a preview image only on the last iteration
47+
48+
That last point matters: preview capture is intentionally outside the measured timing path. The reported time measures scene submission and backend flush work only. Any readback, clone, or bitmap conversion needed for the UI happens afterward.
49+
50+
## Timing model
51+
52+
All backends follow the same basic timing rule:
53+
54+
- start the stopwatch immediately before drawing the scene
55+
- stop it immediately after the backend flush or submission boundary
56+
- capture preview pixels only after the stopwatch stops
57+
58+
In practice that means:
59+
60+
- `CPU` measures drawing into the cached `Image<Bgra32>` through `DrawingCanvas.Flush()`
61+
- `SkiaSharp` measures drawing through `SKCanvas.Flush()` and optional GPU context flush
62+
- `WebGPU` measures drawing through `DrawingCanvas.Flush()` into the offscreen `WebGPURenderTarget<Bgra32>`
63+
64+
So the numbers are comparable as "render and submit" timings, not "render plus preview extraction" timings.
65+
66+
## Backend resource reuse
67+
68+
The sample uses a fixed benchmark size, so the backends keep their render targets alive across iterations instead of reallocating them every run:
69+
70+
- `CpuBenchmarkBackend` caches one `Image<Bgra32>`
71+
- `SkiaSharpBenchmarkBackend` caches one `SKSurface`
72+
- `WebGpuBenchmarkBackend` caches one `WebGPURenderTarget<Bgra32>`
73+
74+
This keeps the benchmark focused on scene rendering rather than repeated target allocation noise.
75+
76+
## WebGPU path
77+
78+
The WebGPU backend is intentionally small:
79+
80+
- it probes support up front with `WebGPUEnvironment.TryProbeComputePipelineSupport(...)`
81+
- it renders into an owned offscreen `WebGPURenderTarget<Bgra32>`
82+
- it draws through `CreateCanvas(...)`, not a hybrid CPU plus GPU canvas
83+
- it reads back the final frame only when the UI requests the last-iteration preview
84+
85+
The status line also reports whether the last WebGPU flush completed on the staged GPU path or had to fall back to CPU execution.
86+
87+
## File guide
2688

27-
Internally the sample now uses an owned `WebGPURenderTarget<TPixel>` so the offscreen WebGPU setup stays small while still supporting the benchmark's hybrid CPU+GPU frame flow.
89+
- [BenchmarkForm.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/DrawingBackendBenchmark/BenchmarkForm.cs): WinForms UI, backend selection, iteration loop, and preview display
90+
- [CpuBenchmarkBackend.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/DrawingBackendBenchmark/CpuBenchmarkBackend.cs): ImageSharp CPU baseline backend
91+
- [SkiaSharpBenchmarkBackend.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/DrawingBackendBenchmark/SkiaSharpBenchmarkBackend.cs): Skia CPU and GPU benchmark backend
92+
- [WebGpuBenchmarkBackend.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/DrawingBackendBenchmark/WebGpuBenchmarkBackend.cs): offscreen WebGPU benchmark backend
93+
- [VisualLine.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/DrawingBackendBenchmark/VisualLine.cs): shared randomized line scene description and canvas render helper

samples/DrawingBackendBenchmark/SkiaSharpBenchmarkBackend.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace DrawingBackendBenchmark;
1010

11-
internal sealed class SkiaSharpBenchmarkBackend : IBenchmarkBackend, IDisposable
11+
internal sealed class SkiaSharpBenchmarkBackend : IBenchmarkBackend
1212
{
1313
private static readonly SKColor BackgroundColor = SKColor.Parse("#003366");
1414

samples/DrawingBackendBenchmark/WebGpuBenchmarkBackend.cs

Lines changed: 17 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ namespace DrawingBackendBenchmark;
1313
/// <summary>
1414
/// Small offscreen WebGPU host used by the sample so the benchmark can drive the real backend without manual WebGPU bootstrap code.
1515
/// </summary>
16-
internal sealed class WebGpuBenchmarkBackend : IBenchmarkBackend, IDisposable
16+
internal sealed class WebGpuBenchmarkBackend : IBenchmarkBackend
1717
{
18-
private RenderResources? resources;
18+
private WebGPURenderTarget<Bgra32>? renderTarget;
1919

2020
private WebGpuBenchmarkBackend()
2121
{
@@ -29,43 +29,33 @@ public static bool TryCreate([NotNullWhen(true)] out WebGpuBenchmarkBackend? res
2929
return false;
3030
}
3131

32-
try
33-
{
34-
using WebGPURenderTarget<Bgra32> probe = new(1, 1);
35-
result = new WebGpuBenchmarkBackend();
36-
error = null;
37-
return true;
38-
}
39-
catch (Exception ex)
40-
{
41-
result = null;
42-
error = ex.Message;
43-
return false;
44-
}
32+
result = new WebGpuBenchmarkBackend();
33+
error = null;
34+
return true;
4535
}
4636

4737
/// <summary>
4838
/// Renders the benchmark scene through the WebGPU backend and optionally captures a readback preview.
4939
/// </summary>
5040
public BenchmarkRenderResult Render(ReadOnlySpan<VisualLine> lines, int width, int height, bool capturePreview)
5141
{
52-
RenderResources resources = this.EnsureResources(width, height);
42+
WebGPURenderTarget<Bgra32> renderTarget = this.EnsureRenderTarget(width, height);
5343

5444
Stopwatch stopwatch = Stopwatch.StartNew();
55-
using (DrawingCanvas<Bgra32> canvas = resources.RenderTarget.CreateHybridCanvas(resources.CpuImage, new DrawingOptions()))
45+
using (DrawingCanvas<Bgra32> canvas = renderTarget.CreateCanvas(new DrawingOptions()))
5646
{
5747
VisualLine.RenderLinesToCanvas(canvas, lines);
5848
canvas.Flush();
5949
}
6050

6151
stopwatch.Stop();
6252

63-
Image<Bgra32>? preview = capturePreview ? resources.RenderTarget.Readback() : null;
53+
Image<Bgra32>? preview = capturePreview ? renderTarget.Readback() : null;
6454
return new BenchmarkRenderResult(
6555
stopwatch.Elapsed.TotalMilliseconds,
6656
preview,
67-
resources.RenderTarget.Graphics.Backend.DiagnosticLastFlushUsedGPU,
68-
resources.RenderTarget.Graphics.Backend.DiagnosticLastSceneFailure);
57+
renderTarget.Graphics.Backend.DiagnosticLastFlushUsedGPU,
58+
renderTarget.Graphics.Backend.DiagnosticLastSceneFailure);
6959
}
7060

7161
/// <summary>
@@ -76,42 +66,18 @@ public BenchmarkRenderResult Render(ReadOnlySpan<VisualLine> lines, int width, i
7666
/// <inheritdoc />
7767
public void Dispose()
7868
{
79-
this.resources?.Dispose();
80-
this.resources = null;
81-
}
82-
83-
private RenderResources EnsureResources(int width, int height)
84-
{
85-
if (this.resources is RenderResources resources && resources.Width == width && resources.Height == height)
86-
{
87-
return resources;
88-
}
89-
90-
this.resources?.Dispose();
91-
this.resources = new RenderResources(new WebGPURenderTarget<Bgra32>(width, height), new Image<Bgra32>(width, height));
92-
return this.resources;
69+
this.renderTarget?.Dispose();
70+
this.renderTarget = null;
9371
}
9472

95-
private sealed class RenderResources : IDisposable
73+
private WebGPURenderTarget<Bgra32> EnsureRenderTarget(int width, int height)
9674
{
97-
public RenderResources(WebGPURenderTarget<Bgra32> renderTarget, Image<Bgra32> cpuImage)
75+
if (this.renderTarget is not null)
9876
{
99-
this.RenderTarget = renderTarget;
100-
this.CpuImage = cpuImage;
77+
return this.renderTarget;
10178
}
10279

103-
public WebGPURenderTarget<Bgra32> RenderTarget { get; }
104-
105-
public Image<Bgra32> CpuImage { get; }
106-
107-
public int Width => this.CpuImage.Width;
108-
109-
public int Height => this.CpuImage.Height;
110-
111-
public void Dispose()
112-
{
113-
this.RenderTarget.Dispose();
114-
this.CpuImage.Dispose();
115-
}
80+
this.renderTarget = new WebGPURenderTarget<Bgra32>(width, height);
81+
return this.renderTarget;
11682
}
11783
}

0 commit comments

Comments
 (0)