Skip to content

Commit ef8c9c8

Browse files
Merge pull request #420 from SixLabors/js/drawing-cache
Make DrawingTextCache thread-safe
2 parents 9b612ed + 70d94cd commit ef8c9c8

6 files changed

Lines changed: 626 additions & 163 deletions

File tree

‎src/ImageSharp.Drawing/ImageSharp.Drawing.csproj‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@
4444
<InternalsVisibleTo Include="SixLabors.ImageSharp.Drawing.WebGPU" Key="$(SixLaborsPublicKey)" />
4545
</ItemGroup>
4646
<ItemGroup>
47-
<PackageReference Include="SixLabors.Fonts" Version="3.1.0" />
48-
<PackageReference Include="SixLabors.ImageSharp" Version="4.1.0" />
47+
<PackageReference Include="SixLabors.Fonts" Version="3.1.2" />
48+
<PackageReference Include="SixLabors.ImageSharp" Version="4.1.2" />
4949
<PackageReference Include="SixLabors.PolygonClipper" Version="1.0.1" />
5050
</ItemGroup>
5151

‎src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs‎

Lines changed: 23 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -72,26 +72,6 @@ public sealed class DrawingCanvas<TPixel> : DrawingCanvas
7272
/// </summary>
7373
private readonly DrawingTextCache textCache;
7474

75-
/// <summary>
76-
/// Reusable operation list handed to each text renderer. Hosted by the text cache because
77-
/// canvases are per-frame objects; sharing the cache-owned list keeps its capacity across
78-
/// frames instead of regrowing a fresh list of large operation structs per draw.
79-
/// </summary>
80-
private readonly List<DrawingOperation> textOperations;
81-
82-
/// <summary>
83-
/// Reusable sort buffer for <see cref="DrawTextOperations"/>, hosted by the text cache for
84-
/// the same reason as <see cref="textOperations"/>. Only pass and index pairs are sorted;
85-
/// the operations themselves stay in place so the per-draw sort moves eight bytes per
86-
/// entry instead of the full operation struct.
87-
/// </summary>
88-
private readonly List<(byte RenderPass, int Sequence)> textOperationSortBuffer;
89-
90-
/// <summary>
91-
/// Reusable stack pairing the begin and end commands for nested text composite layers.
92-
/// </summary>
93-
private readonly List<DrawingCanvasLayer> textCompositeLayerStack;
94-
9575
/// <summary>
9676
/// Initializes a new instance of the <see cref="DrawingCanvas{TPixel}"/> class.
9777
/// </summary>
@@ -303,9 +283,6 @@ private DrawingCanvas(
303283
this.targetFrame = targetFrame;
304284
this.batcher = batcher;
305285
this.textCache = textCache;
306-
this.textOperations = textCache.OperationScratch;
307-
this.textOperationSortBuffer = textCache.OperationSortScratch;
308-
this.textCompositeLayerStack = textCache.CompositeLayerScratch;
309286
this.ownsBatcher = ownsBatcher;
310287
this.ownsTextCache = ownsTextCache;
311288
this.pendingImageResources = pendingImageResources;
@@ -869,11 +846,11 @@ configuredOptions.VisibleBounds is null &&
869846
};
870847
}
871848

872-
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, configuredPath, pen, brush, this.textCache, this.textOperations);
849+
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, configuredPath, pen, brush, this.textCache);
873850
TextRenderer renderer = new(glyphRenderer);
874851
renderer.Render(text, configuredOptions);
875852

876-
this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions);
853+
this.DrawTextOperations(glyphRenderer.DrawingOperations, glyphRenderer.Scratch, effectiveOptions);
877854
}
878855

879856
/// <inheritdoc />
@@ -900,7 +877,7 @@ public override void DrawText(
900877
Matrix4x4.CreateTranslation(location.X, location.Y, 0) * effectiveOptions.Transform,
901878
effectiveOptions.TextContrast);
902879

903-
using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.textCache, this.textOperations);
880+
using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.textCache);
904881
if (TryGetVisibleTextBounds(state, placedOptions.Transform, out FontRectangle visibleBounds))
905882
{
906883
textBlock.RenderTo(glyphRenderer, wrappingLength, visibleBounds);
@@ -910,7 +887,7 @@ public override void DrawText(
910887
textBlock.RenderTo(glyphRenderer, wrappingLength);
911888
}
912889

913-
this.DrawTextOperations(glyphRenderer.DrawingOperations, placedOptions);
890+
this.DrawTextOperations(glyphRenderer.DrawingOperations, glyphRenderer.Scratch, placedOptions);
914891
}
915892

916893
/// <inheritdoc />
@@ -929,10 +906,10 @@ public override void DrawText(
929906
DrawingCanvasState state = this.ResolveState();
930907
DrawingOptions effectiveOptions = state.Options;
931908

932-
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.textCache, this.textOperations);
909+
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.textCache);
933910
textBlock.RenderTo(glyphRenderer, wrappingLength);
934911

935-
this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions);
912+
this.DrawTextOperations(glyphRenderer.DrawingOperations, glyphRenderer.Scratch, effectiveOptions);
936913
}
937914

938915
/// <inheritdoc />
@@ -958,10 +935,10 @@ public override void DrawText(
958935
Matrix4x4.CreateTranslation(location.X, location.Y, 0) * effectiveOptions.Transform,
959936
effectiveOptions.TextContrast);
960937

