11// Copyright (c) Six Labors.
22// Licensed under the Six Labors Split License.
33
4- using System . Buffers ;
54using SixLabors . ImageSharp . Memory ;
65
76namespace SixLabors . ImageSharp . Drawing . Shapes . Rasterization ;
87
98/// <summary>
10- /// Default CPU rasterizer that processes large paths in parallel vertical bands .
9+ /// Default CPU rasterizer.
1110/// </summary>
1211/// <remarks>
13- /// The algorithm preserves the public scanline callback contract (top-to-bottom emission) while
14- /// parallelizing internal work:
15- /// 1. Partition the interest rectangle into Y-bands.
16- /// 2. Rasterize each band independently into temporary coverage buffers.
17- /// 3. Emit bands back in deterministic top-to-bottom order.
18- ///
19- /// This design avoids concurrent writes to destination pixels and keeps per-band work isolated.
20- /// It also lets the implementation fall back to the single-pass scanner when tiling would not pay
21- /// off (small workloads, huge temporary buffers, or low core counts).
12+ /// This rasterizer delegates to <see cref="PolygonScanner"/>, which performs fixed-point
13+ /// area/cover scanning and chooses an internal execution strategy (parallel row-tiles when
14+ /// profitable, sequential fallback otherwise).
2215/// </remarks>
2316internal sealed class DefaultRasterizer : IRasterizer
2417{
25- // Keep bands reasonably tall so the overhead of per-band setup does not dominate tiny draws.
26- private const int MinimumBandHeight = 96 ;
27-
28- // Require a minimum pixel workload per band so thread scheduling overhead stays amortized.
29- private const int MinimumPixelsPerBand = 196608 ;
30-
31- // Hard cap on buffered pixels across all bands for a single rasterization invocation.
32- // One float is buffered per pixel plus a dirty-row byte map per band.
33- private const int MaximumBufferedPixels = 16777216 ; // 4096 x 4096
34-
35- // Bounding band count limits task fan-out and keeps allocator pressure predictable.
36- private const int MaximumBandCount = 8 ;
37-
3818 /// <summary>
3919 /// Gets the singleton default rasterizer instance.
4020 /// </summary>
@@ -49,330 +29,16 @@ public void Rasterize<TState>(
4929 RasterizerScanlineHandler < TState > scanlineHandler )
5030 where TState : struct
5131 {
52- // Fast argument validation at entry keeps failure behavior consistent with other rasterizers.
5332 Guard . NotNull ( path , nameof ( path ) ) ;
5433 Guard . NotNull ( allocator , nameof ( allocator ) ) ;
5534 Guard . NotNull ( scanlineHandler , nameof ( scanlineHandler ) ) ;
5635
5736 Rectangle interest = options . Interest ;
5837 if ( interest . Equals ( Rectangle . Empty ) )
5938 {
60- // Nothing intersects the destination; skip all work.
61- return ;
62- }
63-
64- if ( ! TryCreateBandPlan ( interest , out Band [ ] ? plannedBands ) || plannedBands is null )
65- {
66- // For small or extreme workloads, single-pass rasterization is cheaper and avoids
67- // temporary band buffers.
68- ScanlineRasterizer . Instance . Rasterize ( path , options , allocator , ref state , scanlineHandler ) ;
6939 return ;
7040 }
7141
72- Band [ ] bands = plannedBands ;
73- RasterizerOptions bandedOptions = options ;
74-
75- // Prime lazy path state once on the caller thread to avoid N workers racing to
76- // materialize the same internal path structures.
77- PrimePathState ( path ) ;
78-
79- try
80- {
81- // Limit parallelism to planned band count. This keeps work partition deterministic
82- // and avoids oversubscribing worker threads for this operation.
83- ParallelOptions parallelOptions = new ( ) { MaxDegreeOfParallelism = bands . Length } ;
84- _ = Parallel . For (
85- 0 ,
86- bands . Length ,
87- parallelOptions ,
88- i => RasterizeBand ( path , bandedOptions , allocator , bands [ i ] ) ) ;
89-
90- // Emit in deterministic order so downstream compositing observes stable scanline order.
91- EmitBands ( bands , interest . Width , ref state , scanlineHandler ) ;
92- }
93- finally
94- {
95- foreach ( Band band in bands )
96- {
97- band . Dispose ( ) ;
98- }
99- }
100- }
101-
102- /// <summary>
103- /// Forces lazy path materialization before worker threads start.
104- /// </summary>
105- /// <param name="path">The source path.</param>
106- private static void PrimePathState ( IPath path )
107- {
108- if ( path is IInternalPathOwner owner )
109- {
110- // Force ring extraction once for paths that expose internal rings. This is the
111- // hot path for ComplexPolygon and avoids repeated per-band conversion cost.
112- _ = owner . GetRingsAsInternalPath ( ) . Count ;
113- return ;
114- }
115-
116- // Fallback for generic paths: force flattening once so lazy point arrays are available
117- // before worker threads begin.
118- foreach ( ISimplePath simplePath in path . Flatten ( ) )
119- {
120- _ = simplePath . Points . Length ;
121- }
122- }
123-
124- /// <summary>
125- /// Computes a band partitioning plan for the destination rectangle.
126- /// </summary>
127- /// <param name="interest">Destination interest rectangle.</param>
128- /// <param name="bands">
129- /// When this method returns <see langword="true"/>, contains the planned rasterization bands.
130- /// </param>
131- /// <returns>
132- /// <see langword="true"/> when banding should be used; otherwise <see langword="false"/>.
133- /// </returns>
134- private static bool TryCreateBandPlan ( Rectangle interest , out Band [ ] ? bands )
135- {
136- bands = null ;
137-
138- int width = interest . Width ;
139- int height = interest . Height ;
140- long totalPixels = ( long ) width * height ;
141- if ( totalPixels > MaximumBufferedPixels )
142- {
143- // Refuse banding for extremely large interests to cap temporary memory use.
144- return false ;
145- }
146-
147- int processorCount = Environment . ProcessorCount ;
148- if ( processorCount < 2 || height < ( MinimumBandHeight * 2 ) || totalPixels < ( MinimumPixelsPerBand * 2L ) )
149- {
150- // Not enough parallel work: prefer single-pass path.
151- return false ;
152- }
153-
154- // Bound candidate band count by three limits:
155- // - image height (minimum band height),
156- // - total pixels (minimum pixels per band),
157- // - hardware + hard cap.
158- int byHeight = height / MinimumBandHeight ;
159- int byPixels = ( int ) ( totalPixels / MinimumPixelsPerBand ) ;
160- int bandCount = Math . Min ( MaximumBandCount , Math . Min ( processorCount , Math . Min ( byHeight , byPixels ) ) ) ;
161- if ( bandCount < 2 )
162- {
163- return false ;
164- }
165-
166- bands = new Band [ bandCount ] ;
167- int baseHeight = height / bandCount ;
168- int remainder = height % bandCount ;
169- int y = interest . Top ;
170-
171- for ( int i = 0 ; i < bandCount ; i ++ )
172- {
173- // Distribute remainder rows to the earliest bands to keep shapes balanced.
174- int bandHeight = baseHeight + ( i < remainder ? 1 : 0 ) ;
175- bands [ i ] = new Band ( y , bandHeight ) ;
176- y += bandHeight ;
177- }
178-
179- return true ;
180- }
181-
182- /// <summary>
183- /// Rasterizes a single band using the fallback scanline rasterizer into temporary buffers.
184- /// </summary>
185- /// <param name="path">Path to rasterize.</param>
186- /// <param name="options">Rasterization options.</param>
187- /// <param name="allocator">Memory allocator.</param>
188- /// <param name="band">The destination band to populate.</param>
189- private static void RasterizeBand (
190- IPath path ,
191- in RasterizerOptions options ,
192- MemoryAllocator allocator ,
193- Band band )
194- {
195- // Band-local buffers keep writes private to the worker and avoid shared state.
196- // coverageLength is width * bandHeight and is bounded by band planning constraints.
197- int width = options . Interest . Width ;
198- int coverageLength = checked ( width * band . Height ) ;
199-
200- IMemoryOwner < float > coverageOwner = allocator . Allocate < float > ( coverageLength , AllocationOptions . Clean ) ;
201- IMemoryOwner < byte > dirtyRowsOwner = allocator . Allocate < byte > ( band . Height , AllocationOptions . Clean ) ;
202-
203- try
204- {
205- RasterizerOptions bandOptions = options . WithInterest (
206- new Rectangle ( options . Interest . Left , band . Top , width , band . Height ) ) ;
207-
208- // Capture state collects scanline output from the fallback scanner into local buffers.
209- BandCaptureState captureState = new ( band . Top , width , coverageOwner . Memory , dirtyRowsOwner . Memory ) ;
210- ScanlineRasterizer . Instance . Rasterize ( path , bandOptions , allocator , ref captureState , CaptureBandScanline ) ;
211-
212- band . SetBuffers ( coverageOwner , dirtyRowsOwner ) ;
213- }
214- catch
215- {
216- coverageOwner . Dispose ( ) ;
217- dirtyRowsOwner . Dispose ( ) ;
218- throw ;
219- }
220- }
221-
222- /// <summary>
223- /// Emits all buffered bands in top-to-bottom scanline order.
224- /// </summary>
225- /// <typeparam name="TState">The rasterization callback state type.</typeparam>
226- /// <param name="bands">Bands containing buffered coverage.</param>
227- /// <param name="scanlineWidth">Width of each scanline.</param>
228- /// <param name="state">Mutable callback state.</param>
229- /// <param name="scanlineHandler">Scanline callback.</param>
230- private static void EmitBands < TState > (
231- Band [ ] bands ,
232- int scanlineWidth ,
233- ref TState state ,
234- RasterizerScanlineHandler < TState > scanlineHandler )
235- where TState : struct
236- {
237- // Serialize final emission in band order so callback consumers receive stable rows.
238- foreach ( Band band in bands )
239- {
240- if ( band . CoverageOwner is null || band . DirtyRowsOwner is null )
241- {
242- continue ;
243- }
244-
245- Span < float > coverage = band . CoverageOwner . Memory . Span ;
246- Span < byte > dirtyRows = band . DirtyRowsOwner . Memory . Span ;
247-
248- for ( int row = 0 ; row < band . Height ; row ++ )
249- {
250- if ( dirtyRows [ row ] == 0 )
251- {
252- // Sparse rows are skipped to avoid unnecessary callback invocations.
253- continue ;
254- }
255-
256- Span < float > scanline = coverage . Slice ( row * scanlineWidth , scanlineWidth ) ;
257- scanlineHandler ( band . Top + row , scanline , ref state ) ;
258- }
259- }
260- }
261-
262- /// <summary>
263- /// Captures one scanline from the fallback scanner into band-local storage.
264- /// </summary>
265- /// <param name="y">Absolute destination Y.</param>
266- /// <param name="scanline">Coverage values for the row.</param>
267- /// <param name="state">Band capture state.</param>
268- private static void CaptureBandScanline ( int y , Span < float > scanline , ref BandCaptureState state )
269- {
270- // The fallback scanner writes one row at a time; copy into contiguous band storage.
271- int row = y - state . Top ;
272- Span < float > coverage = state . Coverage . Span ;
273- scanline . CopyTo ( coverage . Slice ( row * state . Width , state . Width ) ) ;
274- state . DirtyRows . Span [ row ] = 1 ;
275- }
276-
277- /// <summary>
278- /// Mutable capture state used while rasterizing a single band.
279- /// </summary>
280- private readonly struct BandCaptureState
281- {
282- /// <summary>
283- /// Initializes a new instance of the <see cref="BandCaptureState"/> struct.
284- /// </summary>
285- /// <param name="top">Top-most Y of the target band.</param>
286- /// <param name="width">Scanline width for the band.</param>
287- /// <param name="coverage">Contiguous storage for band coverage rows.</param>
288- /// <param name="dirtyRows">Row activity map for sparse emission.</param>
289- public BandCaptureState ( int top , int width , Memory < float > coverage , Memory < byte > dirtyRows )
290- {
291- this . Top = top ;
292- this . Width = width ;
293- this . Coverage = coverage ;
294- this . DirtyRows = dirtyRows ;
295- }
296-
297- /// <summary>
298- /// Gets the top-most destination Y of the band.
299- /// </summary>
300- public int Top { get ; }
301-
302- /// <summary>
303- /// Gets the number of pixels in each band row.
304- /// </summary>
305- public int Width { get ; }
306-
307- /// <summary>
308- /// Gets contiguous per-row coverage storage for this band.
309- /// </summary>
310- public Memory < float > Coverage { get ; }
311-
312- /// <summary>
313- /// Gets the row activity map where non-zero indicates row data is present.
314- /// </summary>
315- public Memory < byte > DirtyRows { get ; }
316- }
317-
318- /// <summary>
319- /// Owns temporary buffers and metadata for a single planned band.
320- /// </summary>
321- private sealed class Band : IDisposable
322- {
323- /// <summary>
324- /// Initializes a new instance of the <see cref="Band"/> class.
325- /// </summary>
326- /// <param name="top">Top-most destination Y for the band.</param>
327- /// <param name="height">Number of rows in the band.</param>
328- public Band ( int top , int height )
329- {
330- this . Top = top ;
331- this . Height = height ;
332- }
333-
334- /// <summary>
335- /// Gets the top-most destination Y for this band.
336- /// </summary>
337- public int Top { get ; }
338-
339- /// <summary>
340- /// Gets the band height in rows.
341- /// </summary>
342- public int Height { get ; }
343-
344- /// <summary>
345- /// Gets the owner of the coverage buffer for this band.
346- /// </summary>
347- public IMemoryOwner < float > ? CoverageOwner { get ; private set ; }
348-
349- /// <summary>
350- /// Gets the owner of the dirty-row map buffer for this band.
351- /// </summary>
352- public IMemoryOwner < byte > ? DirtyRowsOwner { get ; private set ; }
353-
354- /// <summary>
355- /// Assigns buffer ownership to this band instance.
356- /// </summary>
357- /// <param name="coverageOwner">Coverage buffer owner.</param>
358- /// <param name="dirtyRowsOwner">Dirty-row buffer owner.</param>
359- public void SetBuffers ( IMemoryOwner < float > coverageOwner , IMemoryOwner < byte > dirtyRowsOwner )
360- {
361- // Ownership is transferred to the band container and released in Dispose().
362- this . CoverageOwner = coverageOwner ;
363- this . DirtyRowsOwner = dirtyRowsOwner ;
364- }
365-
366- /// <summary>
367- /// Disposes all band-owned buffers.
368- /// </summary>
369- public void Dispose ( )
370- {
371- // Always release pooled buffers even if rasterization fails in other bands.
372- this . CoverageOwner ? . Dispose ( ) ;
373- this . DirtyRowsOwner ? . Dispose ( ) ;
374- this . CoverageOwner = null ;
375- this . DirtyRowsOwner = null ;
376- }
42+ PolygonScanner . Rasterize ( path , options , allocator , ref state , scanlineHandler ) ;
37743 }
37844}
0 commit comments