Skip to content

Commit 819bc18

Browse files
Optimize Bezier subdivision and list allocation
1 parent 355b56c commit 819bc18

5 files changed

Lines changed: 40 additions & 80 deletions

File tree

src/ImageSharp.Drawing/CubicBezierLineSegment.cs

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -109,56 +109,41 @@ private static PointF[] GetDrawingPoints(PointF[] controlPoints)
109109

110110
for (int curveIndex = 0; curveIndex < curveCount; curveIndex++)
111111
{
112-
List<PointF> bezierCurveDrawingPoints = FindDrawingPoints(curveIndex, controlPoints);
113-
114-
if (curveIndex != 0)
112+
if (curveIndex == 0)
115113
{
116-
// remove the fist point, as it coincides with the last point of the previous Bezier curve.
117-
bezierCurveDrawingPoints.RemoveAt(0);
114+
drawingPoints.Add(CalculateBezierPoint(curveIndex, 0, controlPoints));
118115
}
119116

120-
drawingPoints.AddRange(bezierCurveDrawingPoints);
117+
SubdivideAndAppend(curveIndex, 0, 1, controlPoints, drawingPoints, 0);
118+
drawingPoints.Add(CalculateBezierPoint(curveIndex, 1, controlPoints));
121119
}
122120

123121
return [.. drawingPoints];
124122
}
125123

