1169 lines
45 KiB
C#
1169 lines
45 KiB
C#
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading.Tasks;
|
|
using SixLabors.ImageSharp.Memory;
|
|
|
|
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 : IDisposable
|
|
{
|
|
private static readonly FlushScene EmptyScene = new(
|
|
fillItemCount: 0,
|
|
strokeItemCount: 0,
|
|
rowCount: 0,
|
|
rowItemCount: 0,
|
|
totalEdgeCount: 0,
|
|
singleBandItemCount: 0,
|
|
smallEdgeItemCount: 0,
|
|
maxLayerDepth: 0,
|
|
fillItems: [],
|
|
strokeItems: [],
|
|
layerOptions: [],
|
|
rows: []);
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="FlushScene"/> class.
|
|
/// </summary>
|
|
private FlushScene(
|
|
int fillItemCount,
|
|
int strokeItemCount,
|
|
int rowCount,
|
|
int rowItemCount,
|
|
long totalEdgeCount,
|
|
int singleBandItemCount,
|
|
int smallEdgeItemCount,
|
|
int maxLayerDepth,
|
|
FillSceneItem?[] fillItems,
|
|
StrokeSceneItem?[] strokeItems,
|
|
GraphicsOptions?[] layerOptions,
|
|
SceneRow[] rows)
|
|
{
|
|
this.FillItemCount = fillItemCount;
|
|
this.StrokeItemCount = strokeItemCount;
|
|
this.RowCount = rowCount;
|
|
this.RowItemCount = rowItemCount;
|
|
this.TotalEdgeCount = totalEdgeCount;
|
|
this.SingleBandItemCount = singleBandItemCount;
|
|
this.SmallEdgeItemCount = smallEdgeItemCount;
|
|
this.MaxLayerDepth = maxLayerDepth;
|
|
this.FillItems = fillItems;
|
|
this.StrokeItems = strokeItems;
|
|
this.LayerOptions = layerOptions;
|
|
this.Rows = rows;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the number of visible draw items retained by the scene.
|
|
/// </summary>
|
|
public int ItemCount => this.FillItemCount + this.StrokeItemCount;
|
|
|
|
/// <summary>
|
|
/// Gets the number of visible fill items retained by the scene.
|
|
/// </summary>
|
|
public int FillItemCount { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the number of visible stroke items retained by the scene.
|
|
/// </summary>
|
|
public int StrokeItemCount { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the retained visible scene items.
|
|
/// </summary>
|
|
internal FillSceneItem?[] FillItems { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the retained visible stroke scene items.
|
|
/// </summary>
|
|
internal StrokeSceneItem?[] StrokeItems { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the retained layer options indexed by begin-layer command index.
|
|
/// </summary>
|
|
internal GraphicsOptions?[] LayerOptions { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the number of scene rows containing executable work.
|
|
/// </summary>
|
|
public int RowCount { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the retained row lists.
|
|
/// </summary>
|
|
internal SceneRow[] Rows { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the total number of row items retained by the scene.
|
|
/// </summary>
|
|
public int RowItemCount { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the total number of encoded raster edges retained by the scene.
|
|
/// </summary>
|
|
public long TotalEdgeCount { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the number of items that occupy a single row band.
|
|
/// </summary>
|
|
public int SingleBandItemCount { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the number of items whose retained edge count is small.
|
|
/// </summary>
|
|
public int SmallEdgeItemCount { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the maximum retained layer nesting depth in this scene.
|
|
/// </summary>
|
|
public int MaxLayerDepth { get; }
|
|
|
|
/// <summary>
|
|
/// Creates a new scene by scheduling visible draw operations directly over retained rasterizable geometry.
|
|
/// </summary>
|
|
/// <param name="scene">The prepared composition scene.</param>
|
|
/// <param name="targetBounds">The destination bounds of the flush.</param>
|
|
/// <param name="allocator">The allocator used for retained row storage.</param>
|
|
/// <param name="maxDegreeOfParallelism">
|
|
/// The maximum degree of parallelism to use when building the scene, or <c>-1</c> to pass
|
|
/// through the runtime's unlimited sentinel for <see cref="ParallelOptions.MaxDegreeOfParallelism"/>.
|
|
/// </param>
|
|
/// <returns>A flush-ready scene.</returns>
|
|
public static FlushScene Create(
|
|
DrawingCommandBatch scene,
|
|
in Rectangle targetBounds,
|
|
MemoryAllocator allocator,
|
|
int maxDegreeOfParallelism)
|
|
{
|
|
int commandCount = scene.CommandCount;
|
|
|
|
if (commandCount == 0)
|
|
{
|
|
return Empty();
|
|
}
|
|
|
|
IReadOnlyList<CompositionSceneCommand> commands = scene.Commands;
|
|
int firstTargetRowBandIndex = targetBounds.Top / DefaultRasterizer.DefaultTileHeight;
|
|
int lastTargetRowBandIndex = (targetBounds.Bottom - 1) / DefaultRasterizer.DefaultTileHeight;
|
|
int targetRowCount = (lastTargetRowBandIndex - firstTargetRowBandIndex) + 1;
|
|
Rectangle targetRectangle = targetBounds;
|
|
|
|
if (targetRowCount <= 0)
|
|
{
|
|
return Empty();
|
|
}
|
|
|
|
FillSceneItem?[] fillItems = new FillSceneItem?[commandCount];
|
|
StrokeSceneItem?[] strokeItems = new StrokeSceneItem?[commandCount];
|
|
GraphicsOptions?[] layerOptions = new GraphicsOptions?[commandCount];
|
|
int partitionCount = ParallelExecutionHelper.GetPartitionCount(maxDegreeOfParallelism, commandCount, targetRowCount);
|
|
PartitionState[] partitions = new PartitionState[partitionCount];
|
|
|
|
_ = Parallel.For(
|
|
0,
|
|
partitionCount,
|
|
ParallelExecutionHelper.CreateParallelOptions(maxDegreeOfParallelism, partitionCount),
|
|
partitionIndex =>
|
|
{
|
|
// Integer division splits the commands into contiguous half-open ranges,
|
|
// keeping the partitions balanced while assigning each command exactly once.
|
|
int commandStart = (partitionIndex * commandCount) / partitionCount;
|
|
int commandEnd = ((partitionIndex + 1) * commandCount) / partitionCount;
|
|
|
|
partitions[partitionIndex] = ProcessPartition(
|
|
commands,
|
|
commandStart,
|
|
commandEnd,
|
|
targetRectangle,
|
|
firstTargetRowBandIndex,
|
|
targetRowCount,
|
|
allocator,
|
|
fillItems,
|
|
strokeItems,
|
|
layerOptions);
|
|
});
|
|
|
|
RowBuilder[] rowBuilders = new RowBuilder[targetRowCount];
|
|
int fillItemCount = 0;
|
|
int strokeItemCount = 0;
|
|
long totalEdgeCount = 0;
|
|
int singleBandItemCount = 0;
|
|
int smallEdgeItemCount = 0;
|
|
int currentLayerDepth = 0;
|
|
int maxLayerDepth = 0;
|
|
|
|
for (int i = 0; i < partitionCount; i++)
|
|
{
|
|
PartitionState partition = partitions[i];
|
|
fillItemCount += partition.FillItemCount;
|
|
strokeItemCount += partition.StrokeItemCount;
|
|
totalEdgeCount += partition.TotalEdgeCount;
|
|
singleBandItemCount += partition.SingleBandItemCount;
|
|
smallEdgeItemCount += partition.SmallEdgeItemCount;
|
|
maxLayerDepth = Math.Max(maxLayerDepth, currentLayerDepth + partition.MaxLayerDepth);
|
|
currentLayerDepth += partition.LayerDepthDelta;
|
|
|
|
for (int rowSlot = 0; rowSlot < targetRowCount; rowSlot++)
|
|
{
|
|
RowBuilder.AppendBuilder(ref rowBuilders[rowSlot], ref partition.RowBuilders[rowSlot]);
|
|
}
|
|
}
|
|
|
|
int rowCount = 0;
|
|
int rowItemCount = 0;
|
|
for (int i = 0; i < rowBuilders.Length; i++)
|
|
{
|
|
if (!rowBuilders[i].IsInitialized)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
rowCount++;
|
|
rowItemCount += rowBuilders[i].Count;
|
|
}
|
|
|
|
if ((fillItemCount + strokeItemCount) == 0 || rowItemCount == 0)
|
|
{
|
|
DisposeRows(rowBuilders);
|
|
return Empty();
|
|
}
|
|
|
|
SceneRow[] sceneRows = FinalizeRows(rowBuilders, firstTargetRowBandIndex, rowCount);
|
|
return new FlushScene(
|
|
fillItemCount,
|
|
strokeItemCount,
|
|
rowCount,
|
|
rowItemCount,
|
|
totalEdgeCount,
|
|
singleBandItemCount,
|
|
smallEdgeItemCount,
|
|
maxLayerDepth,
|
|
fillItems,
|
|
strokeItems,
|
|
layerOptions,
|
|
sceneRows);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases retained scene storage.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
for (int i = 0; i < this.Rows.Length; i++)
|
|
{
|
|
this.Rows[i].Dispose();
|
|
}
|
|
|
|
for (int i = 0; i < this.FillItems.Length; i++)
|
|
{
|
|
this.FillItems[i]?.Dispose();
|
|
}
|
|
|
|
for (int i = 0; i < this.StrokeItems.Length; i++)
|
|
{
|
|
this.StrokeItems[i]?.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an empty scene instance.
|
|
/// </summary>
|
|
private static FlushScene Empty() => EmptyScene;
|
|
|
|
/// <summary>
|
|
/// Identifies whether a path-backed command contributes executable retained raster work to the scene.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool IsSceneDrawable(in CompositionCommand command)
|
|
=> command.Kind == CompositionCommandKind.FillLayer;
|
|
|
|
/// <summary>
|
|
/// Accumulates retained fill statistics used for scene heuristics.
|
|
/// </summary>
|
|
private static void AccumulateFillItemStats(
|
|
DefaultRasterizer.RasterizableGeometry rasterizable,
|
|
ref long totalEdgeCount,
|
|
ref int smallEdgeItemCount,
|
|
ref int singleBandItemCount)
|
|
{
|
|
for (int localRowIndex = 0; localRowIndex < rasterizable.RowBandCount; localRowIndex++)
|
|
{
|
|
if (!rasterizable.HasCoverage(localRowIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DefaultRasterizer.RasterizableBandInfo info = rasterizable.GetBandInfo(localRowIndex);
|
|
totalEdgeCount += info.LineCount;
|
|
if (info.LineCount <= 8)
|
|
{
|
|
smallEdgeItemCount++;
|
|
}
|
|
}
|
|
|
|
if (rasterizable.RowBandCount == 1)
|
|
{
|
|
singleBandItemCount++;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Accumulates retained stroke statistics used for scene heuristics.
|
|
/// </summary>
|
|
private static void AccumulateStrokeItemStats(
|
|
DefaultRasterizer.StrokeRasterizableGeometry rasterizable,
|
|
ref long totalEdgeCount,
|
|
ref int smallEdgeItemCount,
|
|
ref int singleBandItemCount)
|
|
{
|
|
for (int localRowIndex = 0; localRowIndex < rasterizable.RowBandCount; localRowIndex++)
|
|
{
|
|
if (!rasterizable.HasCoverage(localRowIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DefaultRasterizer.RasterizableBandInfo info = rasterizable.GetBandInfo(localRowIndex);
|
|
totalEdgeCount += info.LineCount;
|
|
if (info.LineCount <= 8)
|
|
{
|
|
smallEdgeItemCount++;
|
|
}
|
|
}
|
|
|
|
if (rasterizable.RowBandCount == 1)
|
|
{
|
|
singleBandItemCount++;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends retained fill row operations for one item into the row builders owned by the current partition.
|
|
/// </summary>
|
|
private static void AppendFillRowOperations(
|
|
RowBuilder[] rowBuilders,
|
|
int rowStart,
|
|
int rowEnd,
|
|
int firstTargetRowBandIndex,
|
|
int itemIndex,
|
|
DefaultRasterizer.RasterizableGeometry rasterizable,
|
|
MemoryAllocator allocator)
|
|
{
|
|
int localRowStart = Math.Max(0, rowStart - (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex));
|
|
int localRowEnd = Math.Min(rasterizable.RowBandCount, rowEnd - (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex));
|
|
for (int localRowIndex = localRowStart; localRowIndex < localRowEnd; localRowIndex++)
|
|
{
|
|
if (!rasterizable.HasCoverage(localRowIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int rowSlot = (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex) + localRowIndex;
|
|
ref RowBuilder builder = ref rowBuilders[rowSlot];
|
|
if (!builder.IsInitialized)
|
|
{
|
|
builder = new RowBuilder(allocator);
|
|
}
|
|
|
|
builder.Append(new SceneOperation(SceneOperationKind.FillItem, itemIndex, localRowIndex));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends retained stroke row operations for one item into the row builders owned by the current partition.
|
|
/// </summary>
|
|
private static void AppendStrokeRowOperations(
|
|
RowBuilder[] rowBuilders,
|
|
int rowStart,
|
|
int rowEnd,
|
|
int firstTargetRowBandIndex,
|
|
int itemIndex,
|
|
DefaultRasterizer.StrokeRasterizableGeometry rasterizable,
|
|
MemoryAllocator allocator)
|
|
{
|
|
int localRowStart = Math.Max(0, rowStart - (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex));
|
|
int localRowEnd = Math.Min(rasterizable.RowBandCount, rowEnd - (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex));
|
|
for (int localRowIndex = localRowStart; localRowIndex < localRowEnd; localRowIndex++)
|
|
{
|
|
if (!rasterizable.HasCoverage(localRowIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int rowSlot = (rasterizable.FirstRowBandIndex - firstTargetRowBandIndex) + localRowIndex;
|
|
ref RowBuilder builder = ref rowBuilders[rowSlot];
|
|
if (!builder.IsInitialized)
|
|
{
|
|
builder = new RowBuilder(allocator);
|
|
}
|
|
|
|
builder.Append(new SceneOperation(SceneOperationKind.StrokeItem, itemIndex, localRowIndex));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the row-slot range a fill or stroke command may write to. When the command was
|
|
/// recorded inside a SaveLayer the row distribution is confined to the layer's row bands so
|
|
/// a command's geometry cannot leak into rows that lie above or below the layer's
|
|
/// <see cref="CompositionCommand.TargetBounds"/>. Outside any layer (root or region scope)
|
|
/// the command is allowed to address every row; constraining row distribution by the
|
|
/// region's bounds would change long-standing rendering behaviour for region-only paths.
|
|
/// </summary>
|
|
/// <param name="commandTargetBounds">The command's absolute target bounds.</param>
|
|
/// <param name="firstTargetRowBandIndex">The first row-band index covered by the partition.</param>
|
|
/// <param name="totalRowSlots">The total number of row slots owned by the partition.</param>
|
|
/// <param name="isInsideLayer">True if the command was recorded inside a SaveLayer scope.</param>
|
|
/// <param name="rowStart">The first row slot the command may write to.</param>
|
|
/// <param name="rowEnd">The exclusive end row slot the command may write to.</param>
|
|
private static void GetEffectiveRowSlotRange(
|
|
Rectangle commandTargetBounds,
|
|
int firstTargetRowBandIndex,
|
|
int totalRowSlots,
|
|
bool isInsideLayer,
|
|
out int rowStart,
|
|
out int rowEnd)
|
|
{
|
|
if (!isInsideLayer)
|
|
{
|
|
rowStart = 0;
|
|
rowEnd = totalRowSlots;
|
|
return;
|
|
}
|
|
|
|
int firstRowBand = commandTargetBounds.Top / DefaultRasterizer.DefaultTileHeight;
|
|
int lastRowBand = (commandTargetBounds.Bottom - 1) / DefaultRasterizer.DefaultTileHeight;
|
|
rowStart = Math.Max(0, firstRowBand - firstTargetRowBandIndex);
|
|
rowEnd = Math.Min(totalRowSlots, lastRowBand - firstTargetRowBandIndex + 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Identifies whether a command contributes retained per-row layer control operations.
|
|
/// </summary>
|
|
private static bool TryGetLayerOperation(
|
|
in CompositionCommand command,
|
|
in Rectangle targetBounds,
|
|
int firstTargetRowBandIndex,
|
|
out CompositionCommandKind operationKind,
|
|
out Rectangle layerBounds,
|
|
out int firstRowSlot,
|
|
out int lastRowSlot)
|
|
{
|
|
operationKind = default;
|
|
layerBounds = default;
|
|
firstRowSlot = 0;
|
|
lastRowSlot = -1;
|
|
|
|
switch (command.Kind)
|
|
{
|
|
case CompositionCommandKind.BeginLayer:
|
|
operationKind = CompositionCommandKind.BeginLayer;
|
|
break;
|
|
|
|
case CompositionCommandKind.EndLayer:
|
|
operationKind = CompositionCommandKind.EndLayer;
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
Rectangle bounds = Rectangle.Intersect(command.LayerBounds, targetBounds);
|
|
if (bounds.Height <= 0 || bounds.Width <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
layerBounds = bounds;
|
|
int firstRowBandIndex = bounds.Top / DefaultRasterizer.DefaultTileHeight;
|
|
int lastRowBandIndex = (bounds.Bottom - 1) / DefaultRasterizer.DefaultTileHeight;
|
|
firstRowSlot = firstRowBandIndex - firstTargetRowBandIndex;
|
|
lastRowSlot = lastRowBandIndex - firstTargetRowBandIndex;
|
|
return firstRowSlot <= lastRowSlot;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finalizes row-owned append builders into immutable scene rows.
|
|
/// </summary>
|
|
private static SceneRow[] FinalizeRows(RowBuilder[] builders, int firstTargetRowBandIndex, int rowCount)
|
|
{
|
|
SceneRow[] rows = new SceneRow[rowCount];
|
|
int writeIndex = 0;
|
|
for (int i = 0; i < builders.Length; i++)
|
|
{
|
|
if (!builders[i].IsInitialized)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
rows[writeIndex++] = builders[i].Finalize(firstTargetRowBandIndex + i);
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes partially created row builders.
|
|
/// </summary>
|
|
private static void DisposeRows(RowBuilder[] builders)
|
|
{
|
|
for (int i = 0; i < builders.Length; i++)
|
|
{
|
|
builders[i].Dispose();
|
|
}
|
|
}
|
|
|
|
private static PartitionState ProcessPartition(
|
|
IReadOnlyList<CompositionSceneCommand> commands,
|
|
int commandStart,
|
|
int commandEnd,
|
|
in Rectangle targetBounds,
|
|
int firstTargetRowBandIndex,
|
|
int targetRowCount,
|
|
MemoryAllocator allocator,
|
|
FillSceneItem?[] fillItems,
|
|
StrokeSceneItem?[] strokeItems,
|
|
GraphicsOptions?[] layerOptions)
|
|
{
|
|
RowBuilder[] rowBuilders = new RowBuilder[targetRowCount];
|
|
int fillItemCount = 0;
|
|
int strokeItemCount = 0;
|
|
long totalEdgeCount = 0;
|
|
int singleBandItemCount = 0;
|
|
int smallEdgeItemCount = 0;
|
|
int currentLayerDepth = 0;
|
|
int maxLayerDepth = 0;
|
|
|
|
for (int commandIndex = commandStart; commandIndex < commandEnd; commandIndex++)
|
|
{
|
|
CompositionSceneCommand command = commands[commandIndex];
|
|
if (command is PathCompositionSceneCommand pathCommand)
|
|
{
|
|
ProcessPathCommand(
|
|
pathCommand.Command,
|
|
commandIndex,
|
|
targetBounds,
|
|
firstTargetRowBandIndex,
|
|
rowBuilders,
|
|
allocator,
|
|
fillItems,
|
|
strokeItems,
|
|
layerOptions,
|
|
ref fillItemCount,
|
|
ref strokeItemCount,
|
|
ref totalEdgeCount,
|
|
ref singleBandItemCount,
|
|
ref smallEdgeItemCount,
|
|
ref currentLayerDepth,
|
|
ref maxLayerDepth);
|
|
}
|
|
else if (command is StrokePathCompositionSceneCommand strokePathCommand)
|
|
{
|
|
ProcessStrokePathCommand(
|
|
strokePathCommand.Command,
|
|
commandIndex,
|
|
targetRowCount,
|
|
firstTargetRowBandIndex,
|
|
rowBuilders,
|
|
allocator,
|
|
strokeItems,
|
|
ref strokeItemCount,
|
|
ref totalEdgeCount,
|
|
ref singleBandItemCount,
|
|
ref smallEdgeItemCount);
|
|
}
|
|
else if (command is LineSegmentCompositionSceneCommand lineSegmentCommand)
|
|
{
|
|
ProcessLineSegmentCommand(
|
|
lineSegmentCommand.Command,
|
|
commandIndex,
|
|
targetRowCount,
|
|
firstTargetRowBandIndex,
|
|
rowBuilders,
|
|
allocator,
|
|
strokeItems,
|
|
ref strokeItemCount,
|
|
ref totalEdgeCount,
|
|
ref singleBandItemCount,
|
|
ref smallEdgeItemCount);
|
|
}
|
|
else
|
|
{
|
|
ProcessPolylineCommand(
|
|
((PolylineCompositionSceneCommand)command).Command,
|
|
commandIndex,
|
|
targetRowCount,
|
|
firstTargetRowBandIndex,
|
|
rowBuilders,
|
|
allocator,
|
|
strokeItems,
|
|
ref strokeItemCount,
|
|
ref totalEdgeCount,
|
|
ref singleBandItemCount,
|
|
ref smallEdgeItemCount);
|
|
}
|
|
}
|
|
|
|
return new PartitionState(
|
|
fillItemCount,
|
|
strokeItemCount,
|
|
totalEdgeCount,
|
|
singleBandItemCount,
|
|
smallEdgeItemCount,
|
|
currentLayerDepth,
|
|
maxLayerDepth,
|
|
rowBuilders);
|
|
}
|
|
|
|
private static void ProcessPathCommand(
|
|
in CompositionCommand command,
|
|
int commandIndex,
|
|
in Rectangle targetBounds,
|
|
int firstTargetRowBandIndex,
|
|
RowBuilder[] rowBuilders,
|
|
MemoryAllocator allocator,
|
|
FillSceneItem?[] fillItems,
|
|
StrokeSceneItem?[] strokeItems,
|
|
GraphicsOptions?[] layerOptions,
|
|
ref int fillItemCount,
|
|
ref int strokeItemCount,
|
|
ref long totalEdgeCount,
|
|
ref int singleBandItemCount,
|
|
ref int smallEdgeItemCount,
|
|
ref int currentLayerDepth,
|
|
ref int maxLayerDepth)
|
|
{
|
|
if (TryGetLayerOperation(
|
|
command,
|
|
targetBounds,
|
|
firstTargetRowBandIndex,
|
|
out CompositionCommandKind operationKind,
|
|
out Rectangle layerBounds,
|
|
out int firstRowSlot,
|
|
out int lastRowSlot))
|
|
{
|
|
if (operationKind == CompositionCommandKind.BeginLayer)
|
|
{
|
|
currentLayerDepth++;
|
|
maxLayerDepth = Math.Max(maxLayerDepth, currentLayerDepth);
|
|
}
|
|
else
|
|
{
|
|
currentLayerDepth--;
|
|
}
|
|
|
|
int layerOptionsIndex = -1;
|
|
if (operationKind == CompositionCommandKind.BeginLayer)
|
|
{
|
|
// BeginLayer carries the compositing options used later by the matching EndLayer.
|
|
// Store them at the command index so row operations can keep a compact integer reference.
|
|
layerOptions[commandIndex] = command.GraphicsOptions;
|
|
layerOptionsIndex = commandIndex;
|
|
}
|
|
|
|
AppendLayerOperations(rowBuilders, firstRowSlot, lastRowSlot, layerBounds, operationKind, layerOptionsIndex, targetBounds, allocator);
|
|
return;
|
|
}
|
|
|
|
if (!IsSceneDrawable(command))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryPrepareFillPath(command, allocator, out PreparedFillItem preparedFill) ||
|
|
preparedFill.Rasterizable.RowBandCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
fillItems[commandIndex] = new FillSceneItem(preparedFill.Brush, preparedFill.GraphicsOptions, preparedFill.BrushBounds, preparedFill.Rasterizable);
|
|
fillItemCount++;
|
|
AccumulateFillItemStats(preparedFill.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount);
|
|
GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, rowBuilders.Length, command.IsInsideLayer, out int rowStart, out int rowEnd);
|
|
AppendFillRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedFill.Rasterizable, allocator);
|
|
}
|
|
|
|
private static void ProcessStrokePathCommand(
|
|
in StrokePathCommand command,
|
|
int commandIndex,
|
|
int targetRowCount,
|
|
int firstTargetRowBandIndex,
|
|
RowBuilder[] rowBuilders,
|
|
MemoryAllocator allocator,
|
|
StrokeSceneItem?[] strokeItems,
|
|
ref int strokeItemCount,
|
|
ref long totalEdgeCount,
|
|
ref int singleBandItemCount,
|
|
ref int smallEdgeItemCount)
|
|
{
|
|
if (!TryPrepareStrokePath(command, allocator, out PreparedStrokeItem preparedStroke) ||
|
|
preparedStroke.Rasterizable.RowBandCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
strokeItems[commandIndex] = new StrokeSceneItem(preparedStroke.Brush, preparedStroke.GraphicsOptions, preparedStroke.BrushBounds, preparedStroke.Rasterizable);
|
|
strokeItemCount++;
|
|
AccumulateStrokeItemStats(preparedStroke.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount);
|
|
GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, targetRowCount, command.IsInsideLayer, out int rowStart, out int rowEnd);
|
|
AppendStrokeRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedStroke.Rasterizable, allocator);
|
|
}
|
|
|
|
private static void ProcessLineSegmentCommand(
|
|
in StrokeLineSegmentCommand command,
|
|
int commandIndex,
|
|
int targetRowCount,
|
|
int firstTargetRowBandIndex,
|
|
RowBuilder[] rowBuilders,
|
|
MemoryAllocator allocator,
|
|
StrokeSceneItem?[] strokeItems,
|
|
ref int strokeItemCount,
|
|
ref long totalEdgeCount,
|
|
ref int singleBandItemCount,
|
|
ref int smallEdgeItemCount)
|
|
{
|
|
if (!TryPrepareLineSegmentStroke(command, allocator, out PreparedStrokeItem preparedStroke) ||
|
|
preparedStroke.Rasterizable.RowBandCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
strokeItems[commandIndex] = new StrokeSceneItem(preparedStroke.Brush, preparedStroke.GraphicsOptions, preparedStroke.BrushBounds, preparedStroke.Rasterizable);
|
|
strokeItemCount++;
|
|
AccumulateStrokeItemStats(preparedStroke.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount);
|
|
GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, targetRowCount, command.IsInsideLayer, out int rowStart, out int rowEnd);
|
|
AppendStrokeRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedStroke.Rasterizable, allocator);
|
|
}
|
|
|
|
private static void ProcessPolylineCommand(
|
|
in StrokePolylineCommand command,
|
|
int commandIndex,
|
|
int targetRowCount,
|
|
int firstTargetRowBandIndex,
|
|
RowBuilder[] rowBuilders,
|
|
MemoryAllocator allocator,
|
|
StrokeSceneItem?[] strokeItems,
|
|
ref int strokeItemCount,
|
|
ref long totalEdgeCount,
|
|
ref int singleBandItemCount,
|
|
ref int smallEdgeItemCount)
|
|
{
|
|
if (!TryPreparePolylineStroke(command, allocator, out PreparedStrokeItem preparedStroke) ||
|
|
preparedStroke.Rasterizable.RowBandCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
strokeItems[commandIndex] = new StrokeSceneItem(preparedStroke.Brush, preparedStroke.GraphicsOptions, preparedStroke.BrushBounds, preparedStroke.Rasterizable);
|
|
strokeItemCount++;
|
|
AccumulateStrokeItemStats(preparedStroke.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount);
|
|
GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, targetRowCount, command.IsInsideLayer, out int rowStart, out int rowEnd);
|
|
AppendStrokeRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedStroke.Rasterizable, allocator);
|
|
}
|
|
|
|
private static void AppendLayerOperations(
|
|
RowBuilder[] rowBuilders,
|
|
int firstRowSlot,
|
|
int lastRowSlot,
|
|
Rectangle layerBandBounds,
|
|
CompositionCommandKind operationKind,
|
|
int layerOptionsIndex,
|
|
in Rectangle targetBounds,
|
|
MemoryAllocator allocator)
|
|
{
|
|
for (int rowSlot = firstRowSlot; rowSlot <= lastRowSlot; rowSlot++)
|
|
{
|
|
ref RowBuilder builder = ref rowBuilders[rowSlot];
|
|
if (!builder.IsInitialized)
|
|
{
|
|
builder = new RowBuilder(allocator);
|
|
}
|
|
|
|
int rowTop = targetBounds.Top + (rowSlot * DefaultRasterizer.DefaultTileHeight);
|
|
Rectangle rowBounds = new(targetBounds.Left, rowTop, targetBounds.Width, DefaultRasterizer.DefaultTileHeight);
|
|
Rectangle rowLayerBounds = Rectangle.Intersect(layerBandBounds, rowBounds);
|
|
builder.Append(new SceneOperation(operationKind, rowLayerBounds, layerOptionsIndex));
|
|
}
|
|
}
|
|
|
|
private static bool TryPrepareFillPath(
|
|
in CompositionCommand command,
|
|
MemoryAllocator allocator,
|
|
out PreparedFillItem prepared)
|
|
{
|
|
IPath path = command.SourcePath;
|
|
Matrix4x4 transform = command.Transform;
|
|
bool hasTransform = !transform.IsIdentity;
|
|
Vector2 scale = ExtractScale(transform);
|
|
Matrix4x4 residual = ComputeResidual(scale, transform);
|
|
LinearGeometry geometry = path.ToLinearGeometry(scale);
|
|
Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush;
|
|
RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual);
|
|
|
|
if (!TryResolveRasterization(
|
|
sourceBrush,
|
|
geometryBounds,
|
|
command.RasterizerOptions,
|
|
command.DestinationOffset,
|
|
command.TargetBounds,
|
|
out Brush brush,
|
|
out RasterizerOptions rasterizerOptions,
|
|
out Rectangle brushBounds))
|
|
{
|
|
prepared = default;
|
|
return false;
|
|
}
|
|
|
|
DefaultRasterizer.RasterizableGeometry? rasterizable = DefaultRasterizer.CreateRasterizableGeometry(
|
|
geometry,
|
|
residual,
|
|
command.DestinationOffset.X,
|
|
command.DestinationOffset.Y,
|
|
rasterizerOptions,
|
|
allocator);
|
|
|
|
if (rasterizable is null)
|
|
{
|
|
prepared = default;
|
|
return false;
|
|
}
|
|
|
|
prepared = new PreparedFillItem(brush, command.GraphicsOptions, brushBounds, rasterizable);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryPrepareStrokePath(
|
|
in StrokePathCommand command,
|
|
MemoryAllocator allocator,
|
|
out PreparedStrokeItem prepared)
|
|
{
|
|
IPath path = command.SourcePath;
|
|
Matrix4x4 transform = command.Transform;
|
|
bool hasTransform = !transform.IsIdentity;
|
|
Vector2 scale = ExtractScale(transform);
|
|
Matrix4x4 residual = ComputeResidual(scale, transform);
|
|
LinearGeometry geometry = path.ToLinearGeometry(scale);
|
|
float widthScale = GetTransformWidthScale(transform);
|
|
RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual);
|
|
RectangleF strokeBounds = GetStrokeBounds(geometryBounds, command.Pen, widthScale);
|
|
Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush;
|
|
|
|
if (!TryResolveRasterization(
|
|
sourceBrush,
|
|
strokeBounds,
|
|
command.RasterizerOptions,
|
|
command.DestinationOffset,
|
|
command.TargetBounds,
|
|
out Brush brush,
|
|
out RasterizerOptions rasterizerOptions,
|
|
out Rectangle brushBounds))
|
|
{
|
|
prepared = default;
|
|
return false;
|
|
}
|
|
|
|
DefaultRasterizer.StrokeRasterizableGeometry? rasterizable = DefaultRasterizer.CreatePathStrokeRasterizableGeometry(
|
|
geometry,
|
|
residual,
|
|
command.Pen,
|
|
command.DestinationOffset.X,
|
|
command.DestinationOffset.Y,
|
|
rasterizerOptions,
|
|
widthScale,
|
|
allocator);
|
|
if (rasterizable is null)
|
|
{
|
|
prepared = default;
|
|
return false;
|
|
}
|
|
|
|
prepared = new PreparedStrokeItem(brush, command.GraphicsOptions, brushBounds, rasterizable);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryPrepareLineSegmentStroke(
|
|
in StrokeLineSegmentCommand command,
|
|
MemoryAllocator allocator,
|
|
out PreparedStrokeItem prepared)
|
|
{
|
|
Matrix4x4 transform = command.Transform;
|
|
bool hasTransform = !transform.IsIdentity;
|
|
PointF start = hasTransform ? PointF.Transform(command.SourceStart, transform) : command.SourceStart;
|
|
PointF end = hasTransform ? PointF.Transform(command.SourceEnd, transform) : command.SourceEnd;
|
|
float widthScale = GetTransformWidthScale(transform);
|
|
RectangleF segmentBounds = RectangleF.FromLTRB(
|
|
MathF.Min(start.X, end.X),
|
|
MathF.Min(start.Y, end.Y),
|
|
MathF.Max(start.X, end.X),
|
|
MathF.Max(start.Y, end.Y));
|
|
RectangleF bounds = GetStrokeBounds(segmentBounds, command.Pen, widthScale);
|
|
Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush;
|
|
|
|
if (!TryResolveRasterization(
|
|
sourceBrush,
|
|
bounds,
|
|
command.RasterizerOptions,
|
|
command.DestinationOffset,
|
|
command.TargetBounds,
|
|
out Brush brush,
|
|
out RasterizerOptions rasterizerOptions,
|
|
out Rectangle brushBounds))
|
|
{
|
|
prepared = default;
|
|
return false;
|
|
}
|
|
|
|
DefaultRasterizer.StrokeRasterizableGeometry? rasterizable = DefaultRasterizer.CreateLineSegmentStrokeRasterizableGeometry(
|
|
start,
|
|
end,
|
|
command.Pen,
|
|
command.DestinationOffset.X,
|
|
command.DestinationOffset.Y,
|
|
rasterizerOptions,
|
|
widthScale,
|
|
allocator);
|
|
|
|
if (rasterizable is null)
|
|
{
|
|
prepared = default;
|
|
return false;
|
|
}
|
|
|
|
prepared = new PreparedStrokeItem(brush, command.GraphicsOptions, brushBounds, rasterizable);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryPreparePolylineStroke(
|
|
in StrokePolylineCommand command,
|
|
MemoryAllocator allocator,
|
|
out PreparedStrokeItem prepared)
|
|
{
|
|
Matrix4x4 transform = command.Transform;
|
|
bool hasTransform = !transform.IsIdentity;
|
|
Vector2 scale = ExtractScale(transform);
|
|
Matrix4x4 residual = ComputeResidual(scale, transform);
|
|
LinearGeometry geometry = LinearGeometry.CreateOpenPolyline(command.SourcePoints, scale);
|
|
float widthScale = GetTransformWidthScale(transform);
|
|
RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual);
|
|
RectangleF strokeBounds = GetStrokeBounds(geometryBounds, command.Pen, widthScale);
|
|
Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush;
|
|
|
|
if (!TryResolveRasterization(
|
|
sourceBrush,
|
|
strokeBounds,
|
|
command.RasterizerOptions,
|
|
command.DestinationOffset,
|
|
command.TargetBounds,
|
|
out Brush brush,
|
|
out RasterizerOptions rasterizerOptions,
|
|
out Rectangle brushBounds))
|
|
{
|
|
prepared = default;
|
|
return false;
|
|
}
|
|
|
|
DefaultRasterizer.StrokeRasterizableGeometry? rasterizable = DefaultRasterizer.CreatePathStrokeRasterizableGeometry(
|
|
geometry,
|
|
residual,
|
|
command.Pen,
|
|
command.DestinationOffset.X,
|
|
command.DestinationOffset.Y,
|
|
rasterizerOptions,
|
|
widthScale,
|
|
allocator);
|
|
|
|
if (rasterizable is null)
|
|
{
|
|
prepared = default;
|
|
return false;
|
|
}
|
|
|
|
prepared = new PreparedStrokeItem(brush, command.GraphicsOptions, brushBounds, rasterizable);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryResolveRasterization(
|
|
Brush brush,
|
|
RectangleF bounds,
|
|
in RasterizerOptions options,
|
|
Point destinationOffset,
|
|
in Rectangle targetBounds,
|
|
out Brush resolvedBrush,
|
|
out RasterizerOptions resolvedOptions,
|
|
out Rectangle brushBounds)
|
|
{
|
|
resolvedBrush = brush;
|
|
|
|
if (options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter)
|
|
{
|
|
bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height);
|
|
}
|
|
|
|
Rectangle localInterest = Rectangle.FromLTRB(
|
|
(int)MathF.Floor(bounds.Left),
|
|
(int)MathF.Floor(bounds.Top),
|
|
(int)MathF.Ceiling(bounds.Right) + 1,
|
|
(int)MathF.Ceiling(bounds.Bottom) + 1);
|
|
|
|
Rectangle absoluteInterest = new(
|
|
localInterest.X + destinationOffset.X,
|
|
localInterest.Y + destinationOffset.Y,
|
|
localInterest.Width,
|
|
localInterest.Height);
|
|
|
|
Rectangle clippedDestination = Rectangle.Intersect(targetBounds, absoluteInterest);
|
|
if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0)
|
|
{
|
|
resolvedOptions = default;
|
|
brushBounds = default;
|
|
return false;
|
|
}
|
|
|
|
resolvedOptions = new RasterizerOptions(
|
|
absoluteInterest,
|
|
options.IntersectionRule,
|
|
options.RasterizationMode,
|
|
options.SamplingOrigin,
|
|
options.AntialiasThreshold);
|
|
|
|
brushBounds = absoluteInterest;
|
|
return true;
|
|
}
|
|
|
|
private static RectangleF GetStrokeBounds(RectangleF bounds, Pen pen, float widthScale)
|
|
{
|
|
float halfWidth = pen.StrokeWidth * widthScale * 0.5F;
|
|
float joinInflate = pen.StrokeOptions.LineJoin switch
|
|
{
|
|
LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)),
|
|
_ => halfWidth
|
|
};
|
|
|
|
float capInflate = pen.StrokeOptions.LineCap == LineCap.Square
|
|
? halfWidth * MathF.Sqrt(2F)
|
|
: halfWidth;
|
|
|
|
float inflate = MathF.Max(joinInflate, capInflate);
|
|
|
|
bounds.Inflate(new SizeF(inflate, inflate));
|
|
return bounds;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the isotropic scale factor embedded in a drawing transform so stroke widths match device-space pixels.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Uses the square root of the absolute 2D determinant, the SVG-style fallback for non-uniform
|
|
/// scale. Reduces to the uniform scale for pure scale/rotate/translate matrices.
|
|
/// </remarks>
|
|
private static float GetTransformWidthScale(Matrix4x4 transform)
|
|
{
|
|
if (transform.IsIdentity)
|
|
{
|
|
return 1F;
|
|
}
|
|
|
|
float det = (transform.M11 * transform.M22) - (transform.M12 * transform.M21);
|
|
return MathF.Sqrt(MathF.Abs(det));
|
|
}
|
|
|
|
private static Vector2 ExtractScale(Matrix4x4 matrix)
|
|
=> new(
|
|
MathF.Sqrt((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12)),
|
|
MathF.Sqrt((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22)));
|
|
|
|
private static Matrix4x4 ComputeResidual(Vector2 scale, Matrix4x4 matrix)
|
|
=> Matrix4x4.CreateScale(1F / scale.X, 1F / scale.Y, 1F) * matrix;
|
|
|
|
private readonly struct PreparedFillItem
|
|
{
|
|
public PreparedFillItem(
|
|
Brush brush,
|
|
GraphicsOptions graphicsOptions,
|
|
Rectangle brushBounds,
|
|
DefaultRasterizer.RasterizableGeometry rasterizable)
|
|
{
|
|
this.Brush = brush;
|
|
this.GraphicsOptions = graphicsOptions;
|
|
this.BrushBounds = brushBounds;
|
|
this.Rasterizable = rasterizable;
|
|
}
|
|
|
|
public Brush Brush { get; }
|
|
|
|
public GraphicsOptions GraphicsOptions { get; }
|
|
|
|
public Rectangle BrushBounds { get; }
|
|
|
|
public DefaultRasterizer.RasterizableGeometry Rasterizable { get; }
|
|
}
|
|
|
|
private readonly struct PreparedStrokeItem
|
|
{
|
|
public PreparedStrokeItem(
|
|
Brush brush,
|
|
GraphicsOptions graphicsOptions,
|
|
Rectangle brushBounds,
|
|
DefaultRasterizer.StrokeRasterizableGeometry rasterizable)
|
|
{
|
|
this.Brush = brush;
|
|
this.GraphicsOptions = graphicsOptions;
|
|
this.BrushBounds = brushBounds;
|
|
this.Rasterizable = rasterizable;
|
|
}
|
|
|
|
public Brush Brush { get; }
|
|
|
|
public GraphicsOptions GraphicsOptions { get; }
|
|
|
|
public Rectangle BrushBounds { get; }
|
|
|
|
public DefaultRasterizer.StrokeRasterizableGeometry Rasterizable { get; }
|
|
}
|
|
|
|
private readonly struct PartitionState
|
|
{
|
|
public PartitionState(
|
|
int fillItemCount,
|
|
int strokeItemCount,
|
|
long totalEdgeCount,
|
|
int singleBandItemCount,
|
|
int smallEdgeItemCount,
|
|
int layerDepthDelta,
|
|
int maxLayerDepth,
|
|
RowBuilder[] rowBuilders)
|
|
{
|
|
this.FillItemCount = fillItemCount;
|
|
this.StrokeItemCount = strokeItemCount;
|
|
this.TotalEdgeCount = totalEdgeCount;
|
|
this.SingleBandItemCount = singleBandItemCount;
|
|
this.SmallEdgeItemCount = smallEdgeItemCount;
|
|
this.LayerDepthDelta = layerDepthDelta;
|
|
this.MaxLayerDepth = maxLayerDepth;
|
|
this.RowBuilders = rowBuilders;
|
|
}
|
|
|
|
public int FillItemCount { get; }
|
|
|
|
public int StrokeItemCount { get; }
|
|
|
|
public long TotalEdgeCount { get; }
|
|
|
|
public int SingleBandItemCount { get; }
|
|
|
|
public int SmallEdgeItemCount { get; }
|
|
|
|
public int LayerDepthDelta { get; }
|
|
|
|
public int MaxLayerDepth { get; }
|
|
|
|
public RowBuilder[] RowBuilders { get; }
|
|
}
|
|
}
|
|
}
|