Skip to content

Commit c852e15

Browse files
Add single tile hot path.
1 parent 1714a6e commit c852e15

File tree

1 file changed

+147
-78
lines changed

1 file changed

+147
-78
lines changed

src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs

Lines changed: 147 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,7 @@ private static void RasterizeCore<TState>(
123123
// canonical representation so path flattening/orientation work is never repeated.
124124
using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator);
125125
using IMemoryOwner<EdgeData> edgeDataOwner = allocator.Allocate<EdgeData>(multipolygon.TotalVertexCount);
126-
Span<EdgeData> edgeBuffer = edgeDataOwner.Memory.Span;
127-
int edgeCount = BuildEdgeTable(multipolygon, interest.Left, interest.Top, height, samplingOffsetX, edgeBuffer);
126+
int edgeCount = BuildEdgeTable(multipolygon, interest.Left, interest.Top, height, samplingOffsetX, edgeDataOwner.Memory.Span);
128127
if (edgeCount <= 0)
129128
{
130129
return;
@@ -291,7 +290,7 @@ private static void RasterizeSequentialBands<TState>(
291290
/// <param name="state">Caller-owned mutable state.</param>
292291
/// <param name="scanlineHandler">Scanline callback invoked in ascending Y order.</param>
293292
/// <returns>
294-
/// <see langword="true"/> when the parallel path executed successfully;
293+
/// <see langword="true"/> when the tiled path executed successfully;
295294
/// <see langword="false"/> when the caller should run sequential fallback.
296295
/// </returns>
297296
private static bool TryRasterizeParallel<TState>(
@@ -310,28 +309,42 @@ private static bool TryRasterizeParallel<TState>(
310309
RasterizerScanlineHandler<TState> scanlineHandler)
311310
where TState : struct
312311
{
313-
if (Environment.ProcessorCount < 2)
312+
int tileHeight = Math.Min(DefaultTileHeight, maxBandRows);
313+
if (tileHeight < 1)
314314
{
315315
return false;
316316
}
317317

318-
long totalPixels = (long)width * height;
319-
if (totalPixels > ParallelOutputPixelBudget)
318+
int tileCount = (height + tileHeight - 1) / tileHeight;
319+
if (tileCount == 1)
320320
{
321-
// Parallel mode buffers tile coverage before ordered emission. Skip when the
322-
// buffered output footprint would exceed our safety budget.
323-
return false;
321+
// Tiny workload fast path: avoid bucket construction, worker scheduling, and
322+
// tile-output buffering when everything fits in a single tile.
323+
RasterizeSingleTileDirect(
324+
edgeMemory.Span[..edgeCount],
325+
width,
326+
height,
327+
interestTop,
328+
wordsPerRow,
329+
coverStride,
330+
intersectionRule,
331+
rasterizationMode,
332+
allocator,
333+
ref state,
334+
scanlineHandler);
335+
return true;
324336
}
325337

326-
int tileHeight = Math.Min(DefaultTileHeight, maxBandRows);
327-
if (tileHeight < 1)
338+
if (Environment.ProcessorCount < 2)
328339
{
329340
return false;
330341
}
331342

332-
int tileCount = (height + tileHeight - 1) / tileHeight;
333-
if (tileCount < 2)
343+
long totalPixels = (long)width * height;
344+
if (totalPixels > ParallelOutputPixelBudget)
334345
{
346+
// Parallel mode buffers tile coverage before ordered emission. Skip when the
347+
// buffered output footprint would exceed our safety budget.
335348
return false;
336349
}
337350

@@ -446,6 +459,46 @@ private static bool TryRasterizeParallel<TState>(
446459
}
447460
}
448461

462+
/// <summary>
463+
/// Rasterizes a single tile directly into the caller callback.
464+
/// </summary>
465+
/// <remarks>
466+
/// This avoids parallel setup and tile-output buffering for tiny workloads while preserving
467+
/// the same scan-conversion math and callback ordering as the general tiled path.
468+
/// </remarks>
469+
/// <typeparam name="TState">The caller-owned mutable state type.</typeparam>
470+
/// <param name="edges">Prebuilt edge table.</param>
471+
/// <param name="width">Destination width in pixels.</param>
472+
/// <param name="height">Destination height in pixels.</param>
473+
/// <param name="interestTop">Absolute top Y of the interest rectangle.</param>
474+
/// <param name="wordsPerRow">Bit-vector words per row.</param>
475+
/// <param name="coverStride">Cover-area stride in ints.</param>
476+
/// <param name="intersectionRule">Fill rule.</param>
477+
/// <param name="rasterizationMode">Coverage mode (AA or aliased).</param>
478+
/// <param name="allocator">Temporary buffer allocator.</param>
479+
/// <param name="state">Caller-owned mutable state.</param>
480+
/// <param name="scanlineHandler">Scanline callback invoked in ascending Y order.</param>
481+
private static void RasterizeSingleTileDirect<TState>(
482+
ReadOnlySpan<EdgeData> edges,
483+
int width,
484+
int height,
485+
int interestTop,
486+
int wordsPerRow,
487+
int coverStride,
488+
IntersectionRule intersectionRule,
489+
RasterizationMode rasterizationMode,
490+
MemoryAllocator allocator,
491+
ref TState state,
492+
RasterizerScanlineHandler<TState> scanlineHandler)
493+
where TState : struct
494+
{
495+
using WorkerScratch scratch = WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, height);
496+
Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode);
497+
context.RasterizeEdgeTable(edges, bandTop: 0);
498+
context.EmitScanlines(interestTop, scratch.Scanline, ref state, scanlineHandler);
499+
context.ResetTouchedRows();
500+
}
501+
449502
/// <summary>
450503
/// Rasterizes one tile/band edge subset into temporary coverage buffers.
451504
/// </summary>
@@ -563,12 +616,8 @@ private static void CaptureTileScanline(int y, Span<float> scanline, ref TileCap
563616
}
564617

565618
/// <summary>
566-
/// Builds a compact edge table in scanner-local coordinates.
619+
/// Builds an edge table in scanner-local coordinates.
567620
/// </summary>
568-
/// <remarks>
569-
/// Edges are converted to 24.8 fixed-point once during table construction so the hot
570-
/// band/tile rasterization loop does not pay float-to-fixed conversion costs repeatedly.
571-
/// </remarks>
572621
/// <param name="multipolygon">Input tessellated rings.</param>
573622
/// <param name="minX">Interest left in absolute coordinates.</param>
574623
/// <param name="minY">Interest top in absolute coordinates.</param>
@@ -608,11 +657,6 @@ private static int BuildEdgeTable(
608657
continue;
609658
}
610659

611-
if (!TryGetEdgeRowRange(y0, y1, height, out int minRow, out int maxRow))
612-
{
613-
continue;
614-
}
615-
616660
int fx0 = FloatToFixed24Dot8(x0);
617661
int fy0 = FloatToFixed24Dot8(y0);
618662
int fx1 = FloatToFixed24Dot8(x1);
@@ -622,48 +666,14 @@ private static int BuildEdgeTable(
622666
continue;
623667
}
624668

669+
ComputeEdgeRowBounds(fy0, fy1, out int minRow, out int maxRow);
625670
destination[count++] = new EdgeData(fx0, fy0, fx1, fy1, minRow, maxRow);
626671
}
627672
}
628673

