// 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 { /// /// CPU backend that executes path coverage rasterization and brush composition directly against a CPU region. /// public sealed partial class DefaultDrawingBackend : IDrawingBackend { /// /// Gets the default backend instance. /// public static DefaultDrawingBackend Instance { get; } = new(); /// public DrawingBackendScene CreateScene( Configuration configuration, Rectangle targetBounds, DrawingCommandBatch commandBatch, IReadOnlyList? ownedResources = null) { FlushScene scene = FlushScene.Create( commandBatch, targetBounds, configuration.MemoryAllocator, configuration.MaxDegreeOfParallelism); return new DefaultDrawingBackendScene(scene, targetBounds, ownedResources); } /// public void RenderScene( Configuration configuration, ICanvasFrame target, DrawingBackendScene scene) where TPixel : unmanaged, IPixel { if (scene is not DefaultDrawingBackendScene cpuScene) { throw new InvalidOperationException("The retained scene is not a CPU drawing backend scene."); } if (!target.TryGetCpuRegion(out Buffer2DRegion 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); } } /// /// Executes one retained flush scene against a CPU destination frame. /// /// The pixel format. /// The active processing configuration. /// The destination CPU region. /// The retained scene to execute. private static void ExecuteScene( Configuration configuration, Buffer2DRegion destinationFrame, FlushScene scene) where TPixel : unmanaged, IPixel { // 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(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(configuration, destinationFrame.Width); } } } int requestedParallelism = configuration.MaxDegreeOfParallelism; _ = Parallel.For( fromInclusive: 0, toExclusive: scene.RowCount, parallelOptions: ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, scene.RowCount), localInit: () => new WorkerState(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()); } /// /// Executes one retained scene row against the destination band it overlaps. /// /// The pixel format. /// The active processing configuration. /// The destination CPU region. /// The retained flush scene. /// The retained scene row to execute. /// The worker-local scratch and compositing state. private static void ExecuteSceneRow( Configuration configuration, Buffer2DRegion destinationFrame, FlushScene scene, in FlushScene.SceneRow row, WorkerState state) where TPixel : unmanaged, IPixel { 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 destinationBand = destinationFrame.GetSubRegion(0, localBandTop, destinationFrame.Width, bandHeight); BandTarget[] targetStack = state.TargetStack; int targetCount = 1; targetStack[0] = new BandTarget(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( configuration.MemoryAllocator.Allocate2D(operation.LayerBounds.Width, operation.LayerBounds.Height, AllocationOptions.Clean), operation.LayerBounds, layerOptions); break; case FlushScene.SceneOperationKind.EndLayer: BandTarget source = targetStack[--targetCount]; BandTarget destination = targetStack[targetCount - 1]; CompositeLayerBand(configuration, source, destination, state.BrushWorkspace); source.Dispose(); break; case FlushScene.SceneOperationKind.FillItem: BandTarget target = targetStack[targetCount - 1]; FlushScene.FillSceneItem sceneItem = scene.FillItems[operation.ItemIndex]!; ExecuteFillOperation( sceneItem.GetRenderer(configuration, destinationFrame.Width), new DefaultRasterizer.RasterizableItem(sceneItem.Rasterizable, operation.LocalRowIndex), target, scratch, state); break; case FlushScene.SceneOperationKind.StrokeItem: BandTarget strokeTarget = targetStack[targetCount - 1]; FlushScene.StrokeSceneItem strokeSceneItem = scene.StrokeItems[operation.ItemIndex]!; ExecuteStrokeOperation( strokeSceneItem.GetRenderer(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!; } } /// /// Computes the minimum reusable scratch width needed to execute one retained scene row. /// /// The retained flush scene. /// The retained scene row. /// The baseline width taken from the destination band. /// The scratch width required by the row. 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; } /// /// Executes one retained fill operation through the rasterizer and brush renderer. /// /// The pixel format. /// The memoized brush renderer for the scene item. /// The retained rasterizable row item to execute. /// The active composition target for the row. /// The worker-local raster scratch. /// The worker-local execution state. private static void ExecuteFillOperation( BrushRenderer renderer, DefaultRasterizer.RasterizableItem item, BandTarget target, DefaultRasterizer.WorkerScratch scratch, WorkerState state) where TPixel : unmanaged, IPixel { DefaultRasterizer.RasterizableBandInfo bandInfo = item.Rasterizable.GetBandInfo(item.LocalRowIndex); DefaultRasterizer.Context context = scratch.CreateContext( bandInfo.IntersectionRule, bandInfo.RasterizationMode, bandInfo.AntialiasThreshold); FillCoverageRowHandler rowHandler = new(renderer, target, state.BrushWorkspace); DefaultRasterizer.ExecuteRasterizableItem( ref context, in item, in bandInfo, scratch.Scanline, ref rowHandler); } /// /// Executes one retained stroke operation through the rasterizer and brush renderer. /// /// The pixel format. /// The memoized brush renderer for the scene item. /// The retained stroke rasterizable row item to execute. /// The active composition target for the row. /// The worker-local raster scratch. /// The worker-local execution state. private static void ExecuteStrokeOperation( BrushRenderer renderer, DefaultRasterizer.StrokeRasterizableItem item, BandTarget target, DefaultRasterizer.WorkerScratch scratch, WorkerState state) where TPixel : unmanaged, IPixel { DefaultRasterizer.RasterizableBandInfo bandInfo = item.Rasterizable.GetBandInfo(item.LocalRowIndex); DefaultRasterizer.Context context = scratch.CreateContext( bandInfo.IntersectionRule, bandInfo.RasterizationMode, bandInfo.AntialiasThreshold); FillCoverageRowHandler rowHandler = new(renderer, target, state.BrushWorkspace); Span strokeBandCoverage = item.Rasterizable.RequiresBandCoverage ? scratch.StrokeBandCoverage : []; DefaultRasterizer.ExecuteStrokeRasterizableItem( ref context, in item, in bandInfo, scratch.Scanline, strokeBandCoverage, ref rowHandler); } /// /// Composites one temporary layer band back into its destination band. /// /// The pixel format. /// The active processing configuration. /// The source layer band. /// The destination band to blend into. /// The worker-local amount buffer workspace. private static void CompositeLayerBand( Configuration configuration, BandTarget source, BandTarget destination, BrushWorkspace brushWorkspace) where TPixel : unmanaged, IPixel { 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 blender = PixelOperations.Instance.GetPixelBlender(graphicsOptions); Span 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 sourceRow = source.Region.DangerousGetRowSpan(sourceOffsetY + y).Slice(sourceOffsetX, overlap.Width); Span destinationRow = destination.Region.DangerousGetRowSpan(destinationOffsetY + y).Slice(destinationOffsetX, overlap.Width); blender.Blend( configuration, destinationRow, destinationRow, sourceRow, amounts[..overlap.Width], brushWorkspace.GetBlendScratch(overlap.Width, 3)); } } /// /// Composites one CPU-backed frame onto another using the supplied graphics options. /// /// The pixel format. /// The active processing configuration. /// The source frame. /// The destination frame. /// The destination offset relative to . /// The graphics options controlling composition. public static void ComposeLayer( Configuration configuration, ICanvasFrame source, ICanvasFrame destination, Point destinationOffset, GraphicsOptions options) where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); if (!source.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) { throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible source frames."); } if (!destination.TryGetCpuRegion(out Buffer2DRegion destinationRegion)) { throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible destination frames."); } PixelBlender blender = PixelOperations.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 amountsOwner = configuration.MemoryAllocator.Allocate(width); Span amounts = amountsOwner.Memory.Span; amounts.Fill(blendPercentage); for (int y = startY; y < endY; y++) { Span srcRow = sourceRegion.DangerousGetRowSpan(y).Slice(startX, width); int dstX = destinationOffset.X + startX; int dstY = destinationOffset.Y + y; Span dstRow = destinationRegion.DangerousGetRowSpan(dstY).Slice(dstX, width); blender.Blend(configuration, dstRow, dstRow, srcRow, amounts); } } /// public void ReadRegion( Configuration configuration, ICanvasFrame target, Rectangle sourceRectangle, Buffer2DRegion destination) where TPixel : unmanaged, IPixel { 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 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)); } } } }