Skip to content

Commit 4df8f1a

Browse files
Document
1 parent ed7c042 commit 4df8f1a

File tree

5 files changed

+1117
-420
lines changed

5 files changed

+1117
-420
lines changed
Lines changed: 5 additions & 339 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,20 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Buffers;
54
using SixLabors.ImageSharp.Memory;
65

76
namespace 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>
2316
internal 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

Comments
 (0)