@@ -13,6 +13,10 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization;
1313/// </summary>
1414internal static class SharpBlazeScanner
1515{
16+ // Upper bound for temporary scanner buffers (bit vectors + cover/area + start-cover rows).
17+ // Keeping this bounded prevents pathological full-image allocations on very large interests.
18+ private const long BandMemoryBudgetBytes = 64L * 1024L * 1024L ;
19+
1620 private const int FixedShift = 8 ;
1721 private const int FixedOne = 1 << FixedShift ;
1822 private static readonly int WordBitCount = IntPtr . Size * 8 ;
@@ -39,39 +43,108 @@ public static bool TryRasterize<TState>(
3943 }
4044
4145 int wordsPerRow = BitVectorsForMaxBitCount ( width ) ;
42- long bitVectorCount = ( long ) wordsPerRow * height ;
4346 long coverStride = ( long ) width * 2 ;
44- long coverCount = coverStride * height ;
45- if ( bitVectorCount > int . MaxValue || coverCount > int . MaxValue )
47+ if ( coverStride > int . MaxValue ||
48+ ! TryGetBandHeight ( width , height , wordsPerRow , coverStride , out int maxBandRows ) )
4649 {
4750 return false ;
4851 }
4952
50- using IMemoryOwner < nuint > bitVectorsOwner = allocator . Allocate < nuint > ( ( int ) bitVectorCount , AllocationOptions . Clean ) ;
51- using IMemoryOwner < int > coverAreaOwner = allocator . Allocate < int > ( ( int ) coverCount , AllocationOptions . Clean ) ;
52- using IMemoryOwner < int > startCoverOwner = allocator . Allocate < int > ( height , AllocationOptions . Clean ) ;
53- using IMemoryOwner < float > scanlineOwner = allocator . Allocate < float > ( width , AllocationOptions . Clean ) ;
53+ int coverStrideInt = ( int ) coverStride ;
54+ int bitVectorCapacity = checked ( wordsPerRow * maxBandRows ) ;
55+ int coverAreaCapacity = checked ( coverStrideInt * maxBandRows ) ;
56+ using IMemoryOwner < nuint > bitVectorsOwner = allocator . Allocate < nuint > ( bitVectorCapacity ) ;
57+ using IMemoryOwner < int > coverAreaOwner = allocator . Allocate < int > ( coverAreaCapacity ) ;
58+ using IMemoryOwner < int > startCoverOwner = allocator . Allocate < int > ( maxBandRows ) ;
59+
60+ // Per-row activity flags avoid scanning the full bit-vector row just to detect "empty row".
61+ using IMemoryOwner < byte > rowHasBitsOwner = allocator . Allocate < byte > ( maxBandRows ) ;
62+ using IMemoryOwner < float > scanlineOwner = allocator . Allocate < float > ( width ) ;
63+
64+ Span < nuint > bitVectorsBuffer = bitVectorsOwner . Memory . Span ;
65+ Span < int > coverAreaBuffer = coverAreaOwner . Memory . Span ;
66+ Span < int > startCoverBuffer = startCoverOwner . Memory . Span ;
67+ Span < byte > rowHasBitsBuffer = rowHasBitsOwner . Memory . Span ;
68+ Span < float > scanline = scanlineOwner . Memory . Span ;
5469
5570 float samplingOffsetX = options . SamplingOrigin == RasterizerSamplingOrigin . PixelCenter ? 0.5F : 0F ;
5671
57- Context context = new (
58- bitVectorsOwner . Memory . Span ,
59- coverAreaOwner . Memory . Span ,
60- startCoverOwner . Memory . Span ,
61- width ,
62- height ,
63- wordsPerRow ,
64- ( int ) coverStride ,
65- options . IntersectionRule ) ;
66-
67- context . RasterizePath ( path , allocator , interest . Left , interest . Top , samplingOffsetX ) ;
68- context . EmitScanlines ( interest . Top , scanlineOwner . Memory . Span , ref state , scanlineHandler ) ;
72+ using TessellatedMultipolygon multipolygon = TessellatedMultipolygon . Create ( path , allocator ) ;
73+ int bandTop = 0 ;
74+ while ( bandTop < height )
75+ {
76+ int bandHeight = Math . Min ( maxBandRows , height - bandTop ) ;
77+ int bitVectorCount = wordsPerRow * bandHeight ;
78+ int coverCount = coverStrideInt * bandHeight ;
79+
80+ Span < nuint > bitVectors = bitVectorsBuffer [ ..bitVectorCount ] ;
81+ Span < int > coverArea = coverAreaBuffer [ ..coverCount ] ;
82+ Span < int > startCover = startCoverBuffer [ ..bandHeight ] ;
83+ Span < byte > rowHasBits = rowHasBitsBuffer [ ..bandHeight ] ;
84+
85+ bitVectors . Clear ( ) ;
86+ coverArea . Clear ( ) ;
87+ startCover . Clear ( ) ;
88+ rowHasBits . Clear ( ) ;
89+
90+ Context context = new (
91+ bitVectors ,
92+ coverArea ,
93+ startCover ,
94+ rowHasBits ,
95+ width ,
96+ bandHeight ,
97+ wordsPerRow ,
98+ coverStrideInt ,
99+ options . IntersectionRule ) ;
100+
101+ context . RasterizeMultipolygon (
102+ multipolygon ,
103+ interest . Left ,
104+ interest . Top + bandTop ,
105+ samplingOffsetX ) ;
106+
107+ context . EmitScanlines ( interest . Top + bandTop , scanline , ref state , scanlineHandler ) ;
108+ bandTop += bandHeight ;
109+ }
110+
69111 return true ;
70112 }
71113
72114 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
73115 private static int BitVectorsForMaxBitCount ( int maxBitCount ) => ( maxBitCount + WordBitCount - 1 ) / WordBitCount ;
74116
117+ private static bool TryGetBandHeight ( int width , int height , int wordsPerRow , long coverStride , out int bandHeight )
118+ {
119+ bandHeight = 0 ;
120+ if ( width <= 0 || height <= 0 || wordsPerRow <= 0 || coverStride <= 0 )
121+ {
122+ return false ;
123+ }
124+
125+ long bytesPerRow =
126+ ( ( long ) wordsPerRow * IntPtr . Size ) +
127+ ( coverStride * sizeof ( int ) ) +
128+ sizeof ( int ) ;
129+
130+ long rowsByBudget = BandMemoryBudgetBytes / bytesPerRow ;
131+ if ( rowsByBudget < 1 )
132+ {
133+ rowsByBudget = 1 ;
134+ }
135+
136+ long rowsByBitVectors = int . MaxValue / wordsPerRow ;
137+ long rowsByCoverArea = int . MaxValue / coverStride ;
138+ long maxRows = Math . Min ( rowsByBudget , Math . Min ( rowsByBitVectors , rowsByCoverArea ) ) ;
139+ if ( maxRows < 1 )
140+ {
141+ return false ;
142+ }
143+
144+ bandHeight = ( int ) Math . Min ( height , maxRows ) ;
145+ return bandHeight > 0 ;
146+ }
147+
75148 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
76149 private static int FloatToFixed24Dot8 ( float value ) => ( int ) MathF . Round ( value * FixedOne ) ;
77150
@@ -163,6 +236,7 @@ private ref struct Context
163236 private readonly Span < nuint > bitVectors ;
164237 private readonly Span < int > coverArea ;
165238 private readonly Span < int > startCover ;
239+ private readonly Span < byte > rowHasBits ;
166240 private readonly int width ;
167241 private readonly int height ;
168242 private readonly int wordsPerRow ;
@@ -173,6 +247,7 @@ public Context(
173247 Span < nuint > bitVectors ,
174248 Span < int > coverArea ,
175249 Span < int > startCover ,
250+ Span < byte > rowHasBits ,
176251 int width ,
177252 int height ,
178253 int wordsPerRow ,
@@ -182,16 +257,16 @@ public Context(
182257 this . bitVectors = bitVectors ;
183258 this . coverArea = coverArea ;
184259 this . startCover = startCover ;
260+ this . rowHasBits = rowHasBits ;
185261 this . width = width ;
186262 this . height = height ;
187263 this . wordsPerRow = wordsPerRow ;
188264 this . coverStride = coverStride ;
189265 this . intersectionRule = intersectionRule ;
190266 }
191267
192- public void RasterizePath ( IPath path , MemoryAllocator allocator , int minX , int minY , float samplingOffsetX )
268+ public void RasterizeMultipolygon ( TessellatedMultipolygon multipolygon , int minX , int minY , float samplingOffsetX )
193269 {
194- using TessellatedMultipolygon multipolygon = TessellatedMultipolygon . Create ( path , allocator ) ;
195270 foreach ( TessellatedMultipolygon . Ring ring in multipolygon )
196271 {
197272 ReadOnlySpan < PointF > vertices = ring . Vertices ;
@@ -234,13 +309,13 @@ public void EmitScanlines<TState>(int destinationTop, Span<float> scanline, ref
234309 {
235310 for ( int row = 0 ; row < this . height ; row ++ )
236311 {
237- Span < nuint > rowBitVectors = this . bitVectors . Slice ( row * this . wordsPerRow , this . wordsPerRow ) ;
238312 int rowCover = this . startCover [ row ] ;
239- if ( rowCover == 0 && IsRowEmpty ( rowBitVectors ) )
313+ if ( rowCover == 0 && this . rowHasBits [ row ] == 0 )
240314 {
241315 continue ;
242316 }
243317
318+ Span < nuint > rowBitVectors = this . bitVectors . Slice ( row * this . wordsPerRow , this . wordsPerRow ) ;
244319 scanline . Clear ( ) ;
245320 bool scanlineDirty = this . EmitRowCoverage ( rowBitVectors , row , rowCover , scanline ) ;
246321 if ( scanlineDirty )
@@ -250,19 +325,6 @@ public void EmitScanlines<TState>(int destinationTop, Span<float> scanline, ref
250325 }
251326 }
252327
253- private static bool IsRowEmpty ( ReadOnlySpan < nuint > rowBitVectors )
254- {
255- for ( int i = 0 ; i < rowBitVectors . Length ; i ++ )
256- {
257- if ( rowBitVectors [ i ] != 0 )
258- {
259- return false ;
260- }
261- }
262-
263- return true ;
264- }
265-
266328 private bool EmitRowCoverage ( ReadOnlySpan < nuint > rowBitVectors , int row , int cover , Span < float > scanline )
267329 {
268330 int rowOffset = row * this . coverStride ;
@@ -397,7 +459,7 @@ private static bool FlushSpan(Span<float> scanline, int start, int end, float co
397459 return false ;
398460 }
399461
400- scanline . Slice ( start , end - start ) . Fill ( coverage ) ;
462+ scanline [ start .. end ] . Fill ( coverage ) ;
401463 return true ;
402464 }
403465
@@ -410,6 +472,7 @@ private bool ConditionalSetBit(int row, int column)
410472 ref nuint word = ref this . bitVectors [ wordIndex ] ;
411473 bool newlySet = ( word & mask ) == 0 ;
412474 word |= mask ;
475+ this . rowHasBits [ row ] = 1 ;
413476 return newlySet ;
414477 }
415478
0 commit comments