Skip to content

Commit 38e9d2d

Browse files
Better IDrawingBackend
1 parent c852e15 commit 38e9d2d

File tree

7 files changed

+372
-204
lines changed

7 files changed

+372
-204
lines changed

src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs

Lines changed: 291 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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>
1216
internal 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
}

src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,47 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
1616
internal interface IDrawingBackend
1717
{
1818
/// <summary>
19-
/// Rasterizes a path into scanline coverage.
19+
/// Fills a path into the destination image using the given brush and drawing options.
2020
/// </summary>
21-
/// <typeparam name="TState">The caller-provided mutable state type.</typeparam>
21+
/// <remarks>
22+
/// This operation-level API keeps processors independent from scanline rasterization details,
23+
/// allowing alternate backend implementations (for example GPU backends) to consume brush
24+
/// and path data directly.
25+
/// </remarks>
26+
/// <typeparam name="TPixel">The pixel format.</typeparam>
27+
/// <param name="configuration">Active processing configuration.</param>
28+
/// <param name="source">Destination image frame.</param>
2229
/// <param name="path">The path to rasterize.</param>
23-
/// <param name="options">Rasterizer options.</param>
30+
/// <param name="brush">Brush used to shade covered pixels.</param>
31+
/// <param name="graphicsOptions">Graphics blending/composition options.</param>
32+
/// <param name="rasterizerOptions">Rasterizer options.</param>
33+
/// <param name="brushBounds">Brush bounds used when creating the applicator.</param>
2434
/// <param name="allocator">Allocator for temporary data.</param>
25-
/// <param name="state">Caller-owned mutable state passed to the scanline callback.</param>
26-
/// <param name="scanlineHandler">Scanline callback.</param>
27-
void RasterizePath<TState>(
35+
public void FillPath<TPixel>(
36+
Configuration configuration,
37+
ImageFrame<TPixel> source,
2838
IPath path,
29-
in RasterizerOptions options,
39+
Brush brush,
40+
in GraphicsOptions graphicsOptions,
41+
in RasterizerOptions rasterizerOptions,
42+
Rectangle brushBounds,
43+
MemoryAllocator allocator)
44+
where TPixel : unmanaged, IPixel<TPixel>;
45+
46+
/// <summary>
47+
/// Rasterizes path coverage into a floating-point destination map.
48+
/// </summary>
49+
/// <remarks>
50+
/// Coverage values are written in local destination coordinates where <c>(0,0)</c> maps to
51+
/// the top-left of <paramref name="destination"/>.
52+
/// </remarks>
53+
/// <param name="path">The path to rasterize.</param>
54+
/// <param name="rasterizerOptions">Rasterizer options.</param>
55+
/// <param name="allocator">Allocator for temporary data.</param>
56+
/// <param name="destination">Destination coverage map.</param>
57+
public void RasterizeCoverage(
58+
IPath path,
59+
in RasterizerOptions rasterizerOptions,
3060
MemoryAllocator allocator,
31-
ref TState state,
32-
RasterizerScanlineHandler<TState> scanlineHandler)
33-
where TState : struct;
61+
Buffer2D<float> destination);
3462
}

0 commit comments

Comments
 (0)