Skip to content

Commit d427d29

Browse files
Replace PolygonScanner with DefaultRasterizer and optimize
1 parent f427d04 commit d427d29

12 files changed

Lines changed: 2303 additions & 2652 deletions

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

Lines changed: 9 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -116,64 +116,23 @@ public static CompositionCommand Create(
116116
}
117117

118118
/// <summary>
119-
/// Computes a coverage definition key from path geometry and rasterization state.
119+
/// Computes a coverage definition key from path identity and rasterization state.
120120
/// </summary>
121121
/// <param name="path">Path to rasterize.</param>
122122
/// <param name="rasterizerOptions">Rasterizer options used for coverage generation.</param>
123-
/// <param name="definitionKeyCache">Optional scoped cache keyed by path identity to avoid repeated path flattening.</param>
123+
/// <param name="definitionKeyCache">Unused. Retained for API compatibility.</param>
124124
/// <returns>A stable key for coverage-equivalent commands.</returns>
125125
public static int ComputeCoverageDefinitionKey(
126126
IPath path,
127127
in RasterizerOptions rasterizerOptions,
128128
Dictionary<int, (IPath Path, int RasterState, int DefinitionKey)>? definitionKeyCache = null)
129129
{
130-
// Fast path: when the caller provides a cache and the same IPath object is
131-
// reused (e.g. cached glyph sub-pixel variants), skip the expensive
132-
// Flatten + point-hash and return the cached key.
133-
if (definitionKeyCache is not null)
134-
{
135-
int pathIdentity = RuntimeHelpers.GetHashCode(path);
136-
int rasterState = HashCode.Combine(
137-
rasterizerOptions.Interest.Size,
138-
(int)rasterizerOptions.IntersectionRule,
139-
(int)rasterizerOptions.RasterizationMode,
140-
(int)rasterizerOptions.SamplingOrigin);
141-
int cacheProbe = HashCode.Combine(pathIdentity, rasterState);
142-
143-
if (definitionKeyCache.TryGetValue(cacheProbe, out (IPath Path, int RasterState, int DefinitionKey) cached) &&
144-
ReferenceEquals(cached.Path, path) &&
145-
cached.RasterState == rasterState)
146-
{
147-
return cached.DefinitionKey;
148-
}
149-
150-
int definitionKey = ComputeCoverageDefinitionKeySlow(path, in rasterizerOptions);
151-
definitionKeyCache[cacheProbe] = (path, rasterState, definitionKey);
152-
return definitionKey;
153-
}
154-
155-
return ComputeCoverageDefinitionKeySlow(path, in rasterizerOptions);
156-
}
157-
158-
private static int ComputeCoverageDefinitionKeySlow(IPath path, in RasterizerOptions rasterizerOptions)
159-
{
160-
HashCode hash = default;
161-
foreach (ISimplePath simplePath in path.Flatten())
162-
{
163-
ReadOnlySpan<PointF> points = simplePath.Points.Span;
164-
hash.Add(simplePath.IsClosed);
165-
hash.Add(points.Length);
166-
for (int i = 0; i < points.Length; i++)
167-
{
168-
hash.Add(points[i].X);
169-
hash.Add(points[i].Y);
170-
}
171-
}
172-
173-
hash.Add(rasterizerOptions.Interest.Size);
174-
hash.Add((int)rasterizerOptions.IntersectionRule);
175-
hash.Add((int)rasterizerOptions.RasterizationMode);
176-
hash.Add((int)rasterizerOptions.SamplingOrigin);
177-
return hash.ToHashCode();
130+
int pathIdentity = RuntimeHelpers.GetHashCode(path);
131+
int rasterState = HashCode.Combine(
132+
rasterizerOptions.Interest.Size,
133+
(int)rasterizerOptions.IntersectionRule,
134+
(int)rasterizerOptions.RasterizationMode,
135+
(int)rasterizerOptions.SamplingOrigin);
136+
return HashCode.Combine(pathIdentity, rasterState);
178137
}
179138
}

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

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Runtime.CompilerServices;
5+
46
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
57

