Browse Source

Fix bulk RGBA-compatible conversion bounds

Slice the destination span to source length in `Vector4Converters.RgbaCompatible.FromVector4` so SIMD/scalar tails never process spare destination capacity. Expanded pixel-format tests to run across hardware intrinsic configurations, added dedicated RGB24/BGR24 bulk conversion coverage with sentinel checks for out-of-range writes, and relaxed associated-alpha half packing assertions to treat NaN payload differences as valid IEEE behavior.
pull/3154/head
James Jackson-South 4 weeks ago
parent
commit
e1c3013f96
  1. 4
      src/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs
  2. 20
      tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs
  3. 8
      tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs
  4. 8
      tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs
  5. 8
      tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs
  6. 8
      tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs
  7. 8
      tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs
  8. 8
      tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs
  9. 8
      tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs
  10. 53
      tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs
  11. 8
      tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs
  12. 8
      tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs

4
src/ImageSharp/PixelFormats/Utils/Vector4Converters.RgbaCompatible.cs

@ -100,6 +100,10 @@ internal static partial class Vector4Converters
int count = source.Length;
// Source defines the conversion length. Do not expose spare destination capacity
// to downstream shuffles whose scalar tail is destination-length driven.
destination = destination[..count];
// Not worth for small buffers:
if (count < Vector4ConversionThreshold)
{

20
tests/ImageSharp.Tests/PixelFormats/AssociatedAlphaPixelTests.cs

@ -395,7 +395,25 @@ public class HalfVector4PTests : AssociatedAlphaPixelTests<HalfVector4P>
PixelOperations<HalfVector4P>.Instance.FromVector4Destructive(Configuration.Default, destructiveSource, actual, PixelConversionModifiers.None);
Assert.Equal(expected, actual);
for (int i = 0; i < expected.Length; i++)
{
for (int component = 0; component < 4; component++)
{
int shift = component * 16;
ushort expectedBits = (ushort)(expected[i].PackedValue >> shift);
ushort actualBits = (ushort)(actual[i].PackedValue >> shift);
// Association performs floating-point arithmetic before packing. IEEE 754 requires a NaN result but does not specify which operand's payload survives that arithmetic.
if (Half.IsNaN(BitConverter.UInt16BitsToHalf(expectedBits)))
{
Assert.True(Half.IsNaN(BitConverter.UInt16BitsToHalf(actualBits)), $"Pixel {i}, component {component} should be NaN.");
}
else
{
Assert.Equal(expectedBits, actualBits);
}
}
}
}
private static void AssertAlphaConversionsMatchScalar()

8
tests/ImageSharp.Tests/PixelFormats/HalfSingleTests.cs

