diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 6ac8ac1..96d29b4 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -16,7 +16,7 @@ jobs: build-and-test: name: build-and-test - runs-on: ubuntu-latest + runs-on: windows-latest steps: - uses: actions/checkout@v3 diff --git a/BCnEnc.Net/AssemblyInfo.cs b/BCnEnc.Net/AssemblyInfo.cs index 6d23fdb..0bce899 100644 --- a/BCnEnc.Net/AssemblyInfo.cs +++ b/BCnEnc.Net/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("BCnEncTests")] +[assembly: InternalsVisibleTo("BCnEncTests.Framework")] diff --git a/BCnEnc.Net/BCnEncoder.csproj b/BCnEnc.Net/BCnEncoder.csproj index 7b60090..84f5a2b 100644 --- a/BCnEnc.Net/BCnEncoder.csproj +++ b/BCnEnc.Net/BCnEncoder.csproj @@ -1,7 +1,8 @@  - netstandard2.1 + netstandard2.1;netstandard2.0 + 8.0 true true snupkg @@ -49,5 +50,12 @@ Supported formats are: + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/BCnEnc.Net/Decoder/BcDecoder.cs b/BCnEnc.Net/Decoder/BcDecoder.cs index 446c83a..c11ec62 100644 --- a/BCnEnc.Net/Decoder/BcDecoder.cs +++ b/BCnEnc.Net/Decoder/BcDecoder.cs @@ -166,7 +166,7 @@ public Task> Decode2DAsync(Stream inputStream, Cancellatio /// The awaitable operation to retrieve the decoded image. public Task[]> DecodeAllMipMaps2DAsync(Stream inputStream, CancellationToken token = default) { - return Task.Run(() => DecodeFromStreamInternal2D(inputStream, false, token), token); + return Task.Run(() => DecodeFromStreamInternal2D(inputStream, true, token), token); } /// @@ -675,7 +675,7 @@ public Task> DecodeHdr2DAsync(Stream inputStream, Cancel /// The awaitable operation to retrieve the decoded image. public Task[]> DecodeAllMipMapsHdr2DAsync(Stream inputStream, CancellationToken token = default) { - return Task.Run(() => DecodeFromStreamInternalHdr2D(inputStream, false, token), token); + return Task.Run(() => DecodeFromStreamInternalHdr2D(inputStream, true, token), token); } /// diff --git a/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs index 3f19504..0eb201d 100644 --- a/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs +++ b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs @@ -5,6 +5,10 @@ using System.Text; using BCnEncoder.Shared; +#if NETSTANDARD2_0 +using MemoryMarshal = BCnEncoder.Shared.MemoryMarshalPolyfills; +#endif + namespace BCnEncoder.Encoder.Bptc { internal static class BptcEncodingHelpers @@ -20,7 +24,7 @@ public static int InterpolateInt(int e0, int e1, int index, int indexPrecision) var aWeights2 = ColorInterpolationWeights2; var aWeights3 = ColorInterpolationWeights3; var aWeights4 = ColorInterpolationWeights4; - + if (indexPrecision == 2) return (((64 - aWeights2[index]) * e0 + aWeights2[index] * e1 + 32) >> 6); if (indexPrecision == 3) @@ -50,9 +54,20 @@ public static int[] Rank2SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, { var output = Enumerable.Range(0, smallIndex ? 32 : 64).ToArray(); + // Copy struct to array before the closure so that reducedIndicesBlock is not + // heap-captured. On .NET Framework, MemoryMarshalPolyfills.CreateSpan uses + // Unsafe.AsPointer, which is only GC-safe for stack-allocated structs. + #if NETSTANDARD2_0 + var indices = new int[16]; + reducedIndicesBlock.AsSpan.CopyTo(indices); + #endif int CalculatePartitionError(int partitionIndex) - { + { + #if NETSTANDARD2_1 + var indices = reducedIndicesBlock.AsSpan; + #endif + var error = 0; ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[partitionIndex]; Span subset0 = stackalloc int[numDistinctClusters]; @@ -60,12 +75,12 @@ int CalculatePartitionError(int partitionIndex) var max0Idx = 0; var max1Idx = 0; - //Calculate largest cluster index for each subset + //Calculate largest cluster index for each subset for (var i = 0; i < 16; i++) { if (partitionTable[i] == 0) { - var r = reducedIndicesBlock[i]; + var r = indices[i]; subset0[r]++; var count = subset0[r]; if (count > subset0[max0Idx]) @@ -75,7 +90,7 @@ int CalculatePartitionError(int partitionIndex) } else { - var r = reducedIndicesBlock[i]; + var r = indices[i]; subset1[r]++; var count = subset1[r]; if (count > subset1[max1Idx]) @@ -90,11 +105,11 @@ int CalculatePartitionError(int partitionIndex) { if (partitionTable[i] == 0) { - if (reducedIndicesBlock[i] != max0Idx) error++; + if (indices[i] != max0Idx) error++; } else { - if (reducedIndicesBlock[i] != max1Idx) error++; + if (indices[i] != max1Idx) error++; } } @@ -110,8 +125,20 @@ public static int[] Rank3SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, { var output = Enumerable.Range(0, 64).ToArray(); + // Copy struct to array before the closure so that reducedIndicesBlock is not + // heap-captured. On .NET Framework, MemoryMarshalPolyfills.CreateSpan uses + // Unsafe.AsPointer, which is only GC-safe for stack-allocated structs. + #if NETSTANDARD2_0 + var indices = new int[16]; + reducedIndicesBlock.AsSpan.CopyTo(indices); + #endif + int CalculatePartitionError(int partitionIndex) { + #if NETSTANDARD2_1 + var indices = reducedIndicesBlock.AsSpan; + #endif + var error = 0; ReadOnlySpan partitionTable = Bc7Block.Subsets3PartitionTable[partitionIndex]; @@ -122,12 +149,12 @@ int CalculatePartitionError(int partitionIndex) var max1Idx = 0; var max2Idx = 0; - //Calculate largest cluster index for each subset + //Calculate largest cluster index for each subset for (var i = 0; i < 16; i++) { if (partitionTable[i] == 0) { - var r = reducedIndicesBlock[i]; + var r = indices[i]; subset0[r]++; var count = subset0[r]; if (count > subset0[max0Idx]) @@ -137,7 +164,7 @@ int CalculatePartitionError(int partitionIndex) } else if (partitionTable[i] == 1) { - var r = reducedIndicesBlock[i]; + var r = indices[i]; subset1[r]++; var count = subset1[r]; if (count > subset1[max1Idx]) @@ -147,7 +174,7 @@ int CalculatePartitionError(int partitionIndex) } else { - var r = reducedIndicesBlock[i]; + var r = indices[i]; subset2[r]++; var count = subset2[r]; if (count > subset2[max2Idx]) @@ -162,15 +189,15 @@ int CalculatePartitionError(int partitionIndex) { if (partitionTable[i] == 0) { - if (reducedIndicesBlock[i] != max0Idx) error++; + if (indices[i] != max0Idx) error++; } else if (partitionTable[i] == 1) { - if (reducedIndicesBlock[i] != max1Idx) error++; + if (indices[i] != max1Idx) error++; } else { - if (reducedIndicesBlock[i] != max2Idx) error++; + if (indices[i] != max2Idx) error++; } } diff --git a/BCnEnc.Net/Encoder/LeastSquares.cs b/BCnEnc.Net/Encoder/LeastSquares.cs index 1c9bf68..bafdb61 100644 --- a/BCnEnc.Net/Encoder/LeastSquares.cs +++ b/BCnEnc.Net/Encoder/LeastSquares.cs @@ -5,6 +5,10 @@ using BCnEncoder.Encoder.Bptc; using BCnEncoder.Shared; +#if NETSTANDARD2_0 +using Math = BCnEncoder.Shared.MathPolyfills; +#endif + namespace BCnEncoder.Encoder { /// diff --git a/BCnEnc.Net/Shared/Colors.cs b/BCnEnc.Net/Shared/Colors.cs index b44645b..488d0f6 100644 --- a/BCnEnc.Net/Shared/Colors.cs +++ b/BCnEnc.Net/Shared/Colors.cs @@ -1241,7 +1241,7 @@ public static ColorLab XyzToLab(ColorXyz xyz) private static float PivotXyz(float n) { - var i = MathF.Cbrt(n); + var i = MathCbrt.Cbrt(n); return n > 0.008856f ? i : 7.787f * n + 16 / 116f; } } diff --git a/BCnEnc.Net/Shared/HdrImage.cs b/BCnEnc.Net/Shared/HdrImage.cs index 0dd33ec..a4143de 100644 --- a/BCnEnc.Net/Shared/HdrImage.cs +++ b/BCnEnc.Net/Shared/HdrImage.cs @@ -73,7 +73,7 @@ private static string ReadFromStream(Stream stream) c = (char)b; buffer[i++] = c; } while (c != (char)10); - return new string(buffer.AsSpan().Slice(0, i)).Trim(); + return new string(buffer, 0, i).Trim(); } private static void WriteLineToStream(BinaryWriter br, string s) diff --git a/BCnEnc.Net/Shared/LinearClustering.cs b/BCnEnc.Net/Shared/LinearClustering.cs index baf10aa..7e23f5b 100644 --- a/BCnEnc.Net/Shared/LinearClustering.cs +++ b/BCnEnc.Net/Shared/LinearClustering.cs @@ -1,6 +1,10 @@ using System; using System.Collections.Generic; +#if NETSTANDARD2_0 +using Array = BCnEncoder.Shared.ArrayPolyfills; +#endif + namespace BCnEncoder.Shared { /// @@ -194,7 +198,7 @@ public static int[] ClusterPixels(ReadOnlySpan pixels, int width, i if (enforceConnectivity) { clusterIndices = EnforceConnectivity(clusterIndices, width, height, clusters); } - + return clusterIndices; } @@ -427,7 +431,7 @@ private static int[] EnforceConnectivity(int[] oldLabels, int width, int height, { ReadOnlySpan neighborX = new[] { -1, 0, 1, 0 }; ReadOnlySpan neighborY = new[] { 0, -1, 0, 1 }; - + var sSquared = width * height / clusters; var clusterX = new List(sSquared); diff --git a/BCnEnc.Net/Shared/MipMapper.cs b/BCnEnc.Net/Shared/MipMapper.cs index e44dbf0..ddbcea7 100644 --- a/BCnEnc.Net/Shared/MipMapper.cs +++ b/BCnEnc.Net/Shared/MipMapper.cs @@ -164,15 +164,14 @@ public static void CalculateMipLevelSize(int width, int height, int mipIdx, out mipHeight = Math.Max(1, height >> mipIdx); } - private static ColorRgba32[,] ResizeToHalf(ReadOnlySpan2D pixelsRgba) + private static Memory2D ResizeToHalf(ReadOnlySpan2D pixelsRgba) { - var oldWidth = pixelsRgba.Width; var oldHeight = pixelsRgba.Height; var newWidth = Math.Max(1, oldWidth >> 1); var newHeight = Math.Max(1, oldHeight >> 1); - var result = new ColorRgba32[newHeight, newWidth]; + var result = new ColorRgba32[newHeight * newWidth]; int ClampW(int x) => Math.Max(0, Math.Min(oldWidth - 1, x)); int ClampH(int y) => Math.Max(0, Math.Min(oldHeight - 1, y)); @@ -186,22 +185,21 @@ public static void CalculateMipLevelSize(int width, int height, int mipIdx, out var ll = pixelsRgba[ClampH(y2 * 2 + 1), ClampW(x2 * 2)].ToFloat(); var lr = pixelsRgba[ClampH(y2 * 2 + 1), ClampW(x2 * 2 + 1)].ToFloat(); - result[y2, x2] = ((ul + ur + ll + lr) / 4).ToRgba32(); + result[y2 * newWidth + x2] = ((ul + ur + ll + lr) / 4).ToRgba32(); } } - return result; + return ((Memory)result).AsMemory2D(newHeight, newWidth); } - private static ColorRgbFloat[,] ResizeToHalf(ReadOnlySpan2D pixelsRgba) + private static Memory2D ResizeToHalf(ReadOnlySpan2D pixelsRgba) { - var oldWidth = pixelsRgba.Width; var oldHeight = pixelsRgba.Height; var newWidth = Math.Max(1, oldWidth >> 1); var newHeight = Math.Max(1, oldHeight >> 1); - var result = new ColorRgbFloat[newHeight, newWidth]; + var result = new ColorRgbFloat[newHeight * newWidth]; int ClampW(int x) => Math.Max(0, Math.Min(oldWidth - 1, x)); int ClampH(int y) => Math.Max(0, Math.Min(oldHeight - 1, y)); @@ -215,11 +213,11 @@ public static void CalculateMipLevelSize(int width, int height, int mipIdx, out var ll = pixelsRgba[ClampH(y2 * 2 + 1), ClampW(x2 * 2)]; var lr = pixelsRgba[ClampH(y2 * 2 + 1), ClampW(x2 * 2 + 1)]; - result[y2, x2] = ((ul + ur + ll + lr) / 4); + result[y2 * newWidth + x2] = ((ul + ur + ll + lr) / 4); } } - return result; + return ((Memory)result).AsMemory2D(newHeight, newWidth); } } } diff --git a/BCnEnc.Net/Shared/NetStdPolyfills.cs b/BCnEnc.Net/Shared/NetStdPolyfills.cs new file mode 100644 index 0000000..3936f96 --- /dev/null +++ b/BCnEnc.Net/Shared/NetStdPolyfills.cs @@ -0,0 +1,155 @@ + +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using CommunityToolkit.HighPerformance; + + +namespace BCnEncoder.Shared +{ +#if NETSTANDARD2_0 + internal static class MemoryPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory2D AsMemory2D(this Memory memory, int height, int width) + { + if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment)) + { + T[] array = segment.Array; + ref T value = ref array.DangerousGetReference(); + return Memory2D.DangerousCreate(array, ref value, height, width, 0); + } + else + { + throw new NotSupportedException(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory2D AsMemory2D(this ReadOnlyMemory memory, int height, int width) + { + if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment)) + { + T[] array = segment.Array; + ref T value = ref array.DangerousGetReference(); + return ReadOnlyMemory2D.DangerousCreate(array, ref value, height, width, 0); + } + else + { + throw new NotSupportedException(); + } + } + } + + internal static class SpanPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe ReadOnlySpan2D AsSpan2D(this ReadOnlySpan span, int height, int width) + { + ref T value = ref span.DangerousGetReference(); + void* pointer = Unsafe.AsPointer(ref value); + return new ReadOnlySpan2D(pointer, height, width, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span GetRowSpan(this Span2D span, int row) + { + ref T value = ref span.DangerousGetReferenceAt(row, 0); + void* pointer = Unsafe.AsPointer(ref value); + return new Span(pointer, span.Width); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe ReadOnlySpan GetRowSpan(this ReadOnlySpan2D span, int row) + { + ref T value = ref span.DangerousGetReferenceAt(row, 0); + void* pointer = Unsafe.AsPointer(ref value); + return new Span(pointer, span.Width); + } + } + + internal static class BinaryWriterPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Write(this BinaryWriter writer, ReadOnlySpan buffer) + { + writer.BaseStream.Write(buffer); + } + } + + internal static class BinaryReaderPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Read(this BinaryReader reader, Span buffer) + { + return reader.BaseStream.Read(buffer); + } + } + + internal static class EncodingPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe string GetString(this Encoding encoding, ReadOnlySpan buffer) + { + ref byte value = ref buffer.DangerousGetReference(); + byte* pointer = (byte*)Unsafe.AsPointer(ref value); + return encoding.GetString(pointer, buffer.Length); + } + } + + internal static class MemoryMarshalPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span CreateSpan(ref T reference, int length) + { + return new Span(Unsafe.AsPointer(ref reference), length); + } + } + + internal static class ArrayPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Fill(T[] array, T value) + { + array.AsSpan().Fill(value); + } + } + + internal static class MathPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Clamp(float value, float min, float max) + { + if (min > max) + { + throw new ArgumentException(); + } + + if (value < min) + { + return min; + } + else if (value > max) + { + return max; + } + + return value; + } + } +#endif + + internal static class MathCbrt + { + public static float Cbrt(float f) + { + #if NETSTANDARD2_0 + return MathF.Pow(f, 1 / 3.0f); + #else + return MathF.Cbrt(f); + #endif + } + } +} diff --git a/BCnEnc.Net/Shared/RawBlocks.cs b/BCnEnc.Net/Shared/RawBlocks.cs index c78e5d8..dadb4cc 100644 --- a/BCnEnc.Net/Shared/RawBlocks.cs +++ b/BCnEnc.Net/Shared/RawBlocks.cs @@ -2,6 +2,10 @@ using System.Runtime.InteropServices; using BCnEncoder.Encoder.Bptc; +#if NETSTANDARD2_0 +using MemoryMarshal = BCnEncoder.Shared.MemoryMarshalPolyfills; +#endif + namespace BCnEncoder.Shared { @@ -19,7 +23,7 @@ public RawBlock4X4Rgba32(ColorRgba32 fillColor) p20 = p21 = p22 = p23 = p30 = p31 = p32 = p33 = fillColor; } - + public Span AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); public ColorRgba32 this[int x, int y] @@ -156,7 +160,7 @@ public RawBlock4X4RgbFloat(ColorRgbFloat fillColor) p20 = p21 = p22 = p23 = p30 = p31 = p32 = p33 = fillColor; } - + public Span AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); public ColorRgbFloat this[int x, int y] @@ -184,7 +188,7 @@ internal float CalculateError(RawBlock4X4RgbFloat other) var re = Math.Sign(col1.r) * MathF.Log( 1 + MathF.Abs(col1.r)) - Math.Sign(col2.r) * MathF.Log( 1 + MathF.Abs(col2.r)); var ge = Math.Sign(col1.g) * MathF.Log( 1 + MathF.Abs(col1.g)) - Math.Sign(col2.g) * MathF.Log( 1 + MathF.Abs(col2.g)); var be = Math.Sign(col1.b) * MathF.Log( 1 + MathF.Abs(col1.b)) - Math.Sign(col2.b) * MathF.Log( 1 + MathF.Abs(col2.b)); - + error += re * re; error += ge * ge; error += be * be; @@ -222,7 +226,7 @@ internal float CalculateYCbCrError(RawBlock4X4RgbFloat other) return error; } - + internal RawBlock4X4Ycbcr ToRawBlockYcbcr() { diff --git a/BCnEncNet.sln b/BCnEncNet.sln index 6e3c4d8..dca5cf7 100644 --- a/BCnEncNet.sln +++ b/BCnEncNet.sln @@ -14,6 +14,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BCnEncoder.NET.ImageSharp", "BCnEncoder.NET.ImageSharp\BCnEncoder.NET.ImageSharp.csproj", "{7D884C56-B982-4B8C-907E-68F231CF3D89}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "BCnEncTests.Shared", "BCnEncTests.Shared\BCnEncTests.Shared.shproj", "{D954291E-2A0B-460D-934E-DC6B0785DB48}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BCnEncTests.Framework", "BCnEncTests.Framework\BCnEncTests.Framework.csproj", "{CA12ED85-B4E8-4D62-9B03-0D02F742B00C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +36,10 @@ Global {7D884C56-B982-4B8C-907E-68F231CF3D89}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D884C56-B982-4B8C-907E-68F231CF3D89}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D884C56-B982-4B8C-907E-68F231CF3D89}.Release|Any CPU.Build.0 = Release|Any CPU + {CA12ED85-B4E8-4D62-9B03-0D02F742B00C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA12ED85-B4E8-4D62-9B03-0D02F742B00C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA12ED85-B4E8-4D62-9B03-0D02F742B00C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA12ED85-B4E8-4D62-9B03-0D02F742B00C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BCnEncTests.Framework/BCnEncTests.Framework.csproj b/BCnEncTests.Framework/BCnEncTests.Framework.csproj new file mode 100644 index 0000000..c928b90 --- /dev/null +++ b/BCnEncTests.Framework/BCnEncTests.Framework.csproj @@ -0,0 +1,33 @@ + + + + net481 + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/BCnEncTests.Framework/HdrImageTests.cs b/BCnEncTests.Framework/HdrImageTests.cs new file mode 100644 index 0000000..75e9c8e --- /dev/null +++ b/BCnEncTests.Framework/HdrImageTests.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class HdrImageTests + { + [Fact] + public void LoadHdr() + { + using (var stream = File.OpenRead("../../../../BCnEncTests/testImages/test_hdr_kiara.hdr")) + { + var hdrImg = HdrImage.Read(stream); + Assert.True(hdrImg.width > 0); + Assert.True(hdrImg.height > 0); + Assert.True(hdrImg.pixels.Length == hdrImg.width * hdrImg.height); + + var rgba = new ColorRgba32[hdrImg.pixels.Length]; + for (var i = 0; i < hdrImg.pixels.Length; i++) + { + var p = hdrImg.pixels[i]; + rgba[i] = new ColorRgba32( + (byte)(Math.Max(0, Math.Min(1, p.r)) * 255 + 0.5f), + (byte)(Math.Max(0, Math.Min(1, p.g)) * 255 + 0.5f), + (byte)(Math.Max(0, Math.Min(1, p.b)) * 255 + 0.5f), + 255); + } + var converted = new Memory2D(rgba, hdrImg.height, hdrImg.width); + var reference = ImageLoader.LoadTestImage("../../../../BCnEncTests/testImages/test_hdr_kiara.png"); + TestHelper.AssertImagesEqual(reference, converted, CompressionQuality.BestQuality); + } + } + + [Fact] + public async Task DecodeAllMipMapsHdrStreamAsync() + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Format = CompressionFormat.Bc6U; + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + + var decoder = new BcDecoder(); + var input = HdrLoader.TestHdrKiara; + var ktxWithMips = encoder.EncodeToKtxHdr(new Memory2D(input.pixels, input.height, input.width)); + using (var ms = new MemoryStream()) + { + ktxWithMips.Write(ms); + ms.Position = 0; + + var images = await decoder.DecodeAllMipMapsHdr2DAsync(ms); + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + Assert.True(images.Length > 1); + } + } + } +} diff --git a/BCnEncTests.Framework/MipMapperTests.cs b/BCnEncTests.Framework/MipMapperTests.cs new file mode 100644 index 0000000..8b9e292 --- /dev/null +++ b/BCnEncTests.Framework/MipMapperTests.cs @@ -0,0 +1,83 @@ +using System; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class MipMapperTests + { + [Fact] + public void MipChainHasCorrectDimensions() + { + var image = ImageLoader.TestGradient1; // 512x416 + var numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + Assert.Equal(chain.Length, numMips); + Assert.True(numMips > 1); + + for (var i = 0; i < numMips; i++) + { + Assert.Equal(Math.Max(1, image.Width >> i), chain[i].Width); + Assert.Equal(Math.Max(1, image.Height >> i), chain[i].Height); + } + + // Last level must be 1x1 + Assert.Equal(1, chain[numMips - 1].Width); + Assert.Equal(1, chain[numMips - 1].Height); + } + + /// + /// A solid-color image must downsample to exactly the same color at + /// every mip level, regardless of how many times the box filter is applied. + /// + [Fact] + public void SolidColorMipChainIsExact() + { + var color = new ColorRgba32(200, 100, 50, 255); + var pixels = new ColorRgba32[16 * 16]; + for (var i = 0; i < pixels.Length; i++) pixels[i] = color; + + var image = new Memory2D(pixels, 16, 16); + var numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + // 16x16 -> 8x8 -> 4x4 -> 2x2 -> 1x1 = 5 levels + Assert.Equal(5, numMips); + + for (var level = 0; level < numMips; level++) + { + var span = chain[level].Span; + for (var y = 0; y < chain[level].Height; y++) + for (var x = 0; x < chain[level].Width; x++) + Assert.Equal(color, span[y, x]); + } + } + + /// + /// Verifies the exact 2x2 box-filter arithmetic with a known input. + /// A 2x2 image of two values repeated both rows should average exactly. + /// + [Fact] + public void KnownPatternDownsamplesCorrectly() + { + // 2-wide, 2-tall: left column = 100 red, right column = 200 red. + // Expected 1x1 average: r = (100+200+100+200)/4 = 150, g=b=0, a=255. + var pixels = new[] + { + new ColorRgba32(100, 0, 0, 255), new ColorRgba32(200, 0, 0, 255), + new ColorRgba32(100, 0, 0, 255), new ColorRgba32(200, 0, 0, 255), + }; + var image = new Memory2D(pixels, 2, 2); + var numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + Assert.Equal(2, numMips); // 2x2 -> 1x1 + Assert.Equal(1, chain[1].Width); + Assert.Equal(1, chain[1].Height); + Assert.Equal(new ColorRgba32(150, 0, 0, 255), chain[1].Span[0, 0]); + } + } +} diff --git a/BCnEncTests.Framework/Support/HdrLoader.cs b/BCnEncTests.Framework/Support/HdrLoader.cs new file mode 100644 index 0000000..51f7d5b --- /dev/null +++ b/BCnEncTests.Framework/Support/HdrLoader.cs @@ -0,0 +1,15 @@ +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; + +namespace BCnEncTests.Support +{ + public static class HdrLoader + { + public static HdrImage TestHdrKiara { get; } = HdrImage.Read("../../../../BCnEncTests/testImages/test_hdr_kiara.hdr"); + public static HdrImage TestHdrProbe { get; } = HdrImage.Read("../../../../BCnEncTests/testImages/test_hdr_probe.hdr"); + public static DdsFile TestHdrKiaraDds { get; } = + DdsLoader.LoadDdsFile("../../../../BCnEncTests/testImages/test_hdr_kiara_bc6h.dds"); + public static KtxFile TestHdrKiaraKtx { get; } = + KtxLoader.LoadKtxFile("../../../../BCnEncTests/testImages/test_hdr_kiara_bc6h_ktx.ktx"); + } +} diff --git a/BCnEncTests.Framework/Support/ImageLoader.cs b/BCnEncTests.Framework/Support/ImageLoader.cs new file mode 100644 index 0000000..0a90454 --- /dev/null +++ b/BCnEncTests.Framework/Support/ImageLoader.cs @@ -0,0 +1,104 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; + +namespace BCnEncTests.Support +{ + public static class ImageLoader + { + public static Memory2D TestDiffuse1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_diffuse_1_512.jpg"); + public static Memory2D TestBlur1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_blur_1_512.jpg"); + public static Memory2D TestNormal1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_normal_1_512.jpg"); + public static Memory2D TestHeight1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_height_1_512.jpg"); + public static Memory2D TestGradient1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_gradient_1_512.jpg"); + + public static Memory2D TestTransparentSprite1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_transparent.png"); + public static Memory2D TestAlphaGradient1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_alphagradient_1_512.png"); + public static Memory2D TestAlpha1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_alpha_1_512.png"); + public static Memory2D TestRedGreen1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_red_green_1_64.png"); + public static Memory2D TestRgbHard1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_rgb_hard_1.png"); + public static Memory2D TestLenna { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_lenna_512.png"); + public static Memory2D TestDecodingBc5Reference { get; } = LoadTestImage("../../../../BCnEncTests/testImages/decoding_dds_bc5_reference.png"); + + public static Memory2D[] TestCubemap { get; } = { + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/right.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/left.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/top.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/bottom.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/back.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/forward.png") + }; + + internal static Memory2D LoadTestImage(string filename) + { + using (var bmp = new Bitmap(filename)) + { + return FromBitmap(bmp); + } + } + + internal static unsafe Memory2D FromBitmap(Bitmap bmp) + { + var pixels = new ColorRgba32[bmp.Width * bmp.Height]; + var data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), + ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + byte* ptr = (byte*)data.Scan0; + for (int i = 0; i < pixels.Length; i++) + { + // GDI+ Format32bppArgb memory order: B,G,R,A + pixels[i] = new ColorRgba32(ptr[2], ptr[1], ptr[0], ptr[3]); + ptr += 4; + } + bmp.UnlockBits(data); + return pixels.AsMemory().AsMemory2D(bmp.Height, bmp.Width); + } + } + + public static class DdsLoader + { + public const string TestDecompressBc1Name = "../../../../BCnEncTests/testImages/test_decompress_bc1.dds"; + public const string TestDecompressBc1AName = "../../../../BCnEncTests/testImages/test_decompress_bc1a.dds"; + public const string TestDecompressBc7Name = "../../../../BCnEncTests/testImages/test_decompress_bc7.dds"; + public const string TestDecompressBc5Name = "../../../../BCnEncTests/testImages/decoding_dds_bc5.dds"; + public const string TestDecompressRgbaName = "../../../../BCnEncTests/testImages/test_decompress_rgba.dds"; + + public static DdsFile TestDecompressBc1 { get; } = LoadDdsFile(TestDecompressBc1Name); + public static DdsFile TestDecompressBc1A { get; } = LoadDdsFile(TestDecompressBc1AName); + public static DdsFile TestDecompressBc5 { get; } = LoadDdsFile(TestDecompressBc5Name); + public static DdsFile TestDecompressBc7 { get; } = LoadDdsFile(TestDecompressBc7Name); + public static DdsFile TestDecompressRgba { get; } = LoadDdsFile(TestDecompressRgbaName); + + internal static DdsFile LoadDdsFile(string filename) + { + using (var fs = File.OpenRead(filename)) + { + return DdsFile.Load(fs); + } + } + } + + public static class KtxLoader + { + public static KtxFile TestDecompressBc1 { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc1.ktx"); + public static KtxFile TestDecompressBc1A { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc1a.ktx"); + public static KtxFile TestDecompressBc2 { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc2.ktx"); + public static KtxFile TestDecompressBc3 { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc3.ktx"); + public static KtxFile TestDecompressBc4Unorm { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc4_unorm.ktx"); + public static KtxFile TestDecompressBc5Unorm { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc5_unorm.ktx"); + public static KtxFile TestDecompressBc7Rgb { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc7_rgb.ktx"); + public static KtxFile TestDecompressBc7Types { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc7_types.ktx"); + public static KtxFile TestDecompressBc7Unorm { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc7_unorm.ktx"); + + internal static KtxFile LoadKtxFile(string filename) + { + using (var fs = File.OpenRead(filename)) + { + return KtxFile.Load(fs); + } + } + } +} diff --git a/BCnEncTests.Framework/Support/TestHelper.cs b/BCnEncTests.Framework/Support/TestHelper.cs new file mode 100644 index 0000000..ba9cc8c --- /dev/null +++ b/BCnEncTests.Framework/Support/TestHelper.cs @@ -0,0 +1,284 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; +using Xunit; +using Xunit.Abstractions; + +namespace BCnEncTests.Support +{ + public static class TestHelper + { + #region Assertions + + public static void AssertPixelsEqual(Span originalPixels, Span pixels, CompressionQuality quality, ITestOutputHelper output = null) + { + var psnr = ImageQuality.PeakSignalToNoiseRatio(originalPixels, pixels); + AssertPSNR(psnr, quality, output); + } + + public static void AssertPixelsEqual(Span originalPixels, Span pixels, CompressionQuality quality, ITestOutputHelper output = null) + { + var rmse = ImageQuality.CalculateLogRMSE(originalPixels, pixels); + AssertRMSE(rmse, quality, output); + } + + public static void AssertImagesEqual(Memory2D original, Memory2D image, CompressionQuality quality, bool countAlpha = true) + { + var psnr = CalculatePSNR(original, image, countAlpha); + AssertPSNR(psnr, quality); + } + + #endregion + + #region Execute methods + + public static void ExecuteDecodingTest(KtxFile file, string outputFile) + { + Assert.True(file.header.VerifyHeader()); + Assert.Equal((uint)1, file.header.NumberOfFaces); + + var decoder = new BcDecoder(); + var pixels = decoder.Decode2D(file); + + Assert.Equal((uint)pixels.Width, file.header.PixelWidth); + Assert.Equal((uint)pixels.Height, file.header.PixelHeight); + + using (var outFs = File.OpenWrite(outputFile)) + { + SaveAsPng(pixels, outFs); + } + } + + #region Dds + + public static void ExecuteDdsWritingTest(Memory2D image, CompressionFormat format, string outputFile) + { + ExecuteDdsWritingTest(new[] { image }, format, outputFile); + } + + public static void ExecuteDdsWritingTest(Memory2D[] images, CompressionFormat format, string outputFile) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + encoder.OutputOptions.FileFormat = OutputFileFormat.Dds; + + using (var fs = File.OpenWrite(outputFile)) + { + if (images.Length == 1) + { + encoder.EncodeToStream(images[0], fs); + } + else + { + encoder.EncodeCubeMapToStream(images[0], images[1], images[2], images[3], images[4], images[5], fs); + } + } + } + + public static void ExecuteDdsReadingTest(DdsFile file, DxgiFormat format, string outputFile, bool assertAlpha = false) + { + Assert.Equal(format, file.header.ddsPixelFormat.DxgiFormat); + Assert.Equal(file.header.dwMipMapCount, (uint)file.Faces[0].MipMaps.Length); + + var decoder = new BcDecoder(); + decoder.InputOptions.DdsBc1ExpectAlpha = assertAlpha; + var images = decoder.DecodeAllMipMaps2D(file); + + Assert.Equal((uint)images[0].Width, file.header.dwWidth); + Assert.Equal((uint)images[0].Height, file.header.dwHeight); + + for (var i = 0; i < images.Length; i++) + { + if (assertAlpha) + { + var pixels = GetSinglePixelArrayAsColors(images[0]); + Assert.Contains(pixels, x => x.a == 0); + } + + using (var outFs = File.OpenWrite(string.Format(outputFile, i))) + { + SaveAsPng(images[i], outFs); + } + } + } + + #endregion + + #region Cancellation + + public static async Task ExecuteCancellationTest(Memory2D image, bool isParallel) + { + var encoder = new BcEncoder(CompressionFormat.Bc7); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.Options.IsParallel = isParallel; + + var source = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); + await Assert.ThrowsAnyAsync(() => + encoder.EncodeToRawBytesAsync(image, source.Token)); + } + + #endregion + + #endregion + + public static float DecodeKtxCheckPSNR(string filename, Memory2D original) + { + using (var fs = File.OpenRead(filename)) + { + var ktx = KtxFile.Load(fs); + var decoder = new BcDecoder() + { + OutputOptions = { Bc4Component = ColorComponent.Luminance } + }; + var decoded = decoder.Decode2D(ktx); + + return CalculatePSNR(original, decoded); + } + } + + public static float DecodeKtxCheckRMSEHdr(string filename, HdrImage original) + { + using (var fs = File.OpenRead(filename)) + { + var ktx = KtxFile.Load(fs); + var decoder = new BcDecoder(); + + var decoded = decoder.DecodeHdr(ktx); + + return ImageQuality.CalculateLogRMSE(original.pixels, decoded); + } + } + + public static void ExecuteEncodingTest(Memory2D image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = quality; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + + var fs = File.OpenWrite(filename); + encoder.EncodeToStream(image, fs); + fs.Close(); + + var psnr = DecodeKtxCheckPSNR(filename, image); + output.WriteLine("RGBA PSNR: " + psnr + "db"); + AssertPSNR(psnr, encoder.OutputOptions.Quality); + } + + public static void ExecuteHdrEncodingTest(HdrImage image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = quality; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + + var fs = File.OpenWrite(filename); + encoder.EncodeToStreamHdr(image.pixels.AsMemory().AsMemory2D(image.height, image.width), fs); + fs.Close(); + + var rmse = DecodeKtxCheckRMSEHdr(filename, image); + output.WriteLine("RGBFloat RMSE: " + rmse); + AssertRMSE(rmse, encoder.OutputOptions.Quality); + } + + private static float CalculatePSNR(Memory2D original, Memory2D decoded, bool countAlpha = true) + { + var pixels = GetSinglePixelArrayAsColors(original); + var pixels2 = GetSinglePixelArrayAsColors(decoded); + + return ImageQuality.PeakSignalToNoiseRatio(pixels, pixels2, countAlpha); + } + + public static void AssertPSNR(float psnr, CompressionQuality quality, ITestOutputHelper output = null) + { + output?.WriteLine($"PSNR: {psnr} , quality: {quality}"); + if (quality == CompressionQuality.Fast) + { + Assert.True(psnr > 25, $"PSNR was less than 25: {psnr} , quality: {quality}"); + } + else + { + Assert.True(psnr > 30, $"PSNR was less than 30: {psnr} , quality: {quality}"); + } + } + + public static void AssertRMSE(float rmse, CompressionQuality quality, ITestOutputHelper output = null) + { + output?.WriteLine($"RMSE: {rmse} , quality: {quality}"); + if (quality == CompressionQuality.Fast) + { + Assert.True(rmse < 0.1); + } + else + { + Assert.True(rmse < 0.04); + } + } + + public static ColorRgba32[] GetSinglePixelArrayAsColors(ReadOnlyMemory2D image) + { + var pixels = new ColorRgba32[image.Width * image.Height]; + var span = image.Span; + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + pixels[y * image.Width + x] = span[y, x]; + } + } + return pixels; + } + + public static unsafe void SaveAsPng(Memory2D image, Stream stream) + { + int width = image.Width; + int height = image.Height; + using (var bmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) + { + var data = bmp.LockBits(new Rectangle(0, 0, width, height), + ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + byte* ptr = (byte*)data.Scan0; + var span = image.Span; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + var c = span[y, x]; + ptr[0] = c.b; + ptr[1] = c.g; + ptr[2] = c.r; + ptr[3] = c.a; + ptr += 4; + } + } + bmp.UnlockBits(data); + bmp.Save(stream, ImageFormat.Png); + } + } + + public static void SaveAsPng(ColorRgbFloat[] pixels, int width, int height, Stream stream) + { + var rgba = new ColorRgba32[pixels.Length]; + for (var i = 0; i < pixels.Length; i++) + { + var p = pixels[i]; + byte r = (byte)(Math.Max(0, Math.Min(1, p.r)) * 255 + 0.5f); + byte g = (byte)(Math.Max(0, Math.Min(1, p.g)) * 255 + 0.5f); + byte b = (byte)(Math.Max(0, Math.Min(1, p.b)) * 255 + 0.5f); + rgba[i] = new ColorRgba32(r, g, b, 255); + } + var mem = new Memory2D(rgba, height, width); + SaveAsPng(mem, stream); + } + } +} diff --git a/BCnEncTests/AtcTests.cs b/BCnEncTests.Shared/AtcTests.cs similarity index 79% rename from BCnEncTests/AtcTests.cs rename to BCnEncTests.Shared/AtcTests.cs index d02da3e..eff342c 100644 --- a/BCnEncTests/AtcTests.cs +++ b/BCnEncTests.Shared/AtcTests.cs @@ -2,8 +2,8 @@ using BCnEncoder.Encoder; using BCnEncoder.Shared; using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; using Xunit; -using BCnEncoder.ImageSharp; namespace BCnEncTests { @@ -12,96 +12,78 @@ public class AtcTests [Fact] public void AtcKtxDecode() { - // Arrange var decoder = new BcDecoder(); var encoder = new BcEncoder(CompressionFormat.Atc); var original = ImageLoader.TestLenna; - // Act var ktx = encoder.EncodeToKtx(original); - var image = decoder.DecodeToImageRgba32(ktx); + var image = decoder.Decode2D(ktx); - // Assert TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } [Fact] public void AtcDdsDecode() { - // Arrange var decoder = new BcDecoder(); var encoder = new BcEncoder(CompressionFormat.Atc); var original = ImageLoader.TestLenna; - // Act var dds = encoder.EncodeToDds(original); - var image = decoder.DecodeToImageRgba32(dds); + var image = decoder.Decode2D(dds); - // Assert TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } [Fact] public void AtcExplicitKtxDecode() { - // Arrange var decoder = new BcDecoder(); var encoder = new BcEncoder(CompressionFormat.AtcExplicitAlpha); var original = ImageLoader.TestAlphaGradient1; - // Act var ktx = encoder.EncodeToKtx(original); - var image = decoder.DecodeToImageRgba32(ktx); + var image = decoder.Decode2D(ktx); - // Assert TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } [Fact] public void AtcExplicitDdsDecode() { - // Arrange var decoder = new BcDecoder(); var encoder = new BcEncoder(CompressionFormat.AtcExplicitAlpha); var original = ImageLoader.TestAlphaGradient1; - // Act var dds = encoder.EncodeToDds(original); - var image = decoder.DecodeToImageRgba32(dds); + var image = decoder.Decode2D(dds); - // Assert TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } [Fact] public void AtcInterpolatedKtxDecode() { - // Arrange var decoder = new BcDecoder(); var encoder = new BcEncoder(CompressionFormat.AtcInterpolatedAlpha); var original = ImageLoader.TestAlphaGradient1; - // Act var ktx = encoder.EncodeToKtx(original); - var image = decoder.DecodeToImageRgba32(ktx); + var image = decoder.Decode2D(ktx); - // Assert TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } [Fact] public void AtcInterpolatedDdsDecode() { - // Arrange var decoder = new BcDecoder(); var encoder = new BcEncoder(CompressionFormat.AtcInterpolatedAlpha); var original = ImageLoader.TestAlphaGradient1; - // Act var dds = encoder.EncodeToDds(original); - var image = decoder.DecodeToImageRgba32(dds); + var image = decoder.Decode2D(dds); - // Assert TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } } diff --git a/BCnEncTests/BC1Tests.cs b/BCnEncTests.Shared/BC1Tests.cs similarity index 91% rename from BCnEncTests/BC1Tests.cs rename to BCnEncTests.Shared/BC1Tests.cs index bfc7320..314c284 100644 --- a/BCnEncTests/BC1Tests.cs +++ b/BCnEncTests.Shared/BC1Tests.cs @@ -1,15 +1,13 @@ using BCnEncoder.Shared; -using SixLabors.ImageSharp.PixelFormats; using Xunit; -using BCnEncoder.ImageSharp; namespace BCnEncTests { public class Bc1Tests { - [Fact] - public void Decode() { + public void Decode() + { var block = new Bc1Block { color0 = new ColorRgb565(255, 255, 255), @@ -60,7 +58,8 @@ public void Decode() { } [Fact] - public void DecodeBlack() { + public void DecodeBlack() + { var block = new Bc1Block { color0 = new ColorRgb565(200, 200, 200), @@ -111,7 +110,8 @@ public void DecodeBlack() { } [Fact] - public void DecodeAlpha() { + public void DecodeAlpha() + { var block = new Bc1Block { color0 = new ColorRgb565(200, 200, 200), @@ -145,10 +145,10 @@ public void DecodeAlpha() { Assert.Equal(new ColorRgba32(206, 203, 206), raw.p20); Assert.Equal(new ColorRgba32(206, 203, 206), raw.p30); - Assert.Equal(new ColorRgba32(0,0,0,0), raw.p01); - Assert.Equal(new ColorRgba32(0,0,0,0), raw.p11); - Assert.Equal(new ColorRgba32(0,0,0,0), raw.p21); - Assert.Equal(new ColorRgba32(0,0,0,0), raw.p31); + Assert.Equal(new ColorRgba32(0, 0, 0, 0), raw.p01); + Assert.Equal(new ColorRgba32(0, 0, 0, 0), raw.p11); + Assert.Equal(new ColorRgba32(0, 0, 0, 0), raw.p21); + Assert.Equal(new ColorRgba32(0, 0, 0, 0), raw.p31); Assert.Equal(new ColorRgba32(230, 229, 230), raw.p02); Assert.Equal(new ColorRgba32(230, 229, 230), raw.p12); diff --git a/BCnEncTests.Shared/BCnEncTests.Shared.projitems b/BCnEncTests.Shared/BCnEncTests.Shared.projitems new file mode 100644 index 0000000..a221845 --- /dev/null +++ b/BCnEncTests.Shared/BCnEncTests.Shared.projitems @@ -0,0 +1,34 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + D954291E-2A0B-460D-934E-DC6B0785DB48 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BCnEncTests.Shared/BCnEncTests.Shared.shproj b/BCnEncTests.Shared/BCnEncTests.Shared.shproj new file mode 100644 index 0000000..be4f3fb --- /dev/null +++ b/BCnEncTests.Shared/BCnEncTests.Shared.shproj @@ -0,0 +1,10 @@ + + + + {D954291E-2A0B-460D-934E-DC6B0785DB48} + 14.0 + + + + + diff --git a/BCnEncTests/Bc5Tests.cs b/BCnEncTests.Shared/Bc5Tests.cs similarity index 94% rename from BCnEncTests/Bc5Tests.cs rename to BCnEncTests.Shared/Bc5Tests.cs index 7eb37ff..4e361ef 100644 --- a/BCnEncTests/Bc5Tests.cs +++ b/BCnEncTests.Shared/Bc5Tests.cs @@ -1,5 +1,4 @@ using BCnEncoder.Decoder; -using BCnEncoder.ImageSharp; using BCnEncoder.Shared; using BCnEncTests.Support; using Xunit; @@ -33,8 +32,8 @@ public void Bc5Indices() public void Bc5DdsDecode() { var reference = ImageLoader.TestDecodingBc5Reference; - var decoded = new BcDecoder().DecodeToImageRgba32(DdsLoader.TestDecompressBc5); - + var decoded = new BcDecoder().Decode2D(DdsLoader.TestDecompressBc5); + var refSpan = TestHelper.GetSinglePixelArrayAsColors(reference); var decSpan = TestHelper.GetSinglePixelArrayAsColors(decoded); diff --git a/BCnEncTests/Bc6EncoderTests.cs b/BCnEncTests.Shared/Bc6EncoderTests.cs similarity index 96% rename from BCnEncTests/Bc6EncoderTests.cs rename to BCnEncTests.Shared/Bc6EncoderTests.cs index 7352f86..ced39c6 100644 --- a/BCnEncTests/Bc6EncoderTests.cs +++ b/BCnEncTests.Shared/Bc6EncoderTests.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Text; using BCnEncoder.Encoder; using BCnEncoder.Encoder.Bptc; using BCnEncoder.Shared; @@ -40,7 +37,7 @@ public void Quantize(int initialQuantizedValue, int endpointBits, bool signed) } var unquantized = Bc6Block.UnQuantize(initialQuantizedValue, endpointBits, signed); var finishedUnquantized = Bc6Block.FinishUnQuantize(unquantized, signed); - + var prequantized = Bc6EncodingHelpers.PreQuantize(finishedUnquantized, signed); var quantized = Bc6EncodingHelpers.Quantize(prequantized, endpointBits, signed); @@ -95,13 +92,11 @@ public void RgbBoundingBox() Assert.InRange(max.b, 0.60, 0.7); } - [Fact] public void PackMode3() { const int endpointPrecision = 10; var random = new Random(); - var rand = random.Next(); var ep0 = (random.Next() & (1 << endpointPrecision) - 1, random.Next() & (1 << endpointPrecision) - 1, @@ -113,7 +108,7 @@ public void PackMode3() var indices = new byte[16]; for (var i = 1; i < indices.Length; i++) { - indices[i] = (byte) random.Next(1 << 4); + indices[i] = (byte)random.Next(1 << 4); } var block = Bc6Block.PackType3(ep0, ep1, indices); @@ -133,7 +128,6 @@ public void PackMode3() } } - [Theory] [InlineData(Bc6BlockType.Type0)] [InlineData(Bc6BlockType.Type1)] @@ -173,7 +167,7 @@ internal void EncodeAllModesUnsigned(Bc6BlockType type) Bc6Block encoded; var badTransform = false; - if(type.HasSubsets()) + if (type.HasSubsets()) { var indexBlock = Bc6Encoder.CreateClusterIndexBlock(testBlock, out var numClusters, 2); var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock, numClusters, true); @@ -271,7 +265,7 @@ public void Encode() { var signed = true; var image = HdrLoader.TestHdrKiara; - var blocks = ImageToBlocks.ImageTo4X4(image.pixels.AsMemory().AsMemory2D(image.height, image.width), out var bW, out var bH); + var blocks = ImageToBlocks.ImageTo4X4(new Memory2D(image.pixels, image.height, image.width), out var bW, out var bH); for (var i = 0; i < blocks.Length; i++) { @@ -281,7 +275,6 @@ public void Encode() var error = decoded.CalculateError(blocks[i]); if (error > 0.06 || i == 14749) { - Debugger.Break(); encoded = Bc6Encoder.Bc6EncoderBalanced.EncodeBlock(blocks[i], signed); } } @@ -337,7 +330,7 @@ public void EncodeProbeSignedFast() } [Fact] - public void EncodeProbSignedBalanced() + public void EncodeProbeSignedBalanced() { TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6S, CompressionQuality.Balanced, "encoding_bc6_probe_signed_balanced.ktx", output); @@ -360,8 +353,10 @@ public void EncodeToKtx() var ktx = encoder.EncodeToKtxHdr(HdrLoader.TestHdrKiara.PixelMemory); - using var fs = File.OpenWrite("encoding_bc6_ktx.ktx"); - ktx.Write(fs); + using (var fs = File.OpenWrite("encoding_bc6_ktx.ktx")) + { + ktx.Write(fs); + } } [Fact] @@ -374,8 +369,10 @@ public void EncodeToDds() var dds = encoder.EncodeToDdsHdr(HdrLoader.TestHdrKiara.PixelMemory); - using var fs = File.OpenWrite("encoding_bc6_dds.dds"); - dds.Write(fs); + using (var fs = File.OpenWrite("encoding_bc6_dds.dds")) + { + dds.Write(fs); + } } [Fact] diff --git a/BCnEncTests/Bc6HDecoderTests.cs b/BCnEncTests.Shared/Bc6HDecoderTests.cs similarity index 59% rename from BCnEncTests/Bc6HDecoderTests.cs rename to BCnEncTests.Shared/Bc6HDecoderTests.cs index 60a99fa..2cbb609 100644 --- a/BCnEncTests/Bc6HDecoderTests.cs +++ b/BCnEncTests.Shared/Bc6HDecoderTests.cs @@ -6,8 +6,6 @@ using BCnEncoder.Shared; using BCnEncTests.Support; using CommunityToolkit.HighPerformance; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; using Xunit; using Xunit.Abstractions; using Half = BCnEncoder.Shared.Half; @@ -26,28 +24,22 @@ public void DecodeDds() var decoder = new BcDecoder(); var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraDds); - var img = new Image((int)HdrLoader.TestHdrKiaraDds.header.dwWidth, - (int)HdrLoader.TestHdrKiaraDds.header.dwHeight); - - var pixels = TestHelper.GetSinglePixelArray(img); - - for (var i = 0; i < decoded.Length; i++) - { - pixels[i] = new RgbaVector(decoded[i].r, decoded[i].g, decoded[i].b); - } - - TestHelper.SetSinglePixelArray(img, pixels); - - var hdr = new HdrImage((int) HdrLoader.TestHdrKiaraDds.header.dwWidth, - (int) HdrLoader.TestHdrKiaraDds.header.dwHeight); + var width = (int)HdrLoader.TestHdrKiaraDds.header.dwWidth; + var height = (int)HdrLoader.TestHdrKiaraDds.header.dwHeight; + var hdr = new HdrImage(width, height); Assert.Equal(hdr.pixels.Length, decoded.Length); hdr.pixels = decoded; - using var sfs = File.OpenWrite("decoding_test_dds_bc6h.hdr"); - hdr.Write(sfs); + using (var sfs = File.OpenWrite("decoding_test_dds_bc6h.hdr")) + { + hdr.Write(sfs); + } - img.SaveAsPng("decoding_test_dds_bc6h.png"); + using (var pngFs = File.OpenWrite("decoding_test_dds_bc6h.png")) + { + TestHelper.SaveAsPng(decoded, width, height, pngFs); + } TestHelper.AssertPixelsEqual(HdrLoader.TestHdrKiara.pixels, decoded, CompressionQuality.Fast, output); } @@ -58,28 +50,22 @@ public void DecodeKtx() var decoder = new BcDecoder(); var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraKtx); - var img = new Image((int)HdrLoader.TestHdrKiaraKtx.header.PixelWidth, - (int)HdrLoader.TestHdrKiaraKtx.header.PixelHeight); - - var pixels = TestHelper.GetSinglePixelArray(img); - - for (var i = 0; i < decoded.Length; i++) - { - pixels[i] = new RgbaVector(decoded[i].r, decoded[i].g, decoded[i].b); - } - - TestHelper.SetSinglePixelArray(img, pixels); - - var hdr = new HdrImage((int)HdrLoader.TestHdrKiaraKtx.header.PixelWidth, - (int)HdrLoader.TestHdrKiaraKtx.header.PixelHeight); + var width = (int)HdrLoader.TestHdrKiaraKtx.header.PixelWidth; + var height = (int)HdrLoader.TestHdrKiaraKtx.header.PixelHeight; + var hdr = new HdrImage(width, height); Assert.Equal(hdr.pixels.Length, decoded.Length); hdr.pixels = decoded; - using var sfs = File.OpenWrite("decoding_test_ktx_bc6h.hdr"); - hdr.Write(sfs); + using (var sfs = File.OpenWrite("decoding_test_ktx_bc6h.hdr")) + { + hdr.Write(sfs); + } - img.SaveAsPng("decoding_test_ktx_bc6h.png"); + using (var pngFs = File.OpenWrite("decoding_test_ktx_bc6h.png")) + { + TestHelper.SaveAsPng(decoded, width, height, pngFs); + } TestHelper.AssertPixelsEqual(HdrLoader.TestHdrKiara.pixels, decoded, CompressionQuality.BestQuality, output); } @@ -93,24 +79,32 @@ public void AllBlocksDecodesExact() var decoder = new BcDecoder(); var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraDds); - using var fs = File.OpenRead("../../../testImages/test_hdr_kiara_dds_float16_data.bin"); - using var ms = new MemoryStream(); - fs.CopyTo(ms); - var length = (int)ms.Position; - - var bytes = ms.GetBuffer().AsSpan(0, length); - var halfs = MemoryMarshal.Cast(bytes); - Assert.Equal(halfs.Length / 4, decoded.Length); - - for (var i = 0; i < decoded.Length; i++) + Stream fs; +#if NETCOREAPP + fs = File.OpenRead("../../../testImages/test_hdr_kiara_dds_float16_data.bin"); +#else + fs = File.OpenRead("../../../../BCnEncTests/testImages/test_hdr_kiara_dds_float16_data.bin"); +#endif + using (fs) + using (var ms = new MemoryStream()) { - float r = halfs[i * 4 + 0]; - float g = halfs[i * 4 + 1]; - float b = halfs[i * 4 + 2]; - - Assert.Equal(r, decoded[i].r); - Assert.Equal(g, decoded[i].g); - Assert.Equal(b, decoded[i].b); + fs.CopyTo(ms); + var length = (int)ms.Position; + + var bytes = ms.GetBuffer().AsSpan(0, length); + var halfs = MemoryMarshal.Cast(bytes); + Assert.Equal(halfs.Length / 4, decoded.Length); + + for (var i = 0; i < decoded.Length; i++) + { + float r = halfs[i * 4 + 0]; + float g = halfs[i * 4 + 1]; + float b = halfs[i * 4 + 2]; + + Assert.Equal(r, decoded[i].r); + Assert.Equal(g, decoded[i].g); + Assert.Equal(b, decoded[i].b); + } } } @@ -129,7 +123,7 @@ public void DecodeModes() { var block = blocks[i]; var mode = block.Type; - modes[(int) mode]++; + modes[(int)mode]++; } for (var i = 0; i < modes.Length; i++) @@ -163,16 +157,17 @@ public void DecodeErrorBlock() var bufferSize = decoder.GetBlockSize(CompressionFormat.Bc6U) * width * height; var buffer = new byte[bufferSize]; - Random r = new Random(44); + var r = new System.Random(44); r.NextBytes(buffer); var decoded = decoder.DecodeRawHdr(buffer, width * 4, height * 4, CompressionFormat.Bc6U); Assert.Contains(new ColorRgbFloat(1, 0, 1), decoded); HdrImage image = new HdrImage(new Span2D(decoded, height * 4, width * 4)); - using var fs = File.OpenWrite("test_decode_bc6h_error.hdr"); - image.Write(fs); + using (var fs = File.OpenWrite("test_decode_bc6h_error.hdr")) + { + image.Write(fs); + } } - } } diff --git a/BCnEncTests/Bc7BlockTests.cs b/BCnEncTests.Shared/Bc7BlockTests.cs similarity index 90% rename from BCnEncTests/Bc7BlockTests.cs rename to BCnEncTests.Shared/Bc7BlockTests.cs index c05fdc6..8ac34ea 100644 --- a/BCnEncTests/Bc7BlockTests.cs +++ b/BCnEncTests.Shared/Bc7BlockTests.cs @@ -3,12 +3,13 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using BCnEncoder.Decoder; -using BCnEncoder.ImageSharp; using BCnEncoder.Shared; using BCnEncoder.Shared.ImageFiles; -using SixLabors.ImageSharp; +using BCnEncTests.Support; using Xunit; +using CommunityToolkit.HighPerformance; + namespace BCnEncTests { public class Bc7BlockTests @@ -79,15 +80,17 @@ public void DecodeErrorBlock() var bufferSize = decoder.GetBlockSize(CompressionFormat.Bc7) * width * height; var buffer = new byte[bufferSize]; - Random r = new Random(50); + var r = new Random(50); r.NextBytes(buffer); var pixels = decoder.DecodeRaw(buffer, width * 4, height * 4, CompressionFormat.Bc7); - var decoded = decoder.DecodeRawToImageRgba32(buffer, width * 4, height * 4, CompressionFormat.Bc7); Assert.Contains(new ColorRgba32(255, 0, 255), pixels); - using var fs = File.OpenWrite("test_decode_bc7_error.png"); - decoded.SaveAsPng(fs); + var decoded = new Memory2D(pixels, height * 4, width * 4); + using (var fs = File.OpenWrite("test_decode_bc7_error.png")) + { + TestHelper.SaveAsPng(decoded, fs); + } } #region Type Packs @@ -105,9 +108,7 @@ private void Type0Pack(Span output) new byte[]{0, 0, 0b1111}, new byte[]{0, 0, 0b1000} }; - var pBits = new byte[] { - 1, 0, 1, 0, 1, 1 - }; + var pBits = new byte[] { 1, 0, 1, 0, 1, 1 }; var indices = new byte[] { 0, 1, 2, 3, 0, 1, 2, 3, @@ -121,16 +122,13 @@ private void Type0Pack(Span output) Assert.Equal(i, output[i].PartitionSetId); } - pBits = new byte[] { - 0, 0, 0, 0, 0, 0 - }; + pBits = new byte[] { 0, 0, 0, 0, 0, 0 }; indices = new byte[] { 0, 1, 0, 3, 1, 1, 1, 3, 2, 2, 2, 0, 3, 2, 3, 0 }; - for (var i = 0; i < 16; i++) { output[i + 16].PackType0(i, subsetEndpoints, pBits, indices); @@ -138,16 +136,13 @@ private void Type0Pack(Span output) Assert.Equal(i, output[i].PartitionSetId); } - pBits = new byte[] { - 1, 1, 1, 1, 1, 1 - }; + pBits = new byte[] { 1, 1, 1, 1, 1, 1 }; indices = new byte[] { 1, 0, 3, 3, 2, 1, 2, 3, 3, 2, 1, 0, 0, 3, 0, 0 }; - for (var i = 0; i < 16; i++) { output[i + 32].PackType0(i, subsetEndpoints, pBits, indices); @@ -155,16 +150,13 @@ private void Type0Pack(Span output) Assert.Equal(i, output[i].PartitionSetId); } - pBits = new byte[] { - 0, 1, 0, 1, 0, 0 - }; + pBits = new byte[] { 0, 1, 0, 1, 0, 0 }; indices = new byte[] { 3, 2, 1, 0, 0, 1, 2, 3, 3, 2, 1, 0, 0, 1, 2, 3 }; - for (var i = 0; i < 16; i++) { output[i + 48].PackType0(i, subsetEndpoints, pBits, indices); @@ -183,9 +175,7 @@ private void Type1Pack(Span output) new byte[]{0, 0, 0b111111}, new byte[]{0, 0, 0b1010} }; - var pBits = new byte[] { - 1, 1 - }; + var pBits = new byte[] { 1, 1 }; var indices = new byte[] { 0, 1, 2, 3, 0, 1, 2, 3, @@ -237,9 +227,7 @@ private void Type3Pack(Span output) new byte[]{0, 0, 0b1111111}, new byte[]{0, 0, 0b11010} }; - var pBits = new byte[] { - 1, 0, 0, 1 - }; + var pBits = new byte[] { 1, 0, 0, 1 }; var indices = new byte[] { 0, 1, 2, 3, 0, 1, 2, 3, @@ -261,17 +249,13 @@ private void Type4Pack(Span output) new byte[]{0b11111, 0, 0b0100}, new byte[]{0, 0b11111, 0b0100} }; - var alphaEndPoints = new byte[]{ - 0b111111, - 0 - }; + var alphaEndPoints = new byte[] { 0b111111, 0 }; var indices2Bit = new byte[] { 0, 1, 2, 3, 0, 1, 2, 3, 3, 2, 1, 0, 3, 2, 1, 0 }; - var indices3Bit = new byte[] { 0, 1, 2, 3, 7, 6, 5, 4, @@ -295,13 +279,10 @@ private void Type5Pack(Span output) { var colorEndpoints = new[] { //subset 1 - new byte[]{0b1111111, 0b0100, 0}, + new byte[]{0b1111111, 0b0100, 0}, new byte[]{0, 0, 0b1111111} }; - var alphaEndPoints = new byte[]{ - 255, - 100 - }; + var alphaEndPoints = new byte[] { 255, 100 }; var colorIndices = new byte[] { 0, 1, 2, 3, 0, 1, 2, 3, @@ -328,12 +309,10 @@ private void Type6Pack(Span output) { var colorEndpoints = new[] { //subset 1 - new byte[]{0b1111111, 0b0100, 0, 0b1111111}, + new byte[]{0b1111111, 0b0100, 0, 0b1111111}, new byte[]{0, 0, 0b1111111, 0b1111} }; - var pBits = new byte[] { - 1, 0 - }; + var pBits = new byte[] { 1, 0 }; var colorIndices = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, @@ -351,15 +330,13 @@ private void Type7Pack(Span output) { var colorEndpoints = new[] { //subset 1 - new byte[]{0b11111, 0b0100, 0, 0b11111}, + new byte[]{0b11111, 0b0100, 0, 0b11111}, new byte[]{0, 0b11111, 0, 0b111}, //subset 2 - new byte[]{0b11111, 0b0100, 0, 0b1111}, + new byte[]{0b11111, 0b0100, 0, 0b1111}, new byte[]{0b10100, 0, 0b11111, 0b11111} }; - var pBits = new byte[] { - 1, 0, 1, 0 - }; + var pBits = new byte[] { 1, 0, 1, 0 }; var indices = new byte[] { 0, 1, 2, 3, 0, 1, 2, 3, diff --git a/BCnEncTests/BgraTests.cs b/BCnEncTests.Shared/BgraTests.cs similarity index 79% rename from BCnEncTests/BgraTests.cs rename to BCnEncTests.Shared/BgraTests.cs index 7ae666e..6f0e8bf 100644 --- a/BCnEncTests/BgraTests.cs +++ b/BCnEncTests.Shared/BgraTests.cs @@ -1,6 +1,5 @@ using BCnEncoder.Decoder; using BCnEncoder.Encoder; -using BCnEncoder.ImageSharp; using BCnEncoder.Shared; using BCnEncTests.Support; using Xunit; @@ -12,66 +11,52 @@ public class BgraTests [Fact] public void BgraDdsDecode() { - // Arrange var decoder = new BcDecoder(); var encoder = new BcEncoder(CompressionFormat.Bgra); var original = ImageLoader.TestLenna; - // Act var dds = encoder.EncodeToDds(original); - var image = decoder.DecodeToImageRgba32(dds); + var image = decoder.Decode2D(dds); - - - // Assert TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } [Fact] public void BgraAlphaDdsDecode() { - // Arrange var decoder = new BcDecoder(); var encoder = new BcEncoder(CompressionFormat.Bgra); var original = ImageLoader.TestAlphaGradient1; - // Act var dds = encoder.EncodeToDds(original); - var image = decoder.DecodeToImageRgba32(dds); + var image = decoder.Decode2D(dds); - // Assert TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } [Fact] public void BgraKtxDecode() { - // Arrange var decoder = new BcDecoder(); var encoder = new BcEncoder(CompressionFormat.Bgra); var original = ImageLoader.TestLenna; - // Act var ktx = encoder.EncodeToKtx(original); - var image = decoder.DecodeToImageRgba32(ktx); + var image = decoder.Decode2D(ktx); - // Assert TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } [Fact] public void BgraAlphaKtxDecode() { - // Arrange var decoder = new BcDecoder(); var encoder = new BcEncoder(CompressionFormat.Bgra); var original = ImageLoader.TestAlphaGradient1; - // Act var ktx = encoder.EncodeToKtx(original); - var image = decoder.DecodeToImageRgba32(ktx); + var image = decoder.Decode2D(ktx); - // Assert TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } } diff --git a/BCnEncTests/BlockTests.cs b/BCnEncTests.Shared/BlockTests.cs similarity index 79% rename from BCnEncTests/BlockTests.cs rename to BCnEncTests.Shared/BlockTests.cs index 98ffc59..14d6621 100644 --- a/BCnEncTests/BlockTests.cs +++ b/BCnEncTests.Shared/BlockTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using BCnEncoder.Shared; using CommunityToolkit.HighPerformance; using Xunit; @@ -36,10 +36,10 @@ public void PaddingColor() { var testImage = new ColorRgba32[15, 11]; - var pixels = testImage.AsSpan(); - - for (var i = 0; i < pixels.Length; i++) { - pixels[i] = new ColorRgba32(0, 125, 125); + for (var y = 0; y < testImage.GetLength(0); y++) { + for (var x = 0; x < testImage.GetLength(1); x++) { + testImage[y, x] = new ColorRgba32(0, 125, 125); + } } var blocks = ImageToBlocks.ImageTo4X4(testImage, out var blocksWidth, out var blocksHeight); @@ -63,14 +63,14 @@ public void BlocksToImage() var r = new Random(0); var testImage = new ColorRgba32[16, 16]; - var pixels = testImage.AsSpan(); - - for (var i = 0; i < pixels.Length; i++) { - pixels[i] = new ColorRgba32( - (byte)r.Next(255), - (byte)r.Next(255), - (byte)r.Next(255), - (byte)r.Next(255)); + for (var y = 0; y < testImage.GetLength(0); y++) { + for (var x = 0; x < testImage.GetLength(1); x++) { + testImage[y, x] = new ColorRgba32( + (byte)r.Next(255), + (byte)r.Next(255), + (byte)r.Next(255), + (byte)r.Next(255)); + } } var blocks = ImageToBlocks.ImageTo4X4(testImage, out var blocksWidth, out var blocksHeight); @@ -83,9 +83,11 @@ public void BlocksToImage() var pixels2 = output.AsSpan(); - Assert.Equal(pixels.Length, pixels2.Length); - for (var i = 0; i < pixels.Length; i++) { - Assert.Equal(pixels[i], pixels2[i]); + Assert.Equal(testImage.Length, pixels2.Length); + for (var y = 0; y < 16; y++) { + for (var x = 0; x < 16; x++) { + Assert.Equal(testImage[y, x], pixels2[y * 16 + x]); + } } } diff --git a/BCnEncTests.Shared/CancellationTests.cs b/BCnEncTests.Shared/CancellationTests.cs new file mode 100644 index 0000000..9932c6c --- /dev/null +++ b/BCnEncTests.Shared/CancellationTests.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using BCnEncTests.Support; +using Xunit; + +namespace BCnEncTests +{ + public class CancellationTests + { + [Fact] + public async Task EncodeParallelCancellation() + { + await TestHelper.ExecuteCancellationTest(ImageLoader.TestAlphaGradient1, true); + } + + [Fact] + public async Task EncodeNonParallelCancellation() + { + await TestHelper.ExecuteCancellationTest(ImageLoader.TestAlphaGradient1, false); + } + } +} diff --git a/BCnEncTests.Shared/ClusterTests.cs b/BCnEncTests.Shared/ClusterTests.cs new file mode 100644 index 0000000..a30ab52 --- /dev/null +++ b/BCnEncTests.Shared/ClusterTests.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class ClusterTests + { + [Fact] + public void Clusterize() + { + var original = ImageLoader.TestBlur1; + int height = original.Height; + int width = original.Width; + + // Copy pixels from the source image + var pixels = new ColorRgba32[height * width]; + var srcSpan = original.Span; + for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) + pixels[y * width + x] = srcSpan[y, x]; + + var numClusters = (width / 32) * (height / 32); + var clusters = LinearClustering.ClusterPixels(pixels, width, height, numClusters, 10, 10); + + var pixC = new ColorYCbCr[numClusters]; + var counts = new int[numClusters]; + + for (var i = 0; i < pixels.Length; i++) + { + pixC[clusters[i]] += new ColorYCbCr(pixels[i]); + counts[clusters[i]]++; + } + + for (var i = 0; i < numClusters; i++) + { + pixC[i] /= counts[i]; + } + + for (var i = 0; i < pixels.Length; i++) + { + pixels[i] = pixC[clusters[i]].ToColorRgba32(); + } + + var result = new Memory2D(pixels, height, width); + using (var fs = File.OpenWrite("test_cluster.png")) + { + TestHelper.SaveAsPng(result, fs); + } + } + } +} diff --git a/BCnEncTests/ColorTest.cs b/BCnEncTests.Shared/ColorTest.cs similarity index 100% rename from BCnEncTests/ColorTest.cs rename to BCnEncTests.Shared/ColorTest.cs diff --git a/BCnEncTests/DdsReadTests.cs b/BCnEncTests.Shared/DdsReadTests.cs similarity index 70% rename from BCnEncTests/DdsReadTests.cs rename to BCnEncTests.Shared/DdsReadTests.cs index 9a5ba2e..3c2a35e 100644 --- a/BCnEncTests/DdsReadTests.cs +++ b/BCnEncTests.Shared/DdsReadTests.cs @@ -1,10 +1,8 @@ using System.IO; using BCnEncoder.Decoder; -using BCnEncoder.ImageSharp; using BCnEncoder.Shared; using BCnEncoder.Shared.ImageFiles; using BCnEncTests.Support; -using SixLabors.ImageSharp; using Xunit; namespace BCnEncTests @@ -38,16 +36,18 @@ public void ReadBc7() [Fact] public void ReadFromStream() { - using var fs = File.OpenRead(DdsLoader.TestDecompressBc1Name); - - var decoder = new BcDecoder(); - var images = decoder.DecodeAllMipMapsToImageRgba32(fs); - - for (var i = 0; i < images.Length; i++) + using (var fs = File.OpenRead(DdsLoader.TestDecompressBc1Name)) { - using var outFs = File.OpenWrite($"decoding_test_dds_stream_bc1_mip{i}.png"); - images[i].SaveAsPng(outFs); - images[i].Dispose(); + var decoder = new BcDecoder(); + var images = decoder.DecodeAllMipMaps2D(fs); + + for (var i = 0; i < images.Length; i++) + { + using (var outFs = File.OpenWrite($"decoding_test_dds_stream_bc1_mip{i}.png")) + { + TestHelper.SaveAsPng(images[i], outFs); + } + } } } } diff --git a/BCnEncTests/DdsWritingTests.cs b/BCnEncTests.Shared/DdsWritingTests.cs similarity index 100% rename from BCnEncTests/DdsWritingTests.cs rename to BCnEncTests.Shared/DdsWritingTests.cs diff --git a/BCnEncTests/DecodingAsyncTests.cs b/BCnEncTests.Shared/DecodingAsyncTests.cs similarity index 63% rename from BCnEncTests/DecodingAsyncTests.cs rename to BCnEncTests.Shared/DecodingAsyncTests.cs index 04cfa29..cba775b 100644 --- a/BCnEncTests/DecodingAsyncTests.cs +++ b/BCnEncTests.Shared/DecodingAsyncTests.cs @@ -1,11 +1,11 @@ -using System.ComponentModel.DataAnnotations; +using System; using System.IO; using System.Threading.Tasks; using BCnEncoder.Decoder; using BCnEncoder.Encoder; -using BCnEncoder.ImageSharp; using BCnEncoder.Shared; using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; using Xunit; namespace BCnEncTests @@ -20,10 +20,9 @@ public async Task DecodeAsync() var original = ImageLoader.TestGradient1; var file = encoder.EncodeToKtx(original); - var image = await decoder.DecodeToImageRgba32Async(file); + var image = await decoder.Decode2DAsync(file); - TestHelper.AssertImagesEqual(original, image,encoder.OutputOptions.Quality); - image.Dispose(); + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); } [Fact] @@ -34,11 +33,9 @@ public async Task DecodeAllMipMapsAsync() var original = ImageLoader.TestGradient1; var file = encoder.EncodeToKtx(original); - var images = await decoder.DecodeAllMipMapsToImageRgba32Async(file); + var images = await decoder.DecodeAllMipMaps2DAsync(file); TestHelper.AssertImagesEqual(original, images[0], encoder.OutputOptions.Quality); - foreach(var img in images) - img.Dispose(); } [Fact] @@ -50,13 +47,17 @@ public async Task DecodeRawAsync() var file = encoder.EncodeToKtx(original); - var ms = new MemoryStream(file.MipMaps[0].Faces[0].Data); + var rawBytes = file.MipMaps[0].Faces[0].Data; + var mipWidth = (int)file.MipMaps[0].Width; + var mipHeight = (int)file.MipMaps[0].Height; - var image = await decoder.DecodeRawToImageRgba32Async(ms, - (int) file.MipMaps[0].Width, (int) file.MipMaps[0].Height, CompressionFormat.Bc1); + var decoded = await Task.Run(() => + { + var pixels = decoder.DecodeRaw(rawBytes, mipWidth, mipHeight, CompressionFormat.Bc1); + return new Memory2D(pixels, mipHeight, mipWidth); + }); - TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); - image.Dispose(); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); } } } diff --git a/BCnEncTests/DecodingTests.cs b/BCnEncTests.Shared/DecodingTests.cs similarity index 100% rename from BCnEncTests/DecodingTests.cs rename to BCnEncTests.Shared/DecodingTests.cs diff --git a/BCnEncTests/EncoderOptionsTests.cs b/BCnEncTests.Shared/EncoderOptionsTests.cs similarity index 92% rename from BCnEncTests/EncoderOptionsTests.cs rename to BCnEncTests.Shared/EncoderOptionsTests.cs index 677fce4..f73ecd3 100644 --- a/BCnEncTests/EncoderOptionsTests.cs +++ b/BCnEncTests.Shared/EncoderOptionsTests.cs @@ -1,16 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Text; using BCnEncoder.Encoder; using BCnEncTests.Support; using Xunit; -using BCnEncoder.ImageSharp; namespace BCnEncTests { public class EncoderOptionsTests { - [Theory] [InlineData(1)] [InlineData(2)] @@ -28,7 +23,7 @@ public void MaxMipMaps(int requestedMipMaps) } }; - Assert.Equal(requestedMipMaps, encoder.CalculateNumberOfMipLevels(testImage)); + Assert.Equal(requestedMipMaps, encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height)); var ktx = encoder.EncodeToKtx(testImage); @@ -55,7 +50,7 @@ public void GenerateMipMaps() } }; - Assert.Equal(requestedMipMaps, encoder.CalculateNumberOfMipLevels(testImage)); + Assert.Equal(requestedMipMaps, encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height)); var ktx = encoder.EncodeToKtx(testImage); diff --git a/BCnEncTests/EncodingAsyncTest.cs b/BCnEncTests.Shared/EncodingAsyncTest.cs similarity index 57% rename from BCnEncTests/EncodingAsyncTest.cs rename to BCnEncTests.Shared/EncodingAsyncTest.cs index 6d3e9ff..88cfb3f 100644 --- a/BCnEncTests/EncodingAsyncTest.cs +++ b/BCnEncTests.Shared/EncodingAsyncTest.cs @@ -1,11 +1,10 @@ +using System; using System.Threading.Tasks; using BCnEncoder.Decoder; using BCnEncoder.Encoder; -using BCnEncoder.ImageSharp; using BCnEncoder.Shared; using BCnEncTests.Support; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; +using CommunityToolkit.HighPerformance; using Xunit; namespace BCnEncTests @@ -14,8 +13,8 @@ public class EncodingAsyncTest { private readonly BcEncoder encoder; private readonly BcDecoder decoder; - private readonly Image originalImage; - private readonly Image[] originalCubeMap; + private readonly Memory2D originalImage; + private readonly Memory2D[] originalCubeMap; public EncodingAsyncTest() { @@ -29,20 +28,18 @@ public EncodingAsyncTest() public async Task EncodeToDdsAsync() { var file = await encoder.EncodeToDdsAsync(originalImage); - var image = decoder.DecodeToImageRgba32(file); + var image = decoder.Decode2D(file); TestHelper.AssertImagesEqual(originalImage, image, encoder.OutputOptions.Quality); - image.Dispose(); } [Fact] public async Task EncodeToKtxAsync() { var file = await encoder.EncodeToKtxAsync(originalImage); - var image = decoder.DecodeToImageRgba32(file); + var image = decoder.Decode2D(file); TestHelper.AssertImagesEqual(originalImage, image, encoder.OutputOptions.Quality); - image.Dispose(); } [Fact] @@ -53,10 +50,11 @@ public async Task EncodeCubemapToDdsAsync() for (var i = 0; i < 6; i++) { - var image = decoder.DecodeRawToImageRgba32(file.Faces[i].MipMaps[0].Data, (int)file.Faces[i].Width, (int)file.Faces[i].Height, CompressionFormat.Bc1); + var rawPixels = decoder.DecodeRaw(file.Faces[i].MipMaps[0].Data, + (int)file.Faces[i].Width, (int)file.Faces[i].Height, CompressionFormat.Bc1); + var decodedImage = new Memory2D(rawPixels, (int)file.Faces[i].Height, (int)file.Faces[i].Width); - TestHelper.AssertImagesEqual(originalCubeMap[i], image, encoder.OutputOptions.Quality); - image.Dispose(); + TestHelper.AssertImagesEqual(originalCubeMap[i], decodedImage, encoder.OutputOptions.Quality); } } @@ -68,10 +66,12 @@ public async Task EncodeCubemapToKtxAsync() for (var i = 0; i < 6; i++) { - var image = decoder.DecodeRawToImageRgba32(file.MipMaps[0].Faces[i].Data, (int)file.MipMaps[0].Faces[i].Width, (int)file.MipMaps[0].Faces[i].Height, CompressionFormat.Bc1); + var rawPixels = decoder.DecodeRaw(file.MipMaps[0].Faces[i].Data, + (int)file.MipMaps[0].Faces[i].Width, (int)file.MipMaps[0].Faces[i].Height, CompressionFormat.Bc1); + var decodedImage = new Memory2D(rawPixels, + (int)file.MipMaps[0].Faces[i].Height, (int)file.MipMaps[0].Faces[i].Width); - TestHelper.AssertImagesEqual(originalCubeMap[i], image, encoder.OutputOptions.Quality); - image.Dispose(); + TestHelper.AssertImagesEqual(originalCubeMap[i], decodedImage, encoder.OutputOptions.Quality); } } @@ -79,10 +79,10 @@ public async Task EncodeCubemapToKtxAsync() public async Task EncodeToRawBytesAsync() { var data = await encoder.EncodeToRawBytesAsync(originalImage); - var image = decoder.DecodeRawToImageRgba32(data[0], originalImage.Width, originalImage.Height, CompressionFormat.Bc1); + var rawPixels = decoder.DecodeRaw(data[0], originalImage.Width, originalImage.Height, CompressionFormat.Bc1); + var image = new Memory2D(rawPixels, originalImage.Height, originalImage.Width); TestHelper.AssertImagesEqual(originalImage, image, encoder.OutputOptions.Quality); - image.Dispose(); } } } diff --git a/BCnEncTests.Shared/EncodingTest.cs b/BCnEncTests.Shared/EncodingTest.cs new file mode 100644 index 0000000..e4f3edd --- /dev/null +++ b/BCnEncTests.Shared/EncodingTest.cs @@ -0,0 +1,317 @@ +using System.IO; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; +using Xunit.Abstractions; + +namespace BCnEncTests +{ + public class Bc1GradientTest + { + private readonly ITestOutputHelper output; + + public Bc1GradientTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc1GradientBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestGradient1, CompressionFormat.Bc1, CompressionQuality.BestQuality, "encoding_bc1_gradient_bestQuality.ktx", output); + } + + [Fact] + public void Bc1GradientBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestGradient1, CompressionFormat.Bc1, CompressionQuality.Balanced, "encoding_bc1_gradient_balanced.ktx", output); + } + + [Fact] + public void Bc1GradientFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestGradient1, CompressionFormat.Bc1, CompressionQuality.Fast, "encoding_bc1_gradient_fast.ktx", output); + } + } + + public class Bc1DiffuseTest + { + private readonly ITestOutputHelper output; + + public Bc1DiffuseTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc1DiffuseBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestDiffuse1, CompressionFormat.Bc1, CompressionQuality.BestQuality, "encoding_bc1_diffuse_bestQuality.ktx", output); + } + + [Fact] + public void Bc1DiffuseBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestDiffuse1, CompressionFormat.Bc1, CompressionQuality.Balanced, "encoding_bc1_diffuse_balanced.ktx", output); + } + + [Fact] + public void Bc1DiffuseFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestDiffuse1, CompressionFormat.Bc1, CompressionQuality.Fast, "encoding_bc1_diffuse_fast.ktx", output); + } + } + + public class Bc1BlurryTest + { + private readonly ITestOutputHelper output; + + public Bc1BlurryTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc1BlurBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestBlur1, CompressionFormat.Bc1, CompressionQuality.BestQuality, "encoding_bc1_blur_bestQuality.ktx", output); + } + + [Fact] + public void Bc1BlurBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestBlur1, CompressionFormat.Bc1, CompressionQuality.Balanced, "encoding_bc1_blur_balanced.ktx", output); + } + + [Fact] + public void Bc1BlurFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestBlur1, CompressionFormat.Bc1, CompressionQuality.Fast, "encoding_bc1_blur_fast.ktx", output); + } + } + + public class Bc1ASpriteTest + { + private readonly ITestOutputHelper output; + + public Bc1ASpriteTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc1ASpriteBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestTransparentSprite1, CompressionFormat.Bc1WithAlpha, CompressionQuality.BestQuality, "encoding_bc1a_sprite_bestQuality.ktx", output); + } + + [Fact] + public void Bc1ASpriteBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestTransparentSprite1, CompressionFormat.Bc1WithAlpha, CompressionQuality.Balanced, "encoding_bc1a_sprite_balanced.ktx", output); + } + + [Fact] + public void Bc1ASpriteFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestTransparentSprite1, CompressionFormat.Bc1WithAlpha, CompressionQuality.Fast, "encoding_bc1a_sprite_fast.ktx", output); + } + } + + public class Bc2GradientTest + { + private readonly ITestOutputHelper output; + + public Bc2GradientTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc2GradientBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc2, CompressionQuality.BestQuality, "encoding_bc2_gradient_bestQuality.ktx", output); + } + + [Fact] + public void Bc2GradientBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc2, CompressionQuality.Balanced, "encoding_bc2_gradient_balanced.ktx", output); + } + + [Fact] + public void Bc2GradientFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc2, CompressionQuality.Fast, "encoding_bc2_gradient_fast.ktx", output); + } + } + + public class Bc3GradientTest + { + private readonly ITestOutputHelper output; + + public Bc3GradientTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc3GradientBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc3, CompressionQuality.BestQuality, "encoding_bc3_gradient_bestQuality.ktx", output); + } + + [Fact] + public void Bc3GradientBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc3, CompressionQuality.Balanced, "encoding_bc3_gradient_balanced.ktx", output); + } + + [Fact] + public void Bc3GradientFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc3, CompressionQuality.Fast, "encoding_bc3_gradient_fast.ktx", output); + } + } + + public class Bc4RedTest + { + private readonly ITestOutputHelper output; + + public Bc4RedTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc4RedBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestHeight1, CompressionFormat.Bc4, CompressionQuality.BestQuality, "encoding_bc4_red_bestQuality.ktx", output); + } + + [Fact] + public void Bc4RedBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestHeight1, CompressionFormat.Bc4, CompressionQuality.Balanced, "encoding_bc4_red_balanced.ktx", output); + } + + [Fact] + public void Bc4RedFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestHeight1, CompressionFormat.Bc4, CompressionQuality.Fast, "encoding_bc4_red_fast.ktx", output); + } + } + + public class Bc5RedGreenTest + { + private readonly ITestOutputHelper output; + + public Bc5RedGreenTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc5RedGreenBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRedGreen1, CompressionFormat.Bc5, CompressionQuality.BestQuality, "encoding_bc5_red_green_bestQuality.ktx", output); + } + + [Fact] + public void Bc5RedGreenBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRedGreen1, CompressionFormat.Bc5, CompressionQuality.Balanced, "encoding_bc5_red_green_balanced.ktx", output); + } + + [Fact] + public void Bc5RedGreenFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRedGreen1, CompressionFormat.Bc5, CompressionQuality.Fast, "encoding_bc5_red_green_fast.ktx", output); + } + } + + public class Bc7RgbTest + { + private readonly ITestOutputHelper output; + + public Bc7RgbTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc7RgbBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRgbHard1, CompressionFormat.Bc7, CompressionQuality.BestQuality, "encoding_bc7_rgb_bestQuality.ktx", output); + } + + [Fact] + public void Bc7RgbBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRgbHard1, CompressionFormat.Bc7, CompressionQuality.Balanced, "encoding_bc7_rgb_balanced.ktx", output); + } + + [Fact] + public void Bc7LennaBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestLenna, CompressionFormat.Bc7, CompressionQuality.Balanced, "encoding_bc7_lenna_balanced.ktx", output); + } + + [Fact] + public void Bc7RgbFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRgbHard1, CompressionFormat.Bc7, CompressionQuality.Fast, "encoding_bc7_rgb_fast.ktx", output); + } + } + + public class Bc7RgbaTest + { + private readonly ITestOutputHelper output; + + public Bc7RgbaTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc7RgbaBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlpha1, CompressionFormat.Bc7, CompressionQuality.BestQuality, "encoding_bc7_rgba_bestQuality.ktx", output); + } + + [Fact] + public void Bc7RgbaBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlpha1, CompressionFormat.Bc7, CompressionQuality.Balanced, "encoding_bc7_rgba_balanced.ktx", output); + } + + [Fact] + public void Bc7RgbaFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlpha1, CompressionFormat.Bc7, CompressionQuality.Fast, "encoding_bc7_rgba_fast.ktx", output); + } + } + + public class CubemapTest + { + [Fact] + public void WriteCubeMapFile() + { + var images = ImageLoader.TestCubemap; + + var filename = "encoding_bc1_cubemap.ktx"; + + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = CompressionFormat.Bc1; + + using (var fs = File.OpenWrite(filename)) + { + encoder.EncodeCubeMapToStream(images[0], images[1], images[2], images[3], images[4], images[5], fs); + } + } + } +} diff --git a/BCnEncTests/IntHelperTests.cs b/BCnEncTests.Shared/IntHelperTests.cs similarity index 100% rename from BCnEncTests/IntHelperTests.cs rename to BCnEncTests.Shared/IntHelperTests.cs diff --git a/BCnEncTests/MathHelperTests.cs b/BCnEncTests.Shared/MathHelperTests.cs similarity index 100% rename from BCnEncTests/MathHelperTests.cs rename to BCnEncTests.Shared/MathHelperTests.cs diff --git a/BCnEncTests/PcaTests.cs b/BCnEncTests.Shared/PcaTests.cs similarity index 100% rename from BCnEncTests/PcaTests.cs rename to BCnEncTests.Shared/PcaTests.cs diff --git a/BCnEncTests/ProgressTests.cs b/BCnEncTests.Shared/ProgressTests.cs similarity index 92% rename from BCnEncTests/ProgressTests.cs rename to BCnEncTests.Shared/ProgressTests.cs index 1a58c31..83dab93 100644 --- a/BCnEncTests/ProgressTests.cs +++ b/BCnEncTests.Shared/ProgressTests.cs @@ -3,12 +3,10 @@ using System.Threading.Tasks; using BCnEncoder.Decoder; using BCnEncoder.Encoder; -using BCnEncoder.ImageSharp; using BCnEncoder.Shared; using BCnEncoder.Shared.ImageFiles; using BCnEncTests.Support; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; +using CommunityToolkit.HighPerformance; using Xunit; using Xunit.Abstractions; @@ -20,7 +18,7 @@ public class ProgressTests public ProgressTests(ITestOutputHelper output) => this.output = output; - private async Task ExecuteEncodeProgressReport(BcEncoder encoder, Image testImage, int expectedTotalBlocks) + private async Task ExecuteEncodeProgressReport(BcEncoder encoder, Memory2D testImage, int expectedTotalBlocks) { var lastProgress = new ProgressElement(0, 1); @@ -33,8 +31,10 @@ private async Task ExecuteEncodeProgressReport(BcEncoder encoder, Image lastProgress = element; }); - await using var ms = new MemoryStream(); - await encoder.EncodeToStreamAsync(testImage, ms); + using (var ms = new MemoryStream()) + { + await encoder.EncodeToStreamAsync(testImage, ms); + } output.WriteLine("LastProgress = " + lastProgress); @@ -242,9 +242,9 @@ public async Task EncodeProgressReportParallelKtx() var expectedTotal = 0; - for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage); i++) + for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height); i++) { - encoder.CalculateMipMapSize(testImage, i, out var mW, out var mH); + encoder.CalculateMipMapSize(testImage.Width, testImage.Height, i, out var mW, out var mH); expectedTotal += encoder.GetBlockCount(mW, mH); } @@ -263,9 +263,9 @@ public async Task EncodeProgressReportNonParallelKtx() var expectedTotal = 0; - for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage); i++) + for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height); i++) { - encoder.CalculateMipMapSize(testImage, i, out var mW, out var mH); + encoder.CalculateMipMapSize(testImage.Width, testImage.Height, i, out var mW, out var mH); expectedTotal += encoder.GetBlockCount(mW, mH); } @@ -314,9 +314,9 @@ public async Task EncodeProgressReportParallelDds() var expectedTotal = 0; - for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage); i++) + for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height); i++) { - encoder.CalculateMipMapSize(testImage, i, out var mW, out var mH); + encoder.CalculateMipMapSize(testImage.Width, testImage.Height, i, out var mW, out var mH); expectedTotal += encoder.GetBlockCount(mW, mH); } @@ -335,9 +335,9 @@ public async Task EncodeProgressReportNonParallelDds() var expectedTotal = 0; - for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage); i++) + for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height); i++) { - encoder.CalculateMipMapSize(testImage, i, out var mW, out var mH); + encoder.CalculateMipMapSize(testImage.Width, testImage.Height, i, out var mW, out var mH); expectedTotal += encoder.GetBlockCount(mW, mH); } @@ -375,7 +375,7 @@ public async Task EncodeProgressReportNonParallelOneMipDds() } } - class SynchronousProgress : IProgress + internal class SynchronousProgress : IProgress { private readonly Action handler; diff --git a/BCnEncTests.Shared/RawTests.cs b/BCnEncTests.Shared/RawTests.cs new file mode 100644 index 0000000..ebd71e2 --- /dev/null +++ b/BCnEncTests.Shared/RawTests.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class RawTests + { + [Fact] + public void EncodeDecode() + { + var inputImage = ImageLoader.TestGradient1; + var decoder = new BcDecoder(); + var encoder = new BcEncoder + { + OutputOptions = { Quality = CompressionQuality.BestQuality } + }; + + var encodedRawBytes = encoder.EncodeToRawBytes(inputImage); + var decodedPixels = decoder.DecodeRaw(encodedRawBytes[0], inputImage.Width, inputImage.Height, CompressionFormat.Bc1); + var decodedImage = new Memory2D(decodedPixels, inputImage.Height, inputImage.Width); + + var originalColors = TestHelper.GetSinglePixelArrayAsColors(inputImage); + var decodedColors = TestHelper.GetSinglePixelArrayAsColors(decodedImage); + + TestHelper.AssertPixelsEqual(originalColors, decodedColors, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeDecodeStream() + { + var inputImage = ImageLoader.TestGradient1; + var decoder = new BcDecoder(); + var encoder = new BcEncoder + { + OutputOptions = { Quality = CompressionQuality.BestQuality } + }; + + var encodedRawBytes = encoder.EncodeToRawBytes(inputImage); + + using (var ms = new MemoryStream(encodedRawBytes[0])) + { + Assert.Equal(0, ms.Position); + + var rawPixels = decoder.DecodeRaw(ms, inputImage.Width, inputImage.Height, CompressionFormat.Bc1); + var decodedImage = new Memory2D(rawPixels, inputImage.Height, inputImage.Width); + + var originalColors = TestHelper.GetSinglePixelArrayAsColors(inputImage); + var decodedColors = TestHelper.GetSinglePixelArrayAsColors(decodedImage); + + TestHelper.AssertPixelsEqual(originalColors, decodedColors, encoder.OutputOptions.Quality); + } + } + + [Fact] + public void EncodeDecodeAllMipMapsStream() + { + var inputImage = ImageLoader.TestGradient1; + var decoder = new BcDecoder(); + var encoder = new BcEncoder + { + OutputOptions = + { + Quality = CompressionQuality.BestQuality, + GenerateMipMaps = true, + MaxMipMapLevel = 0 + } + }; + + using (var ms = new MemoryStream()) + { + var encodedRawBytes = encoder.EncodeToRawBytes(inputImage); + + var mipLevels = encoder.CalculateNumberOfMipLevels(inputImage.Width, inputImage.Height); + Assert.True(mipLevels > 1); + + for (var i = 0; i < mipLevels; i++) + { + ms.Write(encodedRawBytes[i], 0, encodedRawBytes[i].Length); + } + + ms.Position = 0; + Assert.Equal(0, ms.Position); + var resized = ResizeImageToMips(inputImage); + + + for (var i = 0; i < mipLevels; i++) + { + encoder.CalculateMipMapSize(inputImage.Width, inputImage.Height, i, out var mipWidth, out var mipHeight); + + var blockSize = decoder.GetBlockSize(CompressionFormat.Bc1); + var blockCount = decoder.GetBlockCount(mipWidth, mipHeight); + var buffer = new byte[blockSize * blockCount]; + ms.Read(buffer, 0, buffer.Length); + + var rawPixels = decoder.DecodeRaw(buffer, mipWidth, mipHeight, CompressionFormat.Bc1); + var decodedImage = new Memory2D(rawPixels, mipHeight, mipWidth); + + var originalColors = TestHelper.GetSinglePixelArrayAsColors(resized[i]); + var decodedColors = TestHelper.GetSinglePixelArrayAsColors(decodedImage); + + TestHelper.AssertPixelsEqual(originalColors, decodedColors, encoder.OutputOptions.Quality); + } + + encoder.CalculateMipMapSize(inputImage.Width, inputImage.Height, mipLevels - 1, out var lastMWidth, out var lastMHeight); + Assert.Equal(1, lastMWidth); + Assert.Equal(1, lastMHeight); + } + } + + private static ReadOnlyMemory2D[] ResizeImageToMips(Memory2D image) + { + int numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + return chain; + } + } +} diff --git a/BCnEncTests/SingleBlockTests.cs b/BCnEncTests.Shared/SingleBlockTests.cs similarity index 59% rename from BCnEncTests/SingleBlockTests.cs rename to BCnEncTests.Shared/SingleBlockTests.cs index 89b0778..74691ed 100644 --- a/BCnEncTests/SingleBlockTests.cs +++ b/BCnEncTests.Shared/SingleBlockTests.cs @@ -1,22 +1,16 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; -using System.Text; using BCnEncoder.Decoder; using BCnEncoder.Encoder; using BCnEncoder.Shared; using BCnEncTests.Support; using CommunityToolkit.HighPerformance; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; using Xunit; namespace BCnEncTests { public class SingleBlockTests { - [Theory] [InlineData(CompressionFormat.Bc1, CompressionQuality.Fast)] [InlineData(CompressionFormat.Bc1, CompressionQuality.Balanced)] @@ -26,6 +20,8 @@ public class SingleBlockTests public void SingleBlockEncodeDecodeStream(CompressionFormat format, CompressionQuality quality) { var testImage = ImageLoader.TestAlpha1; + int height = testImage.Height; + int width = testImage.Width; var encoder = new BcEncoder() { @@ -36,39 +32,37 @@ public void SingleBlockEncodeDecodeStream(CompressionFormat format, CompressionQ } }; - var pixels = TestHelper.GetSinglePixelArray(testImage); - var colors = MemoryMarshal.Cast(pixels) - .AsSpan2D(testImage.Height, testImage.Width); + var colors = testImage.Span; var ms = new MemoryStream(); - for (var y = 0; y < testImage.Height; y+=4) + for (var y = 0; y < height; y += 4) { - for (var x = 0; x < testImage.Width; x+=4) + for (var x = 0; x < width; x += 4) { encoder.EncodeBlock(colors.Slice(y, x, 4, 4), ms); } } - Assert.Equal(ms.Position, encoder.GetBlockSize() * encoder.GetBlockCount(testImage.Width, testImage.Height)); + Assert.Equal(ms.Position, encoder.GetBlockSize() * encoder.GetBlockCount(width, height)); ms.Position = 0; var decoder = new BcDecoder(); - var decoded = new ColorRgba32[testImage.Height, testImage.Width]; + var decoded = new ColorRgba32[height, width]; - for (var y = 0; y < testImage.Height; y += 4) + for (var y = 0; y < height; y += 4) { - for (var x = 0; x < testImage.Width; x += 4) + for (var x = 0; x < width; x += 4) { decoder.DecodeBlock(ms, format, decoded.AsSpan2D().Slice(y, x, 4, 4)); } } - colors.TryGetSpan(out var oPixels); - decoded.AsSpan2D().TryGetSpan(out var dPixels); + var oPixels = TestHelper.GetSinglePixelArrayAsColors(testImage); + var dPixels = FlattenDecoded(decoded, height, width); var psnr = ImageQuality.PeakSignalToNoiseRatio(oPixels, dPixels, format != CompressionFormat.Bc1); @@ -84,6 +78,8 @@ public void SingleBlockEncodeDecodeStream(CompressionFormat format, CompressionQ public void SingleBlockEncodeDecode(CompressionFormat format, CompressionQuality quality) { var testImage = ImageLoader.TestAlpha1; + int height = testImage.Height; + int width = testImage.Width; var encoder = new BcEncoder() { @@ -94,50 +90,54 @@ public void SingleBlockEncodeDecode(CompressionFormat format, CompressionQuality } }; - var pixels = TestHelper.GetSinglePixelArray(testImage); - var colors = MemoryMarshal.Cast(pixels) - .AsSpan2D(testImage.Height, testImage.Width); + var colors = testImage.Span; - Span buffer = new byte[encoder.GetBlockSize() * encoder.GetBlockCount(testImage.Width, testImage.Height)]; + var encMs = new MemoryStream(); - var blockIndex = 0; - for (var y = 0; y < testImage.Height; y += 4) + for (var y = 0; y < height; y += 4) { - for (var x = 0; x < testImage.Width; x += 4) + for (var x = 0; x < width; x += 4) { - var bytes = encoder.EncodeBlock(colors.Slice(y, x, 4, 4)); - bytes.CopyTo(buffer.Slice(blockIndex * encoder.GetBlockSize())); - blockIndex++; + encoder.EncodeBlock(colors.Slice(y, x, 4, 4), encMs); } } + var buffer = encMs.ToArray(); + var decoder = new BcDecoder(); - var decoded = new ColorRgba32[testImage.Height, testImage.Width]; + var decoded = new ColorRgba32[height, width]; - blockIndex = 0; - for (var y = 0; y < testImage.Height; y += 4) + var blockIndex = 0; + for (var y = 0; y < height; y += 4) { - for (var x = 0; x < testImage.Width; x += 4) + for (var x = 0; x < width; x += 4) { - decoder.DecodeBlock( - buffer.Slice( + new Span(buffer, blockIndex * decoder.GetBlockSize(format), - decoder.GetBlockSize(format) - ), + decoder.GetBlockSize(format)), format, decoded.AsSpan2D().Slice(y, x, 4, 4)); blockIndex++; } } - colors.TryGetSpan(out var oPixels); - decoded.AsSpan2D().TryGetSpan(out var dPixels); + var oPixels = TestHelper.GetSinglePixelArrayAsColors(testImage); + var dPixels = FlattenDecoded(decoded, height, width); var psnr = ImageQuality.PeakSignalToNoiseRatio(oPixels, dPixels, format != CompressionFormat.Bc1); TestHelper.AssertPSNR(psnr, quality); } + + private static ColorRgba32[] FlattenDecoded(ColorRgba32[,] decoded, int height, int width) + { + var result = new ColorRgba32[height * width]; + for (var y = 0; y < height; y++) + for (var x = 0; x < width; x++) + result[y * width + x] = decoded[y, x]; + return result; + } } } diff --git a/BCnEncTests/BCnEncTests.csproj b/BCnEncTests/BCnEncTests.csproj index 7dab56d..74aa454 100644 --- a/BCnEncTests/BCnEncTests.csproj +++ b/BCnEncTests/BCnEncTests.csproj @@ -7,6 +7,7 @@ + @@ -24,4 +25,6 @@ + + diff --git a/BCnEncTests/CancellationTests.cs b/BCnEncTests/CancellationTests.cs deleted file mode 100644 index ca85a9a..0000000 --- a/BCnEncTests/CancellationTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using BCnEncoder.Decoder; -using BCnEncTests.Support; -using Xunit; - -namespace BCnEncTests -{ - public class CancellationTests - { - [Fact] - public async Task EncodeParallelCancellation() - { - await TestHelper.ExecuteCancellationTest(ImageLoader.TestAlphaGradient1, true); - } - - [Fact] - public async Task EncodeNonParallelCancellation() - { - await TestHelper.ExecuteCancellationTest(ImageLoader.TestAlphaGradient1, false); - } - - // HINT: Decoding in general is too fast to be cancelled. - // HINT: For parallel decoding even with TimeSpan.FromTicks(1) the test never successfully threw an exception when executed in bulk with other tests. - // HINT: For non parallel decoding the test was partially successful, due to fluctuations in how much time the decoding needed and when the cancellation was introduced. - } -} diff --git a/BCnEncTests/ClusterTests.cs b/BCnEncTests/ClusterTests.cs deleted file mode 100644 index cee9ccd..0000000 --- a/BCnEncTests/ClusterTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using BCnEncoder.Shared; -using BCnEncTests.Support; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using Xunit; - -namespace BCnEncTests -{ - public class ClusterTests - { - [Fact] - public void Clusterize() - { - var testImage = ImageLoader.TestBlur1.Clone(); - - var pix = TestHelper.GetSinglePixelArray(testImage); - - var pixels = MemoryMarshal.Cast(pix).ToArray(); - - var numClusters = (testImage.Width / 32) * (testImage.Height / 32); - var clusters = LinearClustering.ClusterPixels(pixels, testImage.Width, testImage.Height, numClusters, 10, 10); - - var pixC = new ColorYCbCr[numClusters]; - var counts = new int[numClusters]; - - for (var i = 0; i < pixels.Length; i++) - { - pixC[clusters[i]] += new ColorYCbCr(pixels[i]); - counts[clusters[i]]++; - } - - for (var i = 0; i < numClusters; i++) - { - pixC[i] /= counts[i]; - } - - for (var i = 0; i < pixels.Length; i++) - { - pixels[i] = pixC[clusters[i]].ToColorRgba32(); - pix[i] = new Rgba32(pixels[i].r, pixels[i].g, pixels[i].b, pixels[i].a); - } - - TestHelper.SetSinglePixelArray(testImage, pix); - - using var fs = File.OpenWrite("test_cluster.png"); - testImage.SaveAsPng(fs); - } - } -} diff --git a/BCnEncTests/EncodeByteOrderTests.cs b/BCnEncTests/EncodeByteOrderTests.cs index 928fbc9..9b1d042 100644 --- a/BCnEncTests/EncodeByteOrderTests.cs +++ b/BCnEncTests/EncodeByteOrderTests.cs @@ -19,7 +19,7 @@ public class EncodeByteOrderTests private void EncodeByteOrderTest(PixelFormat pixelFormat) where T : unmanaged, IPixel { - var imageOrig = ImageLoader.TestAlpha1; + using var imageOrig = ImageLoader.LoadTestImageSharp("../../../testImages/test_alpha_1_512.png"); var testImage = imageOrig.CloneAs(); var pixels = testImage.GetPixelMemoryGroup()[0]; diff --git a/BCnEncTests/EncodingTest.cs b/BCnEncTests/EncodingTest.cs deleted file mode 100644 index 00e46f2..0000000 --- a/BCnEncTests/EncodingTest.cs +++ /dev/null @@ -1,501 +0,0 @@ -using System.IO; -using BCnEncoder.Encoder; -using BCnEncoder.ImageSharp; -using BCnEncoder.Shared; -using BCnEncTests.Support; -using Xunit; -using Xunit.Abstractions; - -namespace BCnEncTests -{ - public class Bc1GradientTest - { - private readonly ITestOutputHelper output; - - public Bc1GradientTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void Bc1GradientBestQuality() - { - var image = ImageLoader.TestGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1, - CompressionQuality.BestQuality, - "encoding_bc1_gradient_bestQuality.ktx", - output); - } - - [Fact] - public void Bc1GradientBalanced() - { - var image = ImageLoader.TestGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1, - CompressionQuality.Balanced, - "encoding_bc1_gradient_balanced.ktx", - output); - } - - [Fact] - public void Bc1GradientFast() - { - var image = ImageLoader.TestGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1, - CompressionQuality.Fast, - "encoding_bc1_gradient_fast.ktx", - output); - } - } - - public class Bc1DiffuseTest - { - private readonly ITestOutputHelper output; - - public Bc1DiffuseTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void Bc1DiffuseBestQuality() - { - var image = ImageLoader.TestDiffuse1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1, - CompressionQuality.BestQuality, - "encoding_bc1_diffuse_bestQuality.ktx", - output); - } - - [Fact] - public void Bc1DiffuseBalanced() - { - var image = ImageLoader.TestDiffuse1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1, - CompressionQuality.Balanced, - "encoding_bc1_diffuse_balanced.ktx", - output); - } - - [Fact] - public void Bc1DiffuseFast() - { - var image = ImageLoader.TestDiffuse1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1, - CompressionQuality.Fast, - "encoding_bc1_diffuse_fast.ktx", - output); - } - } - - public class Bc1BlurryTest - { - private readonly ITestOutputHelper output; - - public Bc1BlurryTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void Bc1BlurBestQuality() - { - var image = ImageLoader.TestBlur1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1, - CompressionQuality.BestQuality, - "encoding_bc1_blur_bestQuality.ktx", - output); - } - - [Fact] - public void Bc1BlurBalanced() - { - var image = ImageLoader.TestBlur1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1, - CompressionQuality.Balanced, - "encoding_bc1_blur_balanced.ktx", - output); - } - - [Fact] - public void Bc1BlurFast() - { - var image = ImageLoader.TestBlur1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1, - CompressionQuality.Fast, - "encoding_bc1_blur_fast.ktx", - output); - } - } - - public class Bc1ASpriteTest - { - private readonly ITestOutputHelper output; - - public Bc1ASpriteTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void Bc1ASpriteBestQuality() - { - var image = ImageLoader.TestTransparentSprite1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1WithAlpha, - CompressionQuality.BestQuality, - "encoding_bc1a_sprite_bestQuality.ktx", - output); - } - - [Fact] - public void Bc1ASpriteBalanced() - { - var image = ImageLoader.TestTransparentSprite1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1WithAlpha, - CompressionQuality.Balanced, - "encoding_bc1a_sprite_balanced.ktx", - output); - } - - [Fact] - public void Bc1ASpriteFast() - { - var image = ImageLoader.TestTransparentSprite1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc1WithAlpha, - CompressionQuality.Fast, - "encoding_bc1a_sprite_fast.ktx", - output); - } - } - - public class Bc2GradientTest - { - private readonly ITestOutputHelper output; - - public Bc2GradientTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void Bc2GradientBestQuality() - { - var image = ImageLoader.TestAlphaGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc2, - CompressionQuality.BestQuality, - "encoding_bc2_gradient_bestQuality.ktx", - output); - } - - [Fact] - public void Bc2GradientBalanced() - { - var image = ImageLoader.TestAlphaGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc2, - CompressionQuality.Balanced, - "encoding_bc2_gradient_balanced.ktx", - output); - } - - [Fact] - public void Bc2GradientFast() - { - var image = ImageLoader.TestAlphaGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc2, - CompressionQuality.Fast, - "encoding_bc2_gradient_fast.ktx", - output); - } - } - - public class Bc3GradientTest - { - private readonly ITestOutputHelper output; - - public Bc3GradientTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void Bc3GradientBestQuality() - { - var image = ImageLoader.TestAlphaGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc3, - CompressionQuality.BestQuality, - "encoding_bc3_gradient_bestQuality.ktx", - output); - } - - [Fact] - public void Bc3GradientBalanced() - { - var image = ImageLoader.TestAlphaGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc3, - CompressionQuality.Balanced, - "encoding_bc3_gradient_balanced.ktx", - output); - } - - [Fact] - public void Bc3GradientFast() - { - var image = ImageLoader.TestAlphaGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc3, - CompressionQuality.Fast, - "encoding_bc3_gradient_fast.ktx", - output); - } - } - - public class Bc4RedTest - { - private readonly ITestOutputHelper output; - - public Bc4RedTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void Bc4RedBestQuality() - { - var image = ImageLoader.TestHeight1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc4, - CompressionQuality.BestQuality, - "encoding_bc4_red_bestQuality.ktx", - output); - } - - [Fact] - public void Bc4RedBalanced() - { - var image = ImageLoader.TestHeight1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc4, - CompressionQuality.Balanced, - "encoding_bc4_red_balanced.ktx", - output); - } - - [Fact] - public void Bc4RedFast() - { - var image = ImageLoader.TestHeight1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc4, - CompressionQuality.Fast, - "encoding_bc4_red_fast.ktx", - output); - } - } - - public class Bc5RedGreenTest - { - private readonly ITestOutputHelper output; - - public Bc5RedGreenTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void Bc5RedGreenBestQuality() - { - var image = ImageLoader.TestRedGreen1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc5, - CompressionQuality.BestQuality, - "encoding_bc5_red_green_bestQuality.ktx", - output); - } - - [Fact] - public void Bc5RedGreenBalanced() - { - var image = ImageLoader.TestRedGreen1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc5, - CompressionQuality.Balanced, - "encoding_bc5_red_green_balanced.ktx", - output); - } - - [Fact] - public void Bc5RedGreenFast() - { - var image = ImageLoader.TestRedGreen1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc5, - CompressionQuality.Fast, - "encoding_bc5_red_green_fast.ktx", - output); - } - } - - public class Bc7RgbTest - { - private readonly ITestOutputHelper output; - - public Bc7RgbTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void Bc7RgbBestQuality() - { - var image = ImageLoader.TestRgbHard1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc7, - CompressionQuality.BestQuality, - "encoding_bc7_rgb_bestQuality.ktx", - output); - } - - [Fact] - public void Bc7RgbBalanced() - { - var image = ImageLoader.TestRgbHard1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc7, - CompressionQuality.Balanced, - "encoding_bc7_rgb_balanced.ktx", - output); - } - - [Fact] - public void Bc7LennaBalanced() - { - var image = ImageLoader.TestLenna; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc7, - CompressionQuality.Balanced, - "encoding_bc7_lenna_balanced.ktx", - output); - } - - [Fact] - public void Bc7RgbFast() - { - var image = ImageLoader.TestRgbHard1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc7, - CompressionQuality.Fast, - "encoding_bc7_rgb_fast.ktx", - output); - } - } - - public class Bc7RgbaTest - { - private readonly ITestOutputHelper output; - - public Bc7RgbaTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void Bc7RgbaBestQuality() - { - var image = ImageLoader.TestAlpha1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc7, - CompressionQuality.BestQuality, - "encoding_bc7_rgba_bestQuality.ktx", - output); - } - - [Fact] - public void Bc7RgbaBalanced() - { - var image = ImageLoader.TestAlpha1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc7, - CompressionQuality.Balanced, - "encoding_bc7_rgba_balanced.ktx", - output); - } - - [Fact] - public void Bc7RgbaFast() - { - var image = ImageLoader.TestAlpha1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.Bc7, - CompressionQuality.Fast, - "encoding_bc7_rgba_fast.ktx", - output); - } - } - - public class CubemapTest - { - [Fact] - public void WriteCubeMapFile() - { - var images = ImageLoader.TestCubemap; - - var filename = "encoding_bc1_cubemap.ktx"; - - var encoder = new BcEncoder(); - encoder.OutputOptions.Quality = CompressionQuality.Fast; - encoder.OutputOptions.GenerateMipMaps = true; - encoder.OutputOptions.Format = CompressionFormat.Bc1; - - using var fs = File.OpenWrite(filename); - encoder.EncodeCubeMapToStream(images[0], images[1], images[2], images[3], images[4], images[5], fs); - } - } -} diff --git a/BCnEncTests/HdrImageTests.cs b/BCnEncTests/HdrImageTests.cs index 9e05ab2..14dc3d1 100644 --- a/BCnEncTests/HdrImageTests.cs +++ b/BCnEncTests/HdrImageTests.cs @@ -2,9 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading.Tasks; +using BCnEncoder.Decoder; using BCnEncoder.Encoder; using BCnEncoder.Shared; using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -39,5 +42,25 @@ public void LoadHdr() TestHelper.AssertImagesEqual(HdrLoader.ReferenceKiara, img2, CompressionQuality.BestQuality); } + + [Fact] + public async Task DecodeAllMipMapsHdrStreamAsync() + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Format = CompressionFormat.Bc6U; + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + + var decoder = new BcDecoder(); + var input = HdrLoader.TestHdrKiara; + var ktxWithMips = encoder.EncodeToKtxHdr(new Memory2D(input.pixels, input.height, input.width)); + using var ms = new MemoryStream(); + ktxWithMips.Write(ms); + ms.Position = 0; + + var images = await decoder.DecodeAllMipMapsHdr2DAsync(ms); + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + Assert.True(images.Length > 1); + } } } diff --git a/BCnEncTests/ImageSharpTests.cs b/BCnEncTests/ImageSharpTests.cs new file mode 100644 index 0000000..855a051 --- /dev/null +++ b/BCnEncTests/ImageSharpTests.cs @@ -0,0 +1,388 @@ +using System.IO; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.ImageSharp; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using BCnEncTests.Support; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace BCnEncTests +{ + public class ImageSharpEncodingTests + { + private readonly BcEncoder encoder; + private readonly BcDecoder decoder; + private readonly Image original; + private readonly Image[] cubemap; + + public ImageSharpEncodingTests() + { + encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + decoder = new BcDecoder(); + original = ImageLoader.LoadTestImageSharp("../../../testImages/test_gradient_1_512.jpg"); + cubemap = new[] + { + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/right.png"), + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/left.png"), + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/top.png"), + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/bottom.png"), + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/back.png"), + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/forward.png"), + }; + } + + [Fact] + public void EncodeToKtx() + { + var ktx = encoder.EncodeToKtx(original); + using var decoded = decoder.DecodeToImageRgba32(ktx); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeToDds() + { + var dds = encoder.EncodeToDds(original); + using var decoded = decoder.DecodeToImageRgba32(dds); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeToStreamKtx() + { + encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; + using var ms = new MemoryStream(); + encoder.EncodeToStream(original, ms); + ms.Position = 0; + using var decoded = decoder.DecodeToImageRgba32(ms); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeToStreamDds() + { + encoder.OutputOptions.FileFormat = OutputFileFormat.Dds; + using var ms = new MemoryStream(); + encoder.EncodeToStream(original, ms); + ms.Position = 0; + using var decoded = decoder.DecodeToImageRgba32(ms); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeToRawBytes() + { + var rawBytes = encoder.EncodeToRawBytes(original); + using var decoded = decoder.DecodeRawToImageRgba32(rawBytes[0], original.Width, original.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeToRawBytesSingleMip() + { + var mip0 = encoder.EncodeToRawBytes(original, 0, out var mipWidth, out var mipHeight); + Assert.Equal(original.Width, mipWidth); + Assert.Equal(original.Height, mipHeight); + Assert.True(mip0.Length > 0); + } + + [Fact] + public void CalculateMipLevelCount() + { + var fromImage = encoder.CalculateNumberOfMipLevels(original); + var fromDims = encoder.CalculateNumberOfMipLevels(original.Width, original.Height); + Assert.Equal(fromDims, fromImage); + } + + [Fact] + public void CalculateMipMapSize() + { + encoder.CalculateMipMapSize(original, 0, out var w0, out var h0); + Assert.Equal(original.Width, w0); + Assert.Equal(original.Height, h0); + + encoder.CalculateMipMapSize(original, 1, out var w1, out var h1); + Assert.Equal(original.Width / 2, w1); + Assert.Equal(original.Height / 2, h1); + } + + [Fact] + public void EncodeCubeMapToKtx() + { + var ktx = encoder.EncodeCubeMapToKtx(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5]); + Assert.Equal(6, ktx.MipMaps[0].Faces.Length); + for (var i = 0; i < 6; i++) + { + var face = ktx.MipMaps[0].Faces[i]; + using var decoded = decoder.DecodeRawToImageRgba32(face.Data, (int)face.Width, (int)face.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(cubemap[i], decoded, encoder.OutputOptions.Quality); + } + } + + [Fact] + public void EncodeCubeMapToDds() + { + var dds = encoder.EncodeCubeMapToDds(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5]); + Assert.Equal(6, dds.Faces.Count); + for (var i = 0; i < 6; i++) + { + var face = dds.Faces[i].MipMaps[0]; + using var decoded = decoder.DecodeRawToImageRgba32(face.Data, (int)face.Width, (int)face.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(cubemap[i], decoded, encoder.OutputOptions.Quality); + } + } + + [Fact] + public void EncodeCubeMapToStream() + { + encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; + using var ms = new MemoryStream(); + encoder.EncodeCubeMapToStream(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5], ms); + ms.Position = 0; + var ktx = KtxFile.Load(ms); + Assert.Equal(6, ktx.MipMaps[0].Faces.Length); + } + + [Fact] + public async Task EncodeToKtxAsync() + { + var ktx = await encoder.EncodeToKtxAsync(original); + using var decoded = decoder.DecodeToImageRgba32(ktx); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task EncodeToDdsAsync() + { + var dds = await encoder.EncodeToDdsAsync(original); + using var decoded = decoder.DecodeToImageRgba32(dds); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task EncodeToStreamAsync() + { + encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; + using var ms = new MemoryStream(); + await encoder.EncodeToStreamAsync(original, ms); + ms.Position = 0; + using var decoded = decoder.DecodeToImageRgba32(ms); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task EncodeToRawBytesAsync() + { + var rawBytes = await encoder.EncodeToRawBytesAsync(original); + using var decoded = decoder.DecodeRawToImageRgba32(rawBytes[0], original.Width, original.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task EncodeCubeMapToKtxAsync() + { + var ktx = await encoder.EncodeCubeMapToKtxAsync(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5]); + Assert.Equal(6, ktx.MipMaps[0].Faces.Length); + } + + [Fact] + public async Task EncodeCubeMapToDdsAsync() + { + var dds = await encoder.EncodeCubeMapToDdsAsync(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5]); + Assert.Equal(6, dds.Faces.Count); + } + + [Fact] + public async Task EncodeCubeMapToStreamAsync() + { + encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; + using var ms = new MemoryStream(); + await encoder.EncodeCubeMapToStreamAsync(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5], ms); + ms.Position = 0; + var ktx = KtxFile.Load(ms); + Assert.Equal(6, ktx.MipMaps[0].Faces.Length); + } + } + + public class ImageSharpDecodingTests + { + private readonly BcEncoder encoder; + private readonly BcDecoder decoder; + private readonly Image original; + private readonly KtxFile encodedKtx; + private readonly DdsFile encodedDds; + private readonly byte[] rawEncoded; + + public ImageSharpDecodingTests() + { + encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + decoder = new BcDecoder(); + original = ImageLoader.LoadTestImageSharp("../../../testImages/test_gradient_1_512.jpg"); + encodedKtx = encoder.EncodeToKtx(ImageLoader.TestGradient1); + encodedDds = encoder.EncodeToDds(ImageLoader.TestGradient1); + rawEncoded = encoder.EncodeToRawBytes(ImageLoader.TestGradient1)[0]; + } + + [Fact] + public void DecodeKtxFile() + { + using var decoded = decoder.DecodeToImageRgba32(encodedKtx); + Assert.Equal(original.Width, decoded.Width); + Assert.Equal(original.Height, decoded.Height); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void DecodeDdsFile() + { + using var decoded = decoder.DecodeToImageRgba32(encodedDds); + Assert.Equal(original.Width, decoded.Width); + Assert.Equal(original.Height, decoded.Height); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void DecodeStream() + { + using var ms = new MemoryStream(); + encodedKtx.Write(ms); + ms.Position = 0; + using var decoded = decoder.DecodeToImageRgba32(ms); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void DecodeAllMipMapsKtx() + { + encoder.OutputOptions.GenerateMipMaps = true; + var ktxWithMips = encoder.EncodeToKtx(ImageLoader.TestGradient1); + var images = decoder.DecodeAllMipMapsToImageRgba32(ktxWithMips); + + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + Assert.True(images.Length > 1); + TestHelper.AssertImagesEqual(original, images[0], encoder.OutputOptions.Quality); + + foreach (var img in images) img.Dispose(); + } + + [Fact] + public void DecodeAllMipMapsDds() + { + encoder.OutputOptions.GenerateMipMaps = true; + var ddsWithMips = encoder.EncodeToDds(ImageLoader.TestGradient1); + var images = decoder.DecodeAllMipMapsToImageRgba32(ddsWithMips); + + Assert.Equal((int)ddsWithMips.header.dwMipMapCount, images.Length); + Assert.True(images.Length > 1); + + foreach (var img in images) img.Dispose(); + } + + [Fact] + public void DecodeAllMipMapsStream() + { + encoder.OutputOptions.GenerateMipMaps = true; + var ktxWithMips = encoder.EncodeToKtx(ImageLoader.TestGradient1); + using var ms = new MemoryStream(); + ktxWithMips.Write(ms); + ms.Position = 0; + + var images = decoder.DecodeAllMipMapsToImageRgba32(ms); + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + + foreach (var img in images) img.Dispose(); + } + + [Fact] + public void DecodeRaw() + { + using var decoded = decoder.DecodeRawToImageRgba32(rawEncoded, original.Width, original.Height, CompressionFormat.Bc1); + Assert.Equal(original.Width, decoded.Width); + Assert.Equal(original.Height, decoded.Height); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void DecodeRawStream() + { + using var ms = new MemoryStream(rawEncoded); + using var decoded = decoder.DecodeRawToImageRgba32(ms, original.Width, original.Height, CompressionFormat.Bc1); + Assert.Equal(original.Width, decoded.Width); + Assert.Equal(original.Height, decoded.Height); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeKtxFileAsync() + { + using var decoded = await decoder.DecodeToImageRgba32Async(encodedKtx); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeDdsFileAsync() + { + using var decoded = await decoder.DecodeToImageRgba32Async(encodedDds); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeStreamAsync() + { + using var ms = new MemoryStream(); + encodedKtx.Write(ms); + ms.Position = 0; + using var decoded = await decoder.DecodeToImageRgba32Async(ms); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeAllMipMapsKtxAsync() + { + encoder.OutputOptions.GenerateMipMaps = true; + var ktxWithMips = encoder.EncodeToKtx(ImageLoader.TestGradient1); + var images = await decoder.DecodeAllMipMapsToImageRgba32Async(ktxWithMips); + + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + Assert.True(images.Length > 1); + + foreach (var img in images) img.Dispose(); + } + + [Fact] + public async Task DecodeAllMipMapsStreamAsync() + { + encoder.OutputOptions.GenerateMipMaps = true; + var ktxWithMips = encoder.EncodeToKtx(ImageLoader.TestGradient1); + using var ms = new MemoryStream(); + ktxWithMips.Write(ms); + ms.Position = 0; + + var images = await decoder.DecodeAllMipMapsToImageRgba32Async(ms); + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + + foreach (var img in images) img.Dispose(); + } + + [Fact] + public async Task DecodeRawAsync() + { + using var decoded = await decoder.DecodeRawToImageRgba32Async(rawEncoded, original.Width, original.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeRawStreamAsync() + { + using var ms = new MemoryStream(rawEncoded); + using var decoded = await decoder.DecodeRawToImageRgba32Async(ms, original.Width, original.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + } +} diff --git a/BCnEncTests/MipMapperTests.cs b/BCnEncTests/MipMapperTests.cs new file mode 100644 index 0000000..5da9c7e --- /dev/null +++ b/BCnEncTests/MipMapperTests.cs @@ -0,0 +1,79 @@ +using System; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using Xunit; +using Xunit.Abstractions; + +namespace BCnEncTests +{ + public class MipMapperTests + { + private readonly ITestOutputHelper output; + + public MipMapperTests(ITestOutputHelper output) => this.output = output; + + [Fact] + public void MipChainHasCorrectDimensions() + { + var image = ImageLoader.TestGradient1; // 512x416 + var numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + Assert.Equal(chain.Length, numMips); + Assert.True(numMips > 1); + + for (var i = 0; i < numMips; i++) + { + Assert.Equal(Math.Max(1, image.Width >> i), chain[i].Width); + Assert.Equal(Math.Max(1, image.Height >> i), chain[i].Height); + } + + // Last level must be 1x1 + Assert.Equal(1, chain[numMips - 1].Width); + Assert.Equal(1, chain[numMips - 1].Height); + } + + /// + /// Compares each mip level produced by MipMapper against the equivalent + /// ImageSharp Box-filter resize (Compand=false so both operate in sRGB + /// byte space without gamma correction). The PSNR should be very high + /// because both algorithms perform the same 2x2 box average. + /// + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + public void MipLevelMatchesImageSharpBoxFilter(int mipLevel) + { + var image = ImageLoader.TestGradient1; + var numMips = mipLevel + 1; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + var mipW = chain[mipLevel].Width; + var mipH = chain[mipLevel].Height; + + // ImageSharp Box sampler + Compand=false: no gamma correction, + // averages pixel values in sRGB space — matches MipMapper exactly. + using var imageSharp = ImageLoader.LoadTestImageSharp("../../../testImages/test_gradient_1_512.jpg"); + using var reference = imageSharp.Clone(x => x.Resize(new ResizeOptions + { + Size = new Size(mipW, mipH), + Sampler = KnownResamplers.Box, + Compand = false + })); + + var mipColors = TestHelper.GetSinglePixelArrayAsColors(chain[mipLevel]); + var refColors = TestHelper.GetSinglePixelArrayAsColors(reference); + + var psnr = ImageQuality.PeakSignalToNoiseRatio(mipColors, refColors); + output.WriteLine($"Mip level {mipLevel} ({mipW}x{mipH}): PSNR vs ImageSharp Box = {psnr:F2} dB"); + + Assert.True(psnr > 40, + $"Mip level {mipLevel}: PSNR vs ImageSharp Box was {psnr:F2} dB (expected > 40)"); + } + } +} diff --git a/BCnEncTests/RawTests.cs b/BCnEncTests/RawTests.cs deleted file mode 100644 index 8bfc6f8..0000000 --- a/BCnEncTests/RawTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.IO; -using BCnEncoder.Decoder; -using BCnEncoder.Encoder; -using BCnEncoder.ImageSharp; -using BCnEncoder.Shared; -using BCnEncTests.Support; -using SixLabors.ImageSharp.Processing; -using Xunit; - -namespace BCnEncTests -{ - public class RawTests - { - [Fact] - public void EncodeDecode() - { - var inputImage = ImageLoader.TestGradient1; - var decoder = new BcDecoder(); - var encoder = new BcEncoder - { - OutputOptions = { Quality = CompressionQuality.BestQuality } - }; - - var encodedRawBytes = encoder.EncodeToRawBytes(inputImage); - var decodedImage = decoder.DecodeRawToImageRgba32(encodedRawBytes[0], inputImage.Width, inputImage.Height, CompressionFormat.Bc1); - - var originalPixels = TestHelper.GetSinglePixelArray(ImageLoader.TestGradient1); - var decodedPixels = TestHelper.GetSinglePixelArray(decodedImage); - - TestHelper.AssertPixelsEqual(originalPixels, decodedPixels, encoder.OutputOptions.Quality); - } - - [Fact] - public void EncodeDecodeStream() - { - var inputImage = ImageLoader.TestGradient1; - var decoder = new BcDecoder(); - var encoder = new BcEncoder - { - OutputOptions = { Quality = CompressionQuality.BestQuality } - }; - - var encodedRawBytes = encoder.EncodeToRawBytes(inputImage); - - using var ms = new MemoryStream(encodedRawBytes[0]); - - Assert.Equal(0, ms.Position); - - var decodedImage = decoder.DecodeRawToImageRgba32(ms, inputImage.Width, inputImage.Height, CompressionFormat.Bc1); - - var originalPixels = TestHelper.GetSinglePixelArray(inputImage); - var decodedPixels = TestHelper.GetSinglePixelArray(decodedImage); - - TestHelper.AssertPixelsEqual(originalPixels, decodedPixels, encoder.OutputOptions.Quality); - } - - - [Fact] - public void EncodeDecodeAllMipMapsStream() - { - var inputImage = ImageLoader.TestGradient1; - var decoder = new BcDecoder(); - var encoder = new BcEncoder - { - OutputOptions = - { - Quality = CompressionQuality.BestQuality, - GenerateMipMaps = true, - MaxMipMapLevel = 0 - } - }; - - using var ms = new MemoryStream(); - - var encodedRawBytes = encoder.EncodeToRawBytes(inputImage); - - var mipLevels = encoder.CalculateNumberOfMipLevels(inputImage); - Assert.True(mipLevels > 1); - - for (var i = 0; i < mipLevels; i++) - { - ms.Write(encodedRawBytes[i]); - } - - ms.Position = 0; - Assert.Equal(0, ms.Position); - - for (var i = 0; i < mipLevels; i++) - { - encoder.CalculateMipMapSize(inputImage, i, out var mipWidth, out var mipHeight); - using var resized = inputImage.Clone(x => x.Resize(mipWidth, mipHeight)); - - var decodedImage = decoder.DecodeRawToImageRgba32(ms, mipWidth, mipHeight, CompressionFormat.Bc1); - - var originalPixels = TestHelper.GetSinglePixelArray(resized); - var decodedPixels = TestHelper.GetSinglePixelArray(decodedImage); - - TestHelper.AssertPixelsEqual(originalPixels, decodedPixels, encoder.OutputOptions.Quality); - } - - encoder.CalculateMipMapSize(inputImage, mipLevels - 1, out var lastMWidth, out var lastMHeight); - Assert.Equal(1, lastMWidth); - Assert.Equal(1, lastMHeight); - } - } -} diff --git a/BCnEncTests/Support/HdrLoader.cs b/BCnEncTests/Support/HdrLoader.cs index 519f02e..6080cf4 100644 --- a/BCnEncTests/Support/HdrLoader.cs +++ b/BCnEncTests/Support/HdrLoader.cs @@ -12,7 +12,7 @@ public static class HdrLoader { public static HdrImage TestHdrKiara { get; } = HdrImage.Read("../../../testImages/test_hdr_kiara.hdr"); public static HdrImage TestHdrProbe { get; } = HdrImage.Read("../../../testImages/test_hdr_probe.hdr"); - public static Image ReferenceKiara { get; } = ImageLoader.LoadTestImage("../../../testImages/test_hdr_kiara.png"); + public static Image ReferenceKiara { get; } = ImageLoader.LoadTestImageSharp("../../../testImages/test_hdr_kiara.png"); public static DdsFile TestHdrKiaraDds { get; } = DdsLoader.LoadDdsFile("../../../testImages/test_hdr_kiara_bc6h.dds"); public static KtxFile TestHdrKiaraKtx { get; } = diff --git a/BCnEncTests/Support/ImageLoader.cs b/BCnEncTests/Support/ImageLoader.cs index 3a825d2..37d410b 100644 --- a/BCnEncTests/Support/ImageLoader.cs +++ b/BCnEncTests/Support/ImageLoader.cs @@ -1,6 +1,7 @@ using System.IO; using BCnEncoder.Shared; using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -8,21 +9,21 @@ namespace BCnEncTests.Support { public static class ImageLoader { - public static Image TestDiffuse1 { get; } = LoadTestImage("../../../testImages/test_diffuse_1_512.jpg"); - public static Image TestBlur1 { get; } = LoadTestImage("../../../testImages/test_blur_1_512.jpg"); - public static Image TestNormal1 { get; } = LoadTestImage("../../../testImages/test_normal_1_512.jpg"); - public static Image TestHeight1 { get; } = LoadTestImage("../../../testImages/test_height_1_512.jpg"); - public static Image TestGradient1 { get; } = LoadTestImage("../../../testImages/test_gradient_1_512.jpg"); + public static Memory2D TestDiffuse1 { get; } = LoadTestImage("../../../testImages/test_diffuse_1_512.jpg"); + public static Memory2D TestBlur1 { get; } = LoadTestImage("../../../testImages/test_blur_1_512.jpg"); + public static Memory2D TestNormal1 { get; } = LoadTestImage("../../../testImages/test_normal_1_512.jpg"); + public static Memory2D TestHeight1 { get; } = LoadTestImage("../../../testImages/test_height_1_512.jpg"); + public static Memory2D TestGradient1 { get; } = LoadTestImage("../../../testImages/test_gradient_1_512.jpg"); - public static Image TestTransparentSprite1 { get; } = LoadTestImage("../../../testImages/test_transparent.png"); - public static Image TestAlphaGradient1 { get; } = LoadTestImage("../../../testImages/test_alphagradient_1_512.png"); - public static Image TestAlpha1 { get; } = LoadTestImage("../../../testImages/test_alpha_1_512.png"); - public static Image TestRedGreen1 { get; } = LoadTestImage("../../../testImages/test_red_green_1_64.png"); - public static Image TestRgbHard1 { get; } = LoadTestImage("../../../testImages/test_rgb_hard_1.png"); - public static Image TestLenna { get; } = LoadTestImage("../../../testImages/test_lenna_512.png"); - public static Image TestDecodingBc5Reference { get; } = LoadTestImage("../../../testImages/decoding_dds_bc5_reference.png"); + public static Memory2D TestTransparentSprite1 { get; } = LoadTestImage("../../../testImages/test_transparent.png"); + public static Memory2D TestAlphaGradient1 { get; } = LoadTestImage("../../../testImages/test_alphagradient_1_512.png"); + public static Memory2D TestAlpha1 { get; } = LoadTestImage("../../../testImages/test_alpha_1_512.png"); + public static Memory2D TestRedGreen1 { get; } = LoadTestImage("../../../testImages/test_red_green_1_64.png"); + public static Memory2D TestRgbHard1 { get; } = LoadTestImage("../../../testImages/test_rgb_hard_1.png"); + public static Memory2D TestLenna { get; } = LoadTestImage("../../../testImages/test_lenna_512.png"); + public static Memory2D TestDecodingBc5Reference { get; } = LoadTestImage("../../../testImages/decoding_dds_bc5_reference.png"); - public static Image[] TestCubemap { get; } = { + public static Memory2D[] TestCubemap { get; } = { LoadTestImage("../../../testImages/cubemap/right.png"), LoadTestImage("../../../testImages/cubemap/left.png"), LoadTestImage("../../../testImages/cubemap/top.png"), @@ -31,10 +32,28 @@ public static class ImageLoader LoadTestImage("../../../testImages/cubemap/forward.png") }; - internal static Image LoadTestImage(string filename) + internal static Memory2D LoadTestImage(string filename) + { + using var image = Image.Load(filename); + return ToMemory2D(image); + } + + internal static Image LoadTestImageSharp(string filename) { return Image.Load(filename); } + + private static Memory2D ToMemory2D(Image image) + { + var pixels = new ColorRgba32[image.Width * image.Height]; + for (var y = 0; y < image.Height; y++) + for (var x = 0; x < image.Width; x++) + { + var p = image[x, y]; + pixels[y * image.Width + x] = new ColorRgba32(p.R, p.G, p.B, p.A); + } + return new Memory2D(pixels, image.Height, image.Width); + } } public static class DdsLoader diff --git a/BCnEncTests/Support/TestHelper.cs b/BCnEncTests/Support/TestHelper.cs index 40bc50c..9d6349a 100644 --- a/BCnEncTests/Support/TestHelper.cs +++ b/BCnEncTests/Support/TestHelper.cs @@ -20,17 +20,15 @@ public static class TestHelper { #region Assertions - public static void AssertPixelsEqual(Span originalPixels, Span pixels, CompressionQuality quality) + public static void AssertPixelsEqual(Span originalPixels, Span pixels, CompressionQuality quality, ITestOutputHelper output = null) { - var psnr = ImageQuality.PeakSignalToNoiseRatio( - MemoryMarshal.Cast(originalPixels), - MemoryMarshal.Cast(pixels)); - AssertPSNR(psnr, quality); + var psnr = ImageQuality.PeakSignalToNoiseRatio(originalPixels, pixels); + AssertPSNR(psnr, quality, output); } public static void AssertPixelsEqual(Span originalPixels, Span pixels, CompressionQuality quality, ITestOutputHelper output = null) { - var rmse = ImageQuality.CalculateLogRMSE(originalPixels,pixels); + var rmse = ImageQuality.CalculateLogRMSE(originalPixels, pixels); AssertRMSE(rmse, quality, output); } @@ -40,6 +38,12 @@ public static void AssertImagesEqual(Image original, Image image AssertPSNR(psnr, quality); } + public static void AssertImagesEqual(Memory2D original, Memory2D image, CompressionQuality quality, bool countAlpha = true) + { + var psnr = CalculatePSNR(original, image, countAlpha); + AssertPSNR(psnr, quality); + } + #endregion #region Execute methods @@ -50,23 +54,23 @@ public static void ExecuteDecodingTest(KtxFile file, string outputFile) Assert.Equal((uint)1, file.header.NumberOfFaces); var decoder = new BcDecoder(); - using var image = decoder.DecodeToImageRgba32(file); + var pixels = decoder.Decode2D(file); - Assert.Equal((uint)image.Width, file.header.PixelWidth); - Assert.Equal((uint)image.Height, file.header.PixelHeight); + Assert.Equal((uint)pixels.Width, file.header.PixelWidth); + Assert.Equal((uint)pixels.Height, file.header.PixelHeight); using var outFs = File.OpenWrite(outputFile); - image.SaveAsPng(outFs); + SaveAsPng(pixels, outFs); } #region Dds - public static void ExecuteDdsWritingTest(Image image, CompressionFormat format, string outputFile) + public static void ExecuteDdsWritingTest(Memory2D image, CompressionFormat format, string outputFile) { ExecuteDdsWritingTest(new[] { image }, format, outputFile); } - public static void ExecuteDdsWritingTest(Image[] images, CompressionFormat format, string outputFile) + public static void ExecuteDdsWritingTest(Memory2D[] images, CompressionFormat format, string outputFile) { var encoder = new BcEncoder(); encoder.OutputOptions.Quality = CompressionQuality.Fast; @@ -93,7 +97,7 @@ public static void ExecuteDdsReadingTest(DdsFile file, DxgiFormat format, string var decoder = new BcDecoder(); decoder.InputOptions.DdsBc1ExpectAlpha = assertAlpha; - var images = decoder.DecodeAllMipMapsToImageRgba32(file); + var images = decoder.DecodeAllMipMaps2D(file); Assert.Equal((uint)images[0].Width, file.header.dwWidth); Assert.Equal((uint)images[0].Height, file.header.dwHeight); @@ -107,8 +111,7 @@ public static void ExecuteDdsReadingTest(DdsFile file, DxgiFormat format, string } using var outFs = File.OpenWrite(string.Format(outputFile, i)); - images[i].SaveAsPng(outFs); - images[i].Dispose(); + SaveAsPng(images[i], outFs); } } @@ -116,7 +119,7 @@ public static void ExecuteDdsReadingTest(DdsFile file, DxgiFormat format, string #region Cancellation - public static async Task ExecuteCancellationTest(Image image, bool isParallel) + public static async Task ExecuteCancellationTest(Memory2D image, bool isParallel) { var encoder = new BcEncoder(CompressionFormat.Bc7); encoder.OutputOptions.Quality = CompressionQuality.Fast; @@ -124,14 +127,14 @@ public static async Task ExecuteCancellationTest(Image image, bool isPar var source = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); await Assert.ThrowsAnyAsync(() => - encoder.EncodeToRawBytesAsync(image, 0, source.Token)); + encoder.EncodeToRawBytesAsync(image, source.Token)); } #endregion #endregion - public static float DecodeKtxCheckPSNR(string filename, Image original) + public static float DecodeKtxCheckPSNR(string filename, Memory2D original) { using var fs = File.OpenRead(filename); var ktx = KtxFile.Load(fs); @@ -139,9 +142,9 @@ public static float DecodeKtxCheckPSNR(string filename, Image original) { OutputOptions = { Bc4Component = ColorComponent.Luminance } }; - using var img = decoder.DecodeToImageRgba32(ktx); + var decoded = decoder.Decode2D(ktx); - return CalculatePSNR(original, img); + return CalculatePSNR(original, decoded); } public static float DecodeKtxCheckRMSEHdr(string filename, HdrImage original) @@ -157,8 +160,7 @@ public static float DecodeKtxCheckRMSEHdr(string filename, HdrImage original) return ImageQuality.CalculateLogRMSE(original.pixels, decoded); } - - public static void ExecuteEncodingTest(Image image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) + public static void ExecuteEncodingTest(Memory2D image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) { var encoder = new BcEncoder(); encoder.OutputOptions.Quality = quality; @@ -182,7 +184,7 @@ public static void ExecuteHdrEncodingTest(HdrImage image, CompressionFormat form encoder.OutputOptions.Format = format; var fs = File.OpenWrite(filename); - encoder.EncodeToStreamHdr(image.pixels.AsMemory().AsMemory2D(image.height, image.width), fs); + encoder.EncodeToStreamHdr(new Memory2D(image.pixels, image.height, image.width), fs); fs.Close(); var rmse = DecodeKtxCheckRMSEHdr(filename, image); @@ -198,15 +200,24 @@ private static float CalculatePSNR(Image original, Image decoded return ImageQuality.PeakSignalToNoiseRatio(pixels, pixels2, countAlpha); } - public static void AssertPSNR(float psnr, CompressionQuality quality) + private static float CalculatePSNR(Memory2D original, Memory2D decoded, bool countAlpha = true) { + var pixels = GetSinglePixelArrayAsColors(original); + var pixels2 = GetSinglePixelArrayAsColors(decoded); + + return ImageQuality.PeakSignalToNoiseRatio(pixels, pixels2, countAlpha); + } + + public static void AssertPSNR(float psnr, CompressionQuality quality, ITestOutputHelper output = null) + { + output?.WriteLine($"PSNR: {psnr} , quality: {quality}"); if (quality == CompressionQuality.Fast) { - Assert.True(psnr > 25); + Assert.True(psnr > 25, $"PSNR was less than 25: {psnr} , quality: {quality}"); } else { - Assert.True(psnr > 30); + Assert.True(psnr > 30, $"PSNR was less than 30: {psnr} , quality: {quality}"); } } @@ -237,6 +248,20 @@ public static ColorRgba32[] GetSinglePixelArrayAsColors(Image original) return pixels; } + public static ColorRgba32[] GetSinglePixelArrayAsColors(ReadOnlyMemory2D image) + { + var pixels = new ColorRgba32[image.Width * image.Height]; + var span = image.Span; + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + pixels[y * image.Width + x] = span[y, x]; + } + } + return pixels; + } + public static T[] GetSinglePixelArray(Image original) where T : unmanaged, IPixel { T[] pixels = new T[original.Width * original.Height]; @@ -261,5 +286,35 @@ public static void SetSinglePixelArray(Image dest, T[] pixels) where T : u } } } + + public static void SaveAsPng(Memory2D img, Stream fs) + { + var imageRgba = new Image(img.Width, img.Height); + var span = img.Span; + for (var y = 0; y < img.Height; y++) + { + for (var x = 0; x < img.Width; x++) + { + var col = span[y, x]; + imageRgba[x, y] = new Rgba32(col.r, col.g, col.b, col.a); + } + } + imageRgba.SaveAsPng(fs); + } + + public static void SaveAsPng(ColorRgbFloat[] pixels, int width, int height, Stream stream) + { + var rgba = new ColorRgba32[pixels.Length]; + for (var i = 0; i < pixels.Length; i++) + { + var p = pixels[i]; + byte r = (byte)(Math.Max(0, Math.Min(1, p.r)) * 255 + 0.5f); + byte g = (byte)(Math.Max(0, Math.Min(1, p.g)) * 255 + 0.5f); + byte b = (byte)(Math.Max(0, Math.Min(1, p.b)) * 255 + 0.5f); + rgba[i] = new ColorRgba32(r, g, b, 255); + } + var mem = new Memory2D(rgba, height, width); + SaveAsPng(mem, stream); + } } }