From 0215b91a3cc179369067e34509a3dda8829240e3 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:50:08 +0200 Subject: [PATCH 01/17] Bump ImageSharp version --- BCnEncoder.NET.ImageSharp/BCnEncoder.NET.ImageSharp.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 - + From 9ed649f2b912f79f9488ba78b0871b238b3485b9 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:28:22 +0200 Subject: [PATCH 02/17] Add back netstandard 2.0 support Co-authored-by: Crsky --- BCnEnc.Net/BCnEncoder.csproj | 10 +- .../Encoder/Bptc/BptcEncodingHelpers.cs | 12 +- BCnEnc.Net/Encoder/LeastSquares.cs | 4 + BCnEnc.Net/Shared/Colors.cs | 2 +- BCnEnc.Net/Shared/HdrImage.cs | 2 +- BCnEnc.Net/Shared/LinearClustering.cs | 8 +- BCnEnc.Net/Shared/NetStdPolyfills.cs | 155 ++++++++++++++++++ BCnEnc.Net/Shared/RawBlocks.cs | 12 +- 8 files changed, 192 insertions(+), 13 deletions(-) create mode 100644 BCnEnc.Net/Shared/NetStdPolyfills.cs diff --git a/BCnEnc.Net/BCnEncoder.csproj b/BCnEnc.Net/BCnEncoder.csproj index 7b60090..84f5a2b 100644 --- a/BCnEnc.Net/BCnEncoder.csproj +++ b/BCnEnc.Net/BCnEncoder.csproj @@ -1,7 +1,8 @@  - netstandard2.1 + netstandard2.1;netstandard2.0 + 8.0 true true snupkg @@ -49,5 +50,12 @@ Supported formats are: + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs index 3f19504..df19f58 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) @@ -52,7 +56,7 @@ public static int[] Rank2SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, int CalculatePartitionError(int partitionIndex) - { + { var error = 0; ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[partitionIndex]; Span subset0 = stackalloc int[numDistinctClusters]; @@ -60,7 +64,7 @@ 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) @@ -122,7 +126,7 @@ 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) 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/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() { From 0eee44ebcb672603c2f74632577e46dfa8102b5d Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:29:10 +0200 Subject: [PATCH 03/17] Add dotnet framework tests --- BCnEnc.Net/AssemblyInfo.cs | 1 + .../Encoder/Bptc/BptcEncodingHelpers.cs | 31 +- BCnEnc.Net/Shared/MipMapper.cs | 20 +- BCnEncNet.sln | 8 + BCnEncTests.Framework/AtcTests.cs | 90 ++++ BCnEncTests.Framework/BC1Tests.cs | 164 ++++++++ .../BCnEncTests.Framework.csproj | 34 ++ BCnEncTests.Framework/Bc5Tests.cs | 83 ++++ BCnEncTests.Framework/Bc6EncoderTests.cs | 398 ++++++++++++++++++ BCnEncTests.Framework/Bc6HDecoderTests.cs | 147 +++++++ BCnEncTests.Framework/Bc7BlockTests.cs | 348 +++++++++++++++ BCnEncTests.Framework/BgraTests.cs | 63 +++ BCnEncTests.Framework/CancellationTests.cs | 21 + BCnEncTests.Framework/ClusterTests.cs | 52 +++ BCnEncTests.Framework/DdsReadTests.cs | 51 +++ BCnEncTests.Framework/DdsWritingTests.cs | 57 +++ BCnEncTests.Framework/DecodingAsyncTests.cs | 62 +++ BCnEncTests.Framework/DecodingTests.cs | 62 +++ BCnEncTests.Framework/EncoderOptionsTests.cs | 66 +++ BCnEncTests.Framework/EncodingAsyncTest.cs | 88 ++++ BCnEncTests.Framework/EncodingTest.cs | 315 ++++++++++++++ BCnEncTests.Framework/HdrImageTests.cs | 26 ++ BCnEncTests.Framework/ProgressTests.cs | 390 +++++++++++++++++ BCnEncTests.Framework/RawTests.cs | 153 +++++++ BCnEncTests.Framework/SingleBlockTests.cs | 143 +++++++ BCnEncTests.Framework/Support/HdrLoader.cs | 15 + BCnEncTests.Framework/Support/ImageLoader.cs | 98 +++++ BCnEncTests.Framework/Support/TestHelper.cs | 258 ++++++++++++ .../BCnEncTests.Shared.projitems | 15 + BCnEncTests.Shared/BCnEncTests.Shared.shproj | 10 + .../BlockTests.cs | 34 +- .../ColorTest.cs | 0 .../IntHelperTests.cs | 0 .../MathHelperTests.cs | 0 .../PcaTests.cs | 0 BCnEncTests/BCnEncTests.csproj | 2 + 36 files changed, 3269 insertions(+), 36 deletions(-) create mode 100644 BCnEncTests.Framework/AtcTests.cs create mode 100644 BCnEncTests.Framework/BC1Tests.cs create mode 100644 BCnEncTests.Framework/BCnEncTests.Framework.csproj create mode 100644 BCnEncTests.Framework/Bc5Tests.cs create mode 100644 BCnEncTests.Framework/Bc6EncoderTests.cs create mode 100644 BCnEncTests.Framework/Bc6HDecoderTests.cs create mode 100644 BCnEncTests.Framework/Bc7BlockTests.cs create mode 100644 BCnEncTests.Framework/BgraTests.cs create mode 100644 BCnEncTests.Framework/CancellationTests.cs create mode 100644 BCnEncTests.Framework/ClusterTests.cs create mode 100644 BCnEncTests.Framework/DdsReadTests.cs create mode 100644 BCnEncTests.Framework/DdsWritingTests.cs create mode 100644 BCnEncTests.Framework/DecodingAsyncTests.cs create mode 100644 BCnEncTests.Framework/DecodingTests.cs create mode 100644 BCnEncTests.Framework/EncoderOptionsTests.cs create mode 100644 BCnEncTests.Framework/EncodingAsyncTest.cs create mode 100644 BCnEncTests.Framework/EncodingTest.cs create mode 100644 BCnEncTests.Framework/HdrImageTests.cs create mode 100644 BCnEncTests.Framework/ProgressTests.cs create mode 100644 BCnEncTests.Framework/RawTests.cs create mode 100644 BCnEncTests.Framework/SingleBlockTests.cs create mode 100644 BCnEncTests.Framework/Support/HdrLoader.cs create mode 100644 BCnEncTests.Framework/Support/ImageLoader.cs create mode 100644 BCnEncTests.Framework/Support/TestHelper.cs create mode 100644 BCnEncTests.Shared/BCnEncTests.Shared.projitems create mode 100644 BCnEncTests.Shared/BCnEncTests.Shared.shproj rename {BCnEncTests => BCnEncTests.Shared}/BlockTests.cs (79%) rename {BCnEncTests => BCnEncTests.Shared}/ColorTest.cs (100%) rename {BCnEncTests => BCnEncTests.Shared}/IntHelperTests.cs (100%) rename {BCnEncTests => BCnEncTests.Shared}/MathHelperTests.cs (100%) rename {BCnEncTests => BCnEncTests.Shared}/PcaTests.cs (100%) 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/Encoder/Bptc/BptcEncodingHelpers.cs b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs index df19f58..7c9c911 100644 --- a/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs +++ b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs @@ -54,6 +54,11 @@ 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. + var indices = new int[16]; + for (var k = 0; k < 16; k++) indices[k] = reducedIndicesBlock[k]; int CalculatePartitionError(int partitionIndex) { @@ -69,7 +74,7 @@ int CalculatePartitionError(int partitionIndex) { if (partitionTable[i] == 0) { - var r = reducedIndicesBlock[i]; + var r = indices[i]; subset0[r]++; var count = subset0[r]; if (count > subset0[max0Idx]) @@ -79,7 +84,7 @@ int CalculatePartitionError(int partitionIndex) } else { - var r = reducedIndicesBlock[i]; + var r = indices[i]; subset1[r]++; var count = subset1[r]; if (count > subset1[max1Idx]) @@ -94,11 +99,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++; } } @@ -114,6 +119,12 @@ 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. + var indices = new int[16]; + for (var k = 0; k < 16; k++) indices[k] = reducedIndicesBlock[k]; + int CalculatePartitionError(int partitionIndex) { var error = 0; @@ -131,7 +142,7 @@ int CalculatePartitionError(int partitionIndex) { if (partitionTable[i] == 0) { - var r = reducedIndicesBlock[i]; + var r = indices[i]; subset0[r]++; var count = subset0[r]; if (count > subset0[max0Idx]) @@ -141,7 +152,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]) @@ -151,7 +162,7 @@ int CalculatePartitionError(int partitionIndex) } else { - var r = reducedIndicesBlock[i]; + var r = indices[i]; subset2[r]++; var count = subset2[r]; if (count > subset2[max2Idx]) @@ -166,15 +177,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/Shared/MipMapper.cs b/BCnEnc.Net/Shared/MipMapper.cs index e44dbf0..746ac29 100644 --- a/BCnEnc.Net/Shared/MipMapper.cs +++ b/BCnEnc.Net/Shared/MipMapper.cs @@ -164,15 +164,15 @@ 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]; + // Use a 1D backing array so TryGetMemory() succeeds on all target frameworks + 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 +186,22 @@ 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]; + // Use a 1D backing array so TryGetMemory() succeeds on all target frameworks + 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 +215,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/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/AtcTests.cs b/BCnEncTests.Framework/AtcTests.cs new file mode 100644 index 0000000..eff342c --- /dev/null +++ b/BCnEncTests.Framework/AtcTests.cs @@ -0,0 +1,90 @@ +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class AtcTests + { + [Fact] + public void AtcKtxDecode() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(CompressionFormat.Atc); + var original = ImageLoader.TestLenna; + + var ktx = encoder.EncodeToKtx(original); + var image = decoder.Decode2D(ktx); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + + [Fact] + public void AtcDdsDecode() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(CompressionFormat.Atc); + var original = ImageLoader.TestLenna; + + var dds = encoder.EncodeToDds(original); + var image = decoder.Decode2D(dds); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + + [Fact] + public void AtcExplicitKtxDecode() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(CompressionFormat.AtcExplicitAlpha); + var original = ImageLoader.TestAlphaGradient1; + + var ktx = encoder.EncodeToKtx(original); + var image = decoder.Decode2D(ktx); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + + [Fact] + public void AtcExplicitDdsDecode() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(CompressionFormat.AtcExplicitAlpha); + var original = ImageLoader.TestAlphaGradient1; + + var dds = encoder.EncodeToDds(original); + var image = decoder.Decode2D(dds); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + + [Fact] + public void AtcInterpolatedKtxDecode() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(CompressionFormat.AtcInterpolatedAlpha); + var original = ImageLoader.TestAlphaGradient1; + + var ktx = encoder.EncodeToKtx(original); + var image = decoder.Decode2D(ktx); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + + [Fact] + public void AtcInterpolatedDdsDecode() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(CompressionFormat.AtcInterpolatedAlpha); + var original = ImageLoader.TestAlphaGradient1; + + var dds = encoder.EncodeToDds(original); + var image = decoder.Decode2D(dds); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + } +} diff --git a/BCnEncTests.Framework/BC1Tests.cs b/BCnEncTests.Framework/BC1Tests.cs new file mode 100644 index 0000000..314c284 --- /dev/null +++ b/BCnEncTests.Framework/BC1Tests.cs @@ -0,0 +1,164 @@ +using BCnEncoder.Shared; +using Xunit; + +namespace BCnEncTests +{ + public class Bc1Tests + { + [Fact] + public void Decode() + { + var block = new Bc1Block + { + color0 = new ColorRgb565(255, 255, 255), + color1 = new ColorRgb565(0, 0, 0) + }; + + Assert.False(block.HasAlphaOrBlack); + block[0] = 1; + block[1] = 1; + block[2] = 1; + block[3] = 1; + + block[4] = 3; + block[5] = 3; + block[6] = 3; + block[7] = 3; + + block[8] = 2; + block[9] = 2; + block[10] = 2; + block[11] = 2; + + block[12] = 0; + block[13] = 0; + block[14] = 0; + block[15] = 0; + + var raw = block.Decode(false); + Assert.Equal(new ColorRgba32(0, 0, 0), raw.p00); + Assert.Equal(new ColorRgba32(0, 0, 0), raw.p10); + Assert.Equal(new ColorRgba32(0, 0, 0), raw.p20); + Assert.Equal(new ColorRgba32(0, 0, 0), raw.p30); + + Assert.Equal(new ColorRgba32(85, 85, 85), raw.p01); + Assert.Equal(new ColorRgba32(85, 85, 85), raw.p11); + Assert.Equal(new ColorRgba32(85, 85, 85), raw.p21); + Assert.Equal(new ColorRgba32(85, 85, 85), raw.p31); + + Assert.Equal(new ColorRgba32(170, 170, 170), raw.p02); + Assert.Equal(new ColorRgba32(170, 170, 170), raw.p12); + Assert.Equal(new ColorRgba32(170, 170, 170), raw.p22); + Assert.Equal(new ColorRgba32(170, 170, 170), raw.p32); + + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p03); + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p13); + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p23); + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p33); + } + + [Fact] + public void DecodeBlack() + { + var block = new Bc1Block + { + color0 = new ColorRgb565(200, 200, 200), + color1 = new ColorRgb565(255, 255, 255) + }; + + Assert.True(block.HasAlphaOrBlack); + block[0] = 0; + block[1] = 0; + block[2] = 0; + block[3] = 0; + + block[4] = 3; + block[5] = 3; + block[6] = 3; + block[7] = 3; + + block[8] = 2; + block[9] = 2; + block[10] = 2; + block[11] = 2; + + block[12] = 1; + block[13] = 1; + block[14] = 1; + block[15] = 1; + + var raw = block.Decode(false); + Assert.Equal(new ColorRgba32(206, 203, 206), raw.p00); + Assert.Equal(new ColorRgba32(206, 203, 206), raw.p10); + 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), raw.p01); + Assert.Equal(new ColorRgba32(0, 0, 0), raw.p11); + Assert.Equal(new ColorRgba32(0, 0, 0), raw.p21); + Assert.Equal(new ColorRgba32(0, 0, 0), raw.p31); + + Assert.Equal(new ColorRgba32(230, 229, 230), raw.p02); + Assert.Equal(new ColorRgba32(230, 229, 230), raw.p12); + Assert.Equal(new ColorRgba32(230, 229, 230), raw.p22); + Assert.Equal(new ColorRgba32(230, 229, 230), raw.p32); + + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p03); + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p13); + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p23); + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p33); + } + + [Fact] + public void DecodeAlpha() + { + var block = new Bc1Block + { + color0 = new ColorRgb565(200, 200, 200), + color1 = new ColorRgb565(255, 255, 255) + }; + + Assert.True(block.HasAlphaOrBlack); + block[0] = 0; + block[1] = 0; + block[2] = 0; + block[3] = 0; + + block[4] = 3; + block[5] = 3; + block[6] = 3; + block[7] = 3; + + block[8] = 2; + block[9] = 2; + block[10] = 2; + block[11] = 2; + + block[12] = 1; + block[13] = 1; + block[14] = 1; + block[15] = 1; + + var raw = block.Decode(true); + Assert.Equal(new ColorRgba32(206, 203, 206), raw.p00); + Assert.Equal(new ColorRgba32(206, 203, 206), raw.p10); + 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(230, 229, 230), raw.p02); + Assert.Equal(new ColorRgba32(230, 229, 230), raw.p12); + Assert.Equal(new ColorRgba32(230, 229, 230), raw.p22); + Assert.Equal(new ColorRgba32(230, 229, 230), raw.p32); + + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p03); + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p13); + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p23); + Assert.Equal(new ColorRgba32(255, 255, 255), raw.p33); + } + } +} diff --git a/BCnEncTests.Framework/BCnEncTests.Framework.csproj b/BCnEncTests.Framework/BCnEncTests.Framework.csproj new file mode 100644 index 0000000..e14d3cc --- /dev/null +++ b/BCnEncTests.Framework/BCnEncTests.Framework.csproj @@ -0,0 +1,34 @@ + + + + net481 + 8.0 + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/BCnEncTests.Framework/Bc5Tests.cs b/BCnEncTests.Framework/Bc5Tests.cs new file mode 100644 index 0000000..4e361ef --- /dev/null +++ b/BCnEncTests.Framework/Bc5Tests.cs @@ -0,0 +1,83 @@ +using BCnEncoder.Decoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using Xunit; + +namespace BCnEncTests +{ + public class Bc5Tests + { + [Fact] + public void Bc5Indices() + { + var block = new Bc5Block(); + + for (var i = 0; i < 16; i++) + { + block.SetRedIndex(i, (byte)(i % 8)); + block.SetGreenIndex(i, (byte)((i + 3) % 8)); + } + + for (var i = 0; i < 16; i++) + { + int rI = block.GetRedIndex(i); + int gI = block.GetGreenIndex(i); + + Assert.Equal((byte)(i % 8), rI); + Assert.Equal((byte)((i + 3) % 8), gI); + } + } + + [Fact] + public void Bc5DdsDecode() + { + var reference = ImageLoader.TestDecodingBc5Reference; + var decoded = new BcDecoder().Decode2D(DdsLoader.TestDecompressBc5); + + var refSpan = TestHelper.GetSinglePixelArrayAsColors(reference); + var decSpan = TestHelper.GetSinglePixelArrayAsColors(decoded); + + Assert.Equal(reference.Width, decoded.Width); + Assert.Equal(reference.Height, decoded.Height); + + // Exactly equal + for (var i = 0; i < reference.Width * reference.Height; i++) + Assert.Equal(refSpan[i], decSpan[i]); + } + + [Fact] + public void Bc5BlockDecode() + { + var block = new Bc5Block() + { + greenBlock = new Bc4ComponentBlock() { componentBlock = 0x91824260008935ee }, + redBlock = new Bc4ComponentBlock() { componentBlock = 0x6d900f66d3c0a70d } + }; + + var referenceBlock = new RawBlock4X4Rgba32 + { + p00 = new ColorRgba32(13, 53, 0, 255), + p01 = new ColorRgba32(136, 238, 0, 255), + p02 = new ColorRgba32(255, 212, 0, 255), + p03 = new ColorRgba32(167, 238, 0, 255), + p10 = new ColorRgba32(13, 53, 0, 255), + p11 = new ColorRgba32(136, 238, 0, 255), + p12 = new ColorRgba32(167, 238, 0, 255), + p13 = new ColorRgba32(75, 185, 0, 255), + p20 = new ColorRgba32(255, 212, 0, 255), + p21 = new ColorRgba32(167, 238, 0, 255), + p22 = new ColorRgba32(13, 53, 0, 255), + p23 = new ColorRgba32(75, 159, 0, 255), + p30 = new ColorRgba32(167, 238, 0, 255), + p31 = new ColorRgba32(75, 185, 0, 255), + p32 = new ColorRgba32(13, 53, 0, 255), + p33 = new ColorRgba32(75, 159, 0, 255) + }; + + var decodedBlock = block.Decode(); + + for (var i = 0; i < 16; i++) + Assert.Equal(referenceBlock[i], decodedBlock[i]); + } + } +} diff --git a/BCnEncTests.Framework/Bc6EncoderTests.cs b/BCnEncTests.Framework/Bc6EncoderTests.cs new file mode 100644 index 0000000..5fb1f5d --- /dev/null +++ b/BCnEncTests.Framework/Bc6EncoderTests.cs @@ -0,0 +1,398 @@ +using System; +using System.IO; +using BCnEncoder.Encoder; +using BCnEncoder.Encoder.Bptc; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; +using Xunit.Abstractions; + +namespace BCnEncTests +{ + public class Bc6EncoderTests + { + private ITestOutputHelper output; + + public Bc6EncoderTests(ITestOutputHelper output) => this.output = output; + + [Theory] + [InlineData(0x7F, 8, false)] + [InlineData(0xFF, 8, false)] + [InlineData(0x7F, 8, true)] + [InlineData(0xFF, 8, true)] + [InlineData(0b111_1100_1101, 11, true)] + [InlineData(0b111_1100_1101, 11, false)] + [InlineData(0b011_1100_1101, 11, true)] + [InlineData(0b011_1100_1101, 11, false)] + [InlineData(0b0_1001, 5, true)] + [InlineData(0b0_1001, 5, false)] + [InlineData(0b1_1100, 5, true)] + [InlineData(0b1_1100, 5, false)] + public void Quantize(int initialQuantizedValue, int endpointBits, bool signed) + { + if (signed) + { + initialQuantizedValue = IntHelper.SignExtend(initialQuantizedValue, endpointBits); + } + 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); + + Assert.InRange(prequantized, unquantized - 2, unquantized + 2); + Assert.InRange(quantized, initialQuantizedValue - 1, initialQuantizedValue + 1); + } + + [Fact] + public void PreQuantizeRawEndpoint() + { + var ep0 = new ColorRgbFloat(1, 0.001f, 0.2f); + + var preQuantized = Bc6EncodingHelpers.PreQuantizeRawEndpoint(ep0, false); + var unQu = Bc6Block.FinishUnQuantize(preQuantized, false); + + Assert.InRange(unQu.Item1, ep0.r - 0.001f, ep0.r + 0.001f); + Assert.InRange(unQu.Item2, ep0.g - 0.001f, ep0.g + 0.001f); + Assert.InRange(unQu.Item3, ep0.b - 0.001f, ep0.b + 0.001f); + } + + [Fact] + public void RgbBoundingBox() + { + var testBlock = new RawBlock4X4RgbFloat() + { + p00 = new ColorRgbFloat(0.01f, -0.05f, 0.02f), + p01 = new ColorRgbFloat(0.02f, 0.02f, 0.03f), + p02 = new ColorRgbFloat(0.02f, 0.02f, 0.02f), + p03 = new ColorRgbFloat(0.01f, 0.02f, 0.02f), + p10 = new ColorRgbFloat(0.02f, 0.02f, 0.02f), + p11 = new ColorRgbFloat(0.045f, 0.02f, 0.02f), + p12 = new ColorRgbFloat(0.04f, 0.02f, 0.02f), + p13 = new ColorRgbFloat(0.04f, 0.02f, 0.02f), + p20 = new ColorRgbFloat(0.21f, 0.02f, 0.02f), + p21 = new ColorRgbFloat(0.01f, 0.02f, 0.02f), + p22 = new ColorRgbFloat(0.01f, 0.32f, 0.02f), + p23 = new ColorRgbFloat(1.01f, 0.22f, 0.02f), + p30 = new ColorRgbFloat(0.01f, 0.12f, 0.02f), + p31 = new ColorRgbFloat(0.01f, 0.02f, 0.02f), + p32 = new ColorRgbFloat(0.01f, 0.62f, 0.7f), + p33 = new ColorRgbFloat(0.01f, 0.02f, 0.02f) + }; + + BCnEncoder.Shared.RgbBoundingBox.CreateFloat(testBlock.AsSpan, out var min, out var max); + + Assert.InRange(min.r, 0.01, 0.02); + Assert.InRange(min.g, -0.05, -0.03); + Assert.InRange(min.b, 0.02, 0.03); + + Assert.InRange(max.r, 0.9, 1.01); + Assert.InRange(max.g, 0.59, 0.62); + Assert.InRange(max.b, 0.60, 0.7); + } + + [Fact] + public void PackMode3() + { + const int endpointPrecision = 10; + var random = new Random(); + + var ep0 = (random.Next() & (1 << endpointPrecision) - 1, + random.Next() & (1 << endpointPrecision) - 1, + random.Next() & (1 << endpointPrecision) - 1); + var ep1 = (random.Next() & (1 << endpointPrecision) - 1, + random.Next() & (1 << endpointPrecision) - 1, + random.Next() & (1 << endpointPrecision) - 1); + + var indices = new byte[16]; + for (var i = 1; i < indices.Length; i++) + { + indices[i] = (byte)random.Next(1 << 4); + } + + var block = Bc6Block.PackType3(ep0, ep1, indices); + + Assert.Equal(Bc6BlockType.Type3, block.Type); + + var extracted0 = block.ExtractEp0(); + var extracted1 = block.ExtractEp1(); + + Assert.Equal(ep0, extracted0); + Assert.Equal(ep1, extracted1); + + for (var i = 0; i < indices.Length; i++) + { + var idx = block.GetColorIndex(1, 0, 4, i); + Assert.Equal(indices[i], idx); + } + } + + [Theory] + [InlineData(Bc6BlockType.Type0)] + [InlineData(Bc6BlockType.Type1)] + [InlineData(Bc6BlockType.Type2)] + [InlineData(Bc6BlockType.Type6)] + [InlineData(Bc6BlockType.Type10)] + [InlineData(Bc6BlockType.Type14)] + [InlineData(Bc6BlockType.Type18)] + [InlineData(Bc6BlockType.Type22)] + [InlineData(Bc6BlockType.Type26)] + [InlineData(Bc6BlockType.Type30)] + [InlineData(Bc6BlockType.Type3)] + [InlineData(Bc6BlockType.Type7)] + [InlineData(Bc6BlockType.Type11)] + //[InlineData(Bc6BlockType.Type15)] //deltabits too small to encode the testblock + internal void EncodeAllModesUnsigned(Bc6BlockType type) + { + var testBlock = new RawBlock4X4RgbFloat() + { + p00 = new ColorRgbFloat(1.011f, 10.01f, 2.01f), + p01 = new ColorRgbFloat(1.01f, 10.014f, 2.012f), + p02 = new ColorRgbFloat(1.005f, 10.012f, 2.02f), + p03 = new ColorRgbFloat(1.01f, 10.013f, 2.023f), + p10 = new ColorRgbFloat(1.011f, 10.01f, 2.01f), + p11 = new ColorRgbFloat(1.01f, 10.014f, 2.012f), + p12 = new ColorRgbFloat(1.005f, 10.012f, 2.02f), + p13 = new ColorRgbFloat(1.01f, 10.013f, 2.023f), + p20 = new ColorRgbFloat(1.011f, 10.01f, 2.01f), + p21 = new ColorRgbFloat(1.01f, 10.014f, 2.012f), + p22 = new ColorRgbFloat(1.005f, 10.012f, 2.02f), + p23 = new ColorRgbFloat(1.01f, 10.013f, 2.023f), + p30 = new ColorRgbFloat(1.011f, 10.01f, 2.01f), + p31 = new ColorRgbFloat(1.01f, 10.014f, 2.012f), + p32 = new ColorRgbFloat(1.005f, 10.012f, 2.02f), + p33 = new ColorRgbFloat(1.01f, 10.013f, 2.023f) + }; + Bc6Block encoded; + var badTransform = false; + + if (type.HasSubsets()) + { + var indexBlock = Bc6Encoder.CreateClusterIndexBlock(testBlock, out var numClusters, 2); + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock, numClusters, true); + + var bestPartition = best2SubsetPartitions[0]; + + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(testBlock, out var ep0, out var ep1, bestPartition, 0); + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(testBlock, out var ep2, out var ep3, bestPartition, 1); + + encoded = Bc6ModeEncoder.EncodeBlock2Sub(type, testBlock, ep0, ep1, ep2, ep3, bestPartition, + false, out badTransform); + } + else + { + BCnEncoder.Shared.RgbBoundingBox.CreateFloat(testBlock.AsSpan, out var min, out var max); + encoded = Bc6ModeEncoder.EncodeBlock1Sub(type, testBlock, min, max, false, + out badTransform); + } + + Assert.False(badTransform); + Assert.Equal(type, encoded.Type); + var decoded = encoded.Decode(false); + var error = testBlock.CalculateError(decoded); + Assert.InRange(error, 0, 0.3); + } + + [Theory] + [InlineData(Bc6BlockType.Type0)] + [InlineData(Bc6BlockType.Type1)] + [InlineData(Bc6BlockType.Type2)] + [InlineData(Bc6BlockType.Type6)] + [InlineData(Bc6BlockType.Type10)] + [InlineData(Bc6BlockType.Type14)] + [InlineData(Bc6BlockType.Type18)] + [InlineData(Bc6BlockType.Type22)] + [InlineData(Bc6BlockType.Type26)] + [InlineData(Bc6BlockType.Type30)] + [InlineData(Bc6BlockType.Type3)] + [InlineData(Bc6BlockType.Type7)] + [InlineData(Bc6BlockType.Type11)] + [InlineData(Bc6BlockType.Type15)] + internal void EncodeAllModesSigned(Bc6BlockType type) + { + var testBlock = new RawBlock4X4RgbFloat() + { + p00 = new ColorRgbFloat(-1.011f, 10.01f, 2.01f), + p01 = new ColorRgbFloat(-1.01f, 10.014f, 2.012f), + p02 = new ColorRgbFloat(-1.005f, 10.012f, 2.02f), + p03 = new ColorRgbFloat(-1.01f, 10.013f, 2.023f), + p10 = new ColorRgbFloat(-1.011f, 10.01f, 2.01f), + p11 = new ColorRgbFloat(-1.01f, 10.014f, 2.012f), + p12 = new ColorRgbFloat(-1.005f, 10.012f, 2.02f), + p13 = new ColorRgbFloat(-1.01f, 10.013f, 2.023f), + p20 = new ColorRgbFloat(-1.011f, 10.01f, 2.01f), + p21 = new ColorRgbFloat(-1.01f, 10.014f, 2.012f), + p22 = new ColorRgbFloat(-1.005f, 10.012f, 2.02f), + p23 = new ColorRgbFloat(-1.01f, 10.013f, 2.023f), + p30 = new ColorRgbFloat(-1.011f, 10.01f, 2.01f), + p31 = new ColorRgbFloat(-1.01f, 10.014f, 2.012f), + p32 = new ColorRgbFloat(-1.005f, 10.012f, 2.02f), + p33 = new ColorRgbFloat(-1.01f, 10.013f, 2.023f) + }; + Bc6Block encoded; + var badTransform = false; + + if (type.HasSubsets()) + { + var indexBlock = Bc6Encoder.CreateClusterIndexBlock(testBlock, out var numClusters, 2); + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock, numClusters, true); + + var bestPartition = best2SubsetPartitions[0]; + + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(testBlock, out var ep0, out var ep1, bestPartition, 0); + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(testBlock, out var ep2, out var ep3, bestPartition, 1); + + encoded = Bc6ModeEncoder.EncodeBlock2Sub(type, testBlock, ep0, ep1, ep2, ep3, bestPartition, + true, out badTransform); + } + else + { + BCnEncoder.Shared.RgbBoundingBox.CreateFloat(testBlock.AsSpan, out var min, out var max); + encoded = Bc6ModeEncoder.EncodeBlock1Sub(type, testBlock, min, max, true, + out badTransform); + } + + Assert.False(badTransform); + Assert.Equal(type, encoded.Type); + var decoded = encoded.Decode(true); + var error = testBlock.CalculateError(decoded); + Assert.InRange(error, 0, 0.5); + } + + [Fact] + 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); + + for (var i = 0; i < blocks.Length; i++) + { + var encoded = Bc6Encoder.Bc6EncoderBalanced.EncodeBlock(blocks[i], signed); + var decoded = encoded.Decode(signed); + + var error = decoded.CalculateError(blocks[i]); + if (error > 0.06 || i == 14749) + { + encoded = Bc6Encoder.Bc6EncoderBalanced.EncodeBlock(blocks[i], signed); + } + } + } + + [Fact] + public void EncodeFast() + { + TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrKiara, CompressionFormat.Bc6U, CompressionQuality.Fast, + "encoding_bc6_kiara_fast.ktx", output); + } + + [Fact] + public void EncodeBalanced() + { + TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrKiara, CompressionFormat.Bc6U, CompressionQuality.Balanced, + "encoding_bc6_kiara_balanced.ktx", output); + } + + [Fact] + public void EncodeBestQuality() + { + TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrKiara, CompressionFormat.Bc6U, CompressionQuality.BestQuality, + "encoding_bc6_kiara_bestquality.ktx", output); + } + + [Fact] + public void EncodeProbeFast() + { + TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6U, CompressionQuality.Fast, + "encoding_bc6_probe_fast.ktx", output); + } + + [Fact] + public void EncodeProbeBalanced() + { + TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6U, CompressionQuality.Balanced, + "encoding_bc6_probe_balanced.ktx", output); + } + + [Fact] + public void EncodeProbeBestQuality() + { + TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6U, CompressionQuality.BestQuality, + "encoding_bc6_probe_bestquality.ktx", output); + } + + [Fact] + public void EncodeProbeSignedFast() + { + TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6S, CompressionQuality.Fast, + "encoding_bc6_probe_signed_fast.ktx", output); + } + + [Fact] + public void EncodeProbeSignedBalanced() + { + TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6S, CompressionQuality.Balanced, + "encoding_bc6_probe_signed_balanced.ktx", output); + } + + [Fact] + public void EncodeProbeSignedBestQuality() + { + TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6S, CompressionQuality.BestQuality, + "encoding_bc6_probe_signed_bestquality.ktx", output); + } + + [Fact] + public void EncodeToKtx() + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = CompressionFormat.Bc6U; + + var ktx = encoder.EncodeToKtxHdr(HdrLoader.TestHdrKiara.PixelMemory); + + using var fs = File.OpenWrite("encoding_bc6_ktx.ktx"); + ktx.Write(fs); + } + + [Fact] + public void EncodeToDds() + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = CompressionFormat.Bc6U; + + var dds = encoder.EncodeToDdsHdr(HdrLoader.TestHdrKiara.PixelMemory); + + using var fs = File.OpenWrite("encoding_bc6_dds.dds"); + dds.Write(fs); + } + + [Fact] + public void EncodeToRaw() + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = CompressionFormat.Bc6U; + + var ktx = encoder.EncodeToKtxHdr(HdrLoader.TestHdrKiara.PixelMemory); + + var allMips = encoder.EncodeToRawBytesHdr(HdrLoader.TestHdrKiara.PixelMemory); + + Assert.True(allMips.Length > 1); + Assert.True(allMips.Length == ktx.MipMaps.Count); + + for (var i = 0; i < allMips.Length; i++) + { + var single = encoder.EncodeToRawBytesHdr(HdrLoader.TestHdrKiara.PixelMemory, i, out var mW, out var mH); + + Assert.Equal(ktx.MipMaps[i].Faces[0].Data, single); + Assert.Equal(allMips[i], single); + } + } + } +} diff --git a/BCnEncTests.Framework/Bc6HDecoderTests.cs b/BCnEncTests.Framework/Bc6HDecoderTests.cs new file mode 100644 index 0000000..0453379 --- /dev/null +++ b/BCnEncTests.Framework/Bc6HDecoderTests.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; +using Xunit.Abstractions; +using Half = BCnEncoder.Shared.Half; + +namespace BCnEncTests +{ + public class Bc6HDecoderTests + { + private ITestOutputHelper output; + + public Bc6HDecoderTests(ITestOutputHelper output) => this.output = output; + + [Fact] + public void DecodeDds() + { + var decoder = new BcDecoder(); + var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraDds); + + var hdr = new HdrImage((int)HdrLoader.TestHdrKiaraDds.header.dwWidth, + (int)HdrLoader.TestHdrKiaraDds.header.dwHeight); + + Assert.Equal(hdr.pixels.Length, decoded.Length); + + hdr.pixels = decoded; + using var sfs = File.OpenWrite("decoding_test_dds_bc6h.hdr"); + hdr.Write(sfs); + + TestHelper.AssertPixelsEqual(HdrLoader.TestHdrKiara.pixels, decoded, CompressionQuality.Fast, output); + } + + [Fact] + public void DecodeKtx() + { + var decoder = new BcDecoder(); + var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraKtx); + + var hdr = new HdrImage((int)HdrLoader.TestHdrKiaraKtx.header.PixelWidth, + (int)HdrLoader.TestHdrKiaraKtx.header.PixelHeight); + + Assert.Equal(hdr.pixels.Length, decoded.Length); + + hdr.pixels = decoded; + using var sfs = File.OpenWrite("decoding_test_ktx_bc6h.hdr"); + hdr.Write(sfs); + + TestHelper.AssertPixelsEqual(HdrLoader.TestHdrKiara.pixels, decoded, CompressionQuality.BestQuality, output); + } + + // TestHdrKiaraDds includes some blocks with all modes. + // The test_hdr_kiara_dds_float16_data.bin file contains Half-float values decoded with a reference decoder. + // This test ensures that decoding is bit-exact. + [Fact] + public void AllBlocksDecodesExact() + { + var decoder = new BcDecoder(); + var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraDds); + + using var fs = File.OpenRead("../../../../BCnEncTests/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++) + { + 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); + } + } + + // Test that above statement holds true. And that modes are recognized correctly + [Fact] + public void DecodeModes() + { + var modes = new int[31]; + + var hdr = HdrLoader.TestHdrKiaraDds; + + var bytes = hdr.Faces[0].MipMaps[0].Data; + var blocks = MemoryMarshal.Cast(bytes); + + for (var i = 0; i < blocks.Length; i++) + { + var block = blocks[i]; + var mode = block.Type; + modes[(int)mode]++; + } + + for (var i = 0; i < modes.Length; i++) + { + output.WriteLine($"Mode {i}: {modes[i]}"); + } + + Assert.True(modes[0] > 0); + Assert.True(modes[1] > 0); + Assert.True(modes[2] > 0); + Assert.True(modes[6] > 0); + Assert.True(modes[10] > 0); + Assert.True(modes[14] > 0); + Assert.True(modes[18] > 0); + Assert.True(modes[22] > 0); + Assert.True(modes[26] > 0); + Assert.True(modes[30] > 0); + Assert.True(modes[3] > 0); + Assert.True(modes[7] > 0); + Assert.True(modes[11] > 0); + Assert.True(modes[15] > 0); + } + + [Fact] + public void DecodeErrorBlock() + { + var decoder = new BcDecoder(); + + var width = 16; + var height = 16; + var bufferSize = decoder.GetBlockSize(CompressionFormat.Bc6U) * width * height; + + var buffer = new byte[bufferSize]; + 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); + } + } +} diff --git a/BCnEncTests.Framework/Bc7BlockTests.cs b/BCnEncTests.Framework/Bc7BlockTests.cs new file mode 100644 index 0000000..c356fb7 --- /dev/null +++ b/BCnEncTests.Framework/Bc7BlockTests.cs @@ -0,0 +1,348 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using BCnEncoder.Decoder; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class Bc7BlockTests + { + [Fact] + public void PackTypes() + { + var numWidthBlocks = 4; + var numHeightBlocks = 2; + var outputBlocks = new Bc7Block[64 * numWidthBlocks * numHeightBlocks]; + var encoded = new byte[64 * numWidthBlocks * numHeightBlocks * Unsafe.SizeOf()]; + + var output = new KtxFile( + KtxHeader.InitializeCompressed(numWidthBlocks * 8 * 4, numHeightBlocks * 8 * 4, + GlInternalFormat.GlCompressedRgbaBptcUnormArb, + GlFormat.GlRgba)); + + var type0 = new Span(outputBlocks, 0, 64); + Type0Pack(type0); + PlaceBlock(0, 0, type0, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); + + var type1 = new Span(outputBlocks, 64 * 1, 64); + Type1Pack(type1); + PlaceBlock(1, 0, type1, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); + + var type2 = new Span(outputBlocks, 64 * 2, 64); + Type2Pack(type2); + PlaceBlock(2, 0, type2, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); + + var type3 = new Span(outputBlocks, 64 * 3, 64); + Type3Pack(type3); + PlaceBlock(3, 0, type3, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); + + var type4 = new Span(outputBlocks, 64 * 4, 64); + Type4Pack(type4); + PlaceBlock(0, 1, type4, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); + + var type5 = new Span(outputBlocks, 64 * 5, 64); + Type5Pack(type5); + PlaceBlock(1, 1, type5, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); + + var type6 = new Span(outputBlocks, 64 * 6, 64); + Type6Pack(type6); + PlaceBlock(2, 1, type6, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); + + var type7 = new Span(outputBlocks, 64 * 7, 64); + Type7Pack(type7); + PlaceBlock(3, 1, type7, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); + + output.MipMaps.Add(new KtxMipmap((uint)encoded.Length, + (uint)(8 * 4 * numWidthBlocks), + (uint)(8 * 4 * numHeightBlocks), 1)); + output.MipMaps[0].Faces[0] = new KtxMipFace(encoded, + (uint)(8 * 4 * numWidthBlocks), + (uint)(8 * 4 * numHeightBlocks)); + + var fs = File.OpenWrite("bc7_blocktests.ktx"); + output.Write(fs); + } + + [Fact] + public void DecodeErrorBlock() + { + var decoder = new BcDecoder(); + + var width = 32; + var height = 32; + var bufferSize = decoder.GetBlockSize(CompressionFormat.Bc7) * width * height; + + var buffer = new byte[bufferSize]; + var r = new Random(50); + r.NextBytes(buffer); + + var pixels = decoder.DecodeRaw(buffer, width * 4, height * 4, CompressionFormat.Bc7); + Assert.Contains(new ColorRgba32(255, 0, 255), pixels); + + var decoded = pixels.AsMemory().AsMemory2D(height * 4, width * 4); + using var fs = File.OpenWrite("test_decode_bc7_error.png"); + TestHelper.SaveAsPng(decoded, fs); + } + + #region Type Packs + + private void Type0Pack(Span output) + { + var subsetEndpoints = new[] { + new byte[]{0b1111, 0, 0}, + new byte[]{0b1000, 0, 0}, + new byte[]{0, 0b1111, 0}, + new byte[]{0, 0b1000, 0}, + new byte[]{0, 0, 0b1111}, + new byte[]{0, 0, 0b1000} + }; + var pBits = new byte[] { 1, 0, 1, 0, 1, 1 }; + var indices = new byte[] { + 0, 1, 2, 3, + 0, 1, 2, 3, + 3, 2, 1, 0, + 3, 2, 1, 0 + }; + for (var i = 0; i < 16; i++) + { + output[i].PackType0(i, subsetEndpoints, pBits, indices); + Assert.Equal(Bc7BlockType.Type0, output[i].Type); + Assert.Equal(i, output[i].PartitionSetId); + } + + 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); + Assert.Equal(Bc7BlockType.Type0, output[i].Type); + Assert.Equal(i, output[i].PartitionSetId); + } + + 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); + Assert.Equal(Bc7BlockType.Type0, output[i].Type); + Assert.Equal(i, output[i].PartitionSetId); + } + + 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); + Assert.Equal(Bc7BlockType.Type0, output[i].Type); + Assert.Equal(i, output[i].PartitionSetId); + } + } + + private void Type1Pack(Span output) + { + var subsetEndpoints = new[] { + new byte[]{0b111111, 0b0100, 0}, + new byte[]{0b0100, 0b111111, 0}, + new byte[]{0, 0, 0b111111}, + new byte[]{0, 0, 0b1010} + }; + var pBits = new byte[] { 1, 1 }; + var indices = new byte[] { + 0, 1, 2, 3, + 0, 1, 2, 3, + 3, 2, 1, 0, + 3, 2, 1, 0 + }; + for (var i = 0; i < 64; i++) + { + output[i].PackType1(i, subsetEndpoints, pBits, indices); + Assert.Equal(Bc7BlockType.Type1, output[i].Type); + Assert.Equal(i, output[i].PartitionSetId); + } + } + + private void Type2Pack(Span output) + { + var subsetEndpoints = new[] { + new byte[]{0b11111, 0, 0}, + new byte[]{0b01000, 0, 0}, + new byte[]{0, 0b11111, 0}, + new byte[]{0, 0b01000, 0}, + new byte[]{0, 0, 0b11111}, + new byte[]{0, 0, 0b01000} + }; + var indices = new byte[] { + 0, 1, 2, 3, + 0, 1, 2, 3, + 3, 2, 1, 0, + 3, 2, 1, 0 + }; + for (var i = 0; i < 64; i++) + { + output[i].PackType2(i, subsetEndpoints, indices); + Assert.Equal(Bc7BlockType.Type2, output[i].Type); + Assert.Equal(i, output[i].PartitionSetId); + } + } + + private void Type3Pack(Span output) + { + var subsetEndpoints = new[] { + new byte[]{0b1111111, 0b10100, 0}, + new byte[]{0b10100, 0b1111111, 0}, + new byte[]{0, 0, 0b1111111}, + new byte[]{0, 0, 0b11010} + }; + var pBits = new byte[] { 1, 0, 0, 1 }; + var indices = new byte[] { + 0, 1, 2, 3, + 0, 1, 2, 3, + 3, 2, 1, 0, + 3, 2, 1, 0 + }; + for (var i = 0; i < 64; i++) + { + output[i].PackType3(i, subsetEndpoints, pBits, indices); + Assert.Equal(Bc7BlockType.Type3, output[i].Type); + Assert.Equal(i, output[i].PartitionSetId); + } + } + + private void Type4Pack(Span output) + { + var colorEndpoints = new[] { + new byte[]{0b11111, 0, 0b0100}, + new byte[]{0, 0b11111, 0b0100} + }; + 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, + 7, 6, 5, 4, + 3, 2, 1, 0 + }; + var rotation = 0; + byte idxMode = 0; + for (var i = 0; i < 64; i++) + { + rotation = (rotation + 1) % 4; + idxMode = (byte)((idxMode + 1) % 2); + output[i].PackType4(rotation, idxMode, colorEndpoints, alphaEndPoints, indices2Bit, indices3Bit); + Assert.Equal(Bc7BlockType.Type4, output[i].Type); + Assert.Equal(rotation, output[i].RotationBits); + Assert.Equal(idxMode, output[i].Type4IndexMode); + } + } + + private void Type5Pack(Span output) + { + var colorEndpoints = new[] { + new byte[]{0b1111111, 0b0100, 0}, + new byte[]{0, 0, 0b1111111} + }; + var alphaEndPoints = new byte[] { 255, 100 }; + var colorIndices = new byte[] { + 0, 1, 2, 3, + 0, 1, 2, 3, + 3, 2, 1, 0, + 3, 2, 1, 0 + }; + var alphaIndices = new byte[] { + 1, 2, 3, 0, + 0, 1, 2, 3, + 2, 3, 0, 1, + 3, 2, 1, 0 + }; + var rotation = 0; + for (var i = 0; i < 64; i++) + { + rotation = (rotation + 1) % 4; + output[i].PackType5(rotation, colorEndpoints, alphaEndPoints, colorIndices, alphaIndices); + Assert.Equal(Bc7BlockType.Type5, output[i].Type); + Assert.Equal(rotation, output[i].RotationBits); + } + } + + private void Type6Pack(Span output) + { + var colorEndpoints = new[] { + new byte[]{0b1111111, 0b0100, 0, 0b1111111}, + new byte[]{0, 0, 0b1111111, 0b1111} + }; + var pBits = new byte[] { 1, 0 }; + var colorIndices = new byte[] { + 0, 1, 2, 3, + 4, 5, 6, 7, + 8, 9, 10, 11, + 12, 13, 14, 15 + }; + for (var i = 0; i < 64; i++) + { + output[i].PackType6(colorEndpoints, pBits, colorIndices); + Assert.Equal(Bc7BlockType.Type6, output[i].Type); + } + } + + private void Type7Pack(Span output) + { + var colorEndpoints = new[] { + new byte[]{0b11111, 0b0100, 0, 0b11111}, + new byte[]{0, 0b11111, 0, 0b111}, + new byte[]{0b11111, 0b0100, 0, 0b1111}, + new byte[]{0b10100, 0, 0b11111, 0b11111} + }; + var pBits = new byte[] { 1, 0, 1, 0 }; + var indices = new byte[] { + 0, 1, 2, 3, + 0, 1, 2, 3, + 3, 2, 1, 0, + 3, 2, 1, 0 + }; + for (var i = 0; i < 64; i++) + { + output[i].PackType7(i, colorEndpoints, pBits, indices); + Assert.Equal(Bc7BlockType.Type7, output[i].Type); + Assert.Equal(i, output[i].PartitionSetId); + } + } + + private void PlaceBlock(int x, int y, Span data, Span destination, int destBlockWidth) + { + for (var i = 0; i < 8; i++) + { + var xStart = x * 8 + (y * 8 + i) * 8 * destBlockWidth; + var row = data.Slice(i * 8, 8); + row.CopyTo(destination.Slice(xStart, 8)); + } + } + + #endregion + } +} diff --git a/BCnEncTests.Framework/BgraTests.cs b/BCnEncTests.Framework/BgraTests.cs new file mode 100644 index 0000000..6f0e8bf --- /dev/null +++ b/BCnEncTests.Framework/BgraTests.cs @@ -0,0 +1,63 @@ +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using Xunit; + +namespace BCnEncTests +{ + public class BgraTests + { + [Fact] + public void BgraDdsDecode() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(CompressionFormat.Bgra); + var original = ImageLoader.TestLenna; + + var dds = encoder.EncodeToDds(original); + var image = decoder.Decode2D(dds); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + + [Fact] + public void BgraAlphaDdsDecode() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(CompressionFormat.Bgra); + var original = ImageLoader.TestAlphaGradient1; + + var dds = encoder.EncodeToDds(original); + var image = decoder.Decode2D(dds); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + + [Fact] + public void BgraKtxDecode() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(CompressionFormat.Bgra); + var original = ImageLoader.TestLenna; + + var ktx = encoder.EncodeToKtx(original); + var image = decoder.Decode2D(ktx); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + + [Fact] + public void BgraAlphaKtxDecode() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(CompressionFormat.Bgra); + var original = ImageLoader.TestAlphaGradient1; + + var ktx = encoder.EncodeToKtx(original); + var image = decoder.Decode2D(ktx); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + } +} diff --git a/BCnEncTests.Framework/CancellationTests.cs b/BCnEncTests.Framework/CancellationTests.cs new file mode 100644 index 0000000..9932c6c --- /dev/null +++ b/BCnEncTests.Framework/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.Framework/ClusterTests.cs b/BCnEncTests.Framework/ClusterTests.cs new file mode 100644 index 0000000..c141928 --- /dev/null +++ b/BCnEncTests.Framework/ClusterTests.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using BCnEncoder.Shared; +using BCnEncTests.Support; +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 = pixels.AsMemory().AsMemory2D(height, width); + using var fs = File.OpenWrite("test_cluster.png"); + TestHelper.SaveAsPng(result, fs); + } + } +} diff --git a/BCnEncTests.Framework/DdsReadTests.cs b/BCnEncTests.Framework/DdsReadTests.cs new file mode 100644 index 0000000..90c9279 --- /dev/null +++ b/BCnEncTests.Framework/DdsReadTests.cs @@ -0,0 +1,51 @@ +using System.IO; +using BCnEncoder.Decoder; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using BCnEncTests.Support; +using Xunit; + +namespace BCnEncTests +{ + public class DdsReadTests + { + [Fact] + public void ReadRgba() + { + TestHelper.ExecuteDdsReadingTest(DdsLoader.TestDecompressRgba, DxgiFormat.DxgiFormatR8G8B8A8Unorm, "decoding_test_dds_rgba_mip{0}.png"); + } + + [Fact] + public void ReadBc1() + { + TestHelper.ExecuteDdsReadingTest(DdsLoader.TestDecompressBc1, DxgiFormat.DxgiFormatBc1Unorm, "decoding_test_dds_bc1_mip{0}.png"); + } + + [Fact] + public void ReadBc1A() + { + TestHelper.ExecuteDdsReadingTest(DdsLoader.TestDecompressBc1A, DxgiFormat.DxgiFormatBc1Unorm, "decoding_test_dds_bc1a_mip{0}.png"); + } + + [Fact] + public void ReadBc7() + { + TestHelper.ExecuteDdsReadingTest(DdsLoader.TestDecompressBc7, DxgiFormat.DxgiFormatUnknown, "decoding_test_dds_bc7_mip{0}.png"); + } + + [Fact] + public void ReadFromStream() + { + using var fs = File.OpenRead(DdsLoader.TestDecompressBc1Name); + + 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.Framework/DdsWritingTests.cs b/BCnEncTests.Framework/DdsWritingTests.cs new file mode 100644 index 0000000..44a0616 --- /dev/null +++ b/BCnEncTests.Framework/DdsWritingTests.cs @@ -0,0 +1,57 @@ +using BCnEncoder.Shared; +using BCnEncTests.Support; +using Xunit; + +namespace BCnEncTests +{ + public class DdsWritingTests + { + [Fact] + public void DdsWriteRgba() + { + TestHelper.ExecuteDdsWritingTest(ImageLoader.TestLenna, CompressionFormat.Rgba, "encoding_dds_rgba.dds"); + } + + [Fact] + public void DdsWriteBc1() + { + TestHelper.ExecuteDdsWritingTest(ImageLoader.TestLenna, CompressionFormat.Bc1, "encoding_dds_bc1.dds"); + } + + [Fact] + public void DdsWriteBc2() + { + TestHelper.ExecuteDdsWritingTest(ImageLoader.TestAlpha1, CompressionFormat.Bc2, "encoding_dds_bc2.dds"); + } + + [Fact] + public void DdsWriteBc3() + { + TestHelper.ExecuteDdsWritingTest(ImageLoader.TestAlpha1, CompressionFormat.Bc3, "encoding_dds_bc3.dds"); + } + + [Fact] + public void DdsWriteBc4() + { + TestHelper.ExecuteDdsWritingTest(ImageLoader.TestHeight1, CompressionFormat.Bc4, "encoding_dds_bc4.dds"); + } + + [Fact] + public void DdsWriteBc5() + { + TestHelper.ExecuteDdsWritingTest(ImageLoader.TestRedGreen1, CompressionFormat.Bc5, "encoding_dds_bc5.dds"); + } + + [Fact] + public void DdsWriteBc7() + { + TestHelper.ExecuteDdsWritingTest(ImageLoader.TestLenna, CompressionFormat.Bc7, "encoding_dds_bc7.dds"); + } + + [Fact] + public void DdsWriteCubemap() + { + TestHelper.ExecuteDdsWritingTest(ImageLoader.TestCubemap, CompressionFormat.Bc1, "encoding_dds_cubemap_bc1.dds"); + } + } +} diff --git a/BCnEncTests.Framework/DecodingAsyncTests.cs b/BCnEncTests.Framework/DecodingAsyncTests.cs new file mode 100644 index 0000000..da61e5e --- /dev/null +++ b/BCnEncTests.Framework/DecodingAsyncTests.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using Xunit; + +namespace BCnEncTests +{ + public class DecodingAsyncTests + { + [Fact] + public async Task DecodeAsync() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(); + var original = ImageLoader.TestGradient1; + + var file = encoder.EncodeToKtx(original); + var image = await decoder.Decode2DAsync(file); + + TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeAllMipMapsAsync() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(); + var original = ImageLoader.TestGradient1; + + var file = encoder.EncodeToKtx(original); + var images = await decoder.DecodeAllMipMaps2DAsync(file); + + TestHelper.AssertImagesEqual(original, images[0], encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeRawAsync() + { + var decoder = new BcDecoder(); + var encoder = new BcEncoder(); + var original = ImageLoader.TestGradient1; + + var file = encoder.EncodeToKtx(original); + + var rawBytes = file.MipMaps[0].Faces[0].Data; + var mipWidth = (int)file.MipMaps[0].Width; + var mipHeight = (int)file.MipMaps[0].Height; + + var decoded = await Task.Run(() => + { + var pixels = decoder.DecodeRaw(rawBytes, mipWidth, mipHeight, CompressionFormat.Bc1); + return pixels.AsMemory().AsMemory2D(mipHeight, mipWidth); + }); + + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + } +} diff --git a/BCnEncTests.Framework/DecodingTests.cs b/BCnEncTests.Framework/DecodingTests.cs new file mode 100644 index 0000000..c2fc08c --- /dev/null +++ b/BCnEncTests.Framework/DecodingTests.cs @@ -0,0 +1,62 @@ +using BCnEncTests.Support; +using Xunit; + +namespace BCnEncTests +{ + public class DecodingTests + { + [Fact] + public void Bc1Decode() + { + TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc1, "decoding_test_bc1.png"); + } + + [Fact] + public void Bc1AlphaDecode() + { + TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc1A, "decoding_test_bc1a.png"); + } + + [Fact] + public void Bc2Decode() + { + TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc2, "decoding_test_bc2.png"); + } + + [Fact] + public void Bc3Decode() + { + TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc3, "decoding_test_bc3.png"); + } + + [Fact] + public void Bc4Decode() + { + TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc4Unorm, "decoding_test_bc4.png"); + } + + [Fact] + public void Bc5Decode() + { + TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc5Unorm, "decoding_test_bc5.png"); + } + + [Fact] + public void Bc7DecodeRgb() + { + TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc7Rgb, "decoding_test_bc7_rgb.png"); + } + + [Fact] + public void Bc7DecodeUnorm() + { + TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc7Unorm, "decoding_test_bc7_unorm.png"); + } + + [Fact] + public void Bc7DecodeEveryBlockType() + { + TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc7Types, "decoding_test_bc7_types.png"); + } + } +} diff --git a/BCnEncTests.Framework/EncoderOptionsTests.cs b/BCnEncTests.Framework/EncoderOptionsTests.cs new file mode 100644 index 0000000..f73ecd3 --- /dev/null +++ b/BCnEncTests.Framework/EncoderOptionsTests.cs @@ -0,0 +1,66 @@ +using BCnEncoder.Encoder; +using BCnEncTests.Support; +using Xunit; + +namespace BCnEncTests +{ + public class EncoderOptionsTests + { + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + [InlineData(10)] + public void MaxMipMaps(int requestedMipMaps) + { + var testImage = ImageLoader.TestBlur1; + var encoder = new BcEncoder() + { + OutputOptions = + { + GenerateMipMaps = true, + MaxMipMapLevel = requestedMipMaps + } + }; + + Assert.Equal(requestedMipMaps, encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height)); + + var ktx = encoder.EncodeToKtx(testImage); + + Assert.Equal(requestedMipMaps, (int)ktx.header.NumberOfMipmapLevels); + Assert.Equal(requestedMipMaps, ktx.MipMaps.Count); + + var dds = encoder.EncodeToDds(testImage); + + Assert.Equal(requestedMipMaps, (int)dds.header.dwMipMapCount); + Assert.Equal(requestedMipMaps, dds.Faces[0].MipMaps.Length); + } + + [Fact] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2013:Do not use equality check to check for collection size.", Justification = "")] + public void GenerateMipMaps() + { + var testImage = ImageLoader.TestBlur1; + const int requestedMipMaps = 1; + var encoder = new BcEncoder() + { + OutputOptions = + { + GenerateMipMaps = false + } + }; + + Assert.Equal(requestedMipMaps, encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height)); + + var ktx = encoder.EncodeToKtx(testImage); + + Assert.Equal(requestedMipMaps, (int)ktx.header.NumberOfMipmapLevels); + Assert.Equal(requestedMipMaps, (int)ktx.MipMaps.Count); + + var dds = encoder.EncodeToDds(testImage); + + Assert.Equal(requestedMipMaps, (int)dds.header.dwMipMapCount); + Assert.Equal(requestedMipMaps, dds.Faces[0].MipMaps.Length); + } + } +} diff --git a/BCnEncTests.Framework/EncodingAsyncTest.cs b/BCnEncTests.Framework/EncodingAsyncTest.cs new file mode 100644 index 0000000..a936b7d --- /dev/null +++ b/BCnEncTests.Framework/EncodingAsyncTest.cs @@ -0,0 +1,88 @@ +using System; +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 EncodingAsyncTest + { + private readonly BcEncoder encoder; + private readonly BcDecoder decoder; + private readonly Memory2D originalImage; + private readonly Memory2D[] originalCubeMap; + + public EncodingAsyncTest() + { + encoder = new BcEncoder(); + decoder = new BcDecoder(); + originalImage = ImageLoader.TestGradient1; + originalCubeMap = ImageLoader.TestCubemap; + } + + [Fact] + public async Task EncodeToDdsAsync() + { + var file = await encoder.EncodeToDdsAsync(originalImage); + var image = decoder.Decode2D(file); + + TestHelper.AssertImagesEqual(originalImage, image, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task EncodeToKtxAsync() + { + var file = await encoder.EncodeToKtxAsync(originalImage); + var image = decoder.Decode2D(file); + + TestHelper.AssertImagesEqual(originalImage, image, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task EncodeCubemapToDdsAsync() + { + var file = await encoder.EncodeCubeMapToDdsAsync(originalCubeMap[0], originalCubeMap[1], originalCubeMap[2], + originalCubeMap[3], originalCubeMap[4], originalCubeMap[5]); + + for (var i = 0; i < 6; i++) + { + var rawPixels = decoder.DecodeRaw(file.Faces[i].MipMaps[0].Data, + (int)file.Faces[i].Width, (int)file.Faces[i].Height, CompressionFormat.Bc1); + var decodedImage = rawPixels.AsMemory().AsMemory2D((int)file.Faces[i].Height, (int)file.Faces[i].Width); + + TestHelper.AssertImagesEqual(originalCubeMap[i], decodedImage, encoder.OutputOptions.Quality); + } + } + + [Fact] + public async Task EncodeCubemapToKtxAsync() + { + var file = await encoder.EncodeCubeMapToKtxAsync(originalCubeMap[0], originalCubeMap[1], originalCubeMap[2], + originalCubeMap[3], originalCubeMap[4], originalCubeMap[5]); + + for (var i = 0; i < 6; i++) + { + 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 = rawPixels.AsMemory().AsMemory2D( + (int)file.MipMaps[0].Faces[i].Height, (int)file.MipMaps[0].Faces[i].Width); + + TestHelper.AssertImagesEqual(originalCubeMap[i], decodedImage, encoder.OutputOptions.Quality); + } + } + + [Fact] + public async Task EncodeToRawBytesAsync() + { + var data = await encoder.EncodeToRawBytesAsync(originalImage); + var rawPixels = decoder.DecodeRaw(data[0], originalImage.Width, originalImage.Height, CompressionFormat.Bc1); + var image = rawPixels.AsMemory().AsMemory2D(originalImage.Height, originalImage.Width); + + TestHelper.AssertImagesEqual(originalImage, image, encoder.OutputOptions.Quality); + } + } +} diff --git a/BCnEncTests.Framework/EncodingTest.cs b/BCnEncTests.Framework/EncodingTest.cs new file mode 100644 index 0000000..055de03 --- /dev/null +++ b/BCnEncTests.Framework/EncodingTest.cs @@ -0,0 +1,315 @@ +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.Framework/HdrImageTests.cs b/BCnEncTests.Framework/HdrImageTests.cs new file mode 100644 index 0000000..eda8e56 --- /dev/null +++ b/BCnEncTests.Framework/HdrImageTests.cs @@ -0,0 +1,26 @@ +using System.IO; +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); + + // Save as HDR to verify round-trip + var hdrOut = new HdrImage(new Span2D(hdrImg.pixels, hdrImg.height, hdrImg.width)); + using var outStream = File.OpenWrite("test_hdr_load.hdr"); + hdrOut.Write(outStream); + } + } +} diff --git a/BCnEncTests.Framework/ProgressTests.cs b/BCnEncTests.Framework/ProgressTests.cs new file mode 100644 index 0000000..c82777b --- /dev/null +++ b/BCnEncTests.Framework/ProgressTests.cs @@ -0,0 +1,390 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; +using Xunit.Abstractions; + +namespace BCnEncTests +{ + public class ProgressTests + { + private ITestOutputHelper output; + + public ProgressTests(ITestOutputHelper output) => this.output = output; + + private async Task ExecuteEncodeProgressReport(BcEncoder encoder, Memory2D testImage, int expectedTotalBlocks) + { + var lastProgress = new ProgressElement(0, 1); + + var processedBlocks = 0; + encoder.Options.Progress = new SynchronousProgress(element => + { + output.WriteLine($"Progress = {element.CurrentBlock} / {element.TotalBlocks}"); + + Assert.Equal(++processedBlocks, element.CurrentBlock); + lastProgress = element; + }); + + using var ms = new MemoryStream(); + await encoder.EncodeToStreamAsync(testImage, ms); + + output.WriteLine("LastProgress = " + lastProgress); + + Assert.Equal(expectedTotalBlocks, lastProgress.TotalBlocks); + Assert.Equal(1, lastProgress.Percentage); + } + + private async Task ExecuteDecodeProgressReport(BcDecoder decoder, KtxFile image, int expectedTotalBlocks, bool singleMip) + { + var lastProgress = new ProgressElement(0, 1); + + var processedBlocks = 0; + decoder.Options.Progress = new SynchronousProgress(element => + { + output.WriteLine($"Progress = {element.CurrentBlock} / {element.TotalBlocks}"); + + Assert.Equal(++processedBlocks, element.CurrentBlock); + lastProgress = element; + }); + + if (singleMip) + { + await decoder.DecodeAsync(image); + } + else + { + await decoder.DecodeAllMipMapsAsync(image); + } + + output.WriteLine("LastProgress = " + lastProgress); + + Assert.Equal(expectedTotalBlocks, lastProgress.TotalBlocks); + Assert.Equal(1, lastProgress.Percentage); + } + + private async Task ExecuteDecodeProgressReport(BcDecoder decoder, DdsFile image, int expectedTotalBlocks, bool singleMip) + { + var lastProgress = new ProgressElement(0, 1); + + var processedBlocks = 0; + decoder.Options.Progress = new SynchronousProgress(element => + { + output.WriteLine($"Progress = {element.CurrentBlock} / {element.TotalBlocks}"); + + Assert.Equal(++processedBlocks, element.CurrentBlock); + lastProgress = element; + }); + + if (singleMip) + { + await decoder.DecodeAsync(image); + } + else + { + await decoder.DecodeAllMipMapsAsync(image); + } + + output.WriteLine("LastProgress = " + lastProgress); + + Assert.Equal(expectedTotalBlocks, lastProgress.TotalBlocks); + Assert.Equal(1, lastProgress.Percentage); + } + + [Fact] + public async Task DecodeProgressReportParallel() + { + var testImage = KtxLoader.TestDecompressBc1; + var decoder = new BcDecoder + { + Options = { IsParallel = true } + }; + + var expectedTotal = 0; + + for (var i = 0; i < testImage.header.NumberOfMipmapLevels; i++) + { + expectedTotal += decoder.GetBlockCount((int)testImage.MipMaps[i].Width, (int)testImage.MipMaps[i].Height); + } + + await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, false); + } + + [Fact] + public async Task DecodeProgressReportNonParallel() + { + var testImage = KtxLoader.TestDecompressBc1; + var decoder = new BcDecoder + { + Options = { IsParallel = false } + }; + + var expectedTotal = 0; + + for (var i = 0; i < testImage.header.NumberOfMipmapLevels; i++) + { + expectedTotal += decoder.GetBlockCount((int)testImage.MipMaps[i].Width, (int)testImage.MipMaps[i].Height); + } + + await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, false); + } + + [Fact] + public async Task DecodeProgressReportParallelOneMip() + { + var testImage = KtxLoader.TestDecompressBc1; + var decoder = new BcDecoder + { + Options = { IsParallel = true } + }; + + var expectedTotal = decoder.GetBlockCount((int)testImage.MipMaps[0].Width, (int)testImage.MipMaps[0].Height); + + await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, true); + } + + [Fact] + public async Task DecodeProgressReportNonParallelOneMip() + { + var testImage = KtxLoader.TestDecompressBc1; + var decoder = new BcDecoder + { + Options = { IsParallel = false } + }; + + var expectedTotal = decoder.GetBlockCount((int)testImage.MipMaps[0].Width, (int)testImage.MipMaps[0].Height); + + await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, true); + } + + [Fact] + public async Task DecodeProgressReportParallelDds() + { + var testImage = DdsLoader.TestDecompressBc1; + var decoder = new BcDecoder + { + Options = { IsParallel = true } + }; + + var expectedTotal = 0; + + for (var i = 0; i < testImage.header.dwMipMapCount; i++) + { + expectedTotal += decoder.GetBlockCount((int)testImage.Faces[0].MipMaps[i].Width, (int)testImage.Faces[0].MipMaps[i].Height); + } + + await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, false); + } + + [Fact] + public async Task DecodeProgressReportNonParallelDds() + { + var testImage = DdsLoader.TestDecompressBc1; + var decoder = new BcDecoder + { + Options = { IsParallel = false } + }; + + var expectedTotal = 0; + + for (var i = 0; i < testImage.header.dwMipMapCount; i++) + { + expectedTotal += decoder.GetBlockCount((int)testImage.Faces[0].MipMaps[i].Width, (int)testImage.Faces[0].MipMaps[i].Height); + } + + await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, false); + } + + [Fact] + public async Task DecodeProgressReportParallelOneMipDds() + { + var testImage = DdsLoader.TestDecompressBc1; + var decoder = new BcDecoder + { + Options = { IsParallel = true } + }; + + var expectedTotal = decoder.GetBlockCount((int)testImage.Faces[0].MipMaps[0].Width, (int)testImage.Faces[0].MipMaps[0].Height); + + await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, true); + } + + [Fact] + public async Task DecodeProgressReportNonParallelOneMipDds() + { + var testImage = DdsLoader.TestDecompressBc1; + var decoder = new BcDecoder + { + Options = { IsParallel = false } + }; + + var expectedTotal = decoder.GetBlockCount((int)testImage.Faces[0].MipMaps[0].Width, (int)testImage.Faces[0].MipMaps[0].Height); + + await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, true); + } + + [Fact] + public async Task EncodeProgressReportParallelKtx() + { + var testImage = ImageLoader.TestBlur1; + var encoder = new BcEncoder + { + Options = { IsParallel = true }, + OutputOptions = { FileFormat = OutputFileFormat.Ktx } + }; + + var expectedTotal = 0; + + for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height); i++) + { + encoder.CalculateMipMapSize(testImage.Width, testImage.Height, i, out var mW, out var mH); + expectedTotal += encoder.GetBlockCount(mW, mH); + } + + await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); + } + + [Fact] + public async Task EncodeProgressReportNonParallelKtx() + { + var testImage = ImageLoader.TestBlur1; + var encoder = new BcEncoder + { + Options = { IsParallel = false }, + OutputOptions = { FileFormat = OutputFileFormat.Ktx } + }; + + var expectedTotal = 0; + + for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height); i++) + { + encoder.CalculateMipMapSize(testImage.Width, testImage.Height, i, out var mW, out var mH); + expectedTotal += encoder.GetBlockCount(mW, mH); + } + + await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); + } + + [Fact] + public async Task EncodeProgressReportParallelOneMipKtx() + { + var testImage = ImageLoader.TestBlur1; + var encoder = new BcEncoder + { + Options = { IsParallel = true }, + OutputOptions = { MaxMipMapLevel = 1, FileFormat = OutputFileFormat.Ktx } + }; + + var expectedTotal = encoder.GetBlockCount(testImage.Width, testImage.Height); + + await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); + } + + [Fact] + public async Task EncodeProgressReportNonParallelOneMipKtx() + { + var testImage = ImageLoader.TestBlur1; + var encoder = new BcEncoder + { + Options = { IsParallel = false }, + OutputOptions = { MaxMipMapLevel = 1, FileFormat = OutputFileFormat.Ktx } + }; + + var expectedTotal = encoder.GetBlockCount(testImage.Width, testImage.Height); + + await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); + } + + [Fact] + public async Task EncodeProgressReportParallelDds() + { + var testImage = ImageLoader.TestBlur1; + var encoder = new BcEncoder + { + Options = { IsParallel = true }, + OutputOptions = { FileFormat = OutputFileFormat.Dds } + }; + + var expectedTotal = 0; + + for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height); i++) + { + encoder.CalculateMipMapSize(testImage.Width, testImage.Height, i, out var mW, out var mH); + expectedTotal += encoder.GetBlockCount(mW, mH); + } + + await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); + } + + [Fact] + public async Task EncodeProgressReportNonParallelDds() + { + var testImage = ImageLoader.TestBlur1; + var encoder = new BcEncoder + { + Options = { IsParallel = false }, + OutputOptions = { FileFormat = OutputFileFormat.Dds } + }; + + var expectedTotal = 0; + + for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage.Width, testImage.Height); i++) + { + encoder.CalculateMipMapSize(testImage.Width, testImage.Height, i, out var mW, out var mH); + expectedTotal += encoder.GetBlockCount(mW, mH); + } + + await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); + } + + [Fact] + public async Task EncodeProgressReportParallelOneMipDds() + { + var testImage = ImageLoader.TestBlur1; + var encoder = new BcEncoder + { + Options = { IsParallel = true }, + OutputOptions = { MaxMipMapLevel = 1, FileFormat = OutputFileFormat.Dds } + }; + + var expectedTotal = encoder.GetBlockCount(testImage.Width, testImage.Height); + + await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); + } + + [Fact] + public async Task EncodeProgressReportNonParallelOneMipDds() + { + var testImage = ImageLoader.TestBlur1; + var encoder = new BcEncoder + { + Options = { IsParallel = false }, + OutputOptions = { MaxMipMapLevel = 1, FileFormat = OutputFileFormat.Dds } + }; + + var expectedTotal = encoder.GetBlockCount(testImage.Width, testImage.Height); + + await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); + } + } + + internal class SynchronousProgress : IProgress + { + private readonly Action handler; + + public SynchronousProgress(Action handler) + { + this.handler = handler; + } + + public void Report(T value) + { + handler(value); + } + } +} diff --git a/BCnEncTests.Framework/RawTests.cs b/BCnEncTests.Framework/RawTests.cs new file mode 100644 index 0000000..d2a572a --- /dev/null +++ b/BCnEncTests.Framework/RawTests.cs @@ -0,0 +1,153 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +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 = decodedPixels.AsMemory().AsMemory2D(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 = rawPixels.AsMemory().AsMemory2D(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); + + for (var i = 0; i < mipLevels; i++) + { + encoder.CalculateMipMapSize(inputImage.Width, inputImage.Height, i, out var mipWidth, out var mipHeight); + var resized = ResizeImage(inputImage, mipWidth, 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 = rawPixels.AsMemory().AsMemory2D(mipHeight, mipWidth); + + var originalColors = TestHelper.GetSinglePixelArrayAsColors(resized); + 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 ResizeImage(Memory2D image, int newWidth, int newHeight) + { + int numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + foreach (var memory2D in chain) + { + if (memory2D.Width == newWidth && memory2D.Height == newHeight) + { + return memory2D; + } + } + throw new InvalidOperationException("Cannot resize image to non-mip dimensions"); + } + + private static unsafe Bitmap ToBitmap(Memory2D image) + { + var bmp = new Bitmap(image.Width, image.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + var data = bmp.LockBits(new Rectangle(0, 0, image.Width, image.Height), + ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + byte* ptr = (byte*)data.Scan0; + var span = image.Span; + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.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); + return bmp; + } + } +} diff --git a/BCnEncTests.Framework/SingleBlockTests.cs b/BCnEncTests.Framework/SingleBlockTests.cs new file mode 100644 index 0000000..74691ed --- /dev/null +++ b/BCnEncTests.Framework/SingleBlockTests.cs @@ -0,0 +1,143 @@ +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 SingleBlockTests + { + [Theory] + [InlineData(CompressionFormat.Bc1, CompressionQuality.Fast)] + [InlineData(CompressionFormat.Bc1, CompressionQuality.Balanced)] + [InlineData(CompressionFormat.Bc3, CompressionQuality.Fast)] + [InlineData(CompressionFormat.Bc3, CompressionQuality.Balanced)] + [InlineData(CompressionFormat.Bc7, CompressionQuality.Fast)] + public void SingleBlockEncodeDecodeStream(CompressionFormat format, CompressionQuality quality) + { + var testImage = ImageLoader.TestAlpha1; + int height = testImage.Height; + int width = testImage.Width; + + var encoder = new BcEncoder() + { + OutputOptions = + { + Format = format, + Quality = quality + } + }; + + var colors = testImage.Span; + + var ms = new MemoryStream(); + + for (var y = 0; y < height; y += 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(width, height)); + + ms.Position = 0; + + var decoder = new BcDecoder(); + + var decoded = new ColorRgba32[height, width]; + + for (var y = 0; y < height; y += 4) + { + for (var x = 0; x < width; x += 4) + { + decoder.DecodeBlock(ms, format, + decoded.AsSpan2D().Slice(y, x, 4, 4)); + } + } + + var oPixels = TestHelper.GetSinglePixelArrayAsColors(testImage); + var dPixels = FlattenDecoded(decoded, height, width); + var psnr = ImageQuality.PeakSignalToNoiseRatio(oPixels, dPixels, + format != CompressionFormat.Bc1); + + TestHelper.AssertPSNR(psnr, quality); + } + + [Theory] + [InlineData(CompressionFormat.Bc1, CompressionQuality.Fast)] + [InlineData(CompressionFormat.Bc1, CompressionQuality.Balanced)] + [InlineData(CompressionFormat.Bc3, CompressionQuality.Fast)] + [InlineData(CompressionFormat.Bc3, CompressionQuality.Balanced)] + [InlineData(CompressionFormat.Bc7, CompressionQuality.Fast)] + public void SingleBlockEncodeDecode(CompressionFormat format, CompressionQuality quality) + { + var testImage = ImageLoader.TestAlpha1; + int height = testImage.Height; + int width = testImage.Width; + + var encoder = new BcEncoder() + { + OutputOptions = + { + Format = format, + Quality = quality + } + }; + + var colors = testImage.Span; + + var encMs = new MemoryStream(); + + for (var y = 0; y < height; y += 4) + { + for (var x = 0; x < width; x += 4) + { + encoder.EncodeBlock(colors.Slice(y, x, 4, 4), encMs); + } + } + + var buffer = encMs.ToArray(); + + var decoder = new BcDecoder(); + + var decoded = new ColorRgba32[height, width]; + + var blockIndex = 0; + for (var y = 0; y < height; y += 4) + { + for (var x = 0; x < width; x += 4) + { + decoder.DecodeBlock( + new Span(buffer, + blockIndex * decoder.GetBlockSize(format), + decoder.GetBlockSize(format)), + format, + decoded.AsSpan2D().Slice(y, x, 4, 4)); + blockIndex++; + } + } + + 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.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..2dc9958 --- /dev/null +++ b/BCnEncTests.Framework/Support/ImageLoader.cs @@ -0,0 +1,98 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; + +namespace BCnEncTests.Support +{ + public static class ImageLoader + { + public static Memory2D TestDiffuse1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_diffuse_1_512.jpg"); + public static Memory2D TestBlur1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_blur_1_512.jpg"); + public static Memory2D TestNormal1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_normal_1_512.jpg"); + public static Memory2D TestHeight1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_height_1_512.jpg"); + public static Memory2D TestGradient1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_gradient_1_512.jpg"); + + public static Memory2D TestTransparentSprite1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_transparent.png"); + public static Memory2D TestAlphaGradient1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_alphagradient_1_512.png"); + public static Memory2D TestAlpha1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_alpha_1_512.png"); + public static Memory2D TestRedGreen1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_red_green_1_64.png"); + public static Memory2D TestRgbHard1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_rgb_hard_1.png"); + public static Memory2D TestLenna { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_lenna_512.png"); + public static Memory2D TestDecodingBc5Reference { get; } = LoadTestImage("../../../../BCnEncTests/testImages/decoding_dds_bc5_reference.png"); + + public static Memory2D[] TestCubemap { get; } = { + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/right.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/left.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/top.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/bottom.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/back.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/forward.png") + }; + + internal static Memory2D LoadTestImage(string filename) + { + using var bmp = new Bitmap(filename); + return FromBitmap(bmp); + } + + internal static unsafe Memory2D FromBitmap(Bitmap bmp) + { + var pixels = new ColorRgba32[bmp.Width * bmp.Height]; + var data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), + ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + byte* ptr = (byte*)data.Scan0; + for (int i = 0; i < pixels.Length; i++) + { + // GDI+ Format32bppArgb memory order: B,G,R,A + pixels[i] = new ColorRgba32(ptr[2], ptr[1], ptr[0], ptr[3]); + ptr += 4; + } + bmp.UnlockBits(data); + return pixels.AsMemory().AsMemory2D(bmp.Height, bmp.Width); + } + } + + public static class DdsLoader + { + public const string TestDecompressBc1Name = "../../../../BCnEncTests/testImages/test_decompress_bc1.dds"; + public const string TestDecompressBc1AName = "../../../../BCnEncTests/testImages/test_decompress_bc1a.dds"; + public const string TestDecompressBc7Name = "../../../../BCnEncTests/testImages/test_decompress_bc7.dds"; + public const string TestDecompressBc5Name = "../../../../BCnEncTests/testImages/decoding_dds_bc5.dds"; + public const string TestDecompressRgbaName = "../../../../BCnEncTests/testImages/test_decompress_rgba.dds"; + + public static DdsFile TestDecompressBc1 { get; } = LoadDdsFile(TestDecompressBc1Name); + public static DdsFile TestDecompressBc1A { get; } = LoadDdsFile(TestDecompressBc1AName); + public static DdsFile TestDecompressBc5 { get; } = LoadDdsFile(TestDecompressBc5Name); + public static DdsFile TestDecompressBc7 { get; } = LoadDdsFile(TestDecompressBc7Name); + public static DdsFile TestDecompressRgba { get; } = LoadDdsFile(TestDecompressRgbaName); + + internal static DdsFile LoadDdsFile(string filename) + { + using var fs = File.OpenRead(filename); + return DdsFile.Load(fs); + } + } + + public static class KtxLoader + { + public static KtxFile TestDecompressBc1 { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc1.ktx"); + public static KtxFile TestDecompressBc1A { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc1a.ktx"); + public static KtxFile TestDecompressBc2 { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc2.ktx"); + public static KtxFile TestDecompressBc3 { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc3.ktx"); + public static KtxFile TestDecompressBc4Unorm { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc4_unorm.ktx"); + public static KtxFile TestDecompressBc5Unorm { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc5_unorm.ktx"); + public static KtxFile TestDecompressBc7Rgb { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc7_rgb.ktx"); + public static KtxFile TestDecompressBc7Types { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc7_types.ktx"); + public static KtxFile TestDecompressBc7Unorm { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc7_unorm.ktx"); + + internal static KtxFile LoadKtxFile(string filename) + { + using var fs = File.OpenRead(filename); + return KtxFile.Load(fs); + } + } +} diff --git a/BCnEncTests.Framework/Support/TestHelper.cs b/BCnEncTests.Framework/Support/TestHelper.cs new file mode 100644 index 0000000..0a9bcd1 --- /dev/null +++ b/BCnEncTests.Framework/Support/TestHelper.cs @@ -0,0 +1,258 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; +using Xunit; +using Xunit.Abstractions; + +namespace BCnEncTests.Support +{ + public static class TestHelper + { + #region Assertions + + public static void AssertPixelsEqual(Span originalPixels, Span pixels, CompressionQuality quality, ITestOutputHelper output = null) + { + var psnr = ImageQuality.PeakSignalToNoiseRatio(originalPixels, pixels); + AssertPSNR(psnr, quality, output); + } + + public static void AssertPixelsEqual(Span originalPixels, Span pixels, CompressionQuality quality, ITestOutputHelper output = null) + { + var rmse = ImageQuality.CalculateLogRMSE(originalPixels, pixels); + AssertRMSE(rmse, quality, output); + } + + public static void AssertImagesEqual(Memory2D original, Memory2D image, CompressionQuality quality, bool countAlpha = true) + { + var psnr = CalculatePSNR(original, image, countAlpha); + AssertPSNR(psnr, quality); + } + + #endregion + + #region Execute methods + + public static void ExecuteDecodingTest(KtxFile file, string outputFile) + { + Assert.True(file.header.VerifyHeader()); + Assert.Equal((uint)1, file.header.NumberOfFaces); + + var decoder = new BcDecoder(); + var pixels = decoder.Decode2D(file); + + Assert.Equal((uint)pixels.Width, file.header.PixelWidth); + Assert.Equal((uint)pixels.Height, file.header.PixelHeight); + + using var outFs = File.OpenWrite(outputFile); + SaveAsPng(pixels, outFs); + } + + #region Dds + + public static void ExecuteDdsWritingTest(Memory2D image, CompressionFormat format, string outputFile) + { + ExecuteDdsWritingTest(new[] { image }, format, outputFile); + } + + public static void ExecuteDdsWritingTest(Memory2D[] images, CompressionFormat format, string outputFile) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + encoder.OutputOptions.FileFormat = OutputFileFormat.Dds; + + using var fs = File.OpenWrite(outputFile); + + if (images.Length == 1) + { + encoder.EncodeToStream(images[0], fs); + } + else + { + encoder.EncodeCubeMapToStream(images[0], images[1], images[2], images[3], images[4], images[5], fs); + } + } + + public static void ExecuteDdsReadingTest(DdsFile file, DxgiFormat format, string outputFile, bool assertAlpha = false) + { + Assert.Equal(format, file.header.ddsPixelFormat.DxgiFormat); + Assert.Equal(file.header.dwMipMapCount, (uint)file.Faces[0].MipMaps.Length); + + var decoder = new BcDecoder(); + decoder.InputOptions.DdsBc1ExpectAlpha = assertAlpha; + var images = decoder.DecodeAllMipMaps2D(file); + + Assert.Equal((uint)images[0].Width, file.header.dwWidth); + Assert.Equal((uint)images[0].Height, file.header.dwHeight); + + for (var i = 0; i < images.Length; i++) + { + if (assertAlpha) + { + var pixels = GetSinglePixelArrayAsColors(images[0]); + Assert.Contains(pixels, x => x.a == 0); + } + + using var outFs = File.OpenWrite(string.Format(outputFile, i)); + SaveAsPng(images[i], outFs); + } + } + + #endregion + + #region Cancellation + + public static async Task ExecuteCancellationTest(Memory2D image, bool isParallel) + { + var encoder = new BcEncoder(CompressionFormat.Bc7); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.Options.IsParallel = isParallel; + + var source = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); + await Assert.ThrowsAnyAsync(() => + encoder.EncodeToRawBytesAsync(image, source.Token)); + } + + #endregion + + #endregion + + public static float DecodeKtxCheckPSNR(string filename, Memory2D original) + { + using var fs = File.OpenRead(filename); + var ktx = KtxFile.Load(fs); + var decoder = new BcDecoder() + { + OutputOptions = { Bc4Component = ColorComponent.Luminance } + }; + var decoded = decoder.Decode2D(ktx); + + return CalculatePSNR(original, decoded); + } + + public static float DecodeKtxCheckRMSEHdr(string filename, HdrImage original) + { + using var fs = File.OpenRead(filename); + var ktx = KtxFile.Load(fs); + var decoder = new BcDecoder(); + + var decoded = decoder.DecodeHdr(ktx); + + return ImageQuality.CalculateLogRMSE(original.pixels, decoded); + } + + public static void ExecuteEncodingTest(Memory2D image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = quality; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + + var fs = File.OpenWrite(filename); + encoder.EncodeToStream(image, fs); + fs.Close(); + + var psnr = DecodeKtxCheckPSNR(filename, image); + output.WriteLine("RGBA PSNR: " + psnr + "db"); + AssertPSNR(psnr, encoder.OutputOptions.Quality); + } + + public static void ExecuteHdrEncodingTest(HdrImage image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = quality; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + + var fs = File.OpenWrite(filename); + encoder.EncodeToStreamHdr(image.pixels.AsMemory().AsMemory2D(image.height, image.width), fs); + fs.Close(); + + var rmse = DecodeKtxCheckRMSEHdr(filename, image); + output.WriteLine("RGBFloat RMSE: " + rmse); + AssertRMSE(rmse, encoder.OutputOptions.Quality); + } + + private static float CalculatePSNR(Memory2D original, Memory2D decoded, bool countAlpha = true) + { + var pixels = GetSinglePixelArrayAsColors(original); + var pixels2 = GetSinglePixelArrayAsColors(decoded); + + return ImageQuality.PeakSignalToNoiseRatio(pixels, pixels2, countAlpha); + } + + public static void AssertPSNR(float psnr, CompressionQuality quality, ITestOutputHelper output = null) + { + output?.WriteLine($"PSNR: {psnr} , quality: {quality}"); + if (quality == CompressionQuality.Fast) + { + Assert.True(psnr > 25, $"PSNR was less than 25: {psnr} , quality: {quality}"); + } + else + { + Assert.True(psnr > 30, $"PSNR was less than 30: {psnr} , quality: {quality}"); + } + } + + public static void AssertRMSE(float rmse, CompressionQuality quality, ITestOutputHelper output = null) + { + output?.WriteLine($"RMSE: {rmse} , quality: {quality}"); + if (quality == CompressionQuality.Fast) + { + Assert.True(rmse < 0.1); + } + else + { + Assert.True(rmse < 0.04); + } + } + + public static ColorRgba32[] GetSinglePixelArrayAsColors(ReadOnlyMemory2D image) + { + var pixels = new ColorRgba32[image.Width * image.Height]; + var span = image.Span; + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + pixels[y * image.Width + x] = span[y, x]; + } + } + return pixels; + } + + public static unsafe void SaveAsPng(Memory2D image, Stream stream) + { + int width = image.Width; + int height = image.Height; + using var bmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + var data = bmp.LockBits(new Rectangle(0, 0, width, height), + ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + byte* ptr = (byte*)data.Scan0; + var span = image.Span; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + var c = span[y, x]; + ptr[0] = c.b; + ptr[1] = c.g; + ptr[2] = c.r; + ptr[3] = c.a; + ptr += 4; + } + } + bmp.UnlockBits(data); + bmp.Save(stream, ImageFormat.Png); + } + } +} diff --git a/BCnEncTests.Shared/BCnEncTests.Shared.projitems b/BCnEncTests.Shared/BCnEncTests.Shared.projitems new file mode 100644 index 0000000..f25f88b --- /dev/null +++ b/BCnEncTests.Shared/BCnEncTests.Shared.projitems @@ -0,0 +1,15 @@ + + + + $(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/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/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/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/BCnEncTests.csproj b/BCnEncTests/BCnEncTests.csproj index 7dab56d..fabc4d1 100644 --- a/BCnEncTests/BCnEncTests.csproj +++ b/BCnEncTests/BCnEncTests.csproj @@ -24,4 +24,6 @@ + + From 339582ac14db22468bee2ad3d5447e614185a262 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:49:35 +0200 Subject: [PATCH 04/17] Change test runner to windows --- .github/workflows/dotnetcore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 476d4267de19c7e311b6007206bdc017f99487d0 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:43:18 +0200 Subject: [PATCH 05/17] Fix bug with decoder not decoding all mipmaps on some methods --- BCnEnc.Net/Decoder/BcDecoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); } /// From ae862fbf17bdadafd0665a8312590de77b7f07ca Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:43:27 +0200 Subject: [PATCH 06/17] Remove extra comment --- BCnEnc.Net/Shared/MipMapper.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/BCnEnc.Net/Shared/MipMapper.cs b/BCnEnc.Net/Shared/MipMapper.cs index 746ac29..ddbcea7 100644 --- a/BCnEnc.Net/Shared/MipMapper.cs +++ b/BCnEnc.Net/Shared/MipMapper.cs @@ -171,7 +171,6 @@ private static Memory2D ResizeToHalf(ReadOnlySpan2D pi var newWidth = Math.Max(1, oldWidth >> 1); var newHeight = Math.Max(1, oldHeight >> 1); - // Use a 1D backing array so TryGetMemory() succeeds on all target frameworks var result = new ColorRgba32[newHeight * newWidth]; int ClampW(int x) => Math.Max(0, Math.Min(oldWidth - 1, x)); @@ -200,7 +199,6 @@ private static Memory2D ResizeToHalf(ReadOnlySpan2D> 1); var newHeight = Math.Max(1, oldHeight >> 1); - // Use a 1D backing array so TryGetMemory() succeeds on all target frameworks var result = new ColorRgbFloat[newHeight * newWidth]; int ClampW(int x) => Math.Max(0, Math.Min(oldWidth - 1, x)); From 6eb5f4b0b9611cb8dc935a5aefb4d0e36bf59cc0 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:10:50 +0200 Subject: [PATCH 07/17] Move most tests to shared test project --- BCnEncTests.Framework/MipMapperTests.cs | 83 +++ BCnEncTests.Framework/Support/TestHelper.cs | 15 + .../AtcTests.cs | 0 .../BC1Tests.cs | 0 .../BCnEncTests.Shared.projitems | 19 + .../Bc5Tests.cs | 0 .../Bc6EncoderTests.cs | 2 +- .../Bc6HDecoderTests.cs | 20 +- .../Bc7BlockTests.cs | 5 +- .../BgraTests.cs | 0 .../CancellationTests.cs | 0 .../ClusterTests.cs | 3 +- .../DdsReadTests.cs | 0 .../DdsWritingTests.cs | 0 .../DecodingAsyncTests.cs | 3 +- .../DecodingTests.cs | 0 .../EncoderOptionsTests.cs | 0 .../EncodingAsyncTest.cs | 6 +- .../EncodingTest.cs | 0 .../ProgressTests.cs | 0 .../RawTests.cs | 48 +- .../SingleBlockTests.cs | 0 BCnEncTests/AtcTests.cs | 108 ---- BCnEncTests/BC1Tests.cs | 164 ------ BCnEncTests/BCnEncTests.csproj | 1 + BCnEncTests/Bc5Tests.cs | 84 --- BCnEncTests/Bc6EncoderTests.cs | 405 -------------- BCnEncTests/Bc6HDecoderTests.cs | 178 ------- BCnEncTests/Bc7BlockTests.cs | 389 -------------- BCnEncTests/BgraTests.cs | 78 --- BCnEncTests/CancellationTests.cs | 28 - BCnEncTests/ClusterTests.cs | 52 -- BCnEncTests/DdsReadTests.cs | 54 -- BCnEncTests/DdsWritingTests.cs | 57 -- BCnEncTests/DecodingAsyncTests.cs | 62 --- BCnEncTests/DecodingTests.cs | 62 --- BCnEncTests/EncodeByteOrderTests.cs | 2 +- BCnEncTests/EncoderOptionsTests.cs | 71 --- BCnEncTests/EncodingAsyncTest.cs | 88 --- BCnEncTests/EncodingTest.cs | 501 ------------------ BCnEncTests/ProgressTests.cs | 392 -------------- BCnEncTests/RawTests.cs | 106 ---- BCnEncTests/SingleBlockTests.cs | 143 ----- BCnEncTests/Support/HdrLoader.cs | 2 +- BCnEncTests/Support/ImageLoader.cs | 47 +- BCnEncTests/Support/TestHelper.cs | 107 +++- 46 files changed, 269 insertions(+), 3116 deletions(-) create mode 100644 BCnEncTests.Framework/MipMapperTests.cs rename {BCnEncTests.Framework => BCnEncTests.Shared}/AtcTests.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/BC1Tests.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/Bc5Tests.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/Bc6EncoderTests.cs (98%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/Bc6HDecoderTests.cs (84%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/Bc7BlockTests.cs (99%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/BgraTests.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/CancellationTests.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/ClusterTests.cs (91%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/DdsReadTests.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/DdsWritingTests.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/DecodingAsyncTests.cs (93%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/DecodingTests.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/EncoderOptionsTests.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/EncodingAsyncTest.cs (90%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/EncodingTest.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/ProgressTests.cs (100%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/RawTests.cs (70%) rename {BCnEncTests.Framework => BCnEncTests.Shared}/SingleBlockTests.cs (100%) delete mode 100644 BCnEncTests/AtcTests.cs delete mode 100644 BCnEncTests/BC1Tests.cs delete mode 100644 BCnEncTests/Bc5Tests.cs delete mode 100644 BCnEncTests/Bc6EncoderTests.cs delete mode 100644 BCnEncTests/Bc6HDecoderTests.cs delete mode 100644 BCnEncTests/Bc7BlockTests.cs delete mode 100644 BCnEncTests/BgraTests.cs delete mode 100644 BCnEncTests/CancellationTests.cs delete mode 100644 BCnEncTests/ClusterTests.cs delete mode 100644 BCnEncTests/DdsReadTests.cs delete mode 100644 BCnEncTests/DdsWritingTests.cs delete mode 100644 BCnEncTests/DecodingAsyncTests.cs delete mode 100644 BCnEncTests/DecodingTests.cs delete mode 100644 BCnEncTests/EncoderOptionsTests.cs delete mode 100644 BCnEncTests/EncodingAsyncTest.cs delete mode 100644 BCnEncTests/EncodingTest.cs delete mode 100644 BCnEncTests/ProgressTests.cs delete mode 100644 BCnEncTests/RawTests.cs delete mode 100644 BCnEncTests/SingleBlockTests.cs 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/TestHelper.cs b/BCnEncTests.Framework/Support/TestHelper.cs index 0a9bcd1..e5e2280 100644 --- a/BCnEncTests.Framework/Support/TestHelper.cs +++ b/BCnEncTests.Framework/Support/TestHelper.cs @@ -254,5 +254,20 @@ public static unsafe void SaveAsPng(Memory2D image, Stream stream) 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.Framework/AtcTests.cs b/BCnEncTests.Shared/AtcTests.cs similarity index 100% rename from BCnEncTests.Framework/AtcTests.cs rename to BCnEncTests.Shared/AtcTests.cs diff --git a/BCnEncTests.Framework/BC1Tests.cs b/BCnEncTests.Shared/BC1Tests.cs similarity index 100% rename from BCnEncTests.Framework/BC1Tests.cs rename to BCnEncTests.Shared/BC1Tests.cs diff --git a/BCnEncTests.Shared/BCnEncTests.Shared.projitems b/BCnEncTests.Shared/BCnEncTests.Shared.projitems index f25f88b..a221845 100644 --- a/BCnEncTests.Shared/BCnEncTests.Shared.projitems +++ b/BCnEncTests.Shared/BCnEncTests.Shared.projitems @@ -6,10 +6,29 @@ D954291E-2A0B-460D-934E-DC6B0785DB48 + + + + + + + + + + + + + + + + + + + diff --git a/BCnEncTests.Framework/Bc5Tests.cs b/BCnEncTests.Shared/Bc5Tests.cs similarity index 100% rename from BCnEncTests.Framework/Bc5Tests.cs rename to BCnEncTests.Shared/Bc5Tests.cs diff --git a/BCnEncTests.Framework/Bc6EncoderTests.cs b/BCnEncTests.Shared/Bc6EncoderTests.cs similarity index 98% rename from BCnEncTests.Framework/Bc6EncoderTests.cs rename to BCnEncTests.Shared/Bc6EncoderTests.cs index 5fb1f5d..6a5bcad 100644 --- a/BCnEncTests.Framework/Bc6EncoderTests.cs +++ b/BCnEncTests.Shared/Bc6EncoderTests.cs @@ -265,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++) { diff --git a/BCnEncTests.Framework/Bc6HDecoderTests.cs b/BCnEncTests.Shared/Bc6HDecoderTests.cs similarity index 84% rename from BCnEncTests.Framework/Bc6HDecoderTests.cs rename to BCnEncTests.Shared/Bc6HDecoderTests.cs index 0453379..75aa7c3 100644 --- a/BCnEncTests.Framework/Bc6HDecoderTests.cs +++ b/BCnEncTests.Shared/Bc6HDecoderTests.cs @@ -24,8 +24,9 @@ public void DecodeDds() var decoder = new BcDecoder(); var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraDds); - 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); @@ -33,6 +34,9 @@ public void DecodeDds() using var sfs = File.OpenWrite("decoding_test_dds_bc6h.hdr"); hdr.Write(sfs); + 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); } @@ -42,8 +46,9 @@ public void DecodeKtx() var decoder = new BcDecoder(); var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraKtx); - 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); @@ -51,6 +56,9 @@ public void DecodeKtx() using var sfs = File.OpenWrite("decoding_test_ktx_bc6h.hdr"); hdr.Write(sfs); + 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); } @@ -63,7 +71,11 @@ public void AllBlocksDecodesExact() var decoder = new BcDecoder(); var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraDds); +#if NETCOREAPP + using var fs = File.OpenRead("../../../testImages/test_hdr_kiara_dds_float16_data.bin"); +#else using var fs = File.OpenRead("../../../../BCnEncTests/testImages/test_hdr_kiara_dds_float16_data.bin"); +#endif using var ms = new MemoryStream(); fs.CopyTo(ms); var length = (int)ms.Position; diff --git a/BCnEncTests.Framework/Bc7BlockTests.cs b/BCnEncTests.Shared/Bc7BlockTests.cs similarity index 99% rename from BCnEncTests.Framework/Bc7BlockTests.cs rename to BCnEncTests.Shared/Bc7BlockTests.cs index c356fb7..a2904da 100644 --- a/BCnEncTests.Framework/Bc7BlockTests.cs +++ b/BCnEncTests.Shared/Bc7BlockTests.cs @@ -6,9 +6,10 @@ using BCnEncoder.Shared; using BCnEncoder.Shared.ImageFiles; using BCnEncTests.Support; -using CommunityToolkit.HighPerformance; using Xunit; +using CommunityToolkit.HighPerformance; + namespace BCnEncTests { public class Bc7BlockTests @@ -85,7 +86,7 @@ public void DecodeErrorBlock() var pixels = decoder.DecodeRaw(buffer, width * 4, height * 4, CompressionFormat.Bc7); Assert.Contains(new ColorRgba32(255, 0, 255), pixels); - var decoded = pixels.AsMemory().AsMemory2D(height * 4, width * 4); + var decoded = new Memory2D(pixels, height * 4, width * 4); using var fs = File.OpenWrite("test_decode_bc7_error.png"); TestHelper.SaveAsPng(decoded, fs); } diff --git a/BCnEncTests.Framework/BgraTests.cs b/BCnEncTests.Shared/BgraTests.cs similarity index 100% rename from BCnEncTests.Framework/BgraTests.cs rename to BCnEncTests.Shared/BgraTests.cs diff --git a/BCnEncTests.Framework/CancellationTests.cs b/BCnEncTests.Shared/CancellationTests.cs similarity index 100% rename from BCnEncTests.Framework/CancellationTests.cs rename to BCnEncTests.Shared/CancellationTests.cs diff --git a/BCnEncTests.Framework/ClusterTests.cs b/BCnEncTests.Shared/ClusterTests.cs similarity index 91% rename from BCnEncTests.Framework/ClusterTests.cs rename to BCnEncTests.Shared/ClusterTests.cs index c141928..d8c20b8 100644 --- a/BCnEncTests.Framework/ClusterTests.cs +++ b/BCnEncTests.Shared/ClusterTests.cs @@ -2,6 +2,7 @@ using System.IO; using BCnEncoder.Shared; using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; using Xunit; namespace BCnEncTests @@ -44,7 +45,7 @@ public void Clusterize() pixels[i] = pixC[clusters[i]].ToColorRgba32(); } - var result = pixels.AsMemory().AsMemory2D(height, width); + var result = new Memory2D(pixels, height, width); using var fs = File.OpenWrite("test_cluster.png"); TestHelper.SaveAsPng(result, fs); } diff --git a/BCnEncTests.Framework/DdsReadTests.cs b/BCnEncTests.Shared/DdsReadTests.cs similarity index 100% rename from BCnEncTests.Framework/DdsReadTests.cs rename to BCnEncTests.Shared/DdsReadTests.cs diff --git a/BCnEncTests.Framework/DdsWritingTests.cs b/BCnEncTests.Shared/DdsWritingTests.cs similarity index 100% rename from BCnEncTests.Framework/DdsWritingTests.cs rename to BCnEncTests.Shared/DdsWritingTests.cs diff --git a/BCnEncTests.Framework/DecodingAsyncTests.cs b/BCnEncTests.Shared/DecodingAsyncTests.cs similarity index 93% rename from BCnEncTests.Framework/DecodingAsyncTests.cs rename to BCnEncTests.Shared/DecodingAsyncTests.cs index da61e5e..cba775b 100644 --- a/BCnEncTests.Framework/DecodingAsyncTests.cs +++ b/BCnEncTests.Shared/DecodingAsyncTests.cs @@ -5,6 +5,7 @@ using BCnEncoder.Encoder; using BCnEncoder.Shared; using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; using Xunit; namespace BCnEncTests @@ -53,7 +54,7 @@ public async Task DecodeRawAsync() var decoded = await Task.Run(() => { var pixels = decoder.DecodeRaw(rawBytes, mipWidth, mipHeight, CompressionFormat.Bc1); - return pixels.AsMemory().AsMemory2D(mipHeight, mipWidth); + return new Memory2D(pixels, mipHeight, mipWidth); }); TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); diff --git a/BCnEncTests.Framework/DecodingTests.cs b/BCnEncTests.Shared/DecodingTests.cs similarity index 100% rename from BCnEncTests.Framework/DecodingTests.cs rename to BCnEncTests.Shared/DecodingTests.cs diff --git a/BCnEncTests.Framework/EncoderOptionsTests.cs b/BCnEncTests.Shared/EncoderOptionsTests.cs similarity index 100% rename from BCnEncTests.Framework/EncoderOptionsTests.cs rename to BCnEncTests.Shared/EncoderOptionsTests.cs diff --git a/BCnEncTests.Framework/EncodingAsyncTest.cs b/BCnEncTests.Shared/EncodingAsyncTest.cs similarity index 90% rename from BCnEncTests.Framework/EncodingAsyncTest.cs rename to BCnEncTests.Shared/EncodingAsyncTest.cs index a936b7d..88cfb3f 100644 --- a/BCnEncTests.Framework/EncodingAsyncTest.cs +++ b/BCnEncTests.Shared/EncodingAsyncTest.cs @@ -52,7 +52,7 @@ public async Task EncodeCubemapToDdsAsync() { var rawPixels = decoder.DecodeRaw(file.Faces[i].MipMaps[0].Data, (int)file.Faces[i].Width, (int)file.Faces[i].Height, CompressionFormat.Bc1); - var decodedImage = rawPixels.AsMemory().AsMemory2D((int)file.Faces[i].Height, (int)file.Faces[i].Width); + var decodedImage = new Memory2D(rawPixels, (int)file.Faces[i].Height, (int)file.Faces[i].Width); TestHelper.AssertImagesEqual(originalCubeMap[i], decodedImage, encoder.OutputOptions.Quality); } @@ -68,7 +68,7 @@ public async Task EncodeCubemapToKtxAsync() { 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 = rawPixels.AsMemory().AsMemory2D( + var decodedImage = new Memory2D(rawPixels, (int)file.MipMaps[0].Faces[i].Height, (int)file.MipMaps[0].Faces[i].Width); TestHelper.AssertImagesEqual(originalCubeMap[i], decodedImage, encoder.OutputOptions.Quality); @@ -80,7 +80,7 @@ public async Task EncodeToRawBytesAsync() { var data = await encoder.EncodeToRawBytesAsync(originalImage); var rawPixels = decoder.DecodeRaw(data[0], originalImage.Width, originalImage.Height, CompressionFormat.Bc1); - var image = rawPixels.AsMemory().AsMemory2D(originalImage.Height, originalImage.Width); + var image = new Memory2D(rawPixels, originalImage.Height, originalImage.Width); TestHelper.AssertImagesEqual(originalImage, image, encoder.OutputOptions.Quality); } diff --git a/BCnEncTests.Framework/EncodingTest.cs b/BCnEncTests.Shared/EncodingTest.cs similarity index 100% rename from BCnEncTests.Framework/EncodingTest.cs rename to BCnEncTests.Shared/EncodingTest.cs diff --git a/BCnEncTests.Framework/ProgressTests.cs b/BCnEncTests.Shared/ProgressTests.cs similarity index 100% rename from BCnEncTests.Framework/ProgressTests.cs rename to BCnEncTests.Shared/ProgressTests.cs diff --git a/BCnEncTests.Framework/RawTests.cs b/BCnEncTests.Shared/RawTests.cs similarity index 70% rename from BCnEncTests.Framework/RawTests.cs rename to BCnEncTests.Shared/RawTests.cs index d2a572a..e19d3f9 100644 --- a/BCnEncTests.Framework/RawTests.cs +++ b/BCnEncTests.Shared/RawTests.cs @@ -1,6 +1,4 @@ using System; -using System.Drawing; -using System.Drawing.Imaging; using System.IO; using BCnEncoder.Decoder; using BCnEncoder.Encoder; @@ -25,7 +23,7 @@ public void EncodeDecode() var encodedRawBytes = encoder.EncodeToRawBytes(inputImage); var decodedPixels = decoder.DecodeRaw(encodedRawBytes[0], inputImage.Width, inputImage.Height, CompressionFormat.Bc1); - var decodedImage = decodedPixels.AsMemory().AsMemory2D(inputImage.Height, inputImage.Width); + var decodedImage = new Memory2D(decodedPixels, inputImage.Height, inputImage.Width); var originalColors = TestHelper.GetSinglePixelArrayAsColors(inputImage); var decodedColors = TestHelper.GetSinglePixelArrayAsColors(decodedImage); @@ -50,7 +48,7 @@ public void EncodeDecodeStream() Assert.Equal(0, ms.Position); var rawPixels = decoder.DecodeRaw(ms, inputImage.Width, inputImage.Height, CompressionFormat.Bc1); - var decodedImage = rawPixels.AsMemory().AsMemory2D(inputImage.Height, inputImage.Width); + var decodedImage = new Memory2D(rawPixels, inputImage.Height, inputImage.Width); var originalColors = TestHelper.GetSinglePixelArrayAsColors(inputImage); var decodedColors = TestHelper.GetSinglePixelArrayAsColors(decodedImage); @@ -87,11 +85,12 @@ public void EncodeDecodeAllMipMapsStream() 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 resized = ResizeImage(inputImage, mipWidth, mipHeight); var blockSize = decoder.GetBlockSize(CompressionFormat.Bc1); var blockCount = decoder.GetBlockCount(mipWidth, mipHeight); @@ -99,9 +98,9 @@ public void EncodeDecodeAllMipMapsStream() ms.Read(buffer, 0, buffer.Length); var rawPixels = decoder.DecodeRaw(buffer, mipWidth, mipHeight, CompressionFormat.Bc1); - var decodedImage = rawPixels.AsMemory().AsMemory2D(mipHeight, mipWidth); + var decodedImage = new Memory2D(rawPixels, mipHeight, mipWidth); - var originalColors = TestHelper.GetSinglePixelArrayAsColors(resized); + var originalColors = TestHelper.GetSinglePixelArrayAsColors(resized[i]); var decodedColors = TestHelper.GetSinglePixelArrayAsColors(decodedImage); TestHelper.AssertPixelsEqual(originalColors, decodedColors, encoder.OutputOptions.Quality); @@ -112,42 +111,11 @@ public void EncodeDecodeAllMipMapsStream() Assert.Equal(1, lastMHeight); } - private static ReadOnlyMemory2D ResizeImage(Memory2D image, int newWidth, int newHeight) + private static ReadOnlyMemory2D[] ResizeImageToMips(Memory2D image) { int numMips = 0; var chain = MipMapper.GenerateMipChain(image, ref numMips); - - foreach (var memory2D in chain) - { - if (memory2D.Width == newWidth && memory2D.Height == newHeight) - { - return memory2D; - } - } - throw new InvalidOperationException("Cannot resize image to non-mip dimensions"); - } - - private static unsafe Bitmap ToBitmap(Memory2D image) - { - var bmp = new Bitmap(image.Width, image.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); - var data = bmp.LockBits(new Rectangle(0, 0, image.Width, image.Height), - ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); - byte* ptr = (byte*)data.Scan0; - var span = image.Span; - for (int y = 0; y < image.Height; y++) - { - for (int x = 0; x < image.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); - return bmp; + return chain; } } } diff --git a/BCnEncTests.Framework/SingleBlockTests.cs b/BCnEncTests.Shared/SingleBlockTests.cs similarity index 100% rename from BCnEncTests.Framework/SingleBlockTests.cs rename to BCnEncTests.Shared/SingleBlockTests.cs diff --git a/BCnEncTests/AtcTests.cs b/BCnEncTests/AtcTests.cs deleted file mode 100644 index d02da3e..0000000 --- a/BCnEncTests/AtcTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -using BCnEncoder.Decoder; -using BCnEncoder.Encoder; -using BCnEncoder.Shared; -using BCnEncTests.Support; -using Xunit; -using BCnEncoder.ImageSharp; - -namespace BCnEncTests -{ - 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); - - // 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); - - // 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); - - // 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); - - // 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); - - // 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); - - // Assert - TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); - } - } -} diff --git a/BCnEncTests/BC1Tests.cs b/BCnEncTests/BC1Tests.cs deleted file mode 100644 index bfc7320..0000000 --- a/BCnEncTests/BC1Tests.cs +++ /dev/null @@ -1,164 +0,0 @@ -using BCnEncoder.Shared; -using SixLabors.ImageSharp.PixelFormats; -using Xunit; -using BCnEncoder.ImageSharp; - -namespace BCnEncTests -{ - public class Bc1Tests - { - - [Fact] - public void Decode() { - var block = new Bc1Block - { - color0 = new ColorRgb565(255, 255, 255), - color1 = new ColorRgb565(0, 0, 0) - }; - - Assert.False(block.HasAlphaOrBlack); - block[0] = 1; - block[1] = 1; - block[2] = 1; - block[3] = 1; - - block[4] = 3; - block[5] = 3; - block[6] = 3; - block[7] = 3; - - block[8] = 2; - block[9] = 2; - block[10] = 2; - block[11] = 2; - - block[12] = 0; - block[13] = 0; - block[14] = 0; - block[15] = 0; - - var raw = block.Decode(false); - Assert.Equal(new ColorRgba32(0, 0, 0), raw.p00); - Assert.Equal(new ColorRgba32(0, 0, 0), raw.p10); - Assert.Equal(new ColorRgba32(0, 0, 0), raw.p20); - Assert.Equal(new ColorRgba32(0, 0, 0), raw.p30); - - Assert.Equal(new ColorRgba32(85, 85, 85), raw.p01); - Assert.Equal(new ColorRgba32(85, 85, 85), raw.p11); - Assert.Equal(new ColorRgba32(85, 85, 85), raw.p21); - Assert.Equal(new ColorRgba32(85, 85, 85), raw.p31); - - Assert.Equal(new ColorRgba32(170, 170, 170), raw.p02); - Assert.Equal(new ColorRgba32(170, 170, 170), raw.p12); - Assert.Equal(new ColorRgba32(170, 170, 170), raw.p22); - Assert.Equal(new ColorRgba32(170, 170, 170), raw.p32); - - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p03); - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p13); - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p23); - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p33); - } - - [Fact] - public void DecodeBlack() { - var block = new Bc1Block - { - color0 = new ColorRgb565(200, 200, 200), - color1 = new ColorRgb565(255, 255, 255) - }; - - Assert.True(block.HasAlphaOrBlack); - block[0] = 0; - block[1] = 0; - block[2] = 0; - block[3] = 0; - - block[4] = 3; - block[5] = 3; - block[6] = 3; - block[7] = 3; - - block[8] = 2; - block[9] = 2; - block[10] = 2; - block[11] = 2; - - block[12] = 1; - block[13] = 1; - block[14] = 1; - block[15] = 1; - - var raw = block.Decode(false); - Assert.Equal(new ColorRgba32(206, 203, 206), raw.p00); - Assert.Equal(new ColorRgba32(206, 203, 206), raw.p10); - 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), raw.p01); - Assert.Equal(new ColorRgba32(0, 0, 0), raw.p11); - Assert.Equal(new ColorRgba32(0, 0, 0), raw.p21); - Assert.Equal(new ColorRgba32(0, 0, 0), raw.p31); - - Assert.Equal(new ColorRgba32(230, 229, 230), raw.p02); - Assert.Equal(new ColorRgba32(230, 229, 230), raw.p12); - Assert.Equal(new ColorRgba32(230, 229, 230), raw.p22); - Assert.Equal(new ColorRgba32(230, 229, 230), raw.p32); - - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p03); - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p13); - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p23); - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p33); - } - - [Fact] - public void DecodeAlpha() { - var block = new Bc1Block - { - color0 = new ColorRgb565(200, 200, 200), - color1 = new ColorRgb565(255, 255, 255) - }; - - Assert.True(block.HasAlphaOrBlack); - block[0] = 0; - block[1] = 0; - block[2] = 0; - block[3] = 0; - - block[4] = 3; - block[5] = 3; - block[6] = 3; - block[7] = 3; - - block[8] = 2; - block[9] = 2; - block[10] = 2; - block[11] = 2; - - block[12] = 1; - block[13] = 1; - block[14] = 1; - block[15] = 1; - - var raw = block.Decode(true); - Assert.Equal(new ColorRgba32(206, 203, 206), raw.p00); - Assert.Equal(new ColorRgba32(206, 203, 206), raw.p10); - 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(230, 229, 230), raw.p02); - Assert.Equal(new ColorRgba32(230, 229, 230), raw.p12); - Assert.Equal(new ColorRgba32(230, 229, 230), raw.p22); - Assert.Equal(new ColorRgba32(230, 229, 230), raw.p32); - - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p03); - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p13); - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p23); - Assert.Equal(new ColorRgba32(255, 255, 255), raw.p33); - } - } -} diff --git a/BCnEncTests/BCnEncTests.csproj b/BCnEncTests/BCnEncTests.csproj index fabc4d1..74aa454 100644 --- a/BCnEncTests/BCnEncTests.csproj +++ b/BCnEncTests/BCnEncTests.csproj @@ -7,6 +7,7 @@ + diff --git a/BCnEncTests/Bc5Tests.cs b/BCnEncTests/Bc5Tests.cs deleted file mode 100644 index 7eb37ff..0000000 --- a/BCnEncTests/Bc5Tests.cs +++ /dev/null @@ -1,84 +0,0 @@ -using BCnEncoder.Decoder; -using BCnEncoder.ImageSharp; -using BCnEncoder.Shared; -using BCnEncTests.Support; -using Xunit; - -namespace BCnEncTests -{ - public class Bc5Tests - { - [Fact] - public void Bc5Indices() - { - var block = new Bc5Block(); - - for (var i = 0; i < 16; i++) - { - block.SetRedIndex(i, (byte)(i % 8)); - block.SetGreenIndex(i, (byte)((i + 3) % 8)); - } - - for (var i = 0; i < 16; i++) - { - int rI = block.GetRedIndex(i); - int gI = block.GetGreenIndex(i); - - Assert.Equal((byte)(i % 8), rI); - Assert.Equal((byte)((i + 3) % 8), gI); - } - } - - [Fact] - public void Bc5DdsDecode() - { - var reference = ImageLoader.TestDecodingBc5Reference; - var decoded = new BcDecoder().DecodeToImageRgba32(DdsLoader.TestDecompressBc5); - - var refSpan = TestHelper.GetSinglePixelArrayAsColors(reference); - var decSpan = TestHelper.GetSinglePixelArrayAsColors(decoded); - - Assert.Equal(reference.Width, decoded.Width); - Assert.Equal(reference.Height, decoded.Height); - - // Exactly equal - for (var i = 0; i < reference.Width * reference.Height; i++) - Assert.Equal(refSpan[i], decSpan[i]); - } - - [Fact] - public void Bc5BlockDecode() - { - var block = new Bc5Block() - { - greenBlock = new Bc4ComponentBlock() { componentBlock = 0x91824260008935ee }, - redBlock = new Bc4ComponentBlock() { componentBlock = 0x6d900f66d3c0a70d } - }; - - var referenceBlock = new RawBlock4X4Rgba32 - { - p00 = new ColorRgba32(13, 53, 0, 255), - p01 = new ColorRgba32(136, 238, 0, 255), - p02 = new ColorRgba32(255, 212, 0, 255), - p03 = new ColorRgba32(167, 238, 0, 255), - p10 = new ColorRgba32(13, 53, 0, 255), - p11 = new ColorRgba32(136, 238, 0, 255), - p12 = new ColorRgba32(167, 238, 0, 255), - p13 = new ColorRgba32(75, 185, 0, 255), - p20 = new ColorRgba32(255, 212, 0, 255), - p21 = new ColorRgba32(167, 238, 0, 255), - p22 = new ColorRgba32(13, 53, 0, 255), - p23 = new ColorRgba32(75, 159, 0, 255), - p30 = new ColorRgba32(167, 238, 0, 255), - p31 = new ColorRgba32(75, 185, 0, 255), - p32 = new ColorRgba32(13, 53, 0, 255), - p33 = new ColorRgba32(75, 159, 0, 255) - }; - - var decodedBlock = block.Decode(); - - for (var i = 0; i < 16; i++) - Assert.Equal(referenceBlock[i], decodedBlock[i]); - } - } -} diff --git a/BCnEncTests/Bc6EncoderTests.cs b/BCnEncTests/Bc6EncoderTests.cs deleted file mode 100644 index 7352f86..0000000 --- a/BCnEncTests/Bc6EncoderTests.cs +++ /dev/null @@ -1,405 +0,0 @@ -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; -using BCnEncTests.Support; -using CommunityToolkit.HighPerformance; -using Xunit; -using Xunit.Abstractions; - -namespace BCnEncTests -{ - public class Bc6EncoderTests - { - private ITestOutputHelper output; - - public Bc6EncoderTests(ITestOutputHelper output) => this.output = output; - - [Theory] - [InlineData(0x7F, 8, false)] - [InlineData(0xFF, 8, false)] - [InlineData(0x7F, 8, true)] - [InlineData(0xFF, 8, true)] - [InlineData(0b111_1100_1101, 11, true)] - [InlineData(0b111_1100_1101, 11, false)] - [InlineData(0b011_1100_1101, 11, true)] - [InlineData(0b011_1100_1101, 11, false)] - [InlineData(0b0_1001, 5, true)] - [InlineData(0b0_1001, 5, false)] - [InlineData(0b1_1100, 5, true)] - [InlineData(0b1_1100, 5, false)] - public void Quantize(int initialQuantizedValue, int endpointBits, bool signed) - { - if (signed) - { - initialQuantizedValue = IntHelper.SignExtend(initialQuantizedValue, endpointBits); - } - 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); - - Assert.InRange(prequantized, unquantized - 2, unquantized + 2); - Assert.InRange(quantized, initialQuantizedValue - 1, initialQuantizedValue + 1); - } - - [Fact] - public void PreQuantizeRawEndpoint() - { - var ep0 = new ColorRgbFloat(1, 0.001f, 0.2f); - - var preQuantized = Bc6EncodingHelpers.PreQuantizeRawEndpoint(ep0, false); - var unQu = Bc6Block.FinishUnQuantize(preQuantized, false); - - Assert.InRange(unQu.Item1, ep0.r - 0.001f, ep0.r + 0.001f); - Assert.InRange(unQu.Item2, ep0.g - 0.001f, ep0.g + 0.001f); - Assert.InRange(unQu.Item3, ep0.b - 0.001f, ep0.b + 0.001f); - } - - [Fact] - public void RgbBoundingBox() - { - var testBlock = new RawBlock4X4RgbFloat() - { - p00 = new ColorRgbFloat(0.01f, -0.05f, 0.02f), - p01 = new ColorRgbFloat(0.02f, 0.02f, 0.03f), - p02 = new ColorRgbFloat(0.02f, 0.02f, 0.02f), - p03 = new ColorRgbFloat(0.01f, 0.02f, 0.02f), - p10 = new ColorRgbFloat(0.02f, 0.02f, 0.02f), - p11 = new ColorRgbFloat(0.045f, 0.02f, 0.02f), - p12 = new ColorRgbFloat(0.04f, 0.02f, 0.02f), - p13 = new ColorRgbFloat(0.04f, 0.02f, 0.02f), - p20 = new ColorRgbFloat(0.21f, 0.02f, 0.02f), - p21 = new ColorRgbFloat(0.01f, 0.02f, 0.02f), - p22 = new ColorRgbFloat(0.01f, 0.32f, 0.02f), - p23 = new ColorRgbFloat(1.01f, 0.22f, 0.02f), - p30 = new ColorRgbFloat(0.01f, 0.12f, 0.02f), - p31 = new ColorRgbFloat(0.01f, 0.02f, 0.02f), - p32 = new ColorRgbFloat(0.01f, 0.62f, 0.7f), - p33 = new ColorRgbFloat(0.01f, 0.02f, 0.02f) - }; - - BCnEncoder.Shared.RgbBoundingBox.CreateFloat(testBlock.AsSpan, out var min, out var max); - - Assert.InRange(min.r, 0.01, 0.02); - Assert.InRange(min.g, -0.05, -0.03); - Assert.InRange(min.b, 0.02, 0.03); - - Assert.InRange(max.r, 0.9, 1.01); - Assert.InRange(max.g, 0.59, 0.62); - 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, - random.Next() & (1 << endpointPrecision) - 1); - var ep1 = (random.Next() & (1 << endpointPrecision) - 1, - random.Next() & (1 << endpointPrecision) - 1, - random.Next() & (1 << endpointPrecision) - 1); - - var indices = new byte[16]; - for (var i = 1; i < indices.Length; i++) - { - indices[i] = (byte) random.Next(1 << 4); - } - - var block = Bc6Block.PackType3(ep0, ep1, indices); - - Assert.Equal(Bc6BlockType.Type3, block.Type); - - var extracted0 = block.ExtractEp0(); - var extracted1 = block.ExtractEp1(); - - Assert.Equal(ep0, extracted0); - Assert.Equal(ep1, extracted1); - - for (var i = 0; i < indices.Length; i++) - { - var idx = block.GetColorIndex(1, 0, 4, i); - Assert.Equal(indices[i], idx); - } - } - - - [Theory] - [InlineData(Bc6BlockType.Type0)] - [InlineData(Bc6BlockType.Type1)] - [InlineData(Bc6BlockType.Type2)] - [InlineData(Bc6BlockType.Type6)] - [InlineData(Bc6BlockType.Type10)] - [InlineData(Bc6BlockType.Type14)] - [InlineData(Bc6BlockType.Type18)] - [InlineData(Bc6BlockType.Type22)] - [InlineData(Bc6BlockType.Type26)] - [InlineData(Bc6BlockType.Type30)] - [InlineData(Bc6BlockType.Type3)] - [InlineData(Bc6BlockType.Type7)] - [InlineData(Bc6BlockType.Type11)] - //[InlineData(Bc6BlockType.Type15)] //deltabits too small to encode the testblock - internal void EncodeAllModesUnsigned(Bc6BlockType type) - { - var testBlock = new RawBlock4X4RgbFloat() - { - p00 = new ColorRgbFloat(1.011f, 10.01f, 2.01f), - p01 = new ColorRgbFloat(1.01f, 10.014f, 2.012f), - p02 = new ColorRgbFloat(1.005f, 10.012f, 2.02f), - p03 = new ColorRgbFloat(1.01f, 10.013f, 2.023f), - p10 = new ColorRgbFloat(1.011f, 10.01f, 2.01f), - p11 = new ColorRgbFloat(1.01f, 10.014f, 2.012f), - p12 = new ColorRgbFloat(1.005f, 10.012f, 2.02f), - p13 = new ColorRgbFloat(1.01f, 10.013f, 2.023f), - p20 = new ColorRgbFloat(1.011f, 10.01f, 2.01f), - p21 = new ColorRgbFloat(1.01f, 10.014f, 2.012f), - p22 = new ColorRgbFloat(1.005f, 10.012f, 2.02f), - p23 = new ColorRgbFloat(1.01f, 10.013f, 2.023f), - p30 = new ColorRgbFloat(1.011f, 10.01f, 2.01f), - p31 = new ColorRgbFloat(1.01f, 10.014f, 2.012f), - p32 = new ColorRgbFloat(1.005f, 10.012f, 2.02f), - p33 = new ColorRgbFloat(1.01f, 10.013f, 2.023f) - }; - Bc6Block encoded; - var badTransform = false; - - if(type.HasSubsets()) - { - var indexBlock = Bc6Encoder.CreateClusterIndexBlock(testBlock, out var numClusters, 2); - var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock, numClusters, true); - - var bestPartition = best2SubsetPartitions[0]; - - Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(testBlock, out var ep0, out var ep1, bestPartition, 0); - Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(testBlock, out var ep2, out var ep3, bestPartition, 1); - - encoded = Bc6ModeEncoder.EncodeBlock2Sub(type, testBlock, ep0, ep1, ep2, ep3, bestPartition, - false, out badTransform); - } - else - { - BCnEncoder.Shared.RgbBoundingBox.CreateFloat(testBlock.AsSpan, out var min, out var max); - encoded = Bc6ModeEncoder.EncodeBlock1Sub(type, testBlock, min, max, false, - out badTransform); - } - - Assert.False(badTransform); - Assert.Equal(type, encoded.Type); - var decoded = encoded.Decode(false); - var error = testBlock.CalculateError(decoded); - Assert.InRange(error, 0, 0.3); - } - - [Theory] - [InlineData(Bc6BlockType.Type0)] - [InlineData(Bc6BlockType.Type1)] - [InlineData(Bc6BlockType.Type2)] - [InlineData(Bc6BlockType.Type6)] - [InlineData(Bc6BlockType.Type10)] - [InlineData(Bc6BlockType.Type14)] - [InlineData(Bc6BlockType.Type18)] - [InlineData(Bc6BlockType.Type22)] - [InlineData(Bc6BlockType.Type26)] - [InlineData(Bc6BlockType.Type30)] - [InlineData(Bc6BlockType.Type3)] - [InlineData(Bc6BlockType.Type7)] - [InlineData(Bc6BlockType.Type11)] - [InlineData(Bc6BlockType.Type15)] - internal void EncodeAllModesSigned(Bc6BlockType type) - { - var testBlock = new RawBlock4X4RgbFloat() - { - p00 = new ColorRgbFloat(-1.011f, 10.01f, 2.01f), - p01 = new ColorRgbFloat(-1.01f, 10.014f, 2.012f), - p02 = new ColorRgbFloat(-1.005f, 10.012f, 2.02f), - p03 = new ColorRgbFloat(-1.01f, 10.013f, 2.023f), - p10 = new ColorRgbFloat(-1.011f, 10.01f, 2.01f), - p11 = new ColorRgbFloat(-1.01f, 10.014f, 2.012f), - p12 = new ColorRgbFloat(-1.005f, 10.012f, 2.02f), - p13 = new ColorRgbFloat(-1.01f, 10.013f, 2.023f), - p20 = new ColorRgbFloat(-1.011f, 10.01f, 2.01f), - p21 = new ColorRgbFloat(-1.01f, 10.014f, 2.012f), - p22 = new ColorRgbFloat(-1.005f, 10.012f, 2.02f), - p23 = new ColorRgbFloat(-1.01f, 10.013f, 2.023f), - p30 = new ColorRgbFloat(-1.011f, 10.01f, 2.01f), - p31 = new ColorRgbFloat(-1.01f, 10.014f, 2.012f), - p32 = new ColorRgbFloat(-1.005f, 10.012f, 2.02f), - p33 = new ColorRgbFloat(-1.01f, 10.013f, 2.023f) - }; - Bc6Block encoded; - var badTransform = false; - - if (type.HasSubsets()) - { - var indexBlock = Bc6Encoder.CreateClusterIndexBlock(testBlock, out var numClusters, 2); - var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock, numClusters, true); - - var bestPartition = best2SubsetPartitions[0]; - - Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(testBlock, out var ep0, out var ep1, bestPartition, 0); - Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(testBlock, out var ep2, out var ep3, bestPartition, 1); - - encoded = Bc6ModeEncoder.EncodeBlock2Sub(type, testBlock, ep0, ep1, ep2, ep3, bestPartition, - true, out badTransform); - } - else - { - BCnEncoder.Shared.RgbBoundingBox.CreateFloat(testBlock.AsSpan, out var min, out var max); - encoded = Bc6ModeEncoder.EncodeBlock1Sub(type, testBlock, min, max, true, - out badTransform); - } - - Assert.False(badTransform); - Assert.Equal(type, encoded.Type); - var decoded = encoded.Decode(true); - var error = testBlock.CalculateError(decoded); - Assert.InRange(error, 0, 0.5); - } - - [Fact] - 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); - - for (var i = 0; i < blocks.Length; i++) - { - var encoded = Bc6Encoder.Bc6EncoderBalanced.EncodeBlock(blocks[i], signed); - var decoded = encoded.Decode(signed); - - var error = decoded.CalculateError(blocks[i]); - if (error > 0.06 || i == 14749) - { - Debugger.Break(); - encoded = Bc6Encoder.Bc6EncoderBalanced.EncodeBlock(blocks[i], signed); - } - } - } - - [Fact] - public void EncodeFast() - { - TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrKiara, CompressionFormat.Bc6U, CompressionQuality.Fast, - "encoding_bc6_kiara_fast.ktx", output); - } - - [Fact] - public void EncodeBalanced() - { - TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrKiara, CompressionFormat.Bc6U, CompressionQuality.Balanced, - "encoding_bc6_kiara_balanced.ktx", output); - } - - [Fact] - public void EncodeBestQuality() - { - TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrKiara, CompressionFormat.Bc6U, CompressionQuality.BestQuality, - "encoding_bc6_kiara_bestquality.ktx", output); - } - - [Fact] - public void EncodeProbeFast() - { - TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6U, CompressionQuality.Fast, - "encoding_bc6_probe_fast.ktx", output); - } - - [Fact] - public void EncodeProbeBalanced() - { - TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6U, CompressionQuality.Balanced, - "encoding_bc6_probe_balanced.ktx", output); - } - - [Fact] - public void EncodeProbeBestQuality() - { - TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6U, CompressionQuality.BestQuality, - "encoding_bc6_probe_bestquality.ktx", output); - } - - [Fact] - public void EncodeProbeSignedFast() - { - TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6S, CompressionQuality.Fast, - "encoding_bc6_probe_signed_fast.ktx", output); - } - - [Fact] - public void EncodeProbSignedBalanced() - { - TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6S, CompressionQuality.Balanced, - "encoding_bc6_probe_signed_balanced.ktx", output); - } - - [Fact] - public void EncodeProbeSignedBestQuality() - { - TestHelper.ExecuteHdrEncodingTest(HdrLoader.TestHdrProbe, CompressionFormat.Bc6S, CompressionQuality.BestQuality, - "encoding_bc6_probe_signed_bestquality.ktx", output); - } - - [Fact] - public void EncodeToKtx() - { - var encoder = new BcEncoder(); - encoder.OutputOptions.Quality = CompressionQuality.Fast; - encoder.OutputOptions.GenerateMipMaps = true; - encoder.OutputOptions.Format = CompressionFormat.Bc6U; - - var ktx = encoder.EncodeToKtxHdr(HdrLoader.TestHdrKiara.PixelMemory); - - using var fs = File.OpenWrite("encoding_bc6_ktx.ktx"); - ktx.Write(fs); - } - - [Fact] - public void EncodeToDds() - { - var encoder = new BcEncoder(); - encoder.OutputOptions.Quality = CompressionQuality.Fast; - encoder.OutputOptions.GenerateMipMaps = true; - encoder.OutputOptions.Format = CompressionFormat.Bc6U; - - var dds = encoder.EncodeToDdsHdr(HdrLoader.TestHdrKiara.PixelMemory); - - using var fs = File.OpenWrite("encoding_bc6_dds.dds"); - dds.Write(fs); - } - - [Fact] - public void EncodeToRaw() - { - var encoder = new BcEncoder(); - encoder.OutputOptions.Quality = CompressionQuality.Fast; - encoder.OutputOptions.GenerateMipMaps = true; - encoder.OutputOptions.Format = CompressionFormat.Bc6U; - - var ktx = encoder.EncodeToKtxHdr(HdrLoader.TestHdrKiara.PixelMemory); - - var allMips = encoder.EncodeToRawBytesHdr(HdrLoader.TestHdrKiara.PixelMemory); - - Assert.True(allMips.Length > 1); - Assert.True(allMips.Length == ktx.MipMaps.Count); - - for (var i = 0; i < allMips.Length; i++) - { - var single = encoder.EncodeToRawBytesHdr(HdrLoader.TestHdrKiara.PixelMemory, i, out var mW, out var mH); - - Assert.Equal(ktx.MipMaps[i].Faces[0].Data, single); - Assert.Equal(allMips[i], single); - } - } - } -} diff --git a/BCnEncTests/Bc6HDecoderTests.cs b/BCnEncTests/Bc6HDecoderTests.cs deleted file mode 100644 index 60a99fa..0000000 --- a/BCnEncTests/Bc6HDecoderTests.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using BCnEncoder.Decoder; -using BCnEncoder.Encoder; -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; - -namespace BCnEncTests -{ - public class Bc6HDecoderTests - { - private ITestOutputHelper output; - - public Bc6HDecoderTests(ITestOutputHelper output) => this.output = output; - - [Fact] - 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); - - Assert.Equal(hdr.pixels.Length, decoded.Length); - - hdr.pixels = decoded; - using var sfs = File.OpenWrite("decoding_test_dds_bc6h.hdr"); - hdr.Write(sfs); - - img.SaveAsPng("decoding_test_dds_bc6h.png"); - - TestHelper.AssertPixelsEqual(HdrLoader.TestHdrKiara.pixels, decoded, CompressionQuality.Fast, output); - } - - [Fact] - 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); - - Assert.Equal(hdr.pixels.Length, decoded.Length); - - hdr.pixels = decoded; - using var sfs = File.OpenWrite("decoding_test_ktx_bc6h.hdr"); - hdr.Write(sfs); - - img.SaveAsPng("decoding_test_ktx_bc6h.png"); - - TestHelper.AssertPixelsEqual(HdrLoader.TestHdrKiara.pixels, decoded, CompressionQuality.BestQuality, output); - } - - // TestHdrKiaraDds includes some blocks with all modes. - // The test_hdr_kiara_dds_float16_data.bin file contains Half-float values decoded with a reference decoder. - // This test ensures that decoding is bit-exact. - [Fact] - 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++) - { - 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); - } - } - - // Test that above statement holds true. And that modes are recognized correctly - [Fact] - public void DecodeModes() - { - var modes = new int[31]; - - var hdr = HdrLoader.TestHdrKiaraDds; - - var bytes = hdr.Faces[0].MipMaps[0].Data; - var blocks = MemoryMarshal.Cast(bytes); - - for (var i = 0; i < blocks.Length; i++) - { - var block = blocks[i]; - var mode = block.Type; - modes[(int) mode]++; - } - - for (var i = 0; i < modes.Length; i++) - { - output.WriteLine($"Mode {i}: {modes[i]}"); - } - - Assert.True(modes[0] > 0); - Assert.True(modes[1] > 0); - Assert.True(modes[2] > 0); - Assert.True(modes[6] > 0); - Assert.True(modes[10] > 0); - Assert.True(modes[14] > 0); - Assert.True(modes[18] > 0); - Assert.True(modes[22] > 0); - Assert.True(modes[26] > 0); - Assert.True(modes[30] > 0); - Assert.True(modes[3] > 0); - Assert.True(modes[7] > 0); - Assert.True(modes[11] > 0); - Assert.True(modes[15] > 0); - } - - [Fact] - public void DecodeErrorBlock() - { - var decoder = new BcDecoder(); - - var width = 16; - var height = 16; - var bufferSize = decoder.GetBlockSize(CompressionFormat.Bc6U) * width * height; - - var buffer = new byte[bufferSize]; - Random r = new 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); - } - - } -} diff --git a/BCnEncTests/Bc7BlockTests.cs b/BCnEncTests/Bc7BlockTests.cs deleted file mode 100644 index c05fdc6..0000000 --- a/BCnEncTests/Bc7BlockTests.cs +++ /dev/null @@ -1,389 +0,0 @@ -using System; -using System.IO; -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 Xunit; - -namespace BCnEncTests -{ - public class Bc7BlockTests - { - [Fact] - public void PackTypes() - { - var numWidthBlocks = 4; - var numHeightBlocks = 2; - var outputBlocks = new Bc7Block[64 * numWidthBlocks * numHeightBlocks]; - var encoded = new byte[64 * numWidthBlocks * numHeightBlocks * Unsafe.SizeOf()]; - - var output = new KtxFile( - KtxHeader.InitializeCompressed(numWidthBlocks * 8 * 4, numHeightBlocks * 8 * 4, - GlInternalFormat.GlCompressedRgbaBptcUnormArb, - GlFormat.GlRgba)); - - var type0 = new Span(outputBlocks, 0, 64); - Type0Pack(type0); - PlaceBlock(0, 0, type0, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - var type1 = new Span(outputBlocks, 64 * 1, 64); - Type1Pack(type1); - PlaceBlock(1, 0, type1, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - var type2 = new Span(outputBlocks, 64 * 2, 64); - Type2Pack(type2); - PlaceBlock(2, 0, type2, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - var type3 = new Span(outputBlocks, 64 * 3, 64); - Type3Pack(type3); - PlaceBlock(3, 0, type3, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - var type4 = new Span(outputBlocks, 64 * 4, 64); - Type4Pack(type4); - PlaceBlock(0, 1, type4, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - var type5 = new Span(outputBlocks, 64 * 5, 64); - Type5Pack(type5); - PlaceBlock(1, 1, type5, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - var type6 = new Span(outputBlocks, 64 * 6, 64); - Type6Pack(type6); - PlaceBlock(2, 1, type6, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - var type7 = new Span(outputBlocks, 64 * 7, 64); - Type7Pack(type7); - PlaceBlock(3, 1, type7, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - output.MipMaps.Add(new KtxMipmap((uint)encoded.Length, - (uint)(8 * 4 * numWidthBlocks), - (uint)(8 * 4 * numHeightBlocks), 1)); - output.MipMaps[0].Faces[0] = new KtxMipFace(encoded, - (uint)(8 * 4 * numWidthBlocks), - (uint)(8 * 4 * numHeightBlocks)); - - var fs = File.OpenWrite("bc7_blocktests.ktx"); - output.Write(fs); - } - - [Fact] - public void DecodeErrorBlock() - { - var decoder = new BcDecoder(); - - var width = 32; - var height = 32; - var bufferSize = decoder.GetBlockSize(CompressionFormat.Bc7) * width * height; - - var buffer = new byte[bufferSize]; - Random 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); - } - - #region Type Packs - - private void Type0Pack(Span output) - { - var subsetEndpoints = new[] { - //subset 1 - new byte[]{0b1111, 0, 0}, - new byte[]{0b1000, 0, 0}, - // subset 2 - new byte[]{0, 0b1111, 0}, - new byte[]{0, 0b1000, 0}, - //subset 3 - new byte[]{0, 0, 0b1111}, - new byte[]{0, 0, 0b1000} - }; - var pBits = new byte[] { - 1, 0, 1, 0, 1, 1 - }; - var indices = new byte[] { - 0, 1, 2, 3, - 0, 1, 2, 3, - 3, 2, 1, 0, - 3, 2, 1, 0 - }; - for (var i = 0; i < 16; i++) - { - output[i].PackType0(i, subsetEndpoints, pBits, indices); - Assert.Equal(Bc7BlockType.Type0, output[i].Type); - Assert.Equal(i, output[i].PartitionSetId); - } - - 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); - Assert.Equal(Bc7BlockType.Type0, output[i].Type); - Assert.Equal(i, output[i].PartitionSetId); - } - - 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); - Assert.Equal(Bc7BlockType.Type0, output[i].Type); - Assert.Equal(i, output[i].PartitionSetId); - } - - 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); - Assert.Equal(Bc7BlockType.Type0, output[i].Type); - Assert.Equal(i, output[i].PartitionSetId); - } - } - - private void Type1Pack(Span output) - { - var subsetEndpoints = new[] { - //subset 1 - new byte[]{0b111111, 0b0100, 0}, - new byte[]{0b0100, 0b111111, 0}, - // subset 2 - new byte[]{0, 0, 0b111111}, - new byte[]{0, 0, 0b1010} - }; - var pBits = new byte[] { - 1, 1 - }; - var indices = new byte[] { - 0, 1, 2, 3, - 0, 1, 2, 3, - 3, 2, 1, 0, - 3, 2, 1, 0 - }; - for (var i = 0; i < 64; i++) - { - output[i].PackType1(i, subsetEndpoints, pBits, indices); - Assert.Equal(Bc7BlockType.Type1, output[i].Type); - Assert.Equal(i, output[i].PartitionSetId); - } - } - - private void Type2Pack(Span output) - { - var subsetEndpoints = new[] { - //subset 1 - new byte[]{0b11111, 0, 0}, - new byte[]{0b01000, 0, 0}, - // subset 2 - new byte[]{0, 0b11111, 0}, - new byte[]{0, 0b01000, 0}, - //subset 3 - new byte[]{0, 0, 0b11111}, - new byte[]{0, 0, 0b01000} - }; - var indices = new byte[] { - 0, 1, 2, 3, - 0, 1, 2, 3, - 3, 2, 1, 0, - 3, 2, 1, 0 - }; - for (var i = 0; i < 64; i++) - { - output[i].PackType2(i, subsetEndpoints, indices); - Assert.Equal(Bc7BlockType.Type2, output[i].Type); - Assert.Equal(i, output[i].PartitionSetId); - } - } - - private void Type3Pack(Span output) - { - var subsetEndpoints = new[] { - //subset 1 - new byte[]{0b1111111, 0b10100, 0}, - new byte[]{0b10100, 0b1111111, 0}, - // subset 2 - new byte[]{0, 0, 0b1111111}, - new byte[]{0, 0, 0b11010} - }; - var pBits = new byte[] { - 1, 0, 0, 1 - }; - var indices = new byte[] { - 0, 1, 2, 3, - 0, 1, 2, 3, - 3, 2, 1, 0, - 3, 2, 1, 0 - }; - for (var i = 0; i < 64; i++) - { - output[i].PackType3(i, subsetEndpoints, pBits, indices); - Assert.Equal(Bc7BlockType.Type3, output[i].Type); - Assert.Equal(i, output[i].PartitionSetId); - } - } - - private void Type4Pack(Span output) - { - var colorEndpoints = new[] { - //subset 1 - new byte[]{0b11111, 0, 0b0100}, - new byte[]{0, 0b11111, 0b0100} - }; - 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, - 7, 6, 5, 4, - 3, 2, 1, 0 - }; - var rotation = 0; - byte idxMode = 0; - for (var i = 0; i < 64; i++) - { - rotation = (rotation + 1) % 4; - idxMode = (byte)((idxMode + 1) % 2); - output[i].PackType4(rotation, idxMode, colorEndpoints, alphaEndPoints, indices2Bit, indices3Bit); - Assert.Equal(Bc7BlockType.Type4, output[i].Type); - Assert.Equal(rotation, output[i].RotationBits); - Assert.Equal(idxMode, output[i].Type4IndexMode); - } - } - - private void Type5Pack(Span output) - { - var colorEndpoints = new[] { - //subset 1 - new byte[]{0b1111111, 0b0100, 0}, - new byte[]{0, 0, 0b1111111} - }; - var alphaEndPoints = new byte[]{ - 255, - 100 - }; - var colorIndices = new byte[] { - 0, 1, 2, 3, - 0, 1, 2, 3, - 3, 2, 1, 0, - 3, 2, 1, 0 - }; - var alphaIndices = new byte[] { - 1, 2, 3, 0, - 0, 1, 2, 3, - 2, 3, 0, 1, - 3, 2, 1, 0 - }; - var rotation = 0; - for (var i = 0; i < 64; i++) - { - rotation = (rotation + 1) % 4; - output[i].PackType5(rotation, colorEndpoints, alphaEndPoints, colorIndices, alphaIndices); - Assert.Equal(Bc7BlockType.Type5, output[i].Type); - Assert.Equal(rotation, output[i].RotationBits); - } - } - - private void Type6Pack(Span output) - { - var colorEndpoints = new[] { - //subset 1 - new byte[]{0b1111111, 0b0100, 0, 0b1111111}, - new byte[]{0, 0, 0b1111111, 0b1111} - }; - var pBits = new byte[] { - 1, 0 - }; - var colorIndices = new byte[] { - 0, 1, 2, 3, - 4, 5, 6, 7, - 8, 9, 10, 11, - 12, 13, 14, 15 - }; - for (var i = 0; i < 64; i++) - { - output[i].PackType6(colorEndpoints, pBits, colorIndices); - Assert.Equal(Bc7BlockType.Type6, output[i].Type); - } - } - - private void Type7Pack(Span output) - { - var colorEndpoints = new[] { - //subset 1 - new byte[]{0b11111, 0b0100, 0, 0b11111}, - new byte[]{0, 0b11111, 0, 0b111}, - //subset 2 - new byte[]{0b11111, 0b0100, 0, 0b1111}, - new byte[]{0b10100, 0, 0b11111, 0b11111} - }; - var pBits = new byte[] { - 1, 0, 1, 0 - }; - var indices = new byte[] { - 0, 1, 2, 3, - 0, 1, 2, 3, - 3, 2, 1, 0, - 3, 2, 1, 0 - }; - for (var i = 0; i < 64; i++) - { - output[i].PackType7(i, colorEndpoints, pBits, indices); - Assert.Equal(Bc7BlockType.Type7, output[i].Type); - Assert.Equal(i, output[i].PartitionSetId); - } - } - - private void PlaceBlock(int x, int y, Span data, Span destination, int destBlockWidth) - { - for (var i = 0; i < 8; i++) - { - var xStart = x * 8 + (y * 8 + i) * 8 * destBlockWidth; - var row = data.Slice(i * 8, 8); - row.CopyTo(destination.Slice(xStart, 8)); - } - } - - #endregion - } -} diff --git a/BCnEncTests/BgraTests.cs b/BCnEncTests/BgraTests.cs deleted file mode 100644 index 7ae666e..0000000 --- a/BCnEncTests/BgraTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -using BCnEncoder.Decoder; -using BCnEncoder.Encoder; -using BCnEncoder.ImageSharp; -using BCnEncoder.Shared; -using BCnEncTests.Support; -using Xunit; - -namespace BCnEncTests -{ - 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); - - - - // 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); - - // 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); - - // 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); - - // Assert - TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); - } - } -} 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/DdsReadTests.cs b/BCnEncTests/DdsReadTests.cs deleted file mode 100644 index 9a5ba2e..0000000 --- a/BCnEncTests/DdsReadTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -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 -{ - public class DdsReadTests - { - [Fact] - public void ReadRgba() - { - TestHelper.ExecuteDdsReadingTest(DdsLoader.TestDecompressRgba, DxgiFormat.DxgiFormatR8G8B8A8Unorm, "decoding_test_dds_rgba_mip{0}.png"); - } - - [Fact] - public void ReadBc1() - { - TestHelper.ExecuteDdsReadingTest(DdsLoader.TestDecompressBc1, DxgiFormat.DxgiFormatBc1Unorm, "decoding_test_dds_bc1_mip{0}.png"); - } - - [Fact] - public void ReadBc1A() - { - TestHelper.ExecuteDdsReadingTest(DdsLoader.TestDecompressBc1A, DxgiFormat.DxgiFormatBc1Unorm, "decoding_test_dds_bc1a_mip{0}.png"); - } - - [Fact] - public void ReadBc7() - { - TestHelper.ExecuteDdsReadingTest(DdsLoader.TestDecompressBc7, DxgiFormat.DxgiFormatUnknown, "decoding_test_dds_bc7_mip{0}.png"); - } - - [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 outFs = File.OpenWrite($"decoding_test_dds_stream_bc1_mip{i}.png"); - images[i].SaveAsPng(outFs); - images[i].Dispose(); - } - } - } -} diff --git a/BCnEncTests/DdsWritingTests.cs b/BCnEncTests/DdsWritingTests.cs deleted file mode 100644 index 44a0616..0000000 --- a/BCnEncTests/DdsWritingTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using BCnEncoder.Shared; -using BCnEncTests.Support; -using Xunit; - -namespace BCnEncTests -{ - public class DdsWritingTests - { - [Fact] - public void DdsWriteRgba() - { - TestHelper.ExecuteDdsWritingTest(ImageLoader.TestLenna, CompressionFormat.Rgba, "encoding_dds_rgba.dds"); - } - - [Fact] - public void DdsWriteBc1() - { - TestHelper.ExecuteDdsWritingTest(ImageLoader.TestLenna, CompressionFormat.Bc1, "encoding_dds_bc1.dds"); - } - - [Fact] - public void DdsWriteBc2() - { - TestHelper.ExecuteDdsWritingTest(ImageLoader.TestAlpha1, CompressionFormat.Bc2, "encoding_dds_bc2.dds"); - } - - [Fact] - public void DdsWriteBc3() - { - TestHelper.ExecuteDdsWritingTest(ImageLoader.TestAlpha1, CompressionFormat.Bc3, "encoding_dds_bc3.dds"); - } - - [Fact] - public void DdsWriteBc4() - { - TestHelper.ExecuteDdsWritingTest(ImageLoader.TestHeight1, CompressionFormat.Bc4, "encoding_dds_bc4.dds"); - } - - [Fact] - public void DdsWriteBc5() - { - TestHelper.ExecuteDdsWritingTest(ImageLoader.TestRedGreen1, CompressionFormat.Bc5, "encoding_dds_bc5.dds"); - } - - [Fact] - public void DdsWriteBc7() - { - TestHelper.ExecuteDdsWritingTest(ImageLoader.TestLenna, CompressionFormat.Bc7, "encoding_dds_bc7.dds"); - } - - [Fact] - public void DdsWriteCubemap() - { - TestHelper.ExecuteDdsWritingTest(ImageLoader.TestCubemap, CompressionFormat.Bc1, "encoding_dds_cubemap_bc1.dds"); - } - } -} diff --git a/BCnEncTests/DecodingAsyncTests.cs b/BCnEncTests/DecodingAsyncTests.cs deleted file mode 100644 index 04cfa29..0000000 --- a/BCnEncTests/DecodingAsyncTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Threading.Tasks; -using BCnEncoder.Decoder; -using BCnEncoder.Encoder; -using BCnEncoder.ImageSharp; -using BCnEncoder.Shared; -using BCnEncTests.Support; -using Xunit; - -namespace BCnEncTests -{ - public class DecodingAsyncTests - { - [Fact] - public async Task DecodeAsync() - { - var decoder = new BcDecoder(); - var encoder = new BcEncoder(); - var original = ImageLoader.TestGradient1; - - var file = encoder.EncodeToKtx(original); - var image = await decoder.DecodeToImageRgba32Async(file); - - TestHelper.AssertImagesEqual(original, image,encoder.OutputOptions.Quality); - image.Dispose(); - } - - [Fact] - public async Task DecodeAllMipMapsAsync() - { - var decoder = new BcDecoder(); - var encoder = new BcEncoder(); - var original = ImageLoader.TestGradient1; - - var file = encoder.EncodeToKtx(original); - var images = await decoder.DecodeAllMipMapsToImageRgba32Async(file); - - TestHelper.AssertImagesEqual(original, images[0], encoder.OutputOptions.Quality); - foreach(var img in images) - img.Dispose(); - } - - [Fact] - public async Task DecodeRawAsync() - { - var decoder = new BcDecoder(); - var encoder = new BcEncoder(); - var original = ImageLoader.TestGradient1; - - var file = encoder.EncodeToKtx(original); - - var ms = new MemoryStream(file.MipMaps[0].Faces[0].Data); - - var image = await decoder.DecodeRawToImageRgba32Async(ms, - (int) file.MipMaps[0].Width, (int) file.MipMaps[0].Height, CompressionFormat.Bc1); - - TestHelper.AssertImagesEqual(original, image, encoder.OutputOptions.Quality); - image.Dispose(); - } - } -} diff --git a/BCnEncTests/DecodingTests.cs b/BCnEncTests/DecodingTests.cs deleted file mode 100644 index c2fc08c..0000000 --- a/BCnEncTests/DecodingTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using BCnEncTests.Support; -using Xunit; - -namespace BCnEncTests -{ - public class DecodingTests - { - [Fact] - public void Bc1Decode() - { - TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc1, "decoding_test_bc1.png"); - } - - [Fact] - public void Bc1AlphaDecode() - { - TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc1A, "decoding_test_bc1a.png"); - } - - [Fact] - public void Bc2Decode() - { - TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc2, "decoding_test_bc2.png"); - } - - [Fact] - public void Bc3Decode() - { - TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc3, "decoding_test_bc3.png"); - } - - [Fact] - public void Bc4Decode() - { - TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc4Unorm, "decoding_test_bc4.png"); - } - - [Fact] - public void Bc5Decode() - { - TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc5Unorm, "decoding_test_bc5.png"); - } - - [Fact] - public void Bc7DecodeRgb() - { - TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc7Rgb, "decoding_test_bc7_rgb.png"); - } - - [Fact] - public void Bc7DecodeUnorm() - { - TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc7Unorm, "decoding_test_bc7_unorm.png"); - } - - [Fact] - public void Bc7DecodeEveryBlockType() - { - TestHelper.ExecuteDecodingTest(KtxLoader.TestDecompressBc7Types, "decoding_test_bc7_types.png"); - } - } -} 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/EncoderOptionsTests.cs b/BCnEncTests/EncoderOptionsTests.cs deleted file mode 100644 index 677fce4..0000000 --- a/BCnEncTests/EncoderOptionsTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -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)] - [InlineData(5)] - [InlineData(10)] - public void MaxMipMaps(int requestedMipMaps) - { - var testImage = ImageLoader.TestBlur1; - var encoder = new BcEncoder() - { - OutputOptions = - { - GenerateMipMaps = true, - MaxMipMapLevel = requestedMipMaps - } - }; - - Assert.Equal(requestedMipMaps, encoder.CalculateNumberOfMipLevels(testImage)); - - var ktx = encoder.EncodeToKtx(testImage); - - Assert.Equal(requestedMipMaps, (int)ktx.header.NumberOfMipmapLevels); - Assert.Equal(requestedMipMaps, ktx.MipMaps.Count); - - var dds = encoder.EncodeToDds(testImage); - - Assert.Equal(requestedMipMaps, (int)dds.header.dwMipMapCount); - Assert.Equal(requestedMipMaps, dds.Faces[0].MipMaps.Length); - } - - [Fact] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2013:Do not use equality check to check for collection size.", Justification = "")] - public void GenerateMipMaps() - { - var testImage = ImageLoader.TestBlur1; - const int requestedMipMaps = 1; - var encoder = new BcEncoder() - { - OutputOptions = - { - GenerateMipMaps = false - } - }; - - Assert.Equal(requestedMipMaps, encoder.CalculateNumberOfMipLevels(testImage)); - - var ktx = encoder.EncodeToKtx(testImage); - - Assert.Equal(requestedMipMaps, (int)ktx.header.NumberOfMipmapLevels); - Assert.Equal(requestedMipMaps, (int)ktx.MipMaps.Count); - - var dds = encoder.EncodeToDds(testImage); - - Assert.Equal(requestedMipMaps, (int)dds.header.dwMipMapCount); - Assert.Equal(requestedMipMaps, dds.Faces[0].MipMaps.Length); - } - } -} diff --git a/BCnEncTests/EncodingAsyncTest.cs b/BCnEncTests/EncodingAsyncTest.cs deleted file mode 100644 index 6d3e9ff..0000000 --- a/BCnEncTests/EncodingAsyncTest.cs +++ /dev/null @@ -1,88 +0,0 @@ -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 Xunit; - -namespace BCnEncTests -{ - public class EncodingAsyncTest - { - private readonly BcEncoder encoder; - private readonly BcDecoder decoder; - private readonly Image originalImage; - private readonly Image[] originalCubeMap; - - public EncodingAsyncTest() - { - encoder = new BcEncoder(); - decoder = new BcDecoder(); - originalImage = ImageLoader.TestGradient1; - originalCubeMap = ImageLoader.TestCubemap; - } - - [Fact] - public async Task EncodeToDdsAsync() - { - var file = await encoder.EncodeToDdsAsync(originalImage); - var image = decoder.DecodeToImageRgba32(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); - - TestHelper.AssertImagesEqual(originalImage, image, encoder.OutputOptions.Quality); - image.Dispose(); - } - - [Fact] - public async Task EncodeCubemapToDdsAsync() - { - var file = await encoder.EncodeCubeMapToDdsAsync(originalCubeMap[0], originalCubeMap[1], originalCubeMap[2], - originalCubeMap[3], originalCubeMap[4], originalCubeMap[5]); - - 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); - - TestHelper.AssertImagesEqual(originalCubeMap[i], image, encoder.OutputOptions.Quality); - image.Dispose(); - } - } - - [Fact] - public async Task EncodeCubemapToKtxAsync() - { - var file = await encoder.EncodeCubeMapToKtxAsync(originalCubeMap[0], originalCubeMap[1], originalCubeMap[2], - originalCubeMap[3], originalCubeMap[4], originalCubeMap[5]); - - 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); - - TestHelper.AssertImagesEqual(originalCubeMap[i], image, encoder.OutputOptions.Quality); - image.Dispose(); - } - } - - [Fact] - public async Task EncodeToRawBytesAsync() - { - var data = await encoder.EncodeToRawBytesAsync(originalImage); - var image = decoder.DecodeRawToImageRgba32(data[0], originalImage.Width, originalImage.Height, CompressionFormat.Bc1); - - TestHelper.AssertImagesEqual(originalImage, image, encoder.OutputOptions.Quality); - image.Dispose(); - } - } -} 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/ProgressTests.cs b/BCnEncTests/ProgressTests.cs deleted file mode 100644 index 1a58c31..0000000 --- a/BCnEncTests/ProgressTests.cs +++ /dev/null @@ -1,392 +0,0 @@ -using System; -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; -using Xunit.Abstractions; - -namespace BCnEncTests -{ - public class ProgressTests - { - private ITestOutputHelper output; - - public ProgressTests(ITestOutputHelper output) => this.output = output; - - private async Task ExecuteEncodeProgressReport(BcEncoder encoder, Image testImage, int expectedTotalBlocks) - { - var lastProgress = new ProgressElement(0, 1); - - var processedBlocks = 0; - encoder.Options.Progress = new SynchronousProgress(element => - { - output.WriteLine($"Progress = {element.CurrentBlock} / {element.TotalBlocks}"); - - Assert.Equal(++processedBlocks, element.CurrentBlock); - lastProgress = element; - }); - - await using var ms = new MemoryStream(); - await encoder.EncodeToStreamAsync(testImage, ms); - - output.WriteLine("LastProgress = " + lastProgress); - - Assert.Equal(expectedTotalBlocks, lastProgress.TotalBlocks); - Assert.Equal(1, lastProgress.Percentage); - } - - private async Task ExecuteDecodeProgressReport(BcDecoder decoder, KtxFile image, int expectedTotalBlocks, bool singleMip) - { - var lastProgress = new ProgressElement(0, 1); - - var processedBlocks = 0; - decoder.Options.Progress = new SynchronousProgress(element => - { - output.WriteLine($"Progress = {element.CurrentBlock} / {element.TotalBlocks}"); - - Assert.Equal(++processedBlocks, element.CurrentBlock); - lastProgress = element; - }); - - if (singleMip) - { - await decoder.DecodeAsync(image); - } - else - { - await decoder.DecodeAllMipMapsAsync(image); - } - - output.WriteLine("LastProgress = " + lastProgress); - - Assert.Equal(expectedTotalBlocks, lastProgress.TotalBlocks); - Assert.Equal(1, lastProgress.Percentage); - } - - private async Task ExecuteDecodeProgressReport(BcDecoder decoder, DdsFile image, int expectedTotalBlocks, bool singleMip) - { - var lastProgress = new ProgressElement(0, 1); - - var processedBlocks = 0; - decoder.Options.Progress = new SynchronousProgress(element => - { - output.WriteLine($"Progress = {element.CurrentBlock} / {element.TotalBlocks}"); - - Assert.Equal(++processedBlocks, element.CurrentBlock); - lastProgress = element; - }); - - if (singleMip) - { - await decoder.DecodeAsync(image); - } - else - { - await decoder.DecodeAllMipMapsAsync(image); - } - - output.WriteLine("LastProgress = " + lastProgress); - - Assert.Equal(expectedTotalBlocks, lastProgress.TotalBlocks); - Assert.Equal(1, lastProgress.Percentage); - } - - [Fact] - public async Task DecodeProgressReportParallel() - { - var testImage = KtxLoader.TestDecompressBc1; - var decoder = new BcDecoder - { - Options = { IsParallel = true } - }; - - var expectedTotal = 0; - - for (var i = 0; i < testImage.header.NumberOfMipmapLevels; i++) - { - expectedTotal += decoder.GetBlockCount((int)testImage.MipMaps[i].Width, (int)testImage.MipMaps[i].Height); - } - - await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, false); - } - - [Fact] - public async Task DecodeProgressReportNonParallel() - { - var testImage = KtxLoader.TestDecompressBc1; - var decoder = new BcDecoder - { - Options = { IsParallel = false } - }; - - var expectedTotal = 0; - - for (var i = 0; i < testImage.header.NumberOfMipmapLevels; i++) - { - expectedTotal += decoder.GetBlockCount((int)testImage.MipMaps[i].Width, (int)testImage.MipMaps[i].Height); - } - - await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, false); - } - - [Fact] - public async Task DecodeProgressReportParallelOneMip() - { - var testImage = KtxLoader.TestDecompressBc1; - var decoder = new BcDecoder - { - Options = { IsParallel = true } - }; - - var expectedTotal = decoder.GetBlockCount((int)testImage.MipMaps[0].Width, (int)testImage.MipMaps[0].Height); - - await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, true); - } - - [Fact] - public async Task DecodeProgressReportNonParallelOneMip() - { - var testImage = KtxLoader.TestDecompressBc1; - var decoder = new BcDecoder - { - Options = { IsParallel = false } - }; - - var expectedTotal = decoder.GetBlockCount((int)testImage.MipMaps[0].Width, (int)testImage.MipMaps[0].Height); - - await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, true); - } - - [Fact] - public async Task DecodeProgressReportParallelDds() - { - var testImage = DdsLoader.TestDecompressBc1; - var decoder = new BcDecoder - { - Options = { IsParallel = true } - }; - - var expectedTotal = 0; - - for (var i = 0; i < testImage.header.dwMipMapCount; i++) - { - expectedTotal += decoder.GetBlockCount((int)testImage.Faces[0].MipMaps[i].Width, (int)testImage.Faces[0].MipMaps[i].Height); - } - - await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, false); - } - - [Fact] - public async Task DecodeProgressReportNonParallelDds() - { - var testImage = DdsLoader.TestDecompressBc1; - var decoder = new BcDecoder - { - Options = { IsParallel = false } - }; - - var expectedTotal = 0; - - for (var i = 0; i < testImage.header.dwMipMapCount; i++) - { - expectedTotal += decoder.GetBlockCount((int)testImage.Faces[0].MipMaps[i].Width, (int)testImage.Faces[0].MipMaps[i].Height); - } - - await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, false); - } - - [Fact] - public async Task DecodeProgressReportParallelOneMipDds() - { - var testImage = DdsLoader.TestDecompressBc1; - var decoder = new BcDecoder - { - Options = { IsParallel = true } - }; - - var expectedTotal = decoder.GetBlockCount((int)testImage.Faces[0].MipMaps[0].Width, (int)testImage.Faces[0].MipMaps[0].Height); - - await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, true); - } - - [Fact] - public async Task DecodeProgressReportNonParallelOneMipDds() - { - var testImage = DdsLoader.TestDecompressBc1; - var decoder = new BcDecoder - { - Options = { IsParallel = false } - }; - - var expectedTotal = decoder.GetBlockCount((int)testImage.Faces[0].MipMaps[0].Width, (int)testImage.Faces[0].MipMaps[0].Height); - - await ExecuteDecodeProgressReport(decoder, testImage, expectedTotal, true); - } - - [Fact] - public async Task EncodeProgressReportParallelKtx() - { - var testImage = ImageLoader.TestBlur1; - var encoder = new BcEncoder - { - Options = { IsParallel = true }, - OutputOptions = { FileFormat = OutputFileFormat.Ktx } - }; - - var expectedTotal = 0; - - for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage); i++) - { - encoder.CalculateMipMapSize(testImage, i, out var mW, out var mH); - expectedTotal += encoder.GetBlockCount(mW, mH); - } - - await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); - } - - [Fact] - public async Task EncodeProgressReportNonParallelKtx() - { - var testImage = ImageLoader.TestBlur1; - var encoder = new BcEncoder - { - Options = { IsParallel = false }, - OutputOptions = { FileFormat = OutputFileFormat.Ktx } - }; - - var expectedTotal = 0; - - for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage); i++) - { - encoder.CalculateMipMapSize(testImage, i, out var mW, out var mH); - expectedTotal += encoder.GetBlockCount(mW, mH); - } - - await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); - } - - [Fact] - public async Task EncodeProgressReportParallelOneMipKtx() - { - var testImage = ImageLoader.TestBlur1; - var encoder = new BcEncoder - { - Options = { IsParallel = true }, - OutputOptions = { MaxMipMapLevel = 1, FileFormat = OutputFileFormat.Ktx } - }; - - var expectedTotal = encoder.GetBlockCount(testImage.Width, testImage.Height); - - await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); - } - - [Fact] - public async Task EncodeProgressReportNonParallelOneMipKtx() - { - var testImage = ImageLoader.TestBlur1; - var encoder = new BcEncoder - { - Options = { IsParallel = false }, - OutputOptions = { MaxMipMapLevel = 1, FileFormat = OutputFileFormat.Ktx } - }; - - var expectedTotal = encoder.GetBlockCount(testImage.Width, testImage.Height); - - await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); - } - - [Fact] - public async Task EncodeProgressReportParallelDds() - { - var testImage = ImageLoader.TestBlur1; - var encoder = new BcEncoder - { - Options = { IsParallel = true }, - OutputOptions = { FileFormat = OutputFileFormat.Dds } - }; - - var expectedTotal = 0; - - for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage); i++) - { - encoder.CalculateMipMapSize(testImage, i, out var mW, out var mH); - expectedTotal += encoder.GetBlockCount(mW, mH); - } - - await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); - } - - [Fact] - public async Task EncodeProgressReportNonParallelDds() - { - var testImage = ImageLoader.TestBlur1; - var encoder = new BcEncoder - { - Options = { IsParallel = false }, - OutputOptions = { FileFormat = OutputFileFormat.Dds } - }; - - var expectedTotal = 0; - - for (var i = 0; i < encoder.CalculateNumberOfMipLevels(testImage); i++) - { - encoder.CalculateMipMapSize(testImage, i, out var mW, out var mH); - expectedTotal += encoder.GetBlockCount(mW, mH); - } - - await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); - } - - [Fact] - public async Task EncodeProgressReportParallelOneMipDds() - { - var testImage = ImageLoader.TestBlur1; - var encoder = new BcEncoder - { - Options = { IsParallel = true }, - OutputOptions = { MaxMipMapLevel = 1, FileFormat = OutputFileFormat.Dds } - }; - - var expectedTotal = encoder.GetBlockCount(testImage.Width, testImage.Height); - - await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); - } - - [Fact] - public async Task EncodeProgressReportNonParallelOneMipDds() - { - var testImage = ImageLoader.TestBlur1; - var encoder = new BcEncoder - { - Options = { IsParallel = false }, - OutputOptions = { MaxMipMapLevel = 1, FileFormat = OutputFileFormat.Dds } - }; - - var expectedTotal = encoder.GetBlockCount(testImage.Width, testImage.Height); - - await ExecuteEncodeProgressReport(encoder, testImage, expectedTotal); - } - } - - class SynchronousProgress : IProgress - { - private readonly Action handler; - - public SynchronousProgress(Action handler) - { - this.handler = handler; - } - - public void Report(T value) - { - handler(value); - } - } -} 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/SingleBlockTests.cs b/BCnEncTests/SingleBlockTests.cs deleted file mode 100644 index 89b0778..0000000 --- a/BCnEncTests/SingleBlockTests.cs +++ /dev/null @@ -1,143 +0,0 @@ -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)] - [InlineData(CompressionFormat.Bc3, CompressionQuality.Fast)] - [InlineData(CompressionFormat.Bc3, CompressionQuality.Balanced)] - [InlineData(CompressionFormat.Bc7, CompressionQuality.Fast)] - public void SingleBlockEncodeDecodeStream(CompressionFormat format, CompressionQuality quality) - { - var testImage = ImageLoader.TestAlpha1; - - var encoder = new BcEncoder() - { - OutputOptions = - { - Format = format, - Quality = quality - } - }; - - var pixels = TestHelper.GetSinglePixelArray(testImage); - var colors = MemoryMarshal.Cast(pixels) - .AsSpan2D(testImage.Height, testImage.Width); - - var ms = new MemoryStream(); - - for (var y = 0; y < testImage.Height; y+=4) - { - for (var x = 0; x < testImage.Width; x+=4) - { - encoder.EncodeBlock(colors.Slice(y, x, 4, 4), ms); - } - } - - Assert.Equal(ms.Position, encoder.GetBlockSize() * encoder.GetBlockCount(testImage.Width, testImage.Height)); - - ms.Position = 0; - - var decoder = new BcDecoder(); - - var decoded = new ColorRgba32[testImage.Height, testImage.Width]; - - for (var y = 0; y < testImage.Height; y += 4) - { - for (var x = 0; x < testImage.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 psnr = ImageQuality.PeakSignalToNoiseRatio(oPixels, dPixels, - format != CompressionFormat.Bc1); - - TestHelper.AssertPSNR(psnr, quality); - } - - [Theory] - [InlineData(CompressionFormat.Bc1, CompressionQuality.Fast)] - [InlineData(CompressionFormat.Bc1, CompressionQuality.Balanced)] - [InlineData(CompressionFormat.Bc3, CompressionQuality.Fast)] - [InlineData(CompressionFormat.Bc3, CompressionQuality.Balanced)] - [InlineData(CompressionFormat.Bc7, CompressionQuality.Fast)] - public void SingleBlockEncodeDecode(CompressionFormat format, CompressionQuality quality) - { - var testImage = ImageLoader.TestAlpha1; - - var encoder = new BcEncoder() - { - OutputOptions = - { - Format = format, - Quality = quality - } - }; - - var pixels = TestHelper.GetSinglePixelArray(testImage); - var colors = MemoryMarshal.Cast(pixels) - .AsSpan2D(testImage.Height, testImage.Width); - - Span buffer = new byte[encoder.GetBlockSize() * encoder.GetBlockCount(testImage.Width, testImage.Height)]; - - var blockIndex = 0; - for (var y = 0; y < testImage.Height; y += 4) - { - for (var x = 0; x < testImage.Width; x += 4) - { - var bytes = encoder.EncodeBlock(colors.Slice(y, x, 4, 4)); - bytes.CopyTo(buffer.Slice(blockIndex * encoder.GetBlockSize())); - blockIndex++; - } - } - - var decoder = new BcDecoder(); - - var decoded = new ColorRgba32[testImage.Height, testImage.Width]; - - blockIndex = 0; - for (var y = 0; y < testImage.Height; y += 4) - { - for (var x = 0; x < testImage.Width; x += 4) - { - - decoder.DecodeBlock( - buffer.Slice( - blockIndex * 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 psnr = ImageQuality.PeakSignalToNoiseRatio(oPixels, dPixels, - format != CompressionFormat.Bc1); - - TestHelper.AssertPSNR(psnr, quality); - } - } -} 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); + } } } From b093181611e624f5dd757d15c244a204b759074c Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:11:04 +0200 Subject: [PATCH 08/17] Add separate ImageSharp tests --- BCnEncTests/ImageSharpTests.cs | 388 +++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 BCnEncTests/ImageSharpTests.cs 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); + } + } +} From 69fcfaf63f0bd11d06491643333f1e9f39c0f816 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:12:23 +0200 Subject: [PATCH 09/17] Add MipMapper tests --- BCnEncTests/MipMapperTests.cs | 79 +++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 BCnEncTests/MipMapperTests.cs 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)"); + } + } +} From e529a89730b5b478d24e74a0f8963429e0d6fdb6 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:50:32 +0200 Subject: [PATCH 10/17] Better HdrImageTests --- BCnEncTests.Framework/HdrImageTests.cs | 41 +++++++++++++++++++++++--- BCnEncTests/HdrImageTests.cs | 23 +++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/BCnEncTests.Framework/HdrImageTests.cs b/BCnEncTests.Framework/HdrImageTests.cs index eda8e56..613b104 100644 --- a/BCnEncTests.Framework/HdrImageTests.cs +++ b/BCnEncTests.Framework/HdrImageTests.cs @@ -1,4 +1,8 @@ +using System; using System.IO; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; using BCnEncoder.Shared; using BCnEncTests.Support; using CommunityToolkit.HighPerformance; @@ -17,10 +21,39 @@ public void LoadHdr() Assert.True(hdrImg.height > 0); Assert.True(hdrImg.pixels.Length == hdrImg.width * hdrImg.height); - // Save as HDR to verify round-trip - var hdrOut = new HdrImage(new Span2D(hdrImg.pixels, hdrImg.height, hdrImg.width)); - using var outStream = File.OpenWrite("test_hdr_load.hdr"); - hdrOut.Write(outStream); + 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/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); + } } } From df04fce14c792e1969adc10365e389ca940a1581 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:12:41 +0200 Subject: [PATCH 11/17] Don't heap alloc on indices selection on netstandard 2.1 --- BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs index 7c9c911..0eb201d 100644 --- a/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs +++ b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs @@ -57,11 +57,17 @@ public static int[] Rank2SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, // 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]; - for (var k = 0; k < 16; k++) indices[k] = reducedIndicesBlock[k]; + 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]; @@ -122,11 +128,17 @@ public static int[] Rank3SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, // 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]; - for (var k = 0; k < 16; k++) indices[k] = reducedIndicesBlock[k]; + 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]; From dcb9f71ffe2b4ddb23806aa416c819ae5dc8b090 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:03:49 +0200 Subject: [PATCH 12/17] Add back helpful comments --- BCnEncTests.Shared/Bc7BlockTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/BCnEncTests.Shared/Bc7BlockTests.cs b/BCnEncTests.Shared/Bc7BlockTests.cs index a2904da..ef94955 100644 --- a/BCnEncTests.Shared/Bc7BlockTests.cs +++ b/BCnEncTests.Shared/Bc7BlockTests.cs @@ -96,10 +96,13 @@ public void DecodeErrorBlock() private void Type0Pack(Span output) { var subsetEndpoints = new[] { + //subset 1 new byte[]{0b1111, 0, 0}, new byte[]{0b1000, 0, 0}, + // subset 2 new byte[]{0, 0b1111, 0}, new byte[]{0, 0b1000, 0}, + //subset 3 new byte[]{0, 0, 0b1111}, new byte[]{0, 0, 0b1000} }; @@ -163,8 +166,10 @@ private void Type0Pack(Span output) private void Type1Pack(Span output) { var subsetEndpoints = new[] { + //subset 1 new byte[]{0b111111, 0b0100, 0}, new byte[]{0b0100, 0b111111, 0}, + // subset 2 new byte[]{0, 0, 0b111111}, new byte[]{0, 0, 0b1010} }; @@ -186,10 +191,13 @@ private void Type1Pack(Span output) private void Type2Pack(Span output) { var subsetEndpoints = new[] { + //subset 1 new byte[]{0b11111, 0, 0}, new byte[]{0b01000, 0, 0}, + // subset 2 new byte[]{0, 0b11111, 0}, new byte[]{0, 0b01000, 0}, + //subset 3 new byte[]{0, 0, 0b11111}, new byte[]{0, 0, 0b01000} }; @@ -210,8 +218,10 @@ private void Type2Pack(Span output) private void Type3Pack(Span output) { var subsetEndpoints = new[] { + //subset 1 new byte[]{0b1111111, 0b10100, 0}, new byte[]{0b10100, 0b1111111, 0}, + // subset 2 new byte[]{0, 0, 0b1111111}, new byte[]{0, 0, 0b11010} }; @@ -233,6 +243,7 @@ private void Type3Pack(Span output) private void Type4Pack(Span output) { var colorEndpoints = new[] { + //subset 1 new byte[]{0b11111, 0, 0b0100}, new byte[]{0, 0b11111, 0b0100} }; @@ -265,6 +276,7 @@ private void Type4Pack(Span output) private void Type5Pack(Span output) { var colorEndpoints = new[] { + //subset 1 new byte[]{0b1111111, 0b0100, 0}, new byte[]{0, 0, 0b1111111} }; @@ -294,6 +306,7 @@ private void Type5Pack(Span output) private void Type6Pack(Span output) { var colorEndpoints = new[] { + //subset 1 new byte[]{0b1111111, 0b0100, 0, 0b1111111}, new byte[]{0, 0, 0b1111111, 0b1111} }; @@ -314,8 +327,10 @@ private void Type6Pack(Span output) private void Type7Pack(Span output) { var colorEndpoints = new[] { + //subset 1 new byte[]{0b11111, 0b0100, 0, 0b11111}, new byte[]{0, 0b11111, 0, 0b111}, + //subset 2 new byte[]{0b11111, 0b0100, 0, 0b1111}, new byte[]{0b10100, 0, 0b11111, 0b11111} }; From 26a2c1e199691154bf0ccea8c3c5394b912da34e Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:31:59 +0200 Subject: [PATCH 13/17] Don't use LangVersion 8.0 for .NET Framework tests --- .../BCnEncTests.Framework.csproj | 1 - BCnEncTests.Framework/HdrImageTests.cs | 50 ++++++----- BCnEncTests.Framework/Support/ImageLoader.cs | 18 ++-- BCnEncTests.Framework/Support/TestHelper.cs | 89 +++++++++++-------- BCnEncTests.Shared/Bc6EncoderTests.cs | 12 ++- BCnEncTests.Shared/Bc6HDecoderTests.cs | 70 +++++++++------ BCnEncTests.Shared/Bc7BlockTests.cs | 6 +- BCnEncTests.Shared/ClusterTests.cs | 6 +- BCnEncTests.Shared/DdsReadTests.cs | 19 ++-- BCnEncTests.Shared/EncodingTest.cs | 6 +- BCnEncTests.Shared/ProgressTests.cs | 6 +- BCnEncTests.Shared/RawTests.cs | 72 +++++++-------- 12 files changed, 203 insertions(+), 152 deletions(-) diff --git a/BCnEncTests.Framework/BCnEncTests.Framework.csproj b/BCnEncTests.Framework/BCnEncTests.Framework.csproj index e14d3cc..c928b90 100644 --- a/BCnEncTests.Framework/BCnEncTests.Framework.csproj +++ b/BCnEncTests.Framework/BCnEncTests.Framework.csproj @@ -2,7 +2,6 @@ net481 - 8.0 false true diff --git a/BCnEncTests.Framework/HdrImageTests.cs b/BCnEncTests.Framework/HdrImageTests.cs index 613b104..75e9c8e 100644 --- a/BCnEncTests.Framework/HdrImageTests.cs +++ b/BCnEncTests.Framework/HdrImageTests.cs @@ -15,25 +15,27 @@ 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++) + using (var stream = File.OpenRead("../../../../BCnEncTests/testImages/test_hdr_kiara.hdr")) { - 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 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); } - 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] @@ -47,13 +49,15 @@ public async Task DecodeAllMipMapsHdrStreamAsync() 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; + 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); + var images = await decoder.DecodeAllMipMapsHdr2DAsync(ms); + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + Assert.True(images.Length > 1); + } } } } diff --git a/BCnEncTests.Framework/Support/ImageLoader.cs b/BCnEncTests.Framework/Support/ImageLoader.cs index 2dc9958..0a90454 100644 --- a/BCnEncTests.Framework/Support/ImageLoader.cs +++ b/BCnEncTests.Framework/Support/ImageLoader.cs @@ -35,8 +35,10 @@ public static class ImageLoader internal static Memory2D LoadTestImage(string filename) { - using var bmp = new Bitmap(filename); - return FromBitmap(bmp); + using (var bmp = new Bitmap(filename)) + { + return FromBitmap(bmp); + } } internal static unsafe Memory2D FromBitmap(Bitmap bmp) @@ -72,8 +74,10 @@ public static class DdsLoader internal static DdsFile LoadDdsFile(string filename) { - using var fs = File.OpenRead(filename); - return DdsFile.Load(fs); + using (var fs = File.OpenRead(filename)) + { + return DdsFile.Load(fs); + } } } @@ -91,8 +95,10 @@ public static class KtxLoader internal static KtxFile LoadKtxFile(string filename) { - using var fs = File.OpenRead(filename); - return KtxFile.Load(fs); + using (var fs = File.OpenRead(filename)) + { + return KtxFile.Load(fs); + } } } } diff --git a/BCnEncTests.Framework/Support/TestHelper.cs b/BCnEncTests.Framework/Support/TestHelper.cs index e5e2280..ba9cc8c 100644 --- a/BCnEncTests.Framework/Support/TestHelper.cs +++ b/BCnEncTests.Framework/Support/TestHelper.cs @@ -51,8 +51,10 @@ public static void ExecuteDecodingTest(KtxFile file, string outputFile) 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); + using (var outFs = File.OpenWrite(outputFile)) + { + SaveAsPng(pixels, outFs); + } } #region Dds @@ -70,15 +72,16 @@ public static void ExecuteDdsWritingTest(Memory2D[] images, Compres encoder.OutputOptions.Format = format; encoder.OutputOptions.FileFormat = OutputFileFormat.Dds; - using var fs = File.OpenWrite(outputFile); - - if (images.Length == 1) + using (var fs = File.OpenWrite(outputFile)) { - encoder.EncodeToStream(images[0], fs); - } - else - { - encoder.EncodeCubeMapToStream(images[0], images[1], images[2], images[3], images[4], images[5], fs); + 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); + } } } @@ -102,8 +105,10 @@ public static void ExecuteDdsReadingTest(DdsFile file, DxgiFormat format, string Assert.Contains(pixels, x => x.a == 0); } - using var outFs = File.OpenWrite(string.Format(outputFile, i)); - SaveAsPng(images[i], outFs); + using (var outFs = File.OpenWrite(string.Format(outputFile, i))) + { + SaveAsPng(images[i], outFs); + } } } @@ -128,26 +133,30 @@ await Assert.ThrowsAnyAsync(() => public static float DecodeKtxCheckPSNR(string filename, Memory2D original) { - using var fs = File.OpenRead(filename); - var ktx = KtxFile.Load(fs); - var decoder = new BcDecoder() + using (var fs = File.OpenRead(filename)) { - OutputOptions = { Bc4Component = ColorComponent.Luminance } - }; - var decoded = decoder.Decode2D(ktx); + var ktx = KtxFile.Load(fs); + var decoder = new BcDecoder() + { + OutputOptions = { Bc4Component = ColorComponent.Luminance } + }; + var decoded = decoder.Decode2D(ktx); - return CalculatePSNR(original, decoded); + 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(); + using (var fs = File.OpenRead(filename)) + { + var ktx = KtxFile.Load(fs); + var decoder = new BcDecoder(); - var decoded = decoder.DecodeHdr(ktx); + var decoded = decoder.DecodeHdr(ktx); - return ImageQuality.CalculateLogRMSE(original.pixels, decoded); + return ImageQuality.CalculateLogRMSE(original.pixels, decoded); + } } public static void ExecuteEncodingTest(Memory2D image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) @@ -234,25 +243,27 @@ 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++) + using (var bmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { - for (int x = 0; x < width; x++) + 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++) { - var c = span[y, x]; - ptr[0] = c.b; - ptr[1] = c.g; - ptr[2] = c.r; - ptr[3] = c.a; - ptr += 4; + 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); } - bmp.UnlockBits(data); - bmp.Save(stream, ImageFormat.Png); } public static void SaveAsPng(ColorRgbFloat[] pixels, int width, int height, Stream stream) diff --git a/BCnEncTests.Shared/Bc6EncoderTests.cs b/BCnEncTests.Shared/Bc6EncoderTests.cs index 6a5bcad..ced39c6 100644 --- a/BCnEncTests.Shared/Bc6EncoderTests.cs +++ b/BCnEncTests.Shared/Bc6EncoderTests.cs @@ -353,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] @@ -367,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.Shared/Bc6HDecoderTests.cs b/BCnEncTests.Shared/Bc6HDecoderTests.cs index 75aa7c3..2cbb609 100644 --- a/BCnEncTests.Shared/Bc6HDecoderTests.cs +++ b/BCnEncTests.Shared/Bc6HDecoderTests.cs @@ -31,11 +31,15 @@ public void DecodeDds() 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); + } - using var pngFs = File.OpenWrite("decoding_test_dds_bc6h.png"); - TestHelper.SaveAsPng(decoded, width, height, pngFs); + 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); } @@ -53,11 +57,15 @@ public void DecodeKtx() 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); + } - using var pngFs = File.OpenWrite("decoding_test_ktx_bc6h.png"); - TestHelper.SaveAsPng(decoded, width, height, pngFs); + 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); } @@ -71,28 +79,32 @@ public void AllBlocksDecodesExact() var decoder = new BcDecoder(); var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraDds); + Stream fs; #if NETCOREAPP - using var fs = File.OpenRead("../../../testImages/test_hdr_kiara_dds_float16_data.bin"); + fs = File.OpenRead("../../../testImages/test_hdr_kiara_dds_float16_data.bin"); #else - using var fs = File.OpenRead("../../../../BCnEncTests/testImages/test_hdr_kiara_dds_float16_data.bin"); + fs = File.OpenRead("../../../../BCnEncTests/testImages/test_hdr_kiara_dds_float16_data.bin"); #endif - 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++) + 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); + } } } @@ -152,8 +164,10 @@ public void DecodeErrorBlock() 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.Shared/Bc7BlockTests.cs b/BCnEncTests.Shared/Bc7BlockTests.cs index ef94955..8ac34ea 100644 --- a/BCnEncTests.Shared/Bc7BlockTests.cs +++ b/BCnEncTests.Shared/Bc7BlockTests.cs @@ -87,8 +87,10 @@ public void DecodeErrorBlock() Assert.Contains(new ColorRgba32(255, 0, 255), pixels); var decoded = new Memory2D(pixels, height * 4, width * 4); - using var fs = File.OpenWrite("test_decode_bc7_error.png"); - TestHelper.SaveAsPng(decoded, fs); + using (var fs = File.OpenWrite("test_decode_bc7_error.png")) + { + TestHelper.SaveAsPng(decoded, fs); + } } #region Type Packs diff --git a/BCnEncTests.Shared/ClusterTests.cs b/BCnEncTests.Shared/ClusterTests.cs index d8c20b8..a30ab52 100644 --- a/BCnEncTests.Shared/ClusterTests.cs +++ b/BCnEncTests.Shared/ClusterTests.cs @@ -46,8 +46,10 @@ public void Clusterize() } var result = new Memory2D(pixels, height, width); - using var fs = File.OpenWrite("test_cluster.png"); - TestHelper.SaveAsPng(result, fs); + using (var fs = File.OpenWrite("test_cluster.png")) + { + TestHelper.SaveAsPng(result, fs); + } } } } diff --git a/BCnEncTests.Shared/DdsReadTests.cs b/BCnEncTests.Shared/DdsReadTests.cs index 90c9279..3c2a35e 100644 --- a/BCnEncTests.Shared/DdsReadTests.cs +++ b/BCnEncTests.Shared/DdsReadTests.cs @@ -36,15 +36,18 @@ public void ReadBc7() [Fact] public void ReadFromStream() { - using var fs = File.OpenRead(DdsLoader.TestDecompressBc1Name); - - var decoder = new BcDecoder(); - var images = decoder.DecodeAllMipMaps2D(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"); - TestHelper.SaveAsPng(images[i], outFs); + 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.Shared/EncodingTest.cs b/BCnEncTests.Shared/EncodingTest.cs index 055de03..e4f3edd 100644 --- a/BCnEncTests.Shared/EncodingTest.cs +++ b/BCnEncTests.Shared/EncodingTest.cs @@ -308,8 +308,10 @@ public void WriteCubeMapFile() 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); + 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.Shared/ProgressTests.cs b/BCnEncTests.Shared/ProgressTests.cs index c82777b..83dab93 100644 --- a/BCnEncTests.Shared/ProgressTests.cs +++ b/BCnEncTests.Shared/ProgressTests.cs @@ -31,8 +31,10 @@ private async Task ExecuteEncodeProgressReport(BcEncoder encoder, Memory2D diff --git a/README.md b/README.md index 367c927..d8da209 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,10 +21,16 @@ 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: From 74eda8a330759d81e6307f19aefc547e40687825 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:37:42 +0200 Subject: [PATCH 16/17] Add missing space in readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d8da209..2e157c7 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ 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. From 51d3199b3bc374e0bdda3947d9f347cb6235a9a0 Mon Sep 17 00:00:00 2001 From: Nominom <7089998+Nominom@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:45:33 +0200 Subject: [PATCH 17/17] Add bugfix to release notes --- BCnEnc.Net/BCnEncoder.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BCnEnc.Net/BCnEncoder.csproj b/BCnEnc.Net/BCnEncoder.csproj index 94038b7..64bb373 100644 --- a/BCnEnc.Net/BCnEncoder.csproj +++ b/BCnEnc.Net/BCnEncoder.csproj @@ -33,7 +33,8 @@ Supported formats are: 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.3.0 -- Add .NET Standard 2.0 support +- Add .NET Standard 2.0 support +- Fix some DecodeAllMipMaps methods not decoding all MipMaps