961-
using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.textCache, this.textOperations);
938+
using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.textCache);
962939
lineLayout.RenderTo(glyphRenderer);
963940

964-
this.DrawTextOperations(glyphRenderer.DrawingOperations, placedOptions);
941+
this.DrawTextOperations(glyphRenderer.DrawingOperations, glyphRenderer.Scratch, placedOptions);
965942
}
966943

967944
/// <inheritdoc />
@@ -979,10 +956,10 @@ public override void DrawText(
979956
DrawingCanvasState state = this.ResolveState();
980957
DrawingOptions effectiveOptions = state.Options;
981958

982-
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.textCache, this.textOperations);
959+
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.textCache);
983960
lineLayout.RenderTo(glyphRenderer);
984961

985-
this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions);
962+
this.DrawTextOperations(glyphRenderer.DrawingOperations, glyphRenderer.Scratch, effectiveOptions);
986963
}
987964

988965
/// <inheritdoc />
@@ -999,11 +976,11 @@ public override void DrawText(
999976
DrawingCanvasState state = this.ResolveState();
1000977
DrawingOptions effectiveOptions = state.Options;
1001978

1002-
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path: null, pen, brush, this.textCache, this.textOperations);
979+
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path: null, pen, brush, this.textCache);
1003980
TextRenderer renderer = new(glyphRenderer);
1004981
renderer.Render(glyphId, options);
1005982

1006-
this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions);
983+
this.DrawTextOperations(glyphRenderer.DrawingOperations, glyphRenderer.Scratch, effectiveOptions);
1007984
}
1008985

1009986
/// <inheritdoc />
@@ -1023,11 +1000,11 @@ public override void DrawText(ReadOnlySpan<ushort> glyphIds, ReadOnlySpan<Vector
10231000
DrawingCanvasState state = this.ResolveState();
10241001
DrawingOptions effectiveOptions = state.Options;
10251002

1026-
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path: null, pen, brush, this.textCache, this.textOperations);
1003+
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path: null, pen, brush, this.textCache);
10271004
TextRenderer renderer = new(glyphRenderer);
10281005
renderer.Render(glyphIds, points, options);
10291006

1030-
this.DrawTextOperations(this.BatchGlyphRunOperations(glyphRenderer.DrawingOperations), effectiveOptions);
1007+
this.DrawTextOperations(this.BatchGlyphRunOperations(glyphRenderer.DrawingOperations), glyphRenderer.Scratch, effectiveOptions);
10311008
}
10321009

10331010
/// <inheritdoc />
@@ -1801,17 +1778,20 @@ private static IPath GetPositionedGlyphPath(DrawingTextCache.RunPathCacheEntry e
18011778
/// Converts rendered text operations to composition commands and submits them to the batcher.
18021779
/// </summary>
18031780
/// <param name="operations">Text drawing operations produced by glyph layout/rendering.</param>
1781+
/// <param name="scratch">The working buffers leased by this draw's renderer.</param>
18041782
/// <param name="drawingOptions">Drawing options applied to each operation.</param>
1805-
private void DrawTextOperations(List<DrawingOperation> operations, DrawingOptions drawingOptions)
1783+
private void DrawTextOperations(
1784+
List<DrawingOperation> operations,
1785+
DrawingTextCache.DrawingScratch scratch,
1786+
DrawingOptions drawingOptions)
18061787
{
18071788
// Enforce render-pass ordering while preserving original emission order inside each
18081789
// pass. This preserves overlapping color-font layer compositing semantics (for
18091790
// example emoji mouth/teeth layers) and keeps the composite group markers paired
18101791
// with the fill operations they contain.
1811-
// The cache-owned buffer keeps its capacity across draws; draw calls never overlap on
1812-
// one canvas.
1813-
List<(byte RenderPass, int Sequence)> entries = this.textOperationSortBuffer;
1814-
entries.Clear();
1792+
// The renderer leases these buffers until command submission completes. Other canvases
1793+
// sharing the cache rent different buffers, while sequential draws retain capacity.
1794+
List<(byte RenderPass, int Sequence)> entries = scratch.SortBuffer;
18151795

18161796
// Queued glyph commands never carry the canvas transform: glyph geometry arrives with
18171797
// it already applied, and the sub-pixel remainder rides the command itself. One shared
@@ -1832,8 +1812,7 @@ private void DrawTextOperations(List<DrawingOperation> operations, DrawingOption
18321812
return cmp != 0 ? cmp : a.Sequence.CompareTo(b.Sequence);
18331813
});
18341814

1835-
List<DrawingCanvasLayer> compositeLayers = this.textCompositeLayerStack;
1836-
compositeLayers.Clear();
1815+
List<DrawingCanvasLayer> compositeLayers = scratch.CompositeLayers;
18371816
DrawingCanvasState state = this.ResolveState();
18381817

18391818
for (int i = 0; i < entries.Count; i++)
@@ -1879,11 +1858,6 @@ private void DrawTextOperations(List<DrawingOperation> operations, DrawingOption
18791858
this.batcher.AddStrokePath(((StrokePathCompositionSceneCommand)command).Command);
18801859
}
18811860
}
1882-
1883-
// The buffers outlive the canvas (the text cache hosts them), so drop the layer
1884-
// references now rather than rooting the final draw's state until the next draw.
1885-
entries.Clear();
1886-
compositeLayers.Clear();
18871861
}
18881862

18891863
/// <summary>

0 commit comments

Comments
 (0)