diff --git a/README.md b/README.md index cf58b6b14b..dc30734792 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ SixLabors.ImageSharp
[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp/actions) -[![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp) +[![codecov](https://codecov.io/gh/SixLabors/ImageSharp/graph/badge.svg?token=g2WJwz770q)](https://codecov.io/gh/SixLabors/ImageSharp) [![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) [![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors) diff --git a/shared-infrastructure b/shared-infrastructure index 57699ffb79..a1d3ac2049 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 57699ffb797bc2389c5d6cbb3b1800f2eb5fb947 +Subproject commit a1d3ac20494631e3cc13132897573796b0e4ee6d diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index ff28063f05..efe68977bb 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -1080,4 +1080,47 @@ internal static class Numerics public static nuint Vector512Count(int length) where TVector : struct => (uint)length / (uint)Vector512.Count; + + /// + /// Normalizes the values in a given . + /// + /// The sequence of values to normalize. + /// The sum of the values in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Normalize(Span span, float sum) + { + if (Vector256.IsHardwareAccelerated) + { + ref float startRef = ref MemoryMarshal.GetReference(span); + ref float endRef = ref Unsafe.Add(ref startRef, span.Length & ~7); + Vector256 sum256 = Vector256.Create(sum); + + while (Unsafe.IsAddressLessThan(ref startRef, ref endRef)) + { + Unsafe.As>(ref startRef) /= sum256; + startRef = ref Unsafe.Add(ref startRef, (nuint)8); + } + + if ((span.Length & 7) >= 4) + { + Unsafe.As>(ref startRef) /= sum256.GetLower(); + startRef = ref Unsafe.Add(ref startRef, (nuint)4); + } + + endRef = ref Unsafe.Add(ref startRef, span.Length & 3); + + while (Unsafe.IsAddressLessThan(ref startRef, ref endRef)) + { + startRef /= sum; + startRef = ref Unsafe.Add(ref startRef, (nuint)1); + } + } + else + { + for (int i = 0; i < span.Length; i++) + { + span[i] /= sum; + } + } + } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index 26cf60034d..a85487d1c1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -5,7 +5,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -14,11 +14,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms; /// internal readonly unsafe struct ResizeKernel { + /// + /// The buffer with the convolution factors. + /// Note that when FMA is supported, this is of size 4x that reported in . + /// private readonly float* bufferPtr; /// /// Initializes a new instance of the struct. /// + /// The starting index for the destination row. + /// The pointer to the buffer with the convolution factors. + /// The length of the kernel. [MethodImpl(InliningOptions.ShortMethod)] internal ResizeKernel(int startIndex, float* bufferPtr, int length) { @@ -27,6 +34,15 @@ internal readonly unsafe struct ResizeKernel this.Length = length; } + /// + /// Gets a value indicating whether vectorization is supported. + /// + public static bool IsHardwareAccelerated + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Vector256.IsHardwareAccelerated; + } + /// /// Gets the start index for the destination row. /// @@ -53,7 +69,15 @@ internal readonly unsafe struct ResizeKernel public Span Values { [MethodImpl(InliningOptions.ShortMethod)] - get => new(this.bufferPtr, this.Length); + get + { + if (Vector256.IsHardwareAccelerated) + { + return new(this.bufferPtr, this.Length * 4); + } + + return new(this.bufferPtr, this.Length); + } } /// @@ -68,73 +92,45 @@ internal readonly unsafe struct ResizeKernel [MethodImpl(InliningOptions.ShortMethod)] public Vector4 ConvolveCore(ref Vector4 rowStartRef) { - if (Avx2.IsSupported && Fma.IsSupported) + if (IsHardwareAccelerated) { float* bufferStart = this.bufferPtr; - float* bufferEnd = bufferStart + (this.Length & ~3); + ref Vector4 rowEndRef = ref Unsafe.Add(ref rowStartRef, this.Length & ~3); Vector256 result256_0 = Vector256.Zero; Vector256 result256_1 = Vector256.Zero; - ReadOnlySpan maskBytes = - [ - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 1, 0, 0, 0, 1, 0, 0, 0, - 1, 0, 0, 0, 1, 0, 0, 0 - ]; - Vector256 mask = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetReference(maskBytes)); - while (bufferStart < bufferEnd) + while (Unsafe.IsAddressLessThan(ref rowStartRef, ref rowEndRef)) { - // It is important to use a single expression here so that the JIT will correctly use vfmadd231ps - // for the FMA operation, and execute it directly on the target register and reading directly from - // memory for the first parameter. This skips initializing a SIMD register, and an extra copy. - // The code below should compile in the following assembly on .NET 5 x64: - // - // vmovsd xmm2, [rax] ; load *(double*)bufferStart into xmm2 as [ab, _] - // vpermps ymm2, ymm1, ymm2 ; permute as a float YMM register to [a, a, a, a, b, b, b, b] - // vfmadd231ps ymm0, ymm2, [r8] ; result256_0 = FMA(pixels, factors) + result256_0 - // - // For tracking the codegen issue with FMA, see: https://github.com/dotnet/runtime/issues/12212. - // Additionally, we're also unrolling two computations per each loop iterations to leverage the - // fact that most CPUs have two ports to schedule multiply operations for FMA instructions. - result256_0 = Fma.MultiplyAdd( - Unsafe.As>(ref rowStartRef), - Avx2.PermuteVar8x32(Vector256.CreateScalarUnsafe(*(double*)bufferStart).AsSingle(), mask), - result256_0); - - result256_1 = Fma.MultiplyAdd( - Unsafe.As>(ref Unsafe.Add(ref rowStartRef, 2)), - Avx2.PermuteVar8x32(Vector256.CreateScalarUnsafe(*(double*)(bufferStart + 2)).AsSingle(), mask), - result256_1); - - bufferStart += 4; - rowStartRef = ref Unsafe.Add(ref rowStartRef, 4); + Vector256 pixels256_0 = Unsafe.As>(ref rowStartRef); + Vector256 pixels256_1 = Unsafe.As>(ref Unsafe.Add(ref rowStartRef, (nuint)2)); + + result256_0 = Vector256_.MultiplyAdd(result256_0, Vector256.Load(bufferStart), pixels256_0); + result256_1 = Vector256_.MultiplyAdd(result256_1, Vector256.Load(bufferStart + 8), pixels256_1); + + bufferStart += 16; + rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)4); } - result256_0 = Avx.Add(result256_0, result256_1); + result256_0 += result256_1; if ((this.Length & 3) >= 2) { - result256_0 = Fma.MultiplyAdd( - Unsafe.As>(ref rowStartRef), - Avx2.PermuteVar8x32(Vector256.CreateScalarUnsafe(*(double*)bufferStart).AsSingle(), mask), - result256_0); + Vector256 pixels256_0 = Unsafe.As>(ref rowStartRef); + result256_0 = Vector256_.MultiplyAdd(result256_0, Vector256.Load(bufferStart), pixels256_0); - bufferStart += 2; - rowStartRef = ref Unsafe.Add(ref rowStartRef, 2); + bufferStart += 8; + rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)2); } - Vector128 result128 = Sse.Add(result256_0.GetLower(), result256_0.GetUpper()); + Vector128 result128 = result256_0.GetLower() + result256_0.GetUpper(); if ((this.Length & 1) != 0) { - result128 = Fma.MultiplyAdd( - Unsafe.As>(ref rowStartRef), - Vector128.Create(*bufferStart), - result128); + Vector128 pixels128 = Unsafe.As>(ref rowStartRef); + result128 = Vector128_.MultiplyAdd(result128, Vector128.Load(bufferStart), pixels128); } - return *(Vector4*)&result128; + return result128.AsVector4(); } else { @@ -149,7 +145,7 @@ internal readonly unsafe struct ResizeKernel result += rowStartRef * *bufferStart; bufferStart++; - rowStartRef = ref Unsafe.Add(ref rowStartRef, 1); + rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)1); } return result; @@ -160,17 +156,32 @@ internal readonly unsafe struct ResizeKernel /// Copy the contents of altering /// to the value . /// + /// The new value for . [MethodImpl(InliningOptions.ShortMethod)] internal ResizeKernel AlterLeftValue(int left) => new(left, this.bufferPtr, this.Length); - internal void Fill(Span values) + internal void FillOrCopyAndExpand(Span values) { DebugGuard.IsTrue(values.Length == this.Length, nameof(values), "ResizeKernel.Fill: values.Length != this.Length!"); - for (int i = 0; i < this.Length; i++) + if (IsHardwareAccelerated) + { + Vector4* bufferStart = (Vector4*)this.bufferPtr; + ref float valuesStart = ref MemoryMarshal.GetReference(values); + ref float valuesEnd = ref Unsafe.Add(ref valuesStart, values.Length); + + while (Unsafe.IsAddressLessThan(ref valuesStart, ref valuesEnd)) + { + *bufferStart = new Vector4(valuesStart); + + bufferStart++; + valuesStart = ref Unsafe.Add(ref valuesStart, (nuint)1); + } + } + else { - this.Values[i] = (float)values[i]; + values.CopyTo(this.Values); } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs index ee1ada43ad..d606738abd 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs @@ -16,6 +16,8 @@ internal partial class ResizeKernelMap private readonly int cornerInterval; + private readonly int sourcePeriod; + public PeriodicKernelMap( MemoryAllocator memoryAllocator, int sourceLength, @@ -24,7 +26,8 @@ internal partial class ResizeKernelMap double scale, int radius, int period, - int cornerInterval) + int cornerInterval, + int sourcePeriod) : base( memoryAllocator, sourceLength, @@ -36,6 +39,7 @@ internal partial class ResizeKernelMap { this.cornerInterval = cornerInterval; this.period = period; + this.sourcePeriod = sourcePeriod; } internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}"; @@ -54,10 +58,11 @@ internal partial class ResizeKernelMap int bottomStartDest = this.DestinationLength - this.cornerInterval; for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++) { - double center = ((i + .5) * this.ratio) - .5; - int left = (int)TolerantMath.Ceiling(center - this.radius); ResizeKernel kernel = this.kernels[i - this.period]; - this.kernels[i] = kernel.AlterLeftValue(left); + + // Shift the kernel start index by the source-side period so the same weights align to the + // next repeated sampling window in the source image. + this.kernels[i] = kernel.AlterLeftValue(kernel.StartIndex + this.sourcePeriod); } // Build bottom corner data: diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index bd4a18c2fb..0b8106e0be 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -33,7 +33,7 @@ internal partial class ResizeKernelMap : IDisposable private bool isDisposed; // To avoid both GC allocations, and MemoryAllocator ceremony: - private readonly double[] tempValues; + private readonly float[] tempValues; private ResizeKernelMap( MemoryAllocator memoryAllocator, @@ -50,10 +50,12 @@ internal partial class ResizeKernelMap : IDisposable this.sourceLength = sourceLength; this.DestinationLength = destinationLength; this.MaxDiameter = (radius * 2) + 1; - this.data = memoryAllocator.Allocate2D(this.MaxDiameter, bufferHeight, preferContiguosImageBuffers: true, AllocationOptions.Clean); + + int diameter = ResizeKernel.IsHardwareAccelerated ? this.MaxDiameter * 4 : this.MaxDiameter; + this.data = memoryAllocator.Allocate2D(diameter, bufferHeight, preferContiguosImageBuffers: true); this.pinHandle = this.data.DangerousGetSingleMemory().Pin(); this.kernels = new ResizeKernel[destinationLength]; - this.tempValues = new double[this.MaxDiameter]; + this.tempValues = new float[this.MaxDiameter]; } /// @@ -135,9 +137,13 @@ internal partial class ResizeKernelMap : IDisposable int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius); // 'ratio' is a rational number. - // Multiplying it by destSize/GCD(sourceSize, destSize) will result in a whole number "again". - // This value is determining the length of the periods in repeating kernel map rows. - int period = destinationSize / Numerics.GreatestCommonDivisor(sourceSize, destinationSize); + // Multiplying it by destSize/GCD(sourceSize, destinationSize) yields an integer, so every `period` rows + // the destination-space sampling centers repeat their fractional alignment. `period` is the repeat length + // in destination rows, while `sourcePeriod` is the corresponding integer offset in source pixels that + // must be added to the kernel's left index when we reuse the same weights from a previous period. + int gcd = Numerics.GreatestCommonDivisor(sourceSize, destinationSize); + int period = destinationSize / gcd; + int sourcePeriod = sourceSize / gcd; // the center position at i == 0: double center0 = (ratio - 1) * 0.5; @@ -161,23 +167,24 @@ internal partial class ResizeKernelMap : IDisposable bool hasAtLeast2Periods = 2 * (cornerInterval + period) < destinationSize; ResizeKernelMap result = hasAtLeast2Periods - ? new PeriodicKernelMap( - memoryAllocator, - sourceSize, - destinationSize, - ratio, - scale, - radius, - period, - cornerInterval) - : new ResizeKernelMap( - memoryAllocator, - sourceSize, - destinationSize, - destinationSize, - ratio, - scale, - radius); + ? new PeriodicKernelMap( + memoryAllocator, + sourceSize, + destinationSize, + ratio, + scale, + radius, + period, + cornerInterval, + sourcePeriod) + : new ResizeKernelMap( + memoryAllocator, + sourceSize, + destinationSize, + destinationSize, + ratio, + scale, + radius); result.Initialize(in sampler); @@ -205,6 +212,7 @@ internal partial class ResizeKernelMap : IDisposable where TResampler : struct, IResampler { double center = ((destRowIndex + .5) * this.ratio) - .5; + double scale = this.scale; // Keep inside bounds. int left = (int)TolerantMath.Ceiling(center - this.radius); @@ -220,30 +228,25 @@ internal partial class ResizeKernelMap : IDisposable } ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right); - - Span kernelValues = this.tempValues.AsSpan(0, kernel.Length); - double sum = 0; + Span kernelValues = this.tempValues.AsSpan(0, kernel.Length); + ref float kernelStart = ref MemoryMarshal.GetReference(kernelValues); + float sum = 0; for (int j = left; j <= right; j++) { - double value = sampler.GetValue((float)((j - center) / this.scale)); + float value = sampler.GetValue((float)((j - center) / scale)); sum += value; - - kernelValues[j - left] = value; + kernelStart = value; + kernelStart = ref Unsafe.Add(ref kernelStart, 1); } // Normalize, best to do it here rather than in the pixel loop later on. if (sum > 0) { - for (int j = 0; j < kernel.Length; j++) - { - // weights[w] = weights[w] / sum: - ref double kRef = ref kernelValues[j]; - kRef /= sum; - } + Numerics.Normalize(kernelValues, sum); } - kernel.Fill(kernelValues); + kernel.FillOrCopyAndExpand(kernelValues); return kernel; } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs index 00ad71b4ca..e38bf64c1b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs @@ -16,9 +16,7 @@ public partial class ResizeKernelMapTests private readonly ReferenceKernel[] kernels; public ReferenceKernelMap(ReferenceKernel[] kernels) - { - this.kernels = kernels; - } + => this.kernels = kernels; public int DestinationSize => this.kernels.Length; @@ -28,16 +26,16 @@ public partial class ResizeKernelMapTests where TResampler : struct, IResampler { double ratio = (double)sourceSize / destinationSize; - double scale = ratio; + double scaleD = ratio; - if (scale < 1F) + if (scaleD < 1) { - scale = 1F; + scaleD = 1; } TolerantMath tolerantMath = TolerantMath.Default; - double radius = tolerantMath.Ceiling(scale * sampler.Radius); + double radius = tolerantMath.Ceiling(scaleD * sampler.Radius); List result = []; @@ -58,15 +56,14 @@ public partial class ResizeKernelMapTests right = sourceSize - 1; } - double sum = 0; + float sum = 0; - double[] values = new double[right - left + 1]; + float[] values = new float[right - left + 1]; for (int j = left; j <= right; j++) { - double weight = sampler.GetValue((float)((j - center) / scale)); + float weight = sampler.GetValue((float)((j - center) / scaleD)); sum += weight; - values[j - left] = weight; } @@ -78,16 +75,14 @@ public partial class ResizeKernelMapTests } } - float[] floatVals = values.Select(v => (float)v).ToArray(); - - result.Add(new ReferenceKernel(left, floatVals)); + result.Add(new ReferenceKernel(left, values)); } - return new ReferenceKernelMap(result.ToArray()); + return new ReferenceKernelMap([.. result]); } } - internal struct ReferenceKernel + internal readonly struct ReferenceKernel { public ReferenceKernel(int left, float[] values) { @@ -102,8 +97,6 @@ public partial class ResizeKernelMapTests public int Length => this.Values.Length; public static implicit operator ReferenceKernel(ResizeKernel orig) - { - return new ReferenceKernel(orig.StartIndex, orig.Values.ToArray()); - } + => new(orig.StartIndex, orig.Values.ToArray()); } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs index 0357dda046..5f9a8055ff 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs @@ -13,10 +13,7 @@ public partial class ResizeKernelMapTests { private ITestOutputHelper Output { get; } - public ResizeKernelMapTests(ITestOutputHelper output) - { - this.Output = output; - } + public ResizeKernelMapTests(ITestOutputHelper output) => this.Output = output; /// /// resamplerName, srcSize, destSize @@ -24,71 +21,70 @@ public partial class ResizeKernelMapTests public static readonly TheoryData KernelMapData = new() { - { KnownResamplers.Bicubic, 15, 10 }, - { KnownResamplers.Bicubic, 10, 15 }, - { KnownResamplers.Bicubic, 20, 20 }, - { KnownResamplers.Bicubic, 50, 40 }, - { KnownResamplers.Bicubic, 40, 50 }, - { KnownResamplers.Bicubic, 500, 200 }, - { KnownResamplers.Bicubic, 200, 500 }, - { KnownResamplers.Bicubic, 3032, 400 }, - { KnownResamplers.Bicubic, 10, 25 }, - { KnownResamplers.Lanczos3, 16, 12 }, - { KnownResamplers.Lanczos3, 12, 16 }, - { KnownResamplers.Lanczos3, 12, 9 }, - { KnownResamplers.Lanczos3, 9, 12 }, - { KnownResamplers.Lanczos3, 6, 8 }, - { KnownResamplers.Lanczos3, 8, 6 }, - { KnownResamplers.Lanczos3, 20, 12 }, - { KnownResamplers.Lanczos3, 5, 25 }, - { KnownResamplers.Lanczos3, 5, 50 }, - { KnownResamplers.Lanczos3, 25, 5 }, - { KnownResamplers.Lanczos3, 50, 5 }, - { KnownResamplers.Lanczos3, 49, 5 }, - { KnownResamplers.Lanczos3, 31, 5 }, - { KnownResamplers.Lanczos8, 500, 200 }, - { KnownResamplers.Lanczos8, 100, 10 }, - { KnownResamplers.Lanczos8, 100, 80 }, - { KnownResamplers.Lanczos8, 10, 100 }, - - // Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5: - { KnownResamplers.Box, 378, 149 }, - { KnownResamplers.Box, 349, 174 }, - - // Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData - { KnownResamplers.Box, 201, 100 }, - { KnownResamplers.Box, 199, 99 }, - { KnownResamplers.Box, 10, 299 }, - { KnownResamplers.Box, 299, 10 }, - { KnownResamplers.Box, 301, 300 }, - { KnownResamplers.Box, 1180, 480 }, - { KnownResamplers.Lanczos2, 3264, 3032 }, - { KnownResamplers.Bicubic, 1280, 2240 }, - { KnownResamplers.Bicubic, 1920, 1680 }, - { KnownResamplers.Bicubic, 3072, 2240 }, - { KnownResamplers.Welch, 300, 2008 }, - - // ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData - { KnownResamplers.Bicubic, 10, 50 }, - { KnownResamplers.Bicubic, 49, 301 }, - { KnownResamplers.Bicubic, 301, 49 }, - { KnownResamplers.Bicubic, 1680, 1200 }, - { KnownResamplers.Box, 13, 299 }, - { KnownResamplers.Lanczos5, 3032, 600 }, - - // Large number. https://github.com/SixLabors/ImageSharp/issues/1616 - { KnownResamplers.Bicubic, 207773, 51943 } - }; - - public static TheoryData GeneratedImageResizeData = - GenerateImageResizeData(); + { KnownResamplers.Bicubic, 15, 10 }, + { KnownResamplers.Bicubic, 10, 15 }, + { KnownResamplers.Bicubic, 20, 20 }, + { KnownResamplers.Bicubic, 50, 40 }, + { KnownResamplers.Bicubic, 40, 50 }, + { KnownResamplers.Bicubic, 500, 200 }, + { KnownResamplers.Bicubic, 200, 500 }, + { KnownResamplers.Bicubic, 3032, 400 }, + { KnownResamplers.Bicubic, 10, 25 }, + { KnownResamplers.Lanczos3, 16, 12 }, + { KnownResamplers.Lanczos3, 12, 16 }, + { KnownResamplers.Lanczos3, 12, 9 }, + { KnownResamplers.Lanczos3, 9, 12 }, + { KnownResamplers.Lanczos3, 6, 8 }, + { KnownResamplers.Lanczos3, 8, 6 }, + { KnownResamplers.Lanczos3, 20, 12 }, + { KnownResamplers.Lanczos3, 5, 25 }, + { KnownResamplers.Lanczos3, 5, 50 }, + { KnownResamplers.Lanczos3, 25, 5 }, + { KnownResamplers.Lanczos3, 50, 5 }, + { KnownResamplers.Lanczos3, 49, 5 }, + { KnownResamplers.Lanczos3, 31, 5 }, + { KnownResamplers.Lanczos8, 500, 200 }, + { KnownResamplers.Lanczos8, 100, 10 }, + { KnownResamplers.Lanczos8, 100, 80 }, + { KnownResamplers.Lanczos8, 10, 100 }, + + // Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5: + { KnownResamplers.Box, 378, 149 }, + { KnownResamplers.Box, 349, 174 }, + + // Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData + { KnownResamplers.Box, 201, 100 }, + { KnownResamplers.Box, 199, 99 }, + { KnownResamplers.Box, 10, 299 }, + { KnownResamplers.Box, 299, 10 }, + { KnownResamplers.Box, 301, 300 }, + { KnownResamplers.Box, 1180, 480 }, + { KnownResamplers.Lanczos2, 3264, 3032 }, + { KnownResamplers.Bicubic, 1280, 2240 }, + { KnownResamplers.Bicubic, 1920, 1680 }, + { KnownResamplers.Bicubic, 3072, 2240 }, + { KnownResamplers.Welch, 300, 2008 }, + + // ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData + { KnownResamplers.Bicubic, 10, 50 }, + { KnownResamplers.Bicubic, 49, 301 }, + { KnownResamplers.Bicubic, 301, 49 }, + { KnownResamplers.Bicubic, 1680, 1200 }, + { KnownResamplers.Box, 13, 299 }, + { KnownResamplers.Lanczos5, 3032, 600 }, + + // Large number. https://github.com/SixLabors/ImageSharp/issues/1616 + { KnownResamplers.Bicubic, 207773, 51943 } + }; + + public static TheoryData GeneratedImageResizeData = GenerateImageResizeData(); [Theory(Skip = "Only for debugging and development")] [MemberData(nameof(KernelMapData))] public void PrintNonNormalizedKernelMap(TResampler resampler, int srcSize, int destSize) where TResampler : struct, IResampler { - ReferenceKernelMap kernelMap = ReferenceKernelMap.Calculate(in resampler, destSize, srcSize, false); + ReferenceKernelMap kernelMap = ReferenceKernelMap.Calculate(in resampler, destSize, srcSize, false); this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); } @@ -97,9 +93,7 @@ public partial class ResizeKernelMapTests [MemberData(nameof(KernelMapData))] public void KernelMapContentIsCorrect(TResampler resampler, int srcSize, int destSize) where TResampler : struct, IResampler - { - this.VerifyKernelMapContentIsCorrect(resampler, srcSize, destSize); - } + => this.VerifyKernelMapContentIsCorrect(resampler, srcSize, destSize); // Comprehensive but expensive tests, for ResizeKernelMap. // Enabling them can kill you, but sometimes you have to wear the burden! @@ -124,8 +118,8 @@ public partial class ResizeKernelMapTests this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); #endif - ApproximateFloatComparer comparer = new(1e-6f); + ApproximateFloatComparer comparer = new ApproximateFloatComparer(1e-6f); for (int i = 0; i < kernelMap.DestinationLength; i++) { ResizeKernel kernel = kernelMap.GetKernel((uint)i); @@ -139,7 +133,23 @@ public partial class ResizeKernelMapTests referenceKernel.Left == kernel.StartIndex, $"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.StartIndex}"); float[] expectedValues = referenceKernel.Values; - Span actualValues = kernel.Values; + Span actualValues; + + if (ResizeKernel.IsHardwareAccelerated) + { + Assert.Equal(expectedValues.Length, kernel.Values.Length / 4); + + actualValues = new float[expectedValues.Length]; + + for (int j = 0; j < expectedValues.Length; j++) + { + actualValues[j] = kernel.Values[j * 4]; + } + } + else + { + actualValues = kernel.Values; + } Assert.Equal(expectedValues.Length, actualValues.Length); @@ -199,12 +209,13 @@ public partial class ResizeKernelMapTests int[] dimensionVals = [ + // Arbitrary, small dimensions: - 9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301, + 9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301, - // Typical image sizes: - 640, 480, 800, 600, 1024, 768, 1280, 960, 1536, 1180, 1600, 1200, 2048, 1536, 2240, 1680, 2560, - 1920, 3032, 2008, 3072, 2304, 3264, 2448 + // Typical image sizes: + 640, 480, 800, 600, 1024, 768, 1280, 960, 1536, 1180, 1600, 1200, 2048, 1536, 2240, 1680, 2560, + 1920, 3032, 2008, 3072, 2304, 3264, 2448 ]; IOrderedEnumerable<(int S, int D)> source2Dest = dimensionVals