Skip to content

Commit 2cf5aea

Browse files
Use less memory
1 parent ce69978 commit 2cf5aea

File tree

1 file changed

+100
-37
lines changed

1 file changed

+100
-37
lines changed

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

Lines changed: 100 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization;
1313
/// </summary>
1414
internal 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

Comments
 (0)