From 501afd48c1fb8f10bac707588c82f35861991580 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 25 Jul 2026 08:16:07 +1000 Subject: [PATCH] Normalize stateful Vector4 transforms --- .../Utils/Vector4Converters.Affine.cs | 113 ++++------ .../Vector4Converters.AffineOperators.cs | 151 +++++++++++++ .../PixelConversion/Vector4AffineTransform.cs | 207 ++++++++++++++++++ .../Vector4AffineTransformAssembly.cs | 73 ++++++ .../PixelFormats/Vector4ConvertersTests.cs | 122 +++++++++++ 5 files changed, 592 insertions(+), 74 deletions(-) create mode 100644 src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs create mode 100644 tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransform.cs create mode 100644 tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransformAssembly.cs create mode 100644 tests/ImageSharp.Tests/PixelFormats/Vector4ConvertersTests.cs diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Affine.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Affine.cs index 0096085e9..fba3b8fcd 100644 --- a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Affine.cs +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.Affine.cs @@ -17,59 +17,7 @@ internal static partial class Vector4Converters /// The component-wise multiplier. /// The component-wise offset applied after multiplication. internal static void MultiplyThenAdd(Span vectors, Vector4 multiplier, Vector4 offset) - { - ref Vector4 vectorBase = ref MemoryMarshal.GetReference(vectors); - int index = 0; - - if (Vector512.IsHardwareAccelerated) - { - int vectorsPerVector = Vector512.Count / Vector128.Count; - Vector256 multiplier256 = Vector256.Create(multiplier.AsVector128(), multiplier.AsVector128()); - Vector256 offset256 = Vector256.Create(offset.AsVector128(), offset.AsVector128()); - Vector512 multiplier512 = Vector512.Create(multiplier256, multiplier256); - Vector512 offset512 = Vector512.Create(offset256, offset256); - - for (; index <= vectors.Length - vectorsPerVector; index += vectorsPerVector) - { - ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref vectorBase, (uint)index)); - vector = (vector * multiplier512) + offset512; - } - } - - if (Vector256.IsHardwareAccelerated) - { - int vectorsPerVector = Vector256.Count / Vector128.Count; - Vector256 multiplier256 = Vector256.Create(multiplier.AsVector128(), multiplier.AsVector128()); - Vector256 offset256 = Vector256.Create(offset.AsVector128(), offset.AsVector128()); - - for (; index <= vectors.Length - vectorsPerVector; index += vectorsPerVector) - { - ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref vectorBase, (uint)index)); - vector = (vector * multiplier256) + offset256; - } - } - - if (Vector128.IsHardwareAccelerated) - { - Vector128 multiplier128 = multiplier.AsVector128(); - Vector128 offset128 = offset.AsVector128(); - - for (; index < vectors.Length; index++) - { - ref Vector128 vector = ref Unsafe.As>(ref Unsafe.Add(ref vectorBase, (uint)index)); - vector = (vector * multiplier128) + offset128; - } - - return; - } - - // The scalar fallback retains the same multiply-then-add order as the SIMD paths and the per-pixel contracts. - for (; index < vectors.Length; index++) - { - ref Vector4 vector = ref Unsafe.Add(ref vectorBase, (uint)index); - vector = (vector * multiplier) + offset; - } - } + => Apply(vectors, new MultiplyThenAddOperator(multiplier, offset)); /// /// Adds the corresponding offset component and then divides each vector component by its divisor. @@ -78,57 +26,74 @@ internal static partial class Vector4Converters /// The component-wise offset applied before division. /// The component-wise divisor. internal static void AddThenDivide(Span vectors, Vector4 offset, Vector4 divisor) + => Apply(vectors, new AddThenDivideOperator(offset, divisor)); + + /// + /// Applies a stateful component transform to a vector buffer in place. + /// + /// The transform selected for this closed traversal. + /// The vectors to transform. + /// The transform and its component-wise state. + // Closing and inlining the traversal lets the JIT devirtualize every Invoke call, + // specialize the active hardware-width branches, and discard unused operator state. + [MethodImpl(InliningOptions.AlwaysInline)] + private static void Apply(Span vectors, TOperator transform) + where TOperator : struct, IStatefulVector4Operator { ref Vector4 vectorBase = ref MemoryMarshal.GetReference(vectors); int index = 0; + // A Vector4 is one complete pixel. Descending register widths therefore consume groups + // of four, two, and one pixels without splitting a pixel across traversal boundaries. if (Vector512.IsHardwareAccelerated) { - int vectorsPerVector = Vector512.Count / Vector128.Count; - Vector256 offset256 = Vector256.Create(offset.AsVector128(), offset.AsVector128()); - Vector256 divisor256 = Vector256.Create(divisor.AsVector128(), divisor.AsVector128()); - Vector512 offset512 = Vector512.Create(offset256, offset256); - Vector512 divisor512 = Vector512.Create(divisor256, divisor256); + int vectorsPerRegister = Vector512.Count / Vector128.Count; + int oneRegisterFromEnd = vectors.Length - vectorsPerRegister; - for (; index <= vectors.Length - vectorsPerVector; index += vectorsPerVector) + for (; index <= oneRegisterFromEnd; index += vectorsPerRegister) { - ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref vectorBase, (uint)index)); - vector = (vector + offset512) / divisor512; + ref Vector512 vector = ref Unsafe.As>( + ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = transform.Invoke(vector); } } if (Vector256.IsHardwareAccelerated) { - int vectorsPerVector = Vector256.Count / Vector128.Count; - Vector256 offset256 = Vector256.Create(offset.AsVector128(), offset.AsVector128()); - Vector256 divisor256 = Vector256.Create(divisor.AsVector128(), divisor.AsVector128()); + int vectorsPerRegister = Vector256.Count / Vector128.Count; + int oneRegisterFromEnd = vectors.Length - vectorsPerRegister; - for (; index <= vectors.Length - vectorsPerVector; index += vectorsPerVector) + for (; index <= oneRegisterFromEnd; index += vectorsPerRegister) { - ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref vectorBase, (uint)index)); - vector = (vector + offset256) / divisor256; + ref Vector256 vector = ref Unsafe.As>( + ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = transform.Invoke(vector); } } if (Vector128.IsHardwareAccelerated) { - Vector128 offset128 = offset.AsVector128(); - Vector128 divisor128 = divisor.AsVector128(); - + // Vector128 and Vector4 have the same four-lane layout, so this stage + // consumes every remaining complete pixel and leaves no scalar remainder. for (; index < vectors.Length; index++) { - ref Vector128 vector = ref Unsafe.As>(ref Unsafe.Add(ref vectorBase, (uint)index)); - vector = (vector + offset128) / divisor128; + ref Vector128 vector = ref Unsafe.As>( + ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = transform.Invoke(vector); } return; } - // Native-to-scaled conversion deliberately adds before dividing to match each format's scalar conversion order. + // Vector4 retains the same component order and expression ordering when hardware + // intrinsics are unavailable, preserving the format-specific conversion contract. for (; index < vectors.Length; index++) { ref Vector4 vector = ref Unsafe.Add(ref vectorBase, (uint)index); - vector = (vector + offset) / divisor; + vector = transform.Invoke(vector); } } } diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs new file mode 100644 index 000000000..f5e57f765 --- /dev/null +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.PixelFormats.Utils; + +internal static partial class Vector4Converters +{ + /// + /// Defines a stateful component transform for each register width used by the shared traversal. + /// + private interface IStatefulVector4Operator + { + /// + /// Transforms one pixel represented by four components. + /// + /// The source components. + /// The transformed components. + Vector4 Invoke(Vector4 source); + + /// + /// Transforms one pixel represented by the four single-precision lanes in a 128-bit register. + /// + /// The source components. + /// The transformed components. + Vector128 Invoke(Vector128 source); + + /// + /// Transforms two pixels represented by the eight single-precision lanes in a 256-bit register. + /// + /// The source components. + /// The transformed components. + Vector256 Invoke(Vector256 source); + + /// + /// Transforms four pixels represented by the sixteen single-precision lanes in a 512-bit register. + /// + /// The source components. + /// The transformed components. + Vector512 Invoke(Vector512 source); + } + + /// + /// Carries the component state for a multiply-then-add transform. + /// + private readonly struct MultiplyThenAddOperator : IStatefulVector4Operator + { + private readonly Vector512 multiplier; + private readonly Vector512 offset; + + /// + /// Initializes a new instance of the struct. + /// + /// The component-wise multiplier. + /// The component-wise offset applied after multiplication. + public MultiplyThenAddOperator(Vector4 multiplier, Vector4 offset) + { + Vector128 multiplier128 = multiplier.AsVector128(); + Vector128 offset128 = offset.AsVector128(); + Vector256 multiplier256 = Vector256.Create(multiplier128, multiplier128); + Vector256 offset256 = Vector256.Create(offset128, offset128); + + // Expanding the invariant state once prevents the width-specific Invoke methods + // from rebuilding identical lane groups for every vector processed by the loop. + this.multiplier = Vector512.Create(multiplier256, multiplier256); + this.offset = Vector512.Create(offset256, offset256); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 Invoke(Vector4 source) + { + Vector128 result = + (source.AsVector128() * this.multiplier.GetLower().GetLower()) + + this.offset.GetLower().GetLower(); + + return result.AsVector4(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 source) + => (source * this.multiplier.GetLower().GetLower()) + this.offset.GetLower().GetLower(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 source) + => (source * this.multiplier.GetLower()) + this.offset.GetLower(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 source) + => (source * this.multiplier) + this.offset; + } + + /// + /// Carries the component state for an add-then-divide transform. + /// + private readonly struct AddThenDivideOperator : IStatefulVector4Operator + { + private readonly Vector512 offset; + private readonly Vector512 divisor; + + /// + /// Initializes a new instance of the struct. + /// + /// The component-wise offset applied before division. + /// The component-wise divisor. + public AddThenDivideOperator(Vector4 offset, Vector4 divisor) + { + Vector128 offset128 = offset.AsVector128(); + Vector128 divisor128 = divisor.AsVector128(); + Vector256 offset256 = Vector256.Create(offset128, offset128); + Vector256 divisor256 = Vector256.Create(divisor128, divisor128); + + // All register widths consume prefixes of this repeated four-pixel state, + // so one construction serves the wide loop and every narrower remainder. + this.offset = Vector512.Create(offset256, offset256); + this.divisor = Vector512.Create(divisor256, divisor256); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 Invoke(Vector4 source) + { + Vector128 result = + (source.AsVector128() + this.offset.GetLower().GetLower()) + / this.divisor.GetLower().GetLower(); + + return result.AsVector4(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector128 Invoke(Vector128 source) + => (source + this.offset.GetLower().GetLower()) / this.divisor.GetLower().GetLower(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector256 Invoke(Vector256 source) + => (source + this.offset.GetLower()) / this.divisor.GetLower(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector512 Invoke(Vector512 source) + => (source + this.offset) / this.divisor; + } +} diff --git a/tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransform.cs b/tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransform.cs new file mode 100644 index 000000000..0eb7b9ccd --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransform.cs @@ -0,0 +1,207 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.Benchmarks.General.PixelConversion; + +/// +/// Compares operator-driven affine vector transforms with the duplicated traversals they replace. +/// +[Config(typeof(Config.Short))] +public class Vector4AffineTransform +{ + private static readonly Vector4 Multiplier = new(255F, 2F, 65535F, .5F); + private static readonly Vector4 Offset = new(17F, -1F, 32768F, 3F); + private static readonly Vector4 Divisor = new(255F, 2F, 65535F, .5F); + + private Vector4[] current; + private Vector4[] baseline; + + /// + /// Gets or sets the number of vectors transformed by each invocation. + /// + [Params(1, 3, 4, 17, 256, 4096)] + public int Count { get; set; } + + /// + /// Creates identical non-uniform buffers for the current and baseline traversals. + /// + [GlobalSetup] + public void Setup() + { + this.current = new Vector4[this.Count]; + + for (int i = 0; i < this.current.Length; i++) + { + this.current[i] = new Vector4(i + .25F, i + .5F, i + .75F, i + 1F); + } + + this.baseline = [.. this.current]; + } + + /// + /// Executes the operator-driven multiply-then-add traversal. + /// + [Benchmark] + public void CurrentMultiplyThenAdd() + => Vector4Converters.MultiplyThenAdd(this.current, Multiplier, Offset); + + /// + /// Executes the duplicated multiply-then-add traversal. + /// + [Benchmark(Baseline = true)] + public void BaselineMultiplyThenAdd() + => BaselineMultiplyThenAdd(this.baseline, Multiplier, Offset); + + /// + /// Executes the operator-driven add-then-divide traversal. + /// + [Benchmark] + public void CurrentAddThenDivide() + => Vector4Converters.AddThenDivide(this.current, Offset, Divisor); + + /// + /// Executes the duplicated add-then-divide traversal. + /// + [Benchmark] + public void BaselineAddThenDivide() + => BaselineAddThenDivide(this.baseline, Offset, Divisor); + + /// + /// Retains the multiply-then-add traversal being replaced for direct measurement. + /// + /// The vectors to transform. + /// The component-wise multiplier. + /// The component-wise offset. + internal static void BaselineMultiplyThenAdd(Span vectors, Vector4 multiplier, Vector4 offset) + { + ref Vector4 vectorBase = ref MemoryMarshal.GetReference(vectors); + int index = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorsPerVector = Vector512.Count / Vector128.Count; + Vector256 multiplier256 = Vector256.Create(multiplier.AsVector128(), multiplier.AsVector128()); + Vector256 offset256 = Vector256.Create(offset.AsVector128(), offset.AsVector128()); + Vector512 multiplier512 = Vector512.Create(multiplier256, multiplier256); + Vector512 offset512 = Vector512.Create(offset256, offset256); + + for (; index <= vectors.Length - vectorsPerVector; index += vectorsPerVector) + { + ref Vector512 vector = ref Unsafe.As>( + ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = (vector * multiplier512) + offset512; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorsPerVector = Vector256.Count / Vector128.Count; + Vector256 multiplier256 = Vector256.Create(multiplier.AsVector128(), multiplier.AsVector128()); + Vector256 offset256 = Vector256.Create(offset.AsVector128(), offset.AsVector128()); + + for (; index <= vectors.Length - vectorsPerVector; index += vectorsPerVector) + { + ref Vector256 vector = ref Unsafe.As>( + ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = (vector * multiplier256) + offset256; + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 multiplier128 = multiplier.AsVector128(); + Vector128 offset128 = offset.AsVector128(); + + for (; index < vectors.Length; index++) + { + ref Vector128 vector = ref Unsafe.As>( + ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = (vector * multiplier128) + offset128; + } + + return; + } + + for (; index < vectors.Length; index++) + { + ref Vector4 vector = ref Unsafe.Add(ref vectorBase, (uint)index); + vector = (vector * multiplier) + offset; + } + } + + /// + /// Retains the add-then-divide traversal being replaced for direct measurement. + /// + /// The vectors to transform. + /// The component-wise offset. + /// The component-wise divisor. + internal static void BaselineAddThenDivide(Span vectors, Vector4 offset, Vector4 divisor) + { + ref Vector4 vectorBase = ref MemoryMarshal.GetReference(vectors); + int index = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorsPerVector = Vector512.Count / Vector128.Count; + Vector256 offset256 = Vector256.Create(offset.AsVector128(), offset.AsVector128()); + Vector256 divisor256 = Vector256.Create(divisor.AsVector128(), divisor.AsVector128()); + Vector512 offset512 = Vector512.Create(offset256, offset256); + Vector512 divisor512 = Vector512.Create(divisor256, divisor256); + + for (; index <= vectors.Length - vectorsPerVector; index += vectorsPerVector) + { + ref Vector512 vector = ref Unsafe.As>( + ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = (vector + offset512) / divisor512; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorsPerVector = Vector256.Count / Vector128.Count; + Vector256 offset256 = Vector256.Create(offset.AsVector128(), offset.AsVector128()); + Vector256 divisor256 = Vector256.Create(divisor.AsVector128(), divisor.AsVector128()); + + for (; index <= vectors.Length - vectorsPerVector; index += vectorsPerVector) + { + ref Vector256 vector = ref Unsafe.As>( + ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = (vector + offset256) / divisor256; + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 offset128 = offset.AsVector128(); + Vector128 divisor128 = divisor.AsVector128(); + + for (; index < vectors.Length; index++) + { + ref Vector128 vector = ref Unsafe.As>( + ref Unsafe.Add(ref vectorBase, (uint)index)); + + vector = (vector + offset128) / divisor128; + } + + return; + } + + for (; index < vectors.Length; index++) + { + ref Vector4 vector = ref Unsafe.Add(ref vectorBase, (uint)index); + vector = (vector + offset) / divisor; + } + } +} diff --git a/tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransformAssembly.cs b/tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransformAssembly.cs new file mode 100644 index 000000000..fcecf1306 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransformAssembly.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.Benchmarks.General.PixelConversion; + +/// +/// Exposes every stateful affine operator and traversal remainder for assembly inspection. +/// +[Config(typeof(Config.Analysis))] +public class Vector4AffineTransformAssembly +{ + private static readonly Vector4 Multiplier = new(255F, 2F, 65535F, .5F); + private static readonly Vector4 Offset = new(17F, -1F, 32768F, 3F); + private static readonly Vector4 Divisor = new(255F, 2F, 65535F, .5F); + + private Vector4[] vectors; + + /// + /// Gets or sets the number of vectors transformed by each invocation. + /// + /// + /// Three vectors exercise the 256- and 128-bit stages. Seventeen vectors exercise + /// the 512-bit loop and leave one vector for the 128-bit remainder. + /// + [Params(3, 17)] + public int Count { get; set; } + + /// + /// Creates a non-uniform input buffer. + /// + [GlobalSetup] + public void Setup() + { + this.vectors = new Vector4[this.Count]; + + for (int i = 0; i < this.vectors.Length; i++) + { + this.vectors[i] = new Vector4(i + .25F, i + .5F, i + .75F, i + 1F); + } + } + + /// + /// Executes the multiply-then-add stateful operator. + /// + [Benchmark] + public void MultiplyThenAdd() + => Vector4Converters.MultiplyThenAdd(this.vectors, Multiplier, Offset); + + /// + /// Executes the add-then-divide stateful operator. + /// + [Benchmark] + public void AddThenDivide() + => Vector4Converters.AddThenDivide(this.vectors, Offset, Divisor); + + /// + /// Executes the multiply-then-add traversal being replaced for assembly comparison. + /// + [Benchmark] + public void BaselineMultiplyThenAdd() + => Vector4AffineTransform.BaselineMultiplyThenAdd(this.vectors, Multiplier, Offset); + + /// + /// Executes the add-then-divide traversal being replaced for assembly comparison. + /// + [Benchmark] + public void BaselineAddThenDivide() + => Vector4AffineTransform.BaselineAddThenDivide(this.vectors, Offset, Divisor); +} diff --git a/tests/ImageSharp.Tests/PixelFormats/Vector4ConvertersTests.cs b/tests/ImageSharp.Tests/PixelFormats/Vector4ConvertersTests.cs new file mode 100644 index 000000000..52f45aad7 --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/Vector4ConvertersTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats.Utils; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +/// +/// Verifies the shared stateful traversal used by affine pixel-vector conversions. +/// +[Trait("Category", "PixelFormats")] +public class Vector4ConvertersTests +{ + private static readonly int[] Lengths = [0, 1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 257]; + + /// + /// Verifies multiply-then-add behavior for every SIMD boundary and the software fallback. + /// + [Fact] + public void MultiplyThenAddMatchesComponentArithmeticAcrossHardwareWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertMultiplyThenAddMatchesComponentArithmetic, + HwIntrinsics.AllowAll + | HwIntrinsics.DisableAVX512F + | HwIntrinsics.DisableAVX + | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Verifies add-then-divide behavior for every SIMD boundary and the software fallback. + /// + [Fact] + public void AddThenDivideMatchesComponentArithmeticAcrossHardwareWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + AssertAddThenDivideMatchesComponentArithmetic, + HwIntrinsics.AllowAll + | HwIntrinsics.DisableAVX512F + | HwIntrinsics.DisableAVX + | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Compares the multiply-then-add traversal with independently evaluated component expressions. + /// + private static void AssertMultiplyThenAddMatchesComponentArithmetic() + { + Vector4 multiplier = new(2F, -3F, .5F, 4F); + Vector4 offset = new(-7F, 11F, 13F, -17F); + + foreach (int length in Lengths) + { + Vector4[] actual = CreateSource(length); + Vector4[] expected = new Vector4[length]; + + for (int i = 0; i < expected.Length; i++) + { + Vector4 value = actual[i]; + + expected[i] = new Vector4( + (value.X * multiplier.X) + offset.X, + (value.Y * multiplier.Y) + offset.Y, + (value.Z * multiplier.Z) + offset.Z, + (value.W * multiplier.W) + offset.W); + } + + Vector4Converters.MultiplyThenAdd(actual, multiplier, offset); + + Assert.Equal(expected, actual); + } + } + + /// + /// Compares the add-then-divide traversal with independently evaluated component expressions. + /// + private static void AssertAddThenDivideMatchesComponentArithmetic() + { + Vector4 offset = new(-7F, 11F, 13F, -17F); + Vector4 divisor = new(2F, -3F, .5F, 4F); + + foreach (int length in Lengths) + { + Vector4[] actual = CreateSource(length); + Vector4[] expected = new Vector4[length]; + + for (int i = 0; i < expected.Length; i++) + { + Vector4 value = actual[i]; + + expected[i] = new Vector4( + (value.X + offset.X) / divisor.X, + (value.Y + offset.Y) / divisor.Y, + (value.Z + offset.Z) / divisor.Z, + (value.W + offset.W) / divisor.W); + } + + Vector4Converters.AddThenDivide(actual, offset, divisor); + + Assert.Equal(expected, actual); + } + } + + /// + /// Creates non-uniform values that expose component ordering and traversal overlap errors. + /// + /// The number of vectors to create. + /// The populated vector buffer. + private static Vector4[] CreateSource(int length) + { + Vector4[] result = new Vector4[length]; + + for (int i = 0; i < result.Length; i++) + { + result[i] = new Vector4( + (i * 17F) - 31F, + (i * -23F) + 37F, + (i * .25F) - 41F, + (i * 3F) + 43F); + } + + return result; + } +}