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..64bb373 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 @@ -11,11 +12,11 @@ MIT OR Unlicense false - 2.2.1 + 2.3.0 Nominom BCnEncoder.Net - BCnEncoder.NET is a library for compressing rgba images to different block-compressed formats. Both ktx and dds output file-formats are supported. It has no native dependencies and is .NET Standard 2.1 compatible. + BCnEncoder.NET is a library for compressing rgba images to different block-compressed formats. Both ktx and dds output file-formats are supported. It has no native dependencies and is .NET Standard 2.1 & 2.0 compatible. Supported formats are: Raw unsigned byte R, RG, RGB and RGBA formats @@ -31,9 +32,9 @@ Supported formats are: git BCn BC BC1 BC2 BC3 BC4 BC5 BC6 BC6H BC7 BPTC RGTC S3TC DXT1 DXT3 DXT5 ktx dds texture compression encoding decoding decompression image gpu https://github.com/Nominom/BCnEncoder.NET - 2.2.1 -- Fixed divide by zero error on unsupported format. -- Assume dds files always have at least 1 mipmap. + 2.3.0 +- Add .NET Standard 2.0 support +- Fix some DecodeAllMipMaps methods not decoding all MipMaps @@ -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..c73eea2 --- /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 new Memory2D(pixels, bmp.Width, bmp.Height); + } + } + + 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..24ad009 --- /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(new Memory2D(image.pixels, 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); + } } } diff --git a/BCnEncoder.NET.ImageSharp/BCnEncoder.NET.ImageSharp.csproj b/BCnEncoder.NET.ImageSharp/BCnEncoder.NET.ImageSharp.csproj index 4d7c077..ef8673c 100644 --- a/BCnEncoder.NET.ImageSharp/BCnEncoder.NET.ImageSharp.csproj +++ b/BCnEncoder.NET.ImageSharp/BCnEncoder.NET.ImageSharp.csproj @@ -17,8 +17,8 @@ https://github.com/Nominom/BCnEncoder.NET https://github.com/Nominom/BCnEncoder.NET git - Update to ImageSharp 3.1.7 and CommunityToolkit.HighPerformance 8.4.0 - 1.1.2 + Update to ImageSharp 3.1.12 + 1.1.3 @@ -27,7 +27,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/README.md b/README.md index 367c927..2e157c7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Cross-platform BCn / DXT encoding libary for .NET # What is it? -BCnEncoder.NET is a library for compressing rgba images to different block-compressed formats. It has no native dependencies and is .NET Standard 2.1 compatible. +BCnEncoder.NET is a library for compressing rgba images to different block-compressed formats. It has no native dependencies and is .NET Standard 2.1 & 2.0 compatible. Supported formats are: - Raw unsigned byte R, RG, RGB and RGBA formats @@ -21,11 +21,18 @@ Supported formats are: The current state of this library is in development but quite usable. I'm planning on implementing support for more codecs and different algorithms. The current version is capable of encoding and decoding BC1-BC7 images in both KTX or DDS formats. +.NET Standard 2.0 support is experimental, and relies on [PolySharp](https://www.nuget.org/packages/PolySharp) & [Microsoft.Bcl.Numerics](https://www.nuget.org/packages/Microsoft.Bcl.Numerics). + Please note, that the API might change between versions. + # Dependencies Current dependencies are: * [CommunityToolkit.HighPerformance](https://www.nuget.org/packages/CommunityToolkit.HighPerformance/) licensed under the [MIT](https://opensource.org/licenses/MIT) license for Span2D and Memory2D types. +Additionally, for .NET Standard 2.0: +* [PolySharp](https://www.nuget.org/packages/PolySharp) licensed under the [MIT](https://opensource.org/licenses/MIT) license. +* [Microsoft.Bcl.Numerics](https://www.nuget.org/packages/Microsoft.Bcl.Numerics) licensed under the [MIT](https://opensource.org/licenses/MIT) license. + # Image library extensions This library has extension packages available for the following image libraries: