Integrate SixLabors.PolygonClipper

Merged
ST
9mo ago
+5297-8514

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:
    • PolygonClipperFactory
    • ClippedShapeGenerator
    • StrokedShapeGenerator
  • Clip() now routes through PolygonClipper boolean operations.
  • Stroke generation now uses PolygonClipper PolygonStroker.

2. New stroke semantics and options

  • Added StrokeOptions.NormalizeOutput (default false) to control post-stroke normalization.
  • Updated stroke option model to LineJoin, LineCap, and InnerJoin.
  • 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:
    • IRasterizer
    • DefaultRasterizer
    • ScanlineRasterizer (explicit sequential/reference path)
    • RasterizerOptions / RasterizationMode
  • Removed old rasterizer files (ActiveEdgeList, ScanEdge, ScanEdgeCollection, old extension wrappers).

4. Drawing backend abstraction

  • Added internal backend boundary:
    • IDrawingBackend
    • CpuDrawingBackend
  • 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.IntersectionRule default changed to NonZero.
  • 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.MD for the new scanner behavior.
  • Updated many reference images to match new rasterization output.

Breaking / API Surface Changes

  • Removed old enums/types:
    • ClippingOperation (replaced by BooleanOperation)
    • EndCapStyle (replaced by LineCap)
    • JointStyle (replaced by LineJoin)
  • ShapeOptions now uses BooleanOperation + IntersectionRule = NonZero by 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:

  • ImageSharpStrokeAndClip isolates stroke geometry generation + clipping cost, independent of full draw compositing.
  • FillPolygon isolates 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
MethodMeanErrorStdDevMedianRatioRatioSD
SystemDrawing976.6 us13.82 us44.46 us970.5 us0.970.05
ImageSharpCombinedPathsScanlineRasterizer2,808.7 us91.98 us289.31 us2,721.4 us2.760.27
ImageSharpSeparatePathsScanlineRasterizer1,896.4 us313.35 us985.64 us1,246.3 us1.910.98
ImageSharpCombinedPathsTiled902.9 us16.50 us51.66 us908.4 us0.900.05
ImageSharpSeparatePathsTiled905.7 us11.63 us36.75 us902.0 us0.900.04
SkiaSharp1,007.6 us4.24 us13.07 us1,004.7 us1.000.00
ImageSharpStrokeAndClip288.8 us1.20 us3.69 us287.8 us0.290.00
FillPolygon521.5 us14.04 us44.78 us524.9 us0.520.05