Skip to content

Commit bc0fb83

Browse files
Add per-band stroke rasterization and StrokeEdgeFlags
1 parent fa6b1db commit bc0fb83

6 files changed

Lines changed: 1199 additions & 56 deletions

File tree

samples/WebGPUWindowDemo/Program.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,7 @@ private static void OnLoad()
155155

156156
Console.WriteLine($"Device: 0x{(nuint)device:X}, Queue: 0x{(nuint)queue:X}");
157157

158-
// Query surface capabilities and configure the swap chain.
159-
wgpu.SurfaceGetCapabilities(surface, adapter, ref surfaceCapabilities);
160-
Console.WriteLine($"Surface format: {surfaceCapabilities.Formats[0]}");
158+
// Configure the swap chain.
161159
ConfigureSwapchain();
162160

163161
// Initialize the ImageSharp.Drawing WebGPU backend and attach it to a

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ private static bool TryBuildStrokeEdges(
823823
Y0 = vy,
824824
X1 = (int)MathF.Round(((pv.X - interestX) + samplingOffsetX) * FixedOne),
825825
Y1 = (int)MathF.Round(((pv.Y - interestY) + samplingOffsetY) * FixedOne),
826-
Flags = 32, // EDGE_JOIN
826+
Flags = StrokeEdgeFlags.Join,
827827
AdjX = (int)MathF.Round(((nv.X - interestX) + samplingOffsetX) * FixedOne),
828828
AdjY = (int)MathF.Round(((nv.Y - interestY) + samplingOffsetY) * FixedOne),
829829
});
@@ -844,7 +844,7 @@ private static bool TryBuildStrokeEdges(
844844
Y0 = csy,
845845
X1 = (int)MathF.Round(((adjStart.X - interestX) + samplingOffsetX) * FixedOne),
846846
Y1 = (int)MathF.Round(((adjStart.Y - interestY) + samplingOffsetY) * FixedOne),
847-
Flags = 64, // EDGE_CAP_START
847+
Flags = StrokeEdgeFlags.CapStart,
848848
});
849849

850850
edgeYRanges.Add((csy - yExpansionFixed, csy + yExpansionFixed));
@@ -859,7 +859,7 @@ private static bool TryBuildStrokeEdges(
859859
Y0 = cey,
860860
X1 = (int)MathF.Round(((adjEnd.X - interestX) + samplingOffsetX) * FixedOne),
861861
Y1 = (int)MathF.Round(((adjEnd.Y - interestY) + samplingOffsetY) * FixedOne),
862-
Flags = 128, // EDGE_CAP_END
862+
Flags = StrokeEdgeFlags.CapEnd,
863863
});
864864

865865
edgeYRanges.Add((cey - yExpansionFixed, cey + yExpansionFixed));
@@ -1191,12 +1191,9 @@ private struct GpuEdge
11911191
public int Y1;
11921192

11931193
/// <summary>
1194-
/// Bit flags for stroke edge metadata.
1195-
/// Bit 0: open start — the (X0,Y0) endpoint is an open path start (cap applies).
1196-
/// Bit 1: open end — the (X1,Y1) endpoint is an open path end (cap applies).
1197-
/// Bit 2: bevel fill — this edge is a bevel fill chord (AdjX,AdjY = join vertex).
1194+
/// Stroke edge type flags matching the WGSL shader constants.
11981195
/// </summary>
1199-
public int Flags;
1196+
public StrokeEdgeFlags Flags;
12001197

12011198
/// <summary>
12021199
/// Auxiliary coordinates (fixed-point). For bevel fill edges, stores the

