first commit
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Processor barrier recorded in a drawing backend timeline.
|
||||
/// </summary>
|
||||
internal sealed class ApplyBarrier
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApplyBarrier"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The closed path defining the processed region.</param>
|
||||
/// <param name="options">The drawing options captured when the barrier was recorded.</param>
|
||||
/// <param name="clipPaths">The active clip paths captured when the barrier was recorded.</param>
|
||||
/// <param name="canvasBounds">The canvas-local bounds captured when the barrier was recorded.</param>
|
||||
/// <param name="targetBounds">The absolute target bounds captured when the barrier was recorded.</param>
|
||||
/// <param name="destinationOffset">The absolute destination offset captured when the barrier was recorded.</param>
|
||||
/// <param name="isInsideLayer">Indicates whether the barrier was recorded inside a layer.</param>
|
||||
/// <param name="operation">The processor operation to run against the replay-time snapshot.</param>
|
||||
internal ApplyBarrier(
|
||||
IPath path,
|
||||
DrawingOptions options,
|
||||
IReadOnlyList<IPath> clipPaths,
|
||||
Rectangle canvasBounds,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
bool isInsideLayer,
|
||||
Action<IImageProcessingContext> operation)
|
||||
{
|
||||
this.Path = path;
|
||||
this.Options = options;
|
||||
this.ClipPaths = clipPaths;
|
||||
this.CanvasBounds = canvasBounds;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
this.Operation = operation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the closed path defining the processed region.
|
||||
/// </summary>
|
||||
public IPath Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public DrawingOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active clip paths captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath> ClipPaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the canvas-local bounds captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public Rectangle CanvasBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute target bounds captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the barrier was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the processor operation to run against the replay-time snapshot.
|
||||
/// </summary>
|
||||
public Action<IImageProcessingContext> Operation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates the transient image-brush draw command that writes this barrier's processed snapshot back to the target.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="backend">The backend used to read the replay-time target pixels.</param>
|
||||
/// <param name="target">The target frame.</param>
|
||||
/// <param name="ownedResource">The image resource that must stay alive while the returned command batch is rendered.</param>
|
||||
/// <returns>The transient write-back command batch, or <see langword="null"/> when the barrier has no target coverage.</returns>
|
||||
public DrawingCommandBatch? CreateWriteBackBatch<TPixel>(
|
||||
Configuration configuration,
|
||||
IDrawingBackend backend,
|
||||
ICanvasFrame<TPixel> target,
|
||||
out IDisposable? ownedResource)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
RectangleF rawBounds = RectangleF.Transform(this.Path.Bounds, this.Options.Transform);
|
||||
Rectangle sourceRect = ToConservativeBounds(rawBounds);
|
||||
sourceRect = Rectangle.Intersect(this.CanvasBounds, sourceRect);
|
||||
|
||||
if (sourceRect.Width <= 0 || sourceRect.Height <= 0)
|
||||
{
|
||||
ownedResource = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
Image<TPixel> sourceImage = new(configuration, sourceRect.Width, sourceRect.Height);
|
||||
try
|
||||
{
|
||||
backend.ReadRegion(
|
||||
configuration,
|
||||
target,
|
||||
sourceRect,
|
||||
sourceImage.Frames.RootFrame.PixelBuffer.GetRegion());
|
||||
|
||||
sourceImage.Mutate(this.Operation);
|
||||
|
||||
Point brushOffset = new(
|
||||
sourceRect.X - (int)MathF.Floor(rawBounds.Left),
|
||||
sourceRect.Y - (int)MathF.Floor(rawBounds.Top));
|
||||
|
||||
ImageBrush<TPixel> brush = new(sourceImage, sourceImage.Bounds, brushOffset);
|
||||
GraphicsOptions graphicsOptions = this.Options.GraphicsOptions;
|
||||
RasterizationMode rasterizationMode = graphicsOptions.Antialias
|
||||
? RasterizationMode.Antialiased
|
||||
: RasterizationMode.Aliased;
|
||||
|
||||
RectangleF pathBounds = this.Path.Bounds;
|
||||
Rectangle interest = Rectangle.FromLTRB(
|
||||
(int)MathF.Floor(pathBounds.Left),
|
||||
(int)MathF.Floor(pathBounds.Top),
|
||||
(int)MathF.Ceiling(pathBounds.Right),
|
||||
(int)MathF.Ceiling(pathBounds.Bottom));
|
||||
|
||||
RasterizerOptions rasterizerOptions = new(
|
||||
interest,
|
||||
this.Options.ShapeOptions.IntersectionRule,
|
||||
rasterizationMode,
|
||||
RasterizerSamplingOrigin.PixelBoundary,
|
||||
graphicsOptions.AntialiasThreshold);
|
||||
|
||||
CompositionCommand command = CompositionCommand.Create(
|
||||
this.Path,
|
||||
brush,
|
||||
this.Options,
|
||||
in rasterizerOptions,
|
||||
this.TargetBounds,
|
||||
this.DestinationOffset,
|
||||
this.ClipPaths,
|
||||
this.IsInsideLayer);
|
||||
|
||||
ownedResource = sourceImage;
|
||||
CompositionSceneCommand[] commands = [new PathCompositionSceneCommand(command)];
|
||||
return new DrawingCommandBatch(commands, hasLayers: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
sourceImage.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static Rectangle ToConservativeBounds(RectangleF bounds)
|
||||
=> Rectangle.FromLTRB(
|
||||
(int)MathF.Floor(bounds.Left),
|
||||
(int)MathF.Floor(bounds.Top),
|
||||
(int)MathF.Ceiling(bounds.Right),
|
||||
(int)MathF.Ceiling(bounds.Bottom));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Frame adapter that exposes a clipped subregion of another frame.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
internal sealed class CanvasRegionFrame<TPixel> : ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly ICanvasFrame<TPixel> parent;
|
||||
private readonly Rectangle region;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CanvasRegionFrame{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent frame that owns the target pixels.</param>
|
||||
/// <param name="region">The child region in parent-local coordinates.</param>
|
||||
public CanvasRegionFrame(ICanvasFrame<TPixel> parent, Rectangle region)
|
||||
{
|
||||
Guard.NotNull(parent, nameof(parent));
|
||||
Guard.MustBeGreaterThanOrEqualTo(region.Width, 0, nameof(region));
|
||||
Guard.MustBeGreaterThanOrEqualTo(region.Height, 0, nameof(region));
|
||||
|
||||
this.parent = parent;
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Rectangle Bounds => new(
|
||||
this.parent.Bounds.X + this.region.X,
|
||||
this.parent.Bounds.Y + this.region.Y,
|
||||
this.region.Width,
|
||||
this.region.Height);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
if (!this.parent.TryGetCpuRegion(out Buffer2DRegion<TPixel> parentRegion))
|
||||
{
|
||||
region = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
region = parentRegion.GetSubRegion(this.region);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface)
|
||||
=> this.parent.TryGetNativeSurface(out surface);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Identifies the flush-time role carried by a <see cref="CompositionCommand"/>.
|
||||
/// </summary>
|
||||
public enum CompositionCommandKind : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// A fill-path command.
|
||||
/// </summary>
|
||||
FillLayer = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Starts an isolated compositing layer.
|
||||
/// </summary>
|
||||
BeginLayer = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Ends the most recently opened layer.
|
||||
/// </summary>
|
||||
EndLayer = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One normalized fill-path or layer-based composition command queued for backend execution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This type carries fill-path commands plus inline layer boundaries.
|
||||
/// </remarks>
|
||||
public readonly struct CompositionCommand
|
||||
{
|
||||
private readonly IPath? sourcePath;
|
||||
private readonly Brush? brush;
|
||||
private readonly DrawingOptions? drawingOptions;
|
||||
private readonly GraphicsOptions? layerGraphicsOptions;
|
||||
private readonly IReadOnlyList<IPath>? clipPaths;
|
||||
|
||||
private CompositionCommand(
|
||||
CompositionCommandKind kind,
|
||||
IPath? sourcePath,
|
||||
Brush? brush,
|
||||
DrawingOptions? drawingOptions,
|
||||
GraphicsOptions? layerGraphicsOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Rectangle layerBounds,
|
||||
Point destinationOffset,
|
||||
IReadOnlyList<IPath>? clipPaths,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
this.Kind = kind;
|
||||
this.sourcePath = sourcePath;
|
||||
this.brush = brush;
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.layerGraphicsOptions = layerGraphicsOptions;
|
||||
this.RasterizerOptions = rasterizerOptions;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.LayerBounds = layerBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.clipPaths = clipPaths;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command kind.
|
||||
/// </summary>
|
||||
public CompositionCommandKind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the logical target for this command.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the layer opened by this command.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only meaningful for <see cref="CompositionCommandKind.BeginLayer"/> and
|
||||
/// <see cref="CompositionCommandKind.EndLayer"/>.
|
||||
/// </remarks>
|
||||
public Rectangle LayerBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used during composition.
|
||||
/// </summary>
|
||||
public Brush Brush => this.brush ?? throw new InvalidOperationException("Layer commands do not carry a brush.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options carried by the command.
|
||||
/// </summary>
|
||||
public DrawingOptions DrawingOptions => this.drawingOptions ?? throw new InvalidOperationException("Layer commands do not carry drawing options.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets graphics options used for composition or layer compositing.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions => this.drawingOptions?.GraphicsOptions ?? this.layerGraphicsOptions!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets rasterizer options used to generate coverage.
|
||||
/// </summary>
|
||||
public RasterizerOptions RasterizerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset where the local coverage should be composited.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source path carried by the command.
|
||||
/// </summary>
|
||||
public IPath SourcePath => this.sourcePath ?? throw new InvalidOperationException("Layer commands do not carry path geometry.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform => this.drawingOptions?.Transform ?? Matrix4x4.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the clip paths carried by the command.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath>? ClipPaths => this.clipPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shape options carried by the command.
|
||||
/// </summary>
|
||||
public ShapeOptions ShapeOptions => this.drawingOptions?.ShapeOptions ?? throw new InvalidOperationException("Layer commands do not carry shape options.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fill-path composition command.
|
||||
/// </summary>
|
||||
/// <param name="path">Path in target-local coordinates.</param>
|
||||
/// <param name="brush">Brush used during composition.</param>
|
||||
/// <param name="drawingOptions">Drawing options (graphics, shape, transform) used during composition.</param>
|
||||
/// <param name="rasterizerOptions">Rasterizer options used to generate coverage.</param>
|
||||
/// <param name="targetBounds">The absolute bounds of the logical target for this command.</param>
|
||||
/// <param name="destinationOffset">Absolute destination offset where coverage is composited.</param>
|
||||
/// <param name="clipPaths">Optional clip paths supplied with the command.</param>
|
||||
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
|
||||
/// <returns>The composition command.</returns>
|
||||
public static CompositionCommand Create(
|
||||
IPath path,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
IReadOnlyList<IPath>? clipPaths,
|
||||
bool isInsideLayer)
|
||||
=> new(
|
||||
CompositionCommandKind.FillLayer,
|
||||
path,
|
||||
brush,
|
||||
drawingOptions,
|
||||
null,
|
||||
in rasterizerOptions,
|
||||
targetBounds,
|
||||
default,
|
||||
destinationOffset,
|
||||
clipPaths,
|
||||
isInsideLayer);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a begin-layer composition command. <see cref="IsInsideLayer"/> is false on the
|
||||
/// BeginLayer marker itself; the flag is only meaningful for fills/strokes that follow it.
|
||||
/// </summary>
|
||||
/// <param name="layerBounds">The absolute bounds of the layer.</param>
|
||||
/// <param name="graphicsOptions">The compositing options used when the layer closes.</param>
|
||||
/// <returns>The begin-layer command.</returns>
|
||||
public static CompositionCommand CreateBeginLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions)
|
||||
=> new(
|
||||
CompositionCommandKind.BeginLayer,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
graphicsOptions,
|
||||
default,
|
||||
layerBounds,
|
||||
layerBounds,
|
||||
default,
|
||||
null,
|
||||
false);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an end-layer composition command. <see cref="IsInsideLayer"/> is false on the
|
||||
/// EndLayer marker itself; the flag is only meaningful for fills/strokes that preceded it.
|
||||
/// </summary>
|
||||
/// <param name="layerBounds">The absolute bounds of the layer being closed.</param>
|
||||
/// <param name="graphicsOptions">The compositing options used by the layer.</param>
|
||||
/// <returns>The end-layer command.</returns>
|
||||
public static CompositionCommand CreateEndLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions)
|
||||
=> new(
|
||||
CompositionCommandKind.EndLayer,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
graphicsOptions,
|
||||
default,
|
||||
layerBounds,
|
||||
layerBounds,
|
||||
default,
|
||||
null,
|
||||
false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
#pragma warning disable SA1649 // Scene command types are grouped together in one file.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Visitor contract for one flush-scoped composition scene command.
|
||||
/// </summary>
|
||||
public interface ICompositionSceneCommandVisitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Visits one fill-path or layer-based composition command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(PathCompositionSceneCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Visits one stroked path command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(StrokePathCompositionSceneCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Visits one explicit stroked line-segment command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(LineSegmentCompositionSceneCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Visits one explicit stroked polyline command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(PolylineCompositionSceneCommand command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base type for one draw-order command in a flush-scoped scene stream.
|
||||
/// </summary>
|
||||
public abstract class CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatches the command to a visitor without a per-item kind switch at the call site.
|
||||
/// </summary>
|
||||
/// <param name="visitor">The visitor receiving the command.</param>
|
||||
public abstract void Accept(ICompositionSceneCommandVisitor visitor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for fill-path and layer-based composition commands.
|
||||
/// </summary>
|
||||
public sealed class PathCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped composition command.</param>
|
||||
public PathCompositionSceneCommand(in CompositionCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped composition command.
|
||||
/// </summary>
|
||||
public CompositionCommand Command { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for stroked path commands.
|
||||
/// </summary>
|
||||
public sealed class StrokePathCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokePathCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped stroke path command.</param>
|
||||
public StrokePathCompositionSceneCommand(in StrokePathCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped stroke path command.
|
||||
/// </summary>
|
||||
public StrokePathCommand Command { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for explicit stroked line-segment commands.
|
||||
/// </summary>
|
||||
public sealed class LineSegmentCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LineSegmentCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped stroke line-segment command.</param>
|
||||
public LineSegmentCompositionSceneCommand(in StrokeLineSegmentCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped stroke line-segment command.
|
||||
/// </summary>
|
||||
public StrokeLineSegmentCommand Command { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for explicit stroked polyline commands.
|
||||
/// </summary>
|
||||
public sealed class PolylineCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PolylineCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped stroke polyline command.</param>
|
||||
public PolylineCompositionSceneCommand(in StrokePolylineCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped stroke polyline command.
|
||||
/// </summary>
|
||||
public StrokePolylineCommand Command { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
# DefaultDrawingBackend
|
||||
|
||||
`DefaultDrawingBackend` is the CPU execution backend for ImageSharp.Drawing. It creates retained CPU scenes from prepared drawing command batches, executes those scenes with reusable scratch, and writes the result into a CPU destination buffer.
|
||||
|
||||
This document explains the backend as a system rather than as a list of methods. The goal is to help a newcomer understand:
|
||||
|
||||
- where the CPU backend fits in the canvas/backend selection model
|
||||
- what problem the CPU backend is solving
|
||||
- why the backend is organized around a retained row-oriented execution plan
|
||||
- what `FlushScene` means in this architecture
|
||||
- how rasterization, brush application, and layer composition fit together
|
||||
|
||||
## Where The CPU Backend Fits
|
||||
|
||||
`DefaultDrawingBackend` is the standard CPU execution path behind `DrawingCanvas`.
|
||||
|
||||
The canvas architecture reaches this backend in two common ways:
|
||||
|
||||
- ordinary typed canvas construction resolves `IDrawingBackend` from `Configuration`
|
||||
- specialized infrastructure can construct a canvas with an explicit backend instance
|
||||
|
||||
The CPU path usually uses the first route. The WebGPU helpers use the second route when they need a canvas that targets a native surface through `WebGPUDrawingBackend`.
|
||||
|
||||
That means the CPU backend is one backend implementation within the shared canvas architecture, not a separate public drawing model. It executes against any frame that exposes a writable CPU region, whether that frame is pure memory or a hybrid frame that also carries a native surface.
|
||||
|
||||
## The Main Problem
|
||||
|
||||
By the time work reaches `DefaultDrawingBackend`, the public drawing API has already been normalized into prepared commands. That is helpful, but it does not make CPU execution trivial.
|
||||
|
||||
The backend still has to solve a hard scheduling problem.
|
||||
|
||||
It needs to answer questions such as:
|
||||
|
||||
- which destination rows each command touches
|
||||
- how to preserve draw order while running work in parallel
|
||||
- how to avoid re-deriving geometry information in the hot loop
|
||||
- where temporary memory should live and when it should be reused
|
||||
|
||||
If the CPU backend executed commands directly from the incoming scene, each worker would repeatedly rediscover which rows matter, which parts of the geometry matter in those rows, and how much scratch is needed. That would push expensive planning work into the hottest part of the pipeline.
|
||||
|
||||
So the backend takes a different approach:
|
||||
|
||||
it turns the whole command batch into a row-oriented execution plan first, then executes that plan.
|
||||
|
||||
That decision explains most of the backend architecture.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
The CPU backend is a flush executor, not a command-at-a-time painter.
|
||||
|
||||
Its central idea is:
|
||||
|
||||
> convert a command batch into row-local raster work once, then execute rows directly with reusable worker-local scratch
|
||||
|
||||
That is why the backend is built around `FlushScene`.
|
||||
|
||||
`FlushScene` is a retained execution plan. In non-retained rendering it is short-lived and disposed after one replay entry; in retained rendering it can live with the returned `DefaultDrawingBackendScene`. Its job is to take a prepared command stream and reorganize it into a form that is cheap for the row executor to consume.
|
||||
|
||||
If that idea is clear, most of the important types fall into place.
|
||||
|
||||
## The Most Important Terms
|
||||
|
||||
### Backend
|
||||
|
||||
`DefaultDrawingBackend` is the top-level CPU executor. It owns backend policy and orchestration:
|
||||
|
||||
- acquiring a writable CPU destination
|
||||
- creating the retained execution plan
|
||||
- executing that plan
|
||||
- handling CPU layer composition
|
||||
|
||||
It does not own every detail of geometry planning or scan conversion.
|
||||
|
||||
It also does not own backend selection. By the time `CreateScene(...)` or `RenderScene(...)` is called, the typed canvas implementation has already chosen the backend instance that will receive the prepared work.
|
||||
|
||||
### Scene
|
||||
|
||||
In the canvas architecture, the backend receives a `DrawingCommandBatch`. That batch already contains prepared commands and explicit layer boundaries for one contiguous command range.
|
||||
|
||||
For the CPU backend, that incoming batch is the starting point, not the final execution form.
|
||||
|
||||
### Flush Scene
|
||||
|
||||
`FlushScene` is the most important supporting type in the CPU backend.
|
||||
|
||||
In this codebase, `FlushScene` means:
|
||||
|
||||
"the retained, row-oriented execution plan for one CPU command batch"
|
||||
|
||||
It owns the retained information needed to make execution cheap:
|
||||
|
||||
- the visible prepared commands
|
||||
- retained rasterizable geometry
|
||||
- row membership
|
||||
- row-local execution items
|
||||
- scratch size requirements for the flush
|
||||
|
||||
### Rasterizer
|
||||
|
||||
`DefaultRasterizer` is the geometry-to-coverage engine.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- fixed-point scan conversion
|
||||
- fill-rule handling
|
||||
- coverage accumulation
|
||||
- emitting row coverage spans
|
||||
|
||||
It is not responsible for deciding which commands should run in which rows, and it does not write final pixels directly.
|
||||
|
||||
### Brush Renderer
|
||||
|
||||
`BrushRenderer<TPixel>` is the coverage-to-color engine for one prepared drawing command.
|
||||
|
||||
It receives:
|
||||
|
||||
- a destination row slice
|
||||
- coverage data
|
||||
- destination position
|
||||
- reusable workspace
|
||||
|
||||
and updates pixels accordingly.
|
||||
|
||||
The important separation is:
|
||||
|
||||
- the rasterizer decides coverage
|
||||
- the brush renderer decides color
|
||||
- the backend executor binds the two together
|
||||
|
||||
### Worker State
|
||||
|
||||
`WorkerState<TPixel>` is the reusable per-worker execution state.
|
||||
|
||||
It owns worker-local scratch such as:
|
||||
|
||||
- raster scratch
|
||||
- brush workspace
|
||||
- the coverage row handler state
|
||||
|
||||
This is how the backend avoids allocating fresh buffers for every row item during the hot parallel pass.
|
||||
|
||||
## The Big Picture Flow
|
||||
|
||||
The easiest way to understand the backend is to follow one command batch from scene creation to execution.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[DrawingCanvas disposal replay] --> B[DefaultDrawingBackend.CreateScene]
|
||||
B --> C[FlushScene.Create]
|
||||
C --> D[Prepare visible items]
|
||||
D --> E[Build row-local execution plan]
|
||||
E --> F[DefaultDrawingBackend.RenderScene]
|
||||
F --> G[Acquire CPU destination]
|
||||
G --> H[Execute rows in parallel]
|
||||
H --> I[DefaultRasterizer emits coverage]
|
||||
I --> J[BrushRenderer shades pixels]
|
||||
J --> K[Destination frame updated]
|
||||
```
|
||||
|
||||
There are three major stages in that flow:
|
||||
|
||||
1. build the retained execution plan
|
||||
2. establish the destination frame
|
||||
3. execute rows using that plan
|
||||
|
||||
## What `DefaultDrawingBackend` Owns
|
||||
|
||||
`DefaultDrawingBackend` is intentionally smaller than its supporting types. It owns orchestration, not every low-level detail.
|
||||
|
||||
Its responsibilities are:
|
||||
|
||||
- create a `FlushScene`
|
||||
- acquire a writable CPU region from the target frame
|
||||
- execute that scene
|
||||
- provide CPU layer composition services
|
||||
- manage frame usage for CPU-backed targets
|
||||
|
||||
The expensive work is delegated:
|
||||
|
||||
- `FlushScene` owns retained row planning
|
||||
- `DefaultRasterizer` owns scan conversion
|
||||
- `BrushRenderer<TPixel>` owns brush-specific shading
|
||||
|
||||
That split keeps each type focused on one class of problem.
|
||||
|
||||
The canvas layer above that split is also important:
|
||||
|
||||
- `DrawingCanvas` records public drawing intent
|
||||
- `DrawingCanvasBatcher<TPixel>` prepares commands and constructs `DrawingCommandBatch` values
|
||||
- `DefaultDrawingBackend` executes the retained scene on a CPU destination
|
||||
|
||||
## Building The Flush Scene
|
||||
|
||||
`FlushScene.Create(...)` turns the prepared command stream into an execution plan in several phases. Each phase changes the data into a form that is cheaper for the next phase to consume.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Prepared commands] --> B[Filter and compact visible work]
|
||||
B --> C[Create retained raster geometry]
|
||||
C --> D[Build row membership]
|
||||
D --> E[Build row-local execution items]
|
||||
E --> F[FlushScene]
|
||||
```
|
||||
|
||||
### 1. Filter and compact visible work
|
||||
|
||||
The scene builder begins from the incoming command stream and keeps only the work that is visible and relevant to the flush. The later phases should not pay repeatedly for invisible commands through sparse scans or conditional branching.
|
||||
|
||||
### 2. Create retained raster geometry
|
||||
|
||||
For each visible item, the builder decomposes the command's drawing matrix into an X/Y scale and the rotation-shear-translation-perspective residual, asks the path for its scale-baked `LinearGeometry` via `ToLinearGeometry(Vector2 scale)`, and hands both the geometry and the residual to `DefaultRasterizer` to create the retained rasterizable payload. Curve subdivision therefore happens once per (path, scale) pair — cached on the `IPath` — and any per-frame rotation or translation rides into the rasterizer as the residual without forcing the path to re-flatten.
|
||||
|
||||
This step matters because it moves expensive geometry preparation out of the hot row loop and out of every frame of workloads like text or panning that drift only in their residual.
|
||||
|
||||
### 3. Build row membership
|
||||
|
||||
Once retained geometry exists, the scene builder determines which scene rows each item touches. That produces row-local membership information while preserving original submission order within every row.
|
||||
|
||||
That detail is critical. Parallel execution is allowed, but draw order must remain deterministic within each row.
|
||||
|
||||
### 4. Build row-local execution items
|
||||
|
||||
The scene then materializes the payload that the row executor will visit. Each row item points into flush-owned retained storage and carries just enough metadata to reconstruct a cheap `RasterizableBand` view when execution reaches that row.
|
||||
|
||||
At that point the scene is execution-ready.
|
||||
|
||||
## Why The Backend Is Row-First
|
||||
|
||||
The CPU backend executes rows, not commands.
|
||||
|
||||
This is one of the most important architectural choices in the whole path.
|
||||
|
||||
Why it helps:
|
||||
|
||||
- each worker naturally touches localized destination memory
|
||||
- scratch can be reused across many row items
|
||||
- draw order is straightforward inside a row
|
||||
- geometry planning stays out of the hottest loop
|
||||
|
||||
A row-first executor fits the actual shape of CPU rendering much better than a command-first executor would.
|
||||
|
||||
## The Execution Pass
|
||||
|
||||
When `FlushScene.Execute(...)` runs, the backend prepares brush renderers and then executes scene rows in parallel.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Exec as FlushScene.Execute
|
||||
participant Worker as WorkerState
|
||||
participant Raster as DefaultRasterizer
|
||||
participant Brush as BrushRenderer
|
||||
|
||||
Exec->>Brush: create one renderer per visible item
|
||||
Exec->>Worker: start parallel row pass
|
||||
Worker->>Exec: enumerate row items in order
|
||||
Worker->>Raster: ExecuteRasterizableBand(...)
|
||||
Raster-->>Exec: coverage rows
|
||||
Exec->>Brush: Apply(...)
|
||||
```
|
||||
|
||||
There are two important ownership patterns in that pass:
|
||||
|
||||
- renderers are created once per visible item before the hot row loop
|
||||
- scratch and workspace are reused per worker during the row loop
|
||||
|
||||
That is one of the backend's main performance properties.
|
||||
|
||||
## How Rasterization and Shading Stay Separate
|
||||
|
||||
The rasterizer and the backend solve different problems.
|
||||
|
||||
`DefaultRasterizer` is responsible for geometry and coverage.
|
||||
|
||||
`DefaultDrawingBackend` and `FlushScene` are responsible for:
|
||||
|
||||
- which items execute
|
||||
- when they execute
|
||||
- where their coverage belongs in the destination
|
||||
- which brush renderer should consume that coverage
|
||||
|
||||
That separation is intentional. It lets the rasterizer stay geometry-focused while the backend handles composition and destination layout.
|
||||
|
||||
## Coverage Routing
|
||||
|
||||
The rasterizer does not write destination pixels directly. Instead it emits row coverage through a handler supplied by the backend.
|
||||
|
||||
The backend-side row handler:
|
||||
|
||||
- receives emitted coverage
|
||||
- maps band-local coordinates back into destination coordinates
|
||||
- slices the correct destination row
|
||||
- invokes the correct `BrushRenderer<TPixel>`
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Rasterizer coverage row] --> B[Row handler]
|
||||
B --> C[Map to destination slice]
|
||||
C --> D[BrushRenderer.Apply]
|
||||
D --> E[Pixels updated]
|
||||
```
|
||||
|
||||
This is why the brush renderer can stay target-unbound. It receives the destination row slice and coverage data at execution time rather than owning the destination frame itself.
|
||||
|
||||
## Layer Composition
|
||||
|
||||
CPU layer composition is a separate concern from path rasterization.
|
||||
|
||||
`ComposeLayer<TPixel>()` composites one CPU frame into another using `PixelBlender<TPixel>`. That path exists because compositing an already-rasterized layer is a different problem from scanning geometry into coverage.
|
||||
|
||||
Keeping those paths separate makes the backend easier to reason about.
|
||||
|
||||
## Frame And Memory Lifetime
|
||||
|
||||
The backend aligns ownership with the actual execution lifetime.
|
||||
|
||||
### Flush-owned
|
||||
|
||||
Owned by `FlushScene`:
|
||||
|
||||
- visible item arrays
|
||||
- row structures
|
||||
- retained raster data
|
||||
- start-cover storage
|
||||
|
||||
Disposed when the flush ends.
|
||||
|
||||
### Worker-owned
|
||||
|
||||
Owned by `WorkerState<TPixel>` during execution:
|
||||
|
||||
- raster scratch
|
||||
- brush workspace
|
||||
|
||||
Disposed when the worker completes.
|
||||
|
||||
### Item-owned
|
||||
|
||||
Created once per visible item during execution:
|
||||
|
||||
- `BrushRenderer<TPixel>`
|
||||
|
||||
Retained for the duration of the row pass and then released with the flush-owned scene item state.
|
||||
|
||||
That ownership model keeps allocation and disposal aligned with real work lifetime.
|
||||
|
||||
## Reading Guide
|
||||
|
||||
If you are new to this backend, read the code in this order:
|
||||
|
||||
1. `DrawingCanvas.cs`
|
||||
2. `DrawingCanvas{TPixel}.cs`
|
||||
3. `DrawingCanvasBatcher{TPixel}.cs`
|
||||
4. `DefaultDrawingBackend.cs`
|
||||
5. `FlushScene.cs`
|
||||
6. `FlushScene.RetainedTypes.cs`
|
||||
7. `DefaultDrawingBackend.Helpers.cs`
|
||||
8. `DefaultRasterizer.cs`
|
||||
|
||||
That order mirrors the runtime flow:
|
||||
|
||||
canvas and backend selection -> backend orchestration -> retained row planning -> row execution structures -> worker helpers -> scan conversion
|
||||
|
||||
## The Mental Model To Keep
|
||||
|
||||
The easiest way to keep this backend straight is to remember that it is not a command-at-a-time painter. It is a flush executor that converts visible commands into row-local retained raster work and then executes that work with reusable scratch.
|
||||
|
||||
If that model is clear, the major types fall into place:
|
||||
|
||||
- `DrawingCanvas` records intent, and the typed implementation selects the backend
|
||||
- `DefaultDrawingBackend` orchestrates
|
||||
- `FlushScene` plans
|
||||
- `DefaultRasterizer` converts geometry to coverage
|
||||
- `BrushRenderer<TPixel>` converts coverage to color
|
||||
@@ -0,0 +1,458 @@
|
||||
# DefaultRasterizer
|
||||
|
||||
`DefaultRasterizer` is the CPU polygon scanner used by the retained fill path in ImageSharp.Drawing. Its job is narrow but central: take already-prepared geometry, convert that geometry into fixed-point edge contributions, and emit coverage rows that the CPU backend can turn into pixels.
|
||||
|
||||
This rasterizer is based on ideas and implementation techniques from the Blaze project:
|
||||
|
||||
- https://github.com/aurimasg/blaze
|
||||
|
||||
This document explains the rasterizer as a newcomer needs to understand it:
|
||||
|
||||
- where the rasterizer fits relative to `DrawingCanvas` and `DefaultDrawingBackend`
|
||||
- what problem the rasterizer is solving inside the CPU backend
|
||||
- why the rasterizer is split into retained geometry building and band execution
|
||||
- what retained geometry, bands, and coverage mean in this architecture
|
||||
- how scan conversion stays separate from brush shading and frame ownership
|
||||
|
||||
## Where The Rasterizer Fits
|
||||
|
||||
`DefaultRasterizer` sits below `DrawingCanvas`, the typed canvas implementation, and `DefaultDrawingBackend`.
|
||||
|
||||
The canvas records commands, the batcher prepares them into `DrawingCommandBatch` ranges, and `DefaultDrawingBackend` chooses the row-oriented execution plan for each retained CPU scene. `DefaultRasterizer` then handles the narrower geometry-to-coverage problem inside that CPU execution path.
|
||||
|
||||
That means the rasterizer does not select the backend, own the destination frame, or interpret the public drawing API directly. It receives already-prepared geometry through the CPU backend pipeline, and the backend later routes its coverage into whichever frame exposes the CPU region for the flush.
|
||||
|
||||
## The Main Problem
|
||||
|
||||
The CPU backend does not want to rediscover shape geometry every time it touches a destination row.
|
||||
|
||||
If row execution had to start from raw prepared paths every time, the backend would repeatedly need to:
|
||||
|
||||
- walk contours
|
||||
- split segments against row-band boundaries
|
||||
- compute left-of-band winding influence
|
||||
- rebuild scan-conversion state for the same shape over and over
|
||||
|
||||
That would push expensive geometry work into the hottest part of CPU rendering.
|
||||
|
||||
So the rasterizer solves a different problem:
|
||||
|
||||
it builds retained rasterizable geometry once, then executes compact band-local scanning work many times, cheaply.
|
||||
|
||||
That two-phase design is the core idea behind `DefaultRasterizer`.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
The rasterizer is a retained fixed-point polygon scanner.
|
||||
|
||||
Its central idea is:
|
||||
|
||||
> build band-local retained line data once, then execute fixed-point scan conversion from that retained data
|
||||
|
||||
This is why the rasterizer has two very different modes of work:
|
||||
|
||||
1. retained geometry building
|
||||
2. band execution
|
||||
|
||||
The first phase is a preparation phase. The second is the hot execution phase.
|
||||
|
||||
If that distinction is clear, the code becomes much easier to follow.
|
||||
|
||||
## The Most Important Terms
|
||||
|
||||
### Rasterizer
|
||||
|
||||
`DefaultRasterizer` is the geometry-to-coverage engine.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- converting prepared geometry into retained scan-conversion data
|
||||
- rasterizing retained band data with fixed-point arithmetic
|
||||
- emitting coverage rows
|
||||
|
||||
It is not responsible for:
|
||||
|
||||
- brush color generation
|
||||
- destination frame ownership
|
||||
- layer composition
|
||||
- deciding which scene items should execute
|
||||
|
||||
Those problems belong to the CPU backend and `FlushScene`.
|
||||
|
||||
### Retained Geometry
|
||||
|
||||
Retained geometry is the rasterizer's prepared execution payload.
|
||||
|
||||
In this codebase, retained geometry means:
|
||||
|
||||
"the fixed-point, band-local line data and start-cover seeds needed to rasterize one prepared shape later without revisiting its original contour data"
|
||||
|
||||
That retained form is stored in `RasterizableGeometry`.
|
||||
|
||||
### Band
|
||||
|
||||
A band is one small vertical slice of a shape's retained geometry.
|
||||
|
||||
The rasterizer does not keep one giant scene-wide edge table. It stores data in row bands so execution can stay local and bounded.
|
||||
|
||||
### Rasterizable Geometry
|
||||
|
||||
`RasterizableGeometry` is the retained representation of one prepared shape.
|
||||
|
||||
It stores:
|
||||
|
||||
- clipped local bounds
|
||||
- band-local metadata
|
||||
- retained line arrays
|
||||
- optional start-cover seeds for bands that need carry-in winding
|
||||
|
||||
This is the retained object that the CPU backend keeps in `FlushScene`.
|
||||
|
||||
### Rasterizable Band
|
||||
|
||||
A `RasterizableBand` is the execution-time view over one retained band of one retained shape.
|
||||
|
||||
It is the immediate input to `ExecuteRasterizableBand(...)`.
|
||||
|
||||
### Context
|
||||
|
||||
`DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution.
|
||||
|
||||
It is a `ref struct` because it is tied directly to worker-owned scratch spans and should not escape the execution scope.
|
||||
|
||||
### Coverage
|
||||
|
||||
Coverage is the rasterizer's output.
|
||||
|
||||
The rasterizer does not decide final pixel colors. It decides how much geometric coverage each pixel receives. The backend later passes that coverage to a `BrushRenderer<TPixel>`, which decides how the destination pixels should be shaded.
|
||||
|
||||
## Pipeline Placement
|
||||
|
||||
The rasterizer sits in the middle of the CPU backend pipeline.
|
||||
|
||||
Upstream:
|
||||
|
||||
- `CompositionCommand` preparation produces prepared geometry
|
||||
- the typed canvas implementation and `DrawingCanvasBatcher<TPixel>` have already selected and called the CPU backend
|
||||
- `FlushScene` decides which items are visible and when they execute
|
||||
|
||||
Downstream:
|
||||
|
||||
- the rasterizer emits row coverage
|
||||
- `DefaultDrawingBackend` routes that coverage into `BrushRenderer<TPixel>.Apply(...)`
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Prepared geometry] --> B[DefaultRasterizer.CreateRasterizableGeometry]
|
||||
B --> C[RasterizableGeometry]
|
||||
C --> D[Build RasterizableBand view]
|
||||
D --> E[ExecuteRasterizableBand]
|
||||
E --> F[Coverage rows]
|
||||
F --> G[Brush renderer]
|
||||
```
|
||||
|
||||
That placement is important. The rasterizer is neither the public drawing model nor the final shading model. It is the geometry-to-coverage step between them.
|
||||
|
||||
## Why The Rasterizer Has Two Phases
|
||||
|
||||
The rasterizer separates:
|
||||
|
||||
1. building retained geometry
|
||||
2. executing retained geometry
|
||||
|
||||
### Phase 1: retained geometry building
|
||||
|
||||
`CreateRasterizableGeometry(...)` converts prepared geometry into a retained representation that is cheap to execute later.
|
||||
|
||||
This phase:
|
||||
|
||||
- walks prepared contours
|
||||
- converts coordinates into fixed-point
|
||||
- clips or splits segments as needed for band boundaries
|
||||
- records visible line pieces into retained line storage
|
||||
- records left-of-band winding influence into start-cover tables
|
||||
|
||||
The output is `RasterizableGeometry`.
|
||||
|
||||
### Phase 2: band execution
|
||||
|
||||
`ExecuteRasterizableBand(...)` is the hot execution entry point.
|
||||
|
||||
It does not revisit the original contour data. It receives a `RasterizableBand` view over retained data and performs the minimum work needed to emit coverage rows for that band.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Exec as ExecuteRasterizableBand
|
||||
participant Ctx as Context
|
||||
participant Emit as Coverage Row Handler
|
||||
|
||||
Exec->>Ctx: Reconfigure(...)
|
||||
Exec->>Ctx: SeedStartCovers(...)
|
||||
Exec->>Ctx: Rasterize retained lines
|
||||
Exec->>Ctx: EmitCoverageRows(...)
|
||||
Ctx-->>Emit: coverage rows
|
||||
Exec->>Ctx: ResetTouchedRows()
|
||||
```
|
||||
|
||||
That separation is one of the key reasons the retained fill path performs well. Expensive geometry work happens once; execution consumes compact band-local data.
|
||||
|
||||
## Fixed-Point Precision
|
||||
|
||||
The rasterizer works in 24.8 fixed-point coordinates.
|
||||
|
||||
That means:
|
||||
|
||||
- `1` pixel = `256` fixed-point units
|
||||
- `FixedShift = 8`
|
||||
- `FixedOne = 256`
|
||||
|
||||
This gives the scanner subpixel precision while keeping the hot execution path integer-based. Geometry may begin as floating-point path data, but once a retained line reaches the scan-conversion core it is treated as fixed-point state.
|
||||
|
||||
Coverage is converted back into normalized `float` values only at the emission boundary.
|
||||
|
||||
## Why Bands Exist
|
||||
|
||||
The rasterizer does not retain one monolithic edge table. It retains geometry in vertical row bands.
|
||||
|
||||
That matters because it keeps execution local and bounded.
|
||||
|
||||
When a segment crosses multiple bands, the linearizer splits it so each band receives only the portion it must scan. If a segment influences winding inside the visible band from the left side, that influence is folded into a start-cover seed rather than keeping an invisible off-screen line around forever.
|
||||
|
||||
This gives the backend several important properties:
|
||||
|
||||
- execution only touches the band it is currently composing
|
||||
- left-of-band winding can be precomputed
|
||||
- scratch requirements stay bounded
|
||||
- row-oriented execution consumes compact band-local payloads
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Contour segment] --> B{Touches one band?}
|
||||
B -- Yes --> C[Store visible line in that band]
|
||||
B -- No --> D[Split across band boundaries]
|
||||
D --> E[Store band-local visible pieces]
|
||||
D --> F[Accumulate start-cover seeds where needed]
|
||||
```
|
||||
|
||||
## Retained Geometry: What Gets Stored
|
||||
|
||||
`RasterizableGeometry` stores the retained data needed to rasterize a prepared shape later.
|
||||
|
||||
That includes:
|
||||
|
||||
- the local bounds of the prepared shape
|
||||
- band count and band-local metadata
|
||||
- retained line arrays for each band
|
||||
- optional start-cover arrays for bands that need carry-in winding
|
||||
|
||||
The retained line arrays use specialized storage formats such as:
|
||||
|
||||
- `LineArrayX16Y16`
|
||||
- `LineArrayX32Y16`
|
||||
|
||||
These are storage-oriented types. They exist to retain compact fixed-point line segments so execution does not need to revisit contour data.
|
||||
|
||||
## The Linearizer
|
||||
|
||||
The linearizer is the retained-geometry builder. It is generic over line-array storage, but the conceptual work is the same across variants.
|
||||
|
||||
Its responsibilities are:
|
||||
|
||||
- traverse prepared contours
|
||||
- apply the residual transform per-point as contours are read
|
||||
- clip work to retained bounds
|
||||
- convert coordinates into fixed-point
|
||||
- decide whether a segment is contained or must be split
|
||||
- store visible line pieces
|
||||
- accumulate start covers for left-of-band influence
|
||||
|
||||
For a newcomer, the most important thing to understand is that the linearizer is not the hot coverage emitter. It is the preparation step that turns arbitrary contour geometry into a stable retained scanning payload.
|
||||
|
||||
### Residual transform application
|
||||
|
||||
The prepared `LinearGeometry` passed to `CreateRasterizableGeometry(...)` carries scale-baked points — the effective X/Y scale of the drawing matrix has already been absorbed into the flattened contour, so curve subdivision happens at device-scale precision. The remaining rotation, shear, translation, and perspective is handed to the rasterizer as a separate `Matrix4x4 residual`, which the linearizer applies per-point where the contour is read: at segment emission time in `ProcessContained` / `ProcessUncontained` for fills, and at bounds / closure / contour-segment construction sites in the stroke linearizer.
|
||||
|
||||
This split keeps the scale-baked geometry cacheable across frames (text and panning workloads reuse the same bake at a fixed zoom) while letting per-frame rotation or translation ride through the rasterizer without re-subdividing curves.
|
||||
|
||||
### Contained lines
|
||||
|
||||
A contained line is one whose fixed-point endpoints already fit the assumptions of the current retained band representation. Those lines can be pushed directly into retained storage after the required fixed-point and band-boundary handling.
|
||||
|
||||
### Split lines
|
||||
|
||||
When a line crosses band boundaries, the linearizer splits it so each band receives only the contribution it needs to scan.
|
||||
|
||||
### Start-cover seeding
|
||||
|
||||
When a line contributes winding inside the visible band but lies partially to the left of the visible X range, the retained geometry stores that influence in a start-cover array instead of retaining an off-screen line.
|
||||
|
||||
This is one of the most important ideas in the retained design:
|
||||
|
||||
- visible geometry becomes retained lines
|
||||
- invisible left-of-band winding becomes retained start-cover seeds
|
||||
|
||||
## The Execution Context
|
||||
|
||||
`DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution.
|
||||
|
||||
It owns per-band mutable state such as:
|
||||
|
||||
- `bitVectors`
|
||||
- `coverArea`
|
||||
- `startCover`
|
||||
- `rowMinTouchedColumn`
|
||||
- `rowMaxTouchedColumn`
|
||||
- `rowHasBits`
|
||||
- `rowTouched`
|
||||
- `touchedRows`
|
||||
|
||||
This state is reused across bands by reconfiguration, not by reallocation.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[WorkerScratch] --> B[Context]
|
||||
B --> C[Rasterize retained lines]
|
||||
C --> D[Mutate coverArea and bit vectors]
|
||||
D --> E[Emit coverage rows]
|
||||
E --> F[Reset touched rows]
|
||||
```
|
||||
|
||||
The `Context` bridges retained geometry and emitted coverage.
|
||||
|
||||
## How Coverage Accumulation Works
|
||||
|
||||
The rasterizer uses the classic area-and-cover formulation.
|
||||
|
||||
When a fixed-point line is rasterized, it is broken into cell contributions. Those contributions eventually reach `AddCell(...)`, which updates:
|
||||
|
||||
- delta cover
|
||||
- delta area
|
||||
|
||||
Rows also track sparse touched-column information through bit vectors, so the emitter can avoid scanning the full width of empty rows.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Rasterize fixed-point line] --> B[Decompose into touched cells]
|
||||
B --> C["AddCell(row, column, deltaCover, deltaArea)"]
|
||||
C --> D[Update coverArea]
|
||||
C --> E[Mark bitVectors]
|
||||
C --> F[Track touched rows and bounds]
|
||||
C --> G{column < 0?}
|
||||
G -- Yes --> H[Fold into startCover]
|
||||
G -- No --> I[Keep visible cell contribution]
|
||||
```
|
||||
|
||||
This is why the rasterizer can honor fill rules later. It accumulates signed contributions first and applies the fill rule during coverage emission.
|
||||
|
||||
## Coverage Emission
|
||||
|
||||
`EmitCoverageRows(...)` converts the accumulated fixed-point state into row spans.
|
||||
|
||||
For each touched row, the emitter:
|
||||
|
||||
1. starts from the seeded `startCover`
|
||||
2. walks the row's touched columns using the bit vectors
|
||||
3. updates the running cover from `deltaCover`
|
||||
4. combines running cover and `deltaArea` into signed area
|
||||
5. converts signed area into normalized coverage using the selected fill rule
|
||||
6. coalesces equal-coverage spans
|
||||
7. writes only non-zero spans into the reusable scanline buffer
|
||||
8. invokes the row callback
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Touched row] --> B[Walk set bits]
|
||||
B --> C[Reconstruct cover and area]
|
||||
C --> D[Apply fill rule]
|
||||
D --> E[Coalesce equal coverage]
|
||||
E --> F[Write compact scanline spans]
|
||||
F --> G[Invoke row handler]
|
||||
```
|
||||
|
||||
The rasterizer therefore emits only rows that actually received contributions and only the non-zero spans within those rows.
|
||||
|
||||
## Fill Rules
|
||||
|
||||
The rasterizer supports both `NonZero` and `EvenOdd`.
|
||||
|
||||
### NonZero
|
||||
|
||||
The accumulated signed area is treated as winding magnitude. Coverage is the clamped absolute value of that area.
|
||||
|
||||
### EvenOdd
|
||||
|
||||
The accumulated area is wrapped into the even-odd domain before coverage is produced. This gives parity-based behavior without changing the earlier scan-conversion logic.
|
||||
|
||||
The fill rule is therefore an emission-time decision, not a geometry-preprocessing decision.
|
||||
|
||||
## Antialiased And Aliased Modes
|
||||
|
||||
The rasterizer can emit either continuous or thresholded coverage.
|
||||
|
||||
- `Antialiased` mode keeps the continuous coverage produced by the area-and-cover math
|
||||
- `Aliased` mode thresholds that continuous coverage using `AntialiasThreshold`
|
||||
|
||||
The scan-conversion core stays the same in both modes. Only the final conversion from area to emitted coverage changes.
|
||||
|
||||
## Why Self-Intersections Work
|
||||
|
||||
The rasterizer can handle self-intersections because it does not require geometric boolean normalization before rasterization. It accumulates signed contributions and then applies the selected fill rule during emission.
|
||||
|
||||
That means overlapping or self-crossing contours are resolved by:
|
||||
|
||||
- area-and-cover integration
|
||||
- winding or parity mapping
|
||||
|
||||
instead of by an earlier polygon-boolean pass.
|
||||
|
||||
## How The Rasterizer Stays Separate From The Backend
|
||||
|
||||
The rasterizer and the backend solve different problems.
|
||||
|
||||
The rasterizer decides:
|
||||
|
||||
- how geometry contributes coverage
|
||||
- which rows and columns within a band are touched
|
||||
- how much coverage each emitted span has
|
||||
|
||||
The backend decides:
|
||||
|
||||
- which scene items execute
|
||||
- which retained band is being scanned
|
||||
- which destination slice receives the coverage
|
||||
- which brush renderer consumes the emitted spans
|
||||
|
||||
That separation is one of the main architectural advantages of the current CPU path.
|
||||
|
||||
## Reading Guide
|
||||
|
||||
If you are new to this part of the library, read the rasterizer in this order:
|
||||
|
||||
1. `DrawingCanvas.cs`
|
||||
2. `DrawingCanvas{TPixel}.cs`
|
||||
3. `DrawingCanvasBatcher{TPixel}.cs`
|
||||
4. `DefaultDrawingBackend.cs`
|
||||
5. `FlushScene.cs`
|
||||
6. `CreateRasterizableGeometry(...)` in `DefaultRasterizer.cs`
|
||||
7. `Linearizer<TL>` and the concrete linearizers in `DefaultRasterizer.Linearizer.cs`
|
||||
8. retained line types in `DefaultRasterizer.RetainedTypes.cs`
|
||||
9. `ExecuteRasterizableBand(...)` in `DefaultRasterizer.cs`
|
||||
10. `Context` in `DefaultRasterizer.cs`
|
||||
|
||||
That order mirrors the data lifecycle:
|
||||
|
||||
canvas intent -> prepared geometry -> retained storage -> band execution -> coverage emission
|
||||
|
||||
## The Mental Model To Keep
|
||||
|
||||
The easiest way to reason about `DefaultRasterizer` is this:
|
||||
|
||||
it is a retained fixed-point polygon scanner that transforms prepared geometry into compact band-local line payloads, then turns those payloads into row coverage spans.
|
||||
|
||||
If that model stays clear, the rest of the code becomes easier to read:
|
||||
|
||||
- the canvas and backend docs explain how execution reaches the CPU path
|
||||
- the linearizer explains where retained line data comes from
|
||||
- `RasterizableGeometry` explains what is stored
|
||||
- the `Context` explains how retained data becomes coverage
|
||||
- the backend explains how coverage becomes pixels
|
||||
@@ -0,0 +1,202 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Retained scene created by the CPU drawing backend.
|
||||
/// </summary>
|
||||
public sealed class DefaultDrawingBackendScene : DrawingBackendScene
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultDrawingBackendScene"/> class.
|
||||
/// </summary>
|
||||
/// <param name="scene">The retained CPU flush scene.</param>
|
||||
/// <param name="bounds">The target bounds used to create the scene.</param>
|
||||
/// <param name="ownedResources">Resources that must stay alive for the retained scene.</param>
|
||||
internal DefaultDrawingBackendScene(
|
||||
FlushScene scene,
|
||||
Rectangle bounds,
|
||||
IReadOnlyList<IDisposable>? ownedResources)
|
||||
: base(bounds, ownedResources)
|
||||
=> this.Scene = scene;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained CPU flush scene when this is a leaf scene.
|
||||
/// </summary>
|
||||
internal FlushScene? Scene { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DisposeCore()
|
||||
=> this.Scene?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Buffers;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Contract implemented by retained line-block payloads.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSelf">The concrete retained line-block type.</typeparam>
|
||||
internal interface ILineBlock<TSelf>
|
||||
where TSelf : class, ILineBlock<TSelf>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of lines stored in a full block.
|
||||
/// </summary>
|
||||
public static abstract int LineCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next block in the retained chain.
|
||||
/// </summary>
|
||||
public TSelf? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Rasterizes the leading <paramref name="count"/> lines from this block.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of leading lines to rasterize from this block.</param>
|
||||
/// <param name="context">The mutable scan-conversion context to write into.</param>
|
||||
public void Rasterize(int count, ref Context context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retained tile-space bounds for one linearized geometry payload.
|
||||
/// </summary>
|
||||
internal readonly struct TileBounds
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TileBounds"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="x">The tile-space left coordinate.</param>
|
||||
/// <param name="y">The tile-space top coordinate.</param>
|
||||
/// <param name="columnCount">The tile-space column count.</param>
|
||||
/// <param name="rowCount">The tile-space row count.</param>
|
||||
public TileBounds(int x, int y, int columnCount, int rowCount)
|
||||
{
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
this.ColumnCount = columnCount;
|
||||
this.RowCount = rowCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space left coordinate.
|
||||
/// </summary>
|
||||
public int X { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space top coordinate.
|
||||
/// </summary>
|
||||
public int Y { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space column count.
|
||||
/// </summary>
|
||||
public int ColumnCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space row count.
|
||||
/// </summary>
|
||||
public int RowCount { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the finalized retained raster payload for one line-block encoding.
|
||||
/// </summary>
|
||||
/// <typeparam name="TLineBlock">The concrete retained line-block type.</typeparam>
|
||||
internal sealed class LinearizedRasterData<TLineBlock>
|
||||
where TLineBlock : class, ILineBlock<TLineBlock>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearizedRasterData{TLineBlock}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="geometry">The source linear geometry.</param>
|
||||
/// <param name="bounds">The retained tile-space bounds.</param>
|
||||
/// <param name="lines">The retained line-block chain for each row band.</param>
|
||||
/// <param name="firstBlockLineCounts">The valid line count in each row's front block.</param>
|
||||
/// <param name="startCoverTable">The retained start-cover seeds for each row band.</param>
|
||||
public LinearizedRasterData(
|
||||
LinearGeometry geometry,
|
||||
TileBounds bounds,
|
||||
TLineBlock?[] lines,
|
||||
int[] firstBlockLineCounts,
|
||||
IMemoryOwner<int>?[] startCoverTable)
|
||||
{
|
||||
this.Geometry = geometry;
|
||||
this.Bounds = bounds;
|
||||
this.Lines = lines;
|
||||
this.FirstBlockLineCounts = firstBlockLineCounts;
|
||||
this.StartCoverTable = startCoverTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source linear geometry.
|
||||
/// </summary>
|
||||
public LinearGeometry Geometry { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained tile-space bounds.
|
||||
/// </summary>
|
||||
public TileBounds Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained line-block chain for each row band.
|
||||
/// </summary>
|
||||
public TLineBlock?[] Lines { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the valid front-block line count for each row band.
|
||||
/// </summary>
|
||||
public int[] FirstBlockLineCounts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover seeds for each row band.
|
||||
/// </summary>
|
||||
public IMemoryOwner<int>?[] StartCoverTable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the retained line blocks for one row band.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">The row band index to iterate.</param>
|
||||
/// <param name="context">The mutable scan-conversion context.</param>
|
||||
public void Iterate(int rowIndex, ref Context context)
|
||||
{
|
||||
int count = this.FirstBlockLineCounts[rowIndex];
|
||||
TLineBlock? lineBlock = this.Lines[rowIndex];
|
||||
while (lineBlock is not null)
|
||||
{
|
||||
lineBlock.Rasterize(count, ref context);
|
||||
lineBlock = lineBlock.Next;
|
||||
count = TLineBlock.LineCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,920 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class that lowers translated geometry into retained per-row line storage.
|
||||
/// </summary>
|
||||
/// <typeparam name="TL">The mutable per-row line collector type.</typeparam>
|
||||
private abstract class Linearizer<TL>
|
||||
where TL : class
|
||||
{
|
||||
private bool hasAnyCoverage;
|
||||
|
||||
protected Linearizer(
|
||||
LinearGeometry geometry,
|
||||
Matrix4x4 residual,
|
||||
int translateX,
|
||||
int translateY,
|
||||
int minX,
|
||||
int minY,
|
||||
int width,
|
||||
int height,
|
||||
int firstBandIndex,
|
||||
int rowBandCount,
|
||||
float samplingOffsetX,
|
||||
float samplingOffsetY,
|
||||
MemoryAllocator allocator)
|
||||
{
|
||||
this.Geometry = geometry;
|
||||
this.Residual = residual;
|
||||
this.HasResidual = !residual.IsIdentity;
|
||||
this.TranslateX = translateX;
|
||||
this.TranslateY = translateY;
|
||||
this.MinX = minX;
|
||||
this.MinY = minY;
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
this.FirstBandIndex = firstBandIndex;
|
||||
this.RowBandCount = rowBandCount;
|
||||
this.SamplingOffsetX = samplingOffsetX;
|
||||
this.SamplingOffsetY = samplingOffsetY;
|
||||
this.Allocator = allocator;
|
||||
this.BandTopStart = (firstBandIndex * PreferredRowHeight) - minY;
|
||||
this.FirstBlockLineCounts = new int[rowBandCount];
|
||||
this.LineCounts = new int[rowBandCount];
|
||||
this.StartCoverTable = new IMemoryOwner<int>?[rowBandCount];
|
||||
this.LineArrays = new TL?[rowBandCount];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source geometry being lowered.
|
||||
/// </summary>
|
||||
protected LinearGeometry Geometry { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the residual transform applied to each source point during emission.
|
||||
/// </summary>
|
||||
protected Matrix4x4 Residual { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether <see cref="Residual"/> is non-identity.
|
||||
/// </summary>
|
||||
protected bool HasResidual { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the translated X offset applied to the geometry.
|
||||
/// </summary>
|
||||
protected int TranslateX { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the translated Y offset applied to the geometry.
|
||||
/// </summary>
|
||||
protected int TranslateY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum destination X bound after clipping.
|
||||
/// </summary>
|
||||
protected int MinX { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum destination Y bound after clipping.
|
||||
/// </summary>
|
||||
protected int MinY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible destination width in pixels.
|
||||
/// </summary>
|
||||
protected int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible destination height in pixels.
|
||||
/// </summary>
|
||||
protected int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first retained row-band index touched by the geometry.
|
||||
/// </summary>
|
||||
protected int FirstBandIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of retained row bands owned by the geometry.
|
||||
/// </summary>
|
||||
protected int RowBandCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the horizontal sampling offset applied before fixed-point conversion.
|
||||
/// </summary>
|
||||
protected float SamplingOffsetX { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the vertical sampling offset applied before fixed-point conversion.
|
||||
/// </summary>
|
||||
protected float SamplingOffsetY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the allocator used for retained start-cover storage.
|
||||
/// </summary>
|
||||
protected MemoryAllocator Allocator { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the top offset, in whole pixels, of the first retained row band.
|
||||
/// </summary>
|
||||
protected int BandTopStart { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mutable per-row line collectors used during lowering.
|
||||
/// </summary>
|
||||
protected TL?[] LineArrays { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the valid front-block line count for each retained row band.
|
||||
/// </summary>
|
||||
protected int[] FirstBlockLineCounts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total retained line count for each row band.
|
||||
/// </summary>
|
||||
protected int[] LineCounts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover storage for each row band.
|
||||
/// </summary>
|
||||
protected IMemoryOwner<int>?[] StartCoverTable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether any retained payload was produced.
|
||||
/// </summary>
|
||||
protected ref bool HasAnyCoverage => ref this.hasAnyCoverage;
|
||||
|
||||
/// <summary>
|
||||
/// Executes the linearization pass and finalizes the retained row payloads.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> when any retained coverage was produced; otherwise <see langword="false"/>.</returns>
|
||||
protected virtual bool ProcessCore()
|
||||
{
|
||||
RectangleF translatedBounds = this.HasResidual
|
||||
? RectangleF.Transform(this.Geometry.Info.Bounds, this.Residual)
|
||||
: this.Geometry.Info.Bounds;
|
||||
translatedBounds.Offset(this.TranslateX + this.SamplingOffsetX - this.MinX, this.TranslateY + this.SamplingOffsetY - this.MinY);
|
||||
|
||||
bool contains =
|
||||
translatedBounds.Left >= 0F &&
|
||||
translatedBounds.Top >= 0F &&
|
||||
translatedBounds.Right <= this.Width &&
|
||||
translatedBounds.Bottom <= this.Height;
|
||||
|
||||
// Contained geometry can skip clipping and go straight to the fixed-point band splitter.
|
||||
if (contains)
|
||||
{
|
||||
this.ProcessContained();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Geometry that touches the interest edges needs clipping so start covers and line
|
||||
// segments still match the destination bounds seen by the rasterizer.
|
||||
this.ProcessUncontained();
|
||||
}
|
||||
|
||||
if (!this.hasAnyCoverage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.FinalizeLines();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizes geometry that is fully contained inside the destination interest.
|
||||
/// </summary>
|
||||
protected void ProcessContained()
|
||||
{
|
||||
SegmentEnumerator enumerator = this.Geometry.GetSegments();
|
||||
Matrix4x4 residual = this.Residual;
|
||||
bool hasResidual = this.HasResidual;
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
LinearSegment segment = enumerator.Current;
|
||||
PointF p0 = segment.Start;
|
||||
PointF p1 = segment.End;
|
||||
if (hasResidual)
|
||||
{
|
||||
p0 = PointF.Transform(p0, residual);
|
||||
p1 = PointF.Transform(p1, residual);
|
||||
}
|
||||
|
||||
this.AddContainedLineF24Dot8(
|
||||
FloatToFixed24Dot8(((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX),
|
||||
FloatToFixed24Dot8(((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY),
|
||||
FloatToFixed24Dot8(((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX),
|
||||
FloatToFixed24Dot8(((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizes geometry that intersects the destination interest bounds and requires clipping.
|
||||
/// </summary>
|
||||
protected void ProcessUncontained()
|
||||
{
|
||||
SegmentEnumerator enumerator = this.Geometry.GetSegments();
|
||||
Matrix4x4 residual = this.Residual;
|
||||
bool hasResidual = this.HasResidual;
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
LinearSegment segment = enumerator.Current;
|
||||
PointF p0 = segment.Start;
|
||||
PointF p1 = segment.End;
|
||||
if (hasResidual)
|
||||
{
|
||||
p0 = PointF.Transform(p0, residual);
|
||||
p1 = PointF.Transform(p1, residual);
|
||||
}
|
||||
|
||||
this.AddUncontainedLine(
|
||||
((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX,
|
||||
((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY,
|
||||
((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX,
|
||||
((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clips one geometry line against the destination interest and adds the retained result.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate in translated float space.</param>
|
||||
/// <param name="y0">The starting Y coordinate in translated float space.</param>
|
||||
/// <param name="x1">The ending X coordinate in translated float space.</param>
|
||||
/// <param name="y1">The ending Y coordinate in translated float space.</param>
|
||||
protected void AddUncontainedLine(float x0, float y0, float x1, float y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (y0 <= 0F && y1 <= 0F)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (y0 >= this.Height && y1 >= this.Height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (x0 >= this.Width && x1 >= this.Width)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (x0 == x1)
|
||||
{
|
||||
int x0c = Math.Clamp(FloatToFixed24Dot8(x0), 0, this.Width * FixedOne);
|
||||
int p0y = Math.Clamp(FloatToFixed24Dot8(y0), 0, this.Height * FixedOne);
|
||||
int p1y = Math.Clamp(FloatToFixed24Dot8(y1), 0, this.Height * FixedOne);
|
||||
|
||||
if (x0c == 0)
|
||||
{
|
||||
// Segments clipped fully to the left edge do not produce a visible line, but they
|
||||
// still change winding for rows they cross. Retain that effect as start covers.
|
||||
this.UpdateStartCoversClipped(p0y, p1y);
|
||||
this.hasAnyCoverage = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddContainedLineF24Dot8(x0c, p0y, x0c, p1y);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
double deltayV = Math.Abs(y1 - y0);
|
||||
double deltaxV = x1 - x0;
|
||||
double rx0 = x0;
|
||||
double ry0 = y0;
|
||||
double rx1 = x1;
|
||||
double ry1 = y1;
|
||||
|
||||
if (y1 > y0)
|
||||
{
|
||||
if (y0 < 0F)
|
||||
{
|
||||
double t = -y0 / deltayV;
|
||||
rx0 = x0 + (deltaxV * t);
|
||||
ry0 = 0D;
|
||||
}
|
||||
|
||||
if (y1 > this.Height)
|
||||
{
|
||||
double t = (this.Height - y0) / deltayV;
|
||||
rx1 = x0 + (deltaxV * t);
|
||||
ry1 = this.Height;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (y0 > this.Height)
|
||||
{
|
||||
double t = (y0 - this.Height) / deltayV;
|
||||
rx0 = x0 + (deltaxV * t);
|
||||
ry0 = this.Height;
|
||||
}
|
||||
|
||||
if (y1 < 0F)
|
||||
{
|
||||
double t = y0 / deltayV;
|
||||
rx1 = x0 + (deltaxV * t);
|
||||
ry1 = 0D;
|
||||
}
|
||||
}
|
||||
|
||||
if (rx0 >= this.Width && rx1 >= this.Width)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (rx0 > 0D && rx1 > 0D && rx0 < this.Width && rx1 < this.Width)
|
||||
{
|
||||
this.AddContainedLineF24Dot8(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rx0 <= 0D && rx1 <= 0D)
|
||||
{
|
||||
// A segment that stays left of the visible band contributes winding only.
|
||||
this.UpdateStartCoversClipped(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne));
|
||||
this.hasAnyCoverage = true;
|
||||
return;
|
||||
}
|
||||
|
||||
double deltayH = ry1 - ry0;
|
||||
double deltaxH = Math.Abs(rx1 - rx0);
|
||||
|
||||
if (rx1 > rx0)
|
||||
{
|
||||
double bx1 = rx1;
|
||||
double by1 = ry1;
|
||||
|
||||
if (rx1 > this.Width)
|
||||
{
|
||||
double t = (this.Width - rx0) / deltaxH;
|
||||
by1 = ry0 + (deltayH * t);
|
||||
bx1 = this.Width;
|
||||
}
|
||||
|
||||
if (rx0 < 0D)
|
||||
{
|
||||
double t = -rx0 / deltaxH;
|
||||
int a = Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne);
|
||||
int by = Math.Clamp(FloatToFixed24Dot8((float)(ry0 + (deltayH * t))), 0, this.Height * FixedOne);
|
||||
int cx = Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne);
|
||||
int cy = Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne);
|
||||
|
||||
this.UpdateStartCoversClipped(a, by);
|
||||
this.hasAnyCoverage = true;
|
||||
|
||||
// The visible portion begins exactly at x == 0 after the left-edge clip.
|
||||
this.AddContainedLineF24Dot8(0, by, cx, cy);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddContainedLineF24Dot8(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
double bx0 = rx0;
|
||||
double by0 = ry0;
|
||||
|
||||
if (rx0 > this.Width)
|
||||
{
|
||||
double t = (rx0 - this.Width) / deltaxH;
|
||||
by0 = ry0 + (deltayH * t);
|
||||
bx0 = this.Width;
|
||||
}
|
||||
|
||||
if (rx1 < 0D)
|
||||
{
|
||||
double t = rx0 / deltaxH;
|
||||
int ax = Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne);
|
||||
int ay = Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne);
|
||||
int by = Math.Clamp(FloatToFixed24Dot8((float)(ry0 + (deltayH * t))), 0, this.Height * FixedOne);
|
||||
int c = Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne);
|
||||
|
||||
// The right-to-left case mirrors the left-edge handling above: emit the
|
||||
// visible portion first, then retain the winding-only tail as start covers.
|
||||
this.AddContainedLineF24Dot8(ax, ay, 0, by);
|
||||
this.UpdateStartCoversClipped(by, c);
|
||||
this.hasAnyCoverage = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddContainedLineF24Dot8(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one fully-contained line segment in 24.8 fixed-point coordinates.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate.</param>
|
||||
/// <param name="y0">The starting Y coordinate.</param>
|
||||
/// <param name="x1">The ending X coordinate.</param>
|
||||
/// <param name="y1">The ending Y coordinate.</param>
|
||||
protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (x0 == x1)
|
||||
{
|
||||
if (y0 < y1)
|
||||
{
|
||||
this.VerticalDown(x0, y0, y1);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.VerticalUp(x0, y0, y1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int dx = Math.Abs(x1 - x0);
|
||||
int dy = Math.Abs(y1 - y0);
|
||||
if (dx > MaximumDelta || dy > MaximumDelta)
|
||||
{
|
||||
int mx = (x0 + x1) >> 1;
|
||||
int my = (y0 + y1) >> 1;
|
||||
this.AddContainedLineF24Dot8(x0, y0, mx, my);
|
||||
this.AddContainedLineF24Dot8(mx, my, x1, y1);
|
||||
return;
|
||||
}
|
||||
|
||||
int rowIndex0;
|
||||
int rowIndex1;
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
if (y0 < y1)
|
||||
{
|
||||
rowIndex0 = (y0 - bandTopStart) / bandHeight;
|
||||
rowIndex1 = ((y1 - 1) - bandTopStart) / bandHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
rowIndex0 = ((y0 - 1) - bandTopStart) / bandHeight;
|
||||
rowIndex1 = (y1 - bandTopStart) / bandHeight;
|
||||
}
|
||||
|
||||
if ((uint)rowIndex0 >= (uint)this.RowBandCount || (uint)rowIndex1 >= (uint)this.RowBandCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (rowIndex0 == rowIndex1)
|
||||
{
|
||||
int rowTop = bandTopStart + (rowIndex0 * bandHeight);
|
||||
this.AppendLine(rowIndex0, x0, y0 - rowTop, x1, y1 - rowTop);
|
||||
this.LineCounts[rowIndex0]++;
|
||||
this.hasAnyCoverage = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.SplitAcrossBands(x0, y0, x1, y1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the mutable line collector used for one row band.
|
||||
/// </summary>
|
||||
/// <returns>The mutable line collector.</returns>
|
||||
protected abstract TL CreateLineArray();
|
||||
|
||||
/// <summary>
|
||||
/// Appends one line segment into the retained row-band collector.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">The local row-band index.</param>
|
||||
/// <param name="x0">The starting X coordinate relative to the row band.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="x1">The ending X coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
protected abstract void AppendLine(int rowIndex, int x0, int y0, int x1, int y1);
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes the mutable collectors into the retained line-block representation.
|
||||
/// </summary>
|
||||
protected abstract void FinalizeLines();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mutable line collector for a row band, creating it on first use.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">The local row-band index.</param>
|
||||
/// <returns>The mutable line collector.</returns>
|
||||
protected TL GetOrCreateLineArray(int rowIndex)
|
||||
{
|
||||
TL? lineArray = this.LineArrays[rowIndex];
|
||||
if (lineArray is not null)
|
||||
{
|
||||
return lineArray;
|
||||
}
|
||||
|
||||
lineArray = this.CreateLineArray();
|
||||
this.LineArrays[rowIndex] = lineArray;
|
||||
return lineArray;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a downward vertical segment by delegating to the shared band-splitting path.
|
||||
/// </summary>
|
||||
/// <param name="x">The fixed-point X coordinate.</param>
|
||||
/// <param name="y0">The starting fixed-point Y coordinate.</param>
|
||||
/// <param name="y1">The ending fixed-point Y coordinate.</param>
|
||||
private void VerticalDown(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an upward vertical segment by delegating to the shared band-splitting path.
|
||||
/// </summary>
|
||||
/// <param name="x">The fixed-point X coordinate.</param>
|
||||
/// <param name="y0">The starting fixed-point Y coordinate.</param>
|
||||
/// <param name="y1">The ending fixed-point Y coordinate.</param>
|
||||
private void VerticalUp(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1);
|
||||
|
||||
/// <summary>
|
||||
/// Splits a contained line segment at row-band boundaries and appends each retained piece.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate.</param>
|
||||
/// <param name="y0">The starting Y coordinate.</param>
|
||||
/// <param name="x1">The ending X coordinate.</param>
|
||||
/// <param name="y1">The ending Y coordinate.</param>
|
||||
private void SplitAcrossBands(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
int dy = y1 - y0;
|
||||
int dx = x1 - x0;
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
int startBand = dy > 0 ? (y0 - bandTopStart) / bandHeight : ((y0 - 1) - bandTopStart) / bandHeight;
|
||||
int endBand = dy > 0 ? ((y1 - 1) - bandTopStart) / bandHeight : (y1 - bandTopStart) / bandHeight;
|
||||
int step = dy > 0 ? 1 : -1;
|
||||
int currentBand = startBand;
|
||||
int currentX = x0;
|
||||
int currentY = y0;
|
||||
|
||||
while (currentBand != endBand)
|
||||
{
|
||||
int bandBoundaryY = dy > 0 ? bandTopStart + ((currentBand + 1) * bandHeight) : bandTopStart + (currentBand * bandHeight);
|
||||
int deltaY = bandBoundaryY - currentY;
|
||||
int nextX = currentX + (int)(((long)dx * deltaY) / dy);
|
||||
int rowTop = bandTopStart + (currentBand * bandHeight);
|
||||
|
||||
// Each retained segment is stored in the local coordinate space of its owning band.
|
||||
this.AppendLine(currentBand, currentX, currentY - rowTop, nextX, bandBoundaryY - rowTop);
|
||||
this.LineCounts[currentBand]++;
|
||||
this.hasAnyCoverage = true;
|
||||
currentX = nextX;
|
||||
currentY = bandBoundaryY;
|
||||
currentBand += step;
|
||||
|
||||
if ((uint)currentBand >= (uint)this.RowBandCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int finalRowTop = bandTopStart + (endBand * bandHeight);
|
||||
this.AppendLine(endBand, currentX, currentY - finalRowTop, x1, y1 - finalRowTop);
|
||||
this.LineCounts[endBand]++;
|
||||
this.hasAnyCoverage = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates retained start-cover rows for a line that has been clipped against the visible band.
|
||||
/// </summary>
|
||||
/// <param name="y0">The clipped starting Y coordinate.</param>
|
||||
/// <param name="y1">The clipped ending Y coordinate.</param>
|
||||
private void UpdateStartCoversClipped(int y0, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (y0 < y1)
|
||||
{
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
int rowIndex0 = (y0 - bandTopStart) / bandHeight;
|
||||
int rowIndex1 = ((y1 - 1) - bandTopStart) / bandHeight;
|
||||
rowIndex0 = Math.Clamp(rowIndex0, 0, this.RowBandCount - 1);
|
||||
rowIndex1 = Math.Clamp(rowIndex1, 0, this.RowBandCount - 1);
|
||||
int fy0 = y0 - (bandTopStart + (rowIndex0 * bandHeight));
|
||||
int fy1 = y1 - (bandTopStart + (rowIndex1 * bandHeight));
|
||||
this.UpdateStartCovers(rowIndex0, fy0, rowIndex0 == rowIndex1 ? fy1 : bandHeight);
|
||||
for (int i = rowIndex0 + 1; i < rowIndex1; i++)
|
||||
{
|
||||
// Full interior bands receive a constant winding contribution.
|
||||
this.FillStartCovers(i, -FixedOne);
|
||||
}
|
||||
|
||||
if (rowIndex0 != rowIndex1)
|
||||
{
|
||||
this.UpdateStartCovers(rowIndex1, 0, fy1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
int rowIndex0 = ((y0 - 1) - bandTopStart) / bandHeight;
|
||||
int rowIndex1 = (y1 - bandTopStart) / bandHeight;
|
||||
rowIndex0 = Math.Clamp(rowIndex0, 0, this.RowBandCount - 1);
|
||||
rowIndex1 = Math.Clamp(rowIndex1, 0, this.RowBandCount - 1);
|
||||
int fy0 = y0 - (bandTopStart + (rowIndex0 * bandHeight));
|
||||
int fy1 = y1 - (bandTopStart + (rowIndex1 * bandHeight));
|
||||
this.UpdateStartCovers(rowIndex0, fy0, rowIndex0 == rowIndex1 ? fy1 : 0);
|
||||
for (int i = rowIndex0 - 1; i > rowIndex1; i--)
|
||||
{
|
||||
// Full interior bands receive a constant winding contribution.
|
||||
this.FillStartCovers(i, FixedOne);
|
||||
}
|
||||
|
||||
if (rowIndex0 != rowIndex1)
|
||||
{
|
||||
this.UpdateStartCovers(rowIndex1, bandHeight, fy1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills an entire retained start-cover row with a constant winding value.
|
||||
/// </summary>
|
||||
/// <param name="localBandIndex">The local row-band index.</param>
|
||||
/// <param name="value">The constant winding value to add.</param>
|
||||
private void FillStartCovers(int localBandIndex, int value)
|
||||
{
|
||||
IMemoryOwner<int>? owner = this.StartCoverTable[localBandIndex];
|
||||
if (owner is null)
|
||||
{
|
||||
owner = this.Allocator.Allocate<int>(PreferredRowHeight, AllocationOptions.Clean);
|
||||
this.StartCoverTable[localBandIndex] = owner;
|
||||
owner.Memory.Span[..PreferredRowHeight].Fill(value);
|
||||
return;
|
||||
}
|
||||
|
||||
Span<int> covers = owner.Memory.Span[..PreferredRowHeight];
|
||||
for (int i = 0; i < PreferredRowHeight; i++)
|
||||
{
|
||||
covers[i] += value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a retained start-cover row for one clipped vertical interval.
|
||||
/// </summary>
|
||||
/// <param name="localBandIndex">The local row-band index.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
private void UpdateStartCovers(int localBandIndex, int y0, int y1)
|
||||
{
|
||||
IMemoryOwner<int>? owner = this.StartCoverTable[localBandIndex];
|
||||
if (owner is null)
|
||||
{
|
||||
owner = this.Allocator.Allocate<int>(PreferredRowHeight, AllocationOptions.Clean);
|
||||
this.StartCoverTable[localBandIndex] = owner;
|
||||
}
|
||||
|
||||
Span<int> covers = owner.Memory.Span[..PreferredRowHeight];
|
||||
if (y0 < y1)
|
||||
{
|
||||
UpdateCoverTableDown(covers, y0, y1);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateCoverTableUp(covers, y0, y1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a downward winding contribution to one retained start-cover table.
|
||||
/// </summary>
|
||||
/// <param name="covers">The retained start-cover rows.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
private static void UpdateCoverTableDown(Span<int> covers, int y0, int y1)
|
||||
{
|
||||
int rowIndex0 = y0 >> FixedShift;
|
||||
int rowIndex1 = (y1 - 1) >> FixedShift;
|
||||
int fy0 = y0 - (rowIndex0 << FixedShift);
|
||||
int fy1 = y1 - (rowIndex1 << FixedShift);
|
||||
|
||||
if (rowIndex0 == rowIndex1)
|
||||
{
|
||||
covers[rowIndex0] -= fy1 - fy0;
|
||||
return;
|
||||
}
|
||||
|
||||
covers[rowIndex0] -= FixedOne - fy0;
|
||||
for (int i = rowIndex0 + 1; i < rowIndex1; i++)
|
||||
{
|
||||
covers[i] -= FixedOne;
|
||||
}
|
||||
|
||||
covers[rowIndex1] -= fy1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies an upward winding contribution to one retained start-cover table.
|
||||
/// </summary>
|
||||
/// <param name="covers">The retained start-cover rows.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
private static void UpdateCoverTableUp(Span<int> covers, int y0, int y1)
|
||||
{
|
||||
int rowIndex0 = (y0 - 1) >> FixedShift;
|
||||
int rowIndex1 = y1 >> FixedShift;
|
||||
int fy0 = y0 - (rowIndex0 << FixedShift);
|
||||
int fy1 = y1 - (rowIndex1 << FixedShift);
|
||||
|
||||
if (rowIndex0 == rowIndex1)
|
||||
{
|
||||
covers[rowIndex0] += fy0 - fy1;
|
||||
return;
|
||||
}
|
||||
|
||||
covers[rowIndex0] += fy0;
|
||||
for (int i = rowIndex0 - 1; i > rowIndex1; i--)
|
||||
{
|
||||
covers[i] += FixedOne;
|
||||
}
|
||||
|
||||
covers[rowIndex1] += FixedOne - fy1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizer that finalizes retained lines into the 32-bit-X encoding.
|
||||
/// </summary>
|
||||
private sealed class LinearizerX32Y16 : Linearizer<LineArrayX32Y16>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearizerX32Y16"/> class.
|
||||
/// </summary>
|
||||
public LinearizerX32Y16(
|
||||
LinearGeometry geometry,
|
||||
Matrix4x4 residual,
|
||||
int translateX,
|
||||
int translateY,
|
||||
int minX,
|
||||
int minY,
|
||||
int width,
|
||||
int height,
|
||||
int firstBandIndex,
|
||||
int rowBandCount,
|
||||
float samplingOffsetX,
|
||||
float samplingOffsetY,
|
||||
MemoryAllocator allocator)
|
||||
: base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator)
|
||||
=> this.FinalLines = new LineArrayX32Y16Block?[rowBandCount];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the finalized retained line blocks for each row band.
|
||||
/// </summary>
|
||||
public LineArrayX32Y16Block?[] FinalLines { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override LineArrayX32Y16 CreateLineArray() => new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1)
|
||||
=> this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void FinalizeLines()
|
||||
{
|
||||
for (int i = 0; i < this.RowBandCount; i++)
|
||||
{
|
||||
LineArrayX32Y16? lineArray = this.LineArrays[i];
|
||||
this.FinalLines[i] = lineArray?.GetFrontBlock();
|
||||
this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the 32-bit-X linearization pass and returns the retained result.
|
||||
/// </summary>
|
||||
/// <param name="result">The finalized retained raster data.</param>
|
||||
/// <returns><see langword="true"/> when retained coverage was produced; otherwise <see langword="false"/>.</returns>
|
||||
internal bool TryProcess(out LinearizedRasterData<LineArrayX32Y16Block> result)
|
||||
{
|
||||
if (!this.ProcessCore())
|
||||
{
|
||||
result = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new LinearizedRasterData<LineArrayX32Y16Block>(
|
||||
this.Geometry,
|
||||
new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount),
|
||||
this.FinalLines,
|
||||
this.FirstBlockLineCounts,
|
||||
this.StartCoverTable);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizer that finalizes retained lines into the packed 16-bit-X encoding.
|
||||
/// </summary>
|
||||
private sealed class LinearizerX16Y16 : Linearizer<LineArrayX16Y16>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearizerX16Y16"/> class.
|
||||
/// </summary>
|
||||
public LinearizerX16Y16(
|
||||
LinearGeometry geometry,
|
||||
Matrix4x4 residual,
|
||||
int translateX,
|
||||
int translateY,
|
||||
int minX,
|
||||
int minY,
|
||||
int width,
|
||||
int height,
|
||||
int firstBandIndex,
|
||||
int rowBandCount,
|
||||
float samplingOffsetX,
|
||||
float samplingOffsetY,
|
||||
MemoryAllocator allocator)
|
||||
: base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator)
|
||||
=> this.FinalLines = new LineArrayX16Y16Block?[rowBandCount];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the finalized retained line blocks for each row band.
|
||||
/// </summary>
|
||||
public LineArrayX16Y16Block?[] FinalLines { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override LineArrayX16Y16 CreateLineArray() => new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1)
|
||||
=> this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void FinalizeLines()
|
||||
{
|
||||
for (int i = 0; i < this.RowBandCount; i++)
|
||||
{
|
||||
LineArrayX16Y16? lineArray = this.LineArrays[i];
|
||||
this.FinalLines[i] = lineArray?.GetFrontBlock();
|
||||
this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the 16-bit-X linearization pass and returns the retained result.
|
||||
/// </summary>
|
||||
/// <param name="result">The finalized retained raster data.</param>
|
||||
/// <returns><see langword="true"/> when retained coverage was produced; otherwise <see langword="false"/>.</returns>
|
||||
internal bool TryProcess(out LinearizedRasterData<LineArrayX16Y16Block> result)
|
||||
{
|
||||
if (!this.ProcessCore())
|
||||
{
|
||||
result = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new LinearizedRasterData<LineArrayX16Y16Block>(
|
||||
this.Geometry,
|
||||
new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount),
|
||||
this.FinalLines,
|
||||
this.FirstBlockLineCounts,
|
||||
this.StartCoverTable);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Flush-scoped retained row-local raster payload for one prepared fill geometry.
|
||||
/// </summary>
|
||||
internal sealed class RasterizableGeometry : IDisposable
|
||||
{
|
||||
private readonly RasterizableBandInfo[] bandInfos;
|
||||
private readonly LineArrayX16Y16Block?[]? linesX16;
|
||||
private readonly LineArrayX32Y16Block?[]? linesX32;
|
||||
private readonly int[] firstBlockLineCounts;
|
||||
private readonly IMemoryOwner<int>?[] startCoverTable;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizableGeometry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="firstRowBandIndex">The first absolute row-band index touched by the geometry.</param>
|
||||
/// <param name="rowBandCount">The number of retained local row bands owned by the geometry.</param>
|
||||
/// <param name="width">The geometry-local visible band width in pixels.</param>
|
||||
/// <param name="wordsPerRow">The bit-vector width in machine words required by the geometry.</param>
|
||||
/// <param name="coverStride">The scanner cover/area stride required by the geometry.</param>
|
||||
/// <param name="bandHeight">The retained row-band height in pixels.</param>
|
||||
/// <param name="isX16">Indicates whether the geometry uses the narrow X16Y16 line encoding.</param>
|
||||
/// <param name="bandInfos">The retained metadata for each local row band.</param>
|
||||
/// <param name="linesX16">The retained narrow line chains for each local row band.</param>
|
||||
/// <param name="linesX32">The retained wide line chains for each local row band.</param>
|
||||
/// <param name="firstBlockLineCounts">The valid line count in each front retained block.</param>
|
||||
/// <param name="startCoverTable">The retained start-cover table for each local row band.</param>
|
||||
public RasterizableGeometry(
|
||||
int firstRowBandIndex,
|
||||
int rowBandCount,
|
||||
int width,
|
||||
int wordsPerRow,
|
||||
int coverStride,
|
||||
int bandHeight,
|
||||
bool isX16,
|
||||
RasterizableBandInfo[] bandInfos,
|
||||
LineArrayX16Y16Block?[]? linesX16,
|
||||
LineArrayX32Y16Block?[]? linesX32,
|
||||
int[] firstBlockLineCounts,
|
||||
IMemoryOwner<int>?[] startCoverTable)
|
||||
{
|
||||
this.FirstRowBandIndex = firstRowBandIndex;
|
||||
this.RowBandCount = rowBandCount;
|
||||
this.Width = width;
|
||||
this.WordsPerRow = wordsPerRow;
|
||||
this.CoverStride = coverStride;
|
||||
this.BandHeight = bandHeight;
|
||||
this.IsX16 = isX16;
|
||||
this.bandInfos = bandInfos;
|
||||
this.linesX16 = linesX16;
|
||||
this.linesX32 = linesX32;
|
||||
this.firstBlockLineCounts = firstBlockLineCounts;
|
||||
this.startCoverTable = startCoverTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first absolute row-band index touched by this geometry.
|
||||
/// </summary>
|
||||
public int FirstRowBandIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of retained local row bands owned by this geometry.
|
||||
/// </summary>
|
||||
public int RowBandCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the geometry-local visible band width in pixels.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bit-vector width in machine words required by this geometry.
|
||||
/// </summary>
|
||||
public int WordsPerRow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scanner cover/area stride required by this geometry.
|
||||
/// </summary>
|
||||
public int CoverStride { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained row-band height in pixels.
|
||||
/// </summary>
|
||||
public int BandHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this geometry uses Blaze's narrow X16Y16 line arrays.
|
||||
/// </summary>
|
||||
public bool IsX16 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> when the given local row band has retained coverage payload.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns><see langword="true"/> when the row band has retained coverage; otherwise <see langword="false"/>.</returns>
|
||||
public bool HasCoverage(int localRowIndex) => this.bandInfos[localRowIndex].HasCoverage;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained narrow line block chain for one local row.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained narrow line chain for the row.</returns>
|
||||
public LineArrayX16Y16Block? GetLinesX16ForRow(int localRowIndex) => this.linesX16![localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained wide line block chain for one local row.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained wide line chain for the row.</returns>
|
||||
public LineArrayX32Y16Block? GetLinesX32ForRow(int localRowIndex) => this.linesX32![localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of valid lines in the first retained block for a local row.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The valid line count in the front retained block.</returns>
|
||||
public int GetFirstBlockLineCountForRow(int localRowIndex) => this.firstBlockLineCounts[localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover table entry for a local row, if one exists.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained start-cover span for the row.</returns>
|
||||
public ReadOnlySpan<int> GetCoversForRow(int localRowIndex)
|
||||
{
|
||||
IMemoryOwner<int>? covers = this.startCoverTable[localRowIndex];
|
||||
return covers is null ? ReadOnlySpan<int>.Empty : covers.Memory.Span[..this.BandHeight];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover row payload without further interpretation, matching Blaze naming.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained start-cover span for the row.</returns>
|
||||
public ReadOnlySpan<int> GetActualCoversForRow(int localRowIndex) => this.GetCoversForRow(localRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets retained metadata for one local row band.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained band metadata.</returns>
|
||||
public RasterizableBandInfo GetBandInfo(int localRowIndex) => this.bandInfos[localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Releases the retained line blocks and start-cover storage.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.linesX16 is not null)
|
||||
{
|
||||
Array.Clear(this.linesX16);
|
||||
}
|
||||
|
||||
if (this.linesX32 is not null)
|
||||
{
|
||||
Array.Clear(this.linesX32);
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.startCoverTable.Length; i++)
|
||||
{
|
||||
this.startCoverTable[i]?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// References one retained rasterizable geometry row inside a prepared scene item.
|
||||
/// </summary>
|
||||
internal readonly struct RasterizableItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizableItem"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="rasterizable">The retained rasterizable geometry.</param>
|
||||
/// <param name="localRowIndex">The local row index within <paramref name="rasterizable"/>.</param>
|
||||
public RasterizableItem(RasterizableGeometry rasterizable, int localRowIndex)
|
||||
{
|
||||
this.Rasterizable = rasterizable;
|
||||
this.LocalRowIndex = localRowIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained rasterizable geometry.
|
||||
/// </summary>
|
||||
public RasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local row index within <see cref="Rasterizable"/>.
|
||||
/// </summary>
|
||||
public int LocalRowIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of lines stored in the first retained block for this row.
|
||||
/// </summary>
|
||||
/// <returns>The number of valid lines in the leading block.</returns>
|
||||
public int GetFirstBlockLineCount() => this.Rasterizable.GetFirstBlockLineCountForRow(this.LocalRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the 16-bit X retained line block for the row when the geometry uses the compact encoding.
|
||||
/// </summary>
|
||||
/// <returns>The retained block chain, or <see langword="null"/> when the row uses the 32-bit encoding.</returns>
|
||||
public LineArrayX16Y16Block? GetLineArrayX16() => this.Rasterizable.GetLinesX16ForRow(this.LocalRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the 32-bit X retained line block for the row when the geometry uses the wide encoding.
|
||||
/// </summary>
|
||||
/// <returns>The retained block chain, or <see langword="null"/> when the row uses the 16-bit encoding.</returns>
|
||||
public LineArrayX32Y16Block? GetLineArrayX32() => this.Rasterizable.GetLinesX32ForRow(this.LocalRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover seeds for the row.
|
||||
/// </summary>
|
||||
/// <returns>The retained start-cover span.</returns>
|
||||
public ReadOnlySpan<int> GetActualCovers() => this.Rasterizable.GetActualCoversForRow(this.LocalRowIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// References one retained stroke row inside a prepared scene item.
|
||||
/// </summary>
|
||||
internal readonly struct StrokeRasterizableItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokeRasterizableItem"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="rasterizable">The retained stroke rasterizable geometry.</param>
|
||||
/// <param name="localRowIndex">The local row index within <paramref name="rasterizable"/>.</param>
|
||||
public StrokeRasterizableItem(StrokeRasterizableGeometry rasterizable, int localRowIndex)
|
||||
{
|
||||
this.Rasterizable = rasterizable;
|
||||
this.LocalRowIndex = localRowIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained stroke rasterizable geometry.
|
||||
/// </summary>
|
||||
public StrokeRasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local row index within <see cref="Rasterizable"/>.
|
||||
/// </summary>
|
||||
public int LocalRowIndex { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata that describes one prepared rasterizable band.
|
||||
/// </summary>
|
||||
internal readonly struct RasterizableBandInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizableBandInfo"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="lineCount">The number of retained visible lines in the band.</param>
|
||||
/// <param name="bandHeight">The band height in pixels.</param>
|
||||
/// <param name="width">The visible band width in pixels.</param>
|
||||
/// <param name="wordsPerRow">The bit-vector width in machine words.</param>
|
||||
/// <param name="coverStride">The scanner cover/area stride.</param>
|
||||
/// <param name="destinationLeft">The absolute destination X coordinate of the band's left column.</param>
|
||||
/// <param name="destinationTop">The absolute destination Y coordinate of the band's top row.</param>
|
||||
/// <param name="intersectionRule">The fill rule used when resolving accumulated winding.</param>
|
||||
/// <param name="rasterizationMode">The rasterization mode used by the band.</param>
|
||||
/// <param name="antialiasThreshold">The aliased threshold used when the band runs in aliased mode.</param>
|
||||
/// <param name="hasStartCovers">Indicates whether the band has non-zero start-cover seeds.</param>
|
||||
public RasterizableBandInfo(
|
||||
int lineCount,
|
||||
int bandHeight,
|
||||
int width,
|
||||
int wordsPerRow,
|
||||
int coverStride,
|
||||
int destinationLeft,
|
||||
int destinationTop,
|
||||
IntersectionRule intersectionRule,
|
||||
RasterizationMode rasterizationMode,
|
||||
float antialiasThreshold,
|
||||
bool hasStartCovers)
|
||||
{
|
||||
this.LineCount = lineCount;
|
||||
this.BandHeight = bandHeight;
|
||||
this.Width = width;
|
||||
this.WordsPerRow = wordsPerRow;
|
||||
this.CoverStride = coverStride;
|
||||
this.DestinationLeft = destinationLeft;
|
||||
this.DestinationTop = destinationTop;
|
||||
this.IntersectionRule = intersectionRule;
|
||||
this.RasterizationMode = rasterizationMode;
|
||||
this.AntialiasThreshold = antialiasThreshold;
|
||||
this.HasStartCovers = hasStartCovers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of visible raster lines stored for the band.
|
||||
/// </summary>
|
||||
public int LineCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the band height in pixels.
|
||||
/// </summary>
|
||||
public int BandHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible band width in pixels.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bit-vector width in machine words.
|
||||
/// </summary>
|
||||
public int WordsPerRow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scanner cover/area stride.
|
||||
/// </summary>
|
||||
public int CoverStride { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination X coordinate of the band's left column.
|
||||
/// </summary>
|
||||
public int DestinationLeft { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination Y coordinate of the band's top row.
|
||||
/// </summary>
|
||||
public int DestinationTop { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fill rule used when resolving accumulated winding.
|
||||
/// </summary>
|
||||
public IntersectionRule IntersectionRule { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the coverage mode used by the band.
|
||||
/// </summary>
|
||||
public RasterizationMode RasterizationMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the aliased threshold used when the band runs in aliased mode.
|
||||
/// </summary>
|
||||
public float AntialiasThreshold { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the band has non-zero start-cover seeds.
|
||||
/// </summary>
|
||||
public bool HasStartCovers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the band would emit any coverage.
|
||||
/// </summary>
|
||||
public bool HasCoverage => this.LineCount > 0 || this.HasStartCovers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects retained line segments whose X coordinates require 32-bit storage.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX32Y16
|
||||
{
|
||||
private LineArrayX32Y16Block? current;
|
||||
private int count = LineArrayX32Y16Block.LineCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the front block in the retained line chain.
|
||||
/// </summary>
|
||||
/// <returns>The front retained block, or <see langword="null"/> when no lines were appended.</returns>
|
||||
public LineArrayX32Y16Block? GetFrontBlock() => this.current;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of valid lines in the front retained block.
|
||||
/// </summary>
|
||||
/// <returns>The number of valid front-block lines.</returns>
|
||||
public int GetFrontBlockLineCount() => this.current is null ? 0 : this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Appends one retained line to the chain.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y0">The starting Y coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="x1">The ending X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y1">The ending Y coordinate in 24.8 fixed-point.</param>
|
||||
public void AppendLine(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int packedY0Y1 = Pack(y0, y1);
|
||||
LineArrayX32Y16Block? block = this.current;
|
||||
int currentCount = this.count;
|
||||
if (currentCount < LineArrayX32Y16Block.LineCount)
|
||||
{
|
||||
block!.Set(currentCount, packedY0Y1, x0, x1);
|
||||
this.count = currentCount + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
LineArrayX32Y16Block next = new(block);
|
||||
next.Set(0, packedY0Y1, x0, x1);
|
||||
this.current = next;
|
||||
this.count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs two signed 16-bit fixed-point values into one 32-bit integer.
|
||||
/// </summary>
|
||||
/// <param name="lo">The low 16-bit value.</param>
|
||||
/// <param name="hi">The high 16-bit value.</param>
|
||||
/// <returns>The packed value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int Pack(int lo, int hi) => (lo & 0xFFFF) | (hi << 16);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one retained 32-bit-X line block.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX32Y16Block : ILineBlock<LineArrayX32Y16Block>
|
||||
{
|
||||
private const int BlockLineCount = 32;
|
||||
private PackedLineX32Y16Buffer lines;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LineArrayX32Y16Block"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">The next block in the retained chain.</param>
|
||||
public LineArrayX32Y16Block(LineArrayX32Y16Block? next) => this.Next = next;
|
||||
|
||||
/// <inheritdoc />
|
||||
public static int LineCount => BlockLineCount;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LineArrayX32Y16Block? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores one retained line into the block.
|
||||
/// </summary>
|
||||
/// <param name="index">The block-local line index.</param>
|
||||
/// <param name="packedY0Y1">The packed 16-bit Y endpoints.</param>
|
||||
/// <param name="x0">The starting X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="x1">The ending X coordinate in 24.8 fixed-point.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Set(int index, int packedY0Y1, int x0, int x1)
|
||||
{
|
||||
ref PackedLineX32Y16 line = ref this.lines[index];
|
||||
line.PackedY0Y1 = packedY0Y1;
|
||||
line.X0 = x0;
|
||||
line.X1 = x1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Rasterize(int count, ref Context context)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
PackedLineX32Y16 line = this.lines[i];
|
||||
context.RasterizeLineSegment(line.X0, UnpackLo(line.PackedY0Y1), line.X1, UnpackHi(line.PackedY0Y1));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the retained block chain and rasterizes each block in sequence.
|
||||
/// </summary>
|
||||
/// <param name="firstBlockLineCount">The number of valid lines stored in the front block.</param>
|
||||
/// <param name="context">The mutable scan-conversion context.</param>
|
||||
public void Iterate(int firstBlockLineCount, ref Context context)
|
||||
{
|
||||
int count = firstBlockLineCount;
|
||||
LineArrayX32Y16Block? lineBlock = this;
|
||||
while (lineBlock is not null)
|
||||
{
|
||||
lineBlock.Rasterize(count, ref context);
|
||||
lineBlock = lineBlock.Next;
|
||||
count = LineCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the low signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked low value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackLo(int packed) => (short)(packed & 0xFFFF);
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the high signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked high value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackHi(int packed) => packed >> 16;
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained 32-bit-X line record in block-local storage.
|
||||
/// </summary>
|
||||
private struct PackedLineX32Y16
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the packed Y endpoints.
|
||||
/// </summary>
|
||||
public int PackedY0Y1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting X coordinate.
|
||||
/// </summary>
|
||||
public int X0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ending X coordinate.
|
||||
/// </summary>
|
||||
public int X1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the fixed-capacity retained line payload inline with the block object.
|
||||
/// </summary>
|
||||
[InlineArray(BlockLineCount)]
|
||||
private struct PackedLineX32Y16Buffer
|
||||
{
|
||||
private PackedLineX32Y16 element0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects retained line segments whose X coordinates fit in packed 16-bit storage.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX16Y16
|
||||
{
|
||||
private LineArrayX16Y16Block? current;
|
||||
private int count = LineArrayX16Y16Block.LineCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the front block in the retained line chain.
|
||||
/// </summary>
|
||||
/// <returns>The front retained block, or <see langword="null"/> when no lines were appended.</returns>
|
||||
public LineArrayX16Y16Block? GetFrontBlock() => this.current;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of valid lines in the front retained block.
|
||||
/// </summary>
|
||||
/// <returns>The number of valid front-block lines.</returns>
|
||||
public int GetFrontBlockLineCount() => this.current is null ? 0 : this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Appends one retained line to the chain.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y0">The starting Y coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="x1">The ending X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y1">The ending Y coordinate in 24.8 fixed-point.</param>
|
||||
public void AppendLine(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int packedY0Y1 = Pack(y0, y1);
|
||||
int packedX0X1 = Pack(x0, x1);
|
||||
LineArrayX16Y16Block? block = this.current;
|
||||
int currentCount = this.count;
|
||||
if (currentCount < LineArrayX16Y16Block.LineCount)
|
||||
{
|
||||
block!.Set(currentCount, packedY0Y1, packedX0X1);
|
||||
this.count = currentCount + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
LineArrayX16Y16Block next = new(block);
|
||||
next.Set(0, packedY0Y1, packedX0X1);
|
||||
this.current = next;
|
||||
this.count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs two signed 16-bit fixed-point values into one 32-bit integer.
|
||||
/// </summary>
|
||||
/// <param name="lo">The low 16-bit value.</param>
|
||||
/// <param name="hi">The high 16-bit value.</param>
|
||||
/// <returns>The packed value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int Pack(int lo, int hi) => (lo & 0xFFFF) | (hi << 16);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one retained 16-bit-X line block.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX16Y16Block : ILineBlock<LineArrayX16Y16Block>
|
||||
{
|
||||
private const int BlockLineCount = 32;
|
||||
private PackedLineX16Y16Buffer lines;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LineArrayX16Y16Block"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">The next block in the retained chain.</param>
|
||||
public LineArrayX16Y16Block(LineArrayX16Y16Block? next) => this.Next = next;
|
||||
|
||||
/// <inheritdoc />
|
||||
public static int LineCount => BlockLineCount;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LineArrayX16Y16Block? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores one retained line into the block.
|
||||
/// </summary>
|
||||
/// <param name="index">The block-local line index.</param>
|
||||
/// <param name="packedY0Y1">The packed 16-bit Y endpoints.</param>
|
||||
/// <param name="packedX0X1">The packed 16-bit X endpoints.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Set(int index, int packedY0Y1, int packedX0X1)
|
||||
{
|
||||
ref PackedLineX16Y16 line = ref this.lines[index];
|
||||
line.PackedY0Y1 = packedY0Y1;
|
||||
line.PackedX0X1 = packedX0X1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Rasterize(int count, ref Context context)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
PackedLineX16Y16 line = this.lines[i];
|
||||
context.RasterizeLineSegment(
|
||||
UnpackLo(line.PackedX0X1),
|
||||
UnpackLo(line.PackedY0Y1),
|
||||
UnpackHi(line.PackedX0X1),
|
||||
UnpackHi(line.PackedY0Y1));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the retained block chain and rasterizes each block in sequence.
|
||||
/// </summary>
|
||||
/// <param name="firstBlockLineCount">The number of valid lines stored in the front block.</param>
|
||||
/// <param name="context">The mutable scan-conversion context.</param>
|
||||
public void Iterate(int firstBlockLineCount, ref Context context)
|
||||
{
|
||||
int count = firstBlockLineCount;
|
||||
LineArrayX16Y16Block? lineBlock = this;
|
||||
while (lineBlock is not null)
|
||||
{
|
||||
lineBlock.Rasterize(count, ref context);
|
||||
lineBlock = lineBlock.Next;
|
||||
count = LineCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the low signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked low value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackLo(int packed) => (short)(packed & 0xFFFF);
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the high signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked high value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackHi(int packed) => packed >> 16;
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained 16-bit-X line record in block-local storage.
|
||||
/// </summary>
|
||||
private struct PackedLineX16Y16
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the packed Y endpoints.
|
||||
/// </summary>
|
||||
public int PackedY0Y1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the packed X endpoints.
|
||||
/// </summary>
|
||||
public int PackedX0X1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the fixed-capacity retained line payload inline with the block object.
|
||||
/// </summary>
|
||||
[InlineArray(BlockLineCount)]
|
||||
private struct PackedLineX16Y16Buffer
|
||||
{
|
||||
private PackedLineX16Y16 element0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Base type for retained drawing backend scenes.
|
||||
/// </summary>
|
||||
public abstract class DrawingBackendScene : IDisposable
|
||||
{
|
||||
private readonly IReadOnlyList<IDisposable>? ownedResources;
|
||||
private bool isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingBackendScene"/> class.
|
||||
/// </summary>
|
||||
/// <param name="bounds">The target bounds used to create the scene.</param>
|
||||
/// <param name="ownedResources">Resources that must stay alive for the retained scene.</param>
|
||||
protected DrawingBackendScene(
|
||||
Rectangle bounds,
|
||||
IReadOnlyList<IDisposable>? ownedResources)
|
||||
{
|
||||
this.Bounds = bounds;
|
||||
this.ownedResources = ownedResources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target bounds used to create the scene.
|
||||
/// </summary>
|
||||
public Rectangle Bounds { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.DisposeCore();
|
||||
this.DisposeOwnedResources();
|
||||
this.isDisposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes backend-specific resources retained by this scene.
|
||||
/// </summary>
|
||||
protected virtual void DisposeCore()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources retained for image-brush commands in this scene.
|
||||
/// </summary>
|
||||
private void DisposeOwnedResources()
|
||||
{
|
||||
if (this.ownedResources is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.ownedResources.Count; i++)
|
||||
{
|
||||
this.ownedResources[i].Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// One prepared draw-order command batch consumed by a drawing backend.
|
||||
/// </summary>
|
||||
public readonly struct DrawingCommandBatch
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCommandBatch"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="commands">The draw-order scene commands.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command stream contains layer boundaries.</param>
|
||||
public DrawingCommandBatch(
|
||||
IReadOnlyList<CompositionSceneCommand> commands,
|
||||
bool hasLayers)
|
||||
{
|
||||
this.Commands = commands;
|
||||
this.HasLayers = hasLayers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCommandBatch"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="commands">The backing command buffer.</param>
|
||||
/// <param name="commandCount">The number of commands in the prepared batch.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command stream contains layer boundaries.</param>
|
||||
internal DrawingCommandBatch(
|
||||
CompositionSceneCommand[] commands,
|
||||
int commandCount,
|
||||
bool hasLayers)
|
||||
: this(new ArraySegment<CompositionSceneCommand>(commands, 0, commandCount), hasLayers)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCommandBatch"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="commands">The backing command buffer.</param>
|
||||
/// <param name="startIndex">The first command index.</param>
|
||||
/// <param name="commandCount">The number of commands in the prepared batch.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command stream contains layer boundaries.</param>
|
||||
internal DrawingCommandBatch(
|
||||
CompositionSceneCommand[] commands,
|
||||
int startIndex,
|
||||
int commandCount,
|
||||
bool hasLayers)
|
||||
: this(new ArraySegment<CompositionSceneCommand>(commands, startIndex, commandCount), hasLayers)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the draw-order scene commands.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CompositionSceneCommand> Commands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of draw-order commands in the scene.
|
||||
/// </summary>
|
||||
public int CommandCount => this.Commands.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this scene contains inline layer commands.
|
||||
/// </summary>
|
||||
public bool HasLayers { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Represents a flush-ready CPU scene built from retained row-local raster payload.
|
||||
/// </summary>
|
||||
internal sealed partial class FlushScene
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the retained row operation carried by a <see cref="SceneOperation"/>.
|
||||
/// </summary>
|
||||
internal enum SceneOperationKind : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// A retained fill item.
|
||||
/// </summary>
|
||||
FillItem = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A retained stroke item.
|
||||
/// </summary>
|
||||
StrokeItem = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Starts an isolated compositing layer.
|
||||
/// </summary>
|
||||
BeginLayer = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Ends the most recently opened layer.
|
||||
/// </summary>
|
||||
EndLayer = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained row operation.
|
||||
/// </summary>
|
||||
internal readonly struct SceneOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneOperation"/> struct for a draw item.
|
||||
/// </summary>
|
||||
/// <param name="kind">The retained draw operation kind.</param>
|
||||
/// <param name="itemIndex">The retained scene item index.</param>
|
||||
/// <param name="localRowIndex">The retained rasterizable row index.</param>
|
||||
public SceneOperation(SceneOperationKind kind, int itemIndex, int localRowIndex)
|
||||
{
|
||||
this.Kind = kind;
|
||||
this.ItemIndex = itemIndex;
|
||||
this.LocalRowIndex = localRowIndex;
|
||||
this.LayerBounds = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneOperation"/> struct for a layer control operation.
|
||||
/// </summary>
|
||||
/// <param name="kind">The layer operation kind.</param>
|
||||
/// <param name="layerBounds">The retained row-local layer bounds.</param>
|
||||
/// <param name="itemIndex">The retained layer-options index for begin-layer operations.</param>
|
||||
public SceneOperation(CompositionCommandKind kind, Rectangle layerBounds, int itemIndex)
|
||||
{
|
||||
this.Kind = kind == CompositionCommandKind.BeginLayer ? SceneOperationKind.BeginLayer : SceneOperationKind.EndLayer;
|
||||
this.ItemIndex = itemIndex;
|
||||
this.LocalRowIndex = -1;
|
||||
this.LayerBounds = layerBounds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operation kind.
|
||||
/// </summary>
|
||||
public SceneOperationKind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained scene item index for fill operations.
|
||||
/// </summary>
|
||||
public int ItemIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained rasterizable row index for fill operations.
|
||||
/// </summary>
|
||||
public int LocalRowIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained row-local layer bounds for layer operations.
|
||||
/// </summary>
|
||||
public Rectangle LayerBounds { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained scene row.
|
||||
/// </summary>
|
||||
internal readonly struct SceneRow : IDisposable
|
||||
{
|
||||
private readonly SceneOperationBlock? firstBlock;
|
||||
private readonly SceneOperationBlock? lastBlock;
|
||||
private readonly int rowBandIndex;
|
||||
private readonly int count;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneRow"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="firstBlock">The first retained row-item block.</param>
|
||||
/// <param name="lastBlock">The last retained row-item block.</param>
|
||||
/// <param name="rowBandIndex">The absolute row-band index represented by the row.</param>
|
||||
/// <param name="count">The number of retained operations in the row.</param>
|
||||
public SceneRow(SceneOperationBlock? firstBlock, SceneOperationBlock? lastBlock, int rowBandIndex, int count)
|
||||
{
|
||||
this.firstBlock = firstBlock;
|
||||
this.lastBlock = lastBlock;
|
||||
this.rowBandIndex = rowBandIndex;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute row-band index represented by this scene row.
|
||||
/// </summary>
|
||||
public int RowBandIndex => this.rowBandIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of row items in this scene row.
|
||||
/// </summary>
|
||||
public int Count => this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first retained row-item block.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? FirstBlock => this.firstBlock;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last retained row-item block.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? LastBlock => this.lastBlock;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the row storage.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
SceneOperationBlock? block = this.firstBlock;
|
||||
while (block is not null)
|
||||
{
|
||||
SceneOperationBlock? next = block.Next;
|
||||
block.Dispose();
|
||||
block = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends row items directly into allocator-backed row storage.
|
||||
/// </summary>
|
||||
private struct RowBuilder : IDisposable
|
||||
{
|
||||
private readonly MemoryAllocator allocator;
|
||||
private SceneOperationBlock? firstBlock;
|
||||
private SceneOperationBlock? lastBlock;
|
||||
private int count;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RowBuilder"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="allocator">The allocator used for row-block storage.</param>
|
||||
public RowBuilder(MemoryAllocator allocator)
|
||||
{
|
||||
this.allocator = allocator;
|
||||
this.firstBlock = null;
|
||||
this.lastBlock = null;
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the builder has been initialized.
|
||||
/// </summary>
|
||||
public readonly bool IsInitialized => this.allocator is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of operations appended to this builder.
|
||||
/// </summary>
|
||||
public readonly int Count => this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a row item.
|
||||
/// </summary>
|
||||
/// <param name="operation">The retained operation to append.</param>
|
||||
public void Append(SceneOperation operation)
|
||||
{
|
||||
if (this.lastBlock is null)
|
||||
{
|
||||
SceneOperationBlock block = new(this.allocator);
|
||||
block.Append(operation);
|
||||
this.firstBlock = block;
|
||||
this.lastBlock = block;
|
||||
this.count++;
|
||||
return;
|
||||
}
|
||||
|
||||
SceneOperationBlock current = this.lastBlock;
|
||||
if (current.Count < SceneOperationBlock.ItemsPerBlock)
|
||||
{
|
||||
current.Append(operation);
|
||||
this.count++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Once a row block fills, link a fresh fixed-capacity block instead of reallocating
|
||||
// and copying existing operations. This keeps the retained row builder append-only.
|
||||
SceneOperationBlock next = new(this.allocator);
|
||||
next.Append(operation);
|
||||
current.Next = next;
|
||||
next.Previous = current;
|
||||
this.lastBlock = next;
|
||||
this.count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the retained blocks owned by <paramref name="source"/> to <paramref name="destination"/>
|
||||
/// without copying individual operations.
|
||||
/// </summary>
|
||||
/// <param name="destination">The builder receiving the appended blocks.</param>
|
||||
/// <param name="source">The builder supplying the appended blocks.</param>
|
||||
public static void AppendBuilder(ref RowBuilder destination, ref RowBuilder source)
|
||||
{
|
||||
if (source.firstBlock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (destination.firstBlock is null)
|
||||
{
|
||||
destination = source;
|
||||
source = default;
|
||||
return;
|
||||
}
|
||||
|
||||
destination.lastBlock!.Next = source.firstBlock;
|
||||
source.firstBlock.Previous = destination.lastBlock;
|
||||
destination.lastBlock = source.lastBlock;
|
||||
destination.count += source.count;
|
||||
source = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes the builder into retained scene storage.
|
||||
/// </summary>
|
||||
/// <param name="rowBandIndex">The absolute row-band index represented by the row.</param>
|
||||
/// <returns>The finalized retained row.</returns>
|
||||
public readonly SceneRow Finalize(int rowBandIndex) => new(this.firstBlock, this.lastBlock, rowBandIndex, this.count);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes unfinalized storage.
|
||||
/// </summary>
|
||||
public readonly void Dispose()
|
||||
{
|
||||
SceneOperationBlock? block = this.firstBlock;
|
||||
while (block is not null)
|
||||
{
|
||||
SceneOperationBlock? next = block.Next;
|
||||
block.Dispose();
|
||||
block = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one fixed-capacity row-item block.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This mirrors Blaze's <c>RowItemList<T>::Block</c> shape: append into the current block,
|
||||
/// allocate a fresh block only when that block fills, and never reallocate or copy existing blocks.
|
||||
/// </remarks>
|
||||
internal sealed class SceneOperationBlock : IDisposable
|
||||
{
|
||||
private readonly IMemoryOwner<SceneOperation> owner;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneOperationBlock"/> class.
|
||||
/// </summary>
|
||||
/// <param name="allocator">The allocator used for block storage.</param>
|
||||
public SceneOperationBlock(MemoryAllocator allocator)
|
||||
=> this.owner = allocator.Allocate<SceneOperation>(ItemsPerBlock);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fixed item capacity per block.
|
||||
/// </summary>
|
||||
public static int ItemsPerBlock => 32;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the previous block in the row list.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? Previous { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the next block in the row list.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? Next { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of items written into this block.
|
||||
/// </summary>
|
||||
public int Count { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the items written into this block.
|
||||
/// </summary>
|
||||
public Span<SceneOperation> Items => this.owner.Memory.Span[..this.Count];
|
||||
|
||||
/// <summary>
|
||||
/// Appends an item into this block.
|
||||
/// </summary>
|
||||
/// <param name="operation">The retained operation to append.</param>
|
||||
public void Append(SceneOperation operation) => this.owner.Memory.Span[this.Count++] = operation;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the block storage.
|
||||
/// </summary>
|
||||
public void Dispose() => this.owner.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained fill scene item.
|
||||
/// </summary>
|
||||
internal sealed class FillSceneItem : IDisposable
|
||||
{
|
||||
private object? renderer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FillSceneItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush used by the fill item.</param>
|
||||
/// <param name="graphicsOptions">The graphics options used by the fill item.</param>
|
||||
/// <param name="brushBounds">The brush bounds used for applicator creation.</param>
|
||||
/// <param name="rasterizable">The retained rasterizable geometry.</param>
|
||||
public FillSceneItem(
|
||||
Brush brush,
|
||||
GraphicsOptions graphicsOptions,
|
||||
Rectangle brushBounds,
|
||||
DefaultRasterizer.RasterizableGeometry rasterizable)
|
||||
{
|
||||
this.Brush = brush;
|
||||
this.GraphicsOptions = graphicsOptions;
|
||||
this.BrushBounds = brushBounds;
|
||||
this.Rasterizable = rasterizable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used by the fill item.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options used by the fill item.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush bounds used for applicator creation.
|
||||
/// </summary>
|
||||
public Rectangle BrushBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained rasterizable geometry.
|
||||
/// </summary>
|
||||
public DefaultRasterizer.RasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memoized renderer for this scene item, creating it on first use.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="canvasWidth">The destination canvas width.</param>
|
||||
/// <returns>The memoized renderer for the scene item.</returns>
|
||||
public BrushRenderer<TPixel> GetRenderer<TPixel>(Configuration configuration, int canvasWidth)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
if (this.renderer is BrushRenderer<TPixel> typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
typed = this.Brush.CreateRenderer<TPixel>(
|
||||
configuration,
|
||||
this.GraphicsOptions,
|
||||
canvasWidth,
|
||||
this.BrushBounds);
|
||||
|
||||
this.renderer = typed;
|
||||
return typed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => this.Rasterizable.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained stroke scene item.
|
||||
/// </summary>
|
||||
internal sealed class StrokeSceneItem : IDisposable
|
||||
{
|
||||
private object? renderer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokeSceneItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="brush">The prepared brush for the stroke item.</param>
|
||||
/// <param name="graphicsOptions">The graphics options for the stroke item.</param>
|
||||
/// <param name="brushBounds">The prepared brush bounds.</param>
|
||||
/// <param name="rasterizable">The retained stroke rasterizable geometry.</param>
|
||||
public StrokeSceneItem(
|
||||
Brush brush,
|
||||
GraphicsOptions graphicsOptions,
|
||||
Rectangle brushBounds,
|
||||
DefaultRasterizer.StrokeRasterizableGeometry rasterizable)
|
||||
{
|
||||
this.Brush = brush;
|
||||
this.GraphicsOptions = graphicsOptions;
|
||||
this.BrushBounds = brushBounds;
|
||||
this.Rasterizable = rasterizable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prepared brush for the stroke item.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options for the stroke item.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prepared brush bounds for the stroke item.
|
||||
/// </summary>
|
||||
public Rectangle BrushBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained stroke rasterizable geometry.
|
||||
/// </summary>
|
||||
public DefaultRasterizer.StrokeRasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memoized renderer for this scene item, creating it on first use.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="canvasWidth">The destination canvas width.</param>
|
||||
/// <returns>The memoized renderer for the scene item.</returns>
|
||||
public BrushRenderer<TPixel> GetRenderer<TPixel>(Configuration configuration, int canvasWidth)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
if (this.renderer is BrushRenderer<TPixel> typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
typed = this.Brush.CreateRenderer<TPixel>(
|
||||
configuration,
|
||||
this.GraphicsOptions,
|
||||
canvasWidth,
|
||||
this.BrushBounds);
|
||||
|
||||
this.renderer = typed;
|
||||
return typed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => this.Rasterizable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Per-frame destination for <see cref="DrawingCanvas{TPixel}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public interface ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the frame bounds in root target coordinates.
|
||||
/// </summary>
|
||||
public Rectangle Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get a CPU-accessible destination region.
|
||||
/// </summary>
|
||||
/// <param name="region">The CPU region when available.</param>
|
||||
/// <returns><see langword="true"/> when a CPU region is available.</returns>
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get an opaque native destination surface.
|
||||
/// </summary>
|
||||
/// <param name="surface">The native surface when available.</param>
|
||||
/// <returns><see langword="true"/> when a native surface is available.</returns>
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Defines the contract for creating and rendering retained drawing scenes for canvas targets.
|
||||
/// </summary>
|
||||
public interface IDrawingBackend
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a retained backend scene from a prepared command batch.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="targetBounds">The target bounds used for target-dependent scene data.</param>
|
||||
/// <param name="commandBatch">The scene commands in submission order.</param>
|
||||
/// <param name="ownedResources">The resources that must stay alive for the returned scene.</param>
|
||||
/// <returns>A retained backend scene.</returns>
|
||||
public DrawingBackendScene CreateScene(
|
||||
Configuration configuration,
|
||||
Rectangle targetBounds,
|
||||
DrawingCommandBatch commandBatch,
|
||||
IReadOnlyList<IDisposable>? ownedResources = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a retained backend scene into the target.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="target">The target frame.</param>
|
||||
/// <param name="scene">The retained backend scene to render.</param>
|
||||
public void RenderScene<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> target,
|
||||
DrawingBackendScene scene)
|
||||
where TPixel : unmanaged, IPixel<TPixel>;
|
||||
|
||||
/// <summary>
|
||||
/// Reads source pixels from the target into the destination region.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="target">The target frame.</param>
|
||||
/// <param name="sourceRectangle">The source rectangle in target-local coordinates.</param>
|
||||
/// <param name="destination">The destination region that receives the copied pixels.</param>
|
||||
public void ReadRegion<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> target,
|
||||
Rectangle sourceRectangle,
|
||||
Buffer2DRegion<TPixel> destination)
|
||||
where TPixel : unmanaged, IPixel<TPixel>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Receives one emitted non-zero coverage span from the rasterizer.
|
||||
/// </summary>
|
||||
internal interface IRasterizerCoverageRowHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles one emitted non-zero coverage span.
|
||||
/// </summary>
|
||||
/// <param name="y">The destination y coordinate.</param>
|
||||
/// <param name="startX">The first x coordinate represented by <paramref name="coverage"/>.</param>
|
||||
/// <param name="coverage">Non-zero coverage values starting at <paramref name="startX"/>.</param>
|
||||
public void Handle(int y, int startX, Span<float> coverage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Canvas frame backed by a <see cref="Buffer2DRegion{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public sealed class MemoryCanvasFrame<TPixel> : ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly Buffer2DRegion<TPixel> region;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemoryCanvasFrame{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="region">The pixel buffer region backing this frame.</param>
|
||||
public MemoryCanvasFrame(Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
Guard.NotNull(region.Buffer, nameof(region));
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Rectangle Bounds => this.region.Bounds;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
region = this.region;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface)
|
||||
{
|
||||
surface = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Canvas frame backed by a <see cref="NativeSurface"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public sealed class NativeCanvasFrame<TPixel> : ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly NativeSurface surface;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NativeCanvasFrame{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="bounds">The frame bounds.</param>
|
||||
/// <param name="surface">The native surface backing this frame.</param>
|
||||
public NativeCanvasFrame(Rectangle bounds, NativeSurface surface)
|
||||
{
|
||||
Guard.NotNull(surface, nameof(surface));
|
||||
Guard.MustBeGreaterThan(bounds.Width, 0, nameof(bounds));
|
||||
Guard.MustBeGreaterThan(bounds.Height, 0, nameof(bounds));
|
||||
|
||||
this.Bounds = bounds;
|
||||
this.surface = surface;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Rectangle Bounds { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
region = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface)
|
||||
{
|
||||
surface = this.surface;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Base type for backend-specific native drawing targets.
|
||||
/// </summary>
|
||||
public abstract class NativeSurface
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NativeSurface"/> class.
|
||||
/// </summary>
|
||||
protected NativeSurface()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Centralizes the conversion from configuration parallelism settings to partition counts and
|
||||
/// <see cref="ParallelOptions"/> instances used by retained-scene CPU execution paths.
|
||||
/// </summary>
|
||||
internal static class ParallelExecutionHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the number of partitions to schedule for work constrained by a single work-item limit.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> leaves the runtime
|
||||
/// parallelism cap unbounded, but partition planning remains capped to
|
||||
/// <see cref="Environment.ProcessorCount"/> to avoid excessive fan-out.
|
||||
/// </param>
|
||||
/// <param name="workItemCount">The total number of work items available for partitioning.</param>
|
||||
/// <returns>The number of partitions to schedule.</returns>
|
||||
public static int GetPartitionCount(int maxDegreeOfParallelism, int workItemCount)
|
||||
=> Math.Min(GetPartitionLimit(maxDegreeOfParallelism), workItemCount);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the number of partitions to schedule for work constrained by two independent limits.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> leaves the runtime
|
||||
/// parallelism cap unbounded, but partition planning remains capped to
|
||||
/// <see cref="Environment.ProcessorCount"/> to avoid excessive fan-out.
|
||||
/// </param>
|
||||
/// <param name="workItemCount">The total number of work items available for partitioning.</param>
|
||||
/// <param name="secondaryLimit">An additional caller-specific upper bound on useful partitions.</param>
|
||||
/// <returns>The number of partitions to schedule.</returns>
|
||||
public static int GetPartitionCount(int maxDegreeOfParallelism, int workItemCount, int secondaryLimit)
|
||||
=> Math.Min(GetPartitionLimit(maxDegreeOfParallelism), Math.Min(workItemCount, secondaryLimit));
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="ParallelOptions"/> for a partitioned operation.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> retains the runtime's
|
||||
/// unbounded sentinel because <paramref name="partitionCount"/> is always positive; positive
|
||||
/// values are capped to the smaller of the configured limit and the useful partition count.
|
||||
/// </param>
|
||||
/// <param name="partitionCount">The computed positive number of useful partitions for the operation.</param>
|
||||
/// <returns>The <see cref="ParallelOptions"/> instance for the operation.</returns>
|
||||
public static ParallelOptions CreateParallelOptions(int maxDegreeOfParallelism, int partitionCount)
|
||||
=> new() { MaxDegreeOfParallelism = Math.Min(maxDegreeOfParallelism, partitionCount) };
|
||||
|
||||
/// <summary>
|
||||
/// Computes the internal partition-planning cap for the configured parallelism setting.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> keeps the runtime
|
||||
/// parallelism setting unbounded, but partition planning is capped to
|
||||
/// <see cref="Environment.ProcessorCount"/>.
|
||||
/// </param>
|
||||
/// <returns>The maximum number of partitions to plan for.</returns>
|
||||
private static int GetPartitionLimit(int maxDegreeOfParallelism)
|
||||
=> maxDegreeOfParallelism == -1 ? Environment.ProcessorCount : maxDegreeOfParallelism;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Describes whether rasterizers should emit continuous coverage or binary aliased coverage.
|
||||
/// </summary>
|
||||
public enum RasterizationMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Emit continuous coverage in the range [0, 1].
|
||||
/// </summary>
|
||||
Antialiased = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Emit binary coverage values (0 or 1).
|
||||
/// </summary>
|
||||
Aliased = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes where sample coverage is aligned relative to destination pixels.
|
||||
/// </summary>
|
||||
public enum RasterizerSamplingOrigin
|
||||
{
|
||||
/// <summary>
|
||||
/// Samples are aligned to pixel boundaries.
|
||||
/// </summary>
|
||||
PixelBoundary = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Samples are aligned to pixel centers.
|
||||
/// </summary>
|
||||
PixelCenter = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immutable options used by rasterizers when scan-converting vector geometry.
|
||||
/// </summary>
|
||||
public readonly struct RasterizerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizerOptions"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="interest">Destination bounds to rasterize into.</param>
|
||||
/// <param name="intersectionRule">Polygon intersection rule.</param>
|
||||
/// <param name="rasterizationMode">Rasterization coverage mode.</param>
|
||||
/// <param name="samplingOrigin">Sampling origin alignment.</param>
|
||||
/// <param name="antialiasThreshold">Coverage threshold for aliased mode (0 to 1).</param>
|
||||
public RasterizerOptions(
|
||||
Rectangle interest,
|
||||
IntersectionRule intersectionRule,
|
||||
RasterizationMode rasterizationMode,
|
||||
RasterizerSamplingOrigin samplingOrigin,
|
||||
float antialiasThreshold)
|
||||
{
|
||||
this.Interest = interest;
|
||||
this.IntersectionRule = intersectionRule;
|
||||
this.RasterizationMode = rasterizationMode;
|
||||
this.SamplingOrigin = samplingOrigin;
|
||||
this.AntialiasThreshold = antialiasThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets destination bounds to rasterize into.
|
||||
/// </summary>
|
||||
public Rectangle Interest { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the polygon intersection rule.
|
||||
/// </summary>
|
||||
public IntersectionRule IntersectionRule { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rasterization coverage mode.
|
||||
/// </summary>
|
||||
public RasterizationMode RasterizationMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sampling origin alignment.
|
||||
/// </summary>
|
||||
public RasterizerSamplingOrigin SamplingOrigin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the coverage threshold used when <see cref="RasterizationMode"/> is <see cref="RasterizationMode.Aliased"/>.
|
||||
/// Pixels with coverage above this value are rendered as fully opaque; pixels below are discarded.
|
||||
/// </summary>
|
||||
public float AntialiasThreshold { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the current options with a different interest rectangle.
|
||||
/// </summary>
|
||||
/// <param name="interest">The replacement interest rectangle.</param>
|
||||
/// <returns>A new <see cref="RasterizerOptions"/> value.</returns>
|
||||
public RasterizerOptions WithInterest(Rectangle interest)
|
||||
=> new(interest, this.IntersectionRule, this.RasterizationMode, this.SamplingOrigin, this.AntialiasThreshold);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// One explicit stroked two-point line-segment command queued by the canvas batcher.
|
||||
/// </summary>
|
||||
public readonly struct StrokeLineSegmentCommand
|
||||
{
|
||||
private readonly PointF sourceStart;
|
||||
private readonly PointF sourceEnd;
|
||||
private readonly DrawingOptions drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokeLineSegmentCommand"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="sourceStart">The source line start point.</param>
|
||||
/// <param name="sourceEnd">The source line end point.</param>
|
||||
/// <param name="brush">The brush used to shade the stroke.</param>
|
||||
/// <param name="drawingOptions">The drawing options (graphics, shape, transform) used during composition.</param>
|
||||
/// <param name="rasterizerOptions">The rasterizer options used to generate coverage.</param>
|
||||
/// <param name="targetBounds">The absolute bounds of the logical target.</param>
|
||||
/// <param name="destinationOffset">The absolute destination offset of the command.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
|
||||
public StrokeLineSegmentCommand(
|
||||
PointF sourceStart,
|
||||
PointF sourceEnd,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
Pen pen,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
this.sourceStart = sourceStart;
|
||||
this.sourceEnd = sourceEnd;
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.Brush = brush;
|
||||
this.RasterizerOptions = rasterizerOptions;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.Pen = pen;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used during composition.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options carried by the command.
|
||||
/// </summary>
|
||||
public DrawingOptions DrawingOptions => this.drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options used during composition.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rasterizer options used to generate coverage.
|
||||
/// </summary>
|
||||
public RasterizerOptions RasterizerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the logical target for this command.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset where the local coverage should be composited.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stroke metadata for this command.
|
||||
/// </summary>
|
||||
public Pen Pen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source line start point.
|
||||
/// </summary>
|
||||
public PointF SourceStart => this.sourceStart;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source line end point.
|
||||
/// </summary>
|
||||
public PointF SourceEnd => this.sourceEnd;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform => this.drawingOptions.Transform;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Computes the conservative stroked bounds of one two-point line segment.
|
||||
/// </summary>
|
||||
/// <param name="start">The line start point.</param>
|
||||
/// <param name="end">The line end point.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <returns>The conservative stroked bounds.</returns>
|
||||
public static RectangleF GetConservativeBounds(PointF start, PointF end, Pen pen)
|
||||
{
|
||||
float left = MathF.Min(start.X, end.X);
|
||||
float top = MathF.Min(start.Y, end.Y);
|
||||
float right = MathF.Max(start.X, end.X);
|
||||
float bottom = MathF.Max(start.Y, end.Y);
|
||||
RectangleF bounds = RectangleF.FromLTRB(left, top, right, bottom);
|
||||
return InflateBounds(bounds, pen);
|
||||
}
|
||||
|
||||
private static RectangleF InflateBounds(RectangleF bounds, Pen pen)
|
||||
{
|
||||
float halfWidth = pen.StrokeWidth * 0.5F;
|
||||
float inflate = pen.StrokeOptions.LineJoin switch
|
||||
{
|
||||
LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)),
|
||||
_ => halfWidth
|
||||
};
|
||||
|
||||
bounds.Inflate(new SizeF(inflate, inflate));
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// One stroked path command queued by the canvas batcher.
|
||||
/// </summary>
|
||||
public readonly struct StrokePathCommand
|
||||
{
|
||||
private readonly IPath sourcePath;
|
||||
private readonly DrawingOptions drawingOptions;
|
||||
private readonly IReadOnlyList<IPath>? clipPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokePathCommand"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">The source stroke path.</param>
|
||||
/// <param name="brush">The brush used to shade the stroke.</param>
|
||||
/// <param name="drawingOptions">The drawing options (graphics, shape, transform) used during composition.</param>
|
||||
/// <param name="rasterizerOptions">The rasterizer options used to generate coverage.</param>
|
||||
/// <param name="targetBounds">The absolute bounds of the logical target.</param>
|
||||
/// <param name="destinationOffset">The absolute destination offset of the command.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <param name="clipPaths">Optional clip paths supplied with the command.</param>
|
||||
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
|
||||
public StrokePathCommand(
|
||||
IPath sourcePath,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
Pen pen,
|
||||
IReadOnlyList<IPath>? clipPaths,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
this.sourcePath = sourcePath;
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.clipPaths = clipPaths;
|
||||
this.Brush = brush;
|
||||
this.RasterizerOptions = rasterizerOptions;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.Pen = pen;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used during composition.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options carried by the command.
|
||||
/// </summary>
|
||||
public DrawingOptions DrawingOptions => this.drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options used during composition.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rasterizer options used to generate coverage.
|
||||
/// </summary>
|
||||
public RasterizerOptions RasterizerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the logical target for this command.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset where the local coverage should be composited.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stroke metadata for this command.
|
||||
/// </summary>
|
||||
public Pen Pen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source stroke path.
|
||||
/// </summary>
|
||||
public IPath SourcePath => this.sourcePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform => this.drawingOptions.Transform;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional clip paths carried by the command.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath>? ClipPaths => this.clipPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shape options carried by the command.
|
||||
/// </summary>
|
||||
public ShapeOptions ShapeOptions => this.drawingOptions.ShapeOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// One explicit stroked open polyline command queued by the canvas batcher.
|
||||
/// </summary>
|
||||
public readonly struct StrokePolylineCommand
|
||||
{
|
||||
private readonly PointF[] sourcePoints;
|
||||
private readonly DrawingOptions drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokePolylineCommand"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="sourcePoints">The source polyline points.</param>
|
||||
/// <param name="brush">The brush used to shade the stroke.</param>
|
||||
/// <param name="drawingOptions">The drawing options (graphics, shape, transform) used during composition.</param>
|
||||
/// <param name="rasterizerOptions">The rasterizer options used to generate coverage.</param>
|
||||
/// <param name="targetBounds">The absolute bounds of the logical target.</param>
|
||||
/// <param name="destinationOffset">The absolute destination offset of the command.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
|
||||
public StrokePolylineCommand(
|
||||
PointF[] sourcePoints,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
Pen pen,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sourcePoints);
|
||||
if (sourcePoints.Length < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(sourcePoints), "Open stroke polylines require at least two points.");
|
||||
}
|
||||
|
||||
this.sourcePoints = sourcePoints;
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.Brush = brush;
|
||||
this.RasterizerOptions = rasterizerOptions;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.Pen = pen;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used during composition.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options carried by the command.
|
||||
/// </summary>
|
||||
public DrawingOptions DrawingOptions => this.drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options used during composition.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rasterizer options used to generate coverage.
|
||||
/// </summary>
|
||||
public RasterizerOptions RasterizerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the logical target for this command.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset where the local coverage should be composited.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stroke metadata for this command.
|
||||
/// </summary>
|
||||
public Pen Pen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source polyline points.
|
||||
/// </summary>
|
||||
public PointF[] SourcePoints => this.sourcePoints;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform => this.drawingOptions.Transform;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Computes the conservative stroked bounds of one open polyline.
|
||||
/// </summary>
|
||||
/// <param name="points">The polyline points.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <returns>The conservative stroked bounds.</returns>
|
||||
public static RectangleF GetConservativeBounds(PointF[] points, Pen pen)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(points);
|
||||
if (points.Length == 0)
|
||||
{
|
||||
return RectangleF.Empty;
|
||||
}
|
||||
|
||||
float minX = points[0].X;
|
||||
float minY = points[0].Y;
|
||||
float maxX = minX;
|
||||
float maxY = minY;
|
||||
|
||||
for (int i = 1; i < points.Length; i++)
|
||||
{
|
||||
PointF point = points[i];
|
||||
minX = MathF.Min(minX, point.X);
|
||||
minY = MathF.Min(minY, point.Y);
|
||||
maxX = MathF.Max(maxX, point.X);
|
||||
maxY = MathF.Max(maxY, point.Y);
|
||||
}
|
||||
|
||||
RectangleF bounds = RectangleF.FromLTRB(minX, minY, maxX, maxY);
|
||||
return InflateBounds(bounds, pen);
|
||||
}
|
||||
|
||||
private static RectangleF InflateBounds(RectangleF bounds, Pen pen)
|
||||
{
|
||||
float halfWidth = pen.StrokeWidth * 0.5F;
|
||||
float inflate = pen.StrokeOptions.LineJoin switch
|
||||
{
|
||||
LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)),
|
||||
_ => halfWidth
|
||||
};
|
||||
|
||||
bounds.Inflate(new SizeF(inflate, inflate));
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user