ImageSharp/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs
2026-08-03 22:31:27 +02:00

484 lines
22 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
/// <summary>
/// CPU backend that executes path coverage rasterization and brush composition directly against a CPU region.
/// </summary>
public sealed partial class DefaultDrawingBackend : IDrawingBackend
{
/// <summary>
/// Gets the default backend instance.
/// </summary>
public static DefaultDrawingBackend Instance { get; } = new();
/// <inheritdoc />
public DrawingBackendScene CreateScene(
Configuration configuration,
Rectangle targetBounds,
DrawingCommandBatch commandBatch,
IReadOnlyList<IDisposable>? ownedResources = null)
{
FlushScene scene = FlushScene.Create(
commandBatch,
targetBounds,
configuration.MemoryAllocator,
configuration.MaxDegreeOfParallelism);
return new DefaultDrawingBackendScene(scene, targetBounds, ownedResources);
}
/// <inheritdoc />
public void RenderScene<TPixel>(
Configuration configuration,
ICanvasFrame<TPixel> target,
DrawingBackendScene scene)
where TPixel : unmanaged, IPixel<TPixel>
{
if (scene is not DefaultDrawingBackendScene cpuScene)
{
throw new InvalidOperationException("The retained scene is not a CPU drawing backend scene.");
}
if (!target.TryGetCpuRegion(out Buffer2DRegion<TPixel> destinationFrame))
{
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets.");
}
if (target.Bounds != cpuScene.Bounds)
{
throw new InvalidOperationException("The target bounds do not match the retained CPU scene bounds.");
}
if (cpuScene.Scene is FlushScene flushScene && flushScene.RowCount != 0)
{
ExecuteScene(configuration, destinationFrame, flushScene);
}
}
/// <summary>
/// Executes one retained flush scene against a CPU destination frame.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The active processing configuration.</param>
/// <param name="destinationFrame">The destination CPU region.</param>
/// <param name="scene">The retained scene to execute.</param>
private static void ExecuteScene<TPixel>(
Configuration configuration,
Buffer2DRegion<TPixel> destinationFrame,
FlushScene scene)
where TPixel : unmanaged, IPixel<TPixel>
{
// Warm the cached renderers before the row loop so the hot execution path only
// performs retained-scene work and brush application.
if (scene.FillItemCount > 0)
{
for (int i = 0; i < scene.FillItems.Length; i++)
{
if (scene.FillItems[i] is FlushScene.FillSceneItem item)
{
_ = item.GetRenderer<TPixel>(configuration, destinationFrame.Width);
}
}
}
if (scene.StrokeItemCount > 0)
{
for (int i = 0; i < scene.StrokeItems.Length; i++)
{
if (scene.StrokeItems[i] is FlushScene.StrokeSceneItem item)
{
_ = item.GetRenderer<TPixel>(configuration, destinationFrame.Width);
}
}
}
int requestedParallelism = configuration.MaxDegreeOfParallelism;
_ = Parallel.For(
fromInclusive: 0,
toExclusive: scene.RowCount,
parallelOptions: ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, scene.RowCount),
localInit: () => new WorkerState<TPixel>(configuration.MemoryAllocator, destinationFrame.Width, scene.MaxLayerDepth + 1),
body: (rowIndex, _, state) =>
{
ExecuteSceneRow(
configuration,
destinationFrame,
scene,
scene.Rows[rowIndex],
state);
return state;
},
localFinally: static state => state.Dispose());
}
/// <summary>
/// Executes one retained scene row against the destination band it overlaps.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The active processing configuration.</param>
/// <param name="destinationFrame">The destination CPU region.</param>
/// <param name="scene">The retained flush scene.</param>
/// <param name="row">The retained scene row to execute.</param>
/// <param name="state">The worker-local scratch and compositing state.</param>
private static void ExecuteSceneRow<TPixel>(
Configuration configuration,
Buffer2DRegion<TPixel> destinationFrame,
FlushScene scene,
in FlushScene.SceneRow row,
WorkerState<TPixel> state)
where TPixel : unmanaged, IPixel<TPixel>
{
int bandTop = row.RowBandIndex * DefaultRasterizer.DefaultTileHeight;
int localBandTop = bandTop - destinationFrame.Bounds.Y;
int bandHeight = Math.Min(DefaultRasterizer.DefaultTileHeight, destinationFrame.Height - localBandTop);
if (bandHeight <= 0)
{
return;
}
Buffer2DRegion<TPixel> destinationBand = destinationFrame.GetSubRegion(0, localBandTop, destinationFrame.Width, bandHeight);
BandTarget<TPixel>[] targetStack = state.TargetStack;
int targetCount = 1;
targetStack[0] = new BandTarget<TPixel>(destinationBand, destinationFrame.Bounds.X, bandTop, null);
int scratchWidth = GetRowScratchWidth(scene, row, destinationFrame.Width);
DefaultRasterizer.WorkerScratch scratch = state.GetOrCreateScratch(scratchWidth);
try
{
for (FlushScene.SceneOperationBlock? block = row.FirstBlock; block is not null; block = block.Next)
{
foreach (FlushScene.SceneOperation operation in block.Items)
{
// Each retained row contains a compact mix of layer control operations and
// draw operations in original command order, so the executor can replay the
// row without re-walking the full scene description.
switch (operation.Kind)
{
case FlushScene.SceneOperationKind.BeginLayer:
GraphicsOptions? layerOptions = scene.LayerOptions[operation.ItemIndex];
targetStack[targetCount++] =
new BandTarget<TPixel>(
configuration.MemoryAllocator.Allocate2D<TPixel>(operation.LayerBounds.Width, operation.LayerBounds.Height, AllocationOptions.Clean),
operation.LayerBounds,
layerOptions);
break;
case FlushScene.SceneOperationKind.EndLayer:
BandTarget<TPixel> source = targetStack[--targetCount];
BandTarget<TPixel> destination = targetStack[targetCount - 1];
CompositeLayerBand(configuration, source, destination, state.BrushWorkspace);
source.Dispose();
break;
case FlushScene.SceneOperationKind.FillItem:
BandTarget<TPixel> target = targetStack[targetCount - 1];
FlushScene.FillSceneItem sceneItem = scene.FillItems[operation.ItemIndex]!;
ExecuteFillOperation(
sceneItem.GetRenderer<TPixel>(configuration, destinationFrame.Width),
new DefaultRasterizer.RasterizableItem(sceneItem.Rasterizable, operation.LocalRowIndex),
target,
scratch,
state);
break;
case FlushScene.SceneOperationKind.StrokeItem:
BandTarget<TPixel> strokeTarget = targetStack[targetCount - 1];
FlushScene.StrokeSceneItem strokeSceneItem = scene.StrokeItems[operation.ItemIndex]!;
ExecuteStrokeOperation(
strokeSceneItem.GetRenderer<TPixel>(configuration, destinationFrame.Width),
new DefaultRasterizer.StrokeRasterizableItem(strokeSceneItem.Rasterizable, operation.LocalRowIndex),
strokeTarget,
scratch,
state);
break;
}
}
}
}
finally
{
for (int i = 1; i < targetCount; i++)
{
targetStack[i].Dispose();
targetStack[i] = null!;
}
targetStack[0] = null!;
}
}
/// <summary>
/// Computes the minimum reusable scratch width needed to execute one retained scene row.
/// </summary>
/// <param name="scene">The retained flush scene.</param>
/// <param name="row">The retained scene row.</param>
/// <param name="minimumWidth">The baseline width taken from the destination band.</param>
/// <returns>The scratch width required by the row.</returns>
private static int GetRowScratchWidth(
FlushScene scene,
in FlushScene.SceneRow row,
int minimumWidth)
{
int width = minimumWidth;
for (FlushScene.SceneOperationBlock? block = row.FirstBlock; block is not null; block = block.Next)
{
foreach (FlushScene.SceneOperation operation in block.Items)
{
if (operation.Kind is FlushScene.SceneOperationKind.BeginLayer or FlushScene.SceneOperationKind.EndLayer)
{
continue;
}
int itemWidth = operation.Kind == FlushScene.SceneOperationKind.FillItem
? scene.FillItems[operation.ItemIndex]!.Rasterizable.Width
: scene.StrokeItems[operation.ItemIndex]!.Rasterizable.Width;
if (itemWidth > width)
{
width = itemWidth;
}
}
}
return width;
}
/// <summary>
/// Executes one retained fill operation through the rasterizer and brush renderer.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="renderer">The memoized brush renderer for the scene item.</param>
/// <param name="item">The retained rasterizable row item to execute.</param>
/// <param name="target">The active composition target for the row.</param>
/// <param name="scratch">The worker-local raster scratch.</param>
/// <param name="state">The worker-local execution state.</param>
private static void ExecuteFillOperation<TPixel>(
BrushRenderer<TPixel> renderer,
DefaultRasterizer.RasterizableItem item,
BandTarget<TPixel> target,
DefaultRasterizer.WorkerScratch scratch,
WorkerState<TPixel> state)
where TPixel : unmanaged, IPixel<TPixel>
{
DefaultRasterizer.RasterizableBandInfo bandInfo = item.Rasterizable.GetBandInfo(item.LocalRowIndex);
DefaultRasterizer.Context context = scratch.CreateContext(
bandInfo.IntersectionRule,
bandInfo.RasterizationMode,
bandInfo.AntialiasThreshold);
FillCoverageRowHandler<TPixel> rowHandler = new(renderer, target, state.BrushWorkspace);
DefaultRasterizer.ExecuteRasterizableItem(
ref context,
in item,
in bandInfo,
scratch.Scanline,
ref rowHandler);
}
/// <summary>
/// Executes one retained stroke operation through the rasterizer and brush renderer.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="renderer">The memoized brush renderer for the scene item.</param>
/// <param name="item">The retained stroke rasterizable row item to execute.</param>
/// <param name="target">The active composition target for the row.</param>
/// <param name="scratch">The worker-local raster scratch.</param>
/// <param name="state">The worker-local execution state.</param>
private static void ExecuteStrokeOperation<TPixel>(
BrushRenderer<TPixel> renderer,
DefaultRasterizer.StrokeRasterizableItem item,
BandTarget<TPixel> target,
DefaultRasterizer.WorkerScratch scratch,
WorkerState<TPixel> state)
where TPixel : unmanaged, IPixel<TPixel>
{
DefaultRasterizer.RasterizableBandInfo bandInfo = item.Rasterizable.GetBandInfo(item.LocalRowIndex);
DefaultRasterizer.Context context = scratch.CreateContext(
bandInfo.IntersectionRule,
bandInfo.RasterizationMode,
bandInfo.AntialiasThreshold);
FillCoverageRowHandler<TPixel> rowHandler = new(renderer, target, state.BrushWorkspace);
Span<float> strokeBandCoverage = item.Rasterizable.RequiresBandCoverage ? scratch.StrokeBandCoverage : [];
DefaultRasterizer.ExecuteStrokeRasterizableItem(
ref context,
in item,
in bandInfo,
scratch.Scanline,
strokeBandCoverage,
ref rowHandler);
}
/// <summary>
/// Composites one temporary layer band back into its destination band.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The active processing configuration.</param>
/// <param name="source">The source layer band.</param>
/// <param name="destination">The destination band to blend into.</param>
/// <param name="brushWorkspace">The worker-local amount buffer workspace.</param>
private static void CompositeLayerBand<TPixel>(
Configuration configuration,
BandTarget<TPixel> source,
BandTarget<TPixel> destination,
BrushWorkspace<TPixel> brushWorkspace)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = source.Region.Width;
if (width == 0 || source.Region.Height == 0)
{
return;
}
Rectangle overlap = Rectangle.Intersect(
new Rectangle(source.AbsoluteLeft, source.AbsoluteTop, source.Region.Width, source.Region.Height),
new Rectangle(destination.AbsoluteLeft, destination.AbsoluteTop, destination.Region.Width, destination.Region.Height));
if (overlap.Width <= 0 || overlap.Height <= 0)
{
return;
}
if (source.GraphicsOptions is not GraphicsOptions graphicsOptions)
{
return;
}
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(graphicsOptions);
Span<float> amounts = brushWorkspace.GetAmounts(overlap.Width);
amounts[..overlap.Width].Fill(graphicsOptions.BlendPercentage);
int sourceOffsetX = overlap.X - source.AbsoluteLeft;
int sourceOffsetY = overlap.Y - source.AbsoluteTop;
int destinationOffsetX = overlap.X - destination.AbsoluteLeft;
int destinationOffsetY = overlap.Y - destination.AbsoluteTop;
// Blend the overlapping rows only; the retained scene has already clipped the layer
// bounds so there is no need for extra per-pixel bounds logic here.
for (int y = 0; y < overlap.Height; y++)
{
Span<TPixel> sourceRow = source.Region.DangerousGetRowSpan(sourceOffsetY + y).Slice(sourceOffsetX, overlap.Width);
Span<TPixel> destinationRow = destination.Region.DangerousGetRowSpan(destinationOffsetY + y).Slice(destinationOffsetX, overlap.Width);
blender.Blend(
configuration,
destinationRow,
destinationRow,
sourceRow,
amounts[..overlap.Width],
brushWorkspace.GetBlendScratch(overlap.Width, 3));
}
}
/// <summary>
/// Composites one CPU-backed frame onto another using the supplied graphics options.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The active processing configuration.</param>
/// <param name="source">The source frame.</param>
/// <param name="destination">The destination frame.</param>
/// <param name="destinationOffset">The destination offset relative to <paramref name="destination"/>.</param>
/// <param name="options">The graphics options controlling composition.</param>
public static void ComposeLayer<TPixel>(
Configuration configuration,
ICanvasFrame<TPixel> source,
ICanvasFrame<TPixel> destination,
Point destinationOffset,
GraphicsOptions options)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
if (!source.TryGetCpuRegion(out Buffer2DRegion<TPixel> sourceRegion))
{
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible source frames.");
}
if (!destination.TryGetCpuRegion(out Buffer2DRegion<TPixel> destinationRegion))
{
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible destination frames.");
}
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(options);
float blendPercentage = options.BlendPercentage;
int srcWidth = sourceRegion.Width;
int srcHeight = sourceRegion.Height;
int dstWidth = destinationRegion.Width;
int dstHeight = destinationRegion.Height;
// Clamp the compositing region to both source and destination bounds.
int startX = Math.Max(0, -destinationOffset.X);
int startY = Math.Max(0, -destinationOffset.Y);
int endX = Math.Min(srcWidth, dstWidth - destinationOffset.X);
int endY = Math.Min(srcHeight, dstHeight - destinationOffset.Y);
if (endX <= startX || endY <= startY)
{
return;
}
int width = endX - startX;
// Allocate a reusable per-row amount buffer from the memory pool.
using IMemoryOwner<float> amountsOwner = configuration.MemoryAllocator.Allocate<float>(width);
Span<float> amounts = amountsOwner.Memory.Span;
amounts.Fill(blendPercentage);
for (int y = startY; y < endY; y++)
{
Span<TPixel> srcRow = sourceRegion.DangerousGetRowSpan(y).Slice(startX, width);
int dstX = destinationOffset.X + startX;
int dstY = destinationOffset.Y + y;
Span<TPixel> dstRow = destinationRegion.DangerousGetRowSpan(dstY).Slice(dstX, width);
blender.Blend(configuration, dstRow, dstRow, srcRow, amounts);
}
}
/// <inheritdoc />
public void ReadRegion<TPixel>(
Configuration configuration,
ICanvasFrame<TPixel> target,
Rectangle sourceRectangle,
Buffer2DRegion<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(destination.Buffer, nameof(destination));
// CPU backend readback is available only when the target exposes CPU pixels.
if (!target.TryGetCpuRegion(out Buffer2DRegion<TPixel> sourceRegion))
{
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for readback.");
}
// Clamp the request to the target region to avoid out-of-range row slicing.
Rectangle clipped = Rectangle.Intersect(
new Rectangle(0, 0, sourceRegion.Width, sourceRegion.Height),
sourceRectangle);
if (clipped.Width <= 0 || clipped.Height <= 0)
{
throw new ArgumentException("The requested readback rectangle does not intersect the target bounds.", nameof(sourceRectangle));
}
int copyWidth = Math.Min(clipped.Width, destination.Width);
int copyHeight = Math.Min(clipped.Height, destination.Height);
for (int y = 0; y < copyHeight; y++)
{
sourceRegion.DangerousGetRowSpan(clipped.Y + y)
.Slice(clipped.X, copyWidth)
.CopyTo(destination.DangerousGetRowSpan(y));
}
}
}
}