src/ImageSharp.Drawing.WebGPU/DashPathSplitter.cs renamed to src/ImageSharp.Drawing/Processing/Backends/DashPathSplitter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
99
/// Splits a path into dash segments without performing stroke expansion.
1010
/// Each "on" dash segment is returned as an open sub-path.
1111
/// </summary>
12-
internal static class DashPathSplitter
12+
public static class DashPathSplitter
1313
{
1414
/// <summary>
1515
/// Splits the given path into dash segments based on the provided pattern.

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

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -155,26 +155,30 @@ private void FlushPreparedBatch<TPixel>(
155155

156156
CompositionCoverageDefinition definition = compositionBatch.Definition;
157157

158-
// When the definition carries stroke metadata, expand the centerline
159-
// path into a filled outline before rasterization.
160158
IPath rasterPath = definition.Path;
161159
RasterizerOptions rasterizerOptions = definition.RasterizerOptions;
162160

163-
if (definition.IsStroke)
161+
if (definition.IsStroke && definition.StrokePattern.Length > 0)
164162
{
165-
rasterPath = definition.StrokePattern.Length > 0
166-
? rasterPath.GenerateOutline(definition.StrokeWidth, definition.StrokePattern.Span, definition.StrokeOptions!)
167-
: rasterPath.GenerateOutline(definition.StrokeWidth, definition.StrokeOptions!);
168-
169-
// Compute the exact interest from the actual stroke outline bounds
170-
// so band boundaries and coverage values match the old canvas-side path.
171-
RectangleF outlineBounds = rasterPath.Bounds;
172-
outlineBounds = new RectangleF(outlineBounds.X + 0.5F, outlineBounds.Y + 0.5F, outlineBounds.Width, outlineBounds.Height);
163+
// Dashed strokes: split into dash segments on the CPU, then stroke-expand
164+
// each segment via the per-band parallel path (same as solid strokes).
165+
rasterPath = DashPathSplitter.SplitDashes(rasterPath, definition.StrokeWidth, definition.StrokePattern.Span);
166+
167+
// Recompute interest from the split path bounds with stroke expansion.
168+
float halfWidth = definition.StrokeWidth * 0.5f;
169+
float maxExtent = halfWidth * Math.Max((float)(definition.StrokeOptions?.MiterLimit ?? 4.0), 1.0f);
170+
RectangleF pathBounds = rasterPath.Bounds;
171+
pathBounds = new RectangleF(
172+
pathBounds.X + 0.5F - maxExtent,
173+
pathBounds.Y + 0.5F - maxExtent,
174+
pathBounds.Width + (maxExtent * 2),
175+
pathBounds.Height + (maxExtent * 2));
176+
173177
Rectangle interest = Rectangle.FromLTRB(
174-
(int)MathF.Floor(outlineBounds.Left),
175-
(int)MathF.Floor(outlineBounds.Top),
176-
(int)MathF.Ceiling(outlineBounds.Right),
177-
(int)MathF.Ceiling(outlineBounds.Bottom));
178+
(int)MathF.Floor(pathBounds.Left),
179+
(int)MathF.Floor(pathBounds.Top),
180+
(int)MathF.Ceiling(pathBounds.Right),
181+
(int)MathF.Ceiling(pathBounds.Bottom));
178182

179183
rasterizerOptions = new RasterizerOptions(
180184
interest,
@@ -183,7 +187,7 @@ private void FlushPreparedBatch<TPixel>(
183187
rasterizerOptions.SamplingOrigin,
184188
rasterizerOptions.AntialiasThreshold);
185189

186-
// Re-prepare commands with the actual outline interest so destination
190+
// Re-prepare commands with the dash-split interest so destination
187191
// regions and source offsets are aligned with the rasterizer.
188192
CompositionScenePlanner.ReprepareBatchCommands(compositionBatch.Commands, target.Bounds, interest);
189193
}
@@ -213,12 +217,29 @@ private void FlushPreparedBatch<TPixel>(
213217
destinationBounds,
214218
rasterizerOptions.Interest.Top);
215219

216-
DefaultRasterizer.RasterizeRows(
217-
rasterPath,
218-
rasterizerOptions,
219-
configuration.MemoryAllocator,
220-
operation.InvokeCoverageRow,
221-
ref reusableScratch);
220+
if (definition.IsStroke)
221+
{
222+
// All strokes (solid and dashed) use per-band parallel stroke expansion.
223+
DefaultRasterizer.RasterizeStrokeRows(
224+
rasterPath,
225+
rasterizerOptions,
226+
configuration.MemoryAllocator,
227+
operation.InvokeCoverageRow,
228+
ref reusableScratch,
229+
definition.StrokeWidth,
230+
definition.StrokeOptions!.LineJoin,
231+
definition.StrokeOptions!.LineCap,
232+
(float)definition.StrokeOptions!.MiterLimit);
233+
}
234+
else
235+
{
236+
DefaultRasterizer.RasterizeRows(
237+
rasterPath,
238+
rasterizerOptions,
239+
configuration.MemoryAllocator,
240+
operation.InvokeCoverageRow,
241+
ref reusableScratch);
242+
}
222243
}
223244
finally
224245
{

0 commit comments

Comments
 (0)