Skip to content

Commit 86c1fbe

Browse files
Add retained drawing scenes and refactor WebGPU
1 parent b6bc3f8 commit 86c1fbe

37 files changed

Lines changed: 2222 additions & 816 deletions

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 265 additions & 63 deletions
Large diffs are not rendered by default.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using Silk.NET.WebGPU;
5+
6+
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
7+
8+
/// <summary>
9+
/// Retained scene created by the WebGPU drawing backend.
10+
/// </summary>
11+
public sealed class WebGPUDrawingBackendScene : DrawingBackendScene
12+
{
13+
// These arenas contain mutable GPU scratch/resource buffers. They are cached on the
14+
// scene between renders, but every render must rent them into locals before use.
15+
// The Interlocked.Exchange rent/return methods below make concurrent renders of the
16+
// same retained scene allocate or use distinct arenas instead of sharing scratch state.
17+
private WebGPUSceneResourceArena? resourceArena;
18+
private WebGPUSceneSchedulingArena? schedulingArena;
19+
20+
// Volatile works on int, so the uint scratch capacities are stored bit-for-bit in
21+
// signed fields. The values are compared and restored as uint in the accessors.
22+
// Each counter is monotonic: concurrent renders may race to report usage, but the
23+
// retained capacity never shrinks below the largest value seen for that counter.
24+
private int bumpLines;
25+
private int bumpBinning;
26+
private int bumpPathRows;
27+
private int bumpPathTiles;
28+
private int bumpSegCounts;
29+
private int bumpSegments;
30+
private int bumpBlendSpill;
31+
private int bumpPtcl;
32+
33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="WebGPUDrawingBackendScene"/> class.
35+
/// </summary>
36+
/// <param name="encodedScene">The retained encoded scene.</param>
37+
/// <param name="textureFormat">The WebGPU texture format required by the scene.</param>
38+
/// <param name="requiredFeature">The optional WebGPU feature required by the texture format.</param>
39+
/// <param name="bounds">The target bounds used to encode the scene.</param>
40+
/// <param name="bumpSizes">The initial scratch capacities for the scene.</param>
41+
/// <param name="ownedResources">Resources that must stay alive for the retained scene.</param>
42+
internal WebGPUDrawingBackendScene(
43+
WebGPUEncodedScene encodedScene,
44+
TextureFormat textureFormat,
45+
FeatureName requiredFeature,
46+
Rectangle bounds,
47+
WebGPUSceneBumpSizes bumpSizes,
48+
IReadOnlyList<IDisposable>? ownedResources)
49+
: base(bounds, ownedResources)
50+
{
51+
this.EncodedScene = encodedScene;
52+
this.TextureFormat = textureFormat;
53+
this.RequiredFeature = requiredFeature;
54+
this.UpdateBumpSizes(bumpSizes);
55+
}
56+
57+
/// <summary>
58+
/// Gets the retained encoded scene when this is a leaf scene.
59+
/// </summary>
60+
internal WebGPUEncodedScene? EncodedScene { get; }
61+
62+
/// <summary>
63+
/// Gets the WebGPU texture format required by the scene.
64+
/// </summary>
65+
internal TextureFormat TextureFormat { get; }
66+
67+
/// <summary>
68+
/// Gets the optional WebGPU feature required by the texture format.
69+
/// </summary>
70+
internal FeatureName RequiredFeature { get; }
71+
72+
/// <summary>
73+
/// Gets the scratch capacities for the scene.
74+
/// </summary>
75+
internal WebGPUSceneBumpSizes BumpSizes
76+
=> new(
77+
unchecked((uint)Volatile.Read(ref this.bumpLines)),
78+
unchecked((uint)Volatile.Read(ref this.bumpBinning)),
79+
unchecked((uint)Volatile.Read(ref this.bumpPathRows)),
80+
unchecked((uint)Volatile.Read(ref this.bumpPathTiles)),
81+
unchecked((uint)Volatile.Read(ref this.bumpSegCounts)),
82+
unchecked((uint)Volatile.Read(ref this.bumpSegments)),
83+
unchecked((uint)Volatile.Read(ref this.bumpBlendSpill)),
84+
unchecked((uint)Volatile.Read(ref this.bumpPtcl)));
85+
86+
/// <summary>
87+
/// Gets or sets the backend that should receive this scene's arenas when the scene is disposed.
88+
/// </summary>
89+
internal WebGPUDrawingBackend? ArenaOwner { get; set; }
90+
91+
/// <summary>
92+
/// Updates the scratch capacities retained by this scene.
93+
/// </summary>
94+
internal void UpdateBumpSizes(WebGPUSceneBumpSizes bumpSizes)
95+
{
96+
UpdateBumpSize(ref this.bumpLines, bumpSizes.Lines);
97+
UpdateBumpSize(ref this.bumpBinning, bumpSizes.Binning);
98+
UpdateBumpSize(ref this.bumpPathRows, bumpSizes.PathRows);
99+
UpdateBumpSize(ref this.bumpPathTiles, bumpSizes.PathTiles);
100+
UpdateBumpSize(ref this.bumpSegCounts, bumpSizes.SegCounts);
101+
UpdateBumpSize(ref this.bumpSegments, bumpSizes.Segments);
102+
UpdateBumpSize(ref this.bumpBlendSpill, bumpSizes.BlendSpill);
103+
UpdateBumpSize(ref this.bumpPtcl, bumpSizes.Ptcl);
104+
}
105+
106+
/// <summary>
107+
/// Rents reusable scene resource buffers for one render.
108+
/// </summary>
109+
internal WebGPUSceneResourceArena? RentResourceArena()
110+
=> Interlocked.Exchange(ref this.resourceArena, null);
111+
112+
/// <summary>
113+
/// Rents reusable scheduling scratch buffers for one render.
114+
/// </summary>
115+
internal WebGPUSceneSchedulingArena? RentSchedulingArena()
116+
=> Interlocked.Exchange(ref this.schedulingArena, null);
117+
118+
/// <summary>
119+
/// Returns reusable arenas after one render.
120+
/// </summary>
121+
internal void ReturnArenas(
122+
WebGPUSceneResourceArena? resourceArena,
123+
WebGPUSceneSchedulingArena? schedulingArena,
124+
WebGPUDrawingBackend arenaOwner)
125+
{
126+
if (resourceArena is null && schedulingArena is null)
127+
{
128+
return;
129+
}
130+
131+
WebGPUSceneResourceArena? displacedResourceArena = null;
132+
WebGPUSceneSchedulingArena? displacedSchedulingArena = null;
133+
this.ArenaOwner = arenaOwner;
134+
135+
// Return is also an atomic exchange. If two renders complete concurrently, both
136+
// arenas are valid reuse candidates but only one can remain scene-local; the
137+
// other is handed to the backend cache so it can still serve eager flushes.
138+
if (resourceArena is not null)
139+
{
140+
displacedResourceArena = Interlocked.Exchange(ref this.resourceArena, resourceArena);
141+
}
142+
143+
if (schedulingArena is not null)
144+
{
145+
displacedSchedulingArena = Interlocked.Exchange(ref this.schedulingArena, schedulingArena);
146+
}
147+
148+
if (displacedResourceArena is not null || displacedSchedulingArena is not null)
149+
{
150+
arenaOwner.ReturnArenas(displacedResourceArena, displacedSchedulingArena);
151+
}
152+
}
153+
154+
/// <summary>
155+
/// Updates one retained scratch-capacity counter without allowing a concurrent render to shrink it.
156+
/// </summary>
157+
private static void UpdateBumpSize(ref int target, uint value)
158+
{
159+
while (true)
160+
{
161+
int current = Volatile.Read(ref target);
162+
uint currentValue = unchecked((uint)current);
163+
164+
// Reported scratch usage only ever increases the reusable capacity. Keeping
165+
// the max avoids a later render repeating an already-discovered grow pass.
166+
if (value <= currentValue)
167+
{
168+
return;
169+
}
170+
171+
int replacement = unchecked((int)value);
172+
173+
// CompareExchange retries only when another render updated this counter
174+
// between the read and write; the next loop observes that new maximum.
175+
if (Interlocked.CompareExchange(ref target, replacement, current) == current)
176+
{
177+
return;
178+
}
179+
}
180+
}
181+
182+
/// <inheritdoc />
183+
protected override void DisposeCore()
184+
{
185+
if (this.EncodedScene is not null &&
186+
!ReferenceEquals(this.EncodedScene, WebGPUEncodedScene.Empty))
187+
{
188+
this.EncodedScene.Dispose();
189+
}
190+
191+
// Disposal uses the same rent path as rendering so it cannot release an arena
192+
// currently rented by another render. A scene should still not be disposed while
193+
// user code intends to keep rendering it, but this prevents cached arena slots
194+
// from being shared or double-released during ordinary teardown races.
195+
WebGPUDrawingBackend? arenaOwner = this.ArenaOwner;
196+
WebGPUSceneResourceArena? resourceArena = this.RentResourceArena();
197+
WebGPUSceneSchedulingArena? schedulingArena = this.RentSchedulingArena();
198+
199+
if (arenaOwner is not null)
200+
{
201+
arenaOwner.ReturnArenas(resourceArena, schedulingArena);
202+
}
203+
else
204+
{
205+
WebGPUSceneSchedulingArena.Dispose(schedulingArena);
206+
WebGPUSceneResourceArena.Dispose(resourceArena);
207+
}
208+
209+
this.ArenaOwner = null;
210+
}
211+
}

0 commit comments

Comments
 (0)