Skip to content

Commit 355b56c

Browse files
Add pre-flattened paths and FlattenAndTransform helper
1 parent 3c351a9 commit 355b56c

4 files changed

Lines changed: 327 additions & 5 deletions

File tree

samples/WebGPUWindowDemo/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ private static void OnRender(double deltaTime)
324324
fpsElapsed += deltaTime;
325325
if (fpsElapsed >= 1.0)
326326
{
327-
window.Title = $"ImageSharp.Drawing WebGPU Demo — {frameCount / fpsElapsed:F1} FPS";
327+
window.Title = $"ImageSharp.Drawing WebGPU Demo — {frameCount / fpsElapsed:F1} FPS | GPU: {backend.DiagnosticGpuCompositeCount} Fallback: {backend.DiagnosticFallbackCompositeCount}";
328328
frameCount = 0;
329329
fpsElapsed = 0;
330330
}

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,16 @@ private enum PreparedBrushType : uint
135135
/// </summary>
136136
internal int TestingComputePathBatchCount { get; private set; }
137137

138+
/// <summary>
139+
/// Gets the cumulative number of composition commands executed on the GPU.
140+
/// </summary>
141+
public int DiagnosticGpuCompositeCount => this.TestingGPUCompositeCoverageCallCount;
142+
143+
/// <summary>
144+
/// Gets the cumulative number of composition commands that fell back to the CPU backend.
145+
/// </summary>
146+
public int DiagnosticFallbackCompositeCount => this.TestingFallbackCompositeCoverageCallCount;
147+
138148
/// <summary>
139149
/// Gets a value indicating whether WebGPU is available on the current system.
140150
/// This probes the runtime by attempting to acquire an adapter and device.
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Numerics;
5+
6+
namespace SixLabors.ImageSharp.Drawing.Processing;
7+
8+
/// <content>
9+
/// Contains private pre-flattened path types used by the canvas to avoid redundant curve subdivision.
10+
/// </content>
11+
public sealed partial class DrawingCanvas<TPixel>
12+
{
13+
/// <summary>
14+
/// A lightweight <see cref="IPath"/> wrapper around pre-flattened points.
15+
/// <see cref="Flatten"/> returns <c>this</c> directly, avoiding redundant curve subdivision.
16+
/// Points are mutated in place on <see cref="Transform"/>; no buffers are copied.
17+
/// </summary>
18+
private sealed class PreFlattenedPath : IPath, ISimplePath
19+
{
20+
private readonly PointF[] points;
21+
private readonly bool isClosed;
22+
private RectangleF bounds;
23+
24+
public PreFlattenedPath(PointF[] points, bool isClosed, RectangleF bounds)
25+
{
26+
this.points = points;
27+
this.isClosed = isClosed;
28+
this.bounds = bounds;
29+
}
30+
31+
/// <inheritdoc />
32+
public RectangleF Bounds => this.bounds;
33+
34+
/// <inheritdoc />
35+
public PathTypes PathType => this.isClosed ? PathTypes.Closed : PathTypes.Open;
36+
37+
/// <inheritdoc />
38+
bool ISimplePath.IsClosed => this.isClosed;
39+
40+
/// <inheritdoc />
41+
ReadOnlyMemory<PointF> ISimplePath.Points => this.points;
42+
43+
/// <inheritdoc />
44+
public IEnumerable<ISimplePath> Flatten()
45+
{
46+
yield return this;
47+
}
48+
49+
/// <summary>
50+
/// Transforms all points in place and updates the bounds.
51+
/// This mutates the current instance — the point buffer is not copied.
52+
/// </summary>
53+
/// <param name="matrix">The transform matrix.</param>
54+
/// <returns>This instance, with points and bounds updated.</returns>
55+
public IPath Transform(Matrix4x4 matrix)
56+
{
57+
if (matrix.IsIdentity)
58+
{
59+
return this;
60+
}
61+
62+
float minX = float.MaxValue, minY = float.MaxValue;
63+
float maxX = float.MinValue, maxY = float.MinValue;
64+
65+
for (int i = 0; i < this.points.Length; i++)
66+
{
67+
ref PointF p = ref this.points[i];
68+
p = PointF.Transform(p, matrix);
69+
70+
if (p.X < minX)
71+
{
72+
minX = p.X;
73+
}
74+
75+
if (p.Y < minY)
76+
{
77+
minY = p.Y;
78+
}
79+
80+
if (p.X > maxX)
81+
{
82+
maxX = p.X;
83+
}
84+
85+
if (p.Y > maxY)
86+
{
87+
maxY = p.Y;
88+
}
89+
}
90+
91+
this.bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY);
92+
return this;
93+
}
94+
95+
/// <inheritdoc />
96+
public IPath AsClosedPath()
97+
=> this.isClosed ? this : new PreFlattenedPath(this.points, true, this.bounds);
98+
}
99+
100+
/// <summary>
101+
/// A lightweight <see cref="IPath"/> wrapper around multiple pre-flattened sub-paths.
102+
/// <see cref="Flatten"/> yields each sub-path directly, avoiding redundant curve subdivision.
103+
/// Sub-path points are mutated in place on <see cref="Transform"/>; no buffers are copied.
104+
/// </summary>
105+
private sealed class PreFlattenedCompositePath : IPath
106+
{
107+
private readonly PreFlattenedPath[] subPaths;
108+
private RectangleF bounds;
109+
110+
public PreFlattenedCompositePath(PreFlattenedPath[] subPaths, RectangleF bounds)
111+
{
112+
this.subPaths = subPaths;
113+
this.bounds = bounds;
114+
}
115+
116+
/// <inheritdoc />
117+
public RectangleF Bounds => this.bounds;
118+
119+
/// <inheritdoc />
120+
public PathTypes PathType
121+
{
122+
get
123+
{
124+
bool hasOpen = false;
125+
bool hasClosed = false;
126+
foreach (PreFlattenedPath sp in this.subPaths)
127+
{
128+
if (sp.PathType == PathTypes.Open)
129+
{
130+
hasOpen = true;
131+
}
132+
else
133+
{
134+
hasClosed = true;
135+
}
136+
137+
if (hasOpen && hasClosed)
138+
{
139+
return PathTypes.Mixed;
140+
}
141+
}
142+
143+
return hasClosed ? PathTypes.Closed : PathTypes.Open;
144+
}
145+
}
146+
147+
/// <inheritdoc />
148+
public IEnumerable<ISimplePath> Flatten() => this.subPaths;
149+
150+
/// <summary>
151+
/// Transforms all sub-path points in place and updates the bounds.
152+
/// This mutates the current instance — no buffers are copied.
153+
/// </summary>
154+
/// <param name="matrix">The transform matrix.</param>
155+
/// <returns>This instance, with all sub-paths and bounds updated.</returns>
156+
public IPath Transform(Matrix4x4 matrix)
157+
{
158+
if (matrix.IsIdentity)
159+
{
160+
return this;
161+
}
162+
163+
float minX = float.MaxValue, minY = float.MaxValue;
164+
float maxX = float.MinValue, maxY = float.MinValue;
165+
166+
for (int i = 0; i < this.subPaths.Length; i++)
167+
{
168+
this.subPaths[i].Transform(matrix);
169+
RectangleF spBounds = this.subPaths[i].Bounds;
170+
171+
if (spBounds.Left < minX)
172+
{
173+
minX = spBounds.Left;
174+
}
175+
176+
if (spBounds.Top < minY)
177+
{
178+
minY = spBounds.Top;
179+
}
180+
181+
if (spBounds.Right > maxX)
182+
{
183+
maxX = spBounds.Right;
184+
}
185+
186+
if (spBounds.Bottom > maxY)
187+
{
188+
maxY = spBounds.Bottom;
189+
}
190+
}
191+
192+
this.bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY);
193+
return this;
194+
}
195+
196+
/// <inheritdoc />
197+
public IPath AsClosedPath()
198+
{
199+
if (this.PathType == PathTypes.Closed)
200+
{
201+
return this;
202+
}
203+
204+
PreFlattenedPath[] closed = new PreFlattenedPath[this.subPaths.Length];
205+
for (int i = 0; i < this.subPaths.Length; i++)
206+
{
207+
closed[i] = (PreFlattenedPath)this.subPaths[i].AsClosedPath();
208+
}
209+
210+
return new PreFlattenedCompositePath(closed, this.bounds);
211+
}
212+
}
213+
}

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

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing;
1919
/// A drawing canvas over a frame target.
2020
/// </summary>
2121
/// <typeparam name="TPixel">The pixel format.</typeparam>
22-
public sealed class DrawingCanvas<TPixel> : IDrawingCanvas
22+
public sealed partial class DrawingCanvas<TPixel> : IDrawingCanvas
2323
where TPixel : unmanaged, IPixel<TPixel>
2424
{
2525
/// <summary>
@@ -432,7 +432,7 @@ public void Fill(Brush brush, IPath path)
432432
IPath effectivePath = closed;
433433
if (effectiveOptions.Transform != Matrix4x4.Identity)
434434
{
435-
effectivePath = closed.Transform(effectiveOptions.Transform);
435+
effectivePath = FlattenAndTransform(closed, effectiveOptions.Transform);
436436
effectiveBrush = brush.Transform(effectiveOptions.Transform);
437437
}
438438

@@ -469,7 +469,7 @@ public void Process(IPath path, Action<IImageProcessingContext> operation)
469469
IPath closed = path.AsClosedPath();
470470
IPath transformedPath = effectiveOptions.Transform == Matrix4x4.Identity
471471
? closed
472-
: closed.Transform(effectiveOptions.Transform);
472+
: FlattenAndTransform(closed, effectiveOptions.Transform);
473473
transformedPath = ApplyClipPaths(transformedPath, effectiveOptions.ShapeOptions, state.ClipPaths);
474474

475475
Rectangle sourceRect = ToConservativeBounds(transformedPath.Bounds);
@@ -552,7 +552,7 @@ public void Draw(Pen pen, IPath path)
552552

553553
IPath transformedPath = effectiveOptions.Transform == Matrix4x4.Identity
554554
? path
555-
: path.Transform(effectiveOptions.Transform);
555+
: FlattenAndTransform(path, effectiveOptions.Transform);
556556

557557
// Stroke geometry can self-overlap; non-zero winding preserves stroke semantics.
558558
if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero)
@@ -1156,6 +1156,105 @@ private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList<IPa
11561156
private bool TryCreateProcessSourceImage(Rectangle sourceRect, [NotNullWhen(true)] out Image<TPixel>? sourceImage)
11571157
=> this.backend.TryReadRegion(this.configuration, this.targetFrame, sourceRect, out sourceImage);
11581158