629674
return count;
630675
}
631676

632-
/// <summary>
633-
/// Computes inclusive row bounds touched by the edge in scanner-local coordinates.
634-
/// </summary>
635-
/// <param name="y0">Edge start Y.</param>
636-
/// <param name="y1">Edge end Y.</param>
637-
/// <param name="height">Scanner height.</param>
638-
/// <param name="minRow">Minimum affected row.</param>
639-
/// <param name="maxRow">Maximum affected row.</param>
640-
/// <returns><see langword="true"/> if the edge intersects scanner rows.</returns>
641-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
642-
private static bool TryGetEdgeRowRange(float y0, float y1, int height, out int minRow, out int maxRow)
643-
{
644-
float minY = MathF.Min(y0, y1);
645-
float maxY = MathF.Max(y0, y1);
646-
minRow = (int)MathF.Floor(minY);
647-
maxRow = (int)MathF.Ceiling(maxY) - 1;
648-
649-
if (maxRow < 0 || minRow >= height)
650-
{
651-
return false;
652-
}
653-
654-
if (minRow < 0)
655-
{
656-
minRow = 0;
657-
}
658-
659-
if (maxRow >= height)
660-
{
661-
maxRow = height - 1;
662-
}
663-
664-
return minRow <= maxRow;
665-
}
666-
667677
/// <summary>
668678
/// Converts bit count to the number of machine words needed to hold the bitset row.
669679
/// </summary>
@@ -716,6 +726,33 @@ private static bool TryGetBandHeight(int width, int height, int wordsPerRow, lon
716726
[MethodImpl(MethodImplOptions.AggressiveInlining)]
717727
private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne);
718728

729+
/// <summary>
730+
/// Computes the inclusive row range affected by a clipped non-horizontal edge.
731+
/// </summary>
732+
/// <param name="y0">Edge start Y in 24.8 fixed-point.</param>
733+
/// <param name="y1">Edge end Y in 24.8 fixed-point.</param>
734+
/// <param name="minRow">First affected integer scan row.</param>
735+
/// <param name="maxRow">Last affected integer scan row.</param>
736+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
737+
private static void ComputeEdgeRowBounds(int y0, int y1, out int minRow, out int maxRow)
738+
{
739+
int y0Row = y0 >> FixedShift;
740+
int y1Row = y1 >> FixedShift;
741+
742+
// First touched row is floor(min(y0, y1)).
743+
minRow = y0Row < y1Row ? y0Row : y1Row;
744+
745+
int y0Fraction = y0 & (FixedOne - 1);
746+
int y1Fraction = y1 & (FixedOne - 1);
747+
748+
// Last touched row is ceil(max(y)) - 1:
749+
// - when fractional part is non-zero, row is unchanged;
750+
// - when exactly on a row boundary, subtract 1 (edge ownership rule).
751+
int y0Candidate = y0Row - (((y0Fraction - 1) >> 31) & 1);
752+
int y1Candidate = y1Row - (((y1Fraction - 1) >> 31) & 1);
753+
maxRow = y0Candidate > y1Candidate ? y0Candidate : y1Candidate;
754+
}
755+
719756
/// <summary>
720757
/// Clips a fixed-point segment against vertical bounds.
721758
/// </summary>
@@ -1010,6 +1047,37 @@ public void RasterizeMultipolygon(TessellatedMultipolygon multipolygon, int minX
10101047
}
10111048
}
10121049

