Skip to content

Commit e146a2b

Browse files
Refactor path flattening and remove tessellation
1 parent 7a08695 commit e146a2b

19 files changed

Lines changed: 154 additions & 369 deletions

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CoverageRasterizer.cs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -617,20 +617,23 @@ private static bool TryBuildFixedPointEdges(
617617
int interestY = interest.Y;
618618
int bandCount = (int)DivideRoundUp(height, TileHeight);
619619

620-
// Pass 1: Flatten path and count edges per band.
620+
// Flatten once and reuse the list for both passes.
621+
IEnumerable<ISimplePath> flattened = path.Flatten();
622+
IReadOnlyList<ISimplePath> contours = flattened is IReadOnlyList<ISimplePath> list
623+
? list
624+
: [.. flattened];
625+
626+
// Pass 1: Count edges per band.
621627
int totalSubEdges = 0;
622628
using IMemoryOwner<int> bandCountsOwner = allocator.Allocate<int>(bandCount, AllocationOptions.Clean);
623629
Span<int> bandCounts = bandCountsOwner.Memory.Span;
624630

625-
foreach (ISimplePath simplePath in path.Flatten())
631+
for (int c = 0; c < contours.Count; c++)
626632
{
633+
ISimplePath simplePath = contours[c];
627634
ReadOnlySpan<PointF> points = simplePath.Points.Span;
628-
if (points.Length < 2)
629-
{
630-
continue;
631-
}
632-
633635
int segmentCount = simplePath.IsClosed ? points.Length : points.Length - 1;
636+
634637
for (int j = 0; j < segmentCount; j++)
635638
{
636639
PointF p0 = points[j];
@@ -681,20 +684,16 @@ private static bool TryBuildFixedPointEdges(
681684

682685
offsets[bandCount] = running;
683686

684-
// Pass 2: Flatten again and scatter edges directly into the final buffer.
687+
// Pass 2: Scatter edges directly into the final buffer.
685688
IMemoryOwner<GpuEdge> finalOwner = allocator.Allocate<GpuEdge>(totalSubEdges);
686689
Span<GpuEdge> finalSpan = finalOwner.Memory.Span;
687690
using IMemoryOwner<uint> writeCursorsOwner = allocator.Allocate<uint>(bandCount, AllocationOptions.Clean);
688691
Span<uint> writeCursors = writeCursorsOwner.Memory.Span;
689692

690-
foreach (ISimplePath simplePath in path.Flatten())
693+
for (int c = 0; c < contours.Count; c++)
691694
{
695+
ISimplePath simplePath = contours[c];
692696
ReadOnlySpan<PointF> points = simplePath.Points.Span;
693-
if (points.Length < 2)
694-
{
695-
continue;
696-
}
697-
698697
int segmentCount = simplePath.IsClosed ? points.Length : points.Length - 1;
699698
for (int j = 0; j < segmentCount; j++)
700699
{
@@ -775,17 +774,20 @@ private static bool TryBuildStrokeEdges(
775774
float halfWidth = strokeWidth * 0.5f;
776775
int yExpansionFixed = (int)MathF.Ceiling(Math.Max(miterLimit, 1f) * halfWidth * FixedOne);
777776

777+
// Flatten once and reuse the list.
778+
IEnumerable<ISimplePath> flattened = path.Flatten();
779+
IReadOnlyList<ISimplePath> contours = flattened is IReadOnlyList<ISimplePath> list
780+
? list
781+
: [.. flattened];
782+
778783
// Pass 1: Collect all stroke edges and count per band.
779784
List<GpuEdge> strokeEdges = [];
780785
List<(int YMinFixed, int YMaxFixed)> edgeYRanges = [];
781786

782-
foreach (ISimplePath simplePath in path.Flatten())
787+
for (int c = 0; c < contours.Count; c++)
783788
{
789+
ISimplePath simplePath = contours[c];
784790
ReadOnlySpan<PointF> points = simplePath.Points.Span;
785-
if (points.Length < 2)
786-
{
787-
continue;
788-
}
789791

790792
bool isClosed = simplePath.IsClosed;
791793
int segmentCount = isClosed ? points.Length : points.Length - 1;

src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
<None Include="..\..\shared-infrastructure\branding\icons\imagesharp.drawing\sixlabors.imagesharp.drawing.128.png" Pack="true" PackagePath="" />
4646
</ItemGroup>
4747
<ItemGroup>
48-
<PackageReference Include="SixLabors.Fonts" Version="3.0.0-alpha.0.27" />
48+
<PackageReference Include="SixLabors.Fonts" Version="3.0.0-alpha.0.28" />
4949
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0-alpha.0.84" />
5050
<PackageReference Include="SixLabors.PolygonClipper" Version="1.0.0-alpha.0.52" />
5151
</ItemGroup>

src/ImageSharp.Drawing/InternalPath.cs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -178,19 +178,6 @@ internal SegmentInfo PointAlongPath(float distanceAlongPath)
178178
};
179179
}
180180

181-
internal IMemoryOwner<PointF> ExtractVertices(MemoryAllocator allocator)
182-
{
183-
IMemoryOwner<PointF> buffer = allocator.Allocate<PointF>(this.points.Length + 1);
184-
Span<PointF> span = buffer.Memory.Span;
185-
186-
for (int i = 0; i < this.points.Length; i++)
187-
{
188-
span[i] = this.points[i].Point;
189-
}
190-
191-
return buffer;
192-
}
193-
194181
// Modulo is a very slow operation.
195182
[MethodImpl(MethodImplOptions.AggressiveInlining)]
196183
private static int WrapArrayIndex(int i, int arrayLength) => i < arrayLength ? i : i - arrayLength;

src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ private CompositionCommand(
5353
public int DefinitionKey { get; }
5454

5555
/// <summary>
56-
/// Gets the path to rasterize in target-local coordinates.
56+
/// Gets the flattened path to rasterize in target-local coordinates.
57+
/// All sub-paths are pre-flattened and oriented for correct fill rasterization.
5758
/// </summary>
5859
public IPath Path { get; }
5960

src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public CompositionCoverageDefinition(
5858
public int DefinitionKey { get; }
5959

6060
/// <summary>
61-
/// Gets the path used to generate coverage.
61+
/// Gets the closed, flattened path used to generate coverage.
62+
/// All sub-paths are pre-flattened and oriented for correct fill rasterization.
6263
/// </summary>
6364
public IPath Path { get; }
6465

src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs

Lines changed: 28 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,26 @@ private static void RasterizeCoreRows(
202202
float samplingOffsetX = samplePixelCenter ? 0.5F : 0F;
203203
float samplingOffsetY = samplePixelCenter ? 0.5F : 0F;
204204

205-
using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator);
206-
using IMemoryOwner<EdgeData> edgeDataOwner = allocator.Allocate<EdgeData>(multipolygon.TotalVertexCount);
205+
IEnumerable<ISimplePath> flattened = path.Flatten();
206+
IReadOnlyList<ISimplePath> contours = flattened is IReadOnlyList<ISimplePath> list
207+
? list
208+
: [.. flattened];
209+
210+
int totalSegments = 0;
211+
for (int i = 0; i < contours.Count; i++)
212+
{
213+
ISimplePath sp = contours[i];
214+
totalSegments += sp.IsClosed ? sp.Points.Length : sp.Points.Length - 1;
215+
}
216+
217+
if (totalSegments == 0)
218+
{
219+
return;
220+
}
221+
222+
using IMemoryOwner<EdgeData> edgeDataOwner = allocator.Allocate<EdgeData>(totalSegments);
207223
int edgeCount = BuildEdgeTable(
208-
multipolygon,
224+
contours,
209225
interest.Left,
210226
interest.Top,
211227
height,
@@ -1164,7 +1180,7 @@ private static int BuildStrokeEdgeTable(
11641180
/// <summary>
11651181
/// Builds an edge table in scanner-local coordinates.
11661182
/// </summary>
1167-
/// <param name="multipolygon">Input tessellated rings.</param>
1183+
/// <param name="contours">Flattened path contours.</param>
11681184
/// <param name="minX">Interest left in absolute coordinates.</param>
11691185
/// <param name="minY">Interest top in absolute coordinates.</param>
11701186
/// <param name="height">Interest height in pixels.</param>
@@ -1173,7 +1189,7 @@ private static int BuildStrokeEdgeTable(
11731189
/// <param name="destination">Destination span for edge records.</param>
11741190
/// <returns>Number of valid edge records written.</returns>
11751191
private static int BuildEdgeTable(
1176-
TessellatedMultipolygon multipolygon,
1192+
IReadOnlyList<ISimplePath> contours,
11771193
int minX,
11781194
int minY,
11791195
int height,
@@ -1182,13 +1198,15 @@ private static int BuildEdgeTable(
11821198
Span<EdgeData> destination)
11831199
{
11841200
int count = 0;
1185-
foreach (TessellatedMultipolygon.Ring ring in multipolygon)
1201+
for (int r = 0; r < contours.Count; r++)
11861202
{
1187-
ReadOnlySpan<PointF> vertices = ring.Vertices;
1188-
for (int i = 0; i < ring.VertexCount; i++)
1203+
ISimplePath contour = contours[r];
1204+
ReadOnlySpan<PointF> points = contour.Points.Span;
1205+
int segmentCount = contour.IsClosed ? points.Length : points.Length - 1;
1206+
for (int i = 0; i < segmentCount; i++)
11891207
{
1190-
PointF p0 = vertices[i];
1191-
PointF p1 = vertices[i + 1];
1208+
PointF p0 = points[i];
1209+
PointF p1 = points[i + 1 == points.Length ? 0 : i + 1];
11921210

11931211
float x0 = (p0.X - minX) + samplingOffsetX;
11941212
float y0 = (p0.Y - minY) + samplingOffsetY;
@@ -1530,58 +1548,6 @@ public Context(
15301548
this.touchedRowCount = 0;
15311549
}
15321550

1533-
/// <summary>
1534-
/// Rasterizes all edges in a tessellated multipolygon directly into this context.
1535-
/// </summary>
1536-
/// <param name="multipolygon">Input tessellated rings.</param>
1537-
/// <param name="minX">Absolute left coordinate of the current scanner window.</param>
1538-
/// <param name="minY">Absolute top coordinate of the current scanner window.</param>
1539-
/// <param name="samplingOffsetX">Horizontal sample origin offset.</param>
1540-
/// <param name="samplingOffsetY">Vertical sample origin offset.</param>
1541-
public void RasterizeMultipolygon(
1542-
TessellatedMultipolygon multipolygon,
1543-
int minX,
1544-
int minY,
1545-
float samplingOffsetX,
1546-
float samplingOffsetY)
1547-
{
1548-
foreach (TessellatedMultipolygon.Ring ring in multipolygon)
1549-
{
1550-
ReadOnlySpan<PointF> vertices = ring.Vertices;
1551-
for (int i = 0; i < ring.VertexCount; i++)
1552-
{
1553-
PointF p0 = vertices[i];
1554-
PointF p1 = vertices[i + 1];
1555-
1556-
float x0 = (p0.X - minX) + samplingOffsetX;
1557-
float y0 = (p0.Y - minY) + samplingOffsetY;
1558-
float x1 = (p1.X - minX) + samplingOffsetX;
1559-
float y1 = (p1.Y - minY) + samplingOffsetY;
1560-
1561-
if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1))
1562-
{
1563-
continue;
1564-
}
1565-
1566-
if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, 0F, this.height))
1567-
{
1568-
continue;
1569-
}
1570-
1571-
int fx0 = FloatToFixed24Dot8(x0);
1572-
int fy0 = FloatToFixed24Dot8(y0);
1573-
int fx1 = FloatToFixed24Dot8(x1);
1574-
int fy1 = FloatToFixed24Dot8(y1);
1575-
if (fy0 == fy1)
1576-
{
1577-
continue;
1578-
}
1579-
1580-
this.RasterizeLine(fx0, fy0, fx1, fy1);
1581-
}
1582-
}
1583-
}
1584-
15851551
/// <summary>
15861552
/// Rasterizes all prebuilt edges that overlap this context.
15871553
/// </summary>

src/ImageSharp.Drawing/Processing/DrawingCanvas.PreFlattenedPath.cs

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ public sealed partial class DrawingCanvas<TPixel>
1515
/// <see cref="Flatten"/> returns <c>this</c> directly, avoiding redundant curve subdivision.
1616
/// Points are mutated in place on <see cref="Transform"/>; no buffers are copied.
1717
/// </summary>
18-
private sealed class PreFlattenedPath : IPath, ISimplePath
18+
private sealed class FlattenedPath : IPath, ISimplePath
1919
{
2020
private readonly PointF[] points;
2121
private readonly bool isClosed;
2222
private RectangleF bounds;
2323

24-
public PreFlattenedPath(PointF[] points, bool isClosed, RectangleF bounds)
24+
public FlattenedPath(PointF[] points, bool isClosed, RectangleF bounds)
2525
{
2626
this.points = points;
2727
this.isClosed = isClosed;
@@ -94,20 +94,35 @@ public IPath Transform(Matrix4x4 matrix)
9494

9595
/// <inheritdoc />
9696
public IPath AsClosedPath()
97-
=> this.isClosed ? this : new PreFlattenedPath(this.points, true, this.bounds);
97+
{
98+
if (this.isClosed)
99+
{
100+
return this;
101+
}
102+
103+
PointF[] closedPoints = new PointF[this.points.Length + 1];
104+
for (int i = 0; i < this.points.Length; i++)
105+
{
106+
closedPoints[i] = this.points[i];
107+
}
108+
109+
closedPoints[^1] = this.points[0];
110+
return new FlattenedPath(closedPoints, true, this.bounds);
111+
}
98112
}
99113

100114
/// <summary>
101115
/// A lightweight <see cref="IPath"/> wrapper around multiple pre-flattened sub-paths.
102116
/// <see cref="Flatten"/> yields each sub-path directly, avoiding redundant curve subdivision.
103117
/// Sub-path points are mutated in place on <see cref="Transform"/>; no buffers are copied.
104118
/// </summary>
105-
private sealed class PreFlattenedCompositePath : IPath
119+
private sealed class FlattenedCompositePath : IPath
106120
{
107-
private readonly PreFlattenedPath[] subPaths;
121+
private readonly FlattenedPath[] subPaths;
108122
private RectangleF bounds;
123+
private PathTypes? pathType;
109124

110-
public PreFlattenedCompositePath(PreFlattenedPath[] subPaths, RectangleF bounds)
125+
public FlattenedCompositePath(FlattenedPath[] subPaths, RectangleF bounds)
111126
{
112127
this.subPaths = subPaths;
113128
this.bounds = bounds;
@@ -121,9 +136,14 @@ public PathTypes PathType
121136
{
122137
get
123138
{
139+
if (this.pathType.HasValue)
140+
{
141+
return this.pathType.Value;
142+
}
143+
124144
bool hasOpen = false;
125145
bool hasClosed = false;
126-
foreach (PreFlattenedPath sp in this.subPaths)
146+
foreach (FlattenedPath sp in this.subPaths)
127147
{
128148
if (sp.PathType == PathTypes.Open)
129149
{
@@ -140,7 +160,8 @@ public PathTypes PathType
140160
}
141161
}
142162

143-
return hasClosed ? PathTypes.Closed : PathTypes.Open;
163+
this.pathType = hasClosed ? PathTypes.Closed : PathTypes.Open;
164+
return this.pathType.Value;
144165
}
145166
}
146167

@@ -165,7 +186,7 @@ public IPath Transform(Matrix4x4 matrix)
165186

166187
for (int i = 0; i < this.subPaths.Length; i++)
167188
{
168-
this.subPaths[i].Transform(matrix);
189+
_ = this.subPaths[i].Transform(matrix);
169190
RectangleF spBounds = this.subPaths[i].Bounds;
170191

171192
if (spBounds.Left < minX)
@@ -201,13 +222,13 @@ public IPath AsClosedPath()
201222
return this;
202223
}
203224

204-
PreFlattenedPath[] closed = new PreFlattenedPath[this.subPaths.Length];
225+
FlattenedPath[] closed = new FlattenedPath[this.subPaths.Length];
205226
for (int i = 0; i < this.subPaths.Length; i++)
206227
{
207-
closed[i] = (PreFlattenedPath)this.subPaths[i].AsClosedPath();
228+
closed[i] = (FlattenedPath)this.subPaths[i].AsClosedPath();
208229
}
209230

210-
return new PreFlattenedCompositePath(closed, this.bounds);
231+
return new FlattenedCompositePath(closed, this.bounds);
211232
}
212233
}
213234
}

src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ internal sealed class DrawingCanvasBatcher<TPixel>
1919
{
2020
private readonly Configuration configuration;
2121
private readonly IDrawingBackend backend;
22-
private readonly ICanvasFrame<TPixel> targetFrame;
2322
private readonly List<CompositionCommand> commands = [];
2423

2524
internal DrawingCanvasBatcher(
@@ -29,13 +28,13 @@ internal DrawingCanvasBatcher(
2928
{
3029
this.configuration = configuration;
3130
this.backend = backend;
32-
this.targetFrame = targetFrame;
31+
this.TargetFrame = targetFrame;
3332
}
3433

3534
/// <summary>
3635
/// Gets the target frame that this batcher flushes to.
3736
/// </summary>
38-
public ICanvasFrame<TPixel> TargetFrame => this.targetFrame;
37+
public ICanvasFrame<TPixel> TargetFrame { get; }
3938

4039
/// <summary>
4140
/// Appends one normalized composition command to the pending queue.
@@ -61,7 +60,7 @@ public void FlushCompositions()
6160
try
6261
{
6362
CompositionScene scene = new(this.commands);
64-
this.backend.FlushCompositions(this.configuration, this.targetFrame, scene);
63+
this.backend.FlushCompositions(this.configuration, this.TargetFrame, scene);
6564
}
6665
finally
6766
{

0 commit comments

Comments
 (0)