@ -4,6 +4,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
@ -55,7 +56,12 @@ public class HalfSingleTests
}
[Fact]
public void HalfSingle_BulkScaledConversionsCoverFiniteRange()
public void HalfSingle_BulkScaledConversionsCoverFiniteRange() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertHalfSingleBulkScaledConversionsCoverFiniteRange,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertHalfSingleBulkScaledConversionsCoverFiniteRange()
{
ushort[] packedValues = [0xFBFF, 0, 0x7BFF];
Vector4[] scaledValues = [new(0F, 0F, 0F, 1F), new(.5F, 0F, 0F, 1F), new(1F, 0F, 0F, 1F)];

8
tests/ImageSharp.Tests/PixelFormats/HalfVector2Tests.cs

@ -4,6 +4,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
@ -61,7 +62,12 @@ public class HalfVector2Tests
}
[Fact]
public void HalfVector2_BulkScaledConversionsCoverFiniteRange()
public void HalfVector2_BulkScaledConversionsCoverFiniteRange() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertHalfVector2BulkScaledConversionsCoverFiniteRange,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertHalfVector2BulkScaledConversionsCoverFiniteRange()
{
HalfVector2 pixel = new() { PackedValue = 0x7BFF_FBFF };
HalfVector2[] source = new HalfVector2[17];

8
tests/ImageSharp.Tests/PixelFormats/HalfVector4Tests.cs

@ -4,6 +4,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
@ -69,7 +70,12 @@ public class HalfVector4Tests
}
[Fact]
public void HalfVector4_BulkScaledConversionsCoverFiniteRange()
public void HalfVector4_BulkScaledConversionsCoverFiniteRange() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertHalfVector4BulkScaledConversionsCoverFiniteRange,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertHalfVector4BulkScaledConversionsCoverFiniteRange()
{
const ulong packedValue = 0xFBFF_7BFF_0000_FBFF;
HalfVector4 pixel = new() { PackedValue = packedValue };

8
tests/ImageSharp.Tests/PixelFormats/NormalizedByte2Tests.cs

@ -4,6 +4,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
@ -30,7 +31,12 @@ public class NormalizedByte2Tests
}
[Fact]
public void NormalizedByte2_MinimumStorageCodeDecodesAsNegativeOne()
public void NormalizedByte2_MinimumStorageCodeDecodesAsNegativeOne() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertNormalizedByte2MinimumStorageCodeDecodesAsNegativeOne,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertNormalizedByte2MinimumStorageCodeDecodesAsNegativeOne()
{
NormalizedByte2 pixel = new() { PackedValue = 0x8080 };
Vector4 expectedNative = new(-1F, -1F, 0F, 1F);

8
tests/ImageSharp.Tests/PixelFormats/NormalizedByte4Tests.cs

@ -4,6 +4,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
@ -61,7 +62,12 @@ public class NormalizedByte4Tests
}
[Fact]
public void NormalizedByte4_MinimumStorageCodeDecodesAsNegativeOne()
public void NormalizedByte4_MinimumStorageCodeDecodesAsNegativeOne() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertNormalizedByte4MinimumStorageCodeDecodesAsNegativeOne,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertNormalizedByte4MinimumStorageCodeDecodesAsNegativeOne()
{
NormalizedByte4 pixel = new() { PackedValue = 0x80808080 };

8
tests/ImageSharp.Tests/PixelFormats/NormalizedShort2Tests.cs

@ -4,6 +4,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
@ -34,7 +35,12 @@ public class NormalizedShort2Tests
}
[Fact]
public void NormalizedShort2_MinimumStorageCodeDecodesAsNegativeOne()
public void NormalizedShort2_MinimumStorageCodeDecodesAsNegativeOne() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertNormalizedShort2MinimumStorageCodeDecodesAsNegativeOne,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertNormalizedShort2MinimumStorageCodeDecodesAsNegativeOne()
{
NormalizedShort2 pixel = new() { PackedValue = 0x80008000 };
Vector4 expectedNative = new(-1F, -1F, 0F, 1F);

8
tests/ImageSharp.Tests/PixelFormats/NormalizedShort4Tests.cs

@ -4,6 +4,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
@ -62,7 +63,12 @@ public class NormalizedShort4Tests
}
[Fact]
public void NormalizedShort4_MinimumStorageCodeDecodesAsNegativeOne()
public void NormalizedShort4_MinimumStorageCodeDecodesAsNegativeOne() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertNormalizedShort4MinimumStorageCodeDecodesAsNegativeOne,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertNormalizedShort4MinimumStorageCodeDecodesAsNegativeOne()
{
NormalizedShort4 pixel = new() { PackedValue = 0x8000800080008000 };

53
tests/ImageSharp.Tests/PixelFormats/PixelAlphaRepresentationTests.cs

@ -5,6 +5,7 @@ using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles.Companding;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
@ -307,6 +308,58 @@ public abstract class PixelAlphaRepresentationTests<TPixel>
private delegate void BulkFromVector4(PixelOperations<TPixel> operations, Span<Vector4> source, Span<TPixel> destination);
}
/// <summary>
/// Verifies the shared bulk vector conversion used by RGB byte formats.
/// </summary>
[Trait("Category", "PixelFormats")]
public class RgbaCompatiblePixelOperationsTests
{
[Fact]
public void BulkConversionsMatchScalarAcrossHardwareWidths() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertBulkConversionsMatchScalar,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertBulkConversionsMatchScalar()
{
AssertBulkConversionsMatchScalarForPixel<Bgr24>();
AssertBulkConversionsMatchScalarForPixel<Rgb24>();
}
private static void AssertBulkConversionsMatchScalarForPixel<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
const int length = 259;
Vector4 sourceVector = new(.5F, .25F, .125F, 1F);
TPixel expectedPixel = TPixel.FromUnassociatedScaledVector4(sourceVector);
TPixel[] pixels = new TPixel[length];
Array.Fill(pixels, expectedPixel);
Vector4 expectedVector = expectedPixel.ToUnassociatedScaledVector4();
Vector4 vectorSentinel = new(.125F, .5F, .75F, 1F);
Vector4[] vectors = new Vector4[length + 3];
vectors.AsSpan(length).Fill(vectorSentinel);
PixelOperations<TPixel>.Instance.ToVector4(Configuration.Default, pixels, vectors, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
Assert.All(vectors[..length], vector => Assert.Equal(expectedVector, vector));
Assert.All(vectors[length..], vector => Assert.Equal(vectorSentinel, vector));
Vector4[] source = new Vector4[length];
source.AsSpan().Fill(sourceVector);
TPixel pixelSentinel = TPixel.FromUnassociatedScaledVector4(vectorSentinel);
TPixel[] destination = new TPixel[length + 3];
destination.AsSpan(length).Fill(pixelSentinel);
PixelOperations<TPixel>.Instance.FromVector4Destructive(Configuration.Default, source, destination, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
// A 259-pixel source crosses the 128- and 256-bit optimized thresholds while leaving spare destination capacity to detect writes beyond the source length.
Assert.All(destination[..length], pixel => Assert.Equal(expectedPixel, pixel));
Assert.All(destination[length..], pixel => Assert.Equal(pixelSentinel, pixel));
}
}
/// <summary>
/// Verifies alpha conversion for formats with distinct native vector contracts.
/// </summary>

8
tests/ImageSharp.Tests/PixelFormats/Short2Tests.cs

@ -4,6 +4,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
@ -63,7 +64,12 @@ public class Short2Tests
}
[Fact]
public void Short2_BulkScaledConversionsCoverFullSignedRange()
public void Short2_BulkScaledConversionsCoverFullSignedRange() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertShort2BulkScaledConversionsCoverFullSignedRange,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertShort2BulkScaledConversionsCoverFullSignedRange()
{
const int length = 17;
Short2[] source = new Short2[length];

8
tests/ImageSharp.Tests/PixelFormats/Short4Tests.cs

@ -4,6 +4,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.PixelFormats;
@ -50,7 +51,12 @@ public class Short4Tests
}
[Fact]
public void Short4_BulkScaledConversionsCoverFullSignedRange()
public void Short4_BulkScaledConversionsCoverFullSignedRange() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertShort4BulkScaledConversionsCoverFullSignedRange,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertShort4BulkScaledConversionsCoverFullSignedRange()
{
const int length = 17;
Short4[] source = new Short4[length];

Loading…
Cancel
Save