Skip to content

Commit b796ece

Browse files
Remove GeometryPreparationCache and cache bands
1 parent 5add5a3 commit b796ece

9 files changed

Lines changed: 108 additions & 324 deletions

File tree

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

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,9 @@ public static CompositionCommand Create(
160160
/// and pre-computed visibility against the target.
161161
/// </summary>
162162
/// <param name="targetBounds">The target frame bounds for visibility clipping.</param>
163-
/// <param name="geometryCache">
164-
/// Optional flush-scoped cache used to share prepared paths across commands that
165-
/// have identical geometry-affecting inputs.
166-
/// </param>
167-
internal void Prepare(in Rectangle targetBounds, GeometryPreparationCache? geometryCache = null)
163+
internal void Prepare(in Rectangle targetBounds)
168164
{
169-
IPath preparedPath = geometryCache?.GetOrCreate(this) ?? this.BuildPreparedPath();
165+
IPath preparedPath = this.BuildPreparedPath();
170166
this.PreparedPath = preparedPath;
171167

172168
// Transform the brush to match the path coordinate space.
@@ -232,13 +228,6 @@ internal void Prepare(in Rectangle targetBounds, GeometryPreparationCache? geome
232228
this.IsVisible = true;
233229
}
234230

235-
/// <summary>
236-
/// Creates the flush-scoped cache key used to share prepared paths.
237-
/// </summary>
238-
/// <returns>The geometry preparation cache key.</returns>
239-
internal readonly GeometryPreparationCache.GeometryPreparationKey CreateGeometryPreparationKey()
240-
=> new(this.sourcePath, this.transform, this.pen, this.clipPaths, this.shapeOptions, this.enforceFillOrientation);
241-
242231
/// <summary>
243232
/// Builds the prepared path for this command without consulting any external cache.
244233
/// Applies transform, stroke expansion, and clipping. The returned path is ready

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

Lines changed: 52 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ public static FlushScene Create(
257257
int singleBandItemCount = 0;
258258
int smallEdgeItemCount = 0;
259259

260+
// Segment start offsets into the band assignment cache — one entry per visible item.
261+
// Used in Phases 4 and 5 to slice each item's portion of the cache.
262+
int[] itemSegmentStarts = new int[visibleItemCount];
263+
260264
// Phase 3: derive scene-wide maxima once so every worker can allocate one reusable scratch set.
261265
{
262266
Span<RasterizerOptions> roSpan = rasterizerOptionsBuffer.AsSpan();
@@ -281,6 +285,7 @@ public static FlushScene Create(
281285
maxWidth = Math.Max(maxWidth, width);
282286
maxWordsPerRow = Math.Max(maxWordsPerRow, BitVectorsForMaxBitCount(width));
283287
maxCoverStride = Math.Max(maxCoverStride, (int)coverStride);
288+
itemSegmentStarts[i] = (int)totalEdgeCount;
284289
totalEdgeCount += mpSpan[i].TotalSegmentCount;
285290

286291
if (bcSpan[i] == 1)
@@ -295,6 +300,12 @@ public static FlushScene Create(
295300
}
296301
}
297302

303+
// Band assignment cache: stores the packed (firstBand | lastBand << 16) for each segment,
304+
// or -1 if the segment falls outside the item's interest rectangle.
305+
// Built during Phase 4 so Phase 5 can scatter from integers instead of re-enumerating path geometry.
306+
IMemoryOwner<int> bandAssignmentCacheOwner = allocator.Allocate<int>(Math.Max((int)totalEdgeCount, 1));
307+
Memory<int> bandAssignmentCache = bandAssignmentCacheOwner.Memory;
308+
298309
IMemoryOwner<int> itemBandOffsetStartsOwner = allocator.Allocate<int>(visibleItemCount + 1);
299310
int totalBandOffsetCount = 0;
300311
{
@@ -312,7 +323,9 @@ public static FlushScene Create(
312323
IMemoryOwner<int> bandSegmentOffsetsOwner = allocator.Allocate<int>(totalBandOffsetCount);
313324
long totalBandSegmentRefs = 0;
314325

315-
// Phase 4: count how many prepared segments each item contributes to each row band.
326+
// Phase 4: count how many prepared segments each item contributes to each row band,
327+
// and record the packed band assignment for each segment in the cache so Phase 5
328+
// can scatter from integers rather than re-enumerating path geometry.
316329
_ = Parallel.ForEach(
317330
Partitioner.Create(0, visibleItemCount),
318331
() => 0L,
@@ -323,15 +336,17 @@ public static FlushScene Create(
323336
Span<int> bcSpan = itemBandCounts.Span;
324337
Span<int> offsetSpan = itemBandOffsetStartsOwner.Memory.Span;
325338
Span<int> bsoSpan = bandSegmentOffsetsOwner.Memory.Span;
339+
Span<int> bacSpan = bandAssignmentCache.Span;
326340

327341
for (int i = range.Item1; i < range.Item2; i++)
328342
{
329-
localTotal += CountBandSegmentRefs(
343+
localTotal += CountAndStoreBandSegmentRefs(
330344
mpSpan[i],
331345
compactedCommands[i].DestinationOffset.Y,
332346
in roSpan[i],
333347
bcSpan[i],
334-
bsoSpan.Slice(offsetSpan[i], bcSpan[i]));
348+
bsoSpan.Slice(offsetSpan[i], bcSpan[i]),
349+
bacSpan.Slice(itemSegmentStarts[i], mpSpan[i].TotalSegmentCount));
335350
}
336351

337352
return localTotal;
@@ -365,18 +380,19 @@ public static FlushScene Create(
365380

366381
IMemoryOwner<int> bandSegmentIndicesOwner = allocator.Allocate<int>(Math.Max(runningSegmentOffset, 1));
367382

368-
// Phase 5: materialize the dense per-band segment index lists using the offsets computed above.
383+
// Phase 5: scatter segment indices into the dense per-band lists using offsets from the prefix sum.
384+
// Reads the compact band assignment cache built in Phase 4 — no path geometry re-enumeration.
369385
_ = Parallel.ForEach(
370386
Partitioner.Create(0, visibleItemCount),
371387
() => Array.Empty<int>(),
372388
(range, _, bandCursorBuffer) =>
373389
{
374390
Span<MaterializedPath> mpSpan = pathBuffer.AsSpan();
375-
Span<RasterizerOptions> roSpan = rasterizerOptionsBuffer.AsSpan();
376391
Span<int> bcSpan = itemBandCounts.Span;
377392
Span<int> offsetSpan = itemBandOffsetStartsOwner.Memory.Span;
378393
Span<int> bsoSpan = bandSegmentOffsetsOwner.Memory.Span;
379394
Span<int> bsiSpan = bandSegmentIndicesOwner.Memory.Span;
395+
Span<int> bacSpan = bandAssignmentCache.Span;
380396

381397
for (int i = range.Item1; i < range.Item2; i++)
382398
{
@@ -395,11 +411,8 @@ public static FlushScene Create(
395411
Span<int> bandCursors = bandCursorBuffer.AsSpan(0, bandCount);
396412
bandOffsets[..bandCount].CopyTo(bandCursors);
397413

398-
FillBandSegmentRefs(
399-
mpSpan[i],
400-
compactedCommands[i].DestinationOffset.Y,
401-
in roSpan[i],
402-
bandCount,
414+
FillBandSegmentRefsFromCache(
415+
bacSpan.Slice(itemSegmentStarts[i], mpSpan[i].TotalSegmentCount),
403416
bandCursors,
404417
bsiSpan);
405418
}
@@ -408,6 +421,8 @@ public static FlushScene Create(
408421
},
409422
static _ => { });
410423

424+
bandAssignmentCacheOwner.Dispose();
425+
411426
int rowCount = (maxBandIndex - minBandIndex) + 1;
412427
int[] rowCounts = new int[rowCount];
413428
int totalRefs = 0;
@@ -719,16 +734,22 @@ private static FlushScene Empty()
719734
0);
720735

721736
/// <summary>
722-
/// Counts how many references each band needs for one materialized path.
737+
/// Counts how many references each band needs for one materialized path and records the packed
738+
/// band assignment <c>(firstBand | lastBand &lt;&lt; 16)</c> for each segment in <paramref name="bandAssignments"/>,
739+
/// or <c>-1</c> for segments that fall outside the item's interest rectangle.
740+
/// The cache is consumed by <see cref="FillBandSegmentRefsFromCache"/> in Phase 5, eliminating a
741+
/// second enumeration of path geometry.
723742
/// </summary>
724-
private static int CountBandSegmentRefs(
743+
private static int CountAndStoreBandSegmentRefs(
725744
MaterializedPath path,
726745
int translateY,
727746
in RasterizerOptions options,
728747
int bandCount,
729-
Span<int> bandCounts)
748+
Span<int> bandCounts,
749+
Span<int> bandAssignments)
730750
{
731751
bandCounts.Clear();
752+
bandAssignments.Fill(-1);
732753

733754
if (bandCount <= 0 || path.TotalSegmentCount == 0)
734755
{
@@ -742,10 +763,11 @@ private static int CountBandSegmentRefs(
742763
int bandTopStart = (firstSceneBandIndex * rowHeight) - interest.Top;
743764
int totalCount = 0;
744765

766+
int segmentIndex = 0;
745767
MaterializedPath.SegmentEnumerator enumerator = path.GetSegmentEnumerator();
746768
while (enumerator.MoveNext())
747769
{
748-
if (!TryGetLocalBandSpan(
770+
if (TryGetLocalBandSpan(
749771
enumerator.CurrentMinY,
750772
enumerator.CurrentMaxY,
751773
translateY,
@@ -758,68 +780,43 @@ private static int CountBandSegmentRefs(
758780
out int firstBandIndex,
759781
out int lastBandIndex))
760782
{
761-
continue;
783+
bandAssignments[segmentIndex] = firstBandIndex | (lastBandIndex << 16);
784+
for (int bandIndex = firstBandIndex; bandIndex <= lastBandIndex; bandIndex++)
785+
{
786+
bandCounts[bandIndex]++;
787+
totalCount++;
788+
}
762789
}
763790

764-
for (int bandIndex = firstBandIndex; bandIndex <= lastBandIndex; bandIndex++)
765-
{
766-
bandCounts[bandIndex]++;
767-
totalCount++;
768-
}
791+
segmentIndex++;
769792
}
770793

771794
return totalCount;
772795
}
773796

774797
/// <summary>
775-
/// Fills the dense per-band segment reference table for one materialized path.
798+
/// Scatters segment indices into the dense per-band reference table by reading the compact
799+
/// band assignment cache built during Phase 4. No path geometry is accessed.
776800
/// </summary>
777-
private static void FillBandSegmentRefs(
778-
MaterializedPath path,
779-
int translateY,
780-
in RasterizerOptions options,
781-
int bandCount,
801+
private static void FillBandSegmentRefsFromCache(
802+
ReadOnlySpan<int> bandAssignments,
782803
Span<int> bandCursors,
783804
Span<int> bandSegmentIndices)
784805
{
785-
if (bandCount <= 0 || path.TotalSegmentCount == 0)
786-
{
787-
return;
788-
}
789-
790-
Rectangle interest = options.Interest;
791-
float samplingOffsetY = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F;
792-
int rowHeight = DefaultRasterizer.PreferredRowHeight;
793-
int firstSceneBandIndex = FloorDiv(interest.Top, rowHeight);
794-
int bandTopStart = (firstSceneBandIndex * rowHeight) - interest.Top;
795-
796-
int segmentIndex = 0;
797-
MaterializedPath.SegmentEnumerator enumerator = path.GetSegmentEnumerator();
798-
while (enumerator.MoveNext())
806+
for (int segmentIndex = 0; segmentIndex < bandAssignments.Length; segmentIndex++)
799807
{
800-
if (!TryGetLocalBandSpan(
801-
enumerator.CurrentMinY,
802-
enumerator.CurrentMaxY,
803-
translateY,
804-
interest.Top,
805-
interest.Height,
806-
samplingOffsetY,
807-
bandTopStart,
808-
rowHeight,
809-
bandCount,
810-
out int firstBandIndex,
811-
out int lastBandIndex))
808+
int packed = bandAssignments[segmentIndex];
809+
if (packed < 0)
812810
{
813-
segmentIndex++;
814811
continue;
815812
}
816813

814+
int firstBandIndex = packed & 0xFFFF;
815+
int lastBandIndex = (packed >> 16) & 0xFFFF;
817816
for (int bandIndex = firstBandIndex; bandIndex <= lastBandIndex; bandIndex++)
818817
{
819818
bandSegmentIndices[bandCursors[bandIndex]++] = segmentIndex;
820819
}
821-
822-
segmentIndex++;
823820
}
824821
}
825822

0 commit comments

Comments
 (0)