diff --git a/src/ImageSharp/Advanced/ParallelExecutionSettings.cs b/src/ImageSharp/Advanced/ParallelExecutionSettings.cs
index fd9692f9ae..ad0318297a 100644
--- a/src/ImageSharp/Advanced/ParallelExecutionSettings.cs
+++ b/src/ImageSharp/Advanced/ParallelExecutionSettings.cs
@@ -18,7 +18,10 @@ public readonly struct ParallelExecutionSettings
///
/// Initializes a new instance of the struct.
///
- /// The value used for initializing when using TPL.
+ ///
+ /// The value used for initializing when using TPL.
+ /// Set to -1 to leave the degree of parallelism unbounded.
+ ///
/// The value for .
/// The .
public ParallelExecutionSettings(
@@ -44,7 +47,10 @@ public ParallelExecutionSettings(
///
/// Initializes a new instance of the struct.
///
- /// The value used for initializing when using TPL.
+ ///
+ /// The value used for initializing when using TPL.
+ /// Set to -1 to leave the degree of parallelism unbounded.
+ ///
/// The .
public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator)
: this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator)
@@ -58,6 +64,7 @@ public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator mem
///
/// Gets the value used for initializing when using TPL.
+ /// A value of -1 leaves the degree of parallelism unbounded.
///
public int MaxDegreeOfParallelism { get; }
diff --git a/src/ImageSharp/Advanced/ParallelRowIterator.cs b/src/ImageSharp/Advanced/ParallelRowIterator.cs
index d170631a29..98c2656d11 100644
--- a/src/ImageSharp/Advanced/ParallelRowIterator.cs
+++ b/src/ImageSharp/Advanced/ParallelRowIterator.cs
@@ -44,14 +44,14 @@ public static void IterateRows(
where T : struct, IRowOperation
{
ValidateRectangle(rectangle);
+ ValidateSettings(parallelSettings);
int top = rectangle.Top;
int bottom = rectangle.Bottom;
int width = rectangle.Width;
int height = rectangle.Height;
- int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
- int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
+ int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
@@ -65,7 +65,7 @@ public static void IterateRows(
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
- ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
+ ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowOperationWrapper wrappingOperation = new(top, bottom, verticalStep, in operation);
_ = Parallel.For(
@@ -109,14 +109,14 @@ public static void IterateRows(
where TBuffer : unmanaged
{
ValidateRectangle(rectangle);
+ ValidateSettings(parallelSettings);
int top = rectangle.Top;
int bottom = rectangle.Bottom;
int width = rectangle.Width;
int height = rectangle.Height;
- int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
- int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
+ int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
@@ -135,7 +135,7 @@ public static void IterateRows(
}
int verticalStep = DivideCeil(height, numOfSteps);
- ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
+ ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowOperationWrapper wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
_ = Parallel.For(
@@ -174,14 +174,14 @@ public static void IterateRowIntervals(
where T : struct, IRowIntervalOperation
{
ValidateRectangle(rectangle);
+ ValidateSettings(parallelSettings);
int top = rectangle.Top;
int bottom = rectangle.Bottom;
int width = rectangle.Width;
int height = rectangle.Height;
- int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
- int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
+ int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
@@ -192,7 +192,7 @@ public static void IterateRowIntervals(
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
- ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
+ ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowIntervalOperationWrapper wrappingOperation = new(top, bottom, verticalStep, in operation);
_ = Parallel.For(
@@ -236,14 +236,14 @@ public static void IterateRowIntervals(
where TBuffer : unmanaged
{
ValidateRectangle(rectangle);
+ ValidateSettings(parallelSettings);
int top = rectangle.Top;
int bottom = rectangle.Bottom;
int width = rectangle.Width;
int height = rectangle.Height;
- int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
- int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
+ int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
@@ -259,7 +259,7 @@ public static void IterateRowIntervals(
}
int verticalStep = DivideCeil(height, numOfSteps);
- ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
+ ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowIntervalOperationWrapper wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
_ = Parallel.For(
@@ -272,6 +272,37 @@ public static void IterateRowIntervals(
[MethodImpl(InliningOptions.ShortMethod)]
private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);
+ ///
+ /// Creates the for the current iteration.
+ ///
+ /// The execution settings.
+ /// The number of row partitions to execute.
+ /// The instance.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static ParallelOptions CreateParallelOptions(in ParallelExecutionSettings parallelSettings, int numOfSteps)
+ => new() { MaxDegreeOfParallelism = parallelSettings.MaxDegreeOfParallelism == -1 ? -1 : numOfSteps };
+
+ ///
+ /// Calculates the number of row partitions to execute for the given region.
+ ///
+ /// The width of the region.
+ /// The height of the region.
+ /// The execution settings.
+ /// The number of row partitions to execute.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static int GetNumberOfSteps(int width, int height, in ParallelExecutionSettings parallelSettings)
+ {
+ int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
+
+ if (parallelSettings.MaxDegreeOfParallelism == -1)
+ {
+ // Row batching cannot produce more useful partitions than the number of rows available.
+ return Math.Min(height, maxSteps);
+ }
+
+ return Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
+ }
+
private static void ValidateRectangle(Rectangle rectangle)
{
Guard.MustBeGreaterThan(
@@ -284,4 +315,35 @@ private static void ValidateRectangle(Rectangle rectangle)
0,
$"{nameof(rectangle)}.{nameof(rectangle.Height)}");
}
+
+ ///
+ /// Validates the supplied .
+ ///
+ /// The execution settings.
+ ///
+ /// Thrown when or
+ /// is invalid.
+ ///
+ ///
+ /// Thrown when is null.
+ /// This also guards the public default value, which bypasses constructor validation.
+ ///
+ private static void ValidateSettings(in ParallelExecutionSettings parallelSettings)
+ {
+ // ParallelExecutionSettings is a public struct, so callers can pass default and bypass constructor validation.
+ if (parallelSettings.MaxDegreeOfParallelism is 0 or < -1)
+ {
+ throw new ArgumentOutOfRangeException(
+ $"{nameof(parallelSettings)}.{nameof(ParallelExecutionSettings.MaxDegreeOfParallelism)}");
+ }
+
+ Guard.MustBeGreaterThan(
+ parallelSettings.MinimumPixelsProcessedPerTask,
+ 0,
+ $"{nameof(parallelSettings)}.{nameof(ParallelExecutionSettings.MinimumPixelsProcessedPerTask)}");
+
+ Guard.NotNull(
+ parallelSettings.MemoryAllocator,
+ $"{nameof(parallelSettings)}.{nameof(ParallelExecutionSettings.MemoryAllocator)}");
+ }
}
diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs
index c2b02dedd9..2673927231 100644
--- a/src/ImageSharp/Configuration.cs
+++ b/src/ImageSharp/Configuration.cs
@@ -64,6 +64,7 @@ public Configuration(params IImageFormatConfigurationModule[] configurationModul
///
/// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms
/// configured with this instance.
+ /// Set to -1 to leave the degree of parallelism unbounded.
/// Initialized with by default.
///
public int MaxDegreeOfParallelism
diff --git a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
index 4b06f877fc..cf68f702ac 100644
--- a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
+++ b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
@@ -13,6 +13,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers;
public class ParallelRowIteratorTests
{
+ public delegate void BufferedRowAction(int y, Span span);
public delegate void RowIntervalAction(RowInterval rows, Span span);
private readonly ITestOutputHelper output;
@@ -200,6 +201,47 @@ void RowAction(RowInterval rows, Span buffer)
Assert.Equal(expectedData, actualData);
}
+ [Fact]
+ public void IterateRows_MaxDegreeOfParallelismMinusOne_ShouldVisitAllRows()
+ {
+ ParallelExecutionSettings parallelSettings = new(
+ -1,
+ 10,
+ Configuration.Default.MemoryAllocator);
+
+ Rectangle rectangle = new(0, 0, 10, 10);
+ int[] actualData = new int[rectangle.Height];
+
+ void RowAction(int y) => actualData[y]++;
+
+ TestRowActionOperation operation = new(RowAction);
+
+ ParallelRowIterator.IterateRows(
+ rectangle,
+ in parallelSettings,
+ in operation);
+
+ Assert.Equal(Enumerable.Repeat(1, rectangle.Height), actualData);
+ }
+
+ [Fact]
+ public void IterateRowsWithTempBuffer_DefaultSettingsRequireInitialization()
+ {
+ ParallelExecutionSettings parallelSettings = default;
+ Rectangle rect = new(0, 0, 10, 10);
+
+ void RowAction(int y, Span memory)
+ {
+ }
+
+ TestRowOperation operation = new(RowAction);
+
+ ArgumentOutOfRangeException ex = Assert.Throws(
+ () => ParallelRowIterator.IterateRows, Rgba32>(rect, in parallelSettings, in operation));
+
+ Assert.Contains(nameof(ParallelExecutionSettings.MaxDegreeOfParallelism), ex.Message);
+ }
+
public static TheoryData IterateRows_WithEffectiveMinimumPixelsLimit_Data =
new()
{
@@ -296,6 +338,53 @@ void RowAction(RowInterval rows, Span buffer)
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
}
+ [Fact]
+ public void IterateRowIntervalsWithTempBuffer_MaxDegreeOfParallelismMinusOne_ShouldVisitAllRows()
+ {
+ ParallelExecutionSettings parallelSettings = new(
+ -1,
+ 10,
+ Configuration.Default.MemoryAllocator);
+
+ Rectangle rectangle = new(0, 0, 10, 10);
+ int[] actualData = new int[rectangle.Height];
+
+ void RowAction(RowInterval rows, Span buffer)
+ {
+ for (int y = rows.Min; y < rows.Max; y++)
+ {
+ actualData[y]++;
+ }
+ }
+
+ TestRowIntervalOperation operation = new(RowAction);
+
+ ParallelRowIterator.IterateRowIntervals, Vector4>(
+ rectangle,
+ in parallelSettings,
+ in operation);
+
+ Assert.Equal(Enumerable.Repeat(1, rectangle.Height), actualData);
+ }
+
+ [Fact]
+ public void IterateRows_DefaultSettingsRequireInitialization()
+ {
+ ParallelExecutionSettings parallelSettings = default;
+ Rectangle rect = new(0, 0, 10, 10);
+
+ void RowAction(int y)
+ {
+ }
+
+ TestRowActionOperation operation = new(RowAction);
+
+ ArgumentOutOfRangeException ex = Assert.Throws(
+ () => ParallelRowIterator.IterateRows(rect, in parallelSettings, in operation));
+
+ Assert.Contains(nameof(ParallelExecutionSettings.MaxDegreeOfParallelism), ex.Message);
+ }
+
public static readonly TheoryData IterateRectangularBuffer_Data =
new()
{
@@ -445,6 +534,32 @@ public void Invoke(int y)
}
}
+ private readonly struct TestRowActionOperation : IRowOperation
+ {
+ private readonly Action action;
+
+ public TestRowActionOperation(Action action)
+ => this.action = action;
+
+ public void Invoke(int y)
+ => this.action(y);
+ }
+
+ private readonly struct TestRowOperation : IRowOperation
+ where TBuffer : unmanaged
+ {
+ private readonly BufferedRowAction action;
+
+ public TestRowOperation(BufferedRowAction action)
+ => this.action = action;
+
+ public int GetRequiredBufferLength(Rectangle bounds)
+ => bounds.Width;
+
+ public void Invoke(int y, Span span)
+ => this.action(y, span);
+ }
+
private readonly struct TestRowIntervalOperation : IRowIntervalOperation
{
private readonly Action action;