-
Notifications
You must be signed in to change notification settings - Fork 61
Expand file tree
/
Copy pathProgram.cs
More file actions
334 lines (299 loc) · 12.8 KB
/
Program.cs
File metadata and controls
334 lines (299 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Numerics;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Drawing.Processing.Backends;
using SixLabors.ImageSharp.Drawing.Text;
using SixLabors.ImageSharp.PixelFormats;
using Color = SixLabors.ImageSharp.Color;
namespace SixLabors.Samples.WebGPUWindowDemo;
/// <summary>
/// Demonstrates the ImageSharp.Drawing WebGPU backend rendering directly to a native window.
/// </summary>
public static class Program
{
/// <summary>
/// Creates the demo window and runs the animated sample scene.
/// </summary>
public static void Main()
{
// FIFO is the safest sample default: it presents in display order with normal v-sync behavior.
using WebGPUWindow window = new(new WebGPUWindowOptions
{
Title = "ImageSharp.Drawing WebGPU Demo",
Size = new Size(800, 600),
Format = WebGPUTextureFormat.Bgra8Unorm,
PresentMode = WebGPUPresentMode.Fifo,
});
DemoApp app = new(window);
app.Run();
}
/// <summary>
/// Owns the sample scene, animation state, and render loop callbacks for the demo window.
/// </summary>
private sealed class DemoApp
{
private const int BallCount = 1000;
private static readonly TimeSpan FpsUpdateInterval = TimeSpan.FromSeconds(1);
private static readonly Brush BackgroundBrush = Brushes.Solid(Color.FromPixel(new Bgra32(30, 30, 40, 255)));
private static readonly Brush TextBrush = Brushes.Solid(Color.FromPixel(new Bgra32(70, 70, 100, 255)));
private readonly WebGPUWindow window;
private readonly Random rng = new(42);
private readonly Stopwatch fpsWindow = Stopwatch.StartNew();
private Ball[] balls = [];
private int frameCount;
private IPathCollection scrollPaths = new PathCollection();
private float scrollOffset;
private float scrollTextHeight;
private const string ScrollText =
"ImageSharp.Drawing on WebGPU\n\n" +
"Real-time GPU-accelerated 2D vector graphics " +
"rendered directly to a native window.\n\n" +
"The canvas API provides a familiar drawing model: " +
"Fill, Draw, DrawText, Clip, and Transform - " +
"all composited on the GPU via compute shaders.\n\n" +
"Text is shaped once using SixLabors.Fonts and " +
"converted to vector paths via TextBuilder. " +
"Each frame simply translates the cached geometry.\n\n" +
"Shapes are rasterized into coverage masks on the " +
"CPU, uploaded to GPU textures, then composited " +
"using a WebGPU compute pipeline that evaluates " +
"Porter-Duff blending per pixel.\n\n" +
"The drawing backend automatically manages texture " +
"atlases, bind groups, and pipeline state for the " +
"native target selected by the window.\n\n" +
"SixLabors ImageSharp.Drawing\n" +
"github.com/SixLabors/ImageSharp.Drawing\n\n" +
"Built on the WebGPUWindow wrapper.";
/// <summary>
/// Initializes a new instance of the <see cref="DemoApp"/> class.
/// </summary>
/// <param name="window">The demo window that supplies update and render callbacks.</param>
public DemoApp(WebGPUWindow window)
{
this.window = window;
this.window.Update += this.OnUpdate;
this.InitializeScene();
}
/// <summary>
/// Starts the window-owned render loop for the demo.
/// </summary>
public void Run() => this.window.Run(this.OnRender);
/// <summary>
/// Builds the one-time scene state used by the demo.
/// </summary>
/// <remarks>
/// The scrolling text is shaped once up front and reused every frame so the steady-state loop only applies
/// a translation and submits visible paths.
/// </remarks>
private void InitializeScene()
{
Font scrollFont = SystemFonts.CreateFont("Arial", 24);
Size framebufferSize = this.window.FramebufferSize;
TextOptions textOptions = new(scrollFont)
{
Origin = new Vector2(framebufferSize.Width / 2F, 0),
WrappingLength = framebufferSize.Width - 80,
HorizontalAlignment = HorizontalAlignment.Center,
LineSpacing = 1.6F,
};
this.scrollPaths = TextBuilder.GeneratePaths(ScrollText, textOptions);
FontRectangle bounds = TextMeasurer.MeasureBounds(ScrollText, textOptions);
FontRectangle size = new(0, 0, bounds.Width, bounds.Height);
this.scrollTextHeight = size.Height;
Ball[] balls = new Ball[BallCount];
for (int i = 0; i < balls.Length; i++)
{
balls[i] = Ball.CreateRandom(this.rng, framebufferSize.Width, framebufferSize.Height);
}
this.balls = balls;
}
/// <summary>
/// Advances the animation state for the next frame.
/// </summary>
/// <param name="deltaTime">Elapsed time since the previous update.</param>
private void OnUpdate(TimeSpan deltaTime)
{
Size framebufferSize = this.window.FramebufferSize;
float dt = (float)deltaTime.TotalSeconds;
for (int i = 0; i < this.balls.Length; i++)
{
this.balls[i].Update(dt, framebufferSize.Width, framebufferSize.Height);
}
this.scrollOffset += 200F * dt;
}
/// <summary>
/// Draws one frame into the acquired WebGPU window surface.
/// </summary>
/// <param name="frame">The acquired frame that exposes the drawing canvas.</param>
/// <remarks>
/// The window loop disposes the frame after this callback returns, which renders queued canvas work,
/// presents the swapchain texture, and releases the per-frame WebGPU handles.
/// </remarks>
private void OnRender(WebGPUSurfaceFrame frame)
{
DrawingCanvas canvas = frame.Canvas;
Rectangle bounds = canvas.Bounds;
canvas.Fill(BackgroundBrush);
for (int i = 0; i < this.balls.Length; i++)
{
ref Ball ball = ref this.balls[i];
ball.Draw(canvas);
}
this.DrawScrollingText(canvas, bounds.Width, bounds.Height);
this.frameCount++;
TimeSpan elapsed = this.fpsWindow.Elapsed;
if (elapsed >= FpsUpdateInterval)
{
double fps = this.frameCount / elapsed.TotalSeconds;
double frameTimeMs = elapsed.TotalMilliseconds / this.frameCount;
this.window.Title = $"ImageSharp.Drawing WebGPU Demo - {frameTimeMs:F1} ms / {fps:F1} FPS";
this.frameCount = 0;
this.fpsWindow.Restart();
}
}
/// <summary>
/// Draws the cached scrolling text block with simple viewport culling.
/// </summary>
/// <param name="canvas">The destination canvas for the current frame.</param>
/// <param name="width">The current framebuffer width.</param>
/// <param name="height">The current framebuffer height.</param>
private void DrawScrollingText(DrawingCanvas canvas, int width, int height)
{
if (this.scrollTextHeight <= 0)
{
return;
}
float totalCycle = height + this.scrollTextHeight;
float wrappedOffset = this.scrollOffset % totalCycle;
float y = height - wrappedOffset;
Matrix3x2 translation = Matrix3x2.CreateTranslation(0, y);
RectangleF viewport = new(0, 0, width, height);
DrawingOptions translatedOptions = new()
{
Transform = new Matrix4x4(translation),
};
// Save once with the frame-local translation, then submit only paths that are actually visible.
canvas.Save(translatedOptions);
foreach (IPath path in this.scrollPaths)
{
RectangleF pathBounds = path.Bounds;
RectangleF translated = new(
pathBounds.X + translation.M31,
pathBounds.Y + translation.M32,
pathBounds.Width,
pathBounds.Height);
if (!viewport.IntersectsWith(translated))
{
continue;
}
canvas.Fill(TextBrush, path);
}
canvas.Restore();
}
}
/// <summary>
/// Mutable physics state for one animated ball in the sample scene.
/// </summary>
private struct Ball
{
public float X;
public float Y;
public float VelocityX;
public float VelocityY;
public float Radius;
private readonly Brush brush;
private readonly EllipsePolygon shape;
private readonly DrawingOptions drawingOptions;
private Ball(
float x,
float y,
float velocityX,
float velocityY,
float radius,
Brush brush)
{
this.X = x;
this.Y = y;
this.VelocityX = velocityX;
this.VelocityY = velocityY;
this.Radius = radius;
this.brush = brush;
this.shape = new EllipsePolygon(0, 0, radius);
this.drawingOptions = new DrawingOptions();
}
/// <summary>
/// Creates a random ball that fits within the current framebuffer bounds.
/// </summary>
/// <param name="rng">The deterministic random source used by the sample.</param>
/// <param name="width">The framebuffer width.</param>
/// <param name="height">The framebuffer height.</param>
/// <returns>The initialized ball.</returns>
public static Ball CreateRandom(Random rng, int width, int height)
{
float radius = 20F + (rng.NextSingle() * 40F);
Color color = Color.FromPixel(new Bgra32(
(byte)(80 + rng.Next(176)),
(byte)(80 + rng.Next(176)),
(byte)(80 + rng.Next(176)),
200));
return new Ball(
radius + (rng.NextSingle() * (width - (2 * radius))),
radius + (rng.NextSingle() * (height - (2 * radius))),
(100F + (rng.NextSingle() * 200F)) * (rng.Next(2) == 0 ? -1 : 1),
(100F + (rng.NextSingle() * 200F)) * (rng.Next(2) == 0 ? -1 : 1),
radius,
Brushes.Solid(color));
}
/// <summary>
/// Draws the retained ball shape at its current animated location.
/// </summary>
/// <remarks>
/// The ellipse is centered at the origin and moved through per-ball drawing options so the path geometry
/// remains cached across frames without allocating a new shape each render.
/// </remarks>
/// <param name="canvas">The destination canvas for the current frame.</param>
public readonly void Draw(DrawingCanvas canvas)
{
this.drawingOptions.Transform = Matrix4x4.CreateTranslation(this.X, this.Y, 0);
canvas.Save(this.drawingOptions);
canvas.Fill(this.brush, this.shape);
canvas.Restore();
}
/// <summary>
/// Advances the ball and reflects it off the framebuffer edges.
/// </summary>
/// <param name="dt">Elapsed time since the previous update, in seconds.</param>
/// <param name="width">The framebuffer width.</param>
/// <param name="height">The framebuffer height.</param>
public void Update(float dt, int width, int height)
{
this.X += this.VelocityX * dt;
this.Y += this.VelocityY * dt;
if (this.X - this.Radius < 0)
{
this.X = this.Radius;
this.VelocityX = MathF.Abs(this.VelocityX);
}
else if (this.X + this.Radius > width)
{
this.X = width - this.Radius;
this.VelocityX = -MathF.Abs(this.VelocityX);
}
if (this.Y - this.Radius < 0)
{
this.Y = this.Radius;
this.VelocityY = MathF.Abs(this.VelocityY);
}
else if (this.Y + this.Radius > height)
{
this.Y = height - this.Radius;
this.VelocityY = -MathF.Abs(this.VelocityY);
}
}
}
}