@@ -9,14 +9,20 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
99/// <summary>
1010/// Default CPU drawing backend.
1111/// </summary>
12+ /// <remarks>
13+ /// This backend keeps all CPU-specific scanline handling internal so higher-level processors
14+ /// can remain backend-agnostic.
15+ /// </remarks>
1216internal sealed class CpuDrawingBackend : IDrawingBackend
1317{
14- private readonly IRasterizer primaryRasterizer ;
15-
18+ /// <summary>
19+ /// Initializes a new instance of the <see cref="CpuDrawingBackend"/> class.
20+ /// </summary>
21+ /// <param name="primaryRasterizer">Rasterizer used for CPU coverage generation.</param>
1622 private CpuDrawingBackend ( IRasterizer primaryRasterizer )
1723 {
1824 Guard . NotNull ( primaryRasterizer , nameof ( primaryRasterizer ) ) ;
19- this . primaryRasterizer = primaryRasterizer ;
25+ this . PrimaryRasterizer = primaryRasterizer ;
2026 }
2127
2228 /// <summary>
@@ -27,7 +33,7 @@ private CpuDrawingBackend(IRasterizer primaryRasterizer)
2733 /// <summary>
2834 /// Gets the primary rasterizer used by this backend.
2935 /// </summary>
30- public IRasterizer PrimaryRasterizer => this . primaryRasterizer ;
36+ public IRasterizer PrimaryRasterizer { get ; }
3137
3238 /// <summary>
3339 /// Creates a backend that uses the given rasterizer as the primary implementation.
@@ -41,12 +47,287 @@ public static CpuDrawingBackend Create(IRasterizer rasterizer)
4147 }
4248
4349 /// <inheritdoc />
44- public void RasterizePath < TState > (
50+ public void FillPath < TPixel > (
51+ Configuration configuration ,
52+ ImageFrame < TPixel > source ,
53+ IPath path ,
54+ Brush brush ,
55+ in GraphicsOptions graphicsOptions ,
56+ in RasterizerOptions rasterizerOptions ,
57+ Rectangle brushBounds ,
58+ MemoryAllocator allocator )
59+ where TPixel : unmanaged, IPixel < TPixel >
60+ {
61+ Guard . NotNull ( configuration , nameof ( configuration ) ) ;
62+ Guard . NotNull ( source , nameof ( source ) ) ;
63+ Guard . NotNull ( path , nameof ( path ) ) ;
64+ Guard . NotNull ( brush , nameof ( brush ) ) ;
65+ Guard . NotNull ( allocator , nameof ( allocator ) ) ;
66+
67+ Rectangle interest = rasterizerOptions . Interest ;
68+ if ( interest . Equals ( Rectangle . Empty ) )
69+ {
70+ return ;
71+ }
72+
73+ // Detect the common "opaque solid without blending" case and bypass brush sampling
74+ // for fully covered runs.
75+ TPixel solidBrushColor = default ;
76+ bool isSolidBrushWithoutBlending = false ;
77+ if ( brush is SolidBrush solidBrush && graphicsOptions . IsOpaqueColorWithoutBlending ( solidBrush . Color ) )
78+ {
79+ isSolidBrushWithoutBlending = true ;
80+ solidBrushColor = solidBrush . Color . ToPixel < TPixel > ( ) ;
81+ }
82+
83+ int minX = interest . Left ;
84+ using BrushApplicator < TPixel > applicator = brush . CreateApplicator ( configuration , graphicsOptions , source , brushBounds ) ;
85+ FillRasterizationState < TPixel > state = new (
86+ source ,
87+ applicator ,
88+ minX ,
89+ isSolidBrushWithoutBlending ,
90+ solidBrushColor ) ;
91+
92+ this . PrimaryRasterizer . Rasterize ( path , rasterizerOptions , allocator , ref state , ProcessRasterizedScanline ) ;
93+ }
94+
95+ /// <inheritdoc />
96+ public void RasterizeCoverage (
4597 IPath path ,
46- in RasterizerOptions options ,
98+ in RasterizerOptions rasterizerOptions ,
4799 MemoryAllocator allocator ,
48- ref TState state ,
49- RasterizerScanlineHandler < TState > scanlineHandler )
50- where TState : struct
51- => this . primaryRasterizer . Rasterize ( path , options , allocator , ref state , scanlineHandler ) ;
100+ Buffer2D < float > destination )
101+ {
102+ Guard . NotNull ( path , nameof ( path ) ) ;
103+ Guard . NotNull ( allocator , nameof ( allocator ) ) ;
104+ Guard . NotNull ( destination , nameof ( destination ) ) ;
105+
106+ CoverageRasterizationState state = new ( destination ) ;
107+ this . PrimaryRasterizer . Rasterize ( path , rasterizerOptions , allocator , ref state , ProcessCoverageScanline ) ;
108+ }
109+
110+ /// <summary>
111+ /// Copies one rasterized coverage row into the destination coverage buffer.
112+ /// </summary>
113+ /// <param name="y">Destination row index.</param>
114+ /// <param name="scanline">Source coverage row.</param>
115+ /// <param name="state">Callback state containing destination storage.</param>
116+ private static void ProcessCoverageScanline ( int y , Span < float > scanline , ref CoverageRasterizationState state )
117+ {
118+ Span < float > destination = state . Buffer . DangerousGetRowSpan ( y ) ;
119+ scanline . CopyTo ( destination ) ;
120+ }
121+
122+ /// <summary>
123+ /// Dispatches rasterized coverage to either the generic brush path or the opaque-solid fast path.
124+ /// </summary>
125+ /// <typeparam name="TPixel">The pixel format.</typeparam>
126+ /// <param name="y">Destination row index.</param>
127+ /// <param name="scanline">Rasterized coverage row.</param>
128+ /// <param name="state">Callback state.</param>
129+ private static void ProcessRasterizedScanline < TPixel > ( int y , Span < float > scanline , ref FillRasterizationState < TPixel > state )
130+ where TPixel : unmanaged, IPixel < TPixel >
131+ {
132+ if ( state . IsSolidBrushWithoutBlending )
133+ {
134+ ApplyCoverageRunsForOpaqueSolidBrush ( state . Source , state . Applicator , scanline , state . MinX , y , state . SolidBrushColor ) ;
135+ }
136+ else
137+ {
138+ ApplyPositiveCoverageRuns ( state . Applicator , scanline , state . MinX , y ) ;
139+ }
140+ }
141+
142+ /// <summary>
143+ /// Applies a brush to contiguous positive-coverage runs on a scanline.
144+ /// </summary>
145+ /// <remarks>
146+ /// The rasterizer has already resolved the fill rule (NonZero or EvenOdd) into per-pixel
147+ /// coverage values. This method simply consumes the resulting positive runs.
148+ /// </remarks>
149+ /// <typeparam name="TPixel">The pixel format.</typeparam>
150+ /// <param name="applicator">Brush applicator.</param>
151+ /// <param name="scanline">Coverage values for one row.</param>
152+ /// <param name="minX">Absolute X of scanline index 0.</param>
153+ /// <param name="y">Destination row index.</param>
154+ private static void ApplyPositiveCoverageRuns < TPixel > ( BrushApplicator < TPixel > applicator , Span < float > scanline , int minX , int y )
155+ where TPixel : unmanaged, IPixel < TPixel >
156+ {
157+ int i = 0 ;
158+ while ( i < scanline . Length )
159+ {
160+ while ( i < scanline . Length && scanline [ i ] <= 0F )
161+ {
162+ i ++ ;
163+ }
164+
165+ int runStart = i ;
166+ while ( i < scanline . Length && scanline [ i ] > 0F )
167+ {
168+ i ++ ;
169+ }
170+
171+ int runLength = i - runStart ;
172+ if ( runLength > 0 )
173+ {
174+ // Apply only the positive-coverage run. This avoids invoking brush logic
175+ // for fully transparent gaps.
176+ applicator . Apply ( scanline . Slice ( runStart , runLength ) , minX + runStart , y ) ;
177+ }
178+ }
179+ }
180+
181+ /// <summary>
182+ /// Applies coverage using a mixed strategy for opaque solid brushes.
183+ /// </summary>
184+ /// <remarks>
185+ /// Semi-transparent edges still go through brush blending, but fully covered interior runs
186+ /// are written directly with <paramref name="solidBrushColor"/>.
187+ /// </remarks>
188+ /// <typeparam name="TPixel">The pixel format.</typeparam>
189+ /// <param name="source">Destination frame.</param>
190+ /// <param name="applicator">Brush applicator for non-opaque segments.</param>
191+ /// <param name="scanline">Coverage values for one row.</param>
192+ /// <param name="minX">Absolute X of scanline index 0.</param>
193+ /// <param name="y">Destination row index.</param>
194+ /// <param name="solidBrushColor">Pre-converted solid color for direct writes.</param>
195+ private static void ApplyCoverageRunsForOpaqueSolidBrush < TPixel > (
196+ ImageFrame < TPixel > source ,
197+ BrushApplicator < TPixel > applicator ,
198+ Span < float > scanline ,
199+ int minX ,
200+ int y ,
201+ TPixel solidBrushColor )
202+ where TPixel : unmanaged, IPixel < TPixel >
203+ {
204+ Span < TPixel > destinationRow = source . PixelBuffer . DangerousGetRowSpan ( y ) . Slice ( minX , scanline . Length ) ;
205+ int i = 0 ;
206+
207+ while ( i < scanline . Length )
208+ {
209+ while ( i < scanline . Length && scanline [ i ] <= 0F )
210+ {
211+ i ++ ;
212+ }
213+
214+ int runStart = i ;
215+ while ( i < scanline . Length && scanline [ i ] > 0F )
216+ {
217+ i ++ ;
218+ }
219+
220+ int runEnd = i ;
221+ if ( runEnd <= runStart )
222+ {
223+ continue ;
224+ }
225+
226+ // Leading partially-covered segment.
227+ int opaqueStart = runStart ;
228+ while ( opaqueStart < runEnd && scanline [ opaqueStart ] < 1F )
229+ {
230+ opaqueStart ++ ;
231+ }
232+
233+ if ( opaqueStart > runStart )
234+ {
235+ int prefixLength = opaqueStart - runStart ;
236+ applicator . Apply ( scanline . Slice ( runStart , prefixLength ) , minX + runStart , y ) ;
237+ }
238+
239+ // Trailing partially-covered segment.
240+ int opaqueEnd = runEnd ;
241+ while ( opaqueEnd > opaqueStart && scanline [ opaqueEnd - 1 ] < 1F )
242+ {
243+ opaqueEnd -- ;
244+ }
245+
246+ // Fully covered interior can skip blending entirely.
247+ if ( opaqueEnd > opaqueStart )
248+ {
249+ destinationRow [ opaqueStart ..opaqueEnd ] . Fill ( solidBrushColor ) ;
250+ }
251+
252+ if ( runEnd > opaqueEnd )
253+ {
254+ int suffixLength = runEnd - opaqueEnd ;
255+ applicator . Apply ( scanline . Slice ( opaqueEnd , suffixLength ) , minX + opaqueEnd , y ) ;
256+ }
257+ }
258+ }
259+
260+ /// <summary>
261+ /// Callback state used while writing coverage maps.
262+ /// </summary>
263+ private readonly struct CoverageRasterizationState
264+ {
265+ /// <summary>
266+ /// Initializes a new instance of the <see cref="CoverageRasterizationState"/> struct.
267+ /// </summary>
268+ /// <param name="buffer">Destination coverage buffer.</param>
269+ public CoverageRasterizationState ( Buffer2D < float > buffer ) => this . Buffer = buffer ;
270+
271+ /// <summary>
272+ /// Gets the destination coverage buffer.
273+ /// </summary>
274+ public Buffer2D < float > Buffer { get ; }
275+ }
276+
277+ /// <summary>
278+ /// Callback state used while filling into an image frame.
279+ /// </summary>
280+ /// <typeparam name="TPixel">The pixel format.</typeparam>
281+ private readonly struct FillRasterizationState < TPixel >
282+ where TPixel : unmanaged, IPixel < TPixel >
283+ {
284+ /// <summary>
285+ /// Initializes a new instance of the <see cref="FillRasterizationState{TPixel}"/> struct.
286+ /// </summary>
287+ /// <param name="source">Destination frame.</param>
288+ /// <param name="applicator">Brush applicator for blended segments.</param>
289+ /// <param name="minX">Absolute X corresponding to scanline index 0.</param>
290+ /// <param name="isSolidBrushWithoutBlending">
291+ /// Indicates whether opaque solid fast-path writes are allowed.
292+ /// </param>
293+ /// <param name="solidBrushColor">Pre-converted opaque solid color.</param>
294+ public FillRasterizationState (
295+ ImageFrame < TPixel > source ,
296+ BrushApplicator < TPixel > applicator ,
297+ int minX ,
298+ bool isSolidBrushWithoutBlending ,
299+ TPixel solidBrushColor )
300+ {
301+ this . Source = source ;
302+ this . Applicator = applicator ;
303+ this . MinX = minX ;
304+ this . IsSolidBrushWithoutBlending = isSolidBrushWithoutBlending ;
305+ this . SolidBrushColor = solidBrushColor ;
306+ }
307+
308+ /// <summary>
309+ /// Gets the destination frame.
310+ /// </summary>
311+ public ImageFrame < TPixel > Source { get ; }
312+
313+ /// <summary>
314+ /// Gets the brush applicator used for blended segments.
315+ /// </summary>
316+ public BrushApplicator < TPixel > Applicator { get ; }
317+
318+ /// <summary>
319+ /// Gets the absolute X origin of the current scanline.
320+ /// </summary>
321+ public int MinX { get ; }
322+
323+ /// <summary>
324+ /// Gets a value indicating whether opaque interior runs can be direct-filled.
325+ /// </summary>
326+ public bool IsSolidBrushWithoutBlending { get ; }
327+
328+ /// <summary>
329+ /// Gets the pre-converted solid color used by the opaque fast path.
330+ /// </summary>
331+ public TPixel SolidBrushColor { get ; }
332+ }
52333}
0 commit comments