Skip to content

Commit ba57846

Browse files
Remove some copying.
1 parent fca77ba commit ba57846

8 files changed

Lines changed: 62 additions & 52 deletions

File tree

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Diagnostics;
5-
using System.Diagnostics.CodeAnalysis;
65
using System.Runtime.CompilerServices;
6+
using System.Runtime.InteropServices;
77
using Silk.NET.WebGPU;
88
using Silk.NET.WebGPU.Extensions.WGPU;
9+
using SixLabors.ImageSharp.Memory;
910
using SixLabors.ImageSharp.PixelFormats;
1011
using WgpuBuffer = Silk.NET.WebGPU.Buffer;
1112

@@ -23,14 +24,13 @@ public bool TryReadRegion<TPixel>(
2324
Configuration configuration,
2425
ICanvasFrame<TPixel> target,
2526
Rectangle sourceRectangle,
26-
[NotNullWhen(true)] out Image<TPixel>? image)
27+
Buffer2D<TPixel> destination)
2728
where TPixel : unmanaged, IPixel<TPixel>
2829
{
2930
this.ThrowIfDisposed();
3031
Guard.NotNull(configuration, nameof(configuration));
3132
Guard.NotNull(target, nameof(target));
32-
33-
image = null;
33+
Guard.NotNull(destination, nameof(destination));
3434

3535
// Readback is only available for native WebGPU targets with valid interop handles.
3636
if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) ||
@@ -78,7 +78,6 @@ public bool TryReadRegion<TPixel>(
7878

7979
// WebGPU copy-to-buffer requires bytes-per-row alignment to 256 bytes.
8080
int readbackRowBytes = Align(packedRowBytes, 256);
81-
int packedByteCount = checked(packedRowBytes * source.Height);
8281
ulong readbackByteCount = checked((ulong)readbackRowBytes * (ulong)source.Height);
8382

8483
WgpuBuffer* readbackBuffer = null;
@@ -169,18 +168,17 @@ void Callback(BufferMapAsyncStatus status, void* userData)
169168
try
170169
{
171170
ReadOnlySpan<byte> readback = new(mapped, checked((int)readbackByteCount));
172-
byte[] packed = new byte[packedByteCount];
173-
Span<byte> packedSpan = packed;
174171

175-
// Strip WebGPU row padding so Image.LoadPixelData receives tightly packed rows.
176-
for (int y = 0; y < source.Height; y++)
172+
// Copy directly from the mapped GPU buffer into the caller's buffer,
173+
// stripping WebGPU row padding in the process. Single copy, no intermediate array.
174+
int copyHeight = Math.Min(source.Height, destination.Height);
175+
for (int y = 0; y < copyHeight; y++)
177176
{
178177
readback
179178
.Slice(y * readbackRowBytes, packedRowBytes)
180-
.CopyTo(packedSpan.Slice(y * packedRowBytes, packedRowBytes));
179+
.CopyTo(MemoryMarshal.AsBytes(destination.DangerousGetRowSpan(y)));
181180
}
182181

183-
image = Image.LoadPixelData<TPixel>(configuration, packed, source.Width, source.Height);
184182
return true;
185183
}
186184
finally

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -675,24 +675,25 @@ private void ComposeLayerFallback<TPixel>(
675675
_ = destination.TryGetNativeSurface(out NativeSurface? destSurface);
676676
_ = destSurface!.TryGetCapability(out WebGPUSurfaceCapability? destCapability);
677677

678-
// Read destination and source from the GPU into CPU images.
679-
if (!this.TryReadRegion(configuration, destination, destination.Bounds, out Image<TPixel>? destImage))
678+
MemoryAllocator allocator = configuration.MemoryAllocator;
679+
680+
// Read destination and source from the GPU into CPU buffers.
681+
using Buffer2D<TPixel> destBuffer = allocator.Allocate2D<TPixel>(destination.Bounds.Width, destination.Bounds.Height);
682+
if (!this.TryReadRegion(configuration, destination, destination.Bounds, destBuffer))
680683
{
681684
return;
682685
}
683686

684-
if (!this.TryReadRegion(configuration, source, source.Bounds, out Image<TPixel>? srcImage))
687+
using Buffer2D<TPixel> srcBuffer = allocator.Allocate2D<TPixel>(source.Bounds.Width, source.Bounds.Height);
688+
if (!this.TryReadRegion(configuration, source, source.Bounds, srcBuffer))
685689
{
686-
destImage.Dispose();
687690
return;
688691
}
689692

690-
using (destImage)
691-
using (srcImage)
692693
{
693-
Buffer2DRegion<TPixel> destRegion = new(destImage.Frames.RootFrame.PixelBuffer);
694+
Buffer2DRegion<TPixel> destRegion = new(destBuffer);
694695
ICanvasFrame<TPixel> destFrame = new MemoryCanvasFrame<TPixel>(destRegion);
695-
ICanvasFrame<TPixel> srcFrame = new MemoryCanvasFrame<TPixel>(new Buffer2DRegion<TPixel>(srcImage.Frames.RootFrame.PixelBuffer));
696+
ICanvasFrame<TPixel> srcFrame = new MemoryCanvasFrame<TPixel>(new Buffer2DRegion<TPixel>(srcBuffer));
696697

697698
this.fallbackBackend.ComposeLayer(configuration, srcFrame, destFrame, destinationOffset, options);
698699

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Buffers;
5-
using System.Diagnostics.CodeAnalysis;
65
using SixLabors.ImageSharp.Memory;
76

87
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
@@ -159,15 +158,15 @@ public bool TryReadRegion<TPixel>(
159158
Configuration configuration,
160159
ICanvasFrame<TPixel> target,
161160
Rectangle sourceRectangle,
162-
[NotNullWhen(true)] out Image<TPixel>? image)
161+
Buffer2D<TPixel> destination)
163162
where TPixel : unmanaged, IPixel<TPixel>
164163
{
165164
Guard.NotNull(configuration, nameof(configuration));
165+
Guard.NotNull(destination, nameof(destination));
166166

167167
// CPU backend readback is available only when the target exposes CPU pixels.
168168
if (!target.TryGetCpuRegion(out Buffer2DRegion<TPixel> sourceRegion))
169169
{
170-
image = null;
171170
return false;
172171
}
173172

@@ -178,17 +177,15 @@ public bool TryReadRegion<TPixel>(
178177

179178
if (clipped.Width <= 0 || clipped.Height <= 0)
180179
{
181-
image = null;
182180
return false;
183181
}
184182

185-
// Build a tightly packed temporary image for downstream processing operations.
186-
image = new(configuration, clipped.Width, clipped.Height);
187-
Buffer2D<TPixel> destination = image.Frames.RootFrame.PixelBuffer;
188-
for (int y = 0; y < clipped.Height; y++)
183+
int copyWidth = Math.Min(clipped.Width, destination.Width);
184+
int copyHeight = Math.Min(clipped.Height, destination.Height);
185+
for (int y = 0; y < copyHeight; y++)
189186
{
190187
sourceRegion.DangerousGetRowSpan(clipped.Y + y)
191-
.Slice(clipped.X, clipped.Width)
188+
.Slice(clipped.X, copyWidth)
192189
.CopyTo(destination.DangerousGetRowSpan(y));
193190
}
194191

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Diagnostics.CodeAnalysis;
4+
using SixLabors.ImageSharp.Memory;
55

66
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
77

@@ -29,21 +29,22 @@ public void FlushCompositions<TPixel>(
2929
where TPixel : unmanaged, IPixel<TPixel>;
3030

3131
/// <summary>
32-
/// Attempts to read source pixels from the target into a temporary image.
32+
/// Attempts to read source pixels from the target into a caller-provided buffer.
3333
/// </summary>
34-
/// <typeparam name="TPixel">The destination pixel format.</typeparam>
34+
/// <typeparam name="TPixel">The pixel format.</typeparam>
3535
/// <param name="configuration">The active processing configuration.</param>
3636
/// <param name="target">The target frame.</param>
3737
/// <param name="sourceRectangle">Source rectangle in target-local coordinates.</param>
38-
/// <param name="image">
39-
/// When this method returns <see langword="true"/>, receives a newly allocated source image.
38+
/// <param name="destination">
39+
/// The caller-allocated buffer to receive the pixel data.
40+
/// Must be at least as large as <paramref name="sourceRectangle"/> (clamped to target bounds).
4041
/// </param>
4142
/// <returns><see langword="true"/> when readback succeeds; otherwise <see langword="false"/>.</returns>
4243
public bool TryReadRegion<TPixel>(
4344
Configuration configuration,
4445
ICanvasFrame<TPixel> target,
4546
Rectangle sourceRectangle,
46-
[NotNullWhen(true)] out Image<TPixel>? image)
47+
Buffer2D<TPixel> destination)
4748
where TPixel : unmanaged, IPixel<TPixel>;
4849

4950
/// <summary>

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ public int Save()
240240

241241
/// <inheritdoc />
242242
public int Save(DrawingOptions options, params IPath[] clipPaths)
243+
=> this.SaveCore(options, clipPaths);
244+
245+
private int SaveCore(DrawingOptions options, IReadOnlyList<IPath> clipPaths)
243246
{
244247
this.EnsureNotDisposed();
245248
Guard.NotNull(options, nameof(options));
@@ -1125,7 +1128,7 @@ private void DrawTextOperations(
11251128
private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList<IPath> clipPaths, Action action)
11261129
{
11271130
int saveCount = this.savedStates.Count;
1128-
_ = this.Save(options, [.. clipPaths]);
1131+
_ = this.SaveCore(options, clipPaths);
11291132
try
11301133
{
11311134
action();
@@ -1138,12 +1141,23 @@ private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList<IPa
11381141

11391142
/// <summary>
11401143
/// Attempts to create a source image for process-in-path operations.
1144+
/// The backend copies pixels directly into the image's pixel buffer — single copy.
11411145
/// </summary>
11421146
/// <param name="sourceRect">Source rectangle in local canvas coordinates.</param>
11431147
/// <param name="sourceImage">The readback image when available.</param>
11441148
/// <returns><see langword="true"/> when source pixels were resolved.</returns>
11451149
private bool TryCreateProcessSourceImage(Rectangle sourceRect, [NotNullWhen(true)] out Image<TPixel>? sourceImage)
1146-
=> this.backend.TryReadRegion(this.configuration, this.targetFrame, sourceRect, out sourceImage);
1150+
{
1151+
sourceImage = new Image<TPixel>(this.configuration, sourceRect.Width, sourceRect.Height);
1152+
if (!this.backend.TryReadRegion(this.configuration, this.targetFrame, sourceRect, sourceImage.Frames.RootFrame.PixelBuffer))
1153+
{
1154+
sourceImage.Dispose();
1155+
sourceImage = null;
1156+
return false;
1157+
}
1158+
1159+
return true;
1160+
}
11471161

11481162
/// <summary>
11491163
/// Flattens the path first (reusing any cached curve subdivision), then transforms

tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Diagnostics.CodeAnalysis;
54
using SixLabors.Fonts;
65
using SixLabors.ImageSharp.Drawing.Processing;
76
using SixLabors.ImageSharp.Drawing.Processing.Backends;
@@ -167,12 +166,9 @@ public bool TryReadRegion<TPixel>(
167166
Configuration configuration,
168167
ICanvasFrame<TPixel> target,
169168
Rectangle sourceRectangle,
170-
[NotNullWhen(true)] out Image<TPixel> image)
169+
Buffer2D<TPixel> destination)
171170
where TPixel : unmanaged, IPixel<TPixel>
172-
{
173-
image = null;
174-
return false;
175-
}
171+
=> false;
176172

177173
public void ComposeLayer<TPixel>(
178174
Configuration configuration,

tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,14 +198,13 @@ public bool TryReadRegion<TTargetPixel>(
198198
Configuration configuration,
199199
ICanvasFrame<TTargetPixel> target,
200200
Rectangle sourceRectangle,
201-
out Image<TTargetPixel>? image)
201+
Buffer2D<TTargetPixel> destination)
202202
where TTargetPixel : unmanaged, IPixel<TTargetPixel>
203203
{
204204
this.LastReadbackConfiguration = configuration;
205205

206206
if (this.readbackSource is null)
207207
{
208-
image = null;
209208
return false;
210209
}
211210

@@ -214,12 +213,19 @@ public bool TryReadRegion<TTargetPixel>(
214213
Rectangle clipped = Rectangle.Intersect(this.readbackSource.Bounds, sourceRectangle);
215214
if (clipped.Width <= 0 || clipped.Height <= 0)
216215
{
217-
image = null;
218216
return false;
219217
}
220218

221219
using Image<TPixel> cropped = this.readbackSource.Clone(ctx => ctx.Crop(clipped));
222-
image = cropped.CloneAs<TTargetPixel>();
220+
using Image<TTargetPixel> converted = cropped.CloneAs<TTargetPixel>();
221+
Buffer2D<TTargetPixel> source = converted.Frames.RootFrame.PixelBuffer;
222+
int copyWidth = Math.Min(source.Width, destination.Width);
223+
int copyHeight = Math.Min(source.Height, destination.Height);
224+
for (int y = 0; y < copyHeight; y++)
225+
{
226+
source.DangerousGetRowSpan(y).Slice(0, copyWidth).CopyTo(destination.DangerousGetRowSpan(y));
227+
}
228+
223229
return true;
224230
}
225231

tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Diagnostics.CodeAnalysis;
54
using SixLabors.ImageSharp.Drawing.Processing;
65
using SixLabors.ImageSharp.Drawing.Processing.Backends;
6+
using SixLabors.ImageSharp.Memory;
77
using SixLabors.ImageSharp.PixelFormats;
88

99
namespace SixLabors.ImageSharp.Drawing.Tests.Processing;
@@ -59,12 +59,9 @@ public bool TryReadRegion<TPixel>(
5959
Configuration configuration,
6060
ICanvasFrame<TPixel> target,
6161
Rectangle sourceRectangle,
62-
[NotNullWhen(true)] out Image<TPixel>? image)
62+
Buffer2D<TPixel> destination)
6363
where TPixel : unmanaged, IPixel<TPixel>
64-
{
65-
image = null;
66-
return false;
67-
}
64+
=> false;
6865

6966
public void ComposeLayer<TPixel>(
7067
Configuration configuration,

0 commit comments

Comments
 (0)