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

203 lines
8.9 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using System;
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
{
/// <summary>
/// Adapts rasterizer coverage callbacks into brush application against the active band target.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
private readonly struct FillCoverageRowHandler<TPixel> : IRasterizerCoverageRowHandler
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly BrushRenderer<TPixel> renderer;
private readonly BandTarget<TPixel> target;
private readonly BrushWorkspace<TPixel> brushWorkspace;
/// <summary>
/// Initializes a new instance of the <see cref="FillCoverageRowHandler{TPixel}"/> struct.
/// </summary>
/// <param name="renderer">The brush renderer that will consume emitted coverage spans.</param>
/// <param name="target">The active band target being rendered.</param>
/// <param name="brushWorkspace">The worker-local brush workspace.</param>
public FillCoverageRowHandler(
BrushRenderer<TPixel> renderer,
BandTarget<TPixel> target,
BrushWorkspace<TPixel> brushWorkspace)
{
this.renderer = renderer;
this.target = target;
this.brushWorkspace = brushWorkspace;
}
/// <summary>
/// Applies one emitted coverage span to the active destination band.
/// </summary>
/// <param name="y">The absolute destination row.</param>
/// <param name="startX">The absolute start column of the coverage span.</param>
/// <param name="coverage">The emitted coverage values.</param>
public void Handle(int y, int startX, Span<float> coverage)
{
int localY = y - this.target.AbsoluteTop;
if ((uint)localY >= (uint)this.target.Region.Height)
{
return;
}
int clipStartX = Math.Max(startX, this.target.AbsoluteLeft);
int clipEndX = Math.Min(startX + coverage.Length, this.target.AbsoluteLeft + this.target.Region.Width);
if (clipEndX <= clipStartX)
{
return;
}
// The rasterizer emits absolute coordinates; clip them once here so the brush
// renderer can operate against a tight destination span with no extra bounds work.
int coverageOffset = clipStartX - startX;
int clippedLength = clipEndX - clipStartX;
Span<TPixel> destinationRow = this.target.Region
.DangerousGetRowSpan(localY)
.Slice(clipStartX - this.target.AbsoluteLeft, clippedLength);
this.renderer.Apply(destinationRow, coverage.Slice(coverageOffset, clippedLength), clipStartX, y, this.brushWorkspace);
}
}
/// <summary>
/// Represents one active composition target for a retained row.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
private sealed class BandTarget<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly Buffer2D<TPixel>? owner;
/// <summary>
/// Initializes a new instance of the <see cref="BandTarget{TPixel}"/> class over an existing region.
/// </summary>
/// <param name="region">The destination region.</param>
/// <param name="absoluteLeft">The absolute X origin of the region.</param>
/// <param name="absoluteTop">The absolute Y origin of the region.</param>
/// <param name="graphicsOptions">The graphics options used when this target is later composited.</param>
public BandTarget(Buffer2DRegion<TPixel> region, int absoluteLeft, int absoluteTop, GraphicsOptions? graphicsOptions)
{
this.Region = region;
this.AbsoluteLeft = absoluteLeft;
this.AbsoluteTop = absoluteTop;
this.GraphicsOptions = graphicsOptions;
}
/// <summary>
/// Initializes a new instance of the <see cref="BandTarget{TPixel}"/> class over an owned temporary buffer.
/// </summary>
/// <param name="owner">The owned buffer backing the target.</param>
/// <param name="bounds">The absolute bounds represented by the target.</param>
/// <param name="graphicsOptions">The graphics options used when this target is later composited.</param>
public BandTarget(Buffer2D<TPixel> owner, Rectangle bounds, GraphicsOptions? graphicsOptions)
{
this.owner = owner;
this.Region = owner.GetRegion();
this.AbsoluteLeft = bounds.X;
this.AbsoluteTop = bounds.Y;
this.GraphicsOptions = graphicsOptions;
}
/// <summary>
/// Gets the writable pixel region for the target.
/// </summary>
public Buffer2DRegion<TPixel> Region { get; }
/// <summary>
/// Gets the absolute X origin of <see cref="Region"/>.
/// </summary>
public int AbsoluteLeft { get; }
/// <summary>
/// Gets the absolute Y origin of <see cref="Region"/>.
/// </summary>
public int AbsoluteTop { get; }
/// <summary>
/// Gets the graphics options associated with the target when it is used as a layer.
/// </summary>
public GraphicsOptions? GraphicsOptions { get; }
/// <summary>
/// Releases the owned temporary buffer when the target represents a layer.
/// </summary>
public void Dispose() => this.owner?.Dispose();
}
/// <summary>
/// Holds the reusable worker-local scratch used while executing retained scene rows.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
private sealed class WorkerState<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly MemoryAllocator allocator;
private DefaultRasterizer.WorkerScratch? scratch;
/// <summary>
/// Initializes a new instance of the <see cref="WorkerState{TPixel}"/> class.
/// </summary>
/// <param name="allocator">The memory allocator used for scratch growth.</param>
/// <param name="destinationWidth">The destination width used to size the brush workspace.</param>
/// <param name="layerDepth">The maximum retained layer depth required by the scene.</param>
public WorkerState(
MemoryAllocator allocator,
int destinationWidth,
int layerDepth)
{
this.allocator = allocator;
this.BrushWorkspace = new BrushWorkspace<TPixel>(allocator, destinationWidth);
this.TargetStack = new BandTarget<TPixel>[layerDepth];
}
/// <summary>
/// Gets the reusable brush workspace for the worker.
/// </summary>
public BrushWorkspace<TPixel> BrushWorkspace { get; }
/// <summary>
/// Gets the reusable composition target stack for the worker.
/// </summary>
public BandTarget<TPixel>[] TargetStack { get; }
/// <summary>
/// Returns a reusable raster scratch instance sized for the requested width.
/// </summary>
/// <param name="requiredWidth">The minimum scanline width required by the current row.</param>
/// <returns>A scratch instance that can execute the row.</returns>
public DefaultRasterizer.WorkerScratch GetOrCreateScratch(int requiredWidth)
{
DefaultRasterizer.WorkerScratch? current = this.scratch;
if (current is not null && current.CanReuse(requiredWidth))
{
return current;
}
current?.Dispose();
this.scratch = DefaultRasterizer.CreateWorkerScratch(this.allocator, requiredWidth);
return this.scratch;
}
/// <summary>
/// Releases the worker-local scratch and brush workspace.
/// </summary>
public void Dispose()
{
this.scratch?.Dispose();
this.BrushWorkspace.Dispose();
}
}
}
}