Skip to content

Commit 1771beb

Browse files
Handle point strokes in WebGPU encoder
1 parent d15c48b commit 1771beb

6 files changed

Lines changed: 138 additions & 3 deletions

src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ internal static class WebGPUSceneEncoder
2626
private const int StyleWordCount = 5;
2727
private const int TileWidth = 16;
2828
private const int TileHeight = 16;
29+
private const float PointStrokeSegmentHalfLength = 1F / 128F;
30+
private const float StrokeMicroSegmentEpsilon = 1F / 64F;
2931
private static readonly GraphicsOptions DefaultClipGraphicsOptions = new();
3032

3133
/// <summary>
@@ -1459,10 +1461,22 @@ private static int EncodeStrokePath(
14591461
float pointTranslateX = destinationOffset.X - rootTargetBounds.X;
14601462
float pointTranslateY = destinationOffset.Y - rootTargetBounds.Y;
14611463
lineCount = EstimateStrokeLineCount(geometry, pen, widthScale);
1464+
int encodedContourCount = 0;
14621465

14631466
for (int i = 0; i < geometry.Contours.Count; i++)
14641467
{
14651468
LinearContour contour = geometry.Contours[i];
1469+
if (TryGetPointStrokeContour(geometry, contour, out PointF point))
1470+
{
1471+
EncodePointStrokeContour(
1472+
point,
1473+
pointTranslateX,
1474+
pointTranslateY,
1475+
ref pathTags,
1476+
ref pathData);
1477+
encodedContourCount++;
1478+
continue;
1479+
}
14661480

14671481
int markerWordCount = contour.IsClosed ? 2 : 4;
14681482
Span<uint> contourData = pathData.GetAppendSpan(2 + (contour.SegmentCount * 2) + markerWordCount);
@@ -1501,9 +1515,10 @@ private static int EncodeStrokePath(
15011515

15021516
pathData.Advance(contourData.Length);
15031517
pathTags.Advance(contourTags.Length);
1518+
encodedContourCount++;
15041519
}
15051520

1506-
if (geometry.Info.SegmentCount == 0)
1521+
if (encodedContourCount == 0)
15071522
{
15081523
return 0;
15091524
}
@@ -1512,6 +1527,36 @@ private static int EncodeStrokePath(
15121527
return 1;
15131528
}
15141529

1530+
/// <summary>
1531+
/// Encodes one point-like stroke contour as a tiny centered open segment.
1532+
/// </summary>
1533+
private static void EncodePointStrokeContour(
1534+
PointF point,
1535+
float pointTranslateX,
1536+
float pointTranslateY,
1537+
ref OwnedStream<byte> pathTags,
1538+
ref OwnedStream<uint> pathData)
1539+
{
1540+
PointF start = new(point.X - PointStrokeSegmentHalfLength, point.Y);
1541+
PointF end = new(point.X + PointStrokeSegmentHalfLength, point.Y);
1542+
PointF tangentEnd = GetStrokeMarkerTangentPoint(start, end);
1543+
1544+
Span<uint> contourData = pathData.GetAppendSpan(8);
1545+
Span<byte> contourTags = pathTags.GetAppendSpan(2);
1546+
contourData[0] = BitcastSingle(start.X + pointTranslateX);
1547+
contourData[1] = BitcastSingle(start.Y + pointTranslateY);
1548+
contourData[2] = BitcastSingle(end.X + pointTranslateX);
1549+
contourData[3] = BitcastSingle(end.Y + pointTranslateY);
1550+
contourTags[0] = PackPathTag(PathTag.LineToF32);
1551+
contourData[4] = BitcastSingle(start.X + pointTranslateX);
1552+
contourData[5] = BitcastSingle(start.Y + pointTranslateY);
1553+
contourData[6] = BitcastSingle(tangentEnd.X + pointTranslateX);
1554+
contourData[7] = BitcastSingle(tangentEnd.Y + pointTranslateY);
1555+
contourTags[1] = PackPathTag(PathTag.QuadToF32 | PathTag.SubpathEnd);
1556+
pathData.Advance(contourData.Length);
1557+
pathTags.Advance(contourTags.Length);
1558+
}
1559+
15151560
/// <summary>
15161561
/// Encodes one explicit open two-point stroke centerline into Vello-style path tags and path data in target-local space.
15171562
/// </summary>
@@ -1528,6 +1573,7 @@ private static int EncodeOpenSegmentStrokePath(
15281573
{
15291574
float pointTranslateX = destinationOffset.X - rootTargetBounds.X;
15301575
float pointTranslateY = destinationOffset.Y - rootTargetBounds.Y;
1576+
NormalizePointStrokeSegment(ref start, ref end);
15311577
PointF firstTangentEndPoint = GetStrokeMarkerTangentPoint(start, end);
15321578

15331579
Span<uint> contourData = pathData.GetAppendSpan(8);
@@ -1549,6 +1595,45 @@ private static int EncodeOpenSegmentStrokePath(
15491595
return 1;
15501596
}
15511597

1598+
/// <summary>
1599+
/// Gets whether a contour collapses to the point-stroke path used by the CPU rasterizer.
1600+
/// </summary>
1601+
private static bool TryGetPointStrokeContour(LinearGeometry geometry, LinearContour contour, out PointF point)
1602+
{
1603+
point = default;
1604+
if (contour.PointCount == 0)
1605+
{
1606+
return false;
1607+
}
1608+
1609+
point = geometry.Points[contour.PointStart];
1610+
for (int i = 1; i < contour.PointCount; i++)
1611+
{
1612+
PointF next = geometry.Points[contour.PointStart + i];
1613+
if (Vector2.DistanceSquared(next, point) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon)
1614+
{
1615+
return false;
1616+
}
1617+
}
1618+
1619+
return true;
1620+
}
1621+
1622+
/// <summary>
1623+
/// Replaces a point-like explicit stroke segment with the tiny tangent segment used for point strokes.
1624+
/// </summary>
1625+
private static void NormalizePointStrokeSegment(ref PointF start, ref PointF end)
1626+
{
1627+
if (Vector2.DistanceSquared(start, end) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon)
1628+
{
1629+
return;
1630+
}
1631+
1632+
PointF center = new((start.X + end.X) * 0.5F, (start.Y + end.Y) * 0.5F);
1633+
start = new PointF(center.X - PointStrokeSegmentHalfLength, center.Y);
1634+
end = new PointF(center.X + PointStrokeSegmentHalfLength, center.Y);
1635+
}
1636+
15521637
/// <summary>
15531638
/// Encodes a rectangle as a fixed-size closed path.
15541639
/// </summary>

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,46 @@ public void DrawPath_Stroke_MatchesDefaultOutput<TPixel>(TestImageProvider<TPixe
898898
AssertBackendPairReferenceOutputs(provider, "DrawPath_Stroke", defaultImage, nativeSurfaceImage);
899899
}
900900

901+
[WebGPUTheory]
902+
[WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32, LineCap.Square)]
903+
[WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32, LineCap.Round)]
904+
public void DrawPath_PointStroke_LineCap_MatchesDefaultOutput<TPixel>(TestImageProvider<TPixel> provider, LineCap lineCap)
905+
where TPixel : unmanaged, IPixel<TPixel>
906+
{
907+
DrawingOptions drawingOptions = new()
908+
{
909+
GraphicsOptions = new GraphicsOptions { Antialias = true }
910+
};
911+
912+
PathBuilder pathBuilder = new();
913+
pathBuilder.AddLine(new PointF(128, 128), new PointF(128, 128));
914+
915+
IPath path = pathBuilder.Build();
916+
Pen pen = new SolidPen(new PenOptions(Color.DarkBlue, 48F)
917+
{
918+
StrokeOptions = new StrokeOptions { LineCap = lineCap }
919+
});
920+
921+
void DrawAction(DrawingCanvas<TPixel> canvas) => canvas.Draw(pen, path);
922+
923+
using Image<TPixel> defaultImage = provider.GetImage();
924+
RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction);
925+
926+
using WebGPUDrawingBackend nativeSurfaceBackend = new();
927+
using Image<TPixel> nativeSurfaceInitialImage = provider.GetImage();
928+
using Image<TPixel> nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend(
929+
defaultImage.Width,
930+
defaultImage.Height,
931+
nativeSurfaceBackend,
932+
drawingOptions,
933+
DrawAction,
934+
nativeSurfaceInitialImage);
935+
936+
DebugSaveBackendPair(provider, $"DrawPath_PointStroke_LineCap_{lineCap}", defaultImage, nativeSurfaceImage);
937+
AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.03F);
938+
AssertBackendPairReferenceOutputs(provider, $"DrawPath_PointStroke_LineCap_{lineCap}", defaultImage, nativeSurfaceImage);
939+
}
940+
901941
public static TheoryData<LineJoin> LineJoinValues { get; } = new()
902942
{
903943
LineJoin.Miter,
@@ -2290,6 +2330,4 @@ static void DrawAction(DrawingCanvas<TPixel> canvas)
22902330
AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F);
22912331
AssertBackendPairReferenceOutputs(provider, "SaveLayer_MixedSaveAndSaveLayer", defaultImage, nativeSurfaceImage);
22922332
}
2293-
22942333
}
2295-
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)