Skip to content

Commit a801d03

Browse files
committed
add Skia "benchmark backend" options to DrawingBackendBenchmark
1 parent e327130 commit a801d03

File tree

10 files changed

+990
-781
lines changed

10 files changed

+990
-781
lines changed
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SkiaSharp.Views.Desktop;
5+
using SixLabors.ImageSharp;
6+
using SixLabors.ImageSharp.PixelFormats;
7+
using Color = SixLabors.ImageSharp.Color;
8+
using PointF = SixLabors.ImageSharp.PointF;
9+
10+
namespace DrawingBackendBenchmark;
11+
12+
/// <summary>
13+
/// Interactive benchmark window for comparing the CPU and WebGPU drawing backends.
14+
/// </summary>
15+
internal sealed class BenchmarkForm : Form
16+
{
17+
private const int BenchmarkWidth = 600;
18+
private const int BenchmarkHeight = 400;
19+
20+
private readonly ComboBox backendSelector;
21+
private readonly NumericUpDown iterationSelector;
22+
private readonly TextBox statusTextBox;
23+
private readonly Panel previewHost;
24+
private readonly PictureBox previewBox;
25+
private readonly SKGLControl glControl;
26+
private int lastLineCount;
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="BenchmarkForm"/> class.
30+
/// </summary>
31+
public BenchmarkForm()
32+
{
33+
this.Text = "Drawing Backend Benchmark";
34+
this.ClientSize = new System.Drawing.Size(780, 560);
35+
this.StartPosition = FormStartPosition.CenterScreen;
36+
37+
FlowLayoutPanel toolbar = new()
38+
{
39+
Dock = DockStyle.Top,
40+
AutoSize = true,
41+
Padding = new Padding(8),
42+
};
43+
44+
this.backendSelector = new ComboBox
45+
{
46+
DropDownStyle = ComboBoxStyle.DropDownList,
47+
Width = 140,
48+
};
49+
50+
this.iterationSelector = new NumericUpDown
51+
{
52+
Minimum = 1,
53+
Maximum = 100,
54+
Value = 1,
55+
Width = 70,
56+
};
57+
58+
toolbar.Controls.Add(new Label { AutoSize = true, Text = "Backend:", Margin = new Padding(0, 8, 6, 0) });
59+
toolbar.Controls.Add(this.backendSelector);
60+
toolbar.Controls.Add(new Label { AutoSize = true, Text = "Iterations:", Margin = new Padding(12, 8, 6, 0) });
61+
toolbar.Controls.Add(this.iterationSelector);
62+
toolbar.Controls.Add(this.CreateRunButton("10", 10));
63+
toolbar.Controls.Add(this.CreateRunButton("1k", 1_000));
64+
toolbar.Controls.Add(this.CreateRunButton("10k", 10_000));
65+
toolbar.Controls.Add(this.CreateRunButton("100k", 100_000));
66+
67+
this.statusTextBox = new TextBox
68+
{
69+
Dock = DockStyle.Top,
70+
Multiline = true,
71+
ReadOnly = true,
72+
BorderStyle = BorderStyle.None,
73+
BackColor = SystemColors.Control,
74+
Height = 56,
75+
ScrollBars = ScrollBars.Vertical,
76+
Text = "Select a backend and run a benchmark.",
77+
};
78+
79+
this.previewHost = new Panel
80+
{
81+
Dock = DockStyle.Fill,
82+
AutoScroll = true,
83+
BackColor = System.Drawing.Color.FromArgb(24, 36, 56),
84+
Padding = new Padding(12),
85+
};
86+
87+
this.previewBox = new PictureBox
88+
{
89+
BackColor = System.Drawing.Color.FromArgb(24, 36, 56),
90+
SizeMode = PictureBoxSizeMode.Normal,
91+
Size = new System.Drawing.Size(BenchmarkWidth, BenchmarkHeight),
92+
};
93+
this.previewHost.Controls.Add(this.previewBox);
94+
95+
this.Controls.Add(this.previewHost);
96+
this.Controls.Add(this.statusTextBox);
97+
this.Controls.Add(toolbar);
98+
this.Resize += (_, _) => this.LayoutPreview();
99+
100+
this.glControl = new SKGLControl
101+
{
102+
Size = new System.Drawing.Size(1, 1),
103+
Visible = true,
104+
};
105+
this.Controls.Add(this.glControl);
106+
107+
// Initialize backends
108+
this.backendSelector.Items.Add(new CpuBenchmarkBackend());
109+
this.backendSelector.Items.Add(new SkiaSharpBenchmarkBackend());
110+
111+
if (WebGpuBenchmarkBackend.TryCreate(out WebGpuBenchmarkBackend? webGpuBackend, out string? error))
112+
{
113+
this.backendSelector.Items.Add(webGpuBackend);
114+
}
115+
else
116+
{
117+
this.statusTextBox.Text = $"WebGPU unavailable: {error}";
118+
}
119+
120+
this.glControl.PaintSurface += (_, _) =>
121+
{
122+
bool hasGpuBackend = this.backendSelector.Items.OfType<SkiaSharpBenchmarkBackend>().Any(b => b.IsGpu);
123+
if (!hasGpuBackend)
124+
{
125+
this.backendSelector.Items.Add(new SkiaSharpBenchmarkBackend(this.glControl.GRContext));
126+
}
127+
};
128+
129+
this.backendSelector.SelectedIndexChanged += (_, _) =>
130+
{
131+
if (this.lastLineCount > 0)
132+
{
133+
this.RunBenchmark(this.lastLineCount);
134+
}
135+
};
136+
137+
if (this.backendSelector.Items.Count > 0)
138+
{
139+
this.backendSelector.SelectedIndex = 0;
140+
}
141+
142+
this.LayoutPreview();
143+
this.Shown += (_, _) => this.backendSelector.Focus();
144+
}
145+
146+
/// <inheritdoc />
147+
protected override void Dispose(bool disposing)
148+
{
149+
if (disposing)
150+
{
151+
this.previewBox.Image?.Dispose();
152+
foreach (IDisposable backend in this.backendSelector.Items.OfType<IDisposable>())
153+
{
154+
backend.Dispose();
155+
}
156+
}
157+
158+
base.Dispose(disposing);
159+
}
160+
161+
/// <summary>
162+
/// Creates one toolbar button that runs the benchmark with the requested line count.
163+
/// </summary>
164+
private RadioButton CreateRunButton(string text, int lineCount)
165+
{
166+
RadioButton button = new()
167+
{
168+
AutoSize = true,
169+
Text = text,
170+
Appearance = Appearance.Button,
171+
Margin = new Padding(12, 0, 0, 0),
172+
};
173+
button.Click += (_, _) =>
174+
{
175+
this.lastLineCount = lineCount;
176+
this.RunBenchmark(lineCount);
177+
};
178+
return button;
179+
}
180+
181+
/// <summary>
182+
/// Executes one benchmark run for the selected backend and updates the preview and status text.
183+
/// </summary>
184+
private void RunBenchmark(int lineCount)
185+
{
186+
int iterations = (int)this.iterationSelector.Value;
187+
188+
if (this.backendSelector.SelectedItem is not IBenchmarkBackend backend)
189+
{
190+
return;
191+
}
192+
193+
Random rng = new(0);
194+
List<double> samples = new(iterations);
195+
196+
Cursor previousCursor = this.Cursor;
197+
this.Cursor = Cursors.WaitCursor;
198+
try
199+
{
200+
for (int i = 0; i < iterations; i++)
201+
{
202+
VisualLine[] lines = GenerateLines(lineCount, BenchmarkWidth, BenchmarkHeight, rng);
203+
bool capturePreview = i == iterations - 1;
204+
using BenchmarkRenderResult result = backend.Render(lines, BenchmarkWidth, BenchmarkHeight, capturePreview);
205+
206+
samples.Add(result.RenderMilliseconds);
207+
this.UpdatePreview(result, capturePreview);
208+
BenchmarkStatistics statistics = BenchmarkStatistics.FromSamples(samples);
209+
this.statusTextBox.Text = FormatStatusText(backend.ToString(), result, lineCount, i + 1, iterations, statistics);
210+
211+
Application.DoEvents();
212+
}
213+
}
214+
catch (Exception ex)
215+
{
216+
this.statusTextBox.Text = $"{backend} failed: {ex.Message}";
217+
}
218+
finally
219+
{
220+
this.Cursor = previousCursor;
221+
}
222+
}
223+
224+
/// <summary>
225+
/// Lays out the fixed-size preview surface in the middle of the scroll host.
226+
/// </summary>
227+
private void LayoutPreview()
228+
{
229+
int x = Math.Max(this.previewHost.Padding.Left, (this.previewHost.ClientSize.Width - this.previewBox.Width) / 2);
230+
int y = Math.Max(this.previewHost.Padding.Top, (this.previewHost.ClientSize.Height - this.previewBox.Height) / 2);
231+
this.previewBox.Location = new System.Drawing.Point(x, y);
232+
}
233+
234+
/// <summary>
235+
/// Replaces the preview image with the final captured frame from the current run.
236+
/// </summary>
237+
private void UpdatePreview(BenchmarkRenderResult result, bool capturePreview)
238+
{
239+
if (!capturePreview || result.Preview is null)
240+
{
241+
return;
242+
}
243+
244+
this.previewBox.Image?.Dispose();
245+
this.previewBox.Image = ToBitmap(result.Preview);
246+
}
247+
248+
/// <summary>
249+
/// Formats one status line describing the current sample, running statistics, and backend outcome.
250+
/// </summary>
251+
private static string FormatStatusText(
252+
string backendName,
253+
BenchmarkRenderResult result,
254+
int lineCount,
255+
int iteration,
256+
int totalIterations,
257+
BenchmarkStatistics statistics)
258+
{
259+
string backendStatus = GetBackendStatusText(backendName, result);
260+
string backendFailure = result.BackendFailure is not null ? $" | {result.BackendFailure}" : string.Empty;
261+
262+
return
263+
$"{backendName} ({backendStatus}) | Lines: {lineCount:N0} | Render {iteration:N0}/{totalIterations:N0} | " +
264+
$"Current: {result.RenderMilliseconds:0.000} ms | Mean: {statistics.MeanMilliseconds:0.000} ms | StdDev: {statistics.StdDevMilliseconds:0.000} ms{backendFailure}";
265+
}
266+
267+
/// <summary>
268+
/// Converts the backend result into the short status label shown next to the backend name.
269+
/// </summary>
270+
private static string GetBackendStatusText(string backendName, BenchmarkRenderResult result)
271+
{
272+
if (result.BackendFailure is not null)
273+
{
274+
return $"Failed: {result.BackendFailure}";
275+
}
276+
277+
if (result.UsedGpu)
278+
{
279+
return "GPU";
280+
}
281+
282+
return backendName == "WebGPU" ? "CPU fallback" : "CPU";
283+
}
284+
285+
/// <summary>
286+
/// Generates the random line set used by one benchmark iteration.
287+
/// </summary>
288+
private static VisualLine[] GenerateLines(int lineCount, int width, int height, Random rng)
289+
{
290+
VisualLine[] lines = new VisualLine[lineCount];
291+
for (int i = 0; i < lines.Length; i++)
292+
{
293+
lines[i] = new VisualLine(
294+
new PointF((float)(rng.NextDouble() * width), (float)(rng.NextDouble() * height)),
295+
new PointF((float)(rng.NextDouble() * width), (float)(rng.NextDouble() * height)),
296+
Color.FromPixel(new Rgba32(
297+
(byte)rng.Next(255),
298+
(byte)rng.Next(255),
299+
(byte)rng.Next(255),
300+
(byte)rng.Next(255))),
301+
rng.Next(1, 10));
302+
}
303+
304+
return lines;
305+
}
306+
307+
/// <summary>
308+
/// Converts the ImageSharp preview image into a WinForms bitmap.
309+
/// </summary>
310+
private static Bitmap ToBitmap(Image<Bgra32> image)
311+
{
312+
using MemoryStream stream = new();
313+
image.SaveAsBmp(stream);
314+
stream.Position = 0;
315+
using Bitmap decoded = new(stream);
316+
return new Bitmap(decoded);
317+
}
318+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.ImageSharp;
5+
using SixLabors.ImageSharp.PixelFormats;
6+
7+
namespace DrawingBackendBenchmark;
8+
9+
/// <summary>
10+
/// One completed benchmark render, including timing, optional preview pixels, and backend diagnostics.
11+
/// </summary>
12+
internal sealed class BenchmarkRenderResult : IDisposable
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="BenchmarkRenderResult"/> class.
16+
/// </summary>
17+
public BenchmarkRenderResult(double renderMilliseconds, Image<Bgra32>? preview, bool usedGpu = false, string? backendFailure = null)
18+
{
19+
this.RenderMilliseconds = renderMilliseconds;
20+
this.Preview = preview;
21+
this.UsedGpu = usedGpu;
22+
this.BackendFailure = backendFailure;
23+
}
24+
25+
/// <summary>
26+
/// Gets the elapsed render time for this iteration.
27+
/// </summary>
28+
public double RenderMilliseconds { get; }
29+
30+
/// <summary>
31+
/// Gets the optional preview image captured for the UI.
32+
/// </summary>
33+
public Image<Bgra32>? Preview { get; }
34+
35+
/// <summary>
36+
/// Gets a value indicating whether the WebGPU backend completed on the staged GPU path.
37+
/// </summary>
38+
public bool UsedGpu { get; }
39+
40+
/// <summary>
41+
/// Gets the backend failure or fallback reason, when one was reported.
42+
/// </summary>
43+
public string? BackendFailure { get; }
44+
45+
/// <inheritdoc />
46+
public void Dispose() => this.Preview?.Dispose();
47+
}

0 commit comments

Comments
 (0)