diff --git a/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs b/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs index 8b46fc5c31..dfaec640b8 100644 --- a/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs +++ b/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs @@ -15,12 +15,12 @@ public interface IRowOperation /// /// The bounds of the operation. /// The required buffer length. - int GetRequiredBufferLength(Rectangle bounds); + public int GetRequiredBufferLength(Rectangle bounds); /// /// Invokes the method passing the row and a buffer. /// /// The row y coordinate. /// The contiguous region of memory. - void Invoke(int y, Span span); + public void Invoke(int y, Span span); } diff --git a/src/ImageSharp/Advanced/ParallelRowIterator.cs b/src/ImageSharp/Advanced/ParallelRowIterator.cs index b878f9ec0a..d170631a29 100644 --- a/src/ImageSharp/Advanced/ParallelRowIterator.cs +++ b/src/ImageSharp/Advanced/ParallelRowIterator.cs @@ -68,7 +68,7 @@ public static void IterateRows( ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; RowOperationWrapper wrappingOperation = new(top, bottom, verticalStep, in operation); - Parallel.For( + _ = Parallel.For( 0, numOfSteps, parallelOptions, @@ -138,7 +138,7 @@ public static void IterateRows( ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; RowOperationWrapper wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation); - Parallel.For( + _ = Parallel.For( 0, numOfSteps, parallelOptions, @@ -195,7 +195,7 @@ public static void IterateRowIntervals( ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; RowIntervalOperationWrapper wrappingOperation = new(top, bottom, verticalStep, in operation); - Parallel.For( + _ = Parallel.For( 0, numOfSteps, parallelOptions, @@ -262,7 +262,7 @@ public static void IterateRowIntervals( ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; RowIntervalOperationWrapper wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation); - Parallel.For( + _ = Parallel.For( 0, numOfSteps, parallelOptions, diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index a237054133..71e609fbcb 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Numerics; using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; @@ -456,15 +457,21 @@ private static void DrawDecodedImageFrameOnCanvas( // The destination frame has already been prepopulated with the pixel data from the previous frame // so blending will leave the desired result which takes into consideration restoration to the // background color within the restore area. - PixelBlender blender = - PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + PixelBlender blender = PixelOperations.Instance.GetPixelBlender( + PixelColorBlendingMode.Normal, + PixelAlphaCompositionMode.SrcOver); + + // By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend + // We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending. + using IMemoryOwner workingBufferOwner = imageFrame.Configuration.MemoryAllocator.Allocate(restoreArea.Width * 3); + Span workingBuffer = workingBufferOwner.GetSpan(); for (int y = 0; y < restoreArea.Height; y++) { Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; - blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f); + blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f, workingBuffer); } return; diff --git a/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs b/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs index bad9ae01c1..fcd28796fb 100644 --- a/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs +++ b/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Numerics; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.PixelFormats; @@ -52,16 +51,53 @@ public void Blend( Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 3); - Span destinationVectors = buffer.Slice(0, maxLength); - Span backgroundVectors = buffer.Slice(maxLength, maxLength); - Span sourceVectors = buffer.Slice(maxLength * 2, maxLength); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 3)]); + } + + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + float amount, + Span workingBuffer) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + Span sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength); PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); PixelOperations.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale); this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount); - PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); } /// @@ -87,14 +123,48 @@ public void Blend( Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); - Span destinationVectors = buffer.Slice(0, maxLength); - Span backgroundVectors = buffer.Slice(maxLength, maxLength); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 2)]); + } + + /// + /// Blends a row against a constant source color using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 2 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + float amount, + Span workingBuffer) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount); - PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); } /// @@ -116,6 +186,27 @@ public void Blend( ReadOnlySpan amount) => this.Blend(configuration, destination, background, source, amount); + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + Span workingBuffer) + => this.Blend(configuration, destination, background, source, amount, workingBuffer); + /// /// Blends 2 rows together /// @@ -142,20 +233,89 @@ public void Blend( Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 3); - Span destinationVectors = buffer.Slice(0, maxLength); - Span backgroundVectors = buffer.Slice(maxLength, maxLength); - Span sourceVectors = buffer.Slice(maxLength * 2, maxLength); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 3)]); + } + + /// + /// Blends a row against a constant source color. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + ReadOnlySpan amount) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 2)]); + } + + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + Span workingBuffer) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + Span sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength); PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); PixelOperations.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale); this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount); - PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); } /// - /// Blends a row against a constant source color. + /// Blends a row against a constant source color using caller-provided temporary vector scratch. /// /// to use internally /// the destination span @@ -165,26 +325,28 @@ public void Blend( /// A span with values between 0 and 1 indicating the weight of the second source vector. /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. /// + /// Reusable temporary vector scratch with capacity for at least 2 rows. public void Blend( Configuration configuration, Span destination, ReadOnlySpan background, TPixel source, - ReadOnlySpan amount) + ReadOnlySpan amount, + Span workingBuffer) { int maxLength = destination.Length; Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length)); - using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); - Span destinationVectors = buffer.Slice(0, maxLength); - Span backgroundVectors = buffer.Slice(maxLength, maxLength); + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount); - PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); } /// diff --git a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs index 7a1ffb85f9..f2faee4f9c 100644 --- a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs +++ b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -145,7 +146,7 @@ protected override void OnFrameApply(ImageFrame source) this.Blender, this.Opacity); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRows( configuration, new Rectangle(0, 0, foregroundRectangle.Width, foregroundRectangle.Height), in operation); @@ -161,7 +162,7 @@ protected override void OnFrameApply(ImageFrame source) /// /// A implementing the draw logic for . /// - private readonly struct RowOperation : IRowOperation + private readonly struct RowOperation : IRowOperation { private readonly Buffer2D background; private readonly Buffer2D foreground; @@ -190,13 +191,20 @@ public RowOperation( this.opacity = opacity; } + /// + public int GetRequiredBufferLength(Rectangle bounds) + + // By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend + // We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending. + => 3 * bounds.Width; + /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(int y) + public void Invoke(int y, Span span) { Span background = this.background.DangerousGetRowSpan(y + this.backgroundRectangle.Top).Slice(this.backgroundRectangle.Left, this.backgroundRectangle.Width); Span foreground = this.foreground.DangerousGetRowSpan(y + this.foregroundRectangle.Top).Slice(this.foregroundRectangle.Left, this.foregroundRectangle.Width); - this.blender.Blend(this.configuration, background, background, foreground, this.opacity); + this.blender.Blend(this.configuration, background, background, foreground, this.opacity, span); } } }