// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Generic; using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Processing { /// /// Queues normalized composition commands emitted by /// and prepares them in deterministic draw order. /// /// /// The batcher owns command buffering and replay ordering only; it does not rasterize or composite. /// Draw commands are stored in the command buffer until a timeline command-range entry references /// them. Existing retained scenes passed through are stored /// separately and referenced by timeline entry index. During disposal replay, command ranges are /// lowered to short-lived backend scenes at the position where the canvas recorded the range. /// internal sealed class DrawingCanvasBatcher where TPixel : unmanaged, IPixel { private readonly Configuration configuration; // Draw commands stay in this buffer until replay lowers referenced command ranges // into backend scenes at their recorded timeline position. private CompositionSceneCommand[] commands; private int commandCount; private int sealedCommandCount; // Layer metadata is range-sensitive, so sealing advances this alongside command // sealing instead of letting layer state leak across later command ranges. private int layerCommandCount; private int sealedLayerCommandCount; // Clip and dash flags gate whole-buffer command preparation; prepared commands // remain in the same command buffer until replay consumes it. private bool hasClips; private bool hasDashes; // Timeline entries keep compact indexes into the command, barrier, and retained // scene buffers while preserving the order recorded by the canvas. private DrawingCanvasTimelineEntry[] entries; // Apply barriers carry replay-time target read/process/write operations. private ApplyBarrier[] applyBarriers; private int applyBarrierCount; // These are existing retained scenes recorded through RenderScene, not scenes // produced later from this batcher's own command ranges. private DrawingBackendScene[] insertedScenes; private int insertedSceneCount; internal DrawingCanvasBatcher(Configuration configuration) { this.configuration = configuration; this.commands = []; this.entries = []; this.applyBarriers = []; this.insertedScenes = []; } /// /// Gets a value indicating whether there are queued commands or timeline entries. /// public bool HasRecordedWork => this.commandCount > 0 || this.TimelineEntryCount > 0; /// /// Gets the number of ordered replay items recorded in the canvas timeline. /// /// /// This is not a draw-command count. A single entry can represent a contiguous command range, /// an apply barrier, or an inserted retained scene. /// public int TimelineEntryCount { get; private set; } /// /// Appends one normalized composition command to the pending queue. /// /// The command to queue. public void AddComposition(in CompositionCommand composition) { this.EnsureCommandCapacity(this.commandCount + 1); this.commands[this.commandCount++] = new PathCompositionSceneCommand(composition); if (composition.Kind is not CompositionCommandKind.FillLayer) { this.layerCommandCount++; } this.hasClips |= composition.ClipPaths is not null; } /// /// Appends one stroked path command to the pending queue. /// /// The command to queue. public void AddStrokePath(in StrokePathCommand command) { this.EnsureCommandCapacity(this.commandCount + 1); this.commands[this.commandCount++] = new StrokePathCompositionSceneCommand(command); this.hasClips |= command.ClipPaths is not null; this.hasDashes |= command.Pen.StrokePattern.Length >= 2; } /// /// Appends one explicit stroked line-segment command to the pending queue. /// /// The command to queue. public void AddStrokeLineSegment(in StrokeLineSegmentCommand command) { this.EnsureCommandCapacity(this.commandCount + 1); this.commands[this.commandCount++] = new LineSegmentCompositionSceneCommand(command); } /// /// Appends one explicit stroked polyline command to the pending queue. /// /// The command to queue. public void AddStrokePolyline(in StrokePolylineCommand command) { this.EnsureCommandCapacity(this.commandCount + 1); this.commands[this.commandCount++] = new PolylineCompositionSceneCommand(command); } /// /// Seals currently queued commands into the replay timeline. /// /// /// This records a command range only. Backend scenes are created later by the replay path /// from the referenced command range, so sealing does not render or allocate backend scene state. /// public void SealCommands() { int count = this.commandCount - this.sealedCommandCount; if (count == 0) { return; } this.EnsureEntryCapacity(this.TimelineEntryCount + 1); this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateCommandRange( this.sealedCommandCount, count, this.layerCommandCount != this.sealedLayerCommandCount); this.sealedCommandCount = this.commandCount; this.sealedLayerCommandCount = this.layerCommandCount; } /// /// Appends an apply barrier to the replay timeline after sealing queued commands. /// /// The apply barrier to append. internal void AddApplyBarrier(ApplyBarrier barrier) { this.SealCommands(); this.EnsureApplyBarrierCapacity(this.applyBarrierCount + 1); int barrierIndex = this.applyBarrierCount; this.applyBarriers[this.applyBarrierCount++] = barrier; this.EnsureEntryCapacity(this.TimelineEntryCount + 1); this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateApplyBarrier(barrierIndex); } /// /// Records an existing retained scene in the replay timeline after sealing queued commands. /// /// /// This stores only scenes passed to . Scenes produced /// from this canvas's own command ranges are created later by the backend from command batches. /// /// The retained scene to render at this point in the timeline. public void AddScene(DrawingBackendScene scene) { this.SealCommands(); this.EnsureInsertedSceneCapacity(this.insertedSceneCount + 1); int sceneIndex = this.insertedSceneCount; this.insertedScenes[this.insertedSceneCount++] = scene; this.EnsureEntryCapacity(this.TimelineEntryCount + 1); this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateScene(sceneIndex); } /// /// Creates a retained backend scene from the recorded timeline. /// /// The backend used to create the retained scene. /// The target bounds used for target-dependent scene creation. /// The resources that must stay alive for the returned scene. /// The retained backend scene. public DrawingBackendScene CreateScene( IDrawingBackend backend, Rectangle targetBounds, IReadOnlyList? ownedResources) { if (!this.HasRecordedWork) { throw new InvalidOperationException("Cannot create a retained scene from an empty canvas."); } this.SealAndPrepareCommands(); return backend.CreateScene( this.configuration, targetBounds, new DrawingCommandBatch(this.commands, this.commandCount, this.layerCommandCount > 0), ownedResources); } /// /// Seals any pending commands and prepares queued command data for backend scene creation. /// public void SealAndPrepareCommands() { this.SealCommands(); this.PrepareCommands(); } /// /// Creates a command batch over one recorded command-range timeline entry. /// /// The command-range timeline entry. /// The command batch. public DrawingCommandBatch CreateCommandBatch(DrawingCanvasTimelineEntry entry) => new(this.commands, entry.Index, entry.Count, entry.HasLayers); /// /// Gets one recorded timeline entry. /// /// The entry index. /// The recorded timeline entry. public DrawingCanvasTimelineEntry GetEntry(int index) => this.entries[index]; /// /// Gets one recorded apply barrier. /// /// The apply-barrier index. /// The recorded apply barrier. internal ApplyBarrier GetApplyBarrier(int index) => this.applyBarriers[index]; /// /// Gets one retained scene reference recorded through . /// /// The retained-scene reference index. /// The retained scene to render at the timeline entry. public DrawingBackendScene GetInsertedScene(int index) => this.insertedScenes[index]; /// /// Clears command references after a prepared batch has been consumed. /// public void ClearCommandBatch() { Array.Clear(this.commands, 0, this.commandCount); Array.Clear(this.entries, 0, this.TimelineEntryCount); Array.Clear(this.applyBarriers, 0, this.applyBarrierCount); Array.Clear(this.insertedScenes, 0, this.insertedSceneCount); this.commandCount = 0; this.sealedCommandCount = 0; this.layerCommandCount = 0; this.sealedLayerCommandCount = 0; this.TimelineEntryCount = 0; this.applyBarrierCount = 0; this.insertedSceneCount = 0; this.hasClips = false; this.hasDashes = false; } /// /// Ensures that the command buffer can store the requested command count without reallocating. /// /// The required command capacity. private void EnsureCommandCapacity(int requiredCapacity) { if (requiredCapacity <= this.commands.Length) { return; } int nextCapacity = this.commands.Length == 0 ? 16 : this.commands.Length * 2; if (nextCapacity < requiredCapacity) { nextCapacity = requiredCapacity; } Array.Resize(ref this.commands, nextCapacity); } /// /// Ensures that the timeline entry buffer can store the requested entry count without reallocating. /// /// The required entry capacity. private void EnsureEntryCapacity(int requiredCapacity) { if (requiredCapacity <= this.entries.Length) { return; } int nextCapacity = this.entries.Length == 0 ? 4 : this.entries.Length * 2; if (nextCapacity < requiredCapacity) { nextCapacity = requiredCapacity; } Array.Resize(ref this.entries, nextCapacity); } /// /// Ensures that the apply-barrier buffer can store the requested barrier count without reallocating. /// /// The required barrier capacity. private void EnsureApplyBarrierCapacity(int requiredCapacity) { if (requiredCapacity <= this.applyBarriers.Length) { return; } int nextCapacity = this.applyBarriers.Length == 0 ? 2 : this.applyBarriers.Length * 2; if (nextCapacity < requiredCapacity) { nextCapacity = requiredCapacity; } Array.Resize(ref this.applyBarriers, nextCapacity); } /// /// Ensures that the inserted-scene buffer can store the requested scene count without reallocating. /// /// The required scene capacity. private void EnsureInsertedSceneCapacity(int requiredCapacity) { if (requiredCapacity <= this.insertedScenes.Length) { return; } int nextCapacity = this.insertedScenes.Length == 0 ? 2 : this.insertedScenes.Length * 2; if (nextCapacity < requiredCapacity) { nextCapacity = requiredCapacity; } Array.Resize(ref this.insertedScenes, nextCapacity); } private void PrepareCommands() { if (!this.hasClips && !this.hasDashes) { return; } // If clipping is present we need to apply that now before handing the command // to the backend. This avoids complicating the backend with clipping logic // and allows us to reuse the same optimized backend code for clipped and unclipped paths. int requestedParallelism = this.configuration.MaxDegreeOfParallelism; int partitionCount = ParallelExecutionHelper.GetPartitionCount(requestedParallelism, this.commandCount); if (partitionCount <= 1) { for (int i = 0; i < this.commandCount; i++) { PrepareCommand(ref this.commands[i]); } return; } _ = Parallel.For( 0, partitionCount, ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, 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 * this.commandCount) / partitionCount; int commandEnd = ((partitionIndex + 1) * this.commandCount) / partitionCount; for (int i = commandStart; i < commandEnd; i++) { PrepareCommand(ref this.commands[i]); } }); } private static void PrepareCommand(ref CompositionSceneCommand command) { if (command is PathCompositionSceneCommand pathCommand) { CompositionCommand composition = pathCommand.Command; if (composition.ClipPaths is { Count: > 0 }) { IPath path = composition.SourcePath; DrawingOptions sourceOptions = composition.DrawingOptions; if (sourceOptions.Transform != Matrix4x4.Identity) { path = path.Transform(sourceOptions.Transform); } path = path.Clip(sourceOptions.ShapeOptions, composition.ClipPaths); RasterizerOptions rasterizerOptions = composition.RasterizerOptions; DrawingOptions preparedOptions = WithIdentityTransform(sourceOptions); // Update the command with the clipped path. pathCommand.Command = CompositionCommand.Create( path, composition.Brush.Transform(sourceOptions.Transform), preparedOptions, in rasterizerOptions, composition.TargetBounds, composition.DestinationOffset, null, composition.IsInsideLayer); } } else if (command is StrokePathCompositionSceneCommand strokePathCommand) { StrokePathCommand composition = strokePathCommand.Command; if (composition.ClipPaths is { Count: > 0 }) { IPath path = composition.Pen.GeneratePath(composition.SourcePath); DrawingOptions sourceOptions = composition.DrawingOptions; if (sourceOptions.Transform != Matrix4x4.Identity) { path = path.Transform(sourceOptions.Transform); } path = path.Clip(sourceOptions.ShapeOptions, composition.ClipPaths); RasterizerOptions rasterizerOptions = composition.RasterizerOptions; DrawingOptions preparedOptions = WithIdentityTransform(sourceOptions); command = new PathCompositionSceneCommand( CompositionCommand.Create( path, composition.Brush.Transform(sourceOptions.Transform), preparedOptions, in rasterizerOptions, composition.TargetBounds, composition.DestinationOffset, null, composition.IsInsideLayer)); } else { // We need to dash the path here before sending it to the backend. Pen pen = composition.Pen; if (pen.StrokePattern.Length >= 2) { strokePathCommand.Command = new StrokePathCommand( composition.SourcePath.GenerateDashes(pen.StrokeWidth, pen.StrokePattern.Span), composition.Brush, composition.DrawingOptions, composition.RasterizerOptions, composition.TargetBounds, composition.DestinationOffset, composition.Pen, null, composition.IsInsideLayer); } } } } private static DrawingOptions WithIdentityTransform(DrawingOptions source) => source.Transform == Matrix4x4.Identity ? source : new DrawingOptions(source.GraphicsOptions, source.ShapeOptions, Matrix4x4.Identity); } }