Skip to content

Commit 884c0f7

Browse files
Add ReadbackImage API and Buffer2DRegion usage
1 parent 7c66fb2 commit 884c0f7

18 files changed

Lines changed: 153 additions & 57 deletions

samples/DrawingBackendBenchmark/WebGpuBenchmarkBackend.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public BenchmarkRenderResult Render(ReadOnlySpan<VisualLine> lines, int width, i
6060
{
6161
try
6262
{
63-
preview = renderTarget.Readback<Bgra32>();
63+
preview = renderTarget.ReadbackImage<Bgra32>();
6464
}
6565
catch (Exception ex)
6666
{

src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ internal static void UploadTextureFromRegion<TPixel>(
550550
// Only use the direct path when the stride satisfies WebGPU's alignment requirement.
551551
if ((uint)sourceStrideBytes == alignedRowBytes && directByteCount <= packedByteCountEstimate * 2)
552552
{
553-
int startPixelIndex = checked((sourceRegion.Rectangle.Y * sourceRegion.Buffer.RowStride) + sourceRegion.Rectangle.X);
553+
int startPixelIndex = checked((sourceRegion.Bounds.Y * sourceRegion.Buffer.RowStride) + sourceRegion.Bounds.X);
554554
int startByteOffset = checked(startPixelIndex * pixelSizeInBytes);
555555
int uploadByteCount = checked((int)directByteCount);
556556
nuint uploadByteCountNuint = checked((nuint)uploadByteCount);

src/ImageSharp.Drawing.WebGPU/WebGPURenderTarget.cs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private WebGPURenderTarget(
119119

120120
/// <summary>
121121
/// Gets the native surface backing this render target.
122-
/// Most callers should use <see cref="CreateCanvas()"/> or <see cref="Readback{TPixel}"/> instead.
122+
/// Most callers should use <see cref="CreateCanvas()"/> or <see cref="ReadbackImage()"/> instead.
123123
/// </summary>
124124
internal NativeSurface Surface { get; }
125125

@@ -179,12 +179,27 @@ public DrawingCanvas CreateCanvas(DrawingOptions options)
179179
this.Format);
180180
}
181181

182+
/// <summary>
183+
/// Reads the current GPU texture contents back into a new CPU image.
184+
/// </summary>
185+
/// <returns>The readback image.</returns>
186+
public Image ReadbackImage()
187+
#pragma warning disable CS8524
188+
=> this.Format switch
189+
{
190+
WebGPUTextureFormat.Rgba8Unorm => this.ReadbackImage<Rgba32>(),
191+
WebGPUTextureFormat.Bgra8Unorm => this.ReadbackImage<Bgra32>(),
192+
WebGPUTextureFormat.Rgba8Snorm => this.ReadbackImage<NormalizedByte4>(),
193+
WebGPUTextureFormat.Rgba16Float => this.ReadbackImage<HalfVector4>()
194+
};
195+
#pragma warning restore CS8524
196+
182197
/// <summary>
183198
/// Reads the current GPU texture contents back into a new CPU image.
184199
/// </summary>
185200
/// <typeparam name="TPixel">The destination image pixel format.</typeparam>
186201
/// <returns>The readback image.</returns>
187-
public Image<TPixel> Readback<TPixel>()
202+
public Image<TPixel> ReadbackImage<TPixel>()
188203
where TPixel : unmanaged, IPixel<TPixel>
189204
{
190205
this.ThrowIfDisposed();
@@ -193,7 +208,8 @@ public Image<TPixel> Readback<TPixel>()
193208
Image<TPixel> image = new(this.Width, this.Height);
194209
try
195210
{
196-
this.ReadbackInto(image);
211+
this.ReadbackInto(image.Frames.RootFrame.PixelBuffer.GetRegion());
212+
197213
return image;
198214
}
199215
catch
@@ -204,33 +220,31 @@ public Image<TPixel> Readback<TPixel>()
204220
}
205221

206222
/// <summary>
207-
/// Reads the current GPU texture contents back into an existing CPU image.
223+
/// Reads the current GPU texture contents back into an existing CPU buffer region.
208224
/// </summary>
209225
/// <typeparam name="TPixel">The destination image pixel format.</typeparam>
210-
/// <param name="destination">The destination image that receives the readback pixels.</param>
211-
public void ReadbackInto<TPixel>(Image<TPixel> destination)
226+
/// <param name="destination">The destination buffer region that receives the readback pixels.</param>
227+
public void ReadbackInto<TPixel>(Buffer2DRegion<TPixel> destination)
212228
where TPixel : unmanaged, IPixel<TPixel>
213229
{
214-
Guard.NotNull(destination, nameof(destination));
215230
this.ThrowIfDisposed();
216231
this.deviceContext.ThrowIfDisposed();
217232

218-
if (destination.Width != this.Width || destination.Height != this.Height)
219-
{
220-
throw new ArgumentException(
221-
$"Destination image dimensions ({destination.Width}x{destination.Height}) must match render target dimensions ({this.Width}x{this.Height}).",
222-
nameof(destination));
223-
}
224-
225233
NativeCanvasFrame<TPixel> frame = WebGPUCanvasFactory.CreateFrame<TPixel>(this.Bounds, this.Surface);
226234

235+
// A smaller destination region intentionally reads the matching top-left
236+
// portion of the render target instead of forcing an intermediate full-size image.
237+
int readbackWidth = Math.Min(this.Width, destination.Width);
238+
int readbackHeight = Math.Min(this.Height, destination.Height);
239+
Rectangle sourceRectangle = new(0, 0, readbackWidth, readbackHeight);
240+
227241
// ReadRegion owns the pixel-format check because it is the point where
228242
// typed CPU pixels are copied from the native WebGPU texture.
229243
this.deviceContext.Backend.ReadRegion(
230244
this.deviceContext.Configuration,
231245
frame,
232-
new Rectangle(0, 0, this.Width, this.Height),
233-
new Buffer2DRegion<TPixel>(destination.Frames.RootFrame.PixelBuffer));
246+
sourceRectangle,
247+
destination);
234248
}
235249

236250
/// <summary>

src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
</ItemGroup>
4242
<ItemGroup>
4343
<PackageReference Include="SixLabors.Fonts" Version="3.0.0-alpha.0.43" />
44-
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0-alpha.0.102" />
44+
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0-alpha.0.103" />
4545
<PackageReference Include="SixLabors.PolygonClipper" Version="1.0.0-alpha.0.55" />
4646
</ItemGroup>
4747

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ internal ApplyBarrier(
114114
configuration,
115115
target,
116116
sourceRect,
117-
new Buffer2DRegion<TPixel>(sourceImage.Frames.RootFrame.PixelBuffer));
117+
sourceImage.Frames.RootFrame.PixelBuffer.GetRegion());
118118

119119
sourceImage.Mutate(this.Operation);
120120

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public BandTarget(Buffer2DRegion<TPixel> region, int absoluteLeft, int absoluteT
102102
public BandTarget(Buffer2D<TPixel> owner, Rectangle bounds, GraphicsOptions? graphicsOptions)
103103
{
104104
this.owner = owner;
105-
this.Region = new Buffer2DRegion<TPixel>(owner);
105+
this.Region = owner.GetRegion();
106106
this.AbsoluteLeft = bounds.X;
107107
this.AbsoluteTop = bounds.Y;
108108
this.GraphicsOptions = graphicsOptions;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ private static void ExecuteSceneRow<TPixel>(
135135
where TPixel : unmanaged, IPixel<TPixel>
136136
{
137137
int bandTop = row.RowBandIndex * DefaultRasterizer.DefaultTileHeight;
138-
int localBandTop = bandTop - destinationFrame.Rectangle.Y;
138+
int localBandTop = bandTop - destinationFrame.Bounds.Y;
139139
int bandHeight = Math.Min(DefaultRasterizer.DefaultTileHeight, destinationFrame.Height - localBandTop);
140140
if (bandHeight <= 0)
141141
{
@@ -145,7 +145,7 @@ private static void ExecuteSceneRow<TPixel>(
145145
Buffer2DRegion<TPixel> destinationBand = destinationFrame.GetSubRegion(0, localBandTop, destinationFrame.Width, bandHeight);
146146
BandTarget<TPixel>[] targetStack = state.TargetStack;
147147
int targetCount = 1;
148-
targetStack[0] = new BandTarget<TPixel>(destinationBand, destinationFrame.Rectangle.X, bandTop, null);
148+
targetStack[0] = new BandTarget<TPixel>(destinationBand, destinationFrame.Bounds.X, bandTop, null);
149149
int scratchWidth = GetRowScratchWidth(scene, row, destinationFrame.Width);
150150
DefaultRasterizer.WorkerScratch scratch = state.GetOrCreateScratch(scratchWidth);
151151

src/ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public MemoryCanvasFrame(Buffer2DRegion<TPixel> region)
2626
}
2727

2828
/// <inheritdoc />
29-
public Rectangle Bounds => this.region.Rectangle;
29+
public Rectangle Bounds => this.region.Bounds;
3030

3131
/// <inheritdoc />
3232
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region)

src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ Before looking at the flow, it helps to define the major terms in the sense used
5959

6060
It is not the rasterizer. It is the object that makes the public drawing model coherent.
6161

62-
Callers reach that model through `IImageProcessingContext.Paint(...)`.
62+
Callers usually reach that model through `IImageProcessingContext.Paint(...)`.
63+
64+
When callers already have an `ImageFrame` or `ImageFrame<TPixel>`, the public `CreateCanvas(...)` frame extensions create a canvas directly over that frame. The caller owns the returned canvas and must dispose it to replay recorded work into the frame.
6365

6466
`DrawingCanvas<TPixel>` is the typed implementation that carries the target pixel format for brush normalization,
6567
readback, and backend execution. Factory methods return `DrawingCanvas` so CPU and GPU entry points expose the same
@@ -106,7 +108,7 @@ There are two backend-selection paths in the architecture:
106108
- specialized infrastructure can construct a canvas with an explicit backend
107109

108110
The ordinary CPU entry point is `Paint(...)` on `IImageProcessingContext`, which routes into the typed
109-
implementation internally.
111+
implementation internally. Public `ImageFrame` canvas extensions provide the lower-level frame entry point for callers that want to own the canvas lifetime directly.
110112

111113
That explicit-backend path matters for the WebGPU helpers. `WebGPUWindow`, `WebGPUExternalSurface`, and `WebGPURenderTarget` create canvases that point directly at their owned `WebGPUDrawingBackend` instance instead of storing that backend on the caller's `Configuration`.
112114

src/ImageSharp.Drawing/Processing/DrawingCanvasFactoryExtensions.cs

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

4+
using SixLabors.ImageSharp.Advanced;
45
using SixLabors.ImageSharp.Memory;
56

67
namespace SixLabors.ImageSharp.Drawing.Processing;
78

89
/// <summary>
9-
/// Convenience extension methods for creating drawing canvas instances from ImageSharp image types.
10+
/// Extension methods for creating drawing canvas instances over ImageSharp image frames.
1011
/// </summary>
11-
internal static class DrawingCanvasFactoryExtensions
12+
public static class DrawingCanvasFactoryExtensions
1213
{
1314
/// <summary>
14-
/// Creates a drawing canvas over an existing frame.
15+
/// Creates a drawing canvas over an existing typed image frame.
1516
/// </summary>
17+
/// <remarks>
18+
/// The caller owns the returned canvas and must dispose it to replay recorded work into the frame.
19+
/// </remarks>
1620
/// <typeparam name="TPixel">The pixel format.</typeparam>
1721
/// <param name="frame">The frame backing the canvas.</param>
1822
/// <param name="configuration">The configuration to use for this canvas instance.</param>
1923
/// <param name="options">Initial drawing options for this canvas instance.</param>
2024
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
2125
/// <returns>A drawing canvas targeting <paramref name="frame"/>.</returns>
22-
internal static DrawingCanvas CreateCanvas<TPixel>(
26+
public static DrawingCanvas CreateCanvas<TPixel>(
2327
this ImageFrame<TPixel> frame,
2428
Configuration configuration,
2529
DrawingOptions options,
@@ -33,7 +37,52 @@ internal static DrawingCanvas CreateCanvas<TPixel>(
3337
return new DrawingCanvas<TPixel>(
3438
configuration,
3539
options,
36-
new Buffer2DRegion<TPixel>(frame.PixelBuffer),
40+
frame.PixelBuffer.GetRegion(),
3741
clipPaths);
3842
}
43+
44+
/// <summary>
45+
/// Creates a drawing canvas over an existing image frame.
46+
/// </summary>
47+
/// <remarks>
48+
/// The caller owns the returned canvas and must dispose it to replay recorded work into the frame.
49+
/// </remarks>
50+
/// <param name="frame">The frame backing the canvas.</param>
51+
/// <param name="configuration">The configuration to use for this canvas instance.</param>
52+
/// <param name="options">Initial drawing options for this canvas instance.</param>
53+
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
54+
/// <returns>A drawing canvas targeting <paramref name="frame"/>.</returns>
55+
public static DrawingCanvas CreateCanvas(
56+
this ImageFrame frame,
57+
Configuration configuration,
58+
DrawingOptions options,
59+
params IPath[] clipPaths)
60+
{
61+
Guard.NotNull(frame, nameof(frame));
62+
Guard.NotNull(options, nameof(options));
63+
Guard.NotNull(clipPaths, nameof(clipPaths));
64+
65+
CanvasFactoryVisitor visitor = new(configuration, options, clipPaths);
66+
frame.AcceptVisitor(visitor);
67+
return visitor.Value!;
68+
}
69+
70+
private struct CanvasFactoryVisitor : IImageFrameVisitor
71+
{
72+
private readonly Configuration configuration;
73+
private readonly DrawingOptions options;
74+
private readonly IPath[] clipPaths;
75+
76+
public CanvasFactoryVisitor(Configuration configuration, DrawingOptions options, IPath[] clipPaths)
77+
{
78+
this.configuration = configuration;
79+
this.options = options;
80+
this.clipPaths = clipPaths;
81+
}
82+
83+
public DrawingCanvas? Value { get; private set; }
84+
85+
void IImageFrameVisitor.Visit<TPixel>(ImageFrame<TPixel> frame)
86+
=> this.Value = frame.CreateCanvas(this.configuration, this.options, this.clipPaths);
87+
}
3988
}

0 commit comments

Comments
 (0)