1050+
/// <summary>
1051+
/// Rasterizes all prebuilt edges that overlap this context.
1052+
/// </summary>
1053+
/// <param name="edges">Shared edge table.</param>
1054+
/// <param name="bandTop">Top row of this context in global scanner-local coordinates.</param>
1055+
public void RasterizeEdgeTable(ReadOnlySpan<EdgeData> edges, int bandTop)
1056+
{
1057+
int bandTopFixed = bandTop * FixedOne;
1058+
int bandBottomFixed = bandTopFixed + (this.height * FixedOne);
1059+
1060+
for (int i = 0; i < edges.Length; i++)
1061+
{
1062+
EdgeData edge = edges[i];
1063+
int x0 = edge.X0;
1064+
int y0 = edge.Y0;
1065+
int x1 = edge.X1;
1066+
int y1 = edge.Y1;
1067+
1068+
if (!ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed))
1069+
{
1070+
continue;
1071+
}
1072+
1073+
// Convert global scanner Y to band-local Y after clipping.
1074+
y0 -= bandTopFixed;
1075+
y1 -= bandTopFixed;
1076+
1077+
this.RasterizeLine(x0, y0, x1, y1);
1078+
}
1079+
}
1080+
10131081
/// <summary>
10141082
/// Rasterizes a subset of prebuilt edges that intersect this context's vertical range.
10151083
/// </summary>
@@ -1952,52 +2020,53 @@ private void RasterizeLine(int x0, int y0, int x1, int y1)
19522020
/// Immutable scanner-local edge record with precomputed affected-row bounds.
19532021
/// </summary>
19542022
/// <remarks>
1955-
/// Coordinates are stored as signed 24.8 fixed-point values.
2023+
/// All coordinates are stored as signed 24.8 fixed-point integers for predictable hot-path
2024+
/// access without per-read unpacking.
19562025
/// </remarks>
19572026
private readonly struct EdgeData
19582027
{
1959-
/// <summary>
1960-
/// Initializes a new instance of the <see cref="EdgeData"/> struct.
1961-
/// </summary>
1962-
public EdgeData(int x0, int y0, int x1, int y1, int minRow, int maxRow)
1963-
{
1964-
this.X0 = x0;
1965-
this.Y0 = y0;
1966-
this.X1 = x1;
1967-
this.Y1 = y1;
1968-
this.MinRow = minRow;
1969-
this.MaxRow = maxRow;
1970-
}
1971-
19722028
/// <summary>
19732029
/// Gets edge start X in scanner-local coordinates (24.8 fixed-point).
19742030
/// </summary>
1975-
public int X0 { get; }
2031+
public readonly int X0;
19762032

19772033
/// <summary>
19782034
/// Gets edge start Y in scanner-local coordinates (24.8 fixed-point).
19792035
/// </summary>
1980-
public int Y0 { get; }
2036+
public readonly int Y0;
19812037

19822038
/// <summary>
19832039
/// Gets edge end X in scanner-local coordinates (24.8 fixed-point).
19842040
/// </summary>
1985-
public int X1 { get; }
2041+
public readonly int X1;
19862042

19872043
/// <summary>
19882044
/// Gets edge end Y in scanner-local coordinates (24.8 fixed-point).
19892045
/// </summary>
1990-
public int Y1 { get; }
2046+
public readonly int Y1;
19912047

19922048
/// <summary>
19932049
/// Gets the first scanner row affected by this edge.
19942050
/// </summary>
1995-
public int MinRow { get; }
2051+
public readonly int MinRow;
19962052

19972053
/// <summary>
19982054
/// Gets the last scanner row affected by this edge.
19992055
/// </summary>
2000-
public int MaxRow { get; }
2056+
public readonly int MaxRow;
2057+
2058+
/// <summary>
2059+
/// Initializes a new instance of the <see cref="EdgeData"/> struct.
2060+
/// </summary>
2061+
public EdgeData(int x0, int y0, int x1, int y1, int minRow, int maxRow)
2062+
{
2063+
this.X0 = x0;
2064+
this.Y0 = y0;
2065+
this.X1 = x1;
2066+
this.Y1 = y1;
2067+
this.MinRow = minRow;
2068+
this.MaxRow = maxRow;
2069+
}
20012070
}
20022071

20032072
/// <summary>

0 commit comments

Comments
 (0)