1159+
/// <summary>
1160+
/// Flattens the path first (reusing any cached curve subdivision), then transforms
1161+
/// the resulting flat points. This avoids discarding cached <see cref="CubicBezierLineSegment"/>
1162+
/// subdivision data that <see cref="IPath.Transform"/> would throw away.
1163+
/// </summary>
1164+
/// <summary>
1165+
/// Flattens a path into linear segments, then transforms the resulting points in place.
1166+
/// This avoids redundant curve subdivision that would occur if we transformed the original
1167+
/// path first (which discards cached flattening) and then flattened again.
1168+
/// </summary>
1169+
/// <param name="path">The path to flatten and transform. The original path is not mutated.</param>
1170+
/// <param name="matrix">The transform matrix to apply to the flattened points.</param>
1171+
/// <returns>
1172+
/// A pre-flattened <see cref="IPath"/> whose points are already transformed.
1173+
/// The returned path owns its point buffers and may mutate them on subsequent transforms.
1174+
/// </returns>
1175+
private static IPath FlattenAndTransform(IPath path, Matrix4x4 matrix)
1176+
{
1177+
List<PreFlattenedPath>? subPaths = null;
1178+
float minX = float.MaxValue, minY = float.MaxValue;
1179+
float maxX = float.MinValue, maxY = float.MinValue;
1180+
1181+
foreach (ISimplePath sp in path.Flatten())
1182+
{
1183+
ReadOnlySpan<PointF> srcPoints = sp.Points.Span;
1184+
if (srcPoints.Length < 2)
1185+
{
1186+
continue;
1187+
}
1188+
1189+
PointF[] dstPoints = new PointF[srcPoints.Length];
1190+
float spMinX = float.MaxValue, spMinY = float.MaxValue;
1191+
float spMaxX = float.MinValue, spMaxY = float.MinValue;
1192+
1193+
for (int i = 0; i < srcPoints.Length; i++)
1194+
{
1195+
ref PointF dst = ref dstPoints[i];
1196+
dst = PointF.Transform(srcPoints[i], matrix);
1197+
1198+
if (dst.X < spMinX)
1199+
{
1200+
spMinX = dst.X;
1201+
}
1202+
1203+
if (dst.Y < spMinY)
1204+
{
1205+
spMinY = dst.Y;
1206+
}
1207+
1208+
if (dst.X > spMaxX)
1209+
{
1210+
spMaxX = dst.X;
1211+
}
1212+
1213+
if (dst.Y > spMaxY)
1214+
{
1215+
spMaxY = dst.Y;
1216+
}
1217+
}
1218+
1219+
RectangleF spBounds = new(spMinX, spMinY, spMaxX - spMinX, spMaxY - spMinY);
1220+
subPaths ??= [];
1221+
subPaths.Add(new PreFlattenedPath(dstPoints, sp.IsClosed, spBounds));
1222+
1223+
if (spMinX < minX)
1224+
{
1225+
minX = spMinX;
1226+
}
1227+
1228+
if (spMinY < minY)
1229+
{
1230+
minY = spMinY;
1231+
}
1232+
1233+
if (spMaxX > maxX)
1234+
{
1235+
maxX = spMaxX;
1236+
}
1237+
1238+
if (spMaxY > maxY)
1239+
{
1240+
maxY = spMaxY;
1241+
}
1242+
}
1243+
1244+
if (subPaths is null)
1245+
{
1246+
return Path.Empty;
1247+
}
1248+
1249+
if (subPaths.Count == 1)
1250+
{
1251+
return subPaths[0];
1252+
}
1253+
1254+
RectangleF totalBounds = new(minX, minY, maxX - minX, maxY - minY);
1255+
return new PreFlattenedCompositePath(subPaths.ToArray(), totalBounds);
1256+
}
1257+
11591258
/// <summary>
11601259
/// Applies all clip paths to a subject path using the provided shape options.
11611260
/// </summary>

0 commit comments

Comments
 (0)