Skip to content

Commit 1045220

Browse files
Introduce FlattenedPointBuilder and rendering optimizations
1 parent 5c7fae3 commit 1045220

5 files changed

Lines changed: 162 additions & 37 deletions

File tree

samples/WebGPUWindowDemo/Program.cs

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ private sealed class DemoApp
4343
{
4444
private const int BallCount = 1000;
4545
private static readonly TimeSpan FpsUpdateInterval = TimeSpan.FromSeconds(1);
46+
private static readonly Brush BackgroundBrush = Brushes.Solid(Color.FromPixel(new Bgra32(30, 30, 40, 255)));
47+
private static readonly Brush TextBrush = Brushes.Solid(Color.FromPixel(new Bgra32(70, 70, 100, 255)));
4648

4749
private readonly WebGPUWindow<Bgra32> window;
4850
private readonly Random rng = new(42);
@@ -151,13 +153,12 @@ private void OnRender(WebGPUSurfaceFrame<Bgra32> frame)
151153
{
152154
DrawingCanvas canvas = frame.Canvas;
153155
Rectangle bounds = canvas.Bounds;
154-
canvas.Fill(Brushes.Solid(Color.FromPixel(new Bgra32(30, 30, 40, 255))));
156+
canvas.Fill(BackgroundBrush);
155157

156158
for (int i = 0; i < this.balls.Length; i++)
157159
{
158160
ref Ball ball = ref this.balls[i];
159-
EllipsePolygon ellipse = new(ball.X, ball.Y, ball.Radius);
160-
canvas.Fill(Brushes.Solid(ball.Color), ellipse);
161+
ball.Draw(canvas);
161162
}
162163

163164
this.DrawScrollingText(canvas, bounds.Width, bounds.Height);
@@ -193,7 +194,6 @@ private void DrawScrollingText(DrawingCanvas canvas, int width, int height)
193194

194195
Matrix3x2 translation = Matrix3x2.CreateTranslation(0, y);
195196
RectangleF viewport = new(0, 0, width, height);
196-
Brush textBrush = Brushes.Solid(Color.FromPixel(new Bgra32(70, 70, 100, 255)));
197197
DrawingOptions translatedOptions = new()
198198
{
199199
Transform = new Matrix4x4(translation),
@@ -215,7 +215,7 @@ private void DrawScrollingText(DrawingCanvas canvas, int width, int height)
215215
continue;
216216
}
217217

218-
canvas.Fill(textBrush, path);
218+
canvas.Fill(TextBrush, path);
219219
}
220220

221221
canvas.Restore();
@@ -232,7 +232,27 @@ private struct Ball
232232
public float VelocityX;
233233
public float VelocityY;
234234
public float Radius;
235-
public Color Color;
235+
private readonly Brush brush;
236+
private readonly EllipsePolygon shape;
237+
private readonly DrawingOptions drawingOptions;
238+
239+
private Ball(
240+
float x,
241+
float y,
242+
float velocityX,
243+
float velocityY,
244+
float radius,
245+
Brush brush)
246+
{
247+
this.X = x;
248+
this.Y = y;
249+
this.VelocityX = velocityX;
250+
this.VelocityY = velocityY;
251+
this.Radius = radius;
252+
this.brush = brush;
253+
this.shape = new EllipsePolygon(0, 0, radius);
254+
this.drawingOptions = new DrawingOptions();
255+
}
236256

237257
/// <summary>
238258
/// Creates a random ball that fits within the current framebuffer bounds.
@@ -244,19 +264,36 @@ private struct Ball
244264
public static Ball CreateRandom(Random rng, int width, int height)
245265
{
246266
float radius = 20F + (rng.NextSingle() * 40F);
247-
return new Ball
248-
{
249-
X = radius + (rng.NextSingle() * (width - (2 * radius))),
250-
Y = radius + (rng.NextSingle() * (height - (2 * radius))),
251-
VelocityX = (100F + (rng.NextSingle() * 200F)) * (rng.Next(2) == 0 ? -1 : 1),
252-
VelocityY = (100F + (rng.NextSingle() * 200F)) * (rng.Next(2) == 0 ? -1 : 1),
253-
Radius = radius,
254-
Color = Color.FromPixel(new Bgra32(
255-
(byte)(80 + rng.Next(176)),
256-
(byte)(80 + rng.Next(176)),
257-
(byte)(80 + rng.Next(176)),
258-
200)),
259-
};
267+
Color color = Color.FromPixel(new Bgra32(
268+
(byte)(80 + rng.Next(176)),
269+
(byte)(80 + rng.Next(176)),
270+
(byte)(80 + rng.Next(176)),
271+
200));
272+
273+
return new Ball(
274+
radius + (rng.NextSingle() * (width - (2 * radius))),
275+
radius + (rng.NextSingle() * (height - (2 * radius))),
276+
(100F + (rng.NextSingle() * 200F)) * (rng.Next(2) == 0 ? -1 : 1),
277+
(100F + (rng.NextSingle() * 200F)) * (rng.Next(2) == 0 ? -1 : 1),
278+
radius,
279+
Brushes.Solid(color));
280+
}
281+
282+
/// <summary>
283+
/// Draws the retained ball shape at its current animated location.
284+
/// </summary>
285+
/// <remarks>
286+
/// The ellipse is centered at the origin and moved through per-ball drawing options so the path geometry
287+
/// remains cached across frames without allocating a new shape each render.
288+
/// </remarks>
289+
/// <param name="canvas">The destination canvas for the current frame.</param>
290+
public void Draw(DrawingCanvas canvas)
291+
{
292+
this.drawingOptions.Transform = Matrix4x4.CreateTranslation(this.X, this.Y, 0);
293+
294+
canvas.Save(this.drawingOptions);
295+
canvas.Fill(this.brush, this.shape);
296+
canvas.Restore();
260297
}
261298

262299
/// <summary>

src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,10 @@ internal SupportedSubsetSceneEncoding(
163163
this.DrawData = new OwnedStream<uint>(allocator, Math.Max(commandCount, 16));
164164
this.Transforms = new OwnedStream<uint>(allocator, 8);
165165
this.Styles = new OwnedStream<uint>(allocator, Math.Max(commandCount * StyleWordCount, 16));
166-
this.GradientPixels = new OwnedStream<uint>(allocator, Math.Max(commandCount * GradientWidth, GradientWidth));
167-
this.PathGradientData = new OwnedStream<uint>(allocator, Math.Max(commandCount * PathGradientHeaderWordCount, 16));
166+
167+
// Gradient payloads are sparse in common scenes, so grow these only when a gradient brush is encoded.
168+
this.GradientPixels = new OwnedStream<uint>(allocator, 16);
169+
this.PathGradientData = new OwnedStream<uint>(allocator, 16);
168170
this.Images = [];
169171
this.FillCount = 0;
170172
this.PathCount = 0;

src/ImageSharp.Drawing/ArcLineSegment.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Numerics;
55
using System.Runtime.CompilerServices;
6-
using System.Runtime.InteropServices;
76

87
namespace SixLabors.ImageSharp.Drawing;
98

@@ -219,13 +218,16 @@ private static Vector2 EllipticArcPoint(Vector2 c, Vector2 r, float xAngle, floa
219218

220219
private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, Vector2 radius, float xAngle, float startAngle, float sweepAngle)
221220
{
222-
List<PointF> points = [];
223-
224221
float s = startAngle;
225222
float e = s + sweepAngle;
226223
bool neg = e < s;
227224
float sign = neg ? -1 : 1;
228225
float remain = Math.Abs(e - s);
226+
int curveCount = Math.Max((int)MathF.Ceiling(remain / (MathF.PI / 4F)), 1);
227+
228+
// Arc flattening retains the final point array, so use the builder to avoid the
229+
// intermediate collection and copy a list would generate.
230+
FlattenedPointBuilder points = new(curveCount * 4);
229231

230232
Vector2 prev = EllipticArcPoint(center, radius, xAngle, s);
231233

@@ -244,10 +246,9 @@ private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, V
244246

245247
CubicBezierLineSegment bezier = new(from, q1, q2, p2);
246248
int bezierCount = bezier.LinearVertexCount(Vector2.One);
247-
int startIndex = points.Count;
248-
CollectionsMarshal.SetCount(points, startIndex + bezierCount);
249-
Span<PointF> destination = CollectionsMarshal.AsSpan(points).Slice(startIndex, bezierCount);
249+
Span<PointF> destination = points.GetAppendSpan(bezierCount);
250250
bezier.CopyTo(destination, skipFirstPoint: false, Vector2.One);
251+
points.Advance(bezierCount);
251252

252253
from = p2;
253254

@@ -256,7 +257,7 @@ private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, V
256257
prev = p2;
257258
}
258259

259-
return [.. points];
260+
return points.Detach();
260261
}
261262

262263
private static void EndpointToCenterArcParams(

src/ImageSharp.Drawing/CubicBezierLineSegment.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@ public CubicBezierLineSegment Transform(Matrix4x4 matrix)
136136
private static PointF[] FlattenCurve(PointF[] controlPoints, Vector2 scale)
137137
{
138138
int curveCount = (controlPoints.Length - 1) / 3;
139-
List<PointF> output = new(curveCount * 4);
139+
140+
// Flattened points are cached as a retained array, so use the builder to avoid
141+
// the intermediate collection and copy a list would generate.
142+
FlattenedPointBuilder output = new(curveCount * 4);
140143

141144
for (int curveIndex = 0; curveIndex < curveCount; curveIndex++)
142145
{
@@ -148,14 +151,14 @@ private static PointF[] FlattenCurve(PointF[] controlPoints, Vector2 scale)
148151

149152
if (curveIndex == 0)
150153
{
151-
output.Add(p0);
154+
output.Add((PointF)p0);
152155
}
153156

154-
SubdivideAndAppend(0F, 1F, p0, p1, p2, p3, output, 0);
155-
output.Add(p3);
157+
SubdivideAndAppend(0F, 1F, p0, p1, p2, p3, ref output, 0);
158+
output.Add((PointF)p3);
156159
}
157160

158-
return [.. output];
161+
return output.Detach();
159162
}
160163

161164
/// <summary>
@@ -168,7 +171,7 @@ private static void SubdivideAndAppend(
168171
Vector2 p1,
169172
Vector2 p2,
170173
Vector2 p3,
171-
List<PointF> output,
174+
ref FlattenedPointBuilder output,
172175
int depth)
173176
{
174177
if (depth > 999)
@@ -192,9 +195,9 @@ private static void SubdivideAndAppend(
192195

193196
if (Vector2.Dot(leftDirection, rightDirection) > DivisionThreshold || Math.Abs(midT - 0.5f) < 0.0001f)
194197
{
195-
SubdivideAndAppend(t0, midT, p0, p1, p2, p3, output, depth + 1);
196-
output.Add(mid);
197-
SubdivideAndAppend(midT, t1, p0, p1, p2, p3, output, depth + 1);
198+
SubdivideAndAppend(t0, midT, p0, p1, p2, p3, ref output, depth + 1);
199+
output.Add((PointF)mid);
200+
SubdivideAndAppend(midT, t1, p0, p1, p2, p3, ref output, depth + 1);
198201
}
199202
}
200203

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Drawing;
5+
6+
/// <summary>
7+
/// Builds the retained <see cref="PointF"/> array used by flattened segment caches without an intermediate collection copy.
8+
/// </summary>
9+
/// <remarks>
10+
/// Segment flatteners ultimately need to return a tightly-sized array that can be cached by the segment instance.
11+
/// This builder owns that array while points are appended.
12+
/// </remarks>
13+
internal struct FlattenedPointBuilder
14+
{
15+
private PointF[] points;
16+
private int count;
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="FlattenedPointBuilder"/> struct.
20+
/// </summary>
21+
/// <param name="capacity">The estimated number of points that will be appended.</param>
22+
public FlattenedPointBuilder(int capacity)
23+
{
24+
this.points = new PointF[Math.Max(capacity, 4)];
25+
this.count = 0;
26+
}
27+
28+
/// <summary>
29+
/// Appends one point to the retained point array.
30+
/// </summary>
31+
/// <param name="point">The point to append.</param>
32+
public void Add(PointF point)
33+
{
34+
this.EnsureCapacity(this.count + 1);
35+
this.points[this.count++] = point;
36+
}
37+
38+
/// <summary>
39+
/// Reserves a writable append window for callers that populate multiple points directly.
40+
/// </summary>
41+
/// <param name="length">The number of points to reserve.</param>
42+
/// <returns>A span covering the reserved append window.</returns>
43+
public Span<PointF> GetAppendSpan(int length)
44+
{
45+
this.EnsureCapacity(this.count + length);
46+
return this.points.AsSpan(this.count, length);
47+
}
48+
49+
/// <summary>
50+
/// Commits points previously written through <see cref="GetAppendSpan"/>.
51+
/// </summary>
52+
/// <param name="length">The number of points written to the reserved append window.</param>
53+
public void Advance(int length) => this.count += length;
54+
55+
/// <summary>
56+
/// Returns the owned point array.
57+
/// </summary>
58+
/// <returns>The tightly-sized retained point array.</returns>
59+
public PointF[] Detach()
60+
{
61+
if (this.count != this.points.Length)
62+
{
63+
Array.Resize(ref this.points, this.count);
64+
}
65+
66+
return this.points;
67+
}
68+
69+
/// <summary>
70+
/// Ensures the owned array can store the requested total point count.
71+
/// </summary>
72+
/// <param name="capacity">The total number of points that must fit.</param>
73+
private void EnsureCapacity(int capacity)
74+
{
75+
if (capacity <= this.points.Length)
76+
{
77+
return;
78+
}
79+
80+
Array.Resize(ref this.points, Math.Max(capacity, this.points.Length * 2));
81+
}
82+
}

0 commit comments

Comments
 (0)