126-
private static List<PointF> FindDrawingPoints(int curveIndex, PointF[] controlPoints)
127-
{
128-
List<PointF> pointList = [];
129-
130-
Vector2 left = CalculateBezierPoint(curveIndex, 0, controlPoints);
131-
Vector2 right = CalculateBezierPoint(curveIndex, 1, controlPoints);
132-
133-
pointList.Add(left);
134-
pointList.Add(right);
135-
136-
FindDrawingPoints(curveIndex, 0, 1, pointList, 1, controlPoints, 0);
137-
138-
return pointList;
139-
}
140-
141-
private static int FindDrawingPoints(
124+
/// <summary>
125+
/// Recursively subdivides a cubic bezier curve segment and appends points in left-to-right order.
126+
/// Points are appended (not inserted), avoiding O(n) shifts per point.
127+
/// </summary>
128+
private static void SubdivideAndAppend(
142129
int curveIndex,
143130
float t0,
144131
float t1,
145-
List<PointF> pointList,
146-
int insertionIndex,
147132
PointF[] controlPoints,
133+
List<PointF> output,
148134
int depth)
149135
{
150-
// max recursive depth for control points, means this is approx the max number of points discoverable
151136
if (depth > 999)
152137
{
153-
return 0;
138+
return;
154139
}
155140

156141
Vector2 left = CalculateBezierPoint(curveIndex, t0, controlPoints);
157142
Vector2 right = CalculateBezierPoint(curveIndex, t1, controlPoints);
158143

159144
if ((left - right).LengthSquared() < MinimumSqrDistance)
160145
{
161-
return 0;
146+
return;
162147
}
163148

164149
float midT = (t0 + t1) / 2;
@@ -169,17 +154,11 @@ private static int FindDrawingPoints(
169154

170155
if (Vector2.Dot(leftDirection, rightDirection) > DivisionThreshold || Math.Abs(midT - 0.5f) < 0.0001f)
171156
{
172-
int pointsAddedCount = 0;
173-
174-
pointsAddedCount += FindDrawingPoints(curveIndex, t0, midT, pointList, insertionIndex, controlPoints, depth + 1);
175-
pointList.Insert(insertionIndex + pointsAddedCount, mid);
176-
pointsAddedCount++;
177-
pointsAddedCount += FindDrawingPoints(curveIndex, midT, t1, pointList, insertionIndex + pointsAddedCount, controlPoints, depth + 1);
178-
179-
return pointsAddedCount;
157+
// Recurse left half, emit midpoint, recurse right half — all in order.
158+
SubdivideAndAppend(curveIndex, t0, midT, controlPoints, output, depth + 1);
159+
output.Add(mid);
160+
SubdivideAndAppend(curveIndex, midT, t1, controlPoints, output, depth + 1);
180161
}
181-
182-
return 0;
183162
}
184163

185164
private static PointF CalculateBezierPoint(int curveIndex, float t, PointF[] controlPoints)

src/ImageSharp.Drawing/InternalPath.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,14 @@ private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vecto
223223
/// </returns>
224224
private static PointData[] Simplify(IReadOnlyList<ILineSegment> segments, bool isClosed, bool removeCloseAndCollinear)
225225
{
226-
List<PointF> simplified = new(segments.Count);
226+
// Pre-compute capacity from cached flattened lengths to avoid List resizing.
227+
int totalPoints = 0;
228+
for (int s = 0; s < segments.Count; s++)
229+
{
230+
totalPoints += segments[s].Flatten().Length;
231+
}
232+
233+
List<PointF> simplified = new(totalPoints);
227234

228235
// Track indices where collinear direction reversals represent user-intended
229236
// geometry: interior points of multi-point linear segments, and junction

tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs

Lines changed: 12 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,6 @@ public void DrawPath_Stroke_MatchesDefaultOutput<TPixel>(TestImageProvider<TPixe
882882
using Image<TPixel> defaultImage = provider.GetImage();
883883
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
884884

885-
886885
using WebGPUDrawingBackend nativeSurfaceBackend = new();
887886
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
888887
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -940,7 +939,6 @@ public void DrawPath_Stroke_LineJoin_MatchesDefaultOutput<TPixel>(TestImageProvi
940939
using Image<TPixel> defaultImage = provider.GetImage();
941940
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
942941

943-
944942
using WebGPUDrawingBackend nativeSurfaceBackend = new();
945943
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
946944
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -985,7 +983,6 @@ public void DrawPath_Stroke_LineCap_MatchesDefaultOutput<TPixel>(TestImageProvid
985983
using Image<TPixel> defaultImage = provider.GetImage();
986984
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
987985

988-
989986
using WebGPUDrawingBackend nativeSurfaceBackend = new();
990987
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
991988
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1026,7 +1023,6 @@ void DrawAction(DrawingCanvas<TPixel> canvas)
10261023
using Image<TPixel> defaultImage = provider.GetImage();
10271024
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
10281025

1029-
10301026
using WebGPUDrawingBackend nativeSurfaceBackend = new();
10311027
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
10321028
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1039,7 +1035,6 @@ void DrawAction(DrawingCanvas<TPixel> canvas)
10391035

10401036
DebugSaveBackendPair(provider, "FillPath_MultipleSeparate", defaultImage, nativeSurfaceImage);
10411037

1042-
10431038
AssertCoverageExecutionAccounting(nativeSurfaceBackend);
10441039
AssertGpuPathWhenRequired(nativeSurfaceBackend);
10451040
AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F);
@@ -1088,7 +1083,6 @@ public void FillPath_EvenOddRule_MatchesDefaultOutput<TPixel>(TestImageProvider<
10881083
using Image<TPixel> defaultImage = provider.GetImage();
10891084
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
10901085

1091-
10921086
using WebGPUDrawingBackend nativeSurfaceBackend = new();
10931087
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
10941088
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1126,7 +1120,6 @@ public void FillPath_LargeTileCount_MatchesDefaultOutput<TPixel>(TestImageProvid
11261120
using Image<TPixel> defaultImage = provider.GetImage();
11271121
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
11281122

1129-
11301123
using WebGPUDrawingBackend nativeSurfaceBackend = new();
11311124
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
11321125
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1262,7 +1255,6 @@ public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput<TPixel>(TestIm
12621255
using Image<TPixel> defaultImage = provider.GetImage();
12631256
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
12641257

1265-
12661258
using WebGPUDrawingBackend nativeSurfaceBackend = new();
12671259
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
12681260
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1303,7 +1295,6 @@ public void FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput<TPixel>
13031295
using Image<TPixel> defaultImage = provider.GetImage();
13041296
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
13051297

1306-
13071298
using WebGPUDrawingBackend nativeSurfaceBackend = new();
13081299
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
13091300
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1343,7 +1334,6 @@ public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput<T
13431334
using Image<TPixel> defaultImage = provider.GetImage();
13441335
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
13451336

1346-
13471337
using WebGPUDrawingBackend nativeSurfaceBackend = new();
13481338
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
13491339
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1385,7 +1375,6 @@ public void FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput<TPix
13851375
using Image<TPixel> defaultImage = provider.GetImage();
13861376
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
13871377

1388-
13891378
using WebGPUDrawingBackend nativeSurfaceBackend = new();
13901379
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
13911380
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1426,7 +1415,6 @@ public void FillPath_WithEllipticGradientBrush_MatchesDefaultOutput<TPixel>(Test
14261415
using Image<TPixel> defaultImage = provider.GetImage();
14271416
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
14281417

1429-
14301418
using WebGPUDrawingBackend nativeSurfaceBackend = new();
14311419
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
14321420
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1469,7 +1457,6 @@ public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput<TPixel>(TestIma
14691457
using Image<TPixel> defaultImage = provider.GetImage();
14701458
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
14711459

1472-
14731460
using WebGPUDrawingBackend nativeSurfaceBackend = new();
14741461
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
14751462
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1510,7 +1497,6 @@ public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput<TPix
15101497
using Image<TPixel> defaultImage = provider.GetImage();
15111498
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
15121499

1513-
15141500
using WebGPUDrawingBackend nativeSurfaceBackend = new();
15151501
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
15161502
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1551,7 +1537,6 @@ public void FillPath_WithPatternBrush_MatchesDefaultOutput<TPixel>(TestImageProv
15511537
using Image<TPixel> defaultImage = provider.GetImage();
15521538
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
15531539

1554-
15551540
using WebGPUDrawingBackend nativeSurfaceBackend = new();
15561541
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
15571542
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1586,7 +1571,6 @@ public void FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput<TPixel>(Test
15861571
using Image<TPixel> defaultImage = provider.GetImage();
15871572
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
15881573

1589-
15901574
using WebGPUDrawingBackend nativeSurfaceBackend = new();
15911575
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
15921576
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1621,7 +1605,6 @@ public void FillPath_WithRecolorBrush_MatchesDefaultOutput<TPixel>(TestImageProv
16211605
using Image<TPixel> defaultImage = provider.GetImage();
16221606
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
16231607

1624-
16251608
using WebGPUDrawingBackend nativeSurfaceBackend = new();
16261609
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
16271610
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1662,7 +1645,6 @@ public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput<TPi
16621645
using Image<TPixel> defaultImage = provider.GetImage();
16631646
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
16641647

1665-
16661648
using WebGPUDrawingBackend nativeSurfaceBackend = new();
16671649
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
16681650
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1704,7 +1686,6 @@ public void FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput<TPix
17041686
using Image<TPixel> defaultImage = provider.GetImage();
17051687
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
17061688

1707-
17081689
using WebGPUDrawingBackend nativeSurfaceBackend = new();
17091690
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
17101691
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1777,10 +1758,10 @@ their first victory against
17771758
Rectangle sternBounds = new(0, 0, 300, 80);
17781759
Matrix4x4 sternTransform = transformBuilder
17791760
.AppendQuadDistortion(
1780-
topLeft: new PointF(50, 80),
1781-
topRight: new PointF(400, 90),
1782-
bottomRight: new PointF(390, 135),
1783-
bottomLeft: new PointF(60, 140))
1761+
topLeft: new PointF(70, 80),
1762+
topRight: new PointF(380, 90),
1763+
bottomRight: new PointF(400, 135),
1764+
bottomLeft: new PointF(50, 140))
17841765
.BuildMatrix(sternBounds);
17851766

17861767
PointF[] bottomHull =
@@ -1793,10 +1774,10 @@ their first victory against
17931774
Rectangle hullBounds = new(0, 0, 300, 80);
17941775
Matrix4x4 hullTransform = transformBuilder.Clear()
17951776
.AppendQuadDistortion(
1796-
topLeft: new PointF(60, 140),
1797-
topRight: new PointF(390, 135),
1798-
bottomRight: new PointF(300, 160),
1799-
bottomLeft: new PointF(-30, 170))
1777+
topLeft: new PointF(50, 140),
1778+
topRight: new PointF(400, 135),
1779+
bottomRight: new PointF(310, 170),
1780+
bottomLeft: new PointF(-40, 170))
18001781
.BuildMatrix(hullBounds);
18011782

18021783
PointF[] towerStem =
@@ -1933,7 +1914,6 @@ void DrawAction(DrawingCanvas<TPixel> canvas)
19331914
using Image<TPixel> defaultImage = provider.GetImage();
19341915
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
19351916

1936-
19371917
using WebGPUDrawingBackend nativeSurfaceBackend = new();
19381918
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
19391919
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -1970,7 +1950,6 @@ void DrawAction(DrawingCanvas<TPixel> canvas)
19701950
using Image<TPixel> defaultImage = provider.GetImage();
19711951
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
19721952

1973-
19741953
using WebGPUDrawingBackend nativeSurfaceBackend = new();
19751954
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
19761955
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -2005,7 +1984,6 @@ void DrawAction(DrawingCanvas<TPixel> canvas)
20051984
using Image<TPixel> defaultImage = provider.GetImage();
20061985
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
20071986

2008-
20091987
using WebGPUDrawingBackend nativeSurfaceBackend = new();
20101988
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
20111989
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -2027,7 +2005,7 @@ public void SaveLayer_NestedLayers_MatchesDefaultOutput<TPixel>(TestImageProvide
20272005
{
20282006
DrawingOptions drawingOptions = new();
20292007

2030-
void DrawAction(DrawingCanvas<TPixel> canvas)
2008+
static void DrawAction(DrawingCanvas<TPixel> canvas)
20312009
{
20322010
canvas.Fill(Brushes.Solid(Color.White));
20332011

@@ -2046,7 +2024,6 @@ void DrawAction(DrawingCanvas<TPixel> canvas)
20462024
using Image<TPixel> defaultImage = provider.GetImage();
20472025
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
20482026

2049-
20502027
using WebGPUDrawingBackend nativeSurfaceBackend = new();
20512028
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
20522029
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -2068,7 +2045,7 @@ public void SaveLayer_WithBlendMode_MatchesDefaultOutput<TPixel>(TestImageProvid
20682045
{
20692046
DrawingOptions drawingOptions = new();
20702047

2071-
void DrawAction(DrawingCanvas<TPixel> canvas)
2048+
static void DrawAction(DrawingCanvas<TPixel> canvas)
20722049
{
20732050
canvas.Fill(Brushes.Solid(Color.White));
20742051
canvas.Fill(Brushes.Solid(Color.Red), new RectangularPolygon(20, 20, 88, 88));
@@ -2087,7 +2064,6 @@ void DrawAction(DrawingCanvas<TPixel> canvas)
20872064
using Image<TPixel> defaultImage = provider.GetImage();
20882065
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
20892066

2090-
20912067
using WebGPUDrawingBackend nativeSurfaceBackend = new();
20922068
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
20932069
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -2109,7 +2085,7 @@ public void SaveLayer_WithBounds_MatchesDefaultOutput<TPixel>(TestImageProvider<
21092085
{
21102086
DrawingOptions drawingOptions = new();
21112087

2112-
void DrawAction(DrawingCanvas<TPixel> canvas)
2088+
static void DrawAction(DrawingCanvas<TPixel> canvas)
21132089
{
21142090
canvas.Fill(Brushes.Solid(Color.White));
21152091

@@ -2122,7 +2098,6 @@ void DrawAction(DrawingCanvas<TPixel> canvas)
21222098
using Image<TPixel> defaultImage = provider.GetImage();
21232099
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
21242100

2125-
21262101
using WebGPUDrawingBackend nativeSurfaceBackend = new();
21272102
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
21282103
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
@@ -2144,7 +2119,7 @@ public void SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput<TPixel>(TestIma
21442119
{
21452120
DrawingOptions drawingOptions = new();
21462121

2147-
void DrawAction(DrawingCanvas<TPixel> canvas)
2122+
static void DrawAction(DrawingCanvas<TPixel> canvas)
21482123
{
21492124
canvas.Fill(Brushes.Solid(Color.White));
21502125

@@ -2161,7 +2136,6 @@ void DrawAction(DrawingCanvas<TPixel> canvas)
21612136
using Image<TPixel> defaultImage = provider.GetImage();
21622137
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
21632138

2164-
21652139
using WebGPUDrawingBackend nativeSurfaceBackend = new();
21662140
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
21672141
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading

0 commit comments

Comments
 (0)