// 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 { /// /// Represents a flush-ready CPU scene built from retained row-local raster payload. /// 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: []); /// /// Initializes a new instance of the class. /// 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; } /// /// Gets the number of visible draw items retained by the scene. /// public int ItemCount => this.FillItemCount + this.StrokeItemCount; /// /// Gets the number of visible fill items retained by the scene. /// public int FillItemCount { get; } /// /// Gets the number of visible stroke items retained by the scene. /// public int StrokeItemCount { get; } /// /// Gets the retained visible scene items. /// internal FillSceneItem?[] FillItems { get; } /// /// Gets the retained visible stroke scene items. /// internal StrokeSceneItem?[] StrokeItems { get; } /// /// Gets the retained layer options indexed by begin-layer command index. /// internal GraphicsOptions?[] LayerOptions { get; } /// /// Gets the number of scene rows containing executable work. /// public int RowCount { get; } /// /// Gets the retained row lists. /// internal SceneRow[] Rows { get; } /// /// Gets the total number of row items retained by the scene. /// public int RowItemCount { get; } /// /// Gets the total number of encoded raster edges retained by the scene. /// public long TotalEdgeCount { get; } /// /// Gets the number of items that occupy a single row band. /// public int SingleBandItemCount { get; } /// /// Gets the number of items whose retained edge count is small. /// public int SmallEdgeItemCount { get; } /// /// Gets the maximum retained layer nesting depth in this scene. /// public int MaxLayerDepth { get; } /// /// Creates a new scene by scheduling visible draw operations directly over retained rasterizable geometry. /// /// The prepared composition scene. /// The destination bounds of the flush. /// The allocator used for retained row storage. /// /// The maximum degree of parallelism to use when building the scene, or -1 to pass /// through the runtime's unlimited sentinel for . /// /// A flush-ready scene. public static FlushScene Create( DrawingCommandBatch scene, in Rectangle targetBounds, MemoryAllocator allocator, int maxDegreeOfParallelism) { int commandCount = scene.CommandCount; if (commandCount == 0) { return Empty(); } IReadOnlyList 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); } /// /// Releases retained scene storage. /// 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(); } } /// /// Creates an empty scene instance. /// private static FlushScene Empty() => EmptyScene; /// /// Identifies whether a path-backed command contributes executable retained raster work to the scene. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsSceneDrawable(in CompositionCommand command) => command.Kind == CompositionCommandKind.FillLayer; /// /// Accumulates retained fill statistics used for scene heuristics. /// 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++; } } /// /// Accumulates retained stroke statistics used for scene heuristics. /// 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++; } } /// /// Appends retained fill row operations for one item into the row builders owned by the current partition. /// 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)); } } /// /// Appends retained stroke row operations for one item into the row builders owned by the current partition. /// 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)); } } /// /// 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 /// . 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. /// /// The command's absolute target bounds. /// The first row-band index covered by the partition. /// The total number of row slots owned by the partition. /// True if the command was recorded inside a SaveLayer scope. /// The first row slot the command may write to. /// The exclusive end row slot the command may write to. 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); } /// /// Identifies whether a command contributes retained per-row layer control operations. /// 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; } /// /// Finalizes row-owned append builders into immutable scene rows. /// 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; } /// /// Disposes partially created row builders. /// private static void DisposeRows(RowBuilder[] builders) { for (int i = 0; i < builders.Length; i++) { builders[i].Dispose(); } } private static PartitionState ProcessPartition( IReadOnlyList 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; } /// /// Returns the isotropic scale factor embedded in a drawing transform so stroke widths match device-space pixels. /// /// /// 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. /// 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; } } } }