// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using Xunit; using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.PixelFormats { public class PixelOperationsTests { public class Argb32OperationsTests : PixelOperationsTests { public Argb32OperationsTests(ITestOutputHelper output) : base(output) { } // For 4.6 test runner MemberData does not work without redeclaring the public field in the // derived test class: // TODO: Can this not be delared in the parent class? public static new TheoryData ArraySizesData => new TheoryData { 7, 16, 1111 }; [Fact] public void IsSpecialImplementation() => Assert.IsType(PixelOperations.Instance); } public class Bgr24OperationsTests : PixelOperationsTests { public Bgr24OperationsTests(ITestOutputHelper output) : base(output) { } public static new TheoryData ArraySizesData => new TheoryData { 7, 16, 1111 }; [Fact] public void IsSpecialImplementation() => Assert.IsType(PixelOperations.Instance); } public class Bgra32OperationsTests : PixelOperationsTests { public Bgra32OperationsTests(ITestOutputHelper output) : base(output) { } public static new TheoryData ArraySizesData => new TheoryData { 7, 16, 1111 }; [Fact] public void IsSpecialImplementation() => Assert.IsType(PixelOperations.Instance); } public class Gray8OperationsTests : PixelOperationsTests { public Gray8OperationsTests(ITestOutputHelper output) : base(output) { } public static new TheoryData ArraySizesData => new TheoryData { 7, 16, 1111 }; [Fact] public void IsSpecialImplementation() => Assert.IsType(PixelOperations.Instance); [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromGray8Bytes(int count) { byte[] source = CreateByteTestData(count); var expected = new Gray8[count]; for (int i = 0; i < count; i++) { expected[i].PackFromGray8(new Gray8(source[i])); } TestOperation( source, expected, (s, d) => Operations.PackFromGray8Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToGray8Bytes(int count) { Gray8[] source = CreatePixelTestData(count); byte[] expected = new byte[count]; var gray = default(Gray8); for (int i = 0; i < count; i++) { gray.PackFromScaledVector4(source[i].ToScaledVector4()); expected[i] = gray.PackedValue; } TestOperation( source, expected, (s, d) => Operations.ToGray8Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromGray16Bytes(int count) { byte[] source = CreateByteTestData(count * 2); Span sourceSpan = source.AsSpan(); var expected = new Gray8[count]; for (int i = 0; i < count; i++) { int i2 = i * 2; expected[i].PackFromGray16(MemoryMarshal.Cast(sourceSpan.Slice(i2, 2))[0]); } TestOperation( source, expected, (s, d) => Operations.PackFromGray16Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToGray16Bytes(int count) { Gray8[] source = CreatePixelTestData(count); byte[] expected = new byte[count * 2]; Gray16 gray = default; for (int i = 0; i < count; i++) { int i2 = i * 2; gray.PackFromScaledVector4(source[i].ToScaledVector4()); OctetBytes bytes = Unsafe.As(ref gray); expected[i2] = bytes[0]; expected[i2 + 1] = bytes[1]; } TestOperation( source, expected, (s, d) => Operations.ToGray16Bytes(s, d.GetSpan(), count) ); } } public class Gray16OperationsTests : PixelOperationsTests { public Gray16OperationsTests(ITestOutputHelper output) : base(output) { } public static new TheoryData ArraySizesData => new TheoryData { 7, 16, 1111 }; [Fact] public void IsSpecialImplementation() => Assert.IsType(PixelOperations.Instance); [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromGray8Bytes(int count) { byte[] source = CreateByteTestData(count); var expected = new Gray16[count]; for (int i = 0; i < count; i++) { expected[i].PackFromGray8(new Gray8(source[i])); } TestOperation( source, expected, (s, d) => Operations.PackFromGray8Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToGray8Bytes(int count) { Gray16[] source = CreatePixelTestData(count); byte[] expected = new byte[count]; var gray = default(Gray8); for (int i = 0; i < count; i++) { gray.PackFromScaledVector4(source[i].ToScaledVector4()); expected[i] = gray.PackedValue; } TestOperation( source, expected, (s, d) => Operations.ToGray8Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromGray16Bytes(int count) { byte[] source = CreateByteTestData(count * 2); Span sourceSpan = source.AsSpan(); var expected = new Gray16[count]; for (int i = 0; i < count; i++) { int i2 = i * 2; expected[i].PackFromGray16(MemoryMarshal.Cast(sourceSpan.Slice(i2, 2))[0]); } TestOperation( source, expected, (s, d) => Operations.PackFromGray16Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToGray16Bytes(int count) { Gray16[] source = CreatePixelTestData(count); byte[] expected = new byte[count * 2]; Gray16 gray = default; for (int i = 0; i < count; i++) { int i2 = i * 2; gray.PackFromScaledVector4(source[i].ToScaledVector4()); OctetBytes bytes = Unsafe.As(ref gray); expected[i2] = bytes[0]; expected[i2 + 1] = bytes[1]; } TestOperation( source, expected, (s, d) => Operations.ToGray16Bytes(s, d.GetSpan(), count) ); } } public class Rgba32OperationsTests : PixelOperationsTests { public Rgba32OperationsTests(ITestOutputHelper output) : base(output) { } public static new TheoryData ArraySizesData => new TheoryData { 7, 16, 1111 }; [Fact] public void IsSpecialImplementation() => Assert.IsType(PixelOperations.Instance); [Fact] public void ToVector4SimdAligned() { if (!Vector.IsHardwareAccelerated) { return; } Rgba32[] source = CreatePixelTestData(64); Vector4[] expected = CreateExpectedVector4Data(source); TestOperation( source, expected, (s, d) => Rgba32.PixelOperations.ToVector4SimdAligned(s, d.GetSpan(), 64) ); } // [Fact] // Profiling benchmark - enable manually! #pragma warning disable xUnit1013 // Public method should be marked as test public void Benchmark_ToVector4() #pragma warning restore xUnit1013 // Public method should be marked as test { const int times = 200000; const int count = 1024; using (IMemoryOwner source = Configuration.Default.MemoryAllocator.Allocate(count)) using (IMemoryOwner dest = Configuration.Default.MemoryAllocator.Allocate(count)) { this.Measure( times, () => PixelOperations.Instance.ToVector4(source.GetSpan(), dest.GetSpan(), count)); } } } public class Rgb48OperationsTests : PixelOperationsTests { public Rgb48OperationsTests(ITestOutputHelper output) : base(output) { } public static new TheoryData ArraySizesData => new TheoryData { 7, 16, 1111 }; [Fact] public void IsSpecialImplementation() => Assert.IsType(PixelOperations.Instance); } public class Rgba64OperationsTests : PixelOperationsTests { public Rgba64OperationsTests(ITestOutputHelper output) : base(output) { } public static new TheoryData ArraySizesData => new TheoryData { 7, 16, 1111 }; [Fact] public void IsSpecialImplementation() => Assert.IsType(PixelOperations.Instance); } public class RgbaVectorOperationsTests : PixelOperationsTests { public RgbaVectorOperationsTests(ITestOutputHelper output) : base(output) { } public static new TheoryData ArraySizesData => new TheoryData { 7, 16, 1111 }; [Fact] public void IsSpecialImplementation() => Assert.IsType(PixelOperations.Instance); } [Theory] [WithBlankImages(1, 1, PixelTypes.All)] public void GetGlobalInstance(TestImageProvider _) where TPixel : struct, IPixel => Assert.NotNull(PixelOperations.Instance); [Fact] public void IsOpaqueColor() { Assert.True(new GraphicsOptions(true).IsOpaqueColorWithoutBlending(Rgba32.Red)); Assert.False(new GraphicsOptions(true, 0.5f).IsOpaqueColorWithoutBlending(Rgba32.Red)); Assert.False(new GraphicsOptions(true).IsOpaqueColorWithoutBlending(Rgba32.Transparent)); Assert.False(new GraphicsOptions(true, PixelColorBlendingMode.Lighten, 1).IsOpaqueColorWithoutBlending(Rgba32.Red)); Assert.False(new GraphicsOptions(true, PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.DestOver, 1).IsOpaqueColorWithoutBlending(Rgba32.Red)); } } public abstract class PixelOperationsTests : MeasureFixture where TPixel : struct, IPixel { protected PixelOperationsTests(ITestOutputHelper output) : base(output) { } public static TheoryData ArraySizesData => new TheoryData { 7, 16, 1111 }; internal static PixelOperations Operations => PixelOperations.Instance; internal static TPixel[] CreateExpectedPixelData(Vector4[] source) { var expected = new TPixel[source.Length]; for (int i = 0; i < expected.Length; i++) { expected[i].PackFromVector4(source[i]); } return expected; } internal static TPixel[] CreateScaledExpectedPixelData(Vector4[] source) { var expected = new TPixel[source.Length]; for (int i = 0; i < expected.Length; i++) { expected[i].PackFromScaledVector4(source[i]); } return expected; } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromVector4(int count) { Vector4[] source = CreateVector4TestData(count); TPixel[] expected = CreateExpectedPixelData(source); TestOperation( source, expected, (s, d) => Operations.PackFromVector4(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromScaledVector4(int count) { Vector4[] source = CreateVector4TestData(count); TPixel[] expected = CreateScaledExpectedPixelData(source); TestOperation( source, expected, (s, d) => Operations.PackFromScaledVector4(s, d.GetSpan(), count) ); } internal static Vector4[] CreateExpectedVector4Data(TPixel[] source) { var expected = new Vector4[source.Length]; for (int i = 0; i < expected.Length; i++) { expected[i] = source[i].ToVector4(); } return expected; } internal static Vector4[] CreateExpectedScaledVector4Data(TPixel[] source) { var expected = new Vector4[source.Length]; for (int i = 0; i < expected.Length; i++) { expected[i] = source[i].ToScaledVector4(); } return expected; } [Theory] [MemberData(nameof(ArraySizesData))] public void ToVector4(int count) { TPixel[] source = CreatePixelTestData(count); Vector4[] expected = CreateExpectedVector4Data(source); TestOperation( source, expected, (s, d) => Operations.ToVector4(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToScaledVector4(int count) { TPixel[] source = CreateScaledPixelTestData(count); Vector4[] expected = CreateExpectedScaledVector4Data(source); TestOperation( source, expected, (s, d) => Operations.ToScaledVector4(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromArgb32Bytes(int count) { byte[] source = CreateByteTestData(count * 4); var expected = new TPixel[count]; for (int i = 0; i < count; i++) { int i4 = i * 4; expected[i].PackFromArgb32(new Argb32(source[i4 + 1], source[i4 + 2], source[i4 + 3], source[i4 + 0])); } TestOperation( source, expected, (s, d) => Operations.PackFromArgb32Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToArgb32Bytes(int count) { TPixel[] source = CreatePixelTestData(count); byte[] expected = new byte[count * 4]; var argb = default(Argb32); for (int i = 0; i < count; i++) { int i4 = i * 4; argb.PackFromScaledVector4(source[i].ToScaledVector4()); expected[i4] = argb.A; expected[i4 + 1] = argb.R; expected[i4 + 2] = argb.G; expected[i4 + 3] = argb.B; } TestOperation( source, expected, (s, d) => Operations.ToArgb32Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromBgr24Bytes(int count) { byte[] source = CreateByteTestData(count * 3); var expected = new TPixel[count]; for (int i = 0; i < count; i++) { int i3 = i * 3; expected[i].PackFromBgr24(new Bgr24(source[i3 + 2], source[i3 + 1], source[i3])); } TestOperation( source, expected, (s, d) => Operations.PackFromBgr24Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToBgr24Bytes(int count) { TPixel[] source = CreatePixelTestData(count); byte[] expected = new byte[count * 3]; var bgr = default(Bgr24); for (int i = 0; i < count; i++) { int i3 = i * 3; bgr.PackFromScaledVector4(source[i].ToScaledVector4()); expected[i3] = bgr.B; expected[i3 + 1] = bgr.G; expected[i3 + 2] = bgr.R; } TestOperation( source, expected, (s, d) => Operations.ToBgr24Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromBgra32Bytes(int count) { byte[] source = CreateByteTestData(count * 4); var expected = new TPixel[count]; for (int i = 0; i < count; i++) { int i4 = i * 4; expected[i].PackFromBgra32(new Bgra32(source[i4 + 2], source[i4 + 1], source[i4 + 0], source[i4 + 3])); } TestOperation( source, expected, (s, d) => Operations.PackFromBgra32Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToBgra32Bytes(int count) { TPixel[] source = CreatePixelTestData(count); byte[] expected = new byte[count * 4]; var bgra = default(Bgra32); for (int i = 0; i < count; i++) { int i4 = i * 4; bgra.PackFromScaledVector4(source[i].ToScaledVector4()); expected[i4] = bgra.B; expected[i4 + 1] = bgra.G; expected[i4 + 2] = bgra.R; expected[i4 + 3] = bgra.A; } TestOperation( source, expected, (s, d) => Operations.ToBgra32Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromRgb24Bytes(int count) { byte[] source = CreateByteTestData(count * 3); var expected = new TPixel[count]; for (int i = 0; i < count; i++) { int i3 = i * 3; expected[i].PackFromRgb24(new Rgb24(source[i3 + 0], source[i3 + 1], source[i3 + 2])); } TestOperation( source, expected, (s, d) => Operations.PackFromRgb24Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToRgb24Bytes(int count) { TPixel[] source = CreatePixelTestData(count); byte[] expected = new byte[count * 3]; var rgb = default(Rgb24); for (int i = 0; i < count; i++) { int i3 = i * 3; rgb.PackFromScaledVector4(source[i].ToScaledVector4()); expected[i3] = rgb.R; expected[i3 + 1] = rgb.G; expected[i3 + 2] = rgb.B; } TestOperation( source, expected, (s, d) => Operations.ToRgb24Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromRgba32Bytes(int count) { byte[] source = CreateByteTestData(count * 4); var expected = new TPixel[count]; for (int i = 0; i < count; i++) { int i4 = i * 4; expected[i].PackFromRgba32(new Rgba32(source[i4 + 0], source[i4 + 1], source[i4 + 2], source[i4 + 3])); } TestOperation( source, expected, (s, d) => Operations.PackFromRgba32Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToRgba32Bytes(int count) { TPixel[] source = CreatePixelTestData(count); byte[] expected = new byte[count * 4]; var rgba = default(Rgba32); for (int i = 0; i < count; i++) { int i4 = i * 4; rgba.PackFromScaledVector4(source[i].ToScaledVector4()); expected[i4] = rgba.R; expected[i4 + 1] = rgba.G; expected[i4 + 2] = rgba.B; expected[i4 + 3] = rgba.A; } TestOperation( source, expected, (s, d) => Operations.ToRgba32Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromRgb48Bytes(int count) { byte[] source = CreateByteTestData(count * 6); Span sourceSpan = source.AsSpan(); var expected = new TPixel[count]; for (int i = 0; i < count; i++) { int i6 = i * 6; expected[i].PackFromRgb48(MemoryMarshal.Cast(sourceSpan.Slice(i6, 6))[0]); } TestOperation( source, expected, (s, d) => Operations.PackFromRgb48Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToRgb48Bytes(int count) { TPixel[] source = CreatePixelTestData(count); byte[] expected = new byte[count * 6]; Rgb48 rgb = default; for (int i = 0; i < count; i++) { int i6 = i * 6; rgb.PackFromScaledVector4(source[i].ToScaledVector4()); OctetBytes rgb48Bytes = Unsafe.As(ref rgb); expected[i6] = rgb48Bytes[0]; expected[i6 + 1] = rgb48Bytes[1]; expected[i6 + 2] = rgb48Bytes[2]; expected[i6 + 3] = rgb48Bytes[3]; expected[i6 + 4] = rgb48Bytes[4]; expected[i6 + 5] = rgb48Bytes[5]; } TestOperation( source, expected, (s, d) => Operations.ToRgb48Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void PackFromRgba64Bytes(int count) { byte[] source = CreateByteTestData(count * 8); Span sourceSpan = source.AsSpan(); var expected = new TPixel[count]; for (int i = 0; i < count; i++) { int i8 = i * 8; expected[i].PackFromRgba64(MemoryMarshal.Cast(sourceSpan.Slice(i8, 8))[0]); } TestOperation( source, expected, (s, d) => Operations.PackFromRgba64Bytes(s, d.GetSpan(), count) ); } [Theory] [MemberData(nameof(ArraySizesData))] public void ToRgba64Bytes(int count) { TPixel[] source = CreatePixelTestData(count); byte[] expected = new byte[count * 8]; Rgba64 rgba = default; for (int i = 0; i < count; i++) { int i8 = i * 8; rgba.PackFromScaledVector4(source[i].ToScaledVector4()); OctetBytes rgba64Bytes = Unsafe.As(ref rgba); expected[i8] = rgba64Bytes[0]; expected[i8 + 1] = rgba64Bytes[1]; expected[i8 + 2] = rgba64Bytes[2]; expected[i8 + 3] = rgba64Bytes[3]; expected[i8 + 4] = rgba64Bytes[4]; expected[i8 + 5] = rgba64Bytes[5]; expected[i8 + 6] = rgba64Bytes[6]; expected[i8 + 7] = rgba64Bytes[7]; } TestOperation( source, expected, (s, d) => Operations.ToRgba64Bytes(s, d.GetSpan(), count) ); } internal static void TestOperation( TSource[] source, TDest[] expected, Action> action) where TSource : struct where TDest : struct { using (var buffers = new TestBuffers(source, expected)) { action(buffers.SourceBuffer, buffers.ActualDestBuffer); buffers.Verify(); } } internal static Vector4[] CreateVector4TestData(int length) { var result = new Vector4[length]; var rnd = new Random(42); // Deterministic random values for (int i = 0; i < result.Length; i++) { result[i] = GetVector(rnd); } return result; } internal static TPixel[] CreatePixelTestData(int length) { var result = new TPixel[length]; var rnd = new Random(42); // Deterministic random values for (int i = 0; i < result.Length; i++) { Vector4 v = GetVector(rnd); result[i].PackFromVector4(v); } return result; } internal static TPixel[] CreateScaledPixelTestData(int length) { var result = new TPixel[length]; var rnd = new Random(42); // Deterministic random values for (int i = 0; i < result.Length; i++) { Vector4 v = GetVector(rnd); result[i].PackFromScaledVector4(v); } return result; } internal static byte[] CreateByteTestData(int length) { byte[] result = new byte[length]; var rnd = new Random(42); // Deterministic random values for (int i = 0; i < result.Length; i++) { result[i] = (byte)rnd.Next(255); } return result; } internal static Vector4 GetVector(Random rnd) { return new Vector4( (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble(), (float)rnd.NextDouble() ); } [StructLayout(LayoutKind.Sequential)] internal unsafe struct OctetBytes { public fixed byte Data[8]; public byte this[int idx] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { ref byte self = ref Unsafe.As(ref this); return Unsafe.Add(ref self, idx); } } } private class TestBuffers : IDisposable where TSource : struct where TDest : struct { public TSource[] SourceBuffer { get; } public IMemoryOwner ActualDestBuffer { get; } public TDest[] ExpectedDestBuffer { get; } public TestBuffers(TSource[] source, TDest[] expectedDest) { this.SourceBuffer = source; this.ExpectedDestBuffer = expectedDest; this.ActualDestBuffer = Configuration.Default.MemoryAllocator.Allocate(expectedDest.Length); } public void Dispose() => this.ActualDestBuffer.Dispose(); public void Verify() { int count = this.ExpectedDestBuffer.Length; if (typeof(TDest) == typeof(Vector4)) { Span expected = MemoryMarshal.Cast(this.ExpectedDestBuffer.AsSpan()); Span actual = MemoryMarshal.Cast(this.ActualDestBuffer.GetSpan()); var comparer = new ApproximateFloatComparer(0.001f); for (int i = 0; i < count; i++) { // ReSharper disable PossibleNullReferenceException Assert.Equal(expected[i], actual[i], comparer); // ReSharper restore PossibleNullReferenceException } } else { Span expected = this.ExpectedDestBuffer.AsSpan(); Span actual = this.ActualDestBuffer.GetSpan(); for (int i = 0; i < count; i++) { Assert.Equal(expected[i], actual[i]); } } } } } }