Integrate SixLabors.PolygonClipper
Merged
ST
9mo ago+5297+5297+0123456789012345678901234567890123456789-8514-8514-0123456789012345678901234567890123456789
Prerequisites
- I have written a descriptive pull-request title
- I have verified that there are no overlapping pull-requests open
- I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules :cop:.
- I have provided test coverage for my change (where applicable)
Description
This PR integrates SixLabors.PolygonClipper into ImageSharp.Drawing, replaces the legacy rasterization pipeline with a new scanner-based implementation, and introduces backend/rasterizer abstractions to support future alternate rendering paths (including non-CPU backends).
Main Changes
1. Polygon clipping/stroking integration
- Replaced in-repo polygon clipping/offsetting implementation with
SixLabors.PolygonClipper. - Added/updated polygon geometry adapters:
PolygonClipperFactoryClippedShapeGeneratorStrokedShapeGenerator
Clip()now routes through PolygonClipper boolean operations.- Stroke generation now uses PolygonClipper
PolygonStroker.
2. New stroke semantics and options
- Added
StrokeOptions.NormalizeOutput(defaultfalse) to control post-stroke normalization. - Updated stroke option model to
LineJoin,LineCap, andInnerJoin. - Draw pipeline enforces robust behavior for non-normalized stroke output.
3. Rasterization architecture overhaul
- Replaced legacy scanline internals with a new
PolygonScanner-based path. - Added rasterizer abstraction:
IRasterizerDefaultRasterizerScanlineRasterizer(explicit sequential/reference path)RasterizerOptions/RasterizationMode
- Removed old rasterizer files (
ActiveEdgeList,ScanEdge,ScanEdgeCollection, old extension wrappers).
4. Drawing backend abstraction
- Added internal backend boundary:
IDrawingBackendCpuDrawingBackend
FillPathProcessor<TPixel>now submits operation-level data to backend instead of owning rasterizer details.- Backend contains compositing fast paths (including opaque solid run handling).
5. Fill rule and shape behavior updates
ShapeOptions.IntersectionRuledefault changed toNonZero.- Clip operation options now use
BooleanOperation. - Added/update handling so clipped parent/child contours maintain winding relationships compatible with
NonZero.
6. Build/test/docs updates
- Updated package references and solution/build pipeline files.
- Added/updated rasterizer and processor tests (including new scanner tests).
- Updated
PolygonScanning.MDfor the new scanner behavior. - Updated many reference images to match new rasterization output.
Breaking / API Surface Changes
- Removed old enums/types:
ClippingOperation(replaced byBooleanOperation)EndCapStyle(replaced byLineCap)JointStyle(replaced byLineJoin)
ShapeOptionsnow usesBooleanOperation+IntersectionRule = NonZeroby default.- Internal rasterizer selection moved to backend/rasterizer abstractions.
Why this PR
- Unifies clipping/stroking on a dedicated geometry engine (
PolygonClipper). - Improves rasterizer throughput and scalability.
- Establishes cleaner layering (
processor -> backend -> rasterizer) for future GPU/custom backend work. - Makes stroke normalization optional for performance-sensitive paths while keeping deterministic behavior available when needed.
Benchmarks
The combination of the new stroker and tiled rasterizer makes ImageSharp highly competitive.
This benchmark measures rendering a large, complex polygon with a 10px stroke.
For ImageSharp, results are shown for both rasterizer implementations:
ImageSharp...Tiled: the default tiled/parallel rasterizer (optimized for throughput).ImageSharp...ScanlineRasterizer: the single-pass non-tiled scanline rasterizer (useful as a baseline and for comparing scheduling overhead).
ImageSharpStrokeAndClip and FillPolygon are included intentionally:
ImageSharpStrokeAndClipisolates stroke geometry generation + clipping cost, independent of full draw compositing.FillPolygonisolates pure polygon fill/rasterization cost without stroke expansion.
Together, these rows help separate where time is spent: geometry generation, clipping/normalization, and final rasterization/compositing.
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.26200
Unknown processor
.NET SDK=10.0.103
[Host] : .NET 8.0.24 (8.0.2426.7010), X64 RyuJIT
Toolchain=InProcessEmitToolchain InvocationCount=1 IterationCount=40
LaunchCount=3 UnrollFactor=1 WarmupCount=15
| Method | Mean | Error | StdDev | Median | Ratio | RatioSD |
|---|---|---|---|---|---|---|
| SystemDrawing | 976.6 us | 13.82 us | 44.46 us | 970.5 us | 0.97 | 0.05 |
| ImageSharpCombinedPathsScanlineRasterizer | 2,808.7 us | 91.98 us | 289.31 us | 2,721.4 us | 2.76 | 0.27 |
| ImageSharpSeparatePathsScanlineRasterizer | 1,896.4 us | 313.35 us | 985.64 us | 1,246.3 us | 1.91 | 0.98 |
| ImageSharpCombinedPathsTiled | 902.9 us | 16.50 us | 51.66 us | 908.4 us | 0.90 | 0.05 |
| ImageSharpSeparatePathsTiled | 905.7 us | 11.63 us | 36.75 us | 902.0 us | 0.90 | 0.04 |
| SkiaSharp | 1,007.6 us | 4.24 us | 13.07 us | 1,004.7 us | 1.00 | 0.00 |
| ImageSharpStrokeAndClip | 288.8 us | 1.20 us | 3.69 us | 287.8 us | 0.29 | 0.00 |
| FillPolygon | 521.5 us | 14.04 us | 44.78 us | 524.9 us | 0.52 | 0.05 |