Skip to content

Commit d9e128e

Browse files
Add inline layer support to compositing pipeline
1 parent fdb5ef8 commit d9e128e

30 files changed

+1292
-772
lines changed

samples/WebGPUWindowDemo/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ private static void OnRender(double deltaTime)
324324
fpsElapsed += deltaTime;
325325
if (fpsElapsed >= 1.0)
326326
{
327-
window.Title = $"ImageSharp.Drawing WebGPU Demo — {frameCount / fpsElapsed:F1} FPS | GPU: {backend.DiagnosticGpuCompositeCount} Fallback: {backend.DiagnosticFallbackCompositeCount}";
327+
string flushMode = backend.DiagnosticLastFlushUsedGPU ? "GPU" : "CPU";
328+
window.Title = $"ImageSharp.Drawing WebGPU Demo - {frameCount / fpsElapsed:F1} FPS | Flush: {flushMode} | Failure: {backend.DiagnosticLastSceneFailure}";
328329
frameCount = 0;
329330
fpsElapsed = 0;
330331
}

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 1 addition & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -251,120 +251,6 @@ public void FlushCompositions<TPixel>(
251251
this.FlushCompositionsFallback(configuration, target, compositionScene, compositionBounds: null);
252252
}
253253

254-
/// <inheritdoc />
255-
public ICanvasFrame<TPixel> CreateLayerFrame<TPixel>(
256-
Configuration configuration,
257-
ICanvasFrame<TPixel> parentTarget,
258-
int width,
259-
int height)
260-
where TPixel : unmanaged, IPixel<TPixel>
261-
{
262-
this.ThrowIfDisposed();
263-
264-
if (TryGetCompositeTextureFormat<TPixel>(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature)
265-
&& parentTarget.TryGetNativeSurface(out NativeSurface? parentSurface))
266-
{
267-
_ = parentSurface.TryGetCapability(out WebGPUSurfaceCapability? parentCapability);
268-
using WebGPURuntime.Lease lease = WebGPURuntime.Acquire();
269-
WebGPU api = lease.Api;
270-
Device* device = (Device*)parentCapability!.Device;
271-
272-
WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(api, device);
273-
if (requiredFeature == FeatureName.Undefined || deviceState.HasFeature(requiredFeature))
274-
{
275-
TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId);
276-
TextureDescriptor textureDescriptor = new()
277-
{
278-
Usage = TextureUsage.TextureBinding | TextureUsage.StorageBinding | TextureUsage.CopySrc | TextureUsage.CopyDst,
279-
Dimension = TextureDimension.Dimension2D,
280-
Size = new Extent3D((uint)width, (uint)height, 1),
281-
Format = textureFormat,
282-
MipLevelCount = 1,
283-
SampleCount = 1
284-
};
285-
286-
Texture* texture = api.DeviceCreateTexture(device, in textureDescriptor);
287-
if (texture is not null)
288-
{
289-
TextureViewDescriptor viewDescriptor = new()
290-
{
291-
Format = textureFormat,
292-
Dimension = TextureViewDimension.Dimension2D,
293-
BaseMipLevel = 0,
294-
MipLevelCount = 1,
295-
BaseArrayLayer = 0,
296-
ArrayLayerCount = 1,
297-
Aspect = TextureAspect.All
298-
};
299-
300-
TextureView* textureView = api.TextureCreateView(texture, in viewDescriptor);
301-
if (textureView is not null)
302-
{
303-
NativeSurface surface = WebGPUNativeSurfaceFactory.Create<TPixel>(
304-
parentCapability.Device,
305-
parentCapability.Queue,
306-
(nint)texture,
307-
(nint)textureView,
308-
formatId,
309-
width,
310-
height);
311-
312-
return new NativeCanvasFrame<TPixel>(new Rectangle(0, 0, width, height), surface);
313-
}
314-
315-
api.TextureRelease(texture);
316-
}
317-
}
318-
}
319-
320-
return this.fallbackBackend.CreateLayerFrame(configuration, parentTarget, width, height);
321-
}
322-
323-
/// <inheritdoc />
324-
public void ComposeLayer<TPixel>(
325-
Configuration configuration,
326-
ICanvasFrame<TPixel> source,
327-
ICanvasFrame<TPixel> destination,
328-
Point destinationOffset,
329-
GraphicsOptions options)
330-
where TPixel : unmanaged, IPixel<TPixel>
331-
{
332-
this.ThrowIfDisposed();
333-
334-
if (!destination.TryGetNativeSurface(out _))
335-
{
336-
this.fallbackBackend.ComposeLayer(configuration, source, destination, destinationOffset, options);
337-
return;
338-
}
339-
340-
if (this.TryComposeLayerGpu(configuration, source, destination, destinationOffset, options))
341-
{
342-
return;
343-
}
344-
345-
this.ComposeLayerFallback(configuration, source, destination, destinationOffset, options);
346-
}
347-
348-
/// <inheritdoc />
349-
public void ReleaseFrameResources<TPixel>(
350-
Configuration configuration,
351-
ICanvasFrame<TPixel> target)
352-
where TPixel : unmanaged, IPixel<TPixel>
353-
{
354-
if (target.TryGetNativeSurface(out NativeSurface? nativeSurface))
355-
{
356-
_ = nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability);
357-
using WebGPURuntime.Lease lease = WebGPURuntime.Acquire();
358-
WebGPU api = lease.Api;
359-
api.TextureViewRelease((TextureView*)capability!.TargetTextureView);
360-
api.TextureRelease((Texture*)capability.TargetTexture);
361-
}
362-
else
363-
{
364-
this.fallbackBackend.ReleaseFrameResources(configuration, target);
365-
}
366-
}
367-
368254
/// <summary>
369255
/// Executes the scene on the CPU fallback backend, then uploads the result
370256
/// to the native GPU surface.
@@ -450,7 +336,7 @@ private void ComposeLayerFallback<TPixel>(
450336
ICanvasFrame<TPixel> destFrame = new MemoryCanvasFrame<TPixel>(destRegion);
451337
ICanvasFrame<TPixel> srcFrame = new MemoryCanvasFrame<TPixel>(srcRegion);
452338

453-
this.fallbackBackend.ComposeLayer(configuration, srcFrame, destFrame, destinationOffset, options);
339+
DefaultDrawingBackend.ComposeLayer(configuration, srcFrame, destFrame, destinationOffset, options);
454340

455341
using WebGPURuntime.Lease lease = WebGPURuntime.Acquire();
456342
WebGPUFlushContext.UploadTextureFromRegion(

src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs

Lines changed: 67 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
#pragma warning disable SA1201 // Phase-1 scene-model types are grouped together in one file for now.
55

66
using System.Buffers;
7-
using System.Collections.Generic;
87
using System.Diagnostics;
98
using System.Numerics;
109
using System.Runtime.CompilerServices;
@@ -34,6 +33,7 @@ internal static class WebGPUSceneEncoder
3433
private const int StyleBlendAlphaShift = 14;
3534
private const uint StyleBlendAlphaMask = 0xFFFFU << StyleBlendAlphaShift;
3635
private const uint StyleFlagsFill = 0x40000000U;
36+
private static readonly GraphicsOptions DefaultClipGraphicsOptions = new();
3737

3838
/// <summary>
3939
/// Encodes prepared composition commands into flush-scoped scene buffers.
@@ -69,7 +69,7 @@ public static bool TryValidateBrushSupport(IReadOnlyList<CompositionCommand> com
6969
for (int i = 0; i < commands.Count; i++)
7070
{
7171
CompositionCommand command = commands[i];
72-
if (!command.IsVisible)
72+
if (command.Kind is not CompositionCommandKind.FillLayer || !command.IsVisible)
7373
{
7474
continue;
7575
}
@@ -91,8 +91,10 @@ private ref struct SupportedSubsetSceneEncoding
9191
private bool gradientPixelsDetached;
9292
private uint lastStyle0;
9393
private uint lastStyle1;
94+
private readonly Rectangle rootTargetBounds;
95+
private List<Rectangle>? openLayerBounds;
9496

95-
private SupportedSubsetSceneEncoding(MemoryAllocator allocator, int commandCount)
97+
private SupportedSubsetSceneEncoding(MemoryAllocator allocator, int commandCount, in Rectangle rootTargetBounds)
9698
{
9799
this.PathTags = new OwnedStream<byte>(allocator, Math.Max(commandCount * 8, 256));
98100
this.PathData = new OwnedStream<uint>(allocator, Math.Max(commandCount * 16, 256));
@@ -113,6 +115,8 @@ private SupportedSubsetSceneEncoding(MemoryAllocator allocator, int commandCount
113115
this.gradientPixelsDetached = false;
114116
this.lastStyle0 = 0;
115117
this.lastStyle1 = 0;
118+
this.rootTargetBounds = rootTargetBounds;
119+
this.openLayerBounds = null;
116120

117121
this.PathTags.Add(PathTagTransform);
118122
AppendIdentityTransform(ref this.Transforms);
@@ -155,8 +159,8 @@ public static SupportedSubsetSceneEncoding Create(
155159
in Rectangle targetBounds,
156160
MemoryAllocator allocator)
157161
{
158-
SupportedSubsetSceneEncoding encoding = new(allocator, commands.Count);
159-
encoding.Build(commands, targetBounds);
162+
SupportedSubsetSceneEncoding encoding = new(allocator, commands.Count, targetBounds);
163+
encoding.Build(commands);
160164
return encoding;
161165
}
162166

@@ -177,7 +181,7 @@ public void Dispose()
177181

178182
public void MarkGradientPixelsDetached() => this.gradientPixelsDetached = true;
179183

180-
private void Build(IReadOnlyList<CompositionCommand> commands, in Rectangle targetBounds)
184+
private void Build(IReadOnlyList<CompositionCommand> commands)
181185
{
182186
for (int i = 0; i < commands.Count; i++)
183187
{
@@ -187,15 +191,29 @@ private void Build(IReadOnlyList<CompositionCommand> commands, in Rectangle targ
187191

188192
private void Append(in CompositionCommand command)
189193
{
190-
if (!command.IsVisible)
194+
switch (command.Kind)
191195
{
192-
return;
196+
case CompositionCommandKind.FillLayer:
197+
if (!command.IsVisible)
198+
{
199+
return;
200+
}
201+
202+
IPath preparedPath = command.PreparedPath!;
203+
this.AppendPlainFill(command, preparedPath);
204+
return;
205+
206+
case CompositionCommandKind.BeginLayer:
207+
this.AppendBeginLayer(command);
208+
return;
209+
210+
case CompositionCommandKind.EndLayer:
211+
this.AppendEndLayer();
212+
return;
213+
214+
default:
215+
return;
193216
}
194-
195-
IPath preparedPath = command.PreparedPath
196-
?? throw new InvalidOperationException("Commands must be prepared before GPU scene encoding.");
197-
198-
this.AppendPlainFill(command, preparedPath);
199217
}
200218

201219
private void AppendPlainFill(in CompositionCommand command, IPath preparedPath)
@@ -216,6 +234,7 @@ private void AppendPlainFill(in CompositionCommand command, IPath preparedPath)
216234
int encodedPathCount = EncodePath(
217235
command,
218236
preparedPath,
237+
this.rootTargetBounds,
219238
ref this.PathTags,
220239
ref this.PathData,
221240
out int geometryLineCount,
@@ -246,13 +265,13 @@ private void AppendPlainFill(in CompositionCommand command, IPath preparedPath)
246265
ref gradientRowCount);
247266
this.GradientRowCount = gradientRowCount;
248267

249-
this.TotalTileMembershipCount += CountTileMembership(command.DestinationRegion);
268+
this.TotalTileMembershipCount += CountTileMembership(GetTargetLocalDestination(command, this.rootTargetBounds));
250269
}
251270

252-
private void AppendLayeredFill(in CompositionCommand command, IPath preparedPath)
271+
private void AppendBeginLayer(in CompositionCommand command)
253272
{
254-
Rectangle layerBounds = Rectangle.Inflate(command.DestinationRegion, 1, 1);
255-
(uint style0, uint style1) = GetFillStyle(command.GraphicsOptions, command.RasterizerOptions.IntersectionRule);
273+
Rectangle layerBounds = ToTargetLocal(command.LayerBounds, this.rootTargetBounds);
274+
(uint style0, uint style1) = GetFillStyle(DefaultClipGraphicsOptions, IntersectionRule.NonZero);
256275
int pathTagCheckpoint = this.PathTags.Count;
257276
int styleCheckpoint = this.Styles.Count;
258277

@@ -280,37 +299,20 @@ private void AppendLayeredFill(in CompositionCommand command, IPath preparedPath
280299
AppendBeginClipData(command.GraphicsOptions, ref this.DrawData);
281300
this.ClipCount++;
282301
this.TotalTileMembershipCount += CountTileMembership(layerBounds);
302+
this.openLayerBounds ??= new List<Rectangle>(4);
303+
this.openLayerBounds.Add(layerBounds);
304+
}
283305

284-
uint fillDrawTag = GetDrawTag(command);
285-
GpuSceneDrawMonoid fillDrawTagMonoid = GpuSceneDrawTag.Map(fillDrawTag);
286-
int encodedPathCount = EncodePath(
287-
command,
288-
preparedPath,
289-
ref this.PathTags,
290-
ref this.PathData,
291-
out int geometryLineCount,
292-
out _);
293-
294-
if (encodedPathCount == 0)
306+
private void AppendEndLayer()
307+
{
308+
if (this.openLayerBounds is not { Count: > 0 })
295309
{
296-
throw new InvalidOperationException("A visible layered command encoded an empty fill path.");
310+
return;
297311
}
298312

299-
this.FillCount++;
300-
this.PathCount += encodedPathCount;
301-
this.LineCount += geometryLineCount;
302-
this.InfoWordCount += (int)fillDrawTagMonoid.InfoOffset;
303-
this.DrawTags.Add(fillDrawTag);
304-
int gradientRowCount = this.GradientRowCount;
305-
AppendDrawData(
306-
command,
307-
fillDrawTag,
308-
ref this.DrawData,
309-
ref this.GradientPixels,
310-
this.Images,
311-
ref gradientRowCount);
312-
this.GradientRowCount = gradientRowCount;
313-
this.TotalTileMembershipCount += CountTileMembership(command.DestinationRegion);
313+
int lastIndex = this.openLayerBounds.Count - 1;
314+
Rectangle layerBounds = this.openLayerBounds[lastIndex];
315+
this.openLayerBounds.RemoveAt(lastIndex);
314316

315317
this.DrawTags.Add(GpuSceneDrawTag.EndClip);
316318
this.PathTags.Add(PathTagPath);
@@ -440,15 +442,18 @@ private static uint GetDrawTag(in CompositionCommand command)
440442
private static int EncodePath(
441443
in CompositionCommand command,
442444
IPath preparedPath,
445+
in Rectangle rootTargetBounds,
443446
ref OwnedStream<byte> pathTags,
444447
ref OwnedStream<uint> pathData,
445448
out int lineCount,
446449
out int pathSegmentCount)
447450
{
448451
Rectangle interest = command.RasterizerOptions.Interest;
449452
float samplingOffset = command.RasterizerOptions.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F;
450-
float translateX = command.DestinationRegion.X - command.SourceOffset.X;
451-
float translateY = command.DestinationRegion.Y - command.SourceOffset.Y;
453+
float targetOffsetX = command.TargetBounds.X - rootTargetBounds.X;
454+
float targetOffsetY = command.TargetBounds.Y - rootTargetBounds.Y;
455+
float translateX = targetOffsetX + command.DestinationRegion.X - command.SourceOffset.X;
456+
float translateY = targetOffsetY + command.DestinationRegion.Y - command.SourceOffset.Y;
452457

453458
// Move path points from interest-local coverage space into target-local space.
454459
float pointTranslateX = translateX + samplingOffset - interest.Left;
@@ -636,28 +641,6 @@ private static bool PointsEqual(float ax, float ay, float bx, float by)
636641
private static uint BitcastSingle(float value)
637642
=> unchecked((uint)BitConverter.SingleToInt32Bits(value));
638643

639-
private static int CountLineTileSlices(float x0, float y0, float x1, float y1)
640-
{
641-
if (x0 == x1 && y0 == y1)
642-
{
643-
return 0;
644-
}
645-
646-
bool isDown = y1 >= y0;
647-
float startX = isDown ? x0 : x1;
648-
float startY = isDown ? y0 : y1;
649-
float endX = isDown ? x1 : x0;
650-
float endY = isDown ? y1 : y0;
651-
float s0x = startX / TileWidth;
652-
float s0y = startY / TileHeight;
653-
float s1x = endX / TileWidth;
654-
float s1y = endY / TileHeight;
655-
656-
int spanX = Math.Max((int)MathF.Ceiling(MathF.Max(s0x, s1x)) - (int)MathF.Floor(MathF.Min(s0x, s1x)), 1);
657-
int spanY = Math.Max((int)MathF.Ceiling(MathF.Max(s0y, s1y)) - (int)MathF.Floor(MathF.Min(s0y, s1y)), 1);
658-
return checked((spanX - 1) + spanY);
659-
}
660-
661644
[MethodImpl(MethodImplOptions.AggressiveInlining)]
662645
private static int DivideRoundUp(int value, int divisor)
663646
=> (value + divisor - 1) / divisor;
@@ -676,6 +659,22 @@ private static int CountTileMembership(in Rectangle destinationRegion)
676659
return (tileMaxX - tileMinX) * (tileMaxY - tileMinY);
677660
}
678661

662+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
663+
private static Rectangle ToTargetLocal(in Rectangle absoluteBounds, in Rectangle rootTargetBounds)
664+
=> new(
665+
absoluteBounds.X - rootTargetBounds.X,
666+
absoluteBounds.Y - rootTargetBounds.Y,
667+
absoluteBounds.Width,
668+
absoluteBounds.Height);
669+
670+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
671+
private static Rectangle GetTargetLocalDestination(in CompositionCommand command, in Rectangle rootTargetBounds)
672+
=> new(
673+
(command.TargetBounds.X - rootTargetBounds.X) + command.DestinationRegion.X,
674+
(command.TargetBounds.Y - rootTargetBounds.Y) + command.DestinationRegion.Y,
675+
command.DestinationRegion.Width,
676+
command.DestinationRegion.Height);
677+
679678
[MethodImpl(MethodImplOptions.AggressiveInlining)]
680679
private static uint PackSolidColor(SolidBrush solidBrush)
681680
{

0 commit comments

Comments
 (0)