// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; using System.Runtime.CompilerServices; using Castle.Core.Configuration; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Helpers; public class ParallelRowIteratorTests { public delegate void RowIntervalAction(RowInterval rows, Span span); private readonly ITestOutputHelper output; public ParallelRowIteratorTests(ITestOutputHelper output) { this.output = output; } /// /// maxDegreeOfParallelism, minY, maxY, expectedStepLength, expectedLastStepLength /// public static TheoryData IterateRows_OverMinimumPixelsLimit_Data = new() { { 1, 0, 100, -1, 100, 1 }, { 2, 0, 9, 5, 4, 2 }, { 4, 0, 19, 5, 4, 4 }, { 2, 10, 19, 5, 4, 2 }, { 4, 0, 200, 50, 50, 4 }, { 4, 123, 323, 50, 50, 4 }, { 4, 0, 1201, 301, 298, 4 }, { 8, 10, 236, 29, 23, 8 }, { 16, 0, 209, 14, 13, 15 }, { 24, 0, 209, 9, 2, 24 }, { 32, 0, 209, 7, 6, 30 }, { 64, 0, 209, 4, 1, 53 }, }; [Theory] [MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))] public void IterateRows_OverMinimumPixelsLimit_IntervalsAreCorrect( int maxDegreeOfParallelism, int minY, int maxY, int expectedStepLength, int expectedLastStepLength, int expectedNumberOfSteps) { ParallelExecutionSettings parallelSettings = new( maxDegreeOfParallelism, 1, Configuration.Default.MemoryAllocator); Rectangle rectangle = new(0, minY, 10, maxY - minY); int actualNumberOfSteps = 0; void RowAction(RowInterval rows) { Assert.True(rows.Min >= minY); Assert.True(rows.Max <= maxY); int step = rows.Max - rows.Min; int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength; Interlocked.Increment(ref actualNumberOfSteps); Assert.Equal(expected, step); } TestRowIntervalOperation operation = new(RowAction); ParallelRowIterator.IterateRowIntervals( rectangle, in parallelSettings, in operation); Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); } [Theory] [MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))] public void IterateRows_OverMinimumPixelsLimit_ShouldVisitAllRows( int maxDegreeOfParallelism, int minY, int maxY, int expectedStepLength, int expectedLastStepLength, int expectedNumberOfSteps) { ParallelExecutionSettings parallelSettings = new( maxDegreeOfParallelism, 1, Configuration.Default.MemoryAllocator); Rectangle rectangle = new(0, minY, 10, maxY - minY); int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray(); int[] actualData = new int[maxY]; void RowAction(RowInterval rows) { for (int y = rows.Min; y < rows.Max; y++) { actualData[y] = y; } } TestRowIntervalOperation operation = new(RowAction); ParallelRowIterator.IterateRowIntervals( rectangle, in parallelSettings, in operation); Assert.Equal(expectedData, actualData); } [Theory] [MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))] public void IterateRowsWithTempBuffer_OverMinimumPixelsLimit( int maxDegreeOfParallelism, int minY, int maxY, int expectedStepLength, int expectedLastStepLength, int expectedNumberOfSteps) { ParallelExecutionSettings parallelSettings = new( maxDegreeOfParallelism, 1, Configuration.Default.MemoryAllocator); Rectangle rectangle = new(0, minY, 10, maxY - minY); int actualNumberOfSteps = 0; void RowAction(RowInterval rows, Span buffer) { Assert.True(rows.Min >= minY); Assert.True(rows.Max <= maxY); int step = rows.Max - rows.Min; int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength; Interlocked.Increment(ref actualNumberOfSteps); Assert.Equal(expected, step); } TestRowIntervalOperation operation = new(RowAction); ParallelRowIterator.IterateRowIntervals, Vector4>( rectangle, in parallelSettings, in operation); Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); } [Theory] [MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))] public void IterateRowsWithTempBuffer_OverMinimumPixelsLimit_ShouldVisitAllRows( int maxDegreeOfParallelism, int minY, int maxY, int expectedStepLength, int expectedLastStepLength, int expectedNumberOfSteps) { ParallelExecutionSettings parallelSettings = new( maxDegreeOfParallelism, 1, Configuration.Default.MemoryAllocator); Rectangle rectangle = new(0, minY, 10, maxY - minY); int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray(); int[] actualData = new int[maxY]; void RowAction(RowInterval rows, Span buffer) { for (int y = rows.Min; y < rows.Max; y++) { actualData[y] = y; } } TestRowIntervalOperation operation = new(RowAction); ParallelRowIterator.IterateRowIntervals, Vector4>( rectangle, in parallelSettings, in operation); Assert.Equal(expectedData, actualData); } public static TheoryData IterateRows_WithEffectiveMinimumPixelsLimit_Data = new() { { 2, 200, 50, 2, 1, -1, 2 }, { 2, 200, 200, 1, 1, -1, 1 }, { 4, 200, 100, 4, 2, 2, 2 }, { 4, 300, 100, 8, 3, 3, 2 }, { 2, 5000, 1, 4500, 1, -1, 4500 }, { 2, 5000, 1, 5000, 1, -1, 5000 }, { 2, 5000, 1, 5001, 2, 2501, 2500 }, }; [Theory] [MemberData(nameof(IterateRows_WithEffectiveMinimumPixelsLimit_Data))] public void IterateRows_WithEffectiveMinimumPixelsLimit( int maxDegreeOfParallelism, int minimumPixelsProcessedPerTask, int width, int height, int expectedNumberOfSteps, int expectedStepLength, int expectedLastStepLength) { ParallelExecutionSettings parallelSettings = new( maxDegreeOfParallelism, minimumPixelsProcessedPerTask, Configuration.Default.MemoryAllocator); Rectangle rectangle = new(0, 0, width, height); int actualNumberOfSteps = 0; void RowAction(RowInterval rows) { Assert.True(rows.Min >= 0); Assert.True(rows.Max <= height); int step = rows.Max - rows.Min; int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength; Interlocked.Increment(ref actualNumberOfSteps); Assert.Equal(expected, step); } TestRowIntervalOperation operation = new(RowAction); ParallelRowIterator.IterateRowIntervals( rectangle, in parallelSettings, in operation); Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); } [Theory] [MemberData(nameof(IterateRows_WithEffectiveMinimumPixelsLimit_Data))] public void IterateRowsWithTempBuffer_WithEffectiveMinimumPixelsLimit( int maxDegreeOfParallelism, int minimumPixelsProcessedPerTask, int width, int height, int expectedNumberOfSteps, int expectedStepLength, int expectedLastStepLength) { ParallelExecutionSettings parallelSettings = new( maxDegreeOfParallelism, minimumPixelsProcessedPerTask, Configuration.Default.MemoryAllocator); Rectangle rectangle = new(0, 0, width, height); int actualNumberOfSteps = 0; void RowAction(RowInterval rows, Span buffer) { Assert.True(rows.Min >= 0); Assert.True(rows.Max <= height); int step = rows.Max - rows.Min; int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength; Interlocked.Increment(ref actualNumberOfSteps); Assert.Equal(expected, step); } TestRowIntervalOperation operation = new(RowAction); ParallelRowIterator.IterateRowIntervals, Vector4>( rectangle, in parallelSettings, in operation); Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); } public static readonly TheoryData IterateRectangularBuffer_Data = new() { { 8, 582, 453, 10, 10, 291, 226 }, // boundary data from DetectEdgesTest.DetectEdges_InBox { 2, 582, 453, 10, 10, 291, 226 }, { 16, 582, 453, 10, 10, 291, 226 }, { 16, 582, 453, 10, 10, 1, 226 }, { 16, 1, 453, 0, 10, 1, 226 }, }; [Theory] [MemberData(nameof(IterateRectangularBuffer_Data))] public void IterateRectangularBuffer( int maxDegreeOfParallelism, int bufferWidth, int bufferHeight, int rectX, int rectY, int rectWidth, int rectHeight) { MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; using (Buffer2D expected = memoryAllocator.Allocate2D(bufferWidth, bufferHeight, AllocationOptions.Clean)) using (Buffer2D actual = memoryAllocator.Allocate2D(bufferWidth, bufferHeight, AllocationOptions.Clean)) { Rectangle rect = new(rectX, rectY, rectWidth, rectHeight); void FillRow(int y, Buffer2D buffer) { for (int x = rect.Left; x < rect.Right; x++) { buffer[x, y] = new Point(x, y); } } // Fill Expected data: for (int y = rectY; y < rect.Bottom; y++) { FillRow(y, expected); } // Fill actual data using IterateRows: ParallelExecutionSettings settings = new(maxDegreeOfParallelism, memoryAllocator); void RowAction(RowInterval rows) { this.output.WriteLine(rows.ToString()); for (int y = rows.Min; y < rows.Max; y++) { FillRow(y, actual); } } TestRowIntervalOperation operation = new(RowAction); ParallelRowIterator.IterateRowIntervals( rect, settings, in operation); // Assert: TestImageExtensions.CompareBuffers(expected, actual); } } [Theory] [InlineData(0, 10)] [InlineData(10, 0)] [InlineData(-10, 10)] [InlineData(10, -10)] public void IterateRowsRequiresValidRectangle(int width, int height) { ParallelExecutionSettings parallelSettings = default(ParallelExecutionSettings); Rectangle rect = new(0, 0, width, height); void RowAction(RowInterval rows) { } TestRowIntervalOperation operation = new(RowAction); ArgumentOutOfRangeException ex = Assert.Throws( () => ParallelRowIterator.IterateRowIntervals(rect, in parallelSettings, in operation)); Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); } [Theory] [InlineData(0, 10)] [InlineData(10, 0)] [InlineData(-10, 10)] [InlineData(10, -10)] public void IterateRowsWithTempBufferRequiresValidRectangle(int width, int height) { ParallelExecutionSettings parallelSettings = default(ParallelExecutionSettings); Rectangle rect = new(0, 0, width, height); void RowAction(RowInterval rows, Span memory) { } TestRowIntervalOperation operation = new(RowAction); ArgumentOutOfRangeException ex = Assert.Throws( () => ParallelRowIterator.IterateRowIntervals, Rgba32>(rect, in parallelSettings, in operation)); Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); } [Fact] public void CanIterateWithoutIntOverflow() { ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(Configuration.Default); const int max = 100_000; Rectangle rect = new(0, 0, max, max); int intervalMaxY = 0; void RowAction(RowInterval rows, Span memory) => intervalMaxY = Math.Max(rows.Max, intervalMaxY); TestRowOperation operation = new(); TestRowIntervalOperation intervalOperation = new(RowAction); ParallelRowIterator.IterateRows(Configuration.Default, rect, in operation); Assert.Equal(max - 1, operation.MaxY.Value); ParallelRowIterator.IterateRowIntervals, Rgba32>(rect, in parallelSettings, in intervalOperation); Assert.Equal(max, intervalMaxY); } private readonly struct TestRowOperation : IRowOperation { public TestRowOperation() { } public StrongBox MaxY { get; } = new(); public void Invoke(int y) { lock (this.MaxY) { this.MaxY.Value = Math.Max(y, this.MaxY.Value); } } } private readonly struct TestRowIntervalOperation : IRowIntervalOperation { private readonly Action action; public TestRowIntervalOperation(Action action) => this.action = action; public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width; public void Invoke(in RowInterval rows) => this.action(rows); } private readonly struct TestRowIntervalOperation : IRowIntervalOperation where TBuffer : unmanaged { private readonly RowIntervalAction action; public TestRowIntervalOperation(RowIntervalAction action) => this.action = action; public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width; public void Invoke(in RowInterval rows, Span span) => this.action(rows, span); } }