From 8d841a9488da62111311c59ea690890dd9081492 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 01/12] 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 af3dcaf53603db164aa0c797682ab73398772209 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 02/12] 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 601ce043ee1422d66ff6688f2b79ed67beeaf7d4 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 03/12] 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 a780298c405a30c5a2560082504e61304b08f088 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 04/12] 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 40378aeb696a167e6177934e06503f67f1834d3a 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 05/12] 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 bd546066048c4bae67d353a20c665cfcadccdbde 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 06/12] 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 9764802d6b8c0998056ba97225d18d0704b58fe1 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 07/12] 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 3433608229f94fd2205ed63e83e5cfa835ca3cd0 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 08/12] 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 be6c0b248ff13db477e2910d5280c0e2587bb8d8 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 09/12] 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 1b887e557af8244ffb26c76c99f88418f0d6b9f3 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 10/12] 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 536e7eb8173c5ddc9940dc9fd66da7d9f781af2a 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 11/12] 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 65fa9454648314097f2723962a3d23bf9529818c 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 12/12] 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