@@ -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