68
/// <summary>
@@ -18,14 +20,16 @@ public static List<CompositionBatch> CreatePreparedBatches(
1820
IReadOnlyList<CompositionCommand> commands,
1921
in Rectangle targetBounds)
2022
{
21-
List<CompositionBatch> batches = [];
23+
int commandCount = commands.Count;
24+
List<CompositionBatch> batches = new(EstimateBatchCapacity(commandCount));
2225
int index = 0;
23-
while (index < commands.Count)
26+
while (index < commandCount)
2427
{
2528
CompositionCommand definitionCommand = commands[index];
2629
int definitionKey = definitionCommand.DefinitionKey;
27-
List<PreparedCompositionCommand> preparedCommands = [];
28-
for (; index < commands.Count; index++)
30+
int remainingCount = commandCount - index;
31+
List<PreparedCompositionCommand> preparedCommands = new(EstimatePreparedCommandCapacity(remainingCount));
32+
for (; index < commandCount; index++)
2933
{
3034
CompositionCommand command = commands[index];
3135
if (command.DefinitionKey != definitionKey)
@@ -56,6 +60,55 @@ public static List<CompositionBatch> CreatePreparedBatches(
5660
return batches;
5761
}
5862

63+
/// <summary>
64+
/// Estimates initial capacity for the outer batch list from total scene command count.
65+
/// </summary>
66+
/// <param name="commandCount">Total number of scene commands.</param>
67+
/// <returns>Suggested initial capacity for the batch list.</returns>
68+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
69+
private static int EstimateBatchCapacity(int commandCount)
70+
{
71+
// Typical scenes reuse coverage definitions, so batch count is usually
72+
// meaningfully lower than command count.
73+
if (commandCount <= 8)
74+
{
75+
return commandCount;
76+
}
77+
78+
if (commandCount <= 128)
79+
{
80+
return commandCount / 2;
81+
}
82+
83+
return commandCount / 4;
84+
}
85+
86+
/// <summary>
87+
/// Estimates initial capacity for one contiguous prepared-command run.
88+
/// </summary>
89+
/// <param name="remainingCount">Commands remaining from the current scan index.</param>
90+
/// <returns>Suggested initial capacity for the current prepared-command list.</returns>
91+
/// <remarks>
92+
/// This estimate is intentionally capped for large tails because the list is
93+
/// allocated per run during scanning rather than once per scene.
94+
/// </remarks>
95+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
96+
private static int EstimatePreparedCommandCapacity(int remainingCount)
97+
{
98+
// Most adjacent commands share a definition in small-medium scenes.
99+
if (remainingCount <= 16)
100+
{
101+
return remainingCount;
102+
}
103+
104+
if (remainingCount <= 128)
105+
{
106+
return remainingCount / 2;
107+
}
108+
109+
return 64;
110+
}
111+
59112
/// <summary>
60113
/// Clips one scene command to target bounds and computes coverage source offset mapping.
61114
/// </summary>

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

Lines changed: 22 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Diagnostics.CodeAnalysis;
5-
using System.Numerics;
6-
using SixLabors.ImageSharp.Advanced;
75
using SixLabors.ImageSharp.Drawing.Shapes.Rasterization;
86
using SixLabors.ImageSharp.Memory;
97

@@ -175,13 +173,12 @@ internal void FlushPreparedBatch<TPixel>(
175173
applicators,
176174
destinationBounds,
177175
definition.RasterizerOptions.Interest.Top);
178-
DefaultRasterizer.Instance.Rasterize(
176+
177+
DefaultRasterizer.RasterizeRows(
179178
definition.Path,
180179
definition.RasterizerOptions,
181180
configuration.MemoryAllocator,
182-
ref operation,
183-
static (int y, Span<float> scanline, ref RowOperation<TPixel> callbackState) =>
184-
callbackState.InvokeScanline(y, scanline));
181+
operation.InvokeCoverageRow);
185182
}
186183
finally
187184
{
@@ -212,153 +209,40 @@ public RowOperation(
212209
this.coverageTop = coverageTop;
213210
}
214211

215-
public void InvokeScanline(int y, Span<float> scanline)
212+
public void InvokeCoverageRow(int y, int startX, Span<float> coverage)
216213
{
217214
int sourceY = y - this.coverageTop;
215+
int rowStart = startX;
216+
int rowEnd = startX + coverage.Length;
217+
218+
Rectangle destinationBounds = this.destinationBounds;
219+
BrushApplicator<TPixel>[] applicators = this.applicators;
218220
for (int i = 0; i < this.commands.Count; i++)
219221
{
220222
PreparedCompositionCommand command = this.commands[i];
223+
Rectangle commandDestination = command.DestinationRegion;
224+
221225
int commandY = sourceY - command.SourceOffset.Y;
222-
if ((uint)commandY >= (uint)command.DestinationRegion.Height)
226+
if ((uint)commandY >= (uint)commandDestination.Height)
223227
{
224228
continue;
225229
}
226230

227-
int destinationX = this.destinationBounds.X + command.DestinationRegion.X;
228-
int destinationY = this.destinationBounds.Y + command.DestinationRegion.Y + commandY;
229231
int sourceStartX = command.SourceOffset.X;
230-
Span<float> rowSlice = scanline.Slice(sourceStartX, command.DestinationRegion.Width);
231-
ApplyCoverageSpans(this.applicators[i], rowSlice, destinationX, destinationY);
232-
}
233-
}
234-
235-
/// <summary>
236-
/// Applies only contiguous non-zero coverage spans for a scanline.
237-
/// </summary>
238-
/// <param name="applicator">Brush applicator used to composite pixels.</param>
239-
/// <param name="coverage">Scanline coverage values for the current command row.</param>
240-
/// <param name="destinationX">Destination x coordinate for the start of <paramref name="coverage"/>.</param>
241-
/// <param name="destinationY">Destination y coordinate for the scanline.</param>
242-
private static void ApplyCoverageSpans(
243-
BrushApplicator<TPixel> applicator,
244-
Span<float> coverage,
245-
int destinationX,
246-
int destinationY)
247-
{
248-
// Use SIMD path when available and the span is large enough to amortize setup.
249-
if (Vector.IsHardwareAccelerated && coverage.Length >= (Vector<float>.Count * 2))
250-
{
251-
ApplyCoverageSpansSimd(applicator, coverage, destinationX, destinationY);
252-
return;
253-
}
254-
255-
ApplyCoverageSpansScalar(applicator, coverage, destinationX, destinationY);
256-
}
257-
258-
/// <summary>
259-
/// Applies contiguous non-zero coverage spans using SIMD-accelerated zero/non-zero chunk checks.
260-
/// </summary>
261-
/// <param name="applicator">Brush applicator used to composite pixels.</param>
262-
/// <param name="coverage">Scanline coverage values for the current command row.</param>
263-
/// <param name="destinationX">Destination x coordinate for the start of <paramref name="coverage"/>.</param>
264-
/// <param name="destinationY">Destination y coordinate for the scanline.</param>
265-
private static void ApplyCoverageSpansSimd(
266-
BrushApplicator<TPixel> applicator,
267-
Span<float> coverage,
268-
int destinationX,
269-
int destinationY)
270-
{
271-
int i = 0;
272-
int n = coverage.Length;
273-
int width = Vector<float>.Count;
274-
Vector<float> zero = Vector<float>.Zero;
275-
276-
while (i < n)
277-
{
278-
// Phase 1: skip fully-zero SIMD blocks.
279-
while (i <= n - width)
280-
{
281-
Vector<float> v = new(coverage.Slice(i, width));
282-
if (!Vector.EqualsAll(v, zero))
283-
{
284-
break;
285-
}
286-
287-
i += width;
288-
}
289-
290-
while (i < n && coverage[i] == 0F)
291-
{
292-
i++;
293-
}
294-
295-
if (i >= n)
296-
{
297-
return;
298-
}
299-
300-
int runStart = i;
301-
302-
// Phase 2: advance across fully non-zero SIMD blocks.
303-
while (i <= n - width)
232+
int sourceEndX = sourceStartX + commandDestination.Width;
233+
int overlapStart = Math.Max(rowStart, sourceStartX);
234+
int overlapEnd = Math.Min(rowEnd, sourceEndX);
235+
if (overlapEnd <= overlapStart)
304236
{
305-
Vector<float> v = new(coverage.Slice(i, width));
306-
Vector<int> eqZero = Vector.Equals(v, zero);
307-
if (!Vector.EqualsAll(eqZero, Vector<int>.Zero))
308-
{
309-
break;
310-
}
311-
312-
i += width;
237+
continue;
313238
}
314239

315-
while (i < n && coverage[i] != 0F)
316-
{
317-
i++;
318-
}
240+
int localStart = overlapStart - rowStart;
241+
int localLength = overlapEnd - overlapStart;
242+
int destinationX = destinationBounds.X + commandDestination.X + (overlapStart - sourceStartX);
243+
int destinationY = destinationBounds.Y + commandDestination.Y + commandY;
319244

320-
// Apply exactly one contiguous non-zero run.
321-
applicator.Apply(coverage[runStart..i], destinationX + runStart, destinationY);
322-
}
323-
}
324-
325-
/// <summary>
326-
/// Applies contiguous non-zero coverage spans using a scalar scan.
327-
/// </summary>
328-
/// <param name="applicator">Brush applicator used to composite pixels.</param>
329-
/// <param name="coverage">Scanline coverage values for the current command row.</param>
330-
/// <param name="destinationX">Destination x coordinate for the start of <paramref name="coverage"/>.</param>
331-
/// <param name="destinationY">Destination y coordinate for the scanline.</param>
332-
private static void ApplyCoverageSpansScalar(
333-
BrushApplicator<TPixel> applicator,
334-
Span<float> coverage,
335-
int destinationX,
336-
int destinationY)
337-
{
338-
// Track the start of a contiguous non-zero coverage run.
339-
int runStart = -1;
340-
for (int i = 0; i < coverage.Length; i++)
341-
{
342-
if (coverage[i] > 0F)
343-
{
344-
// Enter a new run when transitioning from zero to non-zero coverage.
345-
if (runStart < 0)
346-
{
347-
runStart = i;
348-
}
349-
}
350-
else if (runStart >= 0)
351-
{
352-
// Coverage returned to zero: apply the finished run only.
353-
applicator.Apply(coverage[runStart..i], destinationX + runStart, destinationY);
354-
runStart = -1;
355-
}
356-
}
357-
358-
if (runStart >= 0)
359-
{
360-
// Flush trailing run that reaches end-of-scanline.
361-
applicator.Apply(coverage[runStart..], destinationX + runStart, destinationY);
245+
applicators[i].Apply(coverage.Slice(localStart, localLength), destinationX, destinationY);
362246
}
363247
}
364248
}

0 commit comments

Comments
 (0)