diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1bf2168 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = tab +indent_size = 4 + +[*.{diff,md}] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 7308a9d..96d29b4 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -1,25 +1,35 @@ -name: Tests +name: build and test on: push: branches: [ master ] pull_request: branches: [ master ] + paths: + - '**.cs' + - '**.csproj' + +env: + DOTNET_VERSION: '9.0.x' # The .NET SDK version to use jobs: - build: + build-and-test: - runs-on: ubuntu-latest + name: build-and-test + runs-on: windows-latest steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 with: - dotnet-version: 3.1.101 + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Install dependencies run: dotnet restore + - name: Build run: dotnet build --configuration Release --no-restore + - name: Test - run: dotnet test --no-restore --verbosity normal + run: dotnet test --no-restore --verbosity normal \ No newline at end of file diff --git a/BCnEnc.Net/AssemblyInfo.cs b/BCnEnc.Net/AssemblyInfo.cs index 6d23fdb..0bce899 100644 --- a/BCnEnc.Net/AssemblyInfo.cs +++ b/BCnEnc.Net/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("BCnEncTests")] +[assembly: InternalsVisibleTo("BCnEncTests.Framework")] diff --git a/BCnEnc.Net/BCnEncoder.csproj b/BCnEnc.Net/BCnEncoder.csproj index dc6a69f..64bb373 100644 --- a/BCnEnc.Net/BCnEncoder.csproj +++ b/BCnEnc.Net/BCnEncoder.csproj @@ -1,8 +1,8 @@  - netstandard2.1 - + netstandard2.1;netstandard2.0 + 8.0 true true snupkg @@ -12,11 +12,11 @@ MIT OR Unlicense false - 1.2.1 + 2.3.0 Nominom BCnEncoder.Net - BCnEncoder.NET is a library for compressing rgba images to different block-compressed formats. Both ktx and dds output formats are supported. It has no native dependencies and is .NET Standard 2.1 compatible. + BCnEncoder.NET is a library for compressing rgba images to different block-compressed formats. Both ktx and dds output file-formats are supported. It has no native dependencies and is .NET Standard 2.1 & 2.0 compatible. Supported formats are: Raw unsigned byte R, RG, RGB and RGBA formats @@ -25,13 +25,16 @@ Supported formats are: BC3 (S3TC DXT5) BC4 (RGTC1) BC5 (RGTC2) + BC6 (BPTC-FLOAT) BC7 (BPTC) BCnEncoder.Net https://github.com/Nominom/BCnEncoder.NET git - BCn BC BC1 BC2 BC3 BC4 BC5 BC7 BPTC RGTC S3TC DXT1 DXT3 DXT5 ktx dds texture compression encoding decoding decompression image gpu + BCn BC BC1 BC2 BC3 BC4 BC5 BC6 BC6H BC7 BPTC RGTC S3TC DXT1 DXT3 DXT5 ktx dds texture compression encoding decoding decompression image gpu https://github.com/Nominom/BCnEncoder.NET - Added xml documentation to nuget. + 2.3.0 +- Add .NET Standard 2.0 support +- Fix some DecodeAllMipMaps methods not decoding all MipMaps @@ -44,8 +47,15 @@ Supported formats are: - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/BCnEnc.Net/Decoder/Bc7Decoder.cs b/BCnEnc.Net/Decoder/Bc7Decoder.cs deleted file mode 100644 index 11a72b6..0000000 --- a/BCnEnc.Net/Decoder/Bc7Decoder.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using BCnEncoder.Shared; - -namespace BCnEncoder.Decoder -{ - internal class Bc7Decoder : IBcBlockDecoder - { - public RawBlock4X4Rgba32[,] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight, out int blockWidth, out int blockHeight) { - blockWidth = (int)MathF.Ceiling(pixelWidth / 4.0f); - blockHeight = (int)MathF.Ceiling(pixelHeight / 4.0f); - - if (data.Length != (blockWidth * blockHeight * Marshal.SizeOf())) { - throw new InvalidDataException(); - } - - var encodedBlocks = MemoryMarshal.Cast(data); - - RawBlock4X4Rgba32[,] output = new RawBlock4X4Rgba32[blockWidth, blockHeight]; - - for (int x = 0; x < blockWidth; x++) { - for (int y = 0; y < blockHeight; y++) { - var rawBlock = encodedBlocks[x + y * blockWidth]; - output[x, y] = rawBlock.Decode(); - } - } - - return output; - } - } -} diff --git a/BCnEnc.Net/Decoder/BcBlockDecoder.cs b/BCnEnc.Net/Decoder/BcBlockDecoder.cs index 5bfa778..7706639 100644 --- a/BCnEnc.Net/Decoder/BcBlockDecoder.cs +++ b/BCnEnc.Net/Decoder/BcBlockDecoder.cs @@ -1,156 +1,190 @@ -using System; +using System; using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using BCnEncoder.Shared; namespace BCnEncoder.Decoder { - internal interface IBcBlockDecoder { - RawBlock4X4Rgba32[,] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight, out int blockWidth, - out int blockHeight); + internal interface IBcBlockDecoder where T : unmanaged + { + T[] Decode(ReadOnlyMemory data, OperationContext context); + T DecodeBlock(ReadOnlySpan data); } - internal class Bc1NoAlphaDecoder : IBcBlockDecoder { - public RawBlock4X4Rgba32[,] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight, out int blockWidth, out int blockHeight) { - blockWidth = (int)MathF.Ceiling(pixelWidth / 4.0f); - blockHeight = (int)MathF.Ceiling(pixelHeight / 4.0f); + internal abstract class BaseBcBlockDecoder : IBcBlockDecoder where T : unmanaged where TBlock : unmanaged + { + private static readonly object lockObj = new object(); - if (data.Length != (blockWidth * blockHeight * Marshal.SizeOf())) { - throw new InvalidDataException(); - } - - var encodedBlocks = MemoryMarshal.Cast(data); + public TBlock[] Decode(ReadOnlyMemory data, OperationContext context) + { - RawBlock4X4Rgba32[,] output = new RawBlock4X4Rgba32[blockWidth, blockHeight]; - - for (int x = 0; x < blockWidth; x++) { - for (int y = 0; y < blockHeight; y++) { - output[x, y] = encodedBlocks[x + y * blockWidth].Decode(false); - } + if (data.Length % Unsafe.SizeOf() != 0) + { + throw new InvalidDataException("Given data does not align with the block length."); } - return output; - } - } - - internal class Bc1ADecoder : IBcBlockDecoder { - public RawBlock4X4Rgba32[,] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight, out int blockWidth, out int blockHeight) { - blockWidth = (int)MathF.Ceiling(pixelWidth / 4.0f); - blockHeight = (int)MathF.Ceiling(pixelHeight / 4.0f); - - if (data.Length != (blockWidth * blockHeight * Marshal.SizeOf())) { - throw new InvalidDataException(); + var blockCount = data.Length / Unsafe.SizeOf(); + var output = new TBlock[blockCount]; + + var currentBlocks = 0; + if (context.IsParallel) + { + var options = new ParallelOptions + { + CancellationToken = context.CancellationToken, + MaxDegreeOfParallelism = context.TaskCount + }; + Parallel.For(0, blockCount, options, i => + { + var encodedBlocks = MemoryMarshal.Cast(data.Span); + output[i] = DecodeBlock(encodedBlocks[i]); + + if (context.Progress != null) + { + lock (lockObj) + { + context.Progress.Report(++currentBlocks); + } + } + }); } + else + { + var encodedBlocks = MemoryMarshal.Cast(data.Span); + for (var i = 0; i < blockCount; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); - var encodedBlocks = MemoryMarshal.Cast(data); + output[i] = DecodeBlock(encodedBlocks[i]); - RawBlock4X4Rgba32[,] output = new RawBlock4X4Rgba32[blockWidth, blockHeight]; - - for (int x = 0; x < blockWidth; x++) { - for (int y = 0; y < blockHeight; y++) { - output[x, y] = encodedBlocks[x + y * blockWidth].Decode(true); + context.Progress?.Report(++currentBlocks); } } return output; } - } - - internal class Bc2Decoder : IBcBlockDecoder { - public RawBlock4X4Rgba32[,] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight, out int blockWidth, out int blockHeight) { - blockWidth = (int)MathF.Ceiling(pixelWidth / 4.0f); - blockHeight = (int)MathF.Ceiling(pixelHeight / 4.0f); - - if (data.Length != (blockWidth * blockHeight * Marshal.SizeOf())) { - throw new InvalidDataException(); - } - var encodedBlocks = MemoryMarshal.Cast(data); - - RawBlock4X4Rgba32[,] output = new RawBlock4X4Rgba32[blockWidth, blockHeight]; + public TBlock DecodeBlock(ReadOnlySpan data) + { + var encodedBlock = MemoryMarshal.Cast(data)[0]; + return DecodeBlock(encodedBlock); + } - for (int x = 0; x < blockWidth; x++) { - for (int y = 0; y < blockHeight; y++) { - output[x, y] = encodedBlocks[x + y * blockWidth].Decode(); - } - } + protected abstract TBlock DecodeBlock(T block); + } - return output; + internal class Bc1NoAlphaDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(Bc1Block block) + { + return block.Decode(false); } } - internal class Bc3Decoder : IBcBlockDecoder { - public RawBlock4X4Rgba32[,] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight, out int blockWidth, out int blockHeight) { - blockWidth = (int)MathF.Ceiling(pixelWidth / 4.0f); - blockHeight = (int)MathF.Ceiling(pixelHeight / 4.0f); - - if (data.Length != (blockWidth * blockHeight * Marshal.SizeOf())) { - throw new InvalidDataException(); - } - - var encodedBlocks = MemoryMarshal.Cast(data); - - RawBlock4X4Rgba32[,] output = new RawBlock4X4Rgba32[blockWidth, blockHeight]; - - for (int x = 0; x < blockWidth; x++) { - for (int y = 0; y < blockHeight; y++) { - output[x, y] = encodedBlocks[x + y * blockWidth].Decode(); - } - } + internal class Bc1ADecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(Bc1Block block) + { + return block.Decode(true); + } + } - return output; + internal class Bc2Decoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(Bc2Block block) + { + return block.Decode(); } } - internal class Bc4Decoder : IBcBlockDecoder { - private readonly bool redAsLuminance; - public Bc4Decoder(bool redAsLuminance) { - this.redAsLuminance = redAsLuminance; + internal class Bc3Decoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(Bc3Block block) + { + return block.Decode(); } + } - public RawBlock4X4Rgba32[,] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight, out int blockWidth, out int blockHeight) { - blockWidth = (int)MathF.Ceiling(pixelWidth / 4.0f); - blockHeight = (int)MathF.Ceiling(pixelHeight / 4.0f); + internal class Bc4Decoder : BaseBcBlockDecoder + { + private readonly ColorComponent component; - if (data.Length != (blockWidth * blockHeight * Marshal.SizeOf())) { - throw new InvalidDataException(); - } + public Bc4Decoder(ColorComponent component) + { + this.component = component; + } - var encodedBlocks = MemoryMarshal.Cast(data); + protected override RawBlock4X4Rgba32 DecodeBlock(Bc4Block block) + { + return block.Decode(component); + } + } - RawBlock4X4Rgba32[,] output = new RawBlock4X4Rgba32[blockWidth, blockHeight]; + internal class Bc5Decoder : BaseBcBlockDecoder + { + private readonly ColorComponent component1; + private readonly ColorComponent component2; - for (int x = 0; x < blockWidth; x++) { - for (int y = 0; y < blockHeight; y++) { - output[x, y] = encodedBlocks[x + y * blockWidth].Decode(redAsLuminance); - } - } + public Bc5Decoder(ColorComponent component1, ColorComponent component2) + { + this.component1 = component1; + this.component2 = component2; + } - return output; + protected override RawBlock4X4Rgba32 DecodeBlock(Bc5Block block) + { + return block.Decode(component1, component2); } } - internal class Bc5Decoder : IBcBlockDecoder { - - public RawBlock4X4Rgba32[,] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight, out int blockWidth, out int blockHeight) { - blockWidth = (int)MathF.Ceiling(pixelWidth / 4.0f); - blockHeight = (int)MathF.Ceiling(pixelHeight / 4.0f); + internal class Bc6UDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4RgbFloat DecodeBlock(Bc6Block block) + { + return block.Decode(false); + } + } - if (data.Length != (blockWidth * blockHeight * Marshal.SizeOf())) { - throw new InvalidDataException(); - } + internal class Bc6SDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4RgbFloat DecodeBlock(Bc6Block block) + { + return block.Decode(true); + } + } - var encodedBlocks = MemoryMarshal.Cast(data); + internal class Bc7Decoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(Bc7Block block) + { + return block.Decode(); + } + } - RawBlock4X4Rgba32[,] output = new RawBlock4X4Rgba32[blockWidth, blockHeight]; + internal class AtcDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(AtcBlock block) + { + return block.Decode(); + } + } - for (int x = 0; x < blockWidth; x++) { - for (int y = 0; y < blockHeight; y++) { - output[x, y] = encodedBlocks[x + y * blockWidth].Decode(); - } - } + internal class AtcExplicitAlphaDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(AtcExplicitAlphaBlock block) + { + return block.Decode(); + } + } - return output; + internal class AtcInterpolatedAlphaDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(AtcInterpolatedAlphaBlock block) + { + return block.Decode(); } } } diff --git a/BCnEnc.Net/Decoder/BcDecoder.cs b/BCnEnc.Net/Decoder/BcDecoder.cs index 2b50ee1..c11ec62 100644 --- a/BCnEnc.Net/Decoder/BcDecoder.cs +++ b/BCnEnc.Net/Decoder/BcDecoder.cs @@ -1,437 +1,2020 @@ -using System; -using System.IO; -using System.Text; +using BCnEncoder.Decoder.Options; using BCnEncoder.Shared; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; + +namespace BCnEncoder.Decoder +{ + /// + /// Decodes compressed files into Rgba Format. + /// + public class BcDecoder + { + /// + /// The input options of the decoder. + /// + public DecoderInputOptions InputOptions { get; } = new DecoderInputOptions(); + + /// + /// The options for the decoder. + /// + public DecoderOptions Options { get; } = new DecoderOptions(); + + /// + /// The output options of the decoder. + /// + public DecoderOutputOptions OutputOptions { get; } = new DecoderOutputOptions(); + #region LDR + #region Async Api + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeRawAsync(Stream inputStream, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return Task.Run(() => DecodeRawInternal(dataArray, pixelWidth, pixelHeight, format, token), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeRawAsync(ReadOnlyMemory input, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default) + { + return Task.Run(() => DecodeRawInternal(input, pixelWidth, pixelHeight, format, token), token); + } + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, false, token)[0], token); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAllMipMapsAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, true, token), token); + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, false, token)[0], token); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAllMipMapsAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, true, token), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the raw encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeRaw2DAsync(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return Task.Run(() => DecodeRawInternal(dataArray, pixelWidth, pixelHeight, format, token) + .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeRaw2DAsync(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + return Task.Run(() => DecodeRawInternal(input, pixelWidth, pixelHeight, format, token) + .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// + /// The stream containing a Ktx or Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> Decode2DAsync(Stream inputStream, CancellationToken token = default) + { + return Task.Run(() => DecodeFromStreamInternal2D(inputStream, false, token)[0], token); + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// + /// The stream containing a Ktx or Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMaps2DAsync(Stream inputStream, CancellationToken token = default) + { + return Task.Run(() => DecodeFromStreamInternal2D(inputStream, true, token), token); + } + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> Decode2DAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, false, token)[0] + .AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth), token); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMaps2DAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => + { + var decoded = DecodeInternal(file, true, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + }, token); + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> Decode2DAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, false, token)[0] + .AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth), token); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMaps2DAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => + { + var decoded = DecodeInternal(file, true, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + }, token); + } + + #endregion + + #region Sync API + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public ColorRgba32[] DecodeRaw(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return DecodeRaw(dataArray, pixelWidth, pixelHeight, format); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The byte array containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public ColorRgba32[] DecodeRaw(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format) + { + return DecodeRawInternal(input, pixelWidth, pixelHeight, format, default); + } + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The decoded image. + public ColorRgba32[] Decode(KtxFile file) + { + return DecodeInternal(file, false, default)[0]; + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// An array of decoded images. + public ColorRgba32[][] DecodeAllMipMaps(KtxFile file) + { + return DecodeInternal(file, true, default); + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The decoded image. + public ColorRgba32[] Decode(DdsFile file) + { + return DecodeInternal(file, false, default)[0]; + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// An array of decoded images. + public ColorRgba32[][] DecodeAllMipMaps(DdsFile file) + { + return DecodeInternal(file, true, default); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public Memory2D DecodeRaw2D(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + var decoded = DecodeRaw(dataArray, pixelWidth, pixelHeight, format); + return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The byte array containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public Memory2D DecodeRaw2D(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var decoded = DecodeRawInternal(input, pixelWidth, pixelHeight, format, default); + return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// + /// The stream containing a Ktx or Dds file. + /// The decoded image. + public Memory2D Decode2D(Stream inputStream) + { + return DecodeFromStreamInternal2D(inputStream, false, default)[0]; + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// + /// The stream containing a Ktx or Dds file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMaps2D(Stream inputStream) + { + return DecodeFromStreamInternal2D(inputStream, true, default); + } + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The decoded image. + public Memory2D Decode2D(KtxFile file) + { + return DecodeInternal(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMaps2D(KtxFile file) + { + var decoded = DecodeInternal(file, true, default); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The decoded image. + public Memory2D Decode2D(DdsFile file) + { + return DecodeInternal(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMaps2D(DdsFile file) + { + var decoded = DecodeInternal(file, true, default); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + } + + /// + /// Decode a single block from raw bytes and return it as a . + /// Input Span size needs to equal the block size. + /// To get the block size (in bytes) of the compression format used, see . + /// + /// The encoded block in bytes. + /// The compression format used. + /// The decoded 4x4 block. + public Memory2D DecodeBlock(ReadOnlySpan blockData, CompressionFormat format) + { + var output = new ColorRgba32[4, 4]; + DecodeBlockInternal(blockData, format, output); + return output; + } + + /// + /// Decode a single block from raw bytes and write it to the given output span. + /// Output span size must be exactly 4x4 and input Span size needs to equal the block size. + /// To get the block size (in bytes) of the compression format used, see . + /// + /// The encoded block in bytes. + /// The compression format used. + /// The destination span of the decoded data. + public void DecodeBlock(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan) + { + if (outputSpan.Width != 4 || outputSpan.Height != 4) + { + throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4"); + } + DecodeBlockInternal(blockData, format, outputSpan); + } + + /// + /// Decode a single block from a stream and write it to the given output span. + /// Output span size must be exactly 4x4. + /// + /// The stream to read encoded blocks from. + /// The compression format used. + /// The destination span of the decoded data. + /// The number of bytes read from the stream. Zero (0) if reached the end of stream. + public int DecodeBlock(Stream inputStream, CompressionFormat format, Span2D outputSpan) + { + if (outputSpan.Width != 4 || outputSpan.Height != 4) + { + throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4"); + } + + Span input = stackalloc byte[16]; + input = input.Slice(0, GetBlockSize(format)); + + var bytesRead = inputStream.Read(input); + + if (bytesRead == 0) + { + return 0; //End of stream + } + + if (bytesRead != input.Length) + { + throw new Exception("Input stream does not have enough data available for a full block."); + } + + DecodeBlockInternal(input, format, outputSpan); + return bytesRead; + } + + /// + /// Check whether a file is encoded in a supported format. + /// + /// The loaded ktx file to check + /// If the format of the file is one of the supported formats. + public bool IsSupportedFormat(KtxFile file) + { + return GetCompressionFormat(file.header.GlInternalFormat) != CompressionFormat.Unknown; + } + + /// + /// Check whether a file is encoded in a supported format. + /// + /// The loaded dds file to check + /// If the format of the file is one of the supported formats. + public bool IsSupportedFormat(DdsFile file) + { + return GetCompressionFormat(file) != CompressionFormat.Unknown; + } + + /// + /// Gets the format of the file. + /// + /// The loaded ktx file to check + /// The of the file. + public CompressionFormat GetFormat(KtxFile file) + { + return GetCompressionFormat(file.header.GlInternalFormat); + } + + /// + /// Gets the format of the file. + /// + /// The loaded dds file to check + /// The of the file. + public CompressionFormat GetFormat(DdsFile file) + { + return GetCompressionFormat(file); + } + + + #endregion + #endregion + + #region HDR + #region Async Api + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeRawHdrAsync(Stream inputStream, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return Task.Run(() => DecodeRawInternalHdr(dataArray, pixelWidth, pixelHeight, format, token), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeRawHdrAsync(ReadOnlyMemory input, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default) + { + return Task.Run(() => DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, token), token); + } + + /// + /// Decode the main image from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeHdrAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, false, token)[0], token); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAllMipMapsHdrAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, true, token), token); + } + + /// + /// Decode the main image from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeHdrAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, false, token)[0], token); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAllMipMapsHdrAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, true, token), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing the raw encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeRawHdr2DAsync(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return Task.Run(() => DecodeRawInternalHdr(dataArray, pixelWidth, pixelHeight, format, token) + .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeRawHdr2DAsync(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + return Task.Run(() => DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, token) + .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing a Ktx or Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeHdr2DAsync(Stream inputStream, CancellationToken token = default) + { + return Task.Run(() => DecodeFromStreamInternalHdr2D(inputStream, false, token)[0], token); + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing a Ktx or Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMapsHdr2DAsync(Stream inputStream, CancellationToken token = default) + { + return Task.Run(() => DecodeFromStreamInternalHdr2D(inputStream, true, token), token); + } + + /// + /// Decode the main image from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeHdr2DAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, false, token)[0] + .AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth), token); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMapsHdr2DAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => + { + var decoded = DecodeInternalHdr(file, true, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + }, token); + } + + /// + /// Decode the main image from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeHdr2DAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, false, token)[0] + .AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth), token); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMapsHdr2DAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => + { + var decoded = DecodeInternalHdr(file, true, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + }, token); + } + + #endregion + + #region Sync API + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public ColorRgbFloat[] DecodeRawHdr(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return DecodeRawHdr(dataArray, pixelWidth, pixelHeight, format); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The byte array containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public ColorRgbFloat[] DecodeRawHdr(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format) + { + return DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, default); + } + + /// + /// Decode the main image from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The decoded image. + public ColorRgbFloat[] DecodeHdr(KtxFile file) + { + return DecodeInternalHdr(file, false, default)[0]; + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// An array of decoded images. + public ColorRgbFloat[][] DecodeAllMipMapsHdr(KtxFile file) + { + return DecodeInternalHdr(file, true, default); + } + + /// + /// Decode the main image from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The decoded image. + public ColorRgbFloat[] DecodeHdr(DdsFile file) + { + return DecodeInternalHdr(file, false, default)[0]; + } + + /// + /// Decode all available mipmaps from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// An array of decoded images. + public ColorRgbFloat[][] DecodeAllMipMapsHdr(DdsFile file) + { + return DecodeInternalHdr(file, true, default); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing the encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public Memory2D DecodeRawHdr2D(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + var decoded = DecodeRawHdr(dataArray, pixelWidth, pixelHeight, format); + return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The byte array containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public Memory2D DecodeRawHdr2D(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var decoded = DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, default); + return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing a Ktx or Dds file. + /// The decoded image. + public Memory2D DecodeHdr2D(Stream inputStream) + { + return DecodeFromStreamInternalHdr2D(inputStream, false, default)[0]; + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing a Ktx or Dds file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMapsHdr2D(Stream inputStream) + { + return DecodeFromStreamInternalHdr2D(inputStream, true, default); + } + + /// + /// Decode the main image from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The decoded image. + public Memory2D DecodeHdr2D(KtxFile file) + { + return DecodeInternalHdr(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMapsHdr2D(KtxFile file) + { + var decoded = DecodeInternalHdr(file, true, default); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + } + + /// + /// Decode the main image from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The decoded image. + public Memory2D DecodeHdr2D(DdsFile file) + { + return DecodeInternalHdr(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMapsHdr2D(DdsFile file) + { + var decoded = DecodeInternalHdr(file, true, default); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + } + + /// + /// Decode a single block from raw bytes and return it as a . + /// Input Span size needs to equal the block size. + /// To get the block size (in bytes) of the compression format used, see . + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The encoded block in bytes. + /// The compression format used. + /// The decoded 4x4 block. + public Memory2D DecodeBlockHdr(ReadOnlySpan blockData, CompressionFormat format) + { + var output = new ColorRgbFloat[4, 4]; + DecodeBlockInternalHdr(blockData, format, output); + return output; + } + + /// + /// Decode a single block from raw bytes and write it to the given output span. + /// Output span size must be exactly 4x4 and input Span size needs to equal the block size. + /// To get the block size (in bytes) of the compression format used, see . + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The encoded block in bytes. + /// The compression format used. + /// The destination span of the decoded data. + public void DecodeBlockHdr(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan) + { + if (outputSpan.Width != 4 || outputSpan.Height != 4) + { + throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4"); + } + DecodeBlockInternalHdr(blockData, format, outputSpan); + } + + /// + /// Decode a single block from a stream and write it to the given output span. + /// Output span size must be exactly 4x4. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream to read encoded blocks from. + /// The compression format used. + /// The destination span of the decoded data. + /// The number of bytes read from the stream. Zero (0) if reached the end of stream. + public int DecodeBlockHdr(Stream inputStream, CompressionFormat format, Span2D outputSpan) + { + if (outputSpan.Width != 4 || outputSpan.Height != 4) + { + throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4"); + } + + Span input = stackalloc byte[16]; + input = input.Slice(0, GetBlockSize(format)); + + var bytesRead = inputStream.Read(input); + + if (bytesRead == 0) + { + return 0; //End of stream + } + + if (bytesRead != input.Length) + { + throw new Exception("Input stream does not have enough data available for a full block."); + } + + DecodeBlockInternalHdr(input, format, outputSpan); + return bytesRead; + } + + /// + /// Check whether a file is encoded in a supported HDR format. + /// + /// The loaded ktx file to check + /// If the format of the file is one of the supported HDR formats. + public bool IsHdrFormat(KtxFile file) + { + return GetCompressionFormat(file.header.GlInternalFormat).IsHdrFormat(); + } + + /// + /// Check whether a file is encoded in a supported HDR format. + /// + /// The loaded dds file to check + /// If the format of the file is one of the supported HDR formats. + public bool IsHdrFormat(DdsFile file) + { + return GetCompressionFormat(file).IsHdrFormat(); + } + + #endregion + #endregion + /// + /// Load a stream and extract either the main image or all mip maps. + /// + /// The stream containing the image file. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private Memory2D[] DecodeFromStreamInternal2D(Stream stream, bool allMipMaps, CancellationToken token) + { + var format = ImageFile.DetermineImageFormat(stream); + + switch (format) + { + case ImageFileFormat.Dds: + { + var file = DdsFile.Load(stream); + var decoded = DecodeInternal(file, allMipMaps, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + + return mem2Ds; + } + + case ImageFileFormat.Ktx: + { + var file = KtxFile.Load(stream); + var decoded = DecodeInternal(file, allMipMaps, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + + return mem2Ds; + } + + default: + throw new InvalidOperationException("Unknown image format."); + } + } + + /// + /// Load a KTX file and extract either the main image or all mip maps. + /// + /// The Ktx file to decode. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private ColorRgba32[][] DecodeInternal(KtxFile file, bool allMipMaps, CancellationToken token) + { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + if (GetCompressionFormat(file.header.GlInternalFormat) == CompressionFormat.Unknown) + { + throw new ArgumentException($"Unsupported compression format: {file.header.GlInternalFormat}"); + } + + var mipMaps = allMipMaps ? file.MipMaps.Count : 1; + var colors = new ColorRgba32[mipMaps][]; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(file.header.GlInternalFormat); + var totalBlocks = file.MipMaps.Take(mipMaps).Sum(m => m.Faces[0].Data.Length / blockSize); + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + if (IsSupportedRawFormat(file.header.GlInternalFormat)) + { + var decoder = GetRawDecoder(file.header.GlInternalFormat); + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.MipMaps[mip].Faces[0].Data; + + colors[mip] = decoder.Decode(data, context); + + context.Progress.SetProcessedBlocks(file.MipMaps.Take(mip + 1).Sum(x => x.Faces[0].Data.Length / blockSize)); + } + } + else + { + var decoder = GetRgba32Decoder(file.header.GlInternalFormat); + var format = GetCompressionFormat(file.header.GlInternalFormat); + if (format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {file.header.GlInternalFormat}"); + } + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.MipMaps[mip].Faces[0].Data; + var pixelWidth = file.MipMaps[mip].Width; + var pixelHeight = file.MipMaps[mip].Height; + + var blocks = decoder.Decode(data, context); + + colors[mip] = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight); + + context.Progress.SetProcessedBlocks(file.MipMaps.Take(mip + 1).Sum(x => x.Faces[0].Data.Length / blockSize)); + } + } + + return colors; + } + + /// + /// Load a DDS file and extract either the main image or all mip maps. + /// + /// The Dds file to decode. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private ColorRgba32[][] DecodeInternal(DdsFile file, bool allMipMaps, CancellationToken token) + { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + if (GetCompressionFormat(file) == CompressionFormat.Unknown) + { + var format = file.header.ddsPixelFormat.IsDxt10Format ? + file.dx10Header.dxgiFormat : + file.header.ddsPixelFormat.DxgiFormat; + throw new ArgumentException($"Unsupported compression format: {format}"); + } + + var mipMaps = allMipMaps ? file.header.dwMipMapCount : 1; + + // Assume at least 1 mip map + mipMaps = Math.Max(1, mipMaps); + + var colors = new ColorRgba32[mipMaps][]; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(file); + var totalBlocks = file.Faces[0].MipMaps.Take((int)mipMaps).Sum(m => m.Data.Length / blockSize); + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + if (IsSupportedRawFormat(file)) + { + var decoder = GetRawDecoder(file); + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.Faces[0].MipMaps[mip].Data; + + colors[mip] = decoder.Decode(data, context); + + context.Progress.SetProcessedBlocks(file.Faces[0].MipMaps.Take(mip + 1).Sum(x => x.Data.Length / blockSize)); + } + } + else + { + var dxtFormat = file.header.ddsPixelFormat.IsDxt10Format + ? file.dx10Header.dxgiFormat + : file.header.ddsPixelFormat.DxgiFormat; + var format = GetCompressionFormat(file); + var decoder = GetRgba32Decoder(format); + + if (format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {dxtFormat}"); + } + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.Faces[0].MipMaps[mip].Data; + var pixelWidth = file.Faces[0].MipMaps[mip].Width; + var pixelHeight = file.Faces[0].MipMaps[mip].Height; + + var blocks = decoder.Decode(data, context); + + var image = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight); + + colors[mip] = image; + + context.Progress.SetProcessedBlocks(file.Faces[0].MipMaps.Take(mip + 1).Sum(x => x.Data.Length / blockSize)); + } + } + + return colors; + } + + /// + /// Decode raw encoded image asynchronously. + /// + /// The containing the encoded data. + /// The width of the image. + /// The height of the image. + /// The Format the encoded data is in. + /// The cancellation token for this operation. May be default, if the operation is not asynchronous. + /// The decoded Rgba32 image. + private ColorRgba32[] DecodeRawInternal(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token) + { + if (format == CompressionFormat.Unknown) + { + throw new ArgumentException($"Unsupported compression format: {format}"); + } + if (input.Length % GetBlockSize(format) != 0) + { + throw new ArgumentException("The size of the input buffer does not align with the compression format."); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(format); + var totalBlocks = input.Length / blockSize; + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + var isCompressedFormat = format.IsCompressedFormat(); + if (isCompressedFormat) + { + // DecodeInternal as compressed data + var decoder = GetRgba32Decoder(format); + + if (format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {format}"); + } + + var blocks = decoder.Decode(input, context); + + return ImageToBlocks.ColorsFromRawBlocks(blocks, pixelWidth, pixelHeight); ; + } + + // DecodeInternal as raw data + var rawDecoder = GetRawDecoder(format); + + return rawDecoder.Decode(input, context); + } + + private void DecodeBlockInternal(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan) + { + var decoder = GetRgba32Decoder(format); + if (format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {format}"); + } + if (blockData.Length != GetBlockSize(format)) + { + throw new ArgumentException("The size of the input buffer does not align with the compression format."); + } + + var rawBlock = decoder.DecodeBlock(blockData); + var pixels = rawBlock.AsSpan; + + pixels.Slice(0, 4).CopyTo(outputSpan.GetRowSpan(0)); + pixels.Slice(4, 4).CopyTo(outputSpan.GetRowSpan(1)); + pixels.Slice(8, 4).CopyTo(outputSpan.GetRowSpan(2)); + pixels.Slice(12, 4).CopyTo(outputSpan.GetRowSpan(3)); + } + + #region Hdr internals + + /// + /// Load a stream and extract either the main image or all mip maps. + /// + /// The stream containing the image file. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private Memory2D[] DecodeFromStreamInternalHdr2D(Stream stream, bool allMipMaps, CancellationToken token) + { + var format = ImageFile.DetermineImageFormat(stream); + + switch (format) + { + case ImageFileFormat.Dds: + { + var file = DdsFile.Load(stream); + var decoded = DecodeInternalHdr(file, allMipMaps, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + + return mem2Ds; + } + + case ImageFileFormat.Ktx: + { + var file = KtxFile.Load(stream); + var decoded = DecodeInternalHdr(file, allMipMaps, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + + return mem2Ds; + } + + default: + throw new InvalidOperationException("Unknown image format."); + } + } + + /// + /// Load a KTX file and extract either the main image or all mip maps. + /// + /// The Ktx file to decode. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private ColorRgbFloat[][] DecodeInternalHdr(KtxFile file, bool allMipMaps, CancellationToken token) + { + if (GetCompressionFormat(file.header.GlInternalFormat) == CompressionFormat.Unknown) + { + throw new ArgumentException($"Unsupported compression format: {file.header.GlInternalFormat}"); + } + + var mipMaps = allMipMaps ? file.MipMaps.Count : 1; + var colors = new ColorRgbFloat[mipMaps][]; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(file.header.GlInternalFormat); + var totalBlocks = file.MipMaps.Take(mipMaps).Sum(m => m.Faces[0].Data.Length / blockSize); + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + var decoder = GetRgbFloatDecoder(file.header.GlInternalFormat); + var format = GetCompressionFormat(file.header.GlInternalFormat); + if (!format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {file.header.GlInternalFormat}"); + } + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.MipMaps[mip].Faces[0].Data; + var pixelWidth = file.MipMaps[mip].Width; + var pixelHeight = file.MipMaps[mip].Height; + + var blocks = decoder.Decode(data, context); + + colors[mip] = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight); + + context.Progress.SetProcessedBlocks(file.MipMaps.Take(mip + 1).Sum(x => x.Faces[0].Data.Length / blockSize)); + } + + return colors; + } -namespace BCnEncoder.Decoder -{ - public class DecoderInputOptions { /// - /// The DDS file format doesn't seem to have a standard for indicating whether a BC1 texture - /// includes 1bit of alpha. This option will assume that all Bc1 textures contain alpha. - /// If this option is false, but the dds header includes a DDPF_ALPHAPIXELS flag, alpha will be included. - /// Default is true. + /// Load a DDS file and extract either the main image or all mip maps. /// - public bool ddsBc1ExpectAlpha = true; - } + /// The Dds file to decode. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private ColorRgbFloat[][] DecodeInternalHdr(DdsFile file, bool allMipMaps, CancellationToken token) + { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + var dxtFormat = file.header.ddsPixelFormat.IsDxt10Format + ? file.dx10Header.dxgiFormat + : file.header.ddsPixelFormat.DxgiFormat; + + if (GetCompressionFormat(file) == CompressionFormat.Unknown) + { + throw new ArgumentException($"Unsupported compression format: {dxtFormat}"); + } + + var mipMaps = allMipMaps ? file.header.dwMipMapCount : 1; + + // Assume at least 1 mip map + mipMaps = Math.Max(1, mipMaps); + + var colors = new ColorRgbFloat[mipMaps][]; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(file); + var totalBlocks = file.Faces[0].MipMaps.Take((int)mipMaps).Sum(m => m.Data.Length / blockSize); + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + + var format = GetCompressionFormat(file); + var decoder = GetRgbFloatDecoder(format); + + if (!format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {dxtFormat}"); + } + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.Faces[0].MipMaps[mip].Data; + var pixelWidth = file.Faces[0].MipMaps[mip].Width; + var pixelHeight = file.Faces[0].MipMaps[mip].Height; + + var blocks = decoder.Decode(data, context); + + var image = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight); + + colors[mip] = image; + + context.Progress.SetProcessedBlocks(file.Faces[0].MipMaps.Take(mip + 1).Sum(x => x.Data.Length / blockSize)); + } + + return colors; + } - public class DecoderOutputOptions { /// - /// If true, when decoding from a format that only includes a red channel, - /// output pixels will have all colors set to the same value (greyscale). Default is true. + /// Decode raw encoded image asynchronously. /// - public bool redAsLuminance = true; - } + /// The containing the encoded data. + /// The width of the image. + /// The height of the image. + /// The Format the encoded data is in. + /// The cancellation token for this operation. May be default, if the operation is not asynchronous. + /// The decoded Rgba32 image. + private ColorRgbFloat[] DecodeRawInternalHdr(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token) + { + if (format == CompressionFormat.Unknown) + { + throw new ArgumentException($"Unsupported compression format: {format}"); + } + if (input.Length % GetBlockSize(format) != 0) + { + throw new ArgumentException("The size of the input buffer does not align with the compression format."); + } - /// - /// Decodes compressed files into Rgba format. - /// - public class BcDecoder - { - public DecoderOutputOptions OutputOptions { get; set; } = new DecoderOutputOptions(); - public DecoderInputOptions InputOptions { get; set; } = new DecoderInputOptions(); + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; - private bool IsSupportedRawFormat(GlInternalFormat format) + // Calculate total blocks + var blockSize = GetBlockSize(format); + var totalBlocks = input.Length / blockSize; + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + var decoder = GetRgbFloatDecoder(format); + + if (!format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {format}"); + } + + var blocks = decoder.Decode(input, context); + + return ImageToBlocks.ColorsFromRawBlocks(blocks, pixelWidth, pixelHeight); + } + + private void DecodeBlockInternalHdr(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan) { - switch (format) + var decoder = GetRgbFloatDecoder(format); + if (!format.IsHdrFormat()) { - case GlInternalFormat.GL_R8: - case GlInternalFormat.GL_RG8: - case GlInternalFormat.GL_RGB8: - case GlInternalFormat.GL_RGBA8: - return true; - default: - return false; + throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {format}"); } + if (blockData.Length != GetBlockSize(format)) + { + throw new ArgumentException("The size of the input buffer does not align with the compression format."); + } + + var rawBlock = decoder.DecodeBlock(blockData); + var pixels = rawBlock.AsSpan; + + pixels.Slice(0, 4).CopyTo(outputSpan.GetRowSpan(0)); + pixels.Slice(4, 4).CopyTo(outputSpan.GetRowSpan(1)); + pixels.Slice(8, 4).CopyTo(outputSpan.GetRowSpan(2)); + pixels.Slice(12, 4).CopyTo(outputSpan.GetRowSpan(3)); + } + #endregion + + #region Support + + #region Is supported format + + private bool IsSupportedRawFormat(GlInternalFormat format) + { + return IsSupportedRawFormat(GetCompressionFormat(format)); + } + + private bool IsSupportedRawFormat(DdsFile file) + { + return IsSupportedRawFormat(GetCompressionFormat(file)); } - private bool IsSupportedRawFormat(DXGI_FORMAT format) + private bool IsSupportedRawFormat(CompressionFormat format) { switch (format) { - case DXGI_FORMAT.DXGI_FORMAT_R8_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM: + case CompressionFormat.R: + case CompressionFormat.Rg: + case CompressionFormat.Rgb: + case CompressionFormat.Rgba: + case CompressionFormat.Bgra: return true; + default: return false; } } - private IBcBlockDecoder GetDecoder(GlInternalFormat format) + #endregion + + #region Get decoder + + private IBcBlockDecoder GetRgba32Decoder(GlInternalFormat format) + { + return GetRgba32Decoder(GetCompressionFormat(format)); + } + + private IBcBlockDecoder GetRgba32Decoder(DdsFile file) + { + return GetRgba32Decoder(GetCompressionFormat(file)); + } + + private IBcBlockDecoder GetRgba32Decoder(CompressionFormat format) { switch (format) { - case GlInternalFormat.GL_COMPRESSED_RGB_S3TC_DXT1_EXT: + case CompressionFormat.Bc1: return new Bc1NoAlphaDecoder(); - case GlInternalFormat.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: + + case CompressionFormat.Bc1WithAlpha: return new Bc1ADecoder(); - case GlInternalFormat.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: + + case CompressionFormat.Bc2: return new Bc2Decoder(); - case GlInternalFormat.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: + + case CompressionFormat.Bc3: return new Bc3Decoder(); - case GlInternalFormat.GL_COMPRESSED_RED_RGTC1_EXT: - return new Bc4Decoder(OutputOptions.redAsLuminance); - case GlInternalFormat.GL_COMPRESSED_RED_GREEN_RGTC2_EXT: - return new Bc5Decoder(); - case GlInternalFormat.GL_COMPRESSED_RGBA_BPTC_UNORM_ARB: - return new Bc7Decoder(); - // TODO: Not sure what to do with SRGB input. - case GlInternalFormat.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB: + + case CompressionFormat.Bc4: + return new Bc4Decoder(OutputOptions.Bc4Component); + + case CompressionFormat.Bc5: + return new Bc5Decoder(OutputOptions.Bc5Component1, OutputOptions.Bc5Component2); + + case CompressionFormat.Bc7: return new Bc7Decoder(); + + case CompressionFormat.Atc: + return new AtcDecoder(); + + case CompressionFormat.AtcExplicitAlpha: + return new AtcExplicitAlphaDecoder(); + + case CompressionFormat.AtcInterpolatedAlpha: + return new AtcInterpolatedAlphaDecoder(); + default: return null; } } - private IBcBlockDecoder GetDecoder(DXGI_FORMAT format, DdsHeader header) + private IBcBlockDecoder GetRgbFloatDecoder(GlInternalFormat format) + { + return GetRgbFloatDecoder(GetCompressionFormat(format)); + } + + private IBcBlockDecoder GetRgbFloatDecoder(DdsFile file) + { + return GetRgbFloatDecoder(GetCompressionFormat(file)); + } + + private IBcBlockDecoder GetRgbFloatDecoder(CompressionFormat format) { switch (format) { - case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB: - case DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS: - if ((header.ddsPixelFormat.dwFlags & PixelFormatFlags.DDPF_ALPHAPIXELS) != 0) { - return new Bc1ADecoder(); - } - else if(InputOptions.ddsBc1ExpectAlpha){ - return new Bc1ADecoder(); - } - else { - return new Bc1NoAlphaDecoder(); - } - - case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB: - case DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS: - return new Bc2Decoder(); - case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB: - case DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS: - return new Bc3Decoder(); - case DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC4_TYPELESS: - return new Bc4Decoder(OutputOptions.redAsLuminance); - case DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC5_TYPELESS: - return new Bc5Decoder(); - case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB: - case DXGI_FORMAT.DXGI_FORMAT_BC7_TYPELESS: - return new Bc7Decoder(); + case CompressionFormat.Bc6S: + return new Bc6SDecoder(); + case CompressionFormat.Bc6U: + return new Bc6UDecoder(); default: return null; } } + #endregion + + #region Get raw decoder + private IRawDecoder GetRawDecoder(GlInternalFormat format) { - switch (format) - { - case GlInternalFormat.GL_R8: - return new RawRDecoder(OutputOptions.redAsLuminance); - case GlInternalFormat.GL_RG8: - return new RawRGDecoder(); - case GlInternalFormat.GL_RGB8: - return new RawRGBDecoder(); - case GlInternalFormat.GL_RGBA8: - return new RawRGBADecoder(); - default: - return null; - } + return GetRawDecoder(GetCompressionFormat(format)); + } + + private IRawDecoder GetRawDecoder(DdsFile file) + { + return GetRawDecoder(GetCompressionFormat(file)); } - private IRawDecoder GetRawDecoder(DXGI_FORMAT format) + private IRawDecoder GetRawDecoder(CompressionFormat format) { switch (format) { - case DXGI_FORMAT.DXGI_FORMAT_R8_UNORM: - return new RawRDecoder(OutputOptions.redAsLuminance); - case DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM: - return new RawRGDecoder(); - case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM: - return new RawRGBADecoder(); - default: - return null; - } - } + case CompressionFormat.R: + return new RawRDecoder(OutputOptions.RedAsLuminance); - /// - /// Read a Ktx or a Dds file from a stream and decode it. - /// - public Image Decode(Stream inputStream) { - var position = inputStream.Position; - try { - if (inputStream is FileStream fs) { - var extension = Path.GetExtension(fs.Name).ToLower(); - if (extension == ".ktx") { - KtxFile file = KtxFile.Load(inputStream); - return Decode(file); - } - else if (extension == ".dds") { - DdsFile file = DdsFile.Load(inputStream); - return Decode(file); - } - } + case CompressionFormat.Rg: + return new RawRgDecoder(); - bool isDDS = false; - using (var br = new BinaryReader(inputStream, Encoding.UTF8, true)) { - var magic = br.ReadUInt32(); - if (magic == 0x20534444U) { - isDDS = true; - } - } + case CompressionFormat.Rgb: + return new RawRgbDecoder(); - inputStream.Seek(position, SeekOrigin.Begin); + case CompressionFormat.Rgba: + return new RawRgbaDecoder(); - if (isDDS) { - DdsFile dds = DdsFile.Load(inputStream); - return Decode(dds); - } - else { - KtxFile ktx = KtxFile.Load(inputStream); - return Decode(ktx); - } - } - catch (Exception) { - inputStream.Seek(position, SeekOrigin.Begin); - throw; + case CompressionFormat.Bgra: + return new RawBgraDecoder(); + + default: + throw new ArgumentOutOfRangeException(nameof(format), format, null); } } + #endregion + + #region Get block size + /// - /// Read a Ktx or a Dds file from a stream and decode it. + /// Gets the number of total blocks in an image with the given pixel width and height. /// - public Image[] DecodeAllMipMaps(Stream inputStream) { - var position = inputStream.Position; - try { - if (inputStream is FileStream fs) { - var extension = Path.GetExtension(fs.Name).ToLower(); - if (extension == ".ktx") { - KtxFile file = KtxFile.Load(inputStream); - return DecodeAllMipMaps(file); - } - else if (extension == ".dds") { - DdsFile file = DdsFile.Load(inputStream); - return DecodeAllMipMaps(file); - } - } + /// The pixel width of the image + /// The pixel height of the image + /// The total number of blocks. + public int GetBlockCount(int pixelWidth, int pixelHeight) + { + return ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight); + } - bool isDDS = false; - using (var br = new BinaryReader(inputStream, Encoding.UTF8, true)) { - var magic = br.ReadUInt32(); - if (magic == 0x20534444U) { - isDDS = true; - } - } + /// + /// Gets the number of blocks in an image with the given pixel width and height. + /// + /// The pixel width of the image + /// The pixel height of the image + /// The amount of blocks in the x-axis + /// The amount of blocks in the y-axis + public void GetBlockCount(int pixelWidth, int pixelHeight, out int blocksWidth, out int blocksHeight) + { + ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight, out blocksWidth, out blocksHeight); + } - inputStream.Seek(position, SeekOrigin.Begin); + private int GetBlockSize(GlInternalFormat format) + { + return GetBlockSize(GetCompressionFormat(format)); + } - if (isDDS) { - DdsFile dds = DdsFile.Load(inputStream); - return DecodeAllMipMaps(dds); - } - else { - KtxFile ktx = KtxFile.Load(inputStream); - return DecodeAllMipMaps(ktx); - } - } - catch (Exception) { - inputStream.Seek(position, SeekOrigin.Begin); - throw; - } + private int GetBlockSize(DdsFile file) + { + return GetBlockSize(GetCompressionFormat(file)); } /// - /// Read a KtxFile and decode it. + /// Get the size of blocks for the given compression format in bytes. /// - public Image Decode(KtxFile file) + /// The compression format used. + /// The size of a single block in bytes. + public int GetBlockSize(CompressionFormat format) { - if (IsSupportedRawFormat(file.Header.GlInternalFormat)) { - var decoder = GetRawDecoder(file.Header.GlInternalFormat); - var data = file.MipMaps[0].Faces[0].Data; - var pixelWidth = file.MipMaps[0].Width; - var pixelHeight = file.MipMaps[0].Height; + switch (format) + { + case CompressionFormat.R: + return 1; - var image = new Image((int)pixelWidth, (int)pixelHeight); - var output = decoder.Decode(data, (int)pixelWidth, (int)pixelHeight); - var pixels = image.GetPixelSpan(); + case CompressionFormat.Rg: + return 2; - output.CopyTo(pixels); - return image; - } - else { - var decoder = GetDecoder(file.Header.GlInternalFormat); - if (decoder == null) - { - throw new NotSupportedException($"This format is not supported: {file.Header.GlInternalFormat}"); - } + case CompressionFormat.Rgb: + return 3; - var data = file.MipMaps[0].Faces[0].Data; - var pixelWidth = file.MipMaps[0].Width; - var pixelHeight = file.MipMaps[0].Height; + case CompressionFormat.Rgba: + return 4; - var blocks = decoder.Decode(data, (int)pixelWidth, (int)pixelHeight, out var blockWidth, out var blockHeight); + case CompressionFormat.Bgra: + return 4; - return ImageToBlocks.ImageFromRawBlocks(blocks, blockWidth, blockHeight, - (int)pixelWidth, (int)pixelHeight); - } - } + case CompressionFormat.Bc1: + case CompressionFormat.Bc1WithAlpha: + return Unsafe.SizeOf(); - /// - /// Read a KtxFile and decode it. - /// - public Image[] DecodeAllMipMaps(KtxFile file) - { - if (IsSupportedRawFormat(file.Header.GlInternalFormat)) { - var decoder = GetRawDecoder(file.Header.GlInternalFormat); - var images = new Image[file.MipMaps.Count]; + case CompressionFormat.Bc2: + return Unsafe.SizeOf(); - for (int mip = 0; mip < file.MipMaps.Count; mip++) { - var data = file.MipMaps[mip].Faces[0].Data; - var pixelWidth = file.MipMaps[mip].Width; - var pixelHeight = file.MipMaps[mip].Height; + case CompressionFormat.Bc3: + return Unsafe.SizeOf(); - var image = new Image((int)pixelWidth, (int)pixelHeight); - var output = decoder.Decode(data, (int)pixelWidth, (int)pixelHeight); - var pixels = image.GetPixelSpan(); + case CompressionFormat.Bc4: + return Unsafe.SizeOf(); - output.CopyTo(pixels); - images[mip] = image; - } - - return images; - } - else { - var decoder = GetDecoder(file.Header.GlInternalFormat); - if (decoder == null) - { - throw new NotSupportedException($"This format is not supported: {file.Header.GlInternalFormat}"); - } - var images = new Image[file.MipMaps.Count]; + case CompressionFormat.Bc5: + return Unsafe.SizeOf(); - for (int mip = 0; mip < file.MipMaps.Count; mip++) { + case CompressionFormat.Bc6S: + case CompressionFormat.Bc6U: + return Unsafe.SizeOf(); - var data = file.MipMaps[mip].Faces[0].Data; - var pixelWidth = file.MipMaps[mip].Width; - var pixelHeight = file.MipMaps[mip].Height; + case CompressionFormat.Bc7: + return Unsafe.SizeOf(); - var blocks = decoder.Decode(data, (int) pixelWidth, (int) pixelHeight, out var blockWidth, - out var blockHeight); + case CompressionFormat.Atc: + return Unsafe.SizeOf(); - var image = ImageToBlocks.ImageFromRawBlocks(blocks, blockWidth, blockHeight, - (int)pixelWidth, (int)pixelHeight); - images[mip] = image; - } - return images; + case CompressionFormat.AtcExplicitAlpha: + return Unsafe.SizeOf(); + + case CompressionFormat.AtcInterpolatedAlpha: + return Unsafe.SizeOf(); + + case CompressionFormat.Unknown: + return 0; + + default: + throw new ArgumentOutOfRangeException(nameof(format), format, null); } } - /// - /// Read a DdsFile and decode it - /// - public Image Decode(DdsFile file) + #endregion + + private CompressionFormat GetCompressionFormat(GlInternalFormat format) { - if (IsSupportedRawFormat(file.Header.ddsPixelFormat.DxgiFormat)) { - var decoder = GetRawDecoder(file.Header.ddsPixelFormat.DxgiFormat); - var data = file.Faces[0].MipMaps[0].Data; - var pixelWidth = file.Faces[0].Width; - var pixelHeight = file.Faces[0].Height; + switch (format) + { + case GlInternalFormat.GlR8: + return CompressionFormat.R; - var image = new Image((int)pixelWidth, (int)pixelHeight); - var output = decoder.Decode(data, (int)pixelWidth, (int)pixelHeight); - var pixels = image.GetPixelSpan(); + case GlInternalFormat.GlRg8: + return CompressionFormat.Rg; - output.CopyTo(pixels); - return image; - } - else { - DXGI_FORMAT format = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN; - if (file.Header.ddsPixelFormat.IsDxt10Format) { - format = file.Dxt10Header.dxgiFormat; - } - else { - format = file.Header.ddsPixelFormat.DxgiFormat; - } - IBcBlockDecoder decoder = GetDecoder(format, file.Header); - - if (decoder == null) - { - throw new NotSupportedException($"This format is not supported: {format}"); - } + case GlInternalFormat.GlRgb8: + return CompressionFormat.Rgb; + + case GlInternalFormat.GlRgba8: + return CompressionFormat.Rgba; + + // HINT: Bgra is not supported by default. The format enum is added by an extension by Apple. + case GlInternalFormat.GlBgra8Extension: + return CompressionFormat.Bgra; + + case GlInternalFormat.GlCompressedRgbS3TcDxt1Ext: + return CompressionFormat.Bc1; + + case GlInternalFormat.GlCompressedRgbaS3TcDxt1Ext: + return CompressionFormat.Bc1WithAlpha; + + case GlInternalFormat.GlCompressedRgbaS3TcDxt3Ext: + return CompressionFormat.Bc2; + + case GlInternalFormat.GlCompressedRgbaS3TcDxt5Ext: + return CompressionFormat.Bc3; - var data = file.Faces[0].MipMaps[0].Data; - var pixelWidth = file.Faces[0].Width; - var pixelHeight = file.Faces[0].Height; + case GlInternalFormat.GlCompressedRedRgtc1Ext: + return CompressionFormat.Bc4; - var blocks = decoder.Decode(data, (int)pixelWidth, (int)pixelHeight, out var blockWidth, out var blockHeight); + case GlInternalFormat.GlCompressedRedGreenRgtc2Ext: + return CompressionFormat.Bc5; - return ImageToBlocks.ImageFromRawBlocks(blocks, blockWidth, blockHeight, - (int)pixelWidth, (int)pixelHeight); + case GlInternalFormat.GlCompressedRgbBptcUnsignedFloatArb: + return CompressionFormat.Bc6U; + + case GlInternalFormat.GlCompressedRgbBptcSignedFloatArb: + return CompressionFormat.Bc6S; + + // TODO: Not sure what to do with SRGB input. + case GlInternalFormat.GlCompressedRgbaBptcUnormArb: + case GlInternalFormat.GlCompressedSrgbAlphaBptcUnormArb: + return CompressionFormat.Bc7; + + case GlInternalFormat.GlCompressedRgbAtc: + return CompressionFormat.Atc; + + case GlInternalFormat.GlCompressedRgbaAtcExplicitAlpha: + return CompressionFormat.AtcExplicitAlpha; + + case GlInternalFormat.GlCompressedRgbaAtcInterpolatedAlpha: + return CompressionFormat.AtcInterpolatedAlpha; + + default: + return CompressionFormat.Unknown; } } - /// - /// Read a DdsFile and decode it - /// - public Image[] DecodeAllMipMaps(DdsFile file) + private CompressionFormat GetCompressionFormat(DdsFile file) { - if (IsSupportedRawFormat(file.Header.ddsPixelFormat.DxgiFormat)) { - var decoder = GetRawDecoder(file.Header.ddsPixelFormat.DxgiFormat); + var format = file.header.ddsPixelFormat.IsDxt10Format ? + file.dx10Header.dxgiFormat : + file.header.ddsPixelFormat.DxgiFormat; - var images = new Image[file.Header.dwMipMapCount]; + switch (format) + { + case DxgiFormat.DxgiFormatR8Unorm: + return CompressionFormat.R; - for (int mip = 0; mip < file.Header.dwMipMapCount; mip++) { - var data = file.Faces[0].MipMaps[mip].Data; - var pixelWidth = file.Faces[0].MipMaps[mip].Width; - var pixelHeight = file.Faces[0].MipMaps[mip].Height; + case DxgiFormat.DxgiFormatR8G8Unorm: + return CompressionFormat.Rg; - var image = new Image((int) pixelWidth, (int) pixelHeight); - var output = decoder.Decode(data, (int) pixelWidth, (int) pixelHeight); - var pixels = image.GetPixelSpan(); + // HINT: R8G8B8 has no DxgiFormat to convert from - output.CopyTo(pixels); - images[mip] = image; - } - return images; + case DxgiFormat.DxgiFormatR8G8B8A8Unorm: + return CompressionFormat.Rgba; + + case DxgiFormat.DxgiFormatB8G8R8A8Unorm: + return CompressionFormat.Bgra; + + case DxgiFormat.DxgiFormatBc1Unorm: + case DxgiFormat.DxgiFormatBc1UnormSrgb: + case DxgiFormat.DxgiFormatBc1Typeless: + if (file.header.ddsPixelFormat.dwFlags.HasFlag(PixelFormatFlags.DdpfAlphaPixels)) + return CompressionFormat.Bc1WithAlpha; + + if (InputOptions.DdsBc1ExpectAlpha) + return CompressionFormat.Bc1WithAlpha; + + return CompressionFormat.Bc1; + + case DxgiFormat.DxgiFormatBc2Unorm: + case DxgiFormat.DxgiFormatBc2UnormSrgb: + case DxgiFormat.DxgiFormatBc2Typeless: + return CompressionFormat.Bc2; + + case DxgiFormat.DxgiFormatBc3Unorm: + case DxgiFormat.DxgiFormatBc3UnormSrgb: + case DxgiFormat.DxgiFormatBc3Typeless: + return CompressionFormat.Bc3; + + case DxgiFormat.DxgiFormatBc4Unorm: + case DxgiFormat.DxgiFormatBc4Snorm: + case DxgiFormat.DxgiFormatBc4Typeless: + return CompressionFormat.Bc4; + + case DxgiFormat.DxgiFormatBc5Unorm: + case DxgiFormat.DxgiFormatBc5Snorm: + case DxgiFormat.DxgiFormatBc5Typeless: + return CompressionFormat.Bc5; + + case DxgiFormat.DxgiFormatBc6HTypeless: + case DxgiFormat.DxgiFormatBc6HUf16: + return CompressionFormat.Bc6U; + + case DxgiFormat.DxgiFormatBc6HSf16: + return CompressionFormat.Bc6S; + + case DxgiFormat.DxgiFormatBc7Unorm: + case DxgiFormat.DxgiFormatBc7UnormSrgb: + case DxgiFormat.DxgiFormatBc7Typeless: + return CompressionFormat.Bc7; + + case DxgiFormat.DxgiFormatAtcExt: + return CompressionFormat.Atc; + + case DxgiFormat.DxgiFormatAtcExplicitAlphaExt: + return CompressionFormat.AtcExplicitAlpha; + + case DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt: + return CompressionFormat.AtcInterpolatedAlpha; + + default: + return CompressionFormat.Unknown; } - else { - DXGI_FORMAT format = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN; - if (file.Header.ddsPixelFormat.IsDxt10Format) { - format = file.Dxt10Header.dxgiFormat; - } - else { - format = file.Header.ddsPixelFormat.DxgiFormat; - } - IBcBlockDecoder decoder = GetDecoder(format, file.Header); - - if (decoder == null) - { - throw new NotSupportedException($"This format is not supported: {format}"); - } - var images = new Image[file.Header.dwMipMapCount]; + } - for (int mip = 0; mip < file.Header.dwMipMapCount; mip++) { - var data = file.Faces[0].MipMaps[mip].Data; - var pixelWidth = file.Faces[0].MipMaps[mip].Width; - var pixelHeight = file.Faces[0].MipMaps[mip].Height; + private int GetBufferSize(CompressionFormat format, int pixelWidth, int pixelHeight) + { + switch (format) + { + case CompressionFormat.R: + return pixelWidth * pixelHeight; - var blocks = decoder.Decode(data, (int) pixelWidth, (int) pixelHeight, out var blockWidth, - out var blockHeight); + case CompressionFormat.Rg: + return 2 * pixelWidth * pixelHeight; - var image = ImageToBlocks.ImageFromRawBlocks(blocks, blockWidth, blockHeight, - (int)pixelWidth, (int)pixelHeight); + case CompressionFormat.Rgb: + return 3 * pixelWidth * pixelHeight; - images[mip] = image; - } + case CompressionFormat.Rgba: + case CompressionFormat.Bgra: + return 4 * pixelWidth * pixelHeight; + + case CompressionFormat.Bc1: + case CompressionFormat.Bc1WithAlpha: + case CompressionFormat.Bc2: + case CompressionFormat.Bc3: + case CompressionFormat.Bc4: + case CompressionFormat.Bc5: + case CompressionFormat.Bc6S: + case CompressionFormat.Bc6U: + case CompressionFormat.Bc7: + case CompressionFormat.Atc: + case CompressionFormat.AtcExplicitAlpha: + case CompressionFormat.AtcInterpolatedAlpha: + return GetBlockSize(format) * ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight); - return images; + case CompressionFormat.Unknown: + return 0; + default: + throw new ArgumentOutOfRangeException(nameof(format), format, null); } } + + #endregion } } diff --git a/BCnEnc.Net/Decoder/BcFloatBlockDecoder.cs b/BCnEnc.Net/Decoder/BcFloatBlockDecoder.cs new file mode 100644 index 0000000..b585bc3 --- /dev/null +++ b/BCnEnc.Net/Decoder/BcFloatBlockDecoder.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BCnEncoder.Decoder +{ + internal class Bc6FloatBlockDecoder + { + private static readonly object lockObj = new object(); + + } +} diff --git a/BCnEnc.Net/Decoder/Options/DecoderInputOptions.cs b/BCnEnc.Net/Decoder/Options/DecoderInputOptions.cs new file mode 100644 index 0000000..ed2e650 --- /dev/null +++ b/BCnEnc.Net/Decoder/Options/DecoderInputOptions.cs @@ -0,0 +1,16 @@ +namespace BCnEncoder.Decoder.Options +{ + /// + /// A class for the decoder input options. + /// + public class DecoderInputOptions + { + /// + /// The DDS file Format doesn't seem to have a standard for indicating whether a BC1 texture + /// includes 1bit of alpha. This option will assume that all Bc1 textures contain alpha. + /// If this option is false, but the dds header includes a DDPF_ALPHAPIXELS flag, alpha will be included. + /// Default is true. + /// + public bool DdsBc1ExpectAlpha { get; set; } = true; + } +} diff --git a/BCnEnc.Net/Decoder/Options/DecoderOptions.cs b/BCnEnc.Net/Decoder/Options/DecoderOptions.cs new file mode 100644 index 0000000..f265000 --- /dev/null +++ b/BCnEnc.Net/Decoder/Options/DecoderOptions.cs @@ -0,0 +1,30 @@ +using System; +using BCnEncoder.Shared; + +namespace BCnEncoder.Decoder.Options +{ + /// + /// General options for the decoder. + /// + public class DecoderOptions + { + /// + /// Whether the blocks should be decoded in parallel. This can be much faster than single-threaded decoding, + /// but is slow if multiple textures are being processed at the same time. + /// When a debugger is attached, the decoder defaults to single-threaded operation to ease debugging. + /// Default is false. + /// + /// Parallel execution will be ignored in RawDecoders, due to minimal performance gain. + public bool IsParallel { get; set; } + + /// + /// Determines how many tasks should be used for parallel processing. + /// + public int TaskCount { get; set; } = Environment.ProcessorCount; + + /// + /// The progress context for the operation. + /// + public IProgress Progress { get; set; } + } +} diff --git a/BCnEnc.Net/Decoder/Options/DecoderOutputOptions.cs b/BCnEnc.Net/Decoder/Options/DecoderOutputOptions.cs new file mode 100644 index 0000000..f221800 --- /dev/null +++ b/BCnEnc.Net/Decoder/Options/DecoderOutputOptions.cs @@ -0,0 +1,33 @@ +using BCnEncoder.Encoder; +using BCnEncoder.Shared; + +namespace BCnEncoder.Decoder.Options +{ + /// + /// A class for the decoder output options. + /// + public class DecoderOutputOptions + { + /// + /// If true, when decoding from R8 raw format, + /// output pixels will have all colors set to the same value (greyscale). + /// Default is true. (Does not apply to BC4 format.) + /// + public bool RedAsLuminance { get; set; } = true; + + /// + /// The color channel to populate with the values of a BC4 block. + /// + public ColorComponent Bc4Component { get; set; } = ColorComponent.R; + + /// + /// The color channel to populate with the values of the first BC5 block. + /// + public ColorComponent Bc5Component1 { get; set; } = ColorComponent.R; + + /// + /// The color channel to populate with the values of the second BC5 block. + /// + public ColorComponent Bc5Component2 { get; set; } = ColorComponent.G; + } +} diff --git a/BCnEnc.Net/Decoder/RawDecoder.cs b/BCnEnc.Net/Decoder/RawDecoder.cs index 75f77fe..880485d 100644 --- a/BCnEnc.Net/Decoder/RawDecoder.cs +++ b/BCnEnc.Net/Decoder/RawDecoder.cs @@ -1,75 +1,190 @@ -using System; -using SixLabors.ImageSharp.PixelFormats; +using System; +using BCnEncoder.Shared; namespace BCnEncoder.Decoder { - internal interface IRawDecoder { - Rgba32[] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight); + internal interface IRawDecoder + { + ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context); } - public class RawRDecoder : IRawDecoder { + /// + /// A class to decode data to R components. + /// + public class RawRDecoder : IRawDecoder + { private readonly bool redAsLuminance; - public RawRDecoder(bool redAsLuminance) { + + /// + /// Create a new instance of . + /// + /// If the decoded component should be used as the red component or luminance. + public RawRDecoder(bool redAsLuminance) + { this.redAsLuminance = redAsLuminance; } - public Rgba32[] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight) { - Rgba32[] output = new Rgba32[pixelWidth * pixelHeight]; - for (int i = 0; i < output.Length; i++) { - if (redAsLuminance) { - output[i].R = data[i]; - output[i].G = data[i]; - output[i].B = data[i]; + /// + /// Decode the data to color components. + /// + /// The data to decode. + /// The context of the current operation. + /// The decoded color components. + public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context) + { + var output = new ColorRgba32[data.Length]; + + // HINT: Ignoring parallel execution since we wouldn't gain performance from it. + + var span = data.Span; + for (var i = 0; i < output.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + if (redAsLuminance) + { + output[i].r = span[i]; + output[i].g = span[i]; + output[i].b = span[i]; } - else { - output[i].R = data[i]; - output[i].G = 0; - output[i].B = 0; + else + { + output[i].r = span[i]; + output[i].g = 0; + output[i].b = 0; } - output[i].A = 255; + + output[i].a = 255; + } + + return output; + } + } + + /// + /// A class to decode data to RG components. + /// + public class RawRgDecoder : IRawDecoder + { + /// + /// Decode the data to color components. + /// + /// The data to decode. + /// The context of the current operation. + /// The decoded color components. + public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context) + { + var output = new ColorRgba32[data.Length / 2]; + + // HINT: Ignoring parallel execution since we wouldn't gain performance from it. + + var span = data.Span; + for (var i = 0; i < output.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + output[i].r = span[i * 2]; + output[i].g = span[i * 2 + 1]; + output[i].b = 0; + output[i].a = 255; } + return output; } } - public class RawRGDecoder : IRawDecoder + /// + /// A class to decode data to RGB components. + /// + public class RawRgbDecoder : IRawDecoder { - public Rgba32[] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight) { - Rgba32[] output = new Rgba32[pixelWidth * pixelHeight]; - for (int i = 0; i < output.Length; i++) { - output[i].R = data[i * 2]; - output[i].G = data[i * 2 + 1]; - output[i].B = 0; - output[i].A = 255; + /// + /// Decode the data to color components. + /// + /// The data to decode. + /// The context of the current operation. + /// The decoded color components. + public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context) + { + var output = new ColorRgba32[data.Length / 3]; + + // HINT: Ignoring parallel execution since we wouldn't gain performance from it. + + var span = data.Span; + for (var i = 0; i < output.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + output[i].r = span[i * 3]; + output[i].g = span[i * 3 + 1]; + output[i].b = span[i * 3 + 2]; + output[i].a = 255; } + return output; } } - public class RawRGBDecoder : IRawDecoder + /// + /// A class to decode data to RGBA components. + /// + public class RawRgbaDecoder : IRawDecoder { - public Rgba32[] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight) { - Rgba32[] output = new Rgba32[pixelWidth * pixelHeight]; - for (int i = 0; i < output.Length; i++) { - output[i].R = data[i * 3]; - output[i].G = data[i * 3 + 1]; - output[i].B = data[i * 3 + 2]; - output[i].A = 255; + /// + /// Decode the data to color components. + /// + /// The data to decode. + /// The context of the current operation. + /// The decoded color components. + public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context) + { + var output = new ColorRgba32[data.Length / 4]; + + // HINT: Ignoring parallel execution since we wouldn't gain performance from it. + + var span = data.Span; + for (var i = 0; i < output.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + output[i].r = span[i * 4]; + output[i].g = span[i * 4 + 1]; + output[i].b = span[i * 4 + 2]; + output[i].a = span[i * 4 + 3]; } + return output; } } - public class RawRGBADecoder : IRawDecoder + /// + /// A class to decode data to BGRA components. + /// + public class RawBgraDecoder : IRawDecoder { - public Rgba32[] Decode(ReadOnlySpan data, int pixelWidth, int pixelHeight) { - Rgba32[] output = new Rgba32[pixelWidth * pixelHeight]; - for (int i = 0; i < output.Length; i++) { - output[i].R = data[i * 4]; - output[i].G = data[i * 4 + 1]; - output[i].B = data[i * 4 + 2]; - output[i].A = data[i * 4 + 3]; + /// + /// Decode the data to color components. + /// + /// The data to decode. + /// The context of the current operation. + /// The decoded color components. + public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context) + { + var output = new ColorRgba32[data.Length / 4]; + + // HINT: Ignoring parallel execution since we wouldn't gain performance from it. + + var span = data.Span; + for (var i = 0; i < output.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + output[i].b = span[i * 4]; + output[i].g = span[i * 4 + 1]; + output[i].r = span[i * 4 + 2]; + output[i].a = span[i * 4 + 3]; } + return output; } } diff --git a/BCnEnc.Net/Encoder/AtcBlockEncoder.cs b/BCnEnc.Net/Encoder/AtcBlockEncoder.cs new file mode 100644 index 0000000..2c3dde2 --- /dev/null +++ b/BCnEnc.Net/Encoder/AtcBlockEncoder.cs @@ -0,0 +1,136 @@ +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; + +namespace BCnEncoder.Encoder +{ + internal unsafe class AtcBlockEncoder : BaseBcBlockEncoder + { + private readonly Bc1BlockEncoder bc1BlockEncoder; + + public AtcBlockEncoder() + { + bc1BlockEncoder = new Bc1BlockEncoder(); + } + + public override AtcBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + var atcBlock = new AtcBlock(); + + // EncodeBlock with BC1 first + var bc1Block = bc1BlockEncoder.EncodeBlock(block, quality); + + // Atc specific modifications to BC1 + // According to http://www.guildsoftware.com/papers/2012.Converting.DXTC.to.Atc.pdf + + // Change color0 from rgb565 to rgb555 with method 0 + atcBlock.color0 = new ColorRgb555(bc1Block.color0.R, bc1Block.color0.G, bc1Block.color0.B); + atcBlock.color1 = bc1Block.color1; + + // Remap color indices from BC1 to ATC + var remap = stackalloc byte[] { 0, 3, 1, 2 }; + for (var i = 0; i < 16; i++) + { + atcBlock[i] = remap[bc1Block[i]]; + } + + return atcBlock; + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbAtc; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgb; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatAtcExt; + } + } + + internal class AtcExplicitAlphaBlockEncoder : BaseBcBlockEncoder + { + private readonly AtcBlockEncoder atcBlockEncoder; + + public AtcExplicitAlphaBlockEncoder() + { + atcBlockEncoder = new AtcBlockEncoder(); + } + + public override AtcExplicitAlphaBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + var atcBlock = atcBlockEncoder.EncodeBlock(block, quality); + + // EncodeBlock alpha + var bc2AlphaBlock = new Bc2AlphaBlock(); + for (var i = 0; i < 16; i++) + { + bc2AlphaBlock.SetAlpha(i, block[i].a); + } + + return new AtcExplicitAlphaBlock + { + alphas = bc2AlphaBlock, + colors = atcBlock + }; + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbaAtcExplicitAlpha; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgba; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatAtcExplicitAlphaExt; + } + } + + internal class AtcInterpolatedAlphaBlockEncoder : BaseBcBlockEncoder + { + private readonly Bc4ComponentBlockEncoder bc4BlockEncoder; + private readonly AtcBlockEncoder atcBlockEncoder; + + public AtcInterpolatedAlphaBlockEncoder() + { + bc4BlockEncoder = new Bc4ComponentBlockEncoder(ColorComponent.A); + atcBlockEncoder = new AtcBlockEncoder(); + } + + public override AtcInterpolatedAlphaBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + var bc4Block = bc4BlockEncoder.EncodeBlock(block, quality); + var atcBlock = atcBlockEncoder.EncodeBlock(block, quality); + + return new AtcInterpolatedAlphaBlock + { + alphas = bc4Block, + colors = atcBlock + }; + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbaAtcInterpolatedAlpha; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgba; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt; + } + } +} diff --git a/BCnEnc.Net/Encoder/BaseBcBlockEncoder.cs b/BCnEnc.Net/Encoder/BaseBcBlockEncoder.cs new file mode 100644 index 0000000..df5b62b --- /dev/null +++ b/BCnEnc.Net/Encoder/BaseBcBlockEncoder.cs @@ -0,0 +1,77 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; + +namespace BCnEncoder.Encoder +{ + internal abstract class BaseBcBlockEncoder : IBcBlockEncoder where T : unmanaged where TBlock : unmanaged + { + private static readonly object lockObj = new object(); + + public byte[] Encode(TBlock[] blocks, int blockWidth, int blockHeight, CompressionQuality quality, OperationContext context) + { + var outputData = new byte[blockWidth * blockHeight * Unsafe.SizeOf()]; + + var currentBlocks = 0; + if (context.IsParallel) + { + var options = new ParallelOptions + { + CancellationToken = context.CancellationToken, + MaxDegreeOfParallelism = context.TaskCount + }; + Parallel.For(0, blocks.Length, options, i => + { + var outputBlocks = MemoryMarshal.Cast(outputData); + outputBlocks[i] = EncodeBlock(blocks[i], quality); + + if (context.Progress != null) + { + lock (lockObj) + { + context.Progress.Report(++currentBlocks); + } + } + }); + } + else + { + var outputBlocks = MemoryMarshal.Cast(outputData); + for (var i = 0; i < blocks.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + outputBlocks[i] = EncodeBlock(blocks[i], quality); + + context.Progress?.Report(++currentBlocks); + } + } + + return outputData; + } + + public void EncodeBlock(TBlock block, CompressionQuality quality, Span output) + { + if (output.Length != Unsafe.SizeOf()) + { + throw new Exception("Cannot encode block! Output buffer is not the correct size."); + } + var encoded = EncodeBlock(block, quality); + MemoryMarshal.Cast(output)[0] = encoded; + } + + public abstract GlInternalFormat GetInternalFormat(); + public abstract GlFormat GetBaseInternalFormat(); + public abstract DxgiFormat GetDxgiFormat(); + public int GetBlockSize() + { + return Unsafe.SizeOf(); + } + + public abstract T EncodeBlock(TBlock block, CompressionQuality quality); + } +} diff --git a/BCnEnc.Net/Encoder/Bc1BlockEncoder.cs b/BCnEnc.Net/Encoder/Bc1BlockEncoder.cs index 2763d89..2904ede 100644 --- a/BCnEnc.Net/Encoder/Bc1BlockEncoder.cs +++ b/BCnEnc.Net/Encoder/Bc1BlockEncoder.cs @@ -1,46 +1,20 @@ -using System; -using System.Runtime.InteropServices; -using System.Threading.Tasks; +using System; using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; namespace BCnEncoder.Encoder { - internal class Bc1BlockEncoder : IBcBlockEncoder + internal class Bc1BlockEncoder : BaseBcBlockEncoder { - - public byte[] Encode(RawBlock4X4Rgba32[] blocks, int blockWidth, int blockHeight, EncodingQuality quality, bool parallel) - { - byte[] outputData = new byte[blockWidth * blockHeight * Marshal.SizeOf()]; - Span outputBlocks = MemoryMarshal.Cast(outputData); - - if (parallel) - { - Parallel.For(0, blocks.Length, i => - { - Span outputBlocks = MemoryMarshal.Cast(outputData); - outputBlocks[i] = EncodeBlock(blocks[i], quality); - }); - } - else - { - for (int i = 0; i < blocks.Length; i++) - { - outputBlocks[i] = EncodeBlock(blocks[i], quality); - } - } - - return outputData; - } - - private Bc1Block EncodeBlock(RawBlock4X4Rgba32 block, EncodingQuality quality) + public override Bc1Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) { switch (quality) { - case EncodingQuality.Fast: + case CompressionQuality.Fast: return Bc1BlockEncoderFast.EncodeBlock(block); - case EncodingQuality.Balanced: + case CompressionQuality.Balanced: return Bc1BlockEncoderBalanced.EncodeBlock(block); - case EncodingQuality.BestQuality: + case CompressionQuality.BestQuality: return Bc1BlockEncoderSlow.EncodeBlock(block); default: @@ -48,26 +22,26 @@ private Bc1Block EncodeBlock(RawBlock4X4Rgba32 block, EncodingQuality quality) } } - public GlInternalFormat GetInternalFormat() + public override GlInternalFormat GetInternalFormat() { - return GlInternalFormat.GL_COMPRESSED_RGB_S3TC_DXT1_EXT; + return GlInternalFormat.GlCompressedRgbS3TcDxt1Ext; } - public GLFormat GetBaseInternalFormat() + public override GlFormat GetBaseInternalFormat() { - return GLFormat.GL_RGB; + return GlFormat.GlRgb; } - public DXGI_FORMAT GetDxgiFormat() + public override DxgiFormat GetDxgiFormat() { - return DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM; + return DxgiFormat.DxgiFormatBc1Unorm; } #region Encoding private stuff private static Bc1Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f) { - Bc1Block output = new Bc1Block(); + var output = new Bc1Block(); var pixels = rawBlock.AsSpan; @@ -81,17 +55,17 @@ private static Bc1Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0 stackalloc ColorRgb24[] { c0, c1, - c0 * (1.0 / 2.0) + c1 * (1.0 / 2.0), + c0.InterpolateHalf(c1), new ColorRgb24(0, 0, 0) } : stackalloc ColorRgb24[] { c0, c1, - c0 * (2.0 / 3.0) + c1 * (1.0 / 3.0), - c0 * (1.0 / 3.0) + c1 * (2.0 / 3.0) + c0.InterpolateThird(c1, 1), + c0.InterpolateThird(c1, 2) }; error = 0; - for (int i = 0; i < 16; i++) + for (var i = 0; i < 16; i++) { var color = pixels[i]; output[i] = ColorChooser.ChooseClosestColor4(colors, color, rWeight, gWeight, bWeight, out var e); @@ -111,14 +85,14 @@ private static class Bc1BlockEncoderFast internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { - Bc1Block output = new Bc1Block(); + var output = new Bc1Block(); var pixels = rawBlock.AsSpan; RgbBoundingBox.Create565(pixels, out var min, out var max); - ColorRgb565 c0 = max; - ColorRgb565 c1 = min; + var c0 = max; + var c1 = min; output = TryColors(rawBlock, c0, c1, out var error); @@ -126,15 +100,16 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) } } - private static class Bc1BlockEncoderBalanced { - private const int maxTries = 24 * 2; - private const float errorThreshold = 0.05f; + private static class Bc1BlockEncoderBalanced + { + private const int MaxTries = 24 * 2; + private const float ErrorThreshold = 0.05f; internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { var pixels = rawBlock.AsSpan; - PcaVectors.Create(pixels, out System.Numerics.Vector3 mean, out System.Numerics.Vector3 pa); + PcaVectors.Create(pixels, out var mean, out var pa); PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); var c0 = max; @@ -147,20 +122,21 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) c1 = c; } - Bc1Block best = TryColors(rawBlock, c0, c1, out float bestError); - - for (int i = 0; i < maxTries; i++) { + var best = TryColors(rawBlock, c0, c1, out var bestError); + + for (var i = 0; i < MaxTries; i++) + { var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); - + if (newC0.data < newC1.data) { var c = newC0; newC0 = newC1; newC1 = c; } - + var block = TryColors(rawBlock, newC0, newC1, out var error); - + if (error < bestError) { best = block; @@ -169,7 +145,8 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) c1 = newC1; } - if (bestError < errorThreshold) { + if (bestError < ErrorThreshold) + { break; } } @@ -180,14 +157,14 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) private static class Bc1BlockEncoderSlow { - private const int maxTries = 9999; - private const float errorThreshold = 0.01f; + private const int MaxTries = 9999; + private const float ErrorThreshold = 0.01f; internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { var pixels = rawBlock.AsSpan; - PcaVectors.Create(pixels, out System.Numerics.Vector3 mean, out System.Numerics.Vector3 pa); + PcaVectors.Create(pixels, out var mean, out var pa); PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); var c0 = max; @@ -200,20 +177,21 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) c1 = c; } - Bc1Block best = TryColors(rawBlock, c0, c1, out float bestError); + var best = TryColors(rawBlock, c0, c1, out var bestError); - int lastChanged = 0; + var lastChanged = 0; - for (int i = 0; i < maxTries; i++) { + for (var i = 0; i < MaxTries; i++) + { var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); - + if (newC0.data < newC1.data) { var c = newC0; newC0 = newC1; newC1 = c; } - + var block = TryColors(rawBlock, newC0, newC1, out var error); lastChanged++; @@ -227,7 +205,8 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) lastChanged = 0; } - if (bestError < errorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) { + if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) + { break; } } @@ -239,42 +218,17 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) #endregion } - internal class Bc1AlphaBlockEncoder : IBcBlockEncoder + internal class Bc1AlphaBlockEncoder : BaseBcBlockEncoder { - - public byte[] Encode(RawBlock4X4Rgba32[] blocks, int blockWidth, int blockHeight, EncodingQuality quality, bool parallel) - { - byte[] outputData = new byte[blockWidth * blockHeight * Marshal.SizeOf()]; - Span outputBlocks = MemoryMarshal.Cast(outputData); - - if (parallel) - { - Parallel.For(0, blocks.Length, i => - { - Span outputBlocks = MemoryMarshal.Cast(outputData); - outputBlocks[i] = EncodeBlock(blocks[i], quality); - }); - } - else - { - for (int i = 0; i < blocks.Length; i++) - { - outputBlocks[i] = EncodeBlock(blocks[i], quality); - } - } - - return outputData; - } - - private Bc1Block EncodeBlock(RawBlock4X4Rgba32 block, EncodingQuality quality) + public override Bc1Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) { switch (quality) { - case EncodingQuality.Fast: + case CompressionQuality.Fast: return Bc1AlphaBlockEncoderFast.EncodeBlock(block); - case EncodingQuality.Balanced: + case CompressionQuality.Balanced: return Bc1AlphaBlockEncoderBalanced.EncodeBlock(block); - case EncodingQuality.BestQuality: + case CompressionQuality.BestQuality: return Bc1AlphaBlockEncoderSlow.EncodeBlock(block); default: @@ -282,24 +236,26 @@ private Bc1Block EncodeBlock(RawBlock4X4Rgba32 block, EncodingQuality quality) } } - public GlInternalFormat GetInternalFormat() + public override GlInternalFormat GetInternalFormat() { - return GlInternalFormat.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT; + return GlInternalFormat.GlCompressedRgbaS3TcDxt1Ext; } - public GLFormat GetBaseInternalFormat() + public override GlFormat GetBaseInternalFormat() { - return GLFormat.GL_RGBA; + return GlFormat.GlRgba; } - public DXGI_FORMAT GetDxgiFormat() + public override DxgiFormat GetDxgiFormat() { - return DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM; + return DxgiFormat.DxgiFormatBc1Unorm; } + #region Encoding private stuff + private static Bc1Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f) { - Bc1Block output = new Bc1Block(); + var output = new Bc1Block(); var pixels = rawBlock.AsSpan; @@ -309,23 +265,23 @@ private static Bc1Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0 var c0 = color0.ToColorRgb24(); var c1 = color1.ToColorRgb24(); - bool hasAlpha = output.HasAlphaOrBlack; + var hasAlpha = output.HasAlphaOrBlack; ReadOnlySpan colors = hasAlpha ? stackalloc ColorRgb24[] { c0, c1, - c0 * (1.0 / 2.0) + c1 * (1.0 / 2.0), + c0.InterpolateHalf(c1), new ColorRgb24(0, 0, 0) } : stackalloc ColorRgb24[] { c0, c1, - c0 * (2.0 / 3.0) + c1 * (1.0 / 3.0), - c0 * (1.0 / 3.0) + c1 * (2.0 / 3.0) + c0.InterpolateThird(c1, 1), + c0.InterpolateThird(c1, 2) }; error = 0; - for (int i = 0; i < 16; i++) + for (var i = 0; i < 16; i++) { var color = pixels[i]; output[i] = ColorChooser.ChooseClosestColor4AlphaCutoff(colors, color, rWeight, gWeight, bWeight, @@ -336,6 +292,7 @@ stackalloc ColorRgb24[] { return output; } + #endregion #region Encoders @@ -344,16 +301,16 @@ private static class Bc1AlphaBlockEncoderFast internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { - Bc1Block output = new Bc1Block(); + var output = new Bc1Block(); var pixels = rawBlock.AsSpan; - bool hasAlpha = rawBlock.HasTransparentPixels(); + var hasAlpha = rawBlock.HasTransparentPixels(); RgbBoundingBox.Create565AlphaCutoff(pixels, out var min, out var max); - ColorRgb565 c0 = max; - ColorRgb565 c1 = min; + var c0 = max; + var c1 = min; if (hasAlpha && c0.data > c1.data) { @@ -370,17 +327,17 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) private static class Bc1AlphaBlockEncoderBalanced { - private const int maxTries = 24 * 2; - private const float errorThreshold = 0.05f; + private const int MaxTries = 24 * 2; + private const float ErrorThreshold = 0.05f; internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { var pixels = rawBlock.AsSpan; - bool hasAlpha = rawBlock.HasTransparentPixels(); + var hasAlpha = rawBlock.HasTransparentPixels(); - PcaVectors.Create(pixels, out System.Numerics.Vector3 mean, out System.Numerics.Vector3 pa); + PcaVectors.Create(pixels, out var mean, out var pa); PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); var c0 = max; @@ -391,30 +348,35 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) var c = c0; c0 = c1; c1 = c; - }else if (hasAlpha && c1.data < c0.data) { + } + else if (hasAlpha && c1.data < c0.data) + { var c = c0; c0 = c1; c1 = c; } - Bc1Block best = TryColors(rawBlock, c0, c1, out float bestError); - - for (int i = 0; i < maxTries; i++) { + var best = TryColors(rawBlock, c0, c1, out var bestError); + + for (var i = 0; i < MaxTries; i++) + { var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); - + if (!hasAlpha && newC0.data < newC1.data) { var c = newC0; newC0 = newC1; newC1 = c; - }else if (hasAlpha && newC1.data < newC0.data) { + } + else if (hasAlpha && newC1.data < newC0.data) + { var c = newC0; newC0 = newC1; newC1 = c; } - + var block = TryColors(rawBlock, newC0, newC1, out var error); - + if (error < bestError) { best = block; @@ -423,7 +385,8 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) c1 = newC1; } - if (bestError < errorThreshold) { + if (bestError < ErrorThreshold) + { break; } } @@ -434,16 +397,16 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) private static class Bc1AlphaBlockEncoderSlow { - private const int maxTries = 9999; - private const float errorThreshold = 0.05f; + private const int MaxTries = 9999; + private const float ErrorThreshold = 0.05f; internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { var pixels = rawBlock.AsSpan; - bool hasAlpha = rawBlock.HasTransparentPixels(); + var hasAlpha = rawBlock.HasTransparentPixels(); - PcaVectors.Create(pixels, out System.Numerics.Vector3 mean, out System.Numerics.Vector3 pa); + PcaVectors.Create(pixels, out var mean, out var pa); PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); var c0 = max; @@ -454,29 +417,34 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) var c = c0; c0 = c1; c1 = c; - }else if (hasAlpha && c1.data < c0.data) { + } + else if (hasAlpha && c1.data < c0.data) + { var c = c0; c0 = c1; c1 = c; } - Bc1Block best = TryColors(rawBlock, c0, c1, out float bestError); + var best = TryColors(rawBlock, c0, c1, out var bestError); - int lastChanged = 0; - for (int i = 0; i < maxTries; i++) { + var lastChanged = 0; + for (var i = 0; i < MaxTries; i++) + { var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); - + if (!hasAlpha && newC0.data < newC1.data) { var c = newC0; newC0 = newC1; newC1 = c; - }else if (hasAlpha && newC1.data < newC0.data) { + } + else if (hasAlpha && newC1.data < newC0.data) + { var c = newC0; newC0 = newC1; newC1 = c; } - + var block = TryColors(rawBlock, newC0, newC1, out var error); lastChanged++; @@ -490,7 +458,8 @@ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) lastChanged = 0; } - if (bestError < errorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) { + if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) + { break; } } diff --git a/BCnEnc.Net/Encoder/Bc2BlockEncoder.cs b/BCnEnc.Net/Encoder/Bc2BlockEncoder.cs index 60d50dc..33b56af 100644 --- a/BCnEnc.Net/Encoder/Bc2BlockEncoder.cs +++ b/BCnEnc.Net/Encoder/Bc2BlockEncoder.cs @@ -1,46 +1,20 @@ -using System; -using System.Runtime.InteropServices; -using System.Threading.Tasks; +using System; using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; namespace BCnEncoder.Encoder { - internal class Bc2BlockEncoder : IBcBlockEncoder + internal class Bc2BlockEncoder : BaseBcBlockEncoder { - - public byte[] Encode(RawBlock4X4Rgba32[] blocks, int blockWidth, int blockHeight, EncodingQuality quality, bool parallel) - { - byte[] outputData = new byte[blockWidth * blockHeight * Marshal.SizeOf()]; - Span outputBlocks = MemoryMarshal.Cast(outputData); - - if (parallel) - { - Parallel.For(0, blocks.Length, i => - { - Span outputBlocks = MemoryMarshal.Cast(outputData); - outputBlocks[i] = EncodeBlock(blocks[i], quality); - }); - } - else - { - for (int i = 0; i < blocks.Length; i++) - { - outputBlocks[i] = EncodeBlock(blocks[i], quality); - } - } - - return outputData; - } - - private Bc2Block EncodeBlock(RawBlock4X4Rgba32 block, EncodingQuality quality) + public override Bc2Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) { switch (quality) { - case EncodingQuality.Fast: + case CompressionQuality.Fast: return Bc2BlockEncoderFast.EncodeBlock(block); - case EncodingQuality.Balanced: + case CompressionQuality.Balanced: return Bc2BlockEncoderBalanced.EncodeBlock(block); - case EncodingQuality.BestQuality: + case CompressionQuality.BestQuality: return Bc2BlockEncoderSlow.EncodeBlock(block); default: @@ -48,25 +22,26 @@ private Bc2Block EncodeBlock(RawBlock4X4Rgba32 block, EncodingQuality quality) } } - public GlInternalFormat GetInternalFormat() + public override GlInternalFormat GetInternalFormat() { - return GlInternalFormat.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; + return GlInternalFormat.GlCompressedRgbaS3TcDxt3Ext; } - public GLFormat GetBaseInternalFormat() + public override GlFormat GetBaseInternalFormat() { - return GLFormat.GL_RGBA; + return GlFormat.GlRgba; } - public DXGI_FORMAT GetDxgiFormat() { - return DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM; + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc2Unorm; } #region Encoding private stuff private static Bc2Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f) { - Bc2Block output = new Bc2Block(); + var output = new Bc2Block(); var pixels = rawBlock.AsSpan; @@ -77,17 +52,17 @@ private static Bc2Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0 var c1 = color1.ToColorRgb24(); ReadOnlySpan colors = stackalloc ColorRgb24[] { - c0, - c1, - c0 * (2.0 / 3.0) + c1 * (1.0 / 3.0), - c0 * (1.0 / 3.0) + c1 * (2.0 / 3.0) - }; + c0, + c1, + c0.InterpolateThird(c1, 1), + c0.InterpolateThird(c1, 2) + }; error = 0; - for (int i = 0; i < 16; i++) + for (var i = 0; i < 16; i++) { var color = pixels[i]; - output.SetAlpha(i, color.A); + output.SetAlpha(i, color.a); output[i] = ColorChooser.ChooseClosestColor4(colors, color, rWeight, gWeight, bWeight, out var e); error += e; } @@ -109,9 +84,9 @@ internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) PcaVectors.Create(pixels, out var mean, out var principalAxis); PcaVectors.GetMinMaxColor565(pixels, mean, principalAxis, out var min, out var max); - - ColorRgb565 c0 = max; - ColorRgb565 c1 = min; + + var c0 = max; + var c1 = min; var output = TryColors(rawBlock, c0, c1, out var _); @@ -119,27 +94,29 @@ internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) } } - private static class Bc2BlockEncoderBalanced { - private const int maxTries = 24 * 2; - private const float errorThreshold = 0.05f; + private static class Bc2BlockEncoderBalanced + { + private const int MaxTries = 24 * 2; + private const float ErrorThreshold = 0.05f; internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { var pixels = rawBlock.AsSpan; - PcaVectors.Create(pixels, out System.Numerics.Vector3 mean, out System.Numerics.Vector3 pa); + PcaVectors.Create(pixels, out var mean, out var pa); PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); var c0 = max; var c1 = min; - Bc2Block best = TryColors(rawBlock, c0, c1, out float bestError); - - for (int i = 0; i < maxTries; i++) { + var best = TryColors(rawBlock, c0, c1, out var bestError); + + for (var i = 0; i < MaxTries; i++) + { var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); - + var block = TryColors(rawBlock, newC0, newC1, out var error); - + if (error < bestError) { best = block; @@ -148,7 +125,8 @@ internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) c1 = newC1; } - if (bestError < errorThreshold) { + if (bestError < ErrorThreshold) + { break; } } @@ -159,15 +137,15 @@ internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) private static class Bc2BlockEncoderSlow { - private const int maxTries = 9999; - private const float errorThreshold = 0.01f; + private const int MaxTries = 9999; + private const float ErrorThreshold = 0.01f; internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { var pixels = rawBlock.AsSpan; - PcaVectors.Create(pixels, out System.Numerics.Vector3 mean, out System.Numerics.Vector3 pa); + PcaVectors.Create(pixels, out var mean, out var pa); PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); var c0 = max; @@ -180,20 +158,21 @@ internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) c1 = c; } - Bc2Block best = TryColors(rawBlock, c0, c1, out float bestError); + var best = TryColors(rawBlock, c0, c1, out var bestError); - int lastChanged = 0; + var lastChanged = 0; - for (int i = 0; i < maxTries; i++) { + for (var i = 0; i < MaxTries; i++) + { var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); - + if (newC0.data < newC1.data) { var c = newC0; newC0 = newC1; newC1 = c; } - + var block = TryColors(rawBlock, newC0, newC1, out var error); lastChanged++; @@ -207,7 +186,8 @@ internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) lastChanged = 0; } - if (bestError < errorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) { + if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) + { break; } } @@ -215,7 +195,7 @@ internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) return best; } } - + #endregion } } diff --git a/BCnEnc.Net/Encoder/Bc3BlockEncoder.cs b/BCnEnc.Net/Encoder/Bc3BlockEncoder.cs index de10edd..d12e55c 100644 --- a/BCnEnc.Net/Encoder/Bc3BlockEncoder.cs +++ b/BCnEnc.Net/Encoder/Bc3BlockEncoder.cs @@ -1,47 +1,22 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading.Tasks; +using System; using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; namespace BCnEncoder.Encoder { - internal class Bc3BlockEncoder : IBcBlockEncoder + internal class Bc3BlockEncoder : BaseBcBlockEncoder { + private static readonly Bc4ComponentBlockEncoder bc4BlockEncoder = new Bc4ComponentBlockEncoder(ColorComponent.A); - public byte[] Encode(RawBlock4X4Rgba32[] blocks, int blockWidth, int blockHeight, EncodingQuality quality, bool parallel) - { - byte[] outputData = new byte[blockWidth * blockHeight * Marshal.SizeOf()]; - Span outputBlocks = MemoryMarshal.Cast(outputData); - - if (parallel) - { - Parallel.For(0, blocks.Length, i => - { - Span outputBlocks = MemoryMarshal.Cast(outputData); - outputBlocks[i] = EncodeBlock(blocks[i], quality); - }); - } - else - { - for (int i = 0; i < blocks.Length; i++) - { - outputBlocks[i] = EncodeBlock(blocks[i], quality); - } - } - - return outputData; - } - - private Bc3Block EncodeBlock(RawBlock4X4Rgba32 block, EncodingQuality quality) + public override Bc3Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) { switch (quality) { - case EncodingQuality.Fast: + case CompressionQuality.Fast: return Bc3BlockEncoderFast.EncodeBlock(block); - case EncodingQuality.Balanced: + case CompressionQuality.Balanced: return Bc3BlockEncoderBalanced.EncodeBlock(block); - case EncodingQuality.BestQuality: + case CompressionQuality.BestQuality: return Bc3BlockEncoderSlow.EncodeBlock(block); default: @@ -49,25 +24,26 @@ private Bc3Block EncodeBlock(RawBlock4X4Rgba32 block, EncodingQuality quality) } } - public GlInternalFormat GetInternalFormat() + public override GlInternalFormat GetInternalFormat() { - return GlInternalFormat.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; + return GlInternalFormat.GlCompressedRgbaS3TcDxt5Ext; } - public GLFormat GetBaseInternalFormat() + public override GlFormat GetBaseInternalFormat() { - return GLFormat.GL_RGBA; + return GlFormat.GlRgba; } - public DXGI_FORMAT GetDxgiFormat() { - return DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM; + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc3Unorm; } #region Encoding private stuff private static Bc3Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f) { - Bc3Block output = new Bc3Block(); + var output = new Bc3Block(); var pixels = rawBlock.AsSpan; @@ -80,12 +56,12 @@ private static Bc3Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0 ReadOnlySpan colors = stackalloc ColorRgb24[] { c0, c1, - c0 * (2.0 / 3.0) + c1 * (1.0 / 3.0), - c0 * (1.0 / 3.0) + c1 * (2.0 / 3.0) + c0.InterpolateThird(c1, 1), + c0.InterpolateThird(c1, 2) }; error = 0; - for (int i = 0; i < 16; i++) + for (var i = 0; i < 16; i++) { var color = pixels[i]; output[i] = ColorChooser.ChooseClosestColor4(colors, color, rWeight, gWeight, bWeight, out var e); @@ -95,172 +71,12 @@ private static Bc3Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0 return output; } - private static Bc3Block FindAlphaValues(Bc3Block colorBlock, RawBlock4X4Rgba32 rawBlock, int variations) { - var pixels = rawBlock.AsSpan; - - //Find min and max alpha - byte minAlpha = 255; - byte maxAlpha = 0; - bool hasExtremeValues = false; - for (int i = 0; i < pixels.Length; i++) { - if (pixels[i].A < 255 && pixels[i].A > 0) { - if (pixels[i].A < minAlpha) minAlpha = pixels[i].A; - if (pixels[i].A > maxAlpha) maxAlpha = pixels[i].A; - } - else { - hasExtremeValues = true; - } - } - - - int SelectAlphaIndices(ref Bc3Block block) { - int cumulativeError = 0; - var a0 = block.Alpha0; - var a1 = block.Alpha1; - Span alphas = a0 > a1 ? stackalloc byte[] { - a0, - a1, - (byte) (6/7.0 * a0 + 1/7.0 * a1), - (byte) (5/7.0 * a0 + 2/7.0 * a1), - (byte) (4/7.0 * a0 + 3/7.0 * a1), - (byte) (3/7.0 * a0 + 4/7.0 * a1), - (byte) (2/7.0 * a0 + 5/7.0 * a1), - (byte) (1/7.0 * a0 + 6/7.0 * a1), - } : stackalloc byte[] { - a0, - a1, - (byte) (4/5.0 * a0 + 1/5.0 * a1), - (byte) (3/5.0 * a0 + 2/5.0 * a1), - (byte) (2/5.0 * a0 + 3/5.0 * a1), - (byte) (1/5.0 * a0 + 4/5.0 * a1), - 0, - 255 - }; - var pixels = rawBlock.AsSpan; - for (int i = 0; i < pixels.Length; i++) { - byte bestIndex = 0; - int bestError = Math.Abs(pixels[i].A - alphas[0]); - for (byte j = 1; j < alphas.Length; j++) { - int error = Math.Abs(pixels[i].A - alphas[j]); - if (error < bestError) { - bestIndex = j; - bestError = error; - } - if (bestError == 0) break; - } - block.SetAlphaIndex(i, bestIndex); - cumulativeError += bestError * bestError; - } - - return cumulativeError; - } - - //everything is either fully opaque or fully transparent - if (hasExtremeValues && minAlpha == 255 && maxAlpha == 0) { - colorBlock.Alpha0 = 0; - colorBlock.Alpha1 = 255; - var error = SelectAlphaIndices(ref colorBlock); - Debug.Assert(0 == error); - return colorBlock; - } - - var best = colorBlock; - best.Alpha0 = maxAlpha; - best.Alpha1 = minAlpha; - int bestError = SelectAlphaIndices(ref best); - if (bestError == 0) { - return best; - } - for (byte i = 1; i < variations; i++) { - { - byte a0 = ByteHelper.ClampToByte(maxAlpha - i * 2); - byte a1 = ByteHelper.ClampToByte(minAlpha + i * 2); - var block = colorBlock; - block.Alpha0 = hasExtremeValues ? a1 : a0; - block.Alpha1 = hasExtremeValues ? a0 : a1; - int error = SelectAlphaIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - } - } - { - byte a0 = ByteHelper.ClampToByte(maxAlpha + i * 2); - byte a1 = ByteHelper.ClampToByte(minAlpha - i * 2); - var block = colorBlock; - block.Alpha0 = hasExtremeValues ? a1 : a0; - block.Alpha1 = hasExtremeValues ? a0 : a1; - int error = SelectAlphaIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - } - } - { - byte a0 = ByteHelper.ClampToByte(maxAlpha); - byte a1 = ByteHelper.ClampToByte(minAlpha - i * 2); - var block = colorBlock; - block.Alpha0 = hasExtremeValues ? a1 : a0; - block.Alpha1 = hasExtremeValues ? a0 : a1; - int error = SelectAlphaIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - } - } - { - byte a0 = ByteHelper.ClampToByte(maxAlpha + i * 2); - byte a1 = ByteHelper.ClampToByte(minAlpha); - var block = colorBlock; - block.Alpha0 = hasExtremeValues ? a1 : a0; - block.Alpha1 = hasExtremeValues ? a0 : a1; - int error = SelectAlphaIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - } - } - { - byte a0 = ByteHelper.ClampToByte(maxAlpha); - byte a1 = ByteHelper.ClampToByte(minAlpha + i * 2); - var block = colorBlock; - block.Alpha0 = hasExtremeValues ? a1 : a0; - block.Alpha1 = hasExtremeValues ? a0 : a1; - int error = SelectAlphaIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - } - } - { - byte a0 = ByteHelper.ClampToByte(maxAlpha - i * 2); - byte a1 = ByteHelper.ClampToByte(minAlpha); - var block = colorBlock; - block.Alpha0 = hasExtremeValues ? a1 : a0; - block.Alpha1 = hasExtremeValues ? a0 : a1; - int error = SelectAlphaIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - } - } - - if (bestError < 10) { - break; - } - } - - return best; - } - - #endregion #region Encoders private static class Bc3BlockEncoderFast { - internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { var pixels = rawBlock.AsSpan; @@ -268,8 +84,8 @@ internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) PcaVectors.Create(pixels, out var mean, out var principalAxis); PcaVectors.GetMinMaxColor565(pixels, mean, principalAxis, out var min, out var max); - ColorRgb565 c0 = max; - ColorRgb565 c1 = min; + var c0 = max; + var c1 = min; if (c0.data <= c1.data) { @@ -278,34 +94,36 @@ internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) c1 = c; } - var output = TryColors(rawBlock, c0, c1, out float _); - output = FindAlphaValues(output, rawBlock, 3); + var output = TryColors(rawBlock, c0, c1, out _); + output.alphaBlock = bc4BlockEncoder.EncodeBlock(rawBlock, CompressionQuality.Fast); return output; } } - private static class Bc3BlockEncoderBalanced { - private const int maxTries = 24 * 2; - private const float errorThreshold = 0.05f; + private static class Bc3BlockEncoderBalanced + { + private const int MaxTries = 24 * 2; + private const float ErrorThreshold = 0.05f; internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { var pixels = rawBlock.AsSpan; - PcaVectors.Create(pixels, out System.Numerics.Vector3 mean, out System.Numerics.Vector3 pa); + PcaVectors.Create(pixels, out var mean, out var pa); PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); var c0 = max; var c1 = min; - Bc3Block best = TryColors(rawBlock, c0, c1, out float bestError); - - for (int i = 0; i < maxTries; i++) { + var best = TryColors(rawBlock, c0, c1, out var bestError); + + for (var i = 0; i < MaxTries; i++) + { var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); - + var block = TryColors(rawBlock, newC0, newC1, out var error); - + if (error < bestError) { best = block; @@ -314,26 +132,27 @@ internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) c1 = newC1; } - if (bestError < errorThreshold) { + if (bestError < ErrorThreshold) + { break; } } - best = FindAlphaValues(best, rawBlock, 5); + best.alphaBlock = bc4BlockEncoder.EncodeBlock(rawBlock, CompressionQuality.Balanced); return best; } } private static class Bc3BlockEncoderSlow { - private const int maxTries = 9999; - private const float errorThreshold = 0.01f; + private const int MaxTries = 9999; + private const float ErrorThreshold = 0.01f; internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { var pixels = rawBlock.AsSpan; - PcaVectors.Create(pixels, out System.Numerics.Vector3 mean, out System.Numerics.Vector3 pa); + PcaVectors.Create(pixels, out var mean, out var pa); PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); var c0 = max; @@ -346,20 +165,21 @@ internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) c1 = c; } - Bc3Block best = TryColors(rawBlock, c0, c1, out float bestError); + var best = TryColors(rawBlock, c0, c1, out var bestError); - int lastChanged = 0; + var lastChanged = 0; - for (int i = 0; i < maxTries; i++) { + for (var i = 0; i < MaxTries; i++) + { var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); - + if (newC0.data < newC1.data) { var c = newC0; newC0 = newC1; newC1 = c; } - + var block = TryColors(rawBlock, newC0, newC1, out var error); lastChanged++; @@ -373,12 +193,13 @@ internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) lastChanged = 0; } - if (bestError < errorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) { + if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) + { break; } } - best = FindAlphaValues(best, rawBlock, 8); + best.alphaBlock = bc4BlockEncoder.EncodeBlock(rawBlock, CompressionQuality.BestQuality); return best; } } diff --git a/BCnEnc.Net/Encoder/Bc4BlockEncoder.cs b/BCnEnc.Net/Encoder/Bc4BlockEncoder.cs index 44d151b..fc52784 100644 --- a/BCnEnc.Net/Encoder/Bc4BlockEncoder.cs +++ b/BCnEnc.Net/Encoder/Bc4BlockEncoder.cs @@ -1,125 +1,134 @@ -using System; +using System; using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading.Tasks; using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; namespace BCnEncoder.Encoder { - internal class Bc4BlockEncoder : IBcBlockEncoder { + internal class Bc4BlockEncoder : BaseBcBlockEncoder + { + private readonly Bc4ComponentBlockEncoder bc4Encoder; - private readonly bool luminanceAsRed; - public Bc4BlockEncoder(bool luminanceAsRed) { - this.luminanceAsRed = luminanceAsRed; + public Bc4BlockEncoder(ColorComponent component) + { + bc4Encoder = new Bc4ComponentBlockEncoder(component); } - public byte[] Encode(RawBlock4X4Rgba32[] blocks, int blockWidth, int blockHeight, EncodingQuality quality, - bool parallel) { - byte[] outputData = new byte[blockWidth * blockHeight * Marshal.SizeOf()]; - Span outputBlocks = MemoryMarshal.Cast(outputData); + public override Bc4Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + var output = new Bc4Block + { + componentBlock = bc4Encoder.EncodeBlock(block, quality) + }; - if (parallel) { - Parallel.For(0, blocks.Length, i => { - Span outputBlocks = MemoryMarshal.Cast(outputData); - outputBlocks[i] = EncodeBlock(blocks[i], quality); - }); - } - else { - for (int i = 0; i < blocks.Length; i++) { - outputBlocks[i] = EncodeBlock(blocks[i], quality); - } - } - - return outputData; + return output; } - private Bc4Block EncodeBlock(RawBlock4X4Rgba32 block, EncodingQuality quality) { - Bc4Block output = new Bc4Block(); - byte[] colors = new byte[16]; - var pixels = block.AsSpan; - for (int i = 0; i < 16; i++) { - if (luminanceAsRed) { - colors[i] = (byte)(new ColorYCbCr(pixels[i]).y * 255); - } - else { - colors[i] = pixels[i].R; - } - } - switch (quality) { - case EncodingQuality.Fast: - return FindRedValues(output, colors, 3); - case EncodingQuality.Balanced: - return FindRedValues(output, colors, 4); - case EncodingQuality.BestQuality: - return FindRedValues(output, colors, 8); + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRedRgtc1Ext; + } - default: - throw new ArgumentOutOfRangeException(nameof(quality), quality, null); - } + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRed; } - public GlInternalFormat GetInternalFormat() { - return GlInternalFormat.GL_COMPRESSED_RED_RGTC1_EXT; + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc4Unorm; } + } + + internal class Bc4ComponentBlockEncoder + { + private readonly ColorComponent component; - public GLFormat GetBaseInternalFormat() { - return GLFormat.GL_RED; + public Bc4ComponentBlockEncoder(ColorComponent component) + { + this.component = component; } - public DXGI_FORMAT GetDxgiFormat() { - return DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM; + public Bc4ComponentBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + var output = new Bc4ComponentBlock(); + + var pixels = block.AsSpan; + var colors = new byte[pixels.Length]; + + for (var i = 0; i < pixels.Length; i++) + colors[i] = ComponentHelper.ColorToComponent(pixels[i], component); + + switch (quality) + { + case CompressionQuality.Fast: + return FindComponentValues(output, colors, 3); + case CompressionQuality.Balanced: + return FindComponentValues(output, colors, 4); + case CompressionQuality.BestQuality: + return FindComponentValues(output, colors, 8); + + default: + throw new ArgumentOutOfRangeException(nameof(quality), quality, null); + } } #region Encoding private stuff - private static Bc4Block FindRedValues(Bc4Block colorBlock, byte[] pixels, int variations) { + private static Bc4ComponentBlock FindComponentValues(Bc4ComponentBlock colorBlock, byte[] pixels, int variations) + { //Find min and max alpha byte min = 255; byte max = 0; - bool hasExtremeValues = false; - for (int i = 0; i < pixels.Length; i++) { - if (pixels[i] < 255 && pixels[i] > 0) { + var hasExtremeValues = false; + for (var i = 0; i < pixels.Length; i++) + { + if (pixels[i] < 255 && pixels[i] > 0) + { if (pixels[i] < min) min = pixels[i]; if (pixels[i] > max) max = pixels[i]; } - else { + else + { hasExtremeValues = true; } } - int SelectIndices(ref Bc4Block block) { - int cumulativeError = 0; - var c0 = block.Red0; - var c1 = block.Red1; - Span colors = c0 > c1 - ? stackalloc byte[] { - c0, - c1, - (byte) (6 / 7.0 * c0 + 1 / 7.0 * c1), - (byte) (5 / 7.0 * c0 + 2 / 7.0 * c1), - (byte) (4 / 7.0 * c0 + 3 / 7.0 * c1), - (byte) (3 / 7.0 * c0 + 4 / 7.0 * c1), - (byte) (2 / 7.0 * c0 + 5 / 7.0 * c1), - (byte) (1 / 7.0 * c0 + 6 / 7.0 * c1), - } - : stackalloc byte[] { - c0, - c1, - (byte) (4 / 5.0 * c0 + 1 / 5.0 * c1), - (byte) (3 / 5.0 * c0 + 2 / 5.0 * c1), - (byte) (2 / 5.0 * c0 + 3 / 5.0 * c1), - (byte) (1 / 5.0 * c0 + 4 / 5.0 * c1), - 0, - 255 - }; - for (int i = 0; i < pixels.Length; i++) { + int SelectIndices(ref Bc4ComponentBlock block) + { + var cumulativeError = 0; + var c0 = block.Endpoint0; + var c1 = block.Endpoint1; + var colors = c0 > c1 ? stackalloc byte[] { + c0, + c1, + c0.InterpolateSeventh(c1, 1), + c0.InterpolateSeventh(c1, 2), + c0.InterpolateSeventh(c1, 3), + c0.InterpolateSeventh(c1, 4), + c0.InterpolateSeventh(c1, 5), + c0.InterpolateSeventh(c1, 6) + } : stackalloc byte[] { + c0, + c1, + c0.InterpolateFifth(c1, 1), + c0.InterpolateFifth(c1, 2), + c0.InterpolateFifth(c1, 3), + c0.InterpolateFifth(c1, 4), + 0, + 255 + }; + for (var i = 0; i < pixels.Length; i++) + { byte bestIndex = 0; - int bestError = Math.Abs(pixels[i] - colors[0]); - for (byte j = 1; j < colors.Length; j++) { - int error = Math.Abs(pixels[i] - colors[j]); - if (error < bestError) { + var bestError = Math.Abs(pixels[i] - colors[0]); + for (byte j = 1; j < colors.Length; j++) + { + var error = Math.Abs(pixels[i] - colors[j]); + if (error < bestError) + { bestIndex = j; bestError = error; } @@ -127,7 +136,7 @@ int SelectIndices(ref Bc4Block block) { if (bestError == 0) break; } - block.SetRedIndex(i, bestIndex); + block.SetComponentIndex(i, bestIndex); cumulativeError += bestError * bestError; } @@ -135,31 +144,35 @@ int SelectIndices(ref Bc4Block block) { } //everything is either fully black or fully red - if (hasExtremeValues && min == 255 && max == 0) { - colorBlock.Red0 = 0; - colorBlock.Red1 = 255; + if (hasExtremeValues && min == 255 && max == 0) + { + colorBlock.Endpoint0 = 0; + colorBlock.Endpoint1 = 255; var error = SelectIndices(ref colorBlock); Debug.Assert(0 == error); return colorBlock; } var best = colorBlock; - best.Red0 = max; - best.Red1 = min; - int bestError = SelectIndices(ref best); - if (bestError == 0) { + best.Endpoint0 = max; + best.Endpoint1 = min; + var bestError = SelectIndices(ref best); + if (bestError == 0) + { return best; } - for (byte i = (byte)variations; i > 0; i--) { + for (var i = (byte)variations; i > 0; i--) + { { - byte c0 = ByteHelper.ClampToByte(max - i); - byte c1 = ByteHelper.ClampToByte(min + i); + var c0 = ByteHelper.ClampToByte(max - i); + var c1 = ByteHelper.ClampToByte(min + i); var block = colorBlock; - block.Red0 = hasExtremeValues ? c1 : c0; - block.Red1 = hasExtremeValues ? c0 : c1; - int error = SelectIndices(ref block); - if (error < bestError) { + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { best = block; bestError = error; max = c0; @@ -167,13 +180,14 @@ int SelectIndices(ref Bc4Block block) { } } { - byte c0 = ByteHelper.ClampToByte(max + i); - byte c1 = ByteHelper.ClampToByte(min - i); + var c0 = ByteHelper.ClampToByte(max + i); + var c1 = ByteHelper.ClampToByte(min - i); var block = colorBlock; - block.Red0 = hasExtremeValues ? c1 : c0; - block.Red1 = hasExtremeValues ? c0 : c1; - int error = SelectIndices(ref block); - if (error < bestError) { + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { best = block; bestError = error; max = c0; @@ -181,13 +195,14 @@ int SelectIndices(ref Bc4Block block) { } } { - byte c0 = ByteHelper.ClampToByte(max); - byte c1 = ByteHelper.ClampToByte(min - i); + var c0 = ByteHelper.ClampToByte(max); + var c1 = ByteHelper.ClampToByte(min - i); var block = colorBlock; - block.Red0 = hasExtremeValues ? c1 : c0; - block.Red1 = hasExtremeValues ? c0 : c1; - int error = SelectIndices(ref block); - if (error < bestError) { + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { best = block; bestError = error; max = c0; @@ -195,13 +210,14 @@ int SelectIndices(ref Bc4Block block) { } } { - byte c0 = ByteHelper.ClampToByte(max + i); - byte c1 = ByteHelper.ClampToByte(min); + var c0 = ByteHelper.ClampToByte(max + i); + var c1 = ByteHelper.ClampToByte(min); var block = colorBlock; - block.Red0 = hasExtremeValues ? c1 : c0; - block.Red1 = hasExtremeValues ? c0 : c1; - int error = SelectIndices(ref block); - if (error < bestError) { + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { best = block; bestError = error; max = c0; @@ -209,13 +225,14 @@ int SelectIndices(ref Bc4Block block) { } } { - byte c0 = ByteHelper.ClampToByte(max); - byte c1 = ByteHelper.ClampToByte(min + i); + var c0 = ByteHelper.ClampToByte(max); + var c1 = ByteHelper.ClampToByte(min + i); var block = colorBlock; - block.Red0 = hasExtremeValues ? c1 : c0; - block.Red1 = hasExtremeValues ? c0 : c1; - int error = SelectIndices(ref block); - if (error < bestError) { + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { best = block; bestError = error; max = c0; @@ -223,13 +240,14 @@ int SelectIndices(ref Bc4Block block) { } } { - byte c0 = ByteHelper.ClampToByte(max - i); - byte c1 = ByteHelper.ClampToByte(min); + var c0 = ByteHelper.ClampToByte(max - i); + var c1 = ByteHelper.ClampToByte(min); var block = colorBlock; - block.Red0 = hasExtremeValues ? c1 : c0; - block.Red1 = hasExtremeValues ? c0 : c1; - int error = SelectIndices(ref block); - if (error < bestError) { + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { best = block; bestError = error; max = c0; @@ -237,7 +255,8 @@ int SelectIndices(ref Bc4Block block) { } } - if (bestError < 5) { + if (bestError < 5) + { break; } } @@ -245,7 +264,6 @@ int SelectIndices(ref Bc4Block block) { return best; } - #endregion } } diff --git a/BCnEnc.Net/Encoder/Bc5BlockEncoder.cs b/BCnEnc.Net/Encoder/Bc5BlockEncoder.cs index f091e5e..6f335b4 100644 --- a/BCnEnc.Net/Encoder/Bc5BlockEncoder.cs +++ b/BCnEnc.Net/Encoder/Bc5BlockEncoder.cs @@ -1,317 +1,41 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading.Tasks; using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; namespace BCnEncoder.Encoder { - internal class Bc5BlockEncoder : IBcBlockEncoder { + internal class Bc5BlockEncoder : BaseBcBlockEncoder + { + private readonly Bc4ComponentBlockEncoder redBlockEncoder; + private readonly Bc4ComponentBlockEncoder greenBlockEncoder; - public byte[] Encode(RawBlock4X4Rgba32[] blocks, int blockWidth, int blockHeight, EncodingQuality quality, bool parallel) + public Bc5BlockEncoder(ColorComponent component1, ColorComponent component2) { - byte[] outputData = new byte[blockWidth * blockHeight * Marshal.SizeOf()]; - Span outputBlocks = MemoryMarshal.Cast(outputData); - - if (parallel) { - Parallel.For(0, blocks.Length, i => { - Span outputBlocks = MemoryMarshal.Cast(outputData); - outputBlocks[i] = EncodeBlock(blocks[i], quality); - }); - } - else { - for (int i = 0; i < blocks.Length; i++) { - outputBlocks[i] = EncodeBlock(blocks[i], quality); - } - } - - return outputData; - } - - private Bc5Block EncodeBlock(RawBlock4X4Rgba32 block, EncodingQuality quality) { - Bc5Block output = new Bc5Block(); - byte[] reds = new byte[16]; - byte[] greens = new byte[16]; - var pixels = block.AsSpan; - for (int i = 0; i < 16; i++) { - reds[i] = pixels[i].R; - greens[i] = pixels[i].G; - } - - int variations = 0; - int errorThreshold = 0; - switch (quality) { - case EncodingQuality.Fast: - variations = 3; - errorThreshold = 5; - break; - case EncodingQuality.Balanced: - variations = 5; - errorThreshold = 1; - break; - case EncodingQuality.BestQuality: - variations = 8; - errorThreshold = 0; - break; - default: - throw new ArgumentOutOfRangeException(nameof(quality), quality, null); - } - - - output = FindValues(output, reds, variations, errorThreshold, - (block, i, idx) => { - block.SetRedIndex(i, idx); - return block; - }, - (block, col) => { - block.Red0 = col; - return block; - }, - (block, col) => { - block.Red1 = col; - return block; - }, - (block) => { - return block.Red0; - }, - (block) => { - return block.Red1; - } - ); - output = FindValues(output, greens, variations, errorThreshold, - (block, i, idx) => { - block.SetGreenIndex(i, idx); - return block; - }, - (block, col) => { - block.Green0 = col; - return block; - }, - (block, col) => { - block.Green1 = col; - return block; - }, - (block) => { - return block.Green0; - }, - (block) => { - return block.Green1; - }); - return output; + redBlockEncoder = new Bc4ComponentBlockEncoder(component1); + greenBlockEncoder = new Bc4ComponentBlockEncoder(component2); } - public GlInternalFormat GetInternalFormat() { - return GlInternalFormat.GL_COMPRESSED_RED_GREEN_RGTC2_EXT; + public override Bc5Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + return new Bc5Block + { + redBlock = redBlockEncoder.EncodeBlock(block, quality), + greenBlock = greenBlockEncoder.EncodeBlock(block, quality) + }; } - public GLFormat GetBaseInternalFormat() { - return GLFormat.GL_RG; + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRedGreenRgtc2Ext; } - public DXGI_FORMAT GetDxgiFormat() { - return DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM; + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRg; } - #region Encoding private stuff - - private static Bc5Block FindValues(Bc5Block colorBlock, byte[] pixels, int variations, int errorThreshold, - Func indexSetter, - Func col0Setter, - Func col1Setter, - Func col0Getter, - Func col1Getter) { - - //Find min and max alpha - byte min = 255; - byte max = 0; - bool hasExtremeValues = false; - for (int i = 0; i < pixels.Length; i++) { - if (pixels[i] < 255 && pixels[i] > 0) { - if (pixels[i] < min) min = pixels[i]; - if (pixels[i] > max) max = pixels[i]; - } - else { - hasExtremeValues = true; - } - } - - - int SelectIndices(ref Bc5Block block) { - int cumulativeError = 0; - //var c0 = block.Red0; - //var c1 = block.Red1; - var c0 = col0Getter(block); - var c1 = col1Getter(block); - Span colors = c0 > c1 - ? stackalloc byte[] { - c0, - c1, - (byte) (6 / 7.0 * c0 + 1 / 7.0 * c1), - (byte) (5 / 7.0 * c0 + 2 / 7.0 * c1), - (byte) (4 / 7.0 * c0 + 3 / 7.0 * c1), - (byte) (3 / 7.0 * c0 + 4 / 7.0 * c1), - (byte) (2 / 7.0 * c0 + 5 / 7.0 * c1), - (byte) (1 / 7.0 * c0 + 6 / 7.0 * c1), - } - : stackalloc byte[] { - c0, - c1, - (byte) (4 / 5.0 * c0 + 1 / 5.0 * c1), - (byte) (3 / 5.0 * c0 + 2 / 5.0 * c1), - (byte) (2 / 5.0 * c0 + 3 / 5.0 * c1), - (byte) (1 / 5.0 * c0 + 4 / 5.0 * c1), - 0, - 255 - }; - for (int i = 0; i < pixels.Length; i++) { - byte bestIndex = 0; - int bestError = Math.Abs(pixels[i] - colors[0]); - for (byte j = 1; j < colors.Length; j++) { - int error = Math.Abs(pixels[i] - colors[j]); - if (error < bestError) { - bestIndex = j; - bestError = error; - } - - if (bestError == 0) break; - } - - block = indexSetter(block, i, bestIndex); - //block.SetRedIndex(i, bestIndex); - cumulativeError += bestError * bestError; - } - - return cumulativeError; - } - - //everything is either fully black or fully red - if (hasExtremeValues && min == 255 && max == 0) { - //colorBlock.Red0 = 0; - //colorBlock.Red1 = 255; - colorBlock = col0Setter(colorBlock, 0); - colorBlock = col1Setter(colorBlock, 255); - var error = SelectIndices(ref colorBlock); - Debug.Assert(0 == error); - return colorBlock; - } - - var best = colorBlock; - //best.Red0 = max; - //best.Red1 = min; - best = col0Setter(best, max); - best = col1Setter(best, min); - int bestError = SelectIndices(ref best); - if (bestError == 0) { - return best; - } - - for (byte i = (byte)variations; i > 0; i--) { - { - byte c0 = ByteHelper.ClampToByte(max - i); - byte c1 = ByteHelper.ClampToByte(min + i); - var block = colorBlock; - //block.Red0 = hasExtremeValues ? c1 : c0; - //block.Red1 = hasExtremeValues ? c0 : c1; - block = col0Setter(block, hasExtremeValues ? c1 : c0); - block = col1Setter(block, hasExtremeValues ? c0 : c1); - int error = SelectIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - max = c0; - min = c1; - } - } - { - byte c0 = ByteHelper.ClampToByte(max + i); - byte c1 = ByteHelper.ClampToByte(min - i); - var block = colorBlock; - //block.Red0 = hasExtremeValues ? c1 : c0; - //block.Red1 = hasExtremeValues ? c0 : c1; - block = col0Setter(block, hasExtremeValues ? c1 : c0); - block = col1Setter(block, hasExtremeValues ? c0 : c1); - int error = SelectIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - max = c0; - min = c1; - } - } - { - byte c0 = ByteHelper.ClampToByte(max); - byte c1 = ByteHelper.ClampToByte(min - i); - var block = colorBlock; - //block.Red0 = hasExtremeValues ? c1 : c0; - //block.Red1 = hasExtremeValues ? c0 : c1; - block = col0Setter(block, hasExtremeValues ? c1 : c0); - block = col1Setter(block, hasExtremeValues ? c0 : c1); - int error = SelectIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - max = c0; - min = c1; - } - } - { - byte c0 = ByteHelper.ClampToByte(max + i); - byte c1 = ByteHelper.ClampToByte(min); - var block = colorBlock; - //block.Red0 = hasExtremeValues ? c1 : c0; - //block.Red1 = hasExtremeValues ? c0 : c1; - block = col0Setter(block, hasExtremeValues ? c1 : c0); - block = col1Setter(block, hasExtremeValues ? c0 : c1); - int error = SelectIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - max = c0; - min = c1; - } - } - { - byte c0 = ByteHelper.ClampToByte(max); - byte c1 = ByteHelper.ClampToByte(min + i); - var block = colorBlock; - //block.Red0 = hasExtremeValues ? c1 : c0; - //block.Red1 = hasExtremeValues ? c0 : c1; - block = col0Setter(block, hasExtremeValues ? c1 : c0); - block = col1Setter(block, hasExtremeValues ? c0 : c1); - int error = SelectIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - max = c0; - min = c1; - } - } - { - byte c0 = ByteHelper.ClampToByte(max - i); - byte c1 = ByteHelper.ClampToByte(min); - var block = colorBlock; - //block.Red0 = hasExtremeValues ? c1 : c0; - //block.Red1 = hasExtremeValues ? c0 : c1; - block = col0Setter(block, hasExtremeValues ? c1 : c0); - block = col1Setter(block, hasExtremeValues ? c0 : c1); - int error = SelectIndices(ref block); - if (error < bestError) { - best = block; - bestError = error; - max = c0; - min = c1; - } - } - - if (bestError <= errorThreshold) { - break; - } - } - - return best; + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc5Unorm; } - - - #endregion } } diff --git a/BCnEnc.Net/Encoder/BcEncoder.cs b/BCnEnc.Net/Encoder/BcEncoder.cs index 9b55e7f..bcd6ba2 100644 --- a/BCnEnc.Net/Encoder/BcEncoder.cs +++ b/BCnEnc.Net/Encoder/BcEncoder.cs @@ -1,476 +1,1844 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using BCnEncoder.Encoder.Bc7; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BCnEncoder.Encoder.Bptc; +using BCnEncoder.Encoder.Options; using BCnEncoder.Shared; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; namespace BCnEncoder.Encoder { - public class EncoderInputOptions + /// + /// The pixel format determining the rgba layout of input data in . + /// + public enum PixelFormat { /// - /// If true, when encoding to a format that only includes a red channel, - /// use the pixel luminance instead of just the red channel. Default is false. + /// 8 bits per channel RGBA. + /// + Rgba32, + + /// + /// 8 bits per channel BGRA. + /// + Bgra32, + + /// + /// 8 bits per channel ARGB. /// - public bool luminanceAsRed = false; + Argb32, + + /// + /// 8 bits per channel RGB. + /// + Rgb24, + + /// + /// 8 bits per channel BGR. + /// + Bgr24 } - public class EncoderOutputOptions - { - /// - /// Whether to generate mipMaps. Default is true. - /// - public bool generateMipMaps = true; - /// - /// The maximum number of mipmap levels to generate. -1 or 0 is unbounded. - /// Default is -1. - /// - public int maxMipMapLevel = -1; - /// - /// The compression format to use. Default is BC1. - /// - public CompressionFormat format = CompressionFormat.BC1; - /// - /// The quality of the encoding. Use either fast or balanced for testing. - /// Fast can be used for near real-time encoding for most algorithms. - /// Use bestQuality when needed. Default is balanced. - /// - public EncodingQuality quality = EncodingQuality.Balanced; - /// - /// The output file format of the data. Either Ktx or Dds. - /// Default is Ktx. - /// - public OutputFileFormat fileFormat = OutputFileFormat.Ktx; - /// - /// The DDS file format doesn't seem to have a standard for indicating whether a BC1 texture - /// includes 1bit of alpha. This option will write DDPF_ALPHAPIXELS flag to the header - /// to indicate the presence of an alpha channel. Some programs read and write this flag, - /// but some programs don't like it and get confused. Your mileage may vary. - /// Default is false. - /// - public bool ddsBc1WriteAlphaFlag = false; - } + /// + /// Handles all encoding of images into compressed or uncompressed formats. For decoding, + /// + public class BcEncoder + { + /// + /// The input options of the encoder. + /// + public EncoderInputOptions InputOptions { get; } = new EncoderInputOptions(); + + /// + /// The output options of the encoder. + /// + public EncoderOutputOptions OutputOptions { get; } = new EncoderOutputOptions(); + + /// + /// The encoder options. + /// + public EncoderOptions Options { get; } = new EncoderOptions(); + + /// + /// Creates a new instance of . + /// + /// The block compression Format to encode an image with. + public BcEncoder(CompressionFormat format = CompressionFormat.Bc1) + { + OutputOptions.Format = format; + } + + #region LDR + #region Async Api + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the given data is in. + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + public Task EncodeToStreamAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, Stream outputStream, CancellationToken token = default) + { + return Task.Run(() => + { + EncodeToStreamInternal(ByteToColorMemory(input.Span, width, height, format), outputStream, token); + }, token); + } + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously. + /// + /// The input to encode represented by a . + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + public Task EncodeToStreamAsync(ReadOnlyMemory2D input, Stream outputStream, CancellationToken token = default) + { + return Task.Run(() => + { + EncodeToStreamInternal(input, outputStream, default); + }, token); + } + + /// + /// Encodes all mipmap levels into a Ktx file asynchronously. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the given data is in. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Ktx file containing the encoded image. + public Task EncodeToKtxAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, CancellationToken token = default) + { + return Task.Run(() => EncodeToKtxInternal(ByteToColorMemory(input.Span, width, height, format), token), token); + } + + /// + /// Encodes all mipmap levels into a Ktx file asynchronously. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Ktx file containing the encoded image. + public Task EncodeToKtxAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToKtxInternal(input, token), token); + } + + /// + /// Encodes all mipmap levels into a Dds file asynchronously. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the given data is in. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Dds file containing the encoded image. + public Task EncodeToDdsAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, CancellationToken token = default) + { + return Task.Run(() => EncodeToDdsInternal(ByteToColorMemory(input.Span, width, height, format), token), token); + } + + /// + /// Encodes all mipmap levels into a Dds file asynchronously. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Dds file containing the encoded image. + public Task EncodeToDdsAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToDdsInternal(input, token), token); + } + + /// + /// Encodes all mipmap levels into a list of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the given data is in. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A list of raw encoded mipmap input. + public Task EncodeToRawBytesAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternal(ByteToColorMemory(input.Span, width, height, format), token), token); + } + + /// + /// Encodes all mipmap levels into an array of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A list of raw encoded mipmap input. + /// To get the width and height of the encoded mip levels, see . + public Task EncodeToRawBytesAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternal(input, token), token); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the given data is in. + /// The mipmap to encode. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The raw encoded input. + /// To get the width and height of the encoded mip level, see . + public Task EncodeToRawBytesAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, int mipLevel, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternal(ByteToColorMemory(input.Span, width, height, format), mipLevel, out _, out _, token), token); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The mipmap to encode. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The raw encoded input. + /// To get the width and height of the encoded mip level, see . + public Task EncodeToRawBytesAsync(ReadOnlyMemory2D input, int mipLevel, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternal(input, mipLevel, out _, out _, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a stream asynchronously either in ktx or dds format. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A . + public Task EncodeCubeMapToStreamAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToStreamInternal(right, left, top, down, back, front, outputStream, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a asynchronously. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A of type . + public Task EncodeCubeMapToKtxAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToKtxInternal(right, left, top, down, back, front, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a asynchronously. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A of type . + public Task EncodeCubeMapToDdsAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToDdsInternal(right, left, top, down, back, front, token), token); + } + + #endregion + + #region Sync Api + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the input data is in. + /// The stream to write the encoded image to. + public void EncodeToStream(ReadOnlySpan input, int width, int height, PixelFormat format, Stream outputStream) + { + EncodeToStream(ByteToColorMemory(input, width, height, format), outputStream); + } + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream. + /// + /// The input to encode represented by a . + /// The stream to write the encoded image to. + public void EncodeToStream(ReadOnlyMemory2D input, Stream outputStream) + { + EncodeToStreamInternal(input, outputStream, default); + } + + /// + /// Encodes all mipmap levels into a Ktx file. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the input data is in. + /// The containing the encoded image. + public KtxFile EncodeToKtx(ReadOnlySpan input, int width, int height, PixelFormat format) + { + return EncodeToKtx(ByteToColorMemory(input, width, height, format)); + } + + /// + /// Encodes all mipmap levels into a Ktx file. + /// + /// The input to encode represented by a . + /// The containing the encoded image. + public KtxFile EncodeToKtx(ReadOnlyMemory2D input) + { + return EncodeToKtxInternal(input, default); + } + + /// + /// Encodes all mipmap levels into a Dds file. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the input data is in. + /// The containing the encoded image. + public DdsFile EncodeToDds(ReadOnlySpan input, int width, int height, PixelFormat format) + { + return EncodeToDds(ByteToColorMemory(input, width, height, format)); + } + + /// + /// Encodes all mipmap levels into a Dds file. + /// + /// The input to encode represented by a . + /// The containing the encoded image. + public DdsFile EncodeToDds(ReadOnlyMemory2D input) + { + return EncodeToDdsInternal(input, default); + } + + /// + /// Encodes all mipmap levels into an array of byte buffers. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the input data is in. + /// An array of byte buffers containing all mipmap levels. + /// To get the width and height of the encoded mip levels, see . + public byte[][] EncodeToRawBytes(ReadOnlySpan input, int width, int height, PixelFormat format) + { + return EncodeToRawBytes(ByteToColorMemory(input, width, height, format)); + } + + /// + /// Encodes all mipmap levels into a list of byte buffers. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// An array of byte buffers containing all mipmap levels. + /// To get the width and height of the encoded mip levels, see . + public byte[][] EncodeToRawBytes(ReadOnlyMemory2D input) + { + return EncodeToRawInternal(input, default); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the input data is in. + /// The mipmap to encode. + /// The width of the mipmap. + /// The height of the mipmap. + /// A byte buffer containing the encoded data of the requested mip-level. + public byte[] EncodeToRawBytes(ReadOnlySpan input, int width, int height, PixelFormat format, int mipLevel, out int mipWidth, out int mipHeight) + { + return EncodeToRawInternal(ByteToColorMemory(input, width, height, format), mipLevel, out mipWidth, out mipHeight, default); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The mipmap to encode. + /// The width of the mipmap. + /// The height of the mipmap. + /// A byte buffer containing the encoded data of the requested mip-level. + public byte[] EncodeToRawBytes(ReadOnlyMemory2D input, int mipLevel, out int mipWidth, out int mipHeight) + { + return EncodeToRawInternal(input, mipLevel, out mipWidth, out mipHeight, default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a stream either in ktx or dds format. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The width of the faces. + /// The height of the faces. + /// The pixel format the input data is in. + /// The stream to write the encoded image to. + public void EncodeCubeMapToStream(ReadOnlySpan right, ReadOnlySpan left, + ReadOnlySpan top, ReadOnlySpan down, + ReadOnlySpan back, ReadOnlySpan front, + int width, int height, PixelFormat format, Stream outputStream) + { + EncodeCubeMapToStreamInternal( + ByteToColorMemory(right, width, height, format), + ByteToColorMemory(left, width, height, format), + ByteToColorMemory(top, width, height, format), + ByteToColorMemory(down, width, height, format), + ByteToColorMemory(back, width, height, format), + ByteToColorMemory(front, width, height, format), + outputStream, default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a stream either in ktx or dds format. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The stream to write the encoded image to. + public void EncodeCubeMapToStream(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream) + { + EncodeCubeMapToStreamInternal(right, left, top, down, back, front, outputStream, default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The width of the faces. + /// The height of the faces. + /// The pixel format the input data is in. + /// The encoded image as a . + public KtxFile EncodeCubeMapToKtx(ReadOnlySpan right, ReadOnlySpan left, + ReadOnlySpan top, ReadOnlySpan down, + ReadOnlySpan back, ReadOnlySpan front, + int width, int height, PixelFormat format) + { + return EncodeCubeMapToKtxInternal( + ByteToColorMemory(right, width, height, format), + ByteToColorMemory(left, width, height, format), + ByteToColorMemory(top, width, height, format), + ByteToColorMemory(down, width, height, format), + ByteToColorMemory(back, width, height, format), + ByteToColorMemory(front, width, height, format), default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The encoded image as a . + public KtxFile EncodeCubeMapToKtx(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front) + { + return EncodeCubeMapToKtxInternal(right, left, top, down, back, front, default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The width of the faces. + /// The height of the faces. + /// The pixel format the input data is in. + /// The encoded image as a . + public DdsFile EncodeCubeMapToDds(ReadOnlySpan right, ReadOnlySpan left, + ReadOnlySpan top, ReadOnlySpan down, + ReadOnlySpan back, ReadOnlySpan front, + int width, int height, PixelFormat format) + { + return EncodeCubeMapToDdsInternal( + ByteToColorMemory(right, width, height, format), + ByteToColorMemory(left, width, height, format), + ByteToColorMemory(top, width, height, format), + ByteToColorMemory(down, width, height, format), + ByteToColorMemory(back, width, height, format), + ByteToColorMemory(front, width, height, format), default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The encoded image as a . + public DdsFile EncodeCubeMapToDds(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front) + { + return EncodeCubeMapToDdsInternal(right, left, top, down, back, front, default); + } + + /// + /// Encodes a single 4x4 block to raw encoded bytes. Input Span length must be exactly 16. + /// + /// Input 4x4 color block + /// Raw encoded data + public byte[] EncodeBlock(ReadOnlySpan inputBlock) + { + if (inputBlock.Length != 16) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + return EncodeBlockInternal(inputBlock.AsSpan2D(4, 4)); + } + + /// + /// Encodes a single 4x4 block to raw encoded bytes. Input Span width and height must be exactly 4. + /// + /// Input 4x4 color block + /// Raw encoded data + public byte[] EncodeBlock(ReadOnlySpan2D inputBlock) + { + if (inputBlock.Width != 4 || inputBlock.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + return EncodeBlockInternal(inputBlock); + } + + /// + /// Encodes a single 4x4 block and writes the encoded block to a stream. Input Span length must be exactly 16. + /// + /// Input 4x4 color block + /// Output stream where the encoded block will be written to. + public void EncodeBlock(ReadOnlySpan inputBlock, Stream outputStream) + { + if (inputBlock.Length != 16) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + EncodeBlockInternal(inputBlock.AsSpan2D(4, 4), outputStream); + } + + /// + /// Encodes a single 4x4 block and writes the encoded block to a stream. Input Span width and height must be exactly 4. + /// + /// Input 4x4 color block + /// Output stream where the encoded block will be written to. + public void EncodeBlock(ReadOnlySpan2D inputBlock, Stream outputStream) + { + if (inputBlock.Width != 4 || inputBlock.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + EncodeBlockInternal(inputBlock, outputStream); + } + + /// + /// Gets the block size of the currently selected compression format in bytes. + /// + /// The size of a single 4x4 block in bytes + public int GetBlockSize() + { + var compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + var hdrEncoder = GetFloatBlockEncoder(OutputOptions.Format); + + if (hdrEncoder == null) + { + throw new NotSupportedException($"This format is either not supported or does not use block compression: {OutputOptions.Format}"); + } + + return hdrEncoder.GetBlockSize(); + } + return compressedEncoder.GetBlockSize(); + } + + /// + /// Gets the number of total blocks in an image with the given pixel width and height. + /// + /// The pixel width of the image + /// The pixel height of the image + /// The total number of blocks. + public int GetBlockCount(int pixelWidth, int pixelHeight) + { + return ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight); + } + + /// + /// Gets the number of blocks in an image with the given pixel width and height. + /// + /// The pixel width of the image + /// The pixel height of the image + /// The amount of blocks in the x-axis + /// The amount of blocks in the y-axis + public void GetBlockCount(int pixelWidth, int pixelHeight, out int blocksWidth, out int blocksHeight) + { + ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight, out blocksWidth, out blocksHeight); + } + + #endregion + #endregion + + #region HDR + + #region Async Api + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously. + /// + /// The input to encode represented by a . + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + public Task EncodeToStreamHdrAsync(ReadOnlyMemory2D input, Stream outputStream, CancellationToken token = default) + { + return Task.Run(() => + { + EncodeToStreamInternalHdr(input, outputStream, default); + }, token); + } + + /// + /// Encodes all mipmap levels into a Ktx file asynchronously. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Ktx file containing the encoded image. + public Task EncodeToKtxHdrAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToKtxInternalHdr(input, token), token); + } + + /// + /// Encodes all mipmap levels into a Dds file asynchronously. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Dds file containing the encoded image. + public Task EncodeToDdsHdrAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToDdsInternalHdr(input, token), token); + } + + /// + /// Encodes all mipmap levels of an HDR image into an array of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A list of raw encoded mipmap input. + /// To get the width and height of the encoded mip levels, see . + public Task EncodeToRawBytesHdrAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternalHdr(input, token), token); + } + + /// + /// Encodes a single mip level of the input HDR image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The mipmap to encode. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The raw encoded input. + /// To get the width and height of the encoded mip level, see . + public Task EncodeToRawBytesHdrAsync(ReadOnlyMemory2D input, int mipLevel, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternalHdr(input, mipLevel, out _, out _, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap HDR image to a stream asynchronously either in ktx or dds format. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A . + public Task EncodeCubeMapToStreamHdrAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToStreamInternalHdr(right, left, top, down, back, front, outputStream, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap HDR image to a asynchronously. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A of type . + public Task EncodeCubeMapToKtxHdrAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToKtxInternalHdr(right, left, top, down, back, front, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap HDR image to a asynchronously. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A of type . + public Task EncodeCubeMapToDdsHdrAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToDdsInternalHdr(right, left, top, down, back, front, token), token); + } + + #endregion + + #region Sync Api + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream. + /// + /// The input to encode represented by a . + /// The stream to write the encoded image to. + public void EncodeToStreamHdr(ReadOnlyMemory2D input, Stream outputStream) + { + EncodeToStreamInternalHdr(input, outputStream, default); + } + + /// + /// Encodes all mipmap levels into a Ktx file. + /// + /// The input to encode represented by a . + /// The containing the encoded image. + public KtxFile EncodeToKtxHdr(ReadOnlyMemory2D input) + { + return EncodeToKtxInternalHdr(input, default); + } + + /// + /// Encodes all mipmap levels into a Dds file. + /// + /// The input to encode represented by a . + /// The containing the encoded image. + public DdsFile EncodeToDdsHdr(ReadOnlyMemory2D input) + { + return EncodeToDdsInternalHdr(input, default); + } + + /// + /// Encodes all mipmap levels of a HDR image into a list of byte buffers. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// An array of byte buffers containing all mipmap levels. + /// To get the width and height of the encoded mip levels, see . + public byte[][] EncodeToRawBytesHdr(ReadOnlyMemory2D input) + { + return EncodeToRawInternalHdr(input, default); + } + + /// + /// Encodes a single mip level of the HDR input image to a byte buffer. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The mipmap to encode. + /// The width of the mipmap. + /// The height of the mipmap. + /// A byte buffer containing the encoded data of the requested mip-level. + public byte[] EncodeToRawBytesHdr(ReadOnlyMemory2D input, int mipLevel, out int mipWidth, out int mipHeight) + { + return EncodeToRawInternalHdr(input, mipLevel, out mipWidth, out mipHeight, default); + } + + /// + /// Encodes all mipMaps of a HDR cubeMap image to a stream either in ktx or dds format. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The stream to write the encoded image to. + public void EncodeCubeMapToStreamHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream) + { + EncodeCubeMapToStreamInternalHdr(right, left, top, down, back, front, outputStream, default); + } + + /// + /// Encodes all mipMaps of a HDR cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The encoded image as a . + public KtxFile EncodeCubeMapToKtxHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front) + { + return EncodeCubeMapToKtxInternalHdr(right, left, top, down, back, front, default); + } + + /// + /// Encodes all mipMaps of a HDR cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The encoded image as a . + public DdsFile EncodeCubeMapToDdsHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front) + { + return EncodeCubeMapToDdsInternalHdr(right, left, top, down, back, front, default); + } + + /// + /// Encodes a single 4x4 HDR block to raw encoded bytes. Input Span length must be exactly 16. + /// + /// Input 4x4 color block + /// Raw encoded data + public byte[] EncodeBlockHdr(ReadOnlySpan inputBlock) + { + if (inputBlock.Length != 16) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + return EncodeBlockInternalHdr(inputBlock.AsSpan2D(4, 4)); + } + + /// + /// Encodes a single 4x4 HDR block to raw encoded bytes. Input Span width and height must be exactly 4. + /// + /// Input 4x4 color block + /// Raw encoded data + public byte[] EncodeBlockHdr(ReadOnlySpan2D inputBlock) + { + if (inputBlock.Width != 4 || inputBlock.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + return EncodeBlockInternalHdr(inputBlock); + } + + /// + /// Encodes a single 4x4 HDR block and writes the encoded block to a stream. Input Span length must be exactly 16. + /// + /// Input 4x4 color block + /// Output stream where the encoded block will be written to. + public void EncodeBlockHdr(ReadOnlySpan inputBlock, Stream outputStream) + { + if (inputBlock.Length != 16) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + EncodeBlockInternalHdr(inputBlock.AsSpan2D(4, 4), outputStream); + } + + /// + /// Encodes a single 4x4 HDR block and writes the encoded block to a stream. Input Span width and height must be exactly 4. + /// + /// Input 4x4 color block + /// Output stream where the encoded block will be written to. + public void EncodeBlockHdr(ReadOnlySpan2D inputBlock, Stream outputStream) + { + if (inputBlock.Width != 4 || inputBlock.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + EncodeBlockInternalHdr(inputBlock, outputStream); + } + + #endregion + + #endregion + #region MipMap operations + + /// + /// Calculates the number of mipmap levels that will be generated for the given input image. + /// + /// The width of the input image in pixels + /// The height of the input image in pixels + /// The number of mipmap levels that will be generated for the input image. + public int CalculateNumberOfMipLevels(int imagePixelWidth, int imagePixelHeight) + { + return MipMapper.CalculateMipChainLength(imagePixelWidth, imagePixelHeight, + OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1); + } + + /// + /// Calculates the size of a given mipmap level. + /// + /// The width of the input image in pixels + /// The height of the input image in pixels + /// The mipLevel to calculate (0 is original image) + /// The mipmap width calculated + /// The mipmap height calculated + public void CalculateMipMapSize(int imagePixelWidth, int imagePixelHeight, int mipLevel, out int mipWidth, out int mipHeight) + { + MipMapper.CalculateMipLevelSize(imagePixelWidth, imagePixelHeight, mipLevel, out mipWidth, + out mipHeight); + } + + #endregion + + #region Private + + #region HDR + + private void EncodeToStreamInternalHdr(ReadOnlyMemory2D input, Stream outputStream, CancellationToken token) + { + switch (OutputOptions.FileFormat) + { + case OutputFileFormat.Dds: + var dds = EncodeToDdsInternalHdr(input, token); + dds.Write(outputStream); + break; + + case OutputFileFormat.Ktx: + var ktx = EncodeToKtxInternalHdr(input, token); + ktx.Write(outputStream); + break; + } + } + + private KtxFile EncodeToKtxInternalHdr(ReadOnlyMemory2D input, CancellationToken token) + { + KtxFile output; + IBcBlockEncoder compressedEncoder = null; + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoders + if (!OutputOptions.Format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not supported for hdr images: {OutputOptions.Format}"); + } + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + output = new KtxFile( + KtxHeader.InitializeCompressed(input.Width, input.Height, + compressedEncoder.GetInternalFormat(), + compressedEncoder.GetBaseInternalFormat())); + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, + out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, + context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); + + + output.MipMaps.Add(new KtxMipmap((uint)encoded.Length, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height, 1)); + output.MipMaps[mip].Faces[0] = new KtxMipFace(encoded, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); + } + + output.header.NumberOfFaces = 1; + output.header.NumberOfMipmapLevels = (uint)numMipMaps; + + return output; + } + + private DdsFile EncodeToDdsInternalHdr(ReadOnlyMemory2D input, CancellationToken token) + { + DdsFile output; + IBcBlockEncoder compressedEncoder = null; + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoder + if (!OutputOptions.Format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not supported for hdr images: {OutputOptions.Format}"); + } + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(input.Width, input.Height, + compressedEncoder.GetDxgiFormat(), OutputOptions.DdsPreferDxt10Header); + output = new DdsFile(ddsHeader, dxt10Header); + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); + + + if (mip == 0) + { + output.Faces.Add(new DdsFace((uint)input.Width, (uint)input.Height, + (uint)encoded.Length, numMipMaps)); + } + + output.Faces[0].MipMaps[mip] = new DdsMipMap(encoded, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); + } + + + output.header.dwMipMapCount = (uint)numMipMaps; + if (numMipMaps > 1) + { + output.header.dwCaps |= HeaderCaps.DdscapsComplex | HeaderCaps.DdscapsMipmap; + } + + return output; + } + + private byte[][] EncodeToRawInternalHdr(ReadOnlyMemory2D input, CancellationToken token) + { + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + var output = new byte[numMipMaps][]; + IBcBlockEncoder compressedEncoder = null; + + // Setup encoder + + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode all mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); + + output[mip] = encoded; + } + + return output; + } + + private byte[] EncodeToRawInternalHdr(ReadOnlyMemory2D input, int mipLevel, out int mipWidth, out int mipHeight, CancellationToken token) + { + mipLevel = Math.Max(0, mipLevel); + + IBcBlockEncoder compressedEncoder = null; + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoder + + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + // Dispose all mipmap levels + if (mipLevel > numMipMaps - 1) + { + throw new ArgumentException($"{nameof(mipLevel)} cannot be more than number of mipmaps."); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode mipmap level + byte[] encoded; + + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mipLevel], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + mipWidth = mipChain[mipLevel].Width; + mipHeight = mipChain[mipLevel].Height; + + return encoded; + } + + private void EncodeCubeMapToStreamInternalHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream, CancellationToken token) + { + switch (OutputOptions.FileFormat) + { + case OutputFileFormat.Ktx: + var ktx = EncodeCubeMapToKtxInternalHdr(right, left, top, down, back, front, token); + ktx.Write(outputStream); + break; + + case OutputFileFormat.Dds: + var dds = EncodeCubeMapToDdsInternalHdr(right, left, top, down, back, front, token); + dds.Write(outputStream); + break; + } + } + + private KtxFile EncodeCubeMapToKtxInternalHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token) + { + KtxFile output; + IBcBlockEncoder compressedEncoder = null; + + var faces = new[] { right, left, top, down, back, front }; + + var width = right.Width; + var height = right.Height; + + // Setup encoder + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + output = new KtxFile( + KtxHeader.InitializeCompressed(width, height, + compressedEncoder.GetInternalFormat(), + compressedEncoder.GetBaseInternalFormat())); + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipLength = MipMapper.CalculateMipChainLength(width, height, numMipMaps); + for (uint i = 0; i < mipLength; i++) + { + output.MipMaps.Add(new KtxMipmap(0, 0, 0, (uint)faces.Length)); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = 0; + foreach (var face in faces) + { + for (var mip = 0; mip < numMipMaps; mip++) + { + MipMapper.CalculateMipLevelSize(width, height, mip, out var mipWidth, out var mipHeight); + totalBlocks += ImageToBlocks.CalculateNumOfBlocks(mipWidth, mipHeight); + } + } + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode all faces + var processedBlocks = 0; + for (var face = 0; face < faces.Length; face++) + { + var mipChain = MipMapper.GenerateMipChain(faces[face], ref numMipMaps); + + // Encode all mipmap levels per face + for (var mipLevel = 0; mipLevel < numMipMaps; mipLevel++) + { + byte[] encoded; + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mipLevel], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + processedBlocks += blocks.Length; + context.Progress.SetProcessedBlocks(processedBlocks); + + if (face == 0) + { + output.MipMaps[mipLevel] = new KtxMipmap((uint)encoded.Length, + (uint)mipChain[mipLevel].Width, + (uint)mipChain[mipLevel].Height, (uint)faces.Length); + } + + output.MipMaps[mipLevel].Faces[face] = new KtxMipFace(encoded, + (uint)mipChain[mipLevel].Width, + (uint)mipChain[mipLevel].Height); + } + } + + output.header.NumberOfFaces = (uint)faces.Length; + output.header.NumberOfMipmapLevels = (uint)mipLength; + + return output; + } + + private DdsFile EncodeCubeMapToDdsInternalHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token) + { + DdsFile output; + IBcBlockEncoder compressedEncoder = null; + + var faces = new[] { right, left, top, down, back, front }; + + var width = right.Width; + var height = right.Height; + + // Setup encoder + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(width, height, + compressedEncoder.GetDxgiFormat(), OutputOptions.DdsPreferDxt10Header); + output = new DdsFile(ddsHeader, dxt10Header); + + if (OutputOptions.DdsBc1WriteAlphaFlag && + OutputOptions.Format == CompressionFormat.Bc1WithAlpha) + { + output.header.ddsPixelFormat.dwFlags |= PixelFormatFlags.DdpfAlphaPixels; + } + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = 0; + foreach (var face in faces) + { + for (var mip = 0; mip < numMipMaps; mip++) + { + MipMapper.CalculateMipLevelSize(width, height, mip, out var mipWidth, out var mipHeight); + totalBlocks += ImageToBlocks.CalculateNumOfBlocks(mipWidth, mipHeight); + } + } + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // EncodeBlock all faces + var processedBlocks = 0; + for (var face = 0; face < faces.Length; face++) + { + var mipChain = MipMapper.GenerateMipChain(faces[face], ref numMipMaps); + + // Encode all mipmap levels per face + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + processedBlocks += blocks.Length; + context.Progress.SetProcessedBlocks(processedBlocks); + + if (mip == 0) + { + output.Faces.Add(new DdsFace((uint)mipChain[mip].Width, (uint)mipChain[mip].Height, + (uint)encoded.Length, mipChain.Length)); + } + + output.Faces[face].MipMaps[mip] = new DdsMipMap(encoded, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); + } + } - public class EncoderOptions { - /// - /// Whether the blocks should be encoded in parallel. This can be much faster than single-threaded encoding, - /// but is slow if multiple textures are being processed at the same time. - /// When a debugger is attached, the encoder defaults to single-threaded operation to ease debugging. - /// Default is true. - /// - public bool multiThreaded = true; - } + output.header.dwCaps |= HeaderCaps.DdscapsComplex; + output.header.dwMipMapCount = (uint)numMipMaps; + if (numMipMaps > 1) + { + output.header.dwCaps |= HeaderCaps.DdscapsMipmap; + } - /// - /// Handles all encoding of images into compressed or uncompressed formats. For decoding, - /// - public class BcEncoder - { - public EncoderInputOptions InputOptions { get; set; } = new EncoderInputOptions(); - public EncoderOutputOptions OutputOptions { get; set; } = new EncoderOutputOptions(); - public EncoderOptions Options { get; set; } = new EncoderOptions(); + output.header.dwCaps2 |= HeaderCaps2.Ddscaps2Cubemap | + HeaderCaps2.Ddscaps2CubemapPositivex | + HeaderCaps2.Ddscaps2CubemapNegativex | + HeaderCaps2.Ddscaps2CubemapPositivey | + HeaderCaps2.Ddscaps2CubemapNegativey | + HeaderCaps2.Ddscaps2CubemapPositivez | + HeaderCaps2.Ddscaps2CubemapNegativez; - public BcEncoder() { } - public BcEncoder(CompressionFormat format) - { - OutputOptions.format = format; + return output; } - private IBcBlockEncoder GetEncoder(CompressionFormat format) + private byte[] EncodeBlockInternalHdr(ReadOnlySpan2D input) { - switch (format) + var compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) { - case CompressionFormat.BC1: - return new Bc1BlockEncoder(); - case CompressionFormat.BC1WithAlpha: - return new Bc1AlphaBlockEncoder(); - case CompressionFormat.BC2: - return new Bc2BlockEncoder(); - case CompressionFormat.BC3: - return new Bc3BlockEncoder(); - case CompressionFormat.BC4: - return new Bc4BlockEncoder(InputOptions.luminanceAsRed); - case CompressionFormat.BC5: - return new Bc5BlockEncoder(); - case CompressionFormat.BC7: - return new Bc7Encoder(); - default: - return null; + throw new NotSupportedException($"This Format is not supported for single block encoding: {OutputOptions.Format}"); } + + var output = new byte[compressedEncoder.GetBlockSize()]; + + var rawBlock = new RawBlock4X4RgbFloat(); + + var pixels = rawBlock.AsSpan; + + input.GetRowSpan(0).CopyTo(pixels); + input.GetRowSpan(1).CopyTo(pixels.Slice(4)); + input.GetRowSpan(2).CopyTo(pixels.Slice(8)); + input.GetRowSpan(3).CopyTo(pixels.Slice(12)); + + compressedEncoder.EncodeBlock(rawBlock, OutputOptions.Quality, output); + + return output; } - private IRawEncoder GetRawEncoder(CompressionFormat format) + private void EncodeBlockInternalHdr(ReadOnlySpan2D input, Stream outputStream) { - switch (format) + var compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) { - case CompressionFormat.R: - return new RawLuminanceEncoder(InputOptions.luminanceAsRed); - case CompressionFormat.RG: - return new RawRGEncoder(); - case CompressionFormat.RGB: - return new RawRGBEncoder(); - case CompressionFormat.RGBA: - return new RawRGBAEncoder(); - default: - throw new ArgumentOutOfRangeException(nameof(format), format, null); + throw new NotSupportedException($"This Format is not supported for single block encoding: {OutputOptions.Format}"); + } + if (input.Width != 4 || input.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); } + + Span output = stackalloc byte[16]; + output = output.Slice(0, compressedEncoder.GetBlockSize()); + + var rawBlock = new RawBlock4X4RgbFloat(); + + var pixels = rawBlock.AsSpan; + + input.GetRowSpan(0).CopyTo(pixels); + input.GetRowSpan(1).CopyTo(pixels.Slice(4)); + input.GetRowSpan(2).CopyTo(pixels.Slice(8)); + input.GetRowSpan(3).CopyTo(pixels.Slice(12)); + + compressedEncoder.EncodeBlock(rawBlock, OutputOptions.Quality, output); + + outputStream.Write(output); } - /// - /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream. - /// - public void Encode(Image inputImage, Stream outputStream) + #endregion + + #region LDR + private void EncodeToStreamInternal(ReadOnlyMemory2D input, Stream outputStream, CancellationToken token) { - if (OutputOptions.fileFormat == OutputFileFormat.Ktx) - { - KtxFile output = EncodeToKtx(inputImage); - output.Write(outputStream); - } - else if (OutputOptions.fileFormat == OutputFileFormat.Dds) + switch (OutputOptions.FileFormat) { - DdsFile output = EncodeToDds(inputImage); - output.Write(outputStream); + case OutputFileFormat.Dds: + var dds = EncodeToDdsInternal(input, token); + dds.Write(outputStream); + break; + + case OutputFileFormat.Ktx: + var ktx = EncodeToKtxInternal(input, token); + ktx.Write(outputStream); + break; } } - /// - /// Encodes all mipmap levels into a Ktx file. - /// - public KtxFile EncodeToKtx(Image inputImage) + private KtxFile EncodeToKtxInternal(ReadOnlyMemory2D input, CancellationToken token) { KtxFile output; - IBcBlockEncoder compressedEncoder = null; + IBcBlockEncoder compressedEncoder = null; IRawEncoder uncompressedEncoder = null; - if (OutputOptions.format.IsCompressedFormat()) + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoders + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + if (isCompressedFormat) { - compressedEncoder = GetEncoder(OutputOptions.format); + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); if (compressedEncoder == null) { - throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); } + output = new KtxFile( - KtxHeader.InitializeCompressed(inputImage.Width, inputImage.Height, + KtxHeader.InitializeCompressed(input.Width, input.Height, compressedEncoder.GetInternalFormat(), compressedEncoder.GetBaseInternalFormat())); } else { - uncompressedEncoder = GetRawEncoder(OutputOptions.format); + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); output = new KtxFile( - KtxHeader.InitializeUncompressed(inputImage.Width, inputImage.Height, + KtxHeader.InitializeUncompressed(input.Width, input.Height, uncompressedEncoder.GetGlType(), uncompressedEncoder.GetGlFormat(), uncompressedEncoder.GetGlTypeSize(), uncompressedEncoder.GetInternalFormat(), uncompressedEncoder.GetBaseInternalFormat())); - } - uint numMipMaps = (uint)OutputOptions.maxMipMapLevel; - if (!OutputOptions.generateMipMaps) + var context = new OperationContext { - numMipMaps = 1; - } + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; - var mipChain = MipMapper.GenerateMipChain(inputImage, ref numMipMaps); + // Calculate total blocks + var totalBlocks = isCompressedFormat ? mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)) : mipChain.Sum(m => m.Width * m.Height); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); - for (int i = 0; i < numMipMaps; i++) + // Encode mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) { - byte[] encoded = null; - if (OutputOptions.format.IsCompressedFormat()) + byte[] encoded; + if (isCompressedFormat) { - var blocks = ImageToBlocks.ImageTo4X4(mipChain[i].Frames[0], out int blocksWidth, out int blocksHeight); - encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, - !Debugger.IsAttached && Options.multiThreaded); + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, + out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, + context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); } else { - encoded = uncompressedEncoder.Encode(mipChain[i].GetPixelSpan()); + if (!mipChain[mip].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => x.Width * x.Height)); } output.MipMaps.Add(new KtxMipmap((uint)encoded.Length, - (uint)inputImage.Width, - (uint)inputImage.Height, 1)); - output.MipMaps[i].Faces[0] = new KtxMipFace(encoded, - (uint)inputImage.Width, - (uint)inputImage.Height); - } - - foreach (var image in mipChain) - { - image.Dispose(); + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height, 1)); + output.MipMaps[mip].Faces[0] = new KtxMipFace(encoded, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); } - output.Header.NumberOfFaces = 1; - output.Header.NumberOfMipmapLevels = numMipMaps; + output.header.NumberOfFaces = 1; + output.header.NumberOfMipmapLevels = (uint)numMipMaps; return output; } - /// - /// Encodes all mipmap levels into a Ktx file. - /// - public DdsFile EncodeToDds(Image inputImage) + private DdsFile EncodeToDdsInternal(ReadOnlyMemory2D input, CancellationToken token) { DdsFile output; - IBcBlockEncoder compressedEncoder = null; + IBcBlockEncoder compressedEncoder = null; IRawEncoder uncompressedEncoder = null; - if (OutputOptions.format.IsCompressedFormat()) + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoder + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + if (isCompressedFormat) { - compressedEncoder = GetEncoder(OutputOptions.format); + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); if (compressedEncoder == null) { - throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); } - var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(inputImage.Width, inputImage.Height, - compressedEncoder.GetDxgiFormat()); + var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(input.Width, input.Height, + compressedEncoder.GetDxgiFormat(), OutputOptions.DdsPreferDxt10Header); output = new DdsFile(ddsHeader, dxt10Header); - if (OutputOptions.ddsBc1WriteAlphaFlag && - OutputOptions.format == CompressionFormat.BC1WithAlpha) + if (OutputOptions.DdsBc1WriteAlphaFlag && + OutputOptions.Format == CompressionFormat.Bc1WithAlpha) { - output.Header.ddsPixelFormat.dwFlags |= PixelFormatFlags.DDPF_ALPHAPIXELS; + output.header.ddsPixelFormat.dwFlags |= PixelFormatFlags.DdpfAlphaPixels; } } else { - uncompressedEncoder = GetRawEncoder(OutputOptions.format); - var ddsHeader = DdsHeader.InitializeUncompressed(inputImage.Width, inputImage.Height, + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); + var ddsHeader = DdsHeader.InitializeUncompressed(input.Width, input.Height, uncompressedEncoder.GetDxgiFormat()); output = new DdsFile(ddsHeader); } - uint numMipMaps = (uint)OutputOptions.maxMipMapLevel; - if (!OutputOptions.generateMipMaps) + var context = new OperationContext { - numMipMaps = 1; - } + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; - var mipChain = MipMapper.GenerateMipChain(inputImage, ref numMipMaps); + // Calculate total blocks + var totalBlocks = isCompressedFormat ? mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)) : mipChain.Sum(m => m.Width * m.Height); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); - for (int mip = 0; mip < numMipMaps; mip++) + // Encode mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) { - byte[] encoded = null; - if (OutputOptions.format.IsCompressedFormat()) + byte[] encoded; + if (isCompressedFormat) { - var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip].Frames[0], out int blocksWidth, out int blocksHeight); - encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, - !Debugger.IsAttached && Options.multiThreaded); + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); } else { - encoded = uncompressedEncoder.Encode(mipChain[mip].GetPixelSpan()); + if (!mipChain[mip].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => x.Width * x.Height)); } if (mip == 0) { - output.Faces.Add(new DdsFace((uint)inputImage.Width, (uint)inputImage.Height, - (uint)encoded.Length, (int)numMipMaps)); + output.Faces.Add(new DdsFace((uint)input.Width, (uint)input.Height, + (uint)encoded.Length, numMipMaps)); } output.Faces[0].MipMaps[mip] = new DdsMipMap(encoded, - (uint)inputImage.Width, - (uint)inputImage.Height); + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); } - foreach (var image in mipChain) - { - image.Dispose(); - } - output.Header.dwMipMapCount = numMipMaps; + output.header.dwMipMapCount = (uint)numMipMaps; if (numMipMaps > 1) { - output.Header.dwCaps |= HeaderCaps.DDSCAPS_COMPLEX | HeaderCaps.DDSCAPS_MIPMAP; + output.header.dwCaps |= HeaderCaps.DdscapsComplex | HeaderCaps.DdscapsMipmap; } return output; } - /// - /// Encodes all mipmap levels into a list of byte buffers. - /// - public List EncodeToRawBytes(Image inputImage) + private byte[][] EncodeToRawInternal(ReadOnlyMemory2D input, CancellationToken token) { - List output = new List(); - IBcBlockEncoder compressedEncoder = null; + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + var output = new byte[numMipMaps][]; + IBcBlockEncoder compressedEncoder = null; IRawEncoder uncompressedEncoder = null; - if (OutputOptions.format.IsCompressedFormat()) + + // Setup encoder + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + + if (isCompressedFormat) { - compressedEncoder = GetEncoder(OutputOptions.format); + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); if (compressedEncoder == null) { - throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); } - } else { - uncompressedEncoder = GetRawEncoder(OutputOptions.format); - + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); } - uint numMipMaps = (uint)OutputOptions.maxMipMapLevel; - if (!OutputOptions.generateMipMaps) + var context = new OperationContext { - numMipMaps = 1; - } + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; - var mipChain = MipMapper.GenerateMipChain(inputImage, ref numMipMaps); + // Calculate total blocks + var totalBlocks = isCompressedFormat ? mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)) : mipChain.Sum(m => m.Width * m.Height); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); - for (int i = 0; i < numMipMaps; i++) + // Encode all mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) { - byte[] encoded = null; - if (OutputOptions.format.IsCompressedFormat()) + byte[] encoded; + if (isCompressedFormat) { - var blocks = ImageToBlocks.ImageTo4X4(mipChain[i].Frames[0], out int blocksWidth, out int blocksHeight); - encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, - !Debugger.IsAttached && Options.multiThreaded); + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); } else { - encoded = uncompressedEncoder.Encode(mipChain[i].GetPixelSpan()); - } + if (!mipChain[mip].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } - output.Add(encoded); - } + encoded = uncompressedEncoder.Encode(mipMemory); - foreach (var image in mipChain) - { - image.Dispose(); + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => x.Width * x.Height)); + } + + output[mip] = encoded; } return output; } - /// - /// Encodes a single mip level of the input image to a byte buffer. - /// - public byte[] EncodeToRawBytes(Image inputImage, int mipLevel, out int mipWidth, out int mipHeight) + private byte[] EncodeToRawInternal(ReadOnlyMemory2D input, int mipLevel, out int mipWidth, out int mipHeight, CancellationToken token) { - if (mipLevel < 0) - { - throw new ArgumentException($"{nameof(mipLevel)} cannot be less than zero."); - } + mipLevel = Math.Max(0, mipLevel); - IBcBlockEncoder compressedEncoder = null; + IBcBlockEncoder compressedEncoder = null; IRawEncoder uncompressedEncoder = null; - if (OutputOptions.format.IsCompressedFormat()) + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoder + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + if (isCompressedFormat) { - compressedEncoder = GetEncoder(OutputOptions.format); + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); if (compressedEncoder == null) { - throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); } } else { - uncompressedEncoder = GetRawEncoder(OutputOptions.format); - - } - - uint numMipMaps = (uint)OutputOptions.maxMipMapLevel; - if (!OutputOptions.generateMipMaps) - { - numMipMaps = 1; + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); } - var mipChain = MipMapper.GenerateMipChain(inputImage, ref numMipMaps); - + // Dispose all mipmap levels if (mipLevel > numMipMaps - 1) { - foreach (var image in mipChain) - { - image.Dispose(); - } - throw new ArgumentException($"{nameof(mipLevel)} cannot be more than number of mipmaps"); + throw new ArgumentException($"{nameof(mipLevel)} cannot be more than number of mipmaps."); } - byte[] encoded = null; - if (OutputOptions.format.IsCompressedFormat()) + var context = new OperationContext { - var blocks = ImageToBlocks.ImageTo4X4(mipChain[mipLevel].Frames[0], out int blocksWidth, out int blocksHeight); - encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, - !Debugger.IsAttached && Options.multiThreaded); + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = isCompressedFormat ? ImageToBlocks.CalculateNumOfBlocks(mipChain[mipLevel].Width, mipChain[mipLevel].Height) : mipChain[mipLevel].Width * mipChain[mipLevel].Height; + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode mipmap level + byte[] encoded; + if (isCompressedFormat) + { + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mipLevel], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); } else { - encoded = uncompressedEncoder.Encode(mipChain[mipLevel].GetPixelSpan()); + if (!mipChain[mipLevel].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); } mipWidth = mipChain[mipLevel].Width; mipHeight = mipChain[mipLevel].Height; - foreach (var image in mipChain) - { - image.Dispose(); - } - return encoded; } - /// - /// Encodes all cubemap faces and mipmap levels into Ktx file and writes it to the output stream. - /// Order is +X, -X, +Y, -Y, +Z, -Z - /// - public void EncodeCubeMap(Image right, Image left, Image top, Image down, - Image back, Image front, Stream outputStream) + private void EncodeCubeMapToStreamInternal(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream, CancellationToken token) { - if (OutputOptions.fileFormat == OutputFileFormat.Ktx) - { - KtxFile output = EncodeCubeMapToKtx(right, left, top, down, back, front); - output.Write(outputStream); - } - else if (OutputOptions.fileFormat == OutputFileFormat.Dds) + switch (OutputOptions.FileFormat) { - DdsFile output = EncodeCubeMapToDds(right, left, top, down, back, front); - output.Write(outputStream); + case OutputFileFormat.Ktx: + var ktx = EncodeCubeMapToKtxInternal(right, left, top, down, back, front, token); + ktx.Write(outputStream); + break; + + case OutputFileFormat.Dds: + var dds = EncodeCubeMapToDdsInternal(right, left, top, down, back, front, token); + dds.Write(outputStream); + break; } } - /// - /// Encodes all cubemap faces and mipmap levels into a Ktx file. - /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. - /// - public KtxFile EncodeCubeMapToKtx(Image right, Image left, Image top, Image down, - Image back, Image front) + private KtxFile EncodeCubeMapToKtxInternal(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token) { KtxFile output; - IBcBlockEncoder compressedEncoder = null; + IBcBlockEncoder compressedEncoder = null; IRawEncoder uncompressedEncoder = null; - if (right.Width != left.Width || right.Width != top.Width || right.Width != down.Width - || right.Width != back.Width || right.Width != front.Width || - right.Height != left.Height || right.Height != top.Height || right.Height != down.Height - || right.Height != back.Height || right.Height != front.Height) - { - throw new ArgumentException("All input images of a cubemap should be the same size."); - } + var faces = new[] { right, left, top, down, back, front }; - Image[] faces = new[] { right, left, top, down, back, front }; + var width = right.Width; + var height = right.Height; - if (OutputOptions.format.IsCompressedFormat()) + // Setup encoder + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + if (isCompressedFormat) { - compressedEncoder = GetEncoder(OutputOptions.format); + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); if (compressedEncoder == null) { - throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); } + output = new KtxFile( - KtxHeader.InitializeCompressed(right.Width, right.Height, + KtxHeader.InitializeCompressed(width, height, compressedEncoder.GetInternalFormat(), compressedEncoder.GetBaseInternalFormat())); } else { - uncompressedEncoder = GetRawEncoder(OutputOptions.format); + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); output = new KtxFile( - KtxHeader.InitializeUncompressed(right.Width, right.Height, + KtxHeader.InitializeUncompressed(width, height, uncompressedEncoder.GetGlType(), uncompressedEncoder.GetGlFormat(), uncompressedEncoder.GetGlTypeSize(), @@ -478,166 +1846,376 @@ public KtxFile EncodeCubeMapToKtx(Image right, Image left, Image uncompressedEncoder.GetBaseInternalFormat())); } - uint numMipMaps = (uint)OutputOptions.maxMipMapLevel; - if (!OutputOptions.generateMipMaps) - { - numMipMaps = 1; - } - uint mipLength = MipMapper.CalculateMipChainLength(right.Width, right.Height, numMipMaps); + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipLength = MipMapper.CalculateMipChainLength(width, height, numMipMaps); for (uint i = 0; i < mipLength; i++) { output.MipMaps.Add(new KtxMipmap(0, 0, 0, (uint)faces.Length)); } - for (int f = 0; f < faces.Length; f++) + var context = new OperationContext { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = 0; + foreach (var face in faces) + { + for (var mip = 0; mip < numMipMaps; mip++) + { + MipMapper.CalculateMipLevelSize(width, height, mip, out var mipWidth, out var mipHeight); + totalBlocks += isCompressedFormat ? ImageToBlocks.CalculateNumOfBlocks(mipWidth, mipHeight) : mipWidth * mipHeight; + } + } + context.Progress = new OperationProgress(Options.Progress, totalBlocks); - var mipChain = MipMapper.GenerateMipChain(faces[f], ref numMipMaps); + // Encode all faces + var processedBlocks = 0; + for (var face = 0; face < faces.Length; face++) + { + var mipChain = MipMapper.GenerateMipChain(faces[face], ref numMipMaps); - for (int i = 0; i < numMipMaps; i++) + // Encode all mipmap levels per face + for (var mipLevel = 0; mipLevel < numMipMaps; mipLevel++) { - byte[] encoded = null; - if (OutputOptions.format.IsCompressedFormat()) + byte[] encoded; + if (isCompressedFormat) { - var blocks = ImageToBlocks.ImageTo4X4(mipChain[i].Frames[0], out int blocksWidth, out int blocksHeight); - encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, - !Debugger.IsAttached && Options.multiThreaded); + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mipLevel], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + processedBlocks += blocks.Length; + context.Progress.SetProcessedBlocks(processedBlocks); } else { - encoded = uncompressedEncoder.Encode(mipChain[i].GetPixelSpan()); + if (!mipChain[mipLevel].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); + + processedBlocks += mipMemory.Length; + context.Progress.SetProcessedBlocks(processedBlocks); } - if (f == 0) + if (face == 0) { - output.MipMaps[i] = new KtxMipmap((uint)encoded.Length, - (uint)mipChain[i].Width, - (uint)mipChain[i].Height, (uint)faces.Length); + output.MipMaps[mipLevel] = new KtxMipmap((uint)encoded.Length, + (uint)mipChain[mipLevel].Width, + (uint)mipChain[mipLevel].Height, (uint)faces.Length); } - output.MipMaps[i].Faces[f] = new KtxMipFace(encoded, - (uint)mipChain[i].Width, - (uint)mipChain[i].Height); - } - - foreach (var image in mipChain) - { - image.Dispose(); + output.MipMaps[mipLevel].Faces[face] = new KtxMipFace(encoded, + (uint)mipChain[mipLevel].Width, + (uint)mipChain[mipLevel].Height); } } - output.Header.NumberOfFaces = (uint)faces.Length; - output.Header.NumberOfMipmapLevels = mipLength; + output.header.NumberOfFaces = (uint)faces.Length; + output.header.NumberOfMipmapLevels = (uint)mipLength; return output; } - /// - /// Encodes all cubemap faces and mipmap levels into a Dds file. - /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. - /// - public DdsFile EncodeCubeMapToDds(Image right, Image left, Image top, Image down, - Image back, Image front) + private DdsFile EncodeCubeMapToDdsInternal(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token) { DdsFile output; - IBcBlockEncoder compressedEncoder = null; + IBcBlockEncoder compressedEncoder = null; IRawEncoder uncompressedEncoder = null; - if (right.Width != left.Width || right.Width != top.Width || right.Width != down.Width - || right.Width != back.Width || right.Width != front.Width || - right.Height != left.Height || right.Height != top.Height || right.Height != down.Height - || right.Height != back.Height || right.Height != front.Height) - { - throw new ArgumentException("All input images of a cubemap should be the same size."); - } + var faces = new[] { right, left, top, down, back, front }; - Image[] faces = new[] { right, left, top, down, back, front }; + var width = right.Width; + var height = right.Height; - if (OutputOptions.format.IsCompressedFormat()) + // Setup encoder + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + if (isCompressedFormat) { - compressedEncoder = GetEncoder(OutputOptions.format); + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); if (compressedEncoder == null) { - throw new NotSupportedException($"This format is not supported: {OutputOptions.format}"); + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); } - var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(right.Width, right.Height, - compressedEncoder.GetDxgiFormat()); + var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(width, height, + compressedEncoder.GetDxgiFormat(), OutputOptions.DdsPreferDxt10Header); output = new DdsFile(ddsHeader, dxt10Header); - if (OutputOptions.ddsBc1WriteAlphaFlag && - OutputOptions.format == CompressionFormat.BC1WithAlpha) + if (OutputOptions.DdsBc1WriteAlphaFlag && + OutputOptions.Format == CompressionFormat.Bc1WithAlpha) { - output.Header.ddsPixelFormat.dwFlags |= PixelFormatFlags.DDPF_ALPHAPIXELS; + output.header.ddsPixelFormat.dwFlags |= PixelFormatFlags.DdpfAlphaPixels; } } else { - uncompressedEncoder = GetRawEncoder(OutputOptions.format); - var ddsHeader = DdsHeader.InitializeUncompressed(right.Width, right.Height, + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); + var ddsHeader = DdsHeader.InitializeUncompressed(width, height, uncompressedEncoder.GetDxgiFormat()); + output = new DdsFile(ddsHeader); } - uint numMipMaps = (uint)OutputOptions.maxMipMapLevel; - if (!OutputOptions.generateMipMaps) + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = 0; + foreach (var face in faces) { - numMipMaps = 1; + for (var mip = 0; mip < numMipMaps; mip++) + { + MipMapper.CalculateMipLevelSize(width, height, mip, out var mipWidth, out var mipHeight); + totalBlocks += isCompressedFormat ? ImageToBlocks.CalculateNumOfBlocks(mipWidth, mipHeight) : mipWidth * mipHeight; + } } + context.Progress = new OperationProgress(Options.Progress, totalBlocks); - for (int f = 0; f < faces.Length; f++) + // EncodeBlock all faces + var processedBlocks = 0; + for (var face = 0; face < faces.Length; face++) { + var mipChain = MipMapper.GenerateMipChain(faces[face], ref numMipMaps); - var mipChain = MipMapper.GenerateMipChain(faces[f], ref numMipMaps); - - - for (int mip = 0; mip < numMipMaps; mip++) + // Encode all mipmap levels per face + for (var mip = 0; mip < numMipMaps; mip++) { - byte[] encoded = null; - if (OutputOptions.format.IsCompressedFormat()) + byte[] encoded; + if (isCompressedFormat) { - var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip].Frames[0], out int blocksWidth, out int blocksHeight); - encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.quality, - !Debugger.IsAttached && Options.multiThreaded); + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + processedBlocks += blocks.Length; + context.Progress.SetProcessedBlocks(processedBlocks); } else { - encoded = uncompressedEncoder.Encode(mipChain[mip].GetPixelSpan()); + if (!mipChain[mip].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); + + processedBlocks += mipMemory.Length; + context.Progress.SetProcessedBlocks(processedBlocks); } if (mip == 0) { output.Faces.Add(new DdsFace((uint)mipChain[mip].Width, (uint)mipChain[mip].Height, - (uint)encoded.Length, mipChain.Count)); + (uint)encoded.Length, mipChain.Length)); } - output.Faces[f].MipMaps[mip] = new DdsMipMap(encoded, + output.Faces[face].MipMaps[mip] = new DdsMipMap(encoded, (uint)mipChain[mip].Width, (uint)mipChain[mip].Height); } - - foreach (var image in mipChain) - { - image.Dispose(); - } } - output.Header.dwCaps |= HeaderCaps.DDSCAPS_COMPLEX; - output.Header.dwMipMapCount = numMipMaps; + output.header.dwCaps |= HeaderCaps.DdscapsComplex; + output.header.dwMipMapCount = (uint)numMipMaps; if (numMipMaps > 1) { - output.Header.dwCaps |= HeaderCaps.DDSCAPS_MIPMAP; + output.header.dwCaps |= HeaderCaps.DdscapsMipmap; + } + + output.header.dwCaps2 |= HeaderCaps2.Ddscaps2Cubemap | + HeaderCaps2.Ddscaps2CubemapPositivex | + HeaderCaps2.Ddscaps2CubemapNegativex | + HeaderCaps2.Ddscaps2CubemapPositivey | + HeaderCaps2.Ddscaps2CubemapNegativey | + HeaderCaps2.Ddscaps2CubemapPositivez | + HeaderCaps2.Ddscaps2CubemapNegativez; + + return output; + } + + private byte[] EncodeBlockInternal(ReadOnlySpan2D input) + { + var compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported for single block encoding: {OutputOptions.Format}"); } - output.Header.dwCaps2 |= HeaderCaps2.DDSCAPS2_CUBEMAP | - HeaderCaps2.DDSCAPS2_CUBEMAP_POSITIVEX | - HeaderCaps2.DDSCAPS2_CUBEMAP_NEGATIVEX | - HeaderCaps2.DDSCAPS2_CUBEMAP_POSITIVEY | - HeaderCaps2.DDSCAPS2_CUBEMAP_NEGATIVEY | - HeaderCaps2.DDSCAPS2_CUBEMAP_POSITIVEZ | - HeaderCaps2.DDSCAPS2_CUBEMAP_NEGATIVEZ; + + var output = new byte[compressedEncoder.GetBlockSize()]; + + var rawBlock = new RawBlock4X4Rgba32(); + + var pixels = rawBlock.AsSpan; + + input.GetRowSpan(0).CopyTo(pixels); + input.GetRowSpan(1).CopyTo(pixels.Slice(4)); + input.GetRowSpan(2).CopyTo(pixels.Slice(8)); + input.GetRowSpan(3).CopyTo(pixels.Slice(12)); + + compressedEncoder.EncodeBlock(rawBlock, OutputOptions.Quality, output); return output; } + + private void EncodeBlockInternal(ReadOnlySpan2D input, Stream outputStream) + { + var compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported for single block encoding: {OutputOptions.Format}"); + } + if (input.Width != 4 || input.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + + Span output = stackalloc byte[16]; + output = output.Slice(0, compressedEncoder.GetBlockSize()); + + var rawBlock = new RawBlock4X4Rgba32(); + + var pixels = rawBlock.AsSpan; + + input.GetRowSpan(0).CopyTo(pixels); + input.GetRowSpan(1).CopyTo(pixels.Slice(4)); + input.GetRowSpan(2).CopyTo(pixels.Slice(8)); + input.GetRowSpan(3).CopyTo(pixels.Slice(12)); + + compressedEncoder.EncodeBlock(rawBlock, OutputOptions.Quality, output); + + outputStream.Write(output); + } + + #endregion + + #endregion + + #region Support + + private IBcBlockEncoder GetRgba32BlockEncoder(CompressionFormat format) + { + switch (format) + { + case CompressionFormat.Bc1: + return new Bc1BlockEncoder(); + + case CompressionFormat.Bc1WithAlpha: + return new Bc1AlphaBlockEncoder(); + + case CompressionFormat.Bc2: + return new Bc2BlockEncoder(); + + case CompressionFormat.Bc3: + return new Bc3BlockEncoder(); + + case CompressionFormat.Bc4: + return new Bc4BlockEncoder(InputOptions.Bc4Component); + + case CompressionFormat.Bc5: + return new Bc5BlockEncoder(InputOptions.Bc5Component1, InputOptions.Bc5Component2); + + case CompressionFormat.Bc7: + return new Bc7Encoder(); + + case CompressionFormat.Atc: + return new AtcBlockEncoder(); + + case CompressionFormat.AtcExplicitAlpha: + return new AtcExplicitAlphaBlockEncoder(); + + case CompressionFormat.AtcInterpolatedAlpha: + return new AtcInterpolatedAlphaBlockEncoder(); + + default: + return null; + } + } + + private IBcBlockEncoder GetFloatBlockEncoder(CompressionFormat format) + { + switch (format) + { + case CompressionFormat.Bc6S: + return new Bc6Encoder(true); + case CompressionFormat.Bc6U: + return new Bc6Encoder(false); + default: + return null; + } + } + + private IRawEncoder GetRawEncoder(CompressionFormat format) + { + switch (format) + { + case CompressionFormat.R: + return new RawLuminanceEncoder(InputOptions.LuminanceAsRed); + + case CompressionFormat.Rg: + return new RawRgEncoder(); + + case CompressionFormat.Rgb: + return new RawRgbEncoder(); + + case CompressionFormat.Rgba: + return new RawRgbaEncoder(); + + case CompressionFormat.Bgra: + return new RawBgraEncoder(); + + default: + throw new ArgumentOutOfRangeException(nameof(format), format, null); + } + } + + private ReadOnlyMemory2D ByteToColorMemory(ReadOnlySpan span, int width, int height, PixelFormat format) + { + var pixels = new ColorRgba32[width * height]; + + switch (format) + { + case PixelFormat.Rgba32: + for (var i = 0; i < width * height * 4; i += 4) + pixels[i / 4] = new ColorRgba32(span[i], span[i + 1], span[i + 2], span[i + 3]); + break; + + case PixelFormat.Rgb24: + for (var i = 0; i < width * height * 3; i += 3) + pixels[i / 3] = new ColorRgba32(span[i], span[i + 1], span[i + 2], 255); + break; + + case PixelFormat.Bgra32: + for (var i = 0; i < width * height * 4; i += 4) + pixels[i / 4] = new ColorRgba32(span[i + 2], span[i + 1], span[i], span[i + 3]); + break; + + case PixelFormat.Bgr24: + for (var i = 0; i < width * height * 3; i += 3) + pixels[i / 3] = new ColorRgba32(span[i + 2], span[i + 1], span[i], 255); + break; + + case PixelFormat.Argb32: + for (var i = 0; i < width * height * 4; i += 4) + pixels[i / 4] = new ColorRgba32(span[i + 1], span[i + 2], span[i + 3], span[i]); + break; + } + + return new ReadOnlyMemory2D(pixels, height, width); + } + + #endregion } } diff --git a/BCnEnc.Net/Encoder/Bptc/Bc6Encoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc6Encoder.cs new file mode 100644 index 0000000..6212760 --- /dev/null +++ b/BCnEnc.Net/Encoder/Bptc/Bc6Encoder.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; + +namespace BCnEncoder.Encoder.Bptc +{ + internal class Bc6Encoder : BaseBcBlockEncoder + { + private readonly bool signed; + + public Bc6Encoder(bool signed) + { + this.signed = signed; + } + + + public override GlInternalFormat GetInternalFormat() + { + return signed ? GlInternalFormat.GlCompressedRgbBptcSignedFloatArb : GlInternalFormat.GlCompressedRgbBptcUnsignedFloatArb; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgb; + } + + public override DxgiFormat GetDxgiFormat() + { + return signed ? DxgiFormat.DxgiFormatBc6HSf16 : DxgiFormat.DxgiFormatBc6HUf16; + } + + public override Bc6Block EncodeBlock(RawBlock4X4RgbFloat block, CompressionQuality quality) + { + switch (quality) + { + case CompressionQuality.Fast: + return Bc6EncoderFast.EncodeBlock(block, signed); + case CompressionQuality.Balanced: + return Bc6EncoderBalanced.EncodeBlock(block, signed); + case CompressionQuality.BestQuality: + return Bc6EncoderBestQuality.EncodeBlock(block, signed); + default: + throw new ArgumentOutOfRangeException(nameof(quality), quality, null); + } + } + + internal static ClusterIndices4X4 CreateClusterIndexBlock(RawBlock4X4RgbFloat raw, out int outputNumClusters, + int numClusters = 2) + { + + var indexBlock = new ClusterIndices4X4(); + + var indices = LinearClustering.ClusterPixels(raw.AsSpan, 4, 4, + numClusters, 1, 10, false); + + var output = indexBlock.AsSpan; + for (var i = 0; i < output.Length; i++) + { + output[i] = indices[i]; + } + + var nClusters = indexBlock.NumClusters; + if (nClusters < numClusters) + { + indexBlock = indexBlock.Reduce(out nClusters); + } + + outputNumClusters = nClusters; + return indexBlock; + } + + internal static class Bc6EncoderFast + { + internal static Bc6Block EncodeBlock(RawBlock4X4RgbFloat block, bool signed) + { + RgbBoundingBox.CreateFloat(block.AsSpan, out var min, out var max); + + LeastSquares.OptimizeEndpoints1Sub(block, ref min, ref max); + + return Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type3, block, min, max, signed, out _); + } + } + + internal static class Bc6EncoderBalanced + { + + private const float TargetError = 0.001f; + private const int MaxTries = 10; + + private static IEnumerable GenerateCandidates(RawBlock4X4RgbFloat block, bool signed) + { + var candidates = 0; + Bc6EncodingHelpers.GetInitialUnscaledEndpoints(block, out var ep0Sub1, out var ep1Sub1); + + if (!signed) + { + LeastSquares.OptimizeEndpoints1Sub(block, ref ep0Sub1, ref ep1Sub1); + } + + ep0Sub1.ClampToHalf(); + ep1Sub1.ClampToHalf(); + + if (!signed) + { + ep0Sub1.ClampToPositive(); + ep1Sub1.ClampToPositive(); + } + + //Type3 Always ok! + yield return Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type3, block, ep0Sub1, ep1Sub1, + signed, out _); + candidates++; + + var type15Block = Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type15, block, ep0Sub1, ep1Sub1, + signed, out var badType15); + candidates++; + if (!badType15) + { + yield return type15Block; + } + else + { + var indexBlock = CreateClusterIndexBlock(block, out var numClusters, 2); + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock, numClusters, true); + + foreach (var subsetPartition in best2SubsetPartitions) + { + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, subsetPartition, 0); + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep2, out var ep3, subsetPartition, 1); + + if (!signed) + { + LeastSquares.OptimizeEndpoints2Sub(block, ref ep0, ref ep1, subsetPartition, 0); + LeastSquares.OptimizeEndpoints2Sub(block, ref ep2, ref ep3, subsetPartition, 1); + } + + ep0.ClampToHalf(); + ep1.ClampToHalf(); + ep2.ClampToHalf(); + ep3.ClampToHalf(); + + if (!signed) + { + ep0.ClampToPositive(); + ep1.ClampToPositive(); + ep2.ClampToPositive(); + ep3.ClampToPositive(); + } + + { + var type1Block = Bc6ModeEncoder.EncodeBlock2Sub(Bc6BlockType.Type1, block, ep0, ep1, ep2, ep3, + subsetPartition, signed, out var badType1); + candidates++; + + if (!badType1) + { + yield return type1Block; + } + + if (candidates >= MaxTries) + { + yield break; + } + } + + { + var type14Block = Bc6ModeEncoder.EncodeBlock2Sub(Bc6BlockType.Type14, block, ep0, ep1, ep2, ep3, + subsetPartition, signed, out var badType14); + candidates++; + + if (!badType14) + { + yield return type14Block; + } + + if (candidates >= MaxTries) + { + yield break; + } + } + } + } + } + + internal static Bc6Block EncodeBlock(RawBlock4X4RgbFloat block, bool signed) + { + var result = new Bc6Block(); + var bestError = 9999999f; + + foreach (var candidate in GenerateCandidates(block, signed)) + { + var error = block.CalculateError(candidate.Decode(signed)); + + if (error < bestError) + { + result = candidate; + bestError = error; + } + + if (error <= TargetError) + { + break; + } + } + + return result; + } + } + + internal static class Bc6EncoderBestQuality + { + private const float TargetError = 0.0005f; + private const int MaxTries = 500; + + private static IEnumerable GenerateCandidates(RawBlock4X4RgbFloat block, bool signed) + { + var candidates = 0; + Bc6EncodingHelpers.GetInitialUnscaledEndpoints(block, out var ep0Sub1, out var ep1Sub1); + + if (!signed) + { + LeastSquares.OptimizeEndpoints1Sub(block, ref ep0Sub1, ref ep1Sub1); + } + + ep0Sub1.ClampToHalf(); + ep1Sub1.ClampToHalf(); + + if (!signed) + { + ep0Sub1.ClampToPositive(); + ep1Sub1.ClampToPositive(); + } + //Type3 Always ok! + yield return Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type3, block, ep0Sub1, ep1Sub1, + signed, out _); + candidates++; + + //Type7 + { + var type7Block = Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type7, block, ep0Sub1, ep1Sub1, + signed, out var badType7); + candidates++; + if (!badType7) + { + yield return type7Block; + } + } + //Type11 + { + var type11Block = Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type11, block, ep0Sub1, ep1Sub1, + signed, out var badType11); + candidates++; + if (!badType11) + { + yield return type11Block; + } + } + //Type15 + { + var type15Block = Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type15, block, ep0Sub1, ep1Sub1, + signed, out var badType15); + candidates++; + if (!badType15) + { + yield return type15Block; + } + } + + var indexBlock = CreateClusterIndexBlock(block, out var numClusters, 2); + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock, numClusters, true); + + foreach (var subsetPartition in best2SubsetPartitions) + { + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, subsetPartition, 0); + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep2, out var ep3, subsetPartition, 1); + + if (!signed) + { + LeastSquares.OptimizeEndpoints2Sub(block, ref ep0, ref ep1, subsetPartition, 0); + LeastSquares.OptimizeEndpoints2Sub(block, ref ep2, ref ep3, subsetPartition, 1); + } + + ep0.ClampToHalf(); + ep1.ClampToHalf(); + ep2.ClampToHalf(); + ep3.ClampToHalf(); + + if (!signed) + { + ep0.ClampToPositive(); + ep1.ClampToPositive(); + ep2.ClampToPositive(); + ep3.ClampToPositive(); + } + + foreach (var type in Bc6Block.Subsets2Types) + { + var sub2Block = Bc6ModeEncoder.EncodeBlock2Sub(type, block, ep0, ep1, ep2, ep3, + subsetPartition, signed, out var badTransform); + candidates++; + + if (!badTransform) + { + yield return sub2Block; + } + + if (candidates >= MaxTries) + { + yield break; + } + } + } + } + + internal static Bc6Block EncodeBlock(RawBlock4X4RgbFloat block, bool signed) + { + var result = new Bc6Block(); + float bestError = 9999999; + + foreach (var candidate in GenerateCandidates(block, signed)) + { + var error = block.CalculateError(candidate.Decode(signed)); + + if (error < bestError) + { + result = candidate; + bestError = error; + } + + if (error <= TargetError) + { + break; + } + } + + return result; + } + } + } +} diff --git a/BCnEnc.Net/Encoder/Bptc/Bc6EncodingHelpers.cs b/BCnEnc.Net/Encoder/Bptc/Bc6EncodingHelpers.cs new file mode 100644 index 0000000..fc321e1 --- /dev/null +++ b/BCnEnc.Net/Encoder/Bptc/Bc6EncodingHelpers.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Text; +using BCnEncoder.Shared; + +namespace BCnEncoder.Encoder.Bptc +{ + internal static class Bc6EncodingHelpers + { + + /// + /// Opposite of + /// + internal static int PreQuantize(float value, bool signed) + { + var half = new Half(value); + var bits = (int)Half.GetBits(half); + if (!signed) + { + + return (bits << 6) / 31; + } + else + { + const int signMask = ~0x8000; + + if (half < new Half(0)) + { + var component = -(bits & signMask); + + return -(((-component) << 5) / 31); //-(((-component) * 31) >> 5) + } + + return (bits << 5) / 31; + } + } + + /// + /// Opposite of + /// + internal static int Quantize(int component, int endpointBits, bool signed) + { + if (!signed) + { + if (endpointBits >= 15) + return component; + if (component == 0) + return 0; + if (component == 0xFFFF) + return ((1 << endpointBits) - 1); + else + return ((component << endpointBits) - 0x8000) >> 16; + + } + else + { + if (endpointBits >= 16) + return component; + else + { + if (component == 0) return 0; + if (component > 0) + { + if (component == 0x7FFF) + { + return (1 << (endpointBits - 1)) - 1; + } + + return ((component << (endpointBits - 1)) - 0x4000) >> 15; + } + else + { + if (-component == 0x7FFF) + { + return -((1 << (endpointBits - 1)) - 1); + } + + return -(((-component << (endpointBits - 1)) + 0x4000) >> 15); + } + } + } + + } + + public static (int, int, int) PreQuantizeRawEndpoint(ColorRgbFloat endpoint, bool signed) + { + var r = PreQuantize(endpoint.r, signed); + var g = PreQuantize(endpoint.g, signed); + var b = PreQuantize(endpoint.b, signed); + + return ( + r, + g, + b + ); + } + + public static (int, int, int) FinishQuantizeEndpoint((int, int, int) endpoint, int endpointBits, bool signed) + { + return ( + Quantize(endpoint.Item1, endpointBits, signed), + Quantize(endpoint.Item2, endpointBits, signed), + Quantize(endpoint.Item3, endpointBits, signed) + ); + } + + public static int CreateTranformedEndpoint(int quantizedEp0, + int quantizedEpT, int deltaBits, ref bool badTransform) + { + var delta = quantizedEpT - quantizedEp0; + var max = (1 << (deltaBits - 1)); + + if (delta >= 0 ? delta >= max : -delta > max) // delta overflow + { + badTransform = true; + } + + if (delta >= 0) + { + if (delta >= max) + { + delta = max - 1; + } + } + else if (-delta > max) + { + delta = max; + } + else + { + delta = delta & ((1 << deltaBits) - 1); + } + return delta; + } + + public static (int, int, int) CreateTransformedEndpoint((int, int, int) quantizedEp0, + (int, int, int) quantizedEpT, (int, int, int) deltaBits, ref bool badTransform) + { + return ( + CreateTranformedEndpoint(quantizedEp0.Item1, quantizedEpT.Item1, deltaBits.Item1, ref badTransform), + CreateTranformedEndpoint(quantizedEp0.Item2, quantizedEpT.Item2, deltaBits.Item2, ref badTransform), + CreateTranformedEndpoint(quantizedEp0.Item3, quantizedEpT.Item3, deltaBits.Item3, ref badTransform) + ); + } + + public static void GeneratePalette(Span palette, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, int indexPrecision, bool signed) + { + var paletteSize = 1 << indexPrecision; + + for (var i = 0; i < paletteSize; i++) + { + var interpolated = Bc6Block.InterpolateColor(unQuantizedEp0, unQuantizedEp1, i, indexPrecision); + var (r, g, b) = Bc6Block.FinishUnQuantize(interpolated, signed); + palette[i] = new ColorRgbFloat(r, g, b); + } + } + + public static void GeneratePaletteInt(Span<(int, int, int)> palette, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, int indexPrecision, bool signed) + { + var paletteSize = 1 << indexPrecision; + + for (var i = 0; i < paletteSize; i++) + { + var interpolated = Bc6Block.InterpolateColor(unQuantizedEp0, unQuantizedEp1, i, indexPrecision); + palette[i] = interpolated; + } + } + + private static int FindClosestColorIndexInt((int, int, int) color, ReadOnlySpan<(int, int, int)> colors, out float bestError) + { + static float CalculateError((int, int, int) c0, (int, int, int) c1) => + Math.Abs(c0.Item1 - c1.Item1) + + Math.Abs(c0.Item2 - c1.Item2) + + Math.Abs(c0.Item3 - c1.Item3); + + bestError = CalculateError(color, colors[0]); + var bestIndex = 0; + for (var i = 1; i < colors.Length; i++) + { + var error = CalculateError(color, colors[i]); + if (error < bestError) + { + bestIndex = i; + bestError = error; + } + if (bestError == 0) + { + break; + } + } + return bestIndex; + } + + private static int FindClosestColorIndex(ColorRgbFloat color, ReadOnlySpan colors, out float bestError) + { + bestError = color.CalcLogDist(colors[0]); + var bestIndex = 0; + for (var i = 1; i < colors.Length; i++) + { + var error = color.CalcLogDist(colors[i]); + if (error < bestError) + { + bestIndex = i; + bestError = error; + } + if (bestError == 0) + { + break; + } + } + return bestIndex; + } + + public static float FindOptimalIndicesInt1Sub(RawBlock4X4RgbFloat block, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, + Span indices, bool signed) + { + const int paletteSize = 1 << 4; + Span<(int, int, int)> palette = stackalloc (int, int, int)[paletteSize]; + GeneratePaletteInt(palette, unQuantizedEp0, unQuantizedEp1, 4, signed); + + var pixels = block.AsSpan; + var error = 0f; + for (var i = 0; i < pixels.Length; i++) + { + var intPixel = PreQuantizeRawEndpoint(pixels[i], signed); + indices[i] = (byte)FindClosestColorIndexInt(intPixel, palette, out var e); + error += e; + } + return MathF.Sqrt(error / (3 * 16)); + } + + + public static float FindOptimalIndices1Sub(RawBlock4X4RgbFloat block, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, + Span indices, bool signed) + { + const int paletteSize = 1 << 4; + Span palette = stackalloc ColorRgbFloat[paletteSize]; + GeneratePalette(palette, unQuantizedEp0, unQuantizedEp1, 4, signed); + + var pixels = block.AsSpan; + var error = 0f; + for (var i = 0; i < pixels.Length; i++) + { + + indices[i] = (byte)FindClosestColorIndex(pixels[i], palette, out var e); + error += e; + } + return error; + } + + public static float FindOptimalIndicesInt2Sub(RawBlock4X4RgbFloat block, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, + Span indices, int partitionSetId, int subsetIndex, bool signed) + { + const int paletteSize = 1 << 3; + Span<(int, int, int)> palette = stackalloc (int, int, int)[paletteSize]; + GeneratePaletteInt(palette, unQuantizedEp0, unQuantizedEp1, 3, signed); + + var pixels = block.AsSpan; + var error = 0f; + for (var i = 0; i < pixels.Length; i++) + { + if (Bc6Block.Subsets2PartitionTable[partitionSetId][i] == subsetIndex) + { + var intPixel = PreQuantizeRawEndpoint(pixels[i], signed); + indices[i] = (byte)FindClosestColorIndexInt(intPixel, palette, out var e); + error += e; + } + } + return error; + } + + public static float FindOptimalIndices2Sub(RawBlock4X4RgbFloat block, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, + Span indices, int partitionSetId, int subsetIndex, bool signed) + { + const int paletteSize = 1 << 3; + Span palette = stackalloc ColorRgbFloat[paletteSize]; + GeneratePalette(palette, unQuantizedEp0, unQuantizedEp1, 3, signed); + + var pixels = block.AsSpan; + var error = 0f; + for (var i = 0; i < pixels.Length; i++) + { + if (Bc6Block.Subsets2PartitionTable[partitionSetId][i] == subsetIndex) + { + indices[i] = (byte)FindClosestColorIndex(pixels[i], palette, out var e); + error += e; + } + } + return error; + } + + public static void SwapIndicesIfNecessary1Sub(RawBlock4X4RgbFloat block, ref (int, int, int) unQuantizedEp0, ref (int, int, int) unQuantizedEp1, + Span indices, bool signed) + { + const int msb = 1 << (3); + + if ((indices[0] & msb) == 0) + { + return; + } + + InternalUtils.Swap(ref unQuantizedEp0, ref unQuantizedEp1); + FindOptimalIndicesInt1Sub(block, unQuantizedEp0, unQuantizedEp1, indices, signed); + } + + public static void SwapIndicesIfNecessary2Sub(RawBlock4X4RgbFloat block, ref (int, int, int) unQuantizedEp0, ref (int, int, int) unQuantizedEp1, + Span indices, int partitionSetId, int subsetIndex, bool signed) + { + const int msb = 1 << (2); + + var anchorIndex = subsetIndex == 0 ? 0 : Bc6Block.Subsets2AnchorIndices[partitionSetId]; + + if ((indices[anchorIndex] & msb) == 0) + { + return; + } + + InternalUtils.Swap(ref unQuantizedEp0, ref unQuantizedEp1); + FindOptimalIndicesInt2Sub(block, unQuantizedEp0, unQuantizedEp1, indices, partitionSetId, subsetIndex, signed); + } + + public static void GetInitialUnscaledEndpointsForSubset(RawBlock4X4RgbFloat block, out ColorRgbFloat ep0, + out ColorRgbFloat ep1, int partitionSetId, int subsetIndex) + { + + var originalPixels = block.AsSpan; + + var count = 0; + for (var i = 0; i < 16; i++) + { + if (Bc6Block.Subsets2PartitionTable[partitionSetId][i] == subsetIndex) + { + count++; + } + } + + Span subsetColors = stackalloc ColorRgbFloat[count]; + var next = 0; + for (var i = 0; i < 16; i++) + { + if (Bc6Block.Subsets2PartitionTable[partitionSetId][i] == subsetIndex) + { + subsetColors[next++] = originalPixels[i]; + } + } + + PcaVectors.Create(subsetColors, out var mean, out var pa); + PcaVectors.GetExtremePoints(subsetColors, mean, pa, out var min, out var max); + + ep0 = new ColorRgbFloat(min); + ep1 = new ColorRgbFloat(max); + } + + public static void GetInitialUnscaledEndpoints(RawBlock4X4RgbFloat block, out ColorRgbFloat ep0, + out ColorRgbFloat ep1) + { + + var originalPixels = block.AsSpan; + + PcaVectors.Create(originalPixels, out var mean, out var pa); + PcaVectors.GetExtremePoints(originalPixels, mean, pa, out var min, out var max); + + ep0 = new ColorRgbFloat(min); + ep1 = new ColorRgbFloat(max); + } + + + } +} diff --git a/BCnEnc.Net/Encoder/Bptc/Bc6ModeEncoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc6ModeEncoder.cs new file mode 100644 index 0000000..987ab1d --- /dev/null +++ b/BCnEnc.Net/Encoder/Bptc/Bc6ModeEncoder.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using BCnEncoder.Shared; + +namespace BCnEncoder.Encoder.Bptc +{ + internal static class Bc6ModeEncoder + { + + public static Bc6Block EncodeBlock1Sub(Bc6BlockType type, RawBlock4X4RgbFloat block, ColorRgbFloat initialEndpoint0, + ColorRgbFloat initialEndpoint1, bool signed, out bool badTransform) + { + var endpointBits = type.EndpointBits(); + var deltaBits = type.DeltaBits(); + var hasTransformedEndpoints = type.HasTransformedEndpoints(); + + var initialPreQuantizedEp0 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint0, signed); + var initialPreQuantizedEp1 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint1, signed); + + var initialQuantizedEp0 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp0, endpointBits, signed); + var initialQuantizedEp1 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp1, endpointBits, signed); + + if (hasTransformedEndpoints) + { + // check for delta overflow before index search + var bTransform = false; + Bc6EncodingHelpers.CreateTransformedEndpoint(initialQuantizedEp0, initialQuantizedEp1, deltaBits, ref bTransform); + if (bTransform) + { + badTransform = true; + return default; + } + } + + var unquantizedEndpoint0 = Bc6Block.UnQuantize(initialQuantizedEp0, endpointBits, signed); + var unquantizedEndpoint1 = Bc6Block.UnQuantize(initialQuantizedEp1, endpointBits, signed); + + Span indices = stackalloc byte[16]; + + Bc6EncodingHelpers.FindOptimalIndicesInt1Sub(block, unquantizedEndpoint0, unquantizedEndpoint1, indices, signed); + + + Bc6EncodingHelpers.SwapIndicesIfNecessary1Sub(block, ref unquantizedEndpoint0, ref unquantizedEndpoint1, indices, signed); + + var quantEp0 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint0, endpointBits, signed); + var quantEp1 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint1, endpointBits, signed); + + badTransform = false; + + if (hasTransformedEndpoints) + { + quantEp1 = Bc6EncodingHelpers.CreateTransformedEndpoint(quantEp0, quantEp1, deltaBits, + ref badTransform); + } + + switch (type) + { + case Bc6BlockType.Type3: + return Bc6Block.PackType3(quantEp0, quantEp1, indices); + case Bc6BlockType.Type7: + return Bc6Block.PackType7(quantEp0, quantEp1, indices); + case Bc6BlockType.Type11: + return Bc6Block.PackType11(quantEp0, quantEp1, indices); + case Bc6BlockType.Type15: + return Bc6Block.PackType15(quantEp0, quantEp1, indices); + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + public static Bc6Block EncodeBlock2Sub(Bc6BlockType type, RawBlock4X4RgbFloat block, ColorRgbFloat initialEndpoint0, + ColorRgbFloat initialEndpoint1, ColorRgbFloat initialEndpoint2, + ColorRgbFloat initialEndpoint3, int partitionSetId, bool signed, out bool badTransform) + { + Debug.Assert(type.HasSubsets(), "Trying to use 2-subset method for 1-subset block type!"); + + var endpointBits = type.EndpointBits(); + var deltaBits = type.DeltaBits(); + var hasTransformedEndpoints = type.HasTransformedEndpoints(); + + var initialPreQuantizedEp0 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint0, signed); + var initialPreQuantizedEp1 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint1, signed); + var initialPreQuantizedEp2 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint2, signed); + var initialPreQuantizedEp3 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint3, signed); + + var initialQuantizedEp0 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp0, endpointBits, signed); + var initialQuantizedEp1 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp1, endpointBits, signed); + var initialQuantizedEp2 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp2, endpointBits, signed); + var initialQuantizedEp3 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp3, endpointBits, signed); + + if (hasTransformedEndpoints) + { + // check for delta overflow before index search + var bTransform = false; + Bc6EncodingHelpers.CreateTransformedEndpoint(initialQuantizedEp0, initialQuantizedEp1, deltaBits, ref bTransform); + Bc6EncodingHelpers.CreateTransformedEndpoint(initialQuantizedEp0, initialQuantizedEp2, deltaBits, ref bTransform); + Bc6EncodingHelpers.CreateTransformedEndpoint(initialQuantizedEp0, initialQuantizedEp3, deltaBits, ref bTransform); + if (bTransform) + { + badTransform = true; + return default; + } + } + + + var unquantizedEndpoint0 = Bc6Block.UnQuantize(initialQuantizedEp0, endpointBits, signed); + var unquantizedEndpoint1 = Bc6Block.UnQuantize(initialQuantizedEp1, endpointBits, signed); + var unquantizedEndpoint2 = Bc6Block.UnQuantize(initialQuantizedEp2, endpointBits, signed); + var unquantizedEndpoint3 = Bc6Block.UnQuantize(initialQuantizedEp3, endpointBits, signed); + + + Span indices = stackalloc byte[16]; + + Bc6EncodingHelpers.FindOptimalIndicesInt2Sub(block, unquantizedEndpoint0, unquantizedEndpoint1, indices, + partitionSetId, 0, signed); + Bc6EncodingHelpers.FindOptimalIndicesInt2Sub(block, unquantizedEndpoint2, unquantizedEndpoint3, indices, + partitionSetId, 1, signed); + + + Bc6EncodingHelpers.SwapIndicesIfNecessary2Sub(block, ref unquantizedEndpoint0, ref unquantizedEndpoint1, indices, + partitionSetId, 0, signed); + Bc6EncodingHelpers.SwapIndicesIfNecessary2Sub(block, ref unquantizedEndpoint2, ref unquantizedEndpoint3, indices, + partitionSetId, 1, signed); + + var quantEp0 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint0, endpointBits, signed); + var quantEp1 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint1, endpointBits, signed); + var quantEp2 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint2, endpointBits, signed); + var quantEp3 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint3, endpointBits, signed); + + badTransform = false; + + if (hasTransformedEndpoints) + { + quantEp1 = Bc6EncodingHelpers.CreateTransformedEndpoint(quantEp0, quantEp1, deltaBits, ref badTransform); + quantEp2 = Bc6EncodingHelpers.CreateTransformedEndpoint(quantEp0, quantEp2, deltaBits, ref badTransform); + quantEp3 = Bc6EncodingHelpers.CreateTransformedEndpoint(quantEp0, quantEp3, deltaBits, ref badTransform); + } + + switch (type) + { + case Bc6BlockType.Type0: + return Bc6Block.PackType0(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type1: + return Bc6Block.PackType1(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type2: + return Bc6Block.PackType2(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type6: + return Bc6Block.PackType6(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type10: + return Bc6Block.PackType10(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type14: + return Bc6Block.PackType14(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type18: + return Bc6Block.PackType18(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type22: + return Bc6Block.PackType22(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type26: + return Bc6Block.PackType26(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type30: + return Bc6Block.PackType30(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + } +} diff --git a/BCnEnc.Net/Encoder/Bc7/Bc7Encoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc7Encoder.cs similarity index 56% rename from BCnEnc.Net/Encoder/Bc7/Bc7Encoder.cs rename to BCnEnc.Net/Encoder/Bptc/Bc7Encoder.cs index 7ae7a7e..2a7f280 100644 --- a/BCnEnc.Net/Encoder/Bc7/Bc7Encoder.cs +++ b/BCnEnc.Net/Encoder/Bptc/Bc7Encoder.cs @@ -1,70 +1,58 @@ -using System; +using System; using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading.Tasks; using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; -namespace BCnEncoder.Encoder.Bc7 +namespace BCnEncoder.Encoder.Bptc { - internal class Bc7Encoder : IBcBlockEncoder + internal class Bc7Encoder : BaseBcBlockEncoder { - public byte[] Encode(RawBlock4X4Rgba32[] blocks, int blockWidth, int blockHeight, EncodingQuality quality, bool parallel) + public override Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock, CompressionQuality quality) { - byte[] outputData = new byte[blockWidth * blockHeight * Marshal.SizeOf()]; - Span outputBlocks = MemoryMarshal.Cast(outputData); - - - if (parallel) - { - Parallel.For(0, blocks.Length, i => - { - Span outputBlocks = MemoryMarshal.Cast(outputData); - outputBlocks[i] = EncodeBlock(blocks[i], quality); - }); - } - else + switch (quality) { - for (int i = 0; i < blocks.Length; i++) - { - outputBlocks[i] = EncodeBlock(blocks[i], quality); - } + case CompressionQuality.Fast: + return Bc7EncoderFast.EncodeBlock(rawBlock); + case CompressionQuality.Balanced: + return Bc7EncoderBalanced.EncodeBlock(rawBlock); + case CompressionQuality.BestQuality: + return Bc7EncoderBestQuality.EncodeBlock(rawBlock); + default: + throw new ArgumentOutOfRangeException(nameof(quality), quality, null); } - - - return outputData; } - public GlInternalFormat GetInternalFormat() + public override GlInternalFormat GetInternalFormat() { - return GlInternalFormat.GL_COMPRESSED_RGBA_BPTC_UNORM_ARB; + return GlInternalFormat.GlCompressedRgbaBptcUnormArb; } - public GLFormat GetBaseInternalFormat() + public override GlFormat GetBaseInternalFormat() { - return GLFormat.GL_RGBA; + return GlFormat.GlRgba; } - public DXGI_FORMAT GetDxgiFormat() { - return DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM; + public override DxgiFormat GetDxgiFormat() { + return DxgiFormat.DxgiFormatBc7Unorm; } private static ClusterIndices4X4 CreateClusterIndexBlock(RawBlock4X4Rgba32 raw, out int outputNumClusters, int numClusters = 3) { - ClusterIndices4X4 indexBlock = new ClusterIndices4X4(); + var indexBlock = new ClusterIndices4X4(); var indices = LinearClustering.ClusterPixels(raw.AsSpan, 4, 4, numClusters, 1, 10, false); var output = indexBlock.AsSpan; - for (int i = 0; i < output.Length; i++) + for (var i = 0; i < output.Length; i++) { output[i] = indices[i]; } - int nClusters = indexBlock.NumClusters; + var nClusters = indexBlock.NumClusters; if (nClusters < numClusters) { indexBlock = indexBlock.Reduce(out nClusters); @@ -74,26 +62,10 @@ private static ClusterIndices4X4 CreateClusterIndexBlock(RawBlock4X4Rgba32 raw, return indexBlock; } - - private static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock, EncodingQuality quality) - { - switch (quality) - { - case EncodingQuality.Fast: - return Bc7EncoderFast.EncodeBlock(rawBlock); - case EncodingQuality.Balanced: - return Bc7EncoderBalanced.EncodeBlock(rawBlock); - case EncodingQuality.BestQuality: - return Bc7EncoderBestQuality.EncodeBlock(rawBlock); - default: - throw new ArgumentOutOfRangeException(nameof(quality), quality, null); - } - } - private static class Bc7EncoderFast { - private const float errorThreshold = 0.005f; - private const int maxTries = 5; + private const float ErrorThreshold = 0.005f; + private const int MaxTries = 5; private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[] best2SubsetPartitions, int[] best3SubsetPartitions, bool alpha) { @@ -105,7 +77,7 @@ private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[ else { yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 6); - for (int i = 0; i < 64; i++) { + for (var i = 0; i < 64; i++) { if(best3SubsetPartitions[i] < 16) { yield return Bc7Mode0Encoder.EncodeBlock(rawBlock, 3, best3SubsetPartitions[i]); } @@ -118,25 +90,25 @@ private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { - bool hasAlpha = rawBlock.HasTransparentPixels(); + var hasAlpha = rawBlock.HasTransparentPixels(); - var indexBlock2 = CreateClusterIndexBlock(rawBlock, out int clusters2, 2); - var indexBlock3 = CreateClusterIndexBlock(rawBlock, out int clusters3, 3); + var indexBlock2 = CreateClusterIndexBlock(rawBlock, out var clusters2, 2); + var indexBlock3 = CreateClusterIndexBlock(rawBlock, out var clusters3, 3); if (clusters2 < 2) { clusters2 = clusters3; indexBlock2 = indexBlock3; } - int[] best2SubsetPartitions = Bc7EncodingHelpers.Rank2SubsetPartitions(indexBlock2, clusters2); - int[] best3SubsetPartitions = Bc7EncodingHelpers.Rank3SubsetPartitions(indexBlock3, clusters3); + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock2, clusters2); + var best3SubsetPartitions = BptcEncodingHelpers.Rank3SubsetPartitions(indexBlock3, clusters3); float bestError = 99999; - Bc7Block best = new Bc7Block(); - int tries = 0; - foreach (Bc7Block block in TryMethods(rawBlock, best2SubsetPartitions, best3SubsetPartitions, hasAlpha)) { + var best = new Bc7Block(); + var tries = 0; + foreach (var block in TryMethods(rawBlock, best2SubsetPartitions, best3SubsetPartitions, hasAlpha)) { var decoded = block.Decode(); - float error = rawBlock.CalculateYCbCrAlphaError(decoded); + var error = rawBlock.CalculateYCbCrAlphaError(decoded); tries++; if(error < bestError) { @@ -144,7 +116,7 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) bestError = error; } - if (error < errorThreshold || tries > maxTries) { + if (error < ErrorThreshold || tries > MaxTries) { break; } @@ -156,8 +128,8 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) private static class Bc7EncoderBalanced { - private const float errorThreshold = 0.005f; - private const int maxTries = 25; + private const float ErrorThreshold = 0.005f; + private const int MaxTries = 25; private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[] best2SubsetPartitions, int[] best3SubsetPartitions, bool alpha) { @@ -166,7 +138,7 @@ private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[ yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 6); yield return Bc7Mode5Encoder.EncodeBlock(rawBlock, 4); yield return Bc7Mode4Encoder.EncodeBlock(rawBlock, 4); - for (int i = 0; i < 64; i++) + for (var i = 0; i < 64; i++) { yield return Bc7Mode7Encoder.EncodeBlock(rawBlock, 3, best2SubsetPartitions[i]); } @@ -176,7 +148,7 @@ private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[ yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 6); yield return Bc7Mode5Encoder.EncodeBlock(rawBlock, 4); yield return Bc7Mode4Encoder.EncodeBlock(rawBlock, 4); - for (int i = 0; i < 64; i++) { + for (var i = 0; i < 64; i++) { if(best3SubsetPartitions[i] < 16) { yield return Bc7Mode0Encoder.EncodeBlock(rawBlock, 3, best3SubsetPartitions[i]); } @@ -191,25 +163,25 @@ private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { - bool hasAlpha = rawBlock.HasTransparentPixels(); + var hasAlpha = rawBlock.HasTransparentPixels(); - var indexBlock2 = CreateClusterIndexBlock(rawBlock, out int clusters2, 2); - var indexBlock3 = CreateClusterIndexBlock(rawBlock, out int clusters3, 3); + var indexBlock2 = CreateClusterIndexBlock(rawBlock, out var clusters2, 2); + var indexBlock3 = CreateClusterIndexBlock(rawBlock, out var clusters3, 3); if (clusters2 < 2) { clusters2 = clusters3; indexBlock2 = indexBlock3; } - int[] best2SubsetPartitions = Bc7EncodingHelpers.Rank2SubsetPartitions(indexBlock2, clusters2); - int[] best3SubsetPartitions = Bc7EncodingHelpers.Rank3SubsetPartitions(indexBlock3, clusters3); + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock2, clusters2); + var best3SubsetPartitions = BptcEncodingHelpers.Rank3SubsetPartitions(indexBlock3, clusters3); float bestError = 99999; - Bc7Block best = new Bc7Block(); - int tries = 0; - foreach (Bc7Block block in TryMethods(rawBlock, best2SubsetPartitions, best3SubsetPartitions, hasAlpha)) { + var best = new Bc7Block(); + var tries = 0; + foreach (var block in TryMethods(rawBlock, best2SubsetPartitions, best3SubsetPartitions, hasAlpha)) { var decoded = block.Decode(); - float error = rawBlock.CalculateYCbCrAlphaError(decoded); + var error = rawBlock.CalculateYCbCrAlphaError(decoded); tries++; if(error < bestError) { @@ -217,7 +189,7 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) bestError = error; } - if (error < errorThreshold || tries > maxTries) { + if (error < ErrorThreshold || tries > MaxTries) { break; } @@ -230,8 +202,8 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) private static class Bc7EncoderBestQuality { - private const float errorThreshold = 0.001f; - private const int maxTries = 40; + private const float ErrorThreshold = 0.001f; + private const int MaxTries = 40; private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[] best2SubsetPartitions, int[] best3SubsetPartitions, bool alpha) { @@ -240,7 +212,7 @@ private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[ yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 8); yield return Bc7Mode5Encoder.EncodeBlock(rawBlock, 5); yield return Bc7Mode4Encoder.EncodeBlock(rawBlock, 5); - for (int i = 0; i < 64; i++) + for (var i = 0; i < 64; i++) { yield return Bc7Mode7Encoder.EncodeBlock(rawBlock, 4, best2SubsetPartitions[i]); @@ -251,7 +223,7 @@ private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[ yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 8); yield return Bc7Mode5Encoder.EncodeBlock(rawBlock, 5); yield return Bc7Mode4Encoder.EncodeBlock(rawBlock, 5); - for (int i = 0; i < 64; i++) { + for (var i = 0; i < 64; i++) { if(best3SubsetPartitions[i] < 16) { yield return Bc7Mode0Encoder.EncodeBlock(rawBlock, 4, best3SubsetPartitions[i]); } @@ -266,26 +238,26 @@ private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) { - bool hasAlpha = rawBlock.HasTransparentPixels(); + var hasAlpha = rawBlock.HasTransparentPixels(); - var indexBlock2 = CreateClusterIndexBlock(rawBlock, out int clusters2, 2); - var indexBlock3 = CreateClusterIndexBlock(rawBlock, out int clusters3, 3); + var indexBlock2 = CreateClusterIndexBlock(rawBlock, out var clusters2, 2); + var indexBlock3 = CreateClusterIndexBlock(rawBlock, out var clusters3, 3); if (clusters2 < 2) { clusters2 = clusters3; indexBlock2 = indexBlock3; } - int[] best2SubsetPartitions = Bc7EncodingHelpers.Rank2SubsetPartitions(indexBlock2, clusters2); - int[] best3SubsetPartitions = Bc7EncodingHelpers.Rank3SubsetPartitions(indexBlock3, clusters3); + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock2, clusters2); + var best3SubsetPartitions = BptcEncodingHelpers.Rank3SubsetPartitions(indexBlock3, clusters3); float bestError = 99999; - Bc7Block best = new Bc7Block(); - int tries = 0; - foreach (Bc7Block block in TryMethods(rawBlock, best2SubsetPartitions, best3SubsetPartitions, hasAlpha)) { + var best = new Bc7Block(); + var tries = 0; + foreach (var block in TryMethods(rawBlock, best2SubsetPartitions, best3SubsetPartitions, hasAlpha)) { var decoded = block.Decode(); - float error = rawBlock.CalculateYCbCrAlphaError(decoded); + var error = rawBlock.CalculateYCbCrAlphaError(decoded); tries++; if(error < bestError) { @@ -293,7 +265,7 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) bestError = error; } - if (error < errorThreshold || tries > maxTries) { + if (error < ErrorThreshold || tries > MaxTries) { break; } diff --git a/BCnEnc.Net/Encoder/Bc7/Bc7EncodingHelpers.cs b/BCnEnc.Net/Encoder/Bptc/Bc7EncodingHelpers.cs similarity index 51% rename from BCnEnc.Net/Encoder/Bc7/Bc7EncodingHelpers.cs rename to BCnEnc.Net/Encoder/Bptc/Bc7EncodingHelpers.cs index 24371cd..5b39c06 100644 --- a/BCnEnc.Net/Encoder/Bc7/Bc7EncodingHelpers.cs +++ b/BCnEnc.Net/Encoder/Bptc/Bc7EncodingHelpers.cs @@ -1,111 +1,24 @@ -using System; +using System; using System.Linq; using System.Runtime.InteropServices; using BCnEncoder.Shared; -using SixLabors.ImageSharp.PixelFormats; -namespace BCnEncoder.Encoder.Bc7 +namespace BCnEncoder.Encoder.Bptc { - internal struct ClusterIndices4X4 - { - public int i00, i10, i20, i30; - public int i01, i11, i21, i31; - public int i02, i12, i22, i32; - public int i03, i13, i23, i33; - - public Span AsSpan => MemoryMarshal.CreateSpan(ref i00, 16); - - public int this[int x, int y] - { - get => AsSpan[x + y * 4]; - set => AsSpan[x + y * 4] = value; - } - - public int this[int index] - { - get => AsSpan[index]; - set => AsSpan[index] = value; - } - - public int NumClusters - { - get - { - var t = AsSpan; - Span clusters = stackalloc int[16]; - int distinct = 0; - for (int i = 0; i < 16; i++) - { - var cluster = t[i]; - bool found = false; - for (int j = 0; j < distinct; j++) - { - if (clusters[j] == cluster) - { - found = true; - break; - } - } - if (!found) - { - clusters[distinct] = cluster; - ++distinct; - } - } - return distinct; - } - } - - /// - /// Reduces block down to adjacent cluster indices. For example, - /// block that contains clusters 5, 16 and 77 will become a block that contains clusters 0, 1 and 2 - /// - public ClusterIndices4X4 Reduce(out int numClusters) - { - var result = new ClusterIndices4X4(); - numClusters = NumClusters; - Span mapKey = stackalloc int[numClusters]; - var indices = AsSpan; - var outIndices = result.AsSpan; - int next = 0; - for (int i = 0; i < 16; i++) - { - var cluster = indices[i]; - bool found = false; - for (int j = 0; j < next; j++) - { - if (mapKey[j] == cluster) - { - found = true; - outIndices[i] = j; - break; - } - } - if (!found) - { - outIndices[i] = next; - mapKey[next] = cluster; - ++next; - } - } - - return result; - } - } internal static class Bc7EncodingHelpers { - private static int[] varPatternRAlpha = new int[] { 1, -1, 1, 0, 0, -1, 0, 0, 0, 0 }; - private static int[] varPatternRNoAlpha = new int[] { 1, -1, 1, 0, 0, -1, 0, 0 }; + private static readonly int[] varPatternRAlpha = new int[] { 1, -1, 1, 0, 0, -1, 0, 0, 0, 0 }; + private static readonly int[] varPatternRNoAlpha = new int[] { 1, -1, 1, 0, 0, -1, 0, 0 }; - private static int[] varPatternGAlpha = new int[] { 1, -1, 0, 1, 0, 0, -1, 0, 0, 0 }; - private static int[] varPatternGNoAlpha = new int[] { 1, -1, 0, 1, 0, 0, -1, 0 }; + private static readonly int[] varPatternGAlpha = new int[] { 1, -1, 0, 1, 0, 0, -1, 0, 0, 0 }; + private static readonly int[] varPatternGNoAlpha = new int[] { 1, -1, 0, 1, 0, 0, -1, 0 }; - private static int[] varPatternBAlpha = new int[] { 1, -1, 0, 0, 1, 0, 0, -1, 0, 0 }; - private static int[] varPatternBNoAlpha = new int[] { 1, -1, 0, 0, 1, 0, 0, -1 }; + private static readonly int[] varPatternBAlpha = new int[] { 1, -1, 0, 0, 1, 0, 0, -1, 0, 0 }; + private static readonly int[] varPatternBNoAlpha = new int[] { 1, -1, 0, 0, 1, 0, 0, -1 }; - private static int[] varPatternAAlpha = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 1, -1 }; - private static int[] varPatternANoAlpha = new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }; + private static readonly int[] varPatternAAlpha = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 1, -1 }; + private static readonly int[] varPatternANoAlpha = new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }; public static bool TypeHasPBits(Bc7BlockType type) => type switch { @@ -210,7 +123,7 @@ public static void ExpandEndpoints(Bc7BlockType type, ColorRgba32[] endpoints, b { if (type == Bc7BlockType.Type0 || type == Bc7BlockType.Type1 || type == Bc7BlockType.Type3 || type == Bc7BlockType.Type6 || type == Bc7BlockType.Type7) { - for (int i = 0; i < endpoints.Length; i++) + for (var i = 0; i < endpoints.Length; i++) { endpoints[i] <<= 1; } @@ -224,7 +137,7 @@ public static void ExpandEndpoints(Bc7BlockType type, ColorRgba32[] endpoints, b } else { - for (int i = 0; i < endpoints.Length; i++) + for (var i = 0; i < endpoints.Length; i++) { endpoints[i] |= pBits[i]; } @@ -233,7 +146,7 @@ public static void ExpandEndpoints(Bc7BlockType type, ColorRgba32[] endpoints, b var colorPrecision = GetColorComponentPrecisionWithPBit(type); var alphaPrecision = GetAlphaComponentPrecisionWithPBit(type); - for (int i = 0; i < endpoints.Length; i++) + for (var i = 0; i < endpoints.Length; i++) { // ColorComponentPrecision & AlphaComponentPrecision includes pbit // left shift endpoint components so that their MSB lies in bit 7 @@ -253,7 +166,7 @@ public static void ExpandEndpoints(Bc7BlockType type, ColorRgba32[] endpoints, b //set alpha equal to 255 if (type == Bc7BlockType.Type0 || type == Bc7BlockType.Type1 || type == Bc7BlockType.Type2 || type == Bc7BlockType.Type3) { - for (int i = 0; i < endpoints.Length; i++) + for (var i = 0; i < endpoints.Length; i++) { endpoints[i].a = 255; } @@ -292,322 +205,6 @@ public static ColorRgba32 ExpandEndpoint(Bc7BlockType type, ColorRgba32 endpoint return endpoint; } - public static int SelectBest2SubsetPartition(ClusterIndices4X4 reducedIndicesBlock, int numDistinctClusters, out int bestError) - { - bool first = true; - bestError = 999; - int bestPartition = 0; - - - int CalculatePartitionError(int partitionIndex) - { - int error = 0; - ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[partitionIndex]; - Span subset0 = stackalloc int[numDistinctClusters]; - Span subset1 = stackalloc int[numDistinctClusters]; - int max0Idx = 0; - int max1Idx = 0; - - //Calculate largest cluster index for each subset - for (int i = 0; i < 16; i++) - { - if (partitionTable[i] == 0) - { - int r = reducedIndicesBlock[i]; - subset0[r]++; - int count = subset0[r]; - if (count > subset0[max0Idx]) - { - max0Idx = r; - } - } - else - { - int r = reducedIndicesBlock[i]; - subset1[r]++; - int count = subset1[r]; - if (count > subset1[max1Idx]) - { - max1Idx = r; - } - } - } - - // Calculate error by counting as error everything that does not match the largest cluster - for (int i = 0; i < 16; i++) - { - if (partitionTable[i] == 0) - { - if (reducedIndicesBlock[i] != max0Idx) error++; - } - else - { - if (reducedIndicesBlock[i] != max1Idx) error++; - } - } - - return error; - } - - for (int i = 0; i < 64; i++) // Loop through all possible indices - { - int error = CalculatePartitionError(i); - if (first) - { - bestError = error; - bestPartition = i; - first = false; - } - else if (error < bestError) - { - bestError = error; - bestPartition = i; - } - // Break early if exact match - if (bestError == 0) - { - break; - } - } - - return bestPartition; - } - - public static int[] Rank2SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, int numDistinctClusters) - { - int[] output = Enumerable.Range(0, 64).ToArray(); - - - int CalculatePartitionError(int partitionIndex) - { - int error = 0; - ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[partitionIndex]; - Span subset0 = stackalloc int[numDistinctClusters]; - Span subset1 = stackalloc int[numDistinctClusters]; - int max0Idx = 0; - int max1Idx = 0; - - //Calculate largest cluster index for each subset - for (int i = 0; i < 16; i++) - { - if (partitionTable[i] == 0) - { - int r = reducedIndicesBlock[i]; - subset0[r]++; - int count = subset0[r]; - if (count > subset0[max0Idx]) - { - max0Idx = r; - } - } - else - { - int r = reducedIndicesBlock[i]; - subset1[r]++; - int count = subset1[r]; - if (count > subset1[max1Idx]) - { - max1Idx = r; - } - } - } - - // Calculate error by counting as error everything that does not match the largest cluster - for (int i = 0; i < 16; i++) - { - if (partitionTable[i] == 0) - { - if (reducedIndicesBlock[i] != max0Idx) error++; - } - else - { - if (reducedIndicesBlock[i] != max1Idx) error++; - } - } - - return error; - } - - output = output.OrderBy(CalculatePartitionError).ToArray(); - - return output; - } - - public static int SelectBest3SubsetPartition(ClusterIndices4X4 reducedIndicesBlock, int numDistinctClusters, out int bestError) - { - bool first = true; - bestError = 999; - int bestPartition = 0; - - - - int CalculatePartitionError(int partitionIndex) - { - int error = 0; - ReadOnlySpan partitionTable = Bc7Block.Subsets3PartitionTable[partitionIndex]; - - Span subset0 = stackalloc int[numDistinctClusters]; - Span subset1 = stackalloc int[numDistinctClusters]; - Span subset2 = stackalloc int[numDistinctClusters]; - int max0Idx = 0; - int max1Idx = 0; - int max2Idx = 0; - - //Calculate largest cluster index for each subset - for (int i = 0; i < 16; i++) - { - if (partitionTable[i] == 0) - { - int r = reducedIndicesBlock[i]; - subset0[r]++; - int count = subset0[r]; - if (count > subset0[max0Idx]) - { - max0Idx = r; - } - } - else if (partitionTable[i] == 1) - { - int r = reducedIndicesBlock[i]; - subset1[r]++; - int count = subset1[r]; - if (count > subset1[max1Idx]) - { - max1Idx = r; - } - } - else - { - int r = reducedIndicesBlock[i]; - subset2[r]++; - int count = subset2[r]; - if (count > subset2[max2Idx]) - { - max2Idx = r; - } - } - } - - // Calculate error by counting as error everything that does not match the largest cluster - for (int i = 0; i < 16; i++) - { - if (partitionTable[i] == 0) - { - if (reducedIndicesBlock[i] != max0Idx) error++; - } - else if (partitionTable[i] == 1) - { - if (reducedIndicesBlock[i] != max1Idx) error++; - } - else - { - if (reducedIndicesBlock[i] != max2Idx) error++; - } - } - - return error; - } - - for (int i = 0; i < 64; i++) // Loop through all possible indices - { - int error = CalculatePartitionError(i); - if (first) - { - bestError = error; - bestPartition = i; - first = false; - } - else if (error < bestError) - { - bestError = error; - bestPartition = i; - } - // Break early if exact match - if (bestError == 0) - { - break; - } - } - - return bestPartition; - } - - public static int[] Rank3SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, int numDistinctClusters) - { - int[] output = Enumerable.Range(0, 64).ToArray(); - - int CalculatePartitionError(int partitionIndex) - { - int error = 0; - ReadOnlySpan partitionTable = Bc7Block.Subsets3PartitionTable[partitionIndex]; - - Span subset0 = stackalloc int[numDistinctClusters]; - Span subset1 = stackalloc int[numDistinctClusters]; - Span subset2 = stackalloc int[numDistinctClusters]; - int max0Idx = 0; - int max1Idx = 0; - int max2Idx = 0; - - //Calculate largest cluster index for each subset - for (int i = 0; i < 16; i++) - { - if (partitionTable[i] == 0) - { - int r = reducedIndicesBlock[i]; - subset0[r]++; - int count = subset0[r]; - if (count > subset0[max0Idx]) - { - max0Idx = r; - } - } - else if (partitionTable[i] == 1) - { - int r = reducedIndicesBlock[i]; - subset1[r]++; - int count = subset1[r]; - if (count > subset1[max1Idx]) - { - max1Idx = r; - } - } - else - { - int r = reducedIndicesBlock[i]; - subset2[r]++; - int count = subset2[r]; - if (count > subset2[max2Idx]) - { - max2Idx = r; - } - } - } - - // Calculate error by counting as error everything that does not match the largest cluster - for (int i = 0; i < 16; i++) - { - if (partitionTable[i] == 0) - { - if (reducedIndicesBlock[i] != max0Idx) error++; - } - else if (partitionTable[i] == 1) - { - if (reducedIndicesBlock[i] != max1Idx) error++; - } - else - { - if (reducedIndicesBlock[i] != max2Idx) error++; - } - } - - return error; - } - - output = output.OrderBy(CalculatePartitionError).ToArray(); - - return output; - } - public static void GetInitialUnscaledEndpoints(RawBlock4X4Rgba32 block, out ColorRgba32 ep0, out ColorRgba32 ep1) @@ -627,8 +224,8 @@ public static void GetInitialUnscaledEndpointsForSubset(RawBlock4X4Rgba32 block, var originalPixels = block.AsSpan; - int count = 0; - for (int i = 0; i < 16; i++) + var count = 0; + for (var i = 0; i < 16; i++) { if (partitionTable[i] == subsetIndex) { @@ -636,9 +233,9 @@ public static void GetInitialUnscaledEndpointsForSubset(RawBlock4X4Rgba32 block, } } - Span subsetColors = stackalloc Rgba32[count]; - int next = 0; - for (int i = 0; i < 16; i++) + Span subsetColors = stackalloc ColorRgba32[count]; + var next = 0; + for (var i = 0; i < 16; i++) { if (partitionTable[i] == subsetIndex) { @@ -655,8 +252,8 @@ public static void GetInitialUnscaledEndpointsForSubset(RawBlock4X4Rgba32 block, public static ColorRgba32 ScaleDownEndpoint(ColorRgba32 endpoint, Bc7BlockType type, bool ignoreAlpha, out byte pBit) { - int colorPrecision = GetColorComponentPrecisionWithPBit(type); - int alphaPrecision = GetAlphaComponentPrecisionWithPBit(type); + var colorPrecision = GetColorComponentPrecisionWithPBit(type); + var alphaPrecision = GetAlphaComponentPrecisionWithPBit(type); var r = (byte)(endpoint.r >> (8 - colorPrecision)); var g = (byte)(endpoint.g >> (8 - colorPrecision)); @@ -665,14 +262,14 @@ public static ColorRgba32 ScaleDownEndpoint(ColorRgba32 endpoint, Bc7BlockType t if (TypeHasPBits(type)) { - int pBitVotingMask = (1 << (8 - colorPrecision + 1)) - 1; + var pBitVotingMask = (1 << (8 - colorPrecision + 1)) - 1; float pBitVotes = 0; pBitVotes += endpoint.r & pBitVotingMask; pBitVotes += endpoint.g & pBitVotingMask; pBitVotes += endpoint.b & pBitVotingMask; pBitVotes /= 3; - if (pBitVotes >= (pBitVotingMask / 2f)) + if (pBitVotes >= pBitVotingMask / 2f) { pBit = 1; } @@ -705,26 +302,11 @@ public static ColorRgba32 InterpolateColor(ColorRgba32 endPointStart, ColorRgba3 int colorIndex, int alphaIndex, int colorBitCount, int alphaBitCount) { - byte InterpolateByte(byte e0, byte e1, int index, int indexPrecision) - { - if (indexPrecision == 0) return e0; - ReadOnlySpan aWeights2 = Bc7Block.colorInterpolationWeights2; - ReadOnlySpan aWeights3 = Bc7Block.colorInterpolationWeights3; - ReadOnlySpan aWeights4 = Bc7Block.colorInterpolationWeights4; - - if(indexPrecision == 2) - return (byte) (((64 - aWeights2[index])* (e0) + aWeights2[index]*(e1) + 32) >> 6); - else if(indexPrecision == 3) - return (byte) (((64 - aWeights3[index])*(e0) + aWeights3[index]*(e1) + 32) >> 6); - else // indexprecision == 4 - return (byte) (((64 - aWeights4[index])*(e0) + aWeights4[index]*(e1) + 32) >> 6); - } - - ColorRgba32 result = new ColorRgba32( - InterpolateByte(endPointStart.r, endPointEnd.r, colorIndex, colorBitCount), - InterpolateByte(endPointStart.g, endPointEnd.g, colorIndex, colorBitCount), - InterpolateByte(endPointStart.b, endPointEnd.b, colorIndex, colorBitCount), - InterpolateByte(endPointStart.a, endPointEnd.a, alphaIndex, alphaBitCount) + var result = new ColorRgba32( + BptcEncodingHelpers.InterpolateByte(endPointStart.r, endPointEnd.r, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.g, endPointEnd.g, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.b, endPointEnd.b, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.a, endPointEnd.a, alphaIndex, alphaBitCount) ); return result; @@ -741,10 +323,10 @@ public static void ClampEndpoint(ref ColorRgba32 endpoint, byte colorMax, byte a private static int FindClosestColorIndex(ColorYCbCrAlpha color, ReadOnlySpan colors, out float bestError) { bestError = color.CalcDistWeighted(colors[0], 4, 2); - int bestIndex = 0; - for (int i = 1; i < colors.Length; i++) + var bestIndex = 0; + for (var i = 1; i < colors.Length; i++) { - float error = color.CalcDistWeighted(colors[i], 4, 2); + var error = color.CalcDistWeighted(colors[i], 4, 2); if (error < bestError) { bestIndex = i; @@ -757,10 +339,10 @@ private static int FindClosestColorIndex(ColorYCbCrAlpha color, ReadOnlySpan colors, out float bestError) { bestError = color.CalcDistWeighted(colors[0], 4); - int bestIndex = 0; - for (int i = 1; i < colors.Length; i++) + var bestIndex = 0; + for (var i = 1; i < colors.Length; i++) { - float error = color.CalcDistWeighted(colors[i], 4); + var error = color.CalcDistWeighted(colors[i], 4); if (error < bestError) { bestIndex = i; @@ -777,8 +359,8 @@ private static int FindClosestColorIndex(ColorYCbCr color, ReadOnlySpan alphas, out float bestError) { bestError = (alpha - alphas[0]) * (alpha - alphas[0]); - int bestIndex = 0; - for (int i = 1; i < alphas.Length; i++) + var bestIndex = 0; + for (var i = 1; i < alphas.Length; i++) { float error = (alpha - alphas[i]) * (alpha - alphas[i]); if (error < bestError) @@ -799,21 +381,21 @@ private static int FindClosestAlphaIndex(byte alpha, ReadOnlySpan alphas, private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw, ColorRgba32 ep0, ColorRgba32 ep1, ReadOnlySpan partitionTable, int subsetIndex, int type4IdxMode) { - int colorIndexPrecision = GetColorIndexBitCount(type, type4IdxMode); - int alphaIndexPrecision = GetAlphaIndexBitCount(type, type4IdxMode); + var colorIndexPrecision = GetColorIndexBitCount(type, type4IdxMode); + var alphaIndexPrecision = GetAlphaIndexBitCount(type, type4IdxMode); if (type == Bc7BlockType.Type4 || type == Bc7BlockType.Type5) { //separate indices for color and alpha Span colors = stackalloc ColorYCbCr[1 << colorIndexPrecision]; Span alphas = stackalloc byte[1 << alphaIndexPrecision]; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { colors[i] = new ColorYCbCr(InterpolateColor(ep0, ep1, i, 0, colorIndexPrecision, 0)); } - for (int i = 0; i < alphas.Length; i++) + for (var i = 0; i < alphas.Length; i++) { alphas[i] = InterpolateColor(ep0, ep1, 0, i, 0, alphaIndexPrecision).a; @@ -822,12 +404,12 @@ private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw var pixels = raw.AsSpan; float error = 0; - for (int i = 0; i < 16; i++) + for (var i = 0; i < 16; i++) { var pixelColor = new ColorYCbCr(pixels[i]); FindClosestColorIndex(pixelColor, colors, out var ce); - FindClosestAlphaIndex(pixels[i].A, alphas, out var ae); + FindClosestAlphaIndex(pixels[i].a, alphas, out var ae); error += ce + ae; } @@ -837,7 +419,7 @@ private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw else { Span colors = stackalloc ColorYCbCrAlpha[1 << colorIndexPrecision]; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { colors[i] = new ColorYCbCrAlpha(InterpolateColor(ep0, ep1, i, i, colorIndexPrecision, alphaIndexPrecision)); @@ -847,7 +429,7 @@ private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw float error = 0; float count = 0; - for (int i = 0; i < 16; i++) + for (var i = 0; i < 16; i++) { if (partitionTable[i] == subsetIndex) { @@ -868,8 +450,8 @@ private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw public static void FillSubsetIndices(Bc7BlockType type, RawBlock4X4Rgba32 raw, ColorRgba32 ep0, ColorRgba32 ep1, ReadOnlySpan partitionTable, int subsetIndex, Span indicesToFill) { - int colorIndexPrecision = GetColorIndexBitCount(type); - int alphaIndexPrecision = GetAlphaIndexBitCount(type); + var colorIndexPrecision = GetColorIndexBitCount(type); + var alphaIndexPrecision = GetAlphaIndexBitCount(type); if (type == Bc7BlockType.Type4 || type == Bc7BlockType.Type5) { //separate indices for color and alpha @@ -878,7 +460,7 @@ public static void FillSubsetIndices(Bc7BlockType type, RawBlock4X4Rgba32 raw, C else { Span colors = stackalloc ColorYCbCrAlpha[1 << colorIndexPrecision]; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { colors[i] = new ColorYCbCrAlpha(InterpolateColor(ep0, ep1, i, i, colorIndexPrecision, alphaIndexPrecision)); @@ -886,7 +468,7 @@ public static void FillSubsetIndices(Bc7BlockType type, RawBlock4X4Rgba32 raw, C var pixels = raw.AsSpan; - for (int i = 0; i < 16; i++) + for (var i = 0; i < 16; i++) { if (partitionTable[i] == subsetIndex) { @@ -905,21 +487,21 @@ public static void FillSubsetIndices(Bc7BlockType type, RawBlock4X4Rgba32 raw, C public static void FillAlphaColorIndices(Bc7BlockType type, RawBlock4X4Rgba32 raw, ColorRgba32 ep0, ColorRgba32 ep1, Span colorIndicesToFill, Span alphaIndicesToFill, int idxMode = 0) { - int colorIndexPrecision = GetColorIndexBitCount(type, idxMode); - int alphaIndexPrecision = GetAlphaIndexBitCount(type, idxMode); + var colorIndexPrecision = GetColorIndexBitCount(type, idxMode); + var alphaIndexPrecision = GetAlphaIndexBitCount(type, idxMode); if (type == Bc7BlockType.Type4 || type == Bc7BlockType.Type5) { Span colors = stackalloc ColorYCbCr[1 << colorIndexPrecision]; Span alphas = stackalloc byte[1 << alphaIndexPrecision]; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { colors[i] = new ColorYCbCr(InterpolateColor(ep0, ep1, i, 0, colorIndexPrecision, 0)); } - for (int i = 0; i < alphas.Length; i++) + for (var i = 0; i < alphas.Length; i++) { alphas[i] = InterpolateColor(ep0, ep1, 0, i, 0, alphaIndexPrecision).a; @@ -927,14 +509,14 @@ public static void FillAlphaColorIndices(Bc7BlockType type, RawBlock4X4Rgba32 ra var pixels = raw.AsSpan; - for (int i = 0; i < 16; i++) + for (var i = 0; i < 16; i++) { var pixelColor = new ColorYCbCr(pixels[i]); - var index = FindClosestColorIndex(pixelColor, colors, out var e); + var index = FindClosestColorIndex(pixelColor, colors, out _); colorIndicesToFill[i] = (byte)index; - index = FindClosestAlphaIndex(pixels[i].A, alphas, out var _); + index = FindClosestAlphaIndex(pixels[i].a, alphas, out _); alphaIndicesToFill[i] = (byte)index; } } @@ -948,51 +530,51 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X int variation, ReadOnlySpan partitionTable, int subsetIndex, bool variatePBits, bool variateAlpha, int type4IdxMode = 0) { - byte colorMax = (byte)((1 << GetColorComponentPrecision(type)) - 1); - byte alphaMax = (byte)((1 << GetAlphaComponentPrecision(type)) - 1); + var colorMax = (byte)((1 << GetColorComponentPrecision(type)) - 1); + var alphaMax = (byte)((1 << GetAlphaComponentPrecision(type)) - 1); - float bestError = TrySubsetEndpoints(type, raw, + var bestError = TrySubsetEndpoints(type, raw, ExpandEndpoint(type, ep0, pBit0), ExpandEndpoint(type, ep1, pBit1), partitionTable, subsetIndex, type4IdxMode ); - ReadOnlySpan varPatternR = variateAlpha + ReadOnlySpan patternR = variateAlpha ? varPatternRAlpha : varPatternRNoAlpha; - ReadOnlySpan varPatternG = variateAlpha + ReadOnlySpan patternG = variateAlpha ? varPatternGAlpha : varPatternGNoAlpha; - ReadOnlySpan varPatternB = variateAlpha + ReadOnlySpan patternB = variateAlpha ? varPatternBAlpha : varPatternBNoAlpha; - ReadOnlySpan varPatternA = variateAlpha + ReadOnlySpan patternA = variateAlpha ? varPatternAAlpha : varPatternANoAlpha; while (variation > 0) { - bool foundBetter = false; + var foundBetter = false; - for (int i = 0; i < varPatternR.Length; i++) + for (var i = 0; i < patternR.Length; i++) { - ColorRgba32 testEndPoint0 = new ColorRgba32( - (byte)(ep0.r - variation * varPatternR[i]), - (byte)(ep0.g - variation * varPatternG[i]), - (byte)(ep0.b - variation * varPatternB[i]), - (byte)(ep0.a - variation * varPatternA[i]) + var testEndPoint0 = new ColorRgba32( + (byte)(ep0.r - variation * patternR[i]), + (byte)(ep0.g - variation * patternG[i]), + (byte)(ep0.b - variation * patternB[i]), + (byte)(ep0.a - variation * patternA[i]) ); - ColorRgba32 testEndPoint1 = new ColorRgba32( - (byte)(ep1.r + variation * varPatternR[i]), - (byte)(ep1.g + variation * varPatternG[i]), - (byte)(ep1.b + variation * varPatternB[i]), - (byte)(ep1.a + variation * varPatternA[i]) + var testEndPoint1 = new ColorRgba32( + (byte)(ep1.r + variation * patternR[i]), + (byte)(ep1.g + variation * patternG[i]), + (byte)(ep1.b + variation * patternB[i]), + (byte)(ep1.a + variation * patternA[i]) ); ClampEndpoint(ref testEndPoint0, colorMax, alphaMax); ClampEndpoint(ref testEndPoint1, colorMax, alphaMax); - float error = TrySubsetEndpoints(type, raw, + var error = TrySubsetEndpoints(type, raw, ExpandEndpoint(type, testEndPoint0, pBit0), ExpandEndpoint(type, testEndPoint1, pBit1), partitionTable, subsetIndex, type4IdxMode ); @@ -1005,17 +587,17 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X } } - for (int i = 0; i < varPatternR.Length; i++) + for (var i = 0; i < patternR.Length; i++) { - ColorRgba32 testEndPoint0 = new ColorRgba32( - (byte)(ep0.r + variation * varPatternR[i]), - (byte)(ep0.g + variation * varPatternG[i]), - (byte)(ep0.b + variation * varPatternB[i]), - (byte)(ep0.a + variation * varPatternA[i]) + var testEndPoint0 = new ColorRgba32( + (byte)(ep0.r + variation * patternR[i]), + (byte)(ep0.g + variation * patternG[i]), + (byte)(ep0.b + variation * patternB[i]), + (byte)(ep0.a + variation * patternA[i]) ); ClampEndpoint(ref testEndPoint0, colorMax, alphaMax); - float error = TrySubsetEndpoints(type, raw, + var error = TrySubsetEndpoints(type, raw, ExpandEndpoint(type, testEndPoint0, pBit0), ExpandEndpoint(type, ep1, pBit1), partitionTable, subsetIndex, type4IdxMode ); @@ -1027,17 +609,17 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X } } - for (int i = 0; i < varPatternR.Length; i++) + for (var i = 0; i < patternR.Length; i++) { - ColorRgba32 testEndPoint1 = new ColorRgba32( - (byte)(ep1.r + variation * varPatternR[i]), - (byte)(ep1.g + variation * varPatternG[i]), - (byte)(ep1.b + variation * varPatternB[i]), - (byte)(ep1.a + variation * varPatternA[i]) + var testEndPoint1 = new ColorRgba32( + (byte)(ep1.r + variation * patternR[i]), + (byte)(ep1.g + variation * patternG[i]), + (byte)(ep1.b + variation * patternB[i]), + (byte)(ep1.a + variation * patternA[i]) ); ClampEndpoint(ref testEndPoint1, colorMax, alphaMax); - float error = TrySubsetEndpoints(type, raw, + var error = TrySubsetEndpoints(type, raw, ExpandEndpoint(type, ep0, pBit0), ExpandEndpoint(type, testEndPoint1, pBit1), partitionTable, subsetIndex, type4IdxMode ); @@ -1052,8 +634,8 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X if (variatePBits) { { - byte testPBit0 = pBit0 == 0 ? (byte)1 : (byte)0; - float error = TrySubsetEndpoints(type, raw, + var testPBit0 = pBit0 == 0 ? (byte)1 : (byte)0; + var error = TrySubsetEndpoints(type, raw, ExpandEndpoint(type, ep0, testPBit0), ExpandEndpoint(type, ep1, pBit1), partitionTable, subsetIndex, type4IdxMode ); @@ -1065,8 +647,8 @@ public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X } } { - byte testPBit1 = pBit1 == 0 ? (byte)1 : (byte)0; - float error = TrySubsetEndpoints(type, raw, + var testPBit1 = pBit1 == 0 ? (byte)1 : (byte)0; + var error = TrySubsetEndpoints(type, raw, ExpandEndpoint(type, ep0, pBit0), ExpandEndpoint(type, ep1, testPBit1), partitionTable, subsetIndex, type4IdxMode ); @@ -1092,22 +674,22 @@ public static RawBlock4X4Rgba32 RotateBlockColors(RawBlock4X4Rgba32 block, int r return block; } - RawBlock4X4Rgba32 rotated = new RawBlock4X4Rgba32(); + var rotated = new RawBlock4X4Rgba32(); var pixels = block.AsSpan; var output = rotated.AsSpan; - for (int i = 0; i < 16; i++) + for (var i = 0; i < 16; i++) { var c = pixels[i]; switch (rotation) { case 1: - output[i] = new Rgba32(c.A, c.G, c.B, c.R); + output[i] = new ColorRgba32(c.a, c.g, c.b, c.r); break; case 2: - output[i] = new Rgba32(c.R, c.A, c.B, c.G); + output[i] = new ColorRgba32(c.r, c.a, c.b, c.g); break; case 3: - output[i] = new Rgba32(c.R, c.G, c.A, c.B); + output[i] = new ColorRgba32(c.r, c.g, c.a, c.b); break; } } diff --git a/BCnEnc.Net/Encoder/Bc7/Bc7Mode0Encoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc7Mode0Encoder.cs similarity index 82% rename from BCnEnc.Net/Encoder/Bc7/Bc7Mode0Encoder.cs rename to BCnEnc.Net/Encoder/Bptc/Bc7Mode0Encoder.cs index 1469173..4b51330 100644 --- a/BCnEnc.Net/Encoder/Bc7/Bc7Mode0Encoder.cs +++ b/BCnEnc.Net/Encoder/Bptc/Bc7Mode0Encoder.cs @@ -1,14 +1,14 @@ -using System; +using System; using BCnEncoder.Shared; -namespace BCnEncoder.Encoder.Bc7 +namespace BCnEncoder.Encoder.Bptc { internal static class Bc7Mode0Encoder { public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation, int bestPartition) { - Bc7Block output = new Bc7Block(); + var output = new Bc7Block(); const Bc7BlockType type = Bc7BlockType.Type0; if(bestPartition >= 16) @@ -16,26 +16,26 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariatio throw new IndexOutOfRangeException("Mode0 only has 16 partitions"); } - ColorRgba32[] endpoints = new ColorRgba32[6]; - byte[] pBits = new byte[6]; + var endpoints = new ColorRgba32[6]; + var pBits = new byte[6]; ReadOnlySpan partitionTable = Bc7Block.Subsets3PartitionTable[bestPartition]; - byte[] indices = new byte[16]; + var indices = new byte[16]; - int[] anchorIndices = new int[] { + var anchorIndices = new int[] { 0, Bc7Block.Subsets3AnchorIndices2[bestPartition], Bc7Block.Subsets3AnchorIndices3[bestPartition] }; - for (int subset = 0; subset < 3; subset++) { + for (var subset = 0; subset < 3; subset++) { Bc7EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, partitionTable, subset); - ColorRgba32 scaledEp0 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out byte pBit0); - ColorRgba32 scaledEp1 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out byte pBit1); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out var pBit0); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out var pBit1); Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, block, ref scaledEp0, ref scaledEp1, ref pBit0, ref pBit1, startingVariation, partitionTable, subset, true, false); diff --git a/BCnEnc.Net/Encoder/Bc7/Bc7Mode1Encoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc7Mode1Encoder.cs similarity index 82% rename from BCnEnc.Net/Encoder/Bc7/Bc7Mode1Encoder.cs rename to BCnEnc.Net/Encoder/Bptc/Bc7Mode1Encoder.cs index e8d9a51..c51c0a0 100644 --- a/BCnEnc.Net/Encoder/Bc7/Bc7Mode1Encoder.cs +++ b/BCnEnc.Net/Encoder/Bptc/Bc7Mode1Encoder.cs @@ -1,35 +1,35 @@ -using System; +using System; using BCnEncoder.Shared; -namespace BCnEncoder.Encoder.Bc7 +namespace BCnEncoder.Encoder.Bptc { internal static class Bc7Mode1Encoder { public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation, int bestPartition) { - Bc7Block output = new Bc7Block(); + var output = new Bc7Block(); const Bc7BlockType type = Bc7BlockType.Type1; - ColorRgba32[] endpoints = new ColorRgba32[4]; - byte[] pBits = new byte[2]; + var endpoints = new ColorRgba32[4]; + var pBits = new byte[2]; ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[bestPartition]; - byte[] indices = new byte[16]; + var indices = new byte[16]; - int[] anchorIndices = new int[] { + var anchorIndices = new int[] { 0, Bc7Block.Subsets2AnchorIndices[bestPartition] }; - for (int subset = 0; subset < 2; subset++) { + for (var subset = 0; subset < 2; subset++) { Bc7EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, partitionTable, subset); - ColorRgba32 scaledEp0 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out byte pBit); - ColorRgba32 scaledEp1 = + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out var pBit); + var scaledEp1 = Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out pBit); Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, block, ref scaledEp0, diff --git a/BCnEnc.Net/Encoder/Bc7/Bc7Mode2Encoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc7Mode2Encoder.cs similarity index 81% rename from BCnEnc.Net/Encoder/Bc7/Bc7Mode2Encoder.cs rename to BCnEnc.Net/Encoder/Bptc/Bc7Mode2Encoder.cs index dee4405..24cfbcf 100644 --- a/BCnEnc.Net/Encoder/Bc7/Bc7Mode2Encoder.cs +++ b/BCnEnc.Net/Encoder/Bptc/Bc7Mode2Encoder.cs @@ -1,35 +1,35 @@ -using System; +using System; using BCnEncoder.Shared; -namespace BCnEncoder.Encoder.Bc7 +namespace BCnEncoder.Encoder.Bptc { internal static class Bc7Mode2Encoder { public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation, int bestPartition) { - Bc7Block output = new Bc7Block(); + var output = new Bc7Block(); const Bc7BlockType type = Bc7BlockType.Type2; - ColorRgba32[] endpoints = new ColorRgba32[6]; + var endpoints = new ColorRgba32[6]; ReadOnlySpan partitionTable = Bc7Block.Subsets3PartitionTable[bestPartition]; - byte[] indices = new byte[16]; + var indices = new byte[16]; - int[] anchorIndices = new int[] { + var anchorIndices = new int[] { 0, Bc7Block.Subsets3AnchorIndices2[bestPartition], Bc7Block.Subsets3AnchorIndices3[bestPartition] }; - for (int subset = 0; subset < 3; subset++) { + for (var subset = 0; subset < 3; subset++) { Bc7EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, partitionTable, subset); - ColorRgba32 scaledEp0 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out byte _); - ColorRgba32 scaledEp1 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out byte _); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out var _); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out var _); byte pBit = 0; Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, block, ref scaledEp0, diff --git a/BCnEnc.Net/Encoder/Bc7/Bc7Mode3Encoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc7Mode3Encoder.cs similarity index 80% rename from BCnEnc.Net/Encoder/Bc7/Bc7Mode3Encoder.cs rename to BCnEnc.Net/Encoder/Bptc/Bc7Mode3Encoder.cs index 61dd40b..5bbed25 100644 --- a/BCnEnc.Net/Encoder/Bc7/Bc7Mode3Encoder.cs +++ b/BCnEnc.Net/Encoder/Bptc/Bc7Mode3Encoder.cs @@ -1,35 +1,35 @@ -using System; +using System; using BCnEncoder.Shared; -namespace BCnEncoder.Encoder.Bc7 +namespace BCnEncoder.Encoder.Bptc { internal static class Bc7Mode3Encoder { public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation, int bestPartition) { - Bc7Block output = new Bc7Block(); + var output = new Bc7Block(); const Bc7BlockType type = Bc7BlockType.Type3; - ColorRgba32[] endpoints = new ColorRgba32[4]; - byte[] pBits = new byte[4]; + var endpoints = new ColorRgba32[4]; + var pBits = new byte[4]; ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[bestPartition]; - byte[] indices = new byte[16]; + var indices = new byte[16]; - int[] anchorIndices = new int[] { + var anchorIndices = new int[] { 0, Bc7Block.Subsets2AnchorIndices[bestPartition] }; - for (int subset = 0; subset < 2; subset++) { + for (var subset = 0; subset < 2; subset++) { Bc7EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, partitionTable, subset); - ColorRgba32 scaledEp0 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out byte pBit0); - ColorRgba32 scaledEp1 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out byte pBit1); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out var pBit0); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out var pBit1); Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, block, ref scaledEp0, ref scaledEp1, ref pBit0, ref pBit1, startingVariation, partitionTable, subset, true, false); diff --git a/BCnEnc.Net/Encoder/Bc7/Bc7Mode4Encoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc7Mode4Encoder.cs similarity index 80% rename from BCnEnc.Net/Encoder/Bc7/Bc7Mode4Encoder.cs rename to BCnEnc.Net/Encoder/Bptc/Bc7Mode4Encoder.cs index c25df51..19b8718 100644 --- a/BCnEnc.Net/Encoder/Bc7/Bc7Mode4Encoder.cs +++ b/BCnEnc.Net/Encoder/Bptc/Bc7Mode4Encoder.cs @@ -1,13 +1,13 @@ -using System; +using System; using BCnEncoder.Shared; -namespace BCnEncoder.Encoder.Bc7 +namespace BCnEncoder.Encoder.Bptc { internal static class Bc7Mode4Encoder { - private static ReadOnlySpan partitionTable => new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - const int subset = 0; + private static ReadOnlySpan PartitionTable => new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + const int Subset = 0; public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation) { @@ -15,36 +15,36 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariatio Span outputs = stackalloc Bc7Block[8]; - for (int idxMode = 0; idxMode < 2; idxMode++) + for (var idxMode = 0; idxMode < 2; idxMode++) { - for (int rotation = 0; rotation < 4; rotation++) + for (var rotation = 0; rotation < 4; rotation++) { var rotatedBlock = Bc7EncodingHelpers.RotateBlockColors(block, rotation); - Bc7Block output = new Bc7Block(); + var output = new Bc7Block(); Bc7EncodingHelpers.GetInitialUnscaledEndpoints(rotatedBlock, out var ep0, out var ep1); - ColorRgba32 scaledEp0 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, false, out byte _); - ColorRgba32 scaledEp1 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, false, out byte _); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, false, out var _); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, false, out var _); byte pBit = 0; //fake pBit Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, rotatedBlock, ref scaledEp0, - ref scaledEp1, ref pBit, ref pBit, startingVariation, partitionTable, subset, + ref scaledEp1, ref pBit, ref pBit, startingVariation, PartitionTable, Subset, false, true, idxMode); ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, 0); ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, 0); - byte[] colorIndices = new byte[16]; - byte[] alphaIndices = new byte[16]; + var colorIndices = new byte[16]; + var alphaIndices = new byte[16]; Bc7EncodingHelpers.FillAlphaColorIndices(type, rotatedBlock, ep0, ep1, colorIndices, alphaIndices, idxMode); - bool needsRedo = false; + var needsRedo = false; if ((colorIndices[0] & (idxMode == 0 ? 0b10 : 0b100)) > 0) //If anchor index most significant bit is 1, switch endpoints @@ -105,16 +105,16 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariatio } } - int bestIndex = 0; + var bestIndex = 0; float bestError = 0; - bool first = true; + var first = true; // Find best out of generated blocks - for (int i = 0; i < outputs.Length; i++) + for (var i = 0; i < outputs.Length; i++) { var decoded = outputs[i].Decode(); - float error = block.CalculateYCbCrAlphaError(decoded); + var error = block.CalculateYCbCrAlphaError(decoded); if (error < bestError || first) { first = false; diff --git a/BCnEnc.Net/Encoder/Bc7/Bc7Mode5Encoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc7Mode5Encoder.cs similarity index 78% rename from BCnEnc.Net/Encoder/Bc7/Bc7Mode5Encoder.cs rename to BCnEnc.Net/Encoder/Bptc/Bc7Mode5Encoder.cs index 1b5c881..8ee9456 100644 --- a/BCnEnc.Net/Encoder/Bc7/Bc7Mode5Encoder.cs +++ b/BCnEnc.Net/Encoder/Bptc/Bc7Mode5Encoder.cs @@ -1,12 +1,12 @@ -using System; +using System; using BCnEncoder.Shared; -namespace BCnEncoder.Encoder.Bc7 +namespace BCnEncoder.Encoder.Bptc { internal static class Bc7Mode5Encoder { - private static ReadOnlySpan partitionTable => new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - const int subset = 0; + private static ReadOnlySpan PartitionTable => new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + const int Subset = 0; public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation) { @@ -14,32 +14,32 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariatio Span outputs = stackalloc Bc7Block[4]; - for (int rotation = 0; rotation < 4; rotation++) { + for (var rotation = 0; rotation < 4; rotation++) { var rotatedBlock = Bc7EncodingHelpers.RotateBlockColors(block, rotation); - Bc7Block output = new Bc7Block(); + var output = new Bc7Block(); Bc7EncodingHelpers.GetInitialUnscaledEndpoints(rotatedBlock, out var ep0, out var ep1); - ColorRgba32 scaledEp0 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, false, out byte _); - ColorRgba32 scaledEp1 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, false, out byte _); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, false, out var _); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, false, out var _); byte pBit = 0; //fake pBit Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, rotatedBlock, ref scaledEp0, - ref scaledEp1, ref pBit, ref pBit, startingVariation, partitionTable, subset, false, true); + ref scaledEp1, ref pBit, ref pBit, startingVariation, PartitionTable, Subset, false, true); ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, 0); ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, 0); - byte[] colorIndices = new byte[16]; - byte[] alphaIndices = new byte[16]; + var colorIndices = new byte[16]; + var alphaIndices = new byte[16]; Bc7EncodingHelpers.FillAlphaColorIndices(type, rotatedBlock, ep0, ep1, colorIndices, alphaIndices); - bool needsRedo = false; + var needsRedo = false; if ((colorIndices[0] & 0b10) > 0) //If anchor index most significant bit is 1, switch endpoints { @@ -85,15 +85,15 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariatio outputs[rotation] = output; } - int bestIndex = 0; + var bestIndex = 0; float bestError = 0; - bool first = true; + var first = true; // Find best out of generated blocks - for (int i = 0; i < outputs.Length; i++) { + for (var i = 0; i < outputs.Length; i++) { var decoded = outputs[i].Decode(); - float error = block.CalculateYCbCrAlphaError(decoded); + var error = block.CalculateYCbCrAlphaError(decoded); if(error < bestError || first) { first = false; bestError = error; diff --git a/BCnEnc.Net/Encoder/Bc7/Bc7Mode6Encoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc7Mode6Encoder.cs similarity index 87% rename from BCnEnc.Net/Encoder/Bc7/Bc7Mode6Encoder.cs rename to BCnEnc.Net/Encoder/Bptc/Bc7Mode6Encoder.cs index 844a51d..4765482 100644 --- a/BCnEnc.Net/Encoder/Bc7/Bc7Mode6Encoder.cs +++ b/BCnEnc.Net/Encoder/Bptc/Bc7Mode6Encoder.cs @@ -1,22 +1,22 @@ -using System; +using System; using BCnEncoder.Shared; -namespace BCnEncoder.Encoder.Bc7 +namespace BCnEncoder.Encoder.Bptc { internal static class Bc7Mode6Encoder { public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation) { - bool hasAlpha = block.HasTransparentPixels(); + var hasAlpha = block.HasTransparentPixels(); - Bc7Block output = new Bc7Block(); + var output = new Bc7Block(); Bc7EncodingHelpers.GetInitialUnscaledEndpoints(block, out var ep0, out var ep1); - ColorRgba32 scaledEp0 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep0, Bc7BlockType.Type6, false, out byte pBit0); - ColorRgba32 scaledEp1 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep1, Bc7BlockType.Type6, false, out byte pBit1); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, Bc7BlockType.Type6, false, out var pBit0); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, Bc7BlockType.Type6, false, out var pBit1); ReadOnlySpan partitionTable = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; const int subset = 0; @@ -33,7 +33,7 @@ public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariatio ep0 = Bc7EncodingHelpers.ExpandEndpoint(Bc7BlockType.Type6, scaledEp0, pBit0); ep1 = Bc7EncodingHelpers.ExpandEndpoint(Bc7BlockType.Type6, scaledEp1, pBit1); - byte[] indices = new byte[16]; + var indices = new byte[16]; Bc7EncodingHelpers.FillSubsetIndices(Bc7BlockType.Type6, block, ep0, ep1, diff --git a/BCnEnc.Net/Encoder/Bc7/Bc7Mode7Encoder.cs b/BCnEnc.Net/Encoder/Bptc/Bc7Mode7Encoder.cs similarity index 85% rename from BCnEnc.Net/Encoder/Bc7/Bc7Mode7Encoder.cs rename to BCnEnc.Net/Encoder/Bptc/Bc7Mode7Encoder.cs index d58bf5e..d3d1e90 100644 --- a/BCnEnc.Net/Encoder/Bc7/Bc7Mode7Encoder.cs +++ b/BCnEnc.Net/Encoder/Bptc/Bc7Mode7Encoder.cs @@ -1,35 +1,35 @@ -using System; +using System; using BCnEncoder.Shared; -namespace BCnEncoder.Encoder.Bc7 +namespace BCnEncoder.Encoder.Bptc { internal static class Bc7Mode7Encoder { public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation, int bestPartition) { - Bc7Block output = new Bc7Block(); + var output = new Bc7Block(); const Bc7BlockType type = Bc7BlockType.Type7; - ColorRgba32[] endpoints = new ColorRgba32[4]; - byte[] pBits = new byte[4]; + var endpoints = new ColorRgba32[4]; + var pBits = new byte[4]; ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[bestPartition]; - byte[] indices = new byte[16]; + var indices = new byte[16]; - int[] anchorIndices = new int[] { + var anchorIndices = new int[] { 0, Bc7Block.Subsets2AnchorIndices[bestPartition] }; - for (int subset = 0; subset < 2; subset++) { + for (var subset = 0; subset < 2; subset++) { Bc7EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, partitionTable, subset); - ColorRgba32 scaledEp0 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, false, out byte pBit0); - ColorRgba32 scaledEp1 = - Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, false, out byte pBit1); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, false, out var pBit0); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, false, out var pBit1); Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, block, ref scaledEp0, ref scaledEp1, ref pBit0, ref pBit1, startingVariation, partitionTable, subset, true, true); diff --git a/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs new file mode 100644 index 0000000..0eb201d --- /dev/null +++ b/BCnEnc.Net/Encoder/Bptc/BptcEncodingHelpers.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using BCnEncoder.Shared; + +#if NETSTANDARD2_0 +using MemoryMarshal = BCnEncoder.Shared.MemoryMarshalPolyfills; +#endif + +namespace BCnEncoder.Encoder.Bptc +{ + internal static class BptcEncodingHelpers + { + private static readonly byte[] ColorInterpolationWeights2 = new byte[] { 0, 21, 43, 64 }; + private static readonly byte[] ColorInterpolationWeights3 = new byte[] { 0, 9, 18, 27, 37, 46, 55, 64 }; + private static readonly byte[] ColorInterpolationWeights4 = new byte[] { 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64 }; + + + public static int InterpolateInt(int e0, int e1, int index, int indexPrecision) + { + if (indexPrecision == 0) return e0; + 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) + return ((64 - aWeights3[index]) * e0 + aWeights3[index] * e1 + 32) >> 6; + else // indexprecision == 4 + return ((64 - aWeights4[index]) * e0 + aWeights4[index] * e1 + 32) >> 6; + } + + public static byte InterpolateByte(byte e0, byte e1, int index, int indexPrecision) + { + if (indexPrecision == 0) return e0; + var aWeights2 = ColorInterpolationWeights2; + var aWeights3 = ColorInterpolationWeights3; + var aWeights4 = ColorInterpolationWeights4; + + if (indexPrecision == 2) + return (byte)(((64 - aWeights2[index]) * e0 + aWeights2[index] * e1 + 32) >> 6); + if (indexPrecision == 3) + return (byte)(((64 - aWeights3[index]) * e0 + aWeights3[index] * e1 + 32) >> 6); + else // indexprecision == 4 + return (byte)(((64 - aWeights4[index]) * e0 + aWeights4[index] * e1 + 32) >> 6); + } + + + + public static int[] Rank2SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, int numDistinctClusters, bool smallIndex = false) + { + var output = Enumerable.Range(0, smallIndex ? 32 : 64).ToArray(); + + // Copy struct to array before the closure so that reducedIndicesBlock is not + // heap-captured. On .NET Framework, MemoryMarshalPolyfills.CreateSpan uses + // Unsafe.AsPointer, which is only GC-safe for stack-allocated structs. + #if NETSTANDARD2_0 + var indices = new int[16]; + reducedIndicesBlock.AsSpan.CopyTo(indices); + #endif + + int CalculatePartitionError(int partitionIndex) + { + #if NETSTANDARD2_1 + var indices = reducedIndicesBlock.AsSpan; + #endif + + var error = 0; + ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[partitionIndex]; + Span subset0 = stackalloc int[numDistinctClusters]; + Span subset1 = stackalloc int[numDistinctClusters]; + var max0Idx = 0; + var max1Idx = 0; + + //Calculate largest cluster index for each subset + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == 0) + { + var r = indices[i]; + subset0[r]++; + var count = subset0[r]; + if (count > subset0[max0Idx]) + { + max0Idx = r; + } + } + else + { + var r = indices[i]; + subset1[r]++; + var count = subset1[r]; + if (count > subset1[max1Idx]) + { + max1Idx = r; + } + } + } + + // Calculate error by counting as error everything that does not match the largest cluster + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == 0) + { + if (indices[i] != max0Idx) error++; + } + else + { + if (indices[i] != max1Idx) error++; + } + } + + return error; + } + + output = output.OrderBy(CalculatePartitionError).ToArray(); + + return output; + } + + public static int[] Rank3SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, int numDistinctClusters) + { + var output = Enumerable.Range(0, 64).ToArray(); + + // Copy struct to array before the closure so that reducedIndicesBlock is not + // heap-captured. On .NET Framework, MemoryMarshalPolyfills.CreateSpan uses + // Unsafe.AsPointer, which is only GC-safe for stack-allocated structs. + #if NETSTANDARD2_0 + var indices = new int[16]; + reducedIndicesBlock.AsSpan.CopyTo(indices); + #endif + + int CalculatePartitionError(int partitionIndex) + { + #if NETSTANDARD2_1 + var indices = reducedIndicesBlock.AsSpan; + #endif + + var error = 0; + ReadOnlySpan partitionTable = Bc7Block.Subsets3PartitionTable[partitionIndex]; + + Span subset0 = stackalloc int[numDistinctClusters]; + Span subset1 = stackalloc int[numDistinctClusters]; + Span subset2 = stackalloc int[numDistinctClusters]; + var max0Idx = 0; + var max1Idx = 0; + var max2Idx = 0; + + //Calculate largest cluster index for each subset + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == 0) + { + var r = indices[i]; + subset0[r]++; + var count = subset0[r]; + if (count > subset0[max0Idx]) + { + max0Idx = r; + } + } + else if (partitionTable[i] == 1) + { + var r = indices[i]; + subset1[r]++; + var count = subset1[r]; + if (count > subset1[max1Idx]) + { + max1Idx = r; + } + } + else + { + var r = indices[i]; + subset2[r]++; + var count = subset2[r]; + if (count > subset2[max2Idx]) + { + max2Idx = r; + } + } + } + + // Calculate error by counting as error everything that does not match the largest cluster + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == 0) + { + if (indices[i] != max0Idx) error++; + } + else if (partitionTable[i] == 1) + { + if (indices[i] != max1Idx) error++; + } + else + { + if (indices[i] != max2Idx) error++; + } + } + + return error; + } + + output = output.OrderBy(CalculatePartitionError).ToArray(); + + return output; + } + } + + internal struct ClusterIndices4X4 + { + public int i00, i10, i20, i30; + public int i01, i11, i21, i31; + public int i02, i12, i22, i32; + public int i03, i13, i23, i33; + + public Span AsSpan => MemoryMarshal.CreateSpan(ref i00, 16); + + public int this[int x, int y] + { + get => AsSpan[x + y * 4]; + set => AsSpan[x + y * 4] = value; + } + + public int this[int index] + { + get => AsSpan[index]; + set => AsSpan[index] = value; + } + + public int NumClusters + { + get + { + var t = AsSpan; + Span clusters = stackalloc int[16]; + var distinct = 0; + for (var i = 0; i < 16; i++) + { + var cluster = t[i]; + var found = false; + for (var j = 0; j < distinct; j++) + { + if (clusters[j] == cluster) + { + found = true; + break; + } + } + if (!found) + { + clusters[distinct] = cluster; + ++distinct; + } + } + return distinct; + } + } + + /// + /// Reduces block down to adjacent cluster indices. For example, + /// block that contains clusters 5, 16 and 77 will become a block that contains clusters 0, 1 and 2 + /// + public ClusterIndices4X4 Reduce(out int numClusters) + { + var result = new ClusterIndices4X4(); + numClusters = NumClusters; + Span mapKey = stackalloc int[numClusters]; + var indices = AsSpan; + var outIndices = result.AsSpan; + var next = 0; + for (var i = 0; i < 16; i++) + { + var cluster = indices[i]; + var found = false; + for (var j = 0; j < next; j++) + { + if (mapKey[j] == cluster) + { + found = true; + outIndices[i] = j; + break; + } + } + if (!found) + { + outIndices[i] = next; + mapKey[next] = cluster; + ++next; + } + } + + return result; + } + } + + + internal struct IndexBlock4x4 + { + public byte i00, i10, i20, i30; + public byte i01, i11, i21, i31; + public byte i02, i12, i22, i32; + public byte i03, i13, i23, i33; + + public Span AsSpan => MemoryMarshal.CreateSpan(ref i00, 16); + + public byte this[int x, int y] + { + get => AsSpan[x + y * 4]; + set => AsSpan[x + y * 4] = value; + } + + public byte this[int index] + { + get => AsSpan[index]; + set => AsSpan[index] = value; + } + } +} diff --git a/BCnEnc.Net/Encoder/ColorChooser.cs b/BCnEnc.Net/Encoder/ColorChooser.cs index ae4ac45..0fbf6ef 100644 --- a/BCnEnc.Net/Encoder/ColorChooser.cs +++ b/BCnEnc.Net/Encoder/ColorChooser.cs @@ -1,99 +1,98 @@ -using System; +using System; using BCnEncoder.Shared; -using SixLabors.ImageSharp.PixelFormats; namespace BCnEncoder.Encoder { internal static class ColorChooser { - public static int ChooseClosestColor4(ReadOnlySpan colors, Rgba32 color, float rWeight, float gWeight, float bWeight, out float error) + public static int ChooseClosestColor4(ReadOnlySpan colors, ColorRgba32 color, float rWeight, float gWeight, float bWeight, out float error) { ReadOnlySpan d = stackalloc float[4] { - MathF.Abs(colors[0].r - color.R) * rWeight - + MathF.Abs(colors[0].g - color.G) * gWeight - + MathF.Abs(colors[0].b - color.B) * bWeight, - MathF.Abs(colors[1].r - color.R) * rWeight - + MathF.Abs(colors[1].g - color.G) * gWeight - + MathF.Abs(colors[1].b - color.B) * bWeight, - MathF.Abs(colors[2].r - color.R) * rWeight - + MathF.Abs(colors[2].g - color.G) * gWeight - + MathF.Abs(colors[2].b - color.B) * bWeight, - MathF.Abs(colors[3].r - color.R) * rWeight - + MathF.Abs(colors[3].g - color.G) * gWeight - + MathF.Abs(colors[3].b - color.B) * bWeight, + MathF.Abs(colors[0].r - color.r) * rWeight + + MathF.Abs(colors[0].g - color.g) * gWeight + + MathF.Abs(colors[0].b - color.b) * bWeight, + MathF.Abs(colors[1].r - color.r) * rWeight + + MathF.Abs(colors[1].g - color.g) * gWeight + + MathF.Abs(colors[1].b - color.b) * bWeight, + MathF.Abs(colors[2].r - color.r) * rWeight + + MathF.Abs(colors[2].g - color.g) * gWeight + + MathF.Abs(colors[2].b - color.b) * bWeight, + MathF.Abs(colors[3].r - color.r) * rWeight + + MathF.Abs(colors[3].g - color.g) * gWeight + + MathF.Abs(colors[3].b - color.b) * bWeight, }; - int b0 = d[0] > d[3] ? 1 : 0; - int b1 = d[1] > d[2] ? 1 : 0; - int b2 = d[0] > d[2] ? 1 : 0; - int b3 = d[1] > d[3] ? 1 : 0; - int b4 = d[2] > d[3] ? 1 : 0; + var b0 = d[0] > d[3] ? 1 : 0; + var b1 = d[1] > d[2] ? 1 : 0; + var b2 = d[0] > d[2] ? 1 : 0; + var b3 = d[1] > d[3] ? 1 : 0; + var b4 = d[2] > d[3] ? 1 : 0; - int x0 = b1 & b2; - int x1 = b0 & b3; - int x2 = b0 & b4; + var x0 = b1 & b2; + var x1 = b0 & b3; + var x2 = b0 & b4; - int idx = (x2 | ((x0 | x1) << 1)); + var idx = x2 | ((x0 | x1) << 1); error = d[idx]; return idx; } - public static int ChooseClosestColor4AlphaCutoff(ReadOnlySpan colors, Rgba32 color, float rWeight, float gWeight, float bWeight, int alphaCutoff, bool hasAlpha, out float error) + public static int ChooseClosestColor4AlphaCutoff(ReadOnlySpan colors, ColorRgba32 color, float rWeight, float gWeight, float bWeight, int alphaCutoff, bool hasAlpha, out float error) { - if (hasAlpha && color.A < alphaCutoff) + if (hasAlpha && color.a < alphaCutoff) { error = 0; return 3; } ReadOnlySpan d = stackalloc float[4] { - MathF.Abs(colors[0].r - color.R) * rWeight - + MathF.Abs(colors[0].g - color.G) * gWeight - + MathF.Abs(colors[0].b - color.B) * bWeight, - MathF.Abs(colors[1].r - color.R) * rWeight - + MathF.Abs(colors[1].g - color.G) * gWeight - + MathF.Abs(colors[1].b - color.B) * bWeight, - MathF.Abs(colors[2].r - color.R) * rWeight - + MathF.Abs(colors[2].g - color.G) * gWeight - + MathF.Abs(colors[2].b - color.B) * bWeight, + MathF.Abs(colors[0].r - color.r) * rWeight + + MathF.Abs(colors[0].g - color.g) * gWeight + + MathF.Abs(colors[0].b - color.b) * bWeight, + MathF.Abs(colors[1].r - color.r) * rWeight + + MathF.Abs(colors[1].g - color.g) * gWeight + + MathF.Abs(colors[1].b - color.b) * bWeight, + MathF.Abs(colors[2].r - color.r) * rWeight + + MathF.Abs(colors[2].g - color.g) * gWeight + + MathF.Abs(colors[2].b - color.b) * bWeight, hasAlpha ? 999 : - MathF.Abs(colors[3].r - color.R) * rWeight - + MathF.Abs(colors[3].g - color.G) * gWeight - + MathF.Abs(colors[3].b - color.B) * bWeight, + MathF.Abs(colors[3].r - color.r) * rWeight + + MathF.Abs(colors[3].g - color.g) * gWeight + + MathF.Abs(colors[3].b - color.b) * bWeight, }; - int b0 = d[0] > d[2] ? 1 : 0; - int b1 = d[1] > d[3] ? 1 : 0; - int b2 = d[0] > d[3] ? 1 : 0; - int b3 = d[1] > d[2] ? 1 : 0; - int nb3 = d[1] > d[2] ? 0 : 1; - int b4 = d[0] > d[1] ? 1 : 0; - int b5 = d[2] > d[3] ? 1 : 0; + var b0 = d[0] > d[2] ? 1 : 0; + var b1 = d[1] > d[3] ? 1 : 0; + var b2 = d[0] > d[3] ? 1 : 0; + var b3 = d[1] > d[2] ? 1 : 0; + var nb3 = d[1] > d[2] ? 0 : 1; + var b4 = d[0] > d[1] ? 1 : 0; + var b5 = d[2] > d[3] ? 1 : 0; - int idx = (nb3 & b4) | (b2 & b5) | (((b0 & b3) | (b1 & b2)) << 1); + var idx = (nb3 & b4) | (b2 & b5) | (((b0 & b3) | (b1 & b2)) << 1); error = d[idx]; return idx; } - public static int ChooseClosestColor(Span colors, Rgba32 color) + public static int ChooseClosestColor(Span colors, ColorRgba32 color) { - int closest = 0; - int closestError = - Math.Abs(colors[0].r - color.R) - + Math.Abs(colors[0].g - color.G) - + Math.Abs(colors[0].b - color.B); + var closest = 0; + var closestError = + Math.Abs(colors[0].r - color.r) + + Math.Abs(colors[0].g - color.g) + + Math.Abs(colors[0].b - color.b); - for (int i = 1; i < colors.Length; i++) + for (var i = 1; i < colors.Length; i++) { - int error = - Math.Abs(colors[i].r - color.R) - + Math.Abs(colors[i].g - color.G) - + Math.Abs(colors[i].b - color.B); + var error = + Math.Abs(colors[i].r - color.r) + + Math.Abs(colors[i].g - color.g) + + Math.Abs(colors[i].b - color.b); if (error < closestError) { closest = i; @@ -103,22 +102,22 @@ public static int ChooseClosestColor(Span colors, Rgba32 color) return closest; } - public static int ChooseClosestColor(Span colors, Rgba32 color) + public static int ChooseClosestColor(Span colors, ColorRgba32 color) { - int closest = 0; - int closestError = - Math.Abs(colors[0].r - color.R) - + Math.Abs(colors[0].g - color.G) - + Math.Abs(colors[0].b - color.B) - + Math.Abs(colors[0].a - color.A); - - for (int i = 1; i < colors.Length; i++) + var closest = 0; + var closestError = + Math.Abs(colors[0].r - color.r) + + Math.Abs(colors[0].g - color.g) + + Math.Abs(colors[0].b - color.b) + + Math.Abs(colors[0].a - color.a); + + for (var i = 1; i < colors.Length; i++) { - int error = - Math.Abs(colors[i].r - color.R) - + Math.Abs(colors[i].g - color.G) - + Math.Abs(colors[i].b - color.B) - + Math.Abs(colors[i].a - color.A); + var error = + Math.Abs(colors[i].r - color.r) + + Math.Abs(colors[i].g - color.g) + + Math.Abs(colors[i].b - color.b) + + Math.Abs(colors[i].a - color.a); if (error < closestError) { closest = i; @@ -128,26 +127,26 @@ public static int ChooseClosestColor(Span colors, Rgba32 color) return closest; } - public static int ChooseClosestColorAlphaCutOff(Span colors, Rgba32 color, byte alphaCutOff = 255 / 2) + public static int ChooseClosestColorAlphaCutOff(Span colors, ColorRgba32 color, byte alphaCutOff = 255 / 2) { - if (color.A <= alphaCutOff) + if (color.a <= alphaCutOff) { return 3; } - int closest = 0; - int closestError = - Math.Abs(colors[0].r - color.R) - + Math.Abs(colors[0].g - color.G) - + Math.Abs(colors[0].b - color.B); + var closest = 0; + var closestError = + Math.Abs(colors[0].r - color.r) + + Math.Abs(colors[0].g - color.g) + + Math.Abs(colors[0].b - color.b); - for (int i = 1; i < colors.Length; i++) + for (var i = 1; i < colors.Length; i++) { if (i == 3) continue; // Skip transparent - int error = - Math.Abs(colors[i].r - color.R) - + Math.Abs(colors[i].g - color.G) - + Math.Abs(colors[i].b - color.B); + var error = + Math.Abs(colors[i].r - color.r) + + Math.Abs(colors[i].g - color.g) + + Math.Abs(colors[i].b - color.b); if (error < closestError) { closest = i; @@ -159,15 +158,15 @@ public static int ChooseClosestColorAlphaCutOff(Span colors, Rgba32 public static int ChooseClosestColor(Span colors, ColorYCbCr color, float luminanceMultiplier = 4) { - int closest = 0; + var closest = 0; float closestError = 0; - bool first = true; + var first = true; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { - float error = MathF.Abs(colors[i].y - color.y) * luminanceMultiplier - + MathF.Abs(colors[i].cb - color.cb) - + MathF.Abs(colors[i].cr - color.cr); + var error = MathF.Abs(colors[i].y - color.y) * luminanceMultiplier + + MathF.Abs(colors[i].cb - color.cb) + + MathF.Abs(colors[i].cr - color.cr); if (first) { closestError = error; @@ -182,7 +181,7 @@ public static int ChooseClosestColor(Span colors, ColorYCbCr color, return closest; } - public static int ChooseClosestColor(Span colors, Rgba32 color, float luminanceMultiplier = 4) + public static int ChooseClosestColor(Span colors, ColorRgba32 color, float luminanceMultiplier = 4) => ChooseClosestColor(colors, new ColorYCbCr(color), luminanceMultiplier); } } diff --git a/BCnEnc.Net/Encoder/ColorVariationGenerator.cs b/BCnEnc.Net/Encoder/ColorVariationGenerator.cs index 96185ed..2f179e3 100644 --- a/BCnEnc.Net/Encoder/ColorVariationGenerator.cs +++ b/BCnEnc.Net/Encoder/ColorVariationGenerator.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using BCnEncoder.Shared; namespace BCnEncoder.Encoder @@ -6,160 +6,47 @@ namespace BCnEncoder.Encoder internal static class ColorVariationGenerator { - private static int[] varPatternEp0R = new int[] { 1, 1, 0, 0, -1, 0, 0, -1, 1, -1, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - private static int[] varPatternEp0G = new int[] { 1, 0, 1, 0, 0, -1, 0, -1, 1, -1, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - private static int[] varPatternEp0B = new int[] { 1, 0, 0, 1, 0, 0, -1, -1, 1, -1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0 }; - private static int[] varPatternEp1R = new int[] { -1, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0 }; - private static int[] varPatternEp1G = new int[] { -1, 0, -1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 1, 0, 0, -1, 0 }; - private static int[] varPatternEp1B = new int[] { -1, 0, 0, -1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 0, 1, 0, 0, -1 }; - public static int VarPatternCount => varPatternEp0R.Length; + private static readonly int[] variatePatternEp0R = new int[] { 1, 1, 0, 0, -1, 0, 0, -1, 1, -1, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + private static readonly int[] variatePatternEp0G = new int[] { 1, 0, 1, 0, 0, -1, 0, -1, 1, -1, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + private static readonly int[] variatePatternEp0B = new int[] { 1, 0, 0, 1, 0, 0, -1, -1, 1, -1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0 }; + private static readonly int[] variatePatternEp1R = new int[] { -1, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0 }; + private static readonly int[] variatePatternEp1G = new int[] { -1, 0, -1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 1, 0, 0, -1, 0 }; + private static readonly int[] variatePatternEp1B = new int[] { -1, 0, 0, -1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 0, 1, 0, 0, -1 }; + public static int VarPatternCount => variatePatternEp0R.Length; - public static (ColorRgb565, ColorRgb565) Variate565(ColorRgb565 c0, ColorRgb565 c1, int i) { - int idx = i % varPatternEp0R.Length; + public static (ColorRgb565, ColorRgb565) Variate565(ColorRgb565 c0, ColorRgb565 c1, int i) + { + var idx = i % variatePatternEp0R.Length; var newEp0 = new ColorRgb565(); var newEp1 = new ColorRgb565(); - newEp0.RawR = ByteHelper.ClampToByte(c0.RawR + varPatternEp0R[idx]); - newEp0.RawG = ByteHelper.ClampToByte(c0.RawG + varPatternEp0G[idx]); - newEp0.RawB = ByteHelper.ClampToByte(c0.RawB + varPatternEp0B[idx]); + newEp0.RawR = ByteHelper.ClampToByte(c0.RawR + variatePatternEp0R[idx]); + newEp0.RawG = ByteHelper.ClampToByte(c0.RawG + variatePatternEp0G[idx]); + newEp0.RawB = ByteHelper.ClampToByte(c0.RawB + variatePatternEp0B[idx]); - newEp1.RawR = ByteHelper.ClampToByte(c1.RawR + varPatternEp1R[idx]); - newEp1.RawG = ByteHelper.ClampToByte(c1.RawG + varPatternEp1G[idx]); - newEp1.RawB = ByteHelper.ClampToByte(c1.RawB + varPatternEp1B[idx]); + newEp1.RawR = ByteHelper.ClampToByte(c1.RawR + variatePatternEp1R[idx]); + newEp1.RawG = ByteHelper.ClampToByte(c1.RawG + variatePatternEp1G[idx]); + newEp1.RawB = ByteHelper.ClampToByte(c1.RawB + variatePatternEp1B[idx]); return (newEp0, newEp1); } - public static List GenerateVariationsSidewaysMax(int variations, ColorYCbCr min, ColorYCbCr max) - { - List colors = new List(); - colors.Add(min.ToColorRgb565()); - colors.Add(max.ToColorRgb565()); - - for (int i = 0; i < variations; i++) - { - max.y -= 0.05f; - min.y += 0.05f; - - var ma = max.ToColorRgb565(); - var mi = min.ToColorRgb565(); - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - - if (!colors.Contains(mi)) - { - colors.Add(mi); - } - - //variate reds in max - ma.RawR += 1; - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - ma.RawR -= 2; - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - - //variate blues in max - ma.RawR += 1; - ma.RawB += 1; - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - ma.RawB -= 2; - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - - } - - return colors; - } - - public static List GenerateVariationsSidewaysMinMax(int variations, ColorYCbCr min, ColorYCbCr max) + public static ((int, int, int), (int, int, int)) VariateInt((int, int, int) ep0, + (int, int, int) ep1, int i) { - List colors = new List(); - colors.Add(min.ToColorRgb565()); - colors.Add(max.ToColorRgb565()); - - for (int i = 0; i < variations; i++) - { - max.y -= 0.05f; - min.y += 0.05f; - - var ma = max.ToColorRgb565(); - var mi = min.ToColorRgb565(); - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - - if (!colors.Contains(mi)) - { - colors.Add(mi); - } - - //variate reds in max - ma.RawR += 1; - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - ma.RawR -= 2; - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - - //variate blues in max - ma.RawR += 1; - ma.RawB += 1; - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - ma.RawB -= 2; - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - - //variate reds in min - mi.RawR += 1; - if (!colors.Contains(mi)) - { - colors.Add(mi); - } - ma.RawR -= 2; - if (!colors.Contains(ma)) - { - colors.Add(ma); - } - - //variate blues in min - mi.RawR += 1; - mi.RawB += 1; - if (!colors.Contains(mi)) - { - colors.Add(mi); - } - mi.RawB -= 2; - if (!colors.Contains(mi)) - { - colors.Add(mi); - } - - } - - return colors; + var idx = i % variatePatternEp0R.Length; + + return (( + ep0.Item1 + variatePatternEp0R[idx], + ep0.Item2 + variatePatternEp0G[idx], + ep0.Item3 + variatePatternEp0B[idx] + ), + ( + ep1.Item1 + variatePatternEp1R[idx], + ep1.Item2 + variatePatternEp1G[idx], + ep1.Item3 + variatePatternEp1B[idx] + )); } } } diff --git a/BCnEnc.Net/Encoder/CompressionQuality.cs b/BCnEnc.Net/Encoder/CompressionQuality.cs new file mode 100644 index 0000000..27dda30 --- /dev/null +++ b/BCnEnc.Net/Encoder/CompressionQuality.cs @@ -0,0 +1,18 @@ +namespace BCnEncoder.Encoder +{ + public enum CompressionQuality + { + /// + /// Fast, but low Quality. Especially bad with gradients. + /// + Fast, + /// + /// Strikes a balance between speed and Quality. Good enough for most purposes. + /// + Balanced, + /// + /// Aims for best Quality encoding. Can be very slow. + /// + BestQuality + } +} diff --git a/BCnEnc.Net/Encoder/EncodingQuality.cs b/BCnEnc.Net/Encoder/EncodingQuality.cs deleted file mode 100644 index 5af737a..0000000 --- a/BCnEnc.Net/Encoder/EncodingQuality.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace BCnEncoder.Encoder -{ - public enum EncodingQuality - { - /// - /// Fast, but low quality. Especially bad with gradients. - /// - Fast, - /// - /// Strikes a balance between speed and quality. Enough for most purposes. - /// - Balanced, - /// - /// Aims for best quality encoding. Can be very slow at times. - /// - BestQuality - } -} diff --git a/BCnEnc.Net/Encoder/IBcBlockEncoder.cs b/BCnEnc.Net/Encoder/IBcBlockEncoder.cs index c3eb246..3fd4182 100644 --- a/BCnEnc.Net/Encoder/IBcBlockEncoder.cs +++ b/BCnEnc.Net/Encoder/IBcBlockEncoder.cs @@ -1,15 +1,17 @@ -using System; +using System; using BCnEncoder.Shared; -using SixLabors.ImageSharp.PixelFormats; +using BCnEncoder.Shared.ImageFiles; namespace BCnEncoder.Encoder { - internal interface IBcBlockEncoder + internal interface IBcBlockEncoder where T : unmanaged { - byte[] Encode(RawBlock4X4Rgba32[] blocks, int blockWidth, int blockHeight, EncodingQuality quality, bool parallel = true); + byte[] Encode(T[] blocks, int blockWidth, int blockHeight, CompressionQuality quality, OperationContext context); + void EncodeBlock(T block, CompressionQuality quality, Span output); GlInternalFormat GetInternalFormat(); - GLFormat GetBaseInternalFormat(); - DXGI_FORMAT GetDxgiFormat(); + GlFormat GetBaseInternalFormat(); + DxgiFormat GetDxgiFormat(); + int GetBlockSize(); } diff --git a/BCnEnc.Net/Encoder/LeastSquares.cs b/BCnEnc.Net/Encoder/LeastSquares.cs new file mode 100644 index 0000000..bafdb61 --- /dev/null +++ b/BCnEnc.Net/Encoder/LeastSquares.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; +using BCnEncoder.Encoder.Bptc; +using BCnEncoder.Shared; + +#if NETSTANDARD2_0 +using Math = BCnEncoder.Shared.MathPolyfills; +#endif + +namespace BCnEncoder.Encoder +{ + /// + /// Least squares optimization from https://github.com/knarkowicz/GPURealTimeBC6H + /// Code is public domain + /// + internal static class LeastSquares + { + + private static int ComputeIndex4(float texelPos, float endPoint0Pos, float endPoint1Pos) + { + var r = (texelPos - endPoint0Pos) / (endPoint1Pos - endPoint0Pos); + return (int)Math.Clamp(r * 15f /*14.93333f + 0.03333f + 0.5f*/, 0.0f, 15.0f); + } + + private static int ComputeIndex3(float texelPos, float endPoint0Pos, float endPoint1Pos) + { + var r = (texelPos - endPoint0Pos) / (endPoint1Pos - endPoint0Pos); + return (int)Math.Clamp(r * 6.98182f + 0.00909f + 0.5f, 0.0f, 7.0f); + } + + private static uint F32ToF16(float f32) + { + return Half.GetBits(new Half(f32)); + } + + private static Vector3 F32ToF16(Vector3 f32) + { + return new Vector3( + F32ToF16(f32.X), + F32ToF16(f32.Y), + F32ToF16(f32.Z) + ); + } + + private static float F16ToF32(uint f16) + { + return Half.ToHalf((ushort)f16); + } + + private static Vector3 F16ToF32(Vector3 f16) + { + return new Vector3( + F16ToF32((uint)f16.X), + F16ToF32((uint)f16.Y), + F16ToF32((uint)f16.Z) + ); + } + + public static void OptimizeEndpoints1Sub(RawBlock4X4RgbFloat block, ref ColorRgbFloat ep0, ref ColorRgbFloat ep1) + { + var ep0v = ep0.ToVector3(); + var ep1v = ep1.ToVector3(); + + var pixels = block.AsSpan; + + var blockDir = ep1v - ep0v; + blockDir = blockDir / (blockDir.X + blockDir.Y + blockDir.Z); + + var endPoint0Pos = (float)F32ToF16(Vector3.Dot(ep0v, blockDir)); + var endPoint1Pos = (float)F32ToF16(Vector3.Dot(ep1v, blockDir)); + + var alphaTexelSum = new Vector3(); + var betaTexelSum = new Vector3(); + var alphaBetaSum = 0.0f; + var alphaSqSum = 0.0f; + var betaSqSum = 0.0f; + + for (var i = 0; i < 16; i++) + { + var texelPos = (float)F32ToF16(Vector3.Dot(pixels[i].ToVector3(), blockDir)); + var texelIndex = ComputeIndex4(texelPos, endPoint0Pos, endPoint1Pos); + + var beta = Math.Clamp(texelIndex / 15.0f, 0f, 1f); + var alpha = 1.0f - beta; + + var texelF16 = F32ToF16(pixels[i].ToVector3()); + alphaTexelSum += alpha * texelF16; + betaTexelSum += beta * texelF16; + + alphaBetaSum += alpha * beta; + + alphaSqSum += alpha * alpha; + betaSqSum += beta * beta; + } + + var det = alphaSqSum * betaSqSum - alphaBetaSum * alphaBetaSum; + + if (MathF.Abs(det) > 0.00001f) + { + var detRcp = 1f / (det); + var ep0f16 = detRcp * (alphaTexelSum * betaSqSum - betaTexelSum * alphaBetaSum); + var ep1f16 = detRcp * (betaTexelSum * alphaSqSum - alphaTexelSum * alphaBetaSum); + ep0f16 = Vector3.Clamp(ep0f16, Vector3.Zero, new Vector3(Half.MaxValue.Value)); + ep1f16 = Vector3.Clamp(ep1f16, Vector3.Zero, new Vector3(Half.MaxValue.Value)); + ep0 = new ColorRgbFloat(F16ToF32(ep0f16)); + ep1 = new ColorRgbFloat(F16ToF32(ep1f16)); + } + } + + public static void OptimizeEndpoints2Sub(RawBlock4X4RgbFloat block, ref ColorRgbFloat ep0, ref ColorRgbFloat ep1, int partitionSetId, int subsetIndex) + { + var ep0v = ep0.ToVector3(); + var ep1v = ep1.ToVector3(); + + var pixels = block.AsSpan; + + var blockDir = ep1v - ep0v; + blockDir = blockDir / (blockDir.X + blockDir.Y + blockDir.Z); + + var endPoint0Pos = (float)F32ToF16(Vector3.Dot(ep0v, blockDir)); + var endPoint1Pos = (float)F32ToF16(Vector3.Dot(ep1v, blockDir)); + + var alphaTexelSum = new Vector3(); + var betaTexelSum = new Vector3(); + var alphaBetaSum = 0.0f; + var alphaSqSum = 0.0f; + var betaSqSum = 0.0f; + + for (var i = 0; i < 16; i++) + { + if (Bc6Block.Subsets2PartitionTable[partitionSetId][i] == subsetIndex) + { + var texelPos = (float)F32ToF16(Vector3.Dot(pixels[i].ToVector3(), blockDir)); + var texelIndex = ComputeIndex3(texelPos, endPoint0Pos, endPoint1Pos); + + var beta = Math.Clamp(texelIndex / 7.0f, 0f, 1f); + var alpha = 1.0f - beta; + + var texelF16 = F32ToF16(pixels[i].ToVector3()); + alphaTexelSum += alpha * texelF16; + betaTexelSum += beta * texelF16; + + alphaBetaSum += alpha * beta; + + alphaSqSum += alpha * alpha; + betaSqSum += beta * beta; + } + } + + var det = alphaSqSum * betaSqSum - alphaBetaSum * alphaBetaSum; + + if (MathF.Abs(det) > 0.00001f) + { + var detRcp = 1f / (det); + var ep0f16 = detRcp * (alphaTexelSum * betaSqSum - betaTexelSum * alphaBetaSum); + var ep1f16 = detRcp * (betaTexelSum * alphaSqSum - alphaTexelSum * alphaBetaSum); + ep0f16 = Vector3.Clamp(ep0f16, Vector3.Zero, new Vector3(Half.MaxValue.Value)); + ep1f16 = Vector3.Clamp(ep1f16, Vector3.Zero, new Vector3(Half.MaxValue.Value)); + ep0 = new ColorRgbFloat(F16ToF32(ep0f16)); + ep1 = new ColorRgbFloat(F16ToF32(ep1f16)); + } + } + } +} diff --git a/BCnEnc.Net/Encoder/Options/EncoderInputOptions.cs b/BCnEnc.Net/Encoder/Options/EncoderInputOptions.cs new file mode 100644 index 0000000..57a2d72 --- /dev/null +++ b/BCnEnc.Net/Encoder/Options/EncoderInputOptions.cs @@ -0,0 +1,31 @@ +using BCnEncoder.Shared; + +namespace BCnEncoder.Encoder.Options +{ + /// + /// The input options for the decoder. + /// + public class EncoderInputOptions + { + /// + /// If true, when encoding to R8 raw format, + /// use the pixel luminance instead of just the red channel. Default is false. (Does not apply to BC4 format) + /// + public bool LuminanceAsRed { get; set; } = false; + + /// + /// The color channel to take for the values of a BC4 block. Default is red. + /// + public ColorComponent Bc4Component { get; set; } = ColorComponent.R; + + /// + /// The color channel to take for the values of the first BC5 block. Default is red. + /// + public ColorComponent Bc5Component1 { get; set; } = ColorComponent.R; + + /// + /// The color channel to take for the values of the second BC5 block. Default is green. + /// + public ColorComponent Bc5Component2 { get; set; } = ColorComponent.G; + } +} diff --git a/BCnEnc.Net/Encoder/Options/EncoderOptions.cs b/BCnEnc.Net/Encoder/Options/EncoderOptions.cs new file mode 100644 index 0000000..5d74d2e --- /dev/null +++ b/BCnEnc.Net/Encoder/Options/EncoderOptions.cs @@ -0,0 +1,29 @@ +using System; +using BCnEncoder.Shared; + +namespace BCnEncoder.Encoder.Options +{ + /// + /// General options for the encoder. + /// + public class EncoderOptions + { + /// + /// Whether the blocks should be encoded in parallel. This can be much faster than single-threaded encoding, + /// but is slow if multiple textures are being processed at the same time. + /// When a debugger is attached, the encoder defaults to single-threaded operation to ease debugging. + /// Default is true. + /// + public bool IsParallel { get; set; } = true; + + /// + /// Determines how many tasks should be used for parallel processing. + /// + public int TaskCount { get; set; } = Environment.ProcessorCount; + + /// + /// The progress context for the operation. + /// + public IProgress Progress { get; set; } + } +} diff --git a/BCnEnc.Net/Encoder/Options/EncoderOutputOptions.cs b/BCnEnc.Net/Encoder/Options/EncoderOutputOptions.cs new file mode 100644 index 0000000..90161c6 --- /dev/null +++ b/BCnEnc.Net/Encoder/Options/EncoderOutputOptions.cs @@ -0,0 +1,54 @@ +using BCnEncoder.Shared; + +namespace BCnEncoder.Encoder.Options +{ + /// + /// The output options for the encoder. + /// + public class EncoderOutputOptions + { + /// + /// Whether to generate mipMaps. Default is true. + /// + public bool GenerateMipMaps { get; set; } = true; + + /// + /// The maximum number of mipmap levels to generate. -1 or 0 is unbounded. + /// Default is -1. + /// + public int MaxMipMapLevel { get; set; } = -1; + + /// + /// The compression Format to use. Default is Bc1. + /// + public CompressionFormat Format { get; set; } = CompressionFormat.Bc1; + + /// + /// The Quality of the compression. Use either fast or balanced for testing. + /// Fast can be used for near real-time encoding for most algorithms. + /// Use bestQuality when needed. Default is balanced. + /// + public CompressionQuality Quality { get; set; } = CompressionQuality.Balanced; + + /// + /// The output file Format of the data. Either Ktx or Dds. + /// Default is Ktx. + /// + public OutputFileFormat FileFormat { get; set; } = OutputFileFormat.Ktx; + + /// + /// The DDS file Format doesn't seem to have a standard for indicating whether a BC1 texture + /// includes 1bit of alpha. This option will write DDPF_ALPHAPIXELS flag to the header + /// to indicate the presence of an alpha channel. Some programs read and write this flag, + /// but some programs don't like it and get confused. Your mileage may vary. + /// Default is false. + /// + public bool DdsBc1WriteAlphaFlag { get; set; } = false; + + /// + /// When writing a dds file, always prefer using the Dxt10 header + /// to write the compression format instead of DwFourCC. + /// + public bool DdsPreferDxt10Header { get; set; } = false; + } +} diff --git a/BCnEnc.Net/Encoder/RawEncoders.cs b/BCnEnc.Net/Encoder/RawEncoders.cs index 0b5c16a..139014b 100644 --- a/BCnEnc.Net/Encoder/RawEncoders.cs +++ b/BCnEnc.Net/Encoder/RawEncoders.cs @@ -1,142 +1,266 @@ -using System; +using System; using BCnEncoder.Shared; -using SixLabors.ImageSharp.PixelFormats; +using BCnEncoder.Shared.ImageFiles; namespace BCnEncoder.Encoder { internal interface IRawEncoder { - byte[] Encode(ReadOnlySpan pixels); + byte[] Encode(ReadOnlyMemory pixels); GlInternalFormat GetInternalFormat(); - GLFormat GetBaseInternalFormat(); - GLFormat GetGlFormat(); - GLType GetGlType(); + GlFormat GetBaseInternalFormat(); + GlFormat GetGlFormat(); + GlType GetGlType(); uint GetGlTypeSize(); - DXGI_FORMAT GetDxgiFormat(); + DxgiFormat GetDxgiFormat(); } - internal class RawLuminanceEncoder : IRawEncoder { + internal class RawLuminanceEncoder : IRawEncoder + { private readonly bool useLuminance; - public RawLuminanceEncoder(bool useLuminance) { + public RawLuminanceEncoder(bool useLuminance) + { this.useLuminance = useLuminance; } - public byte[] Encode(ReadOnlySpan pixels) { - byte[] output = new byte[pixels.Length]; - for (int i = 0; i < pixels.Length; i++) { - if (useLuminance) { - output[i] = (byte)(new ColorYCbCr(pixels[i]).y * 255); + public byte[] Encode(ReadOnlyMemory pixels) + { + var span = pixels.Span; + + var output = new byte[pixels.Length]; + for (var i = 0; i < pixels.Length; i++) + { + if (useLuminance) + { + output[i] = (byte)(new ColorYCbCr(span[i]).y * 255); } - else { - output[i] = pixels[i].R; + else + { + output[i] = span[i].r; } - + } return output; } public GlInternalFormat GetInternalFormat() - => GlInternalFormat.GL_R8; + { + return GlInternalFormat.GlR8; + } - public GLFormat GetBaseInternalFormat() - => GLFormat.GL_RED; + public GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRed; + } - public GLFormat GetGlFormat() => GLFormat.GL_RED; + public GlFormat GetGlFormat() + { + return GlFormat.GlRed; + } - public GLType GetGlType() - => GLType.GL_BYTE; + public GlType GetGlType() + { + return GlType.GlByte; + } public uint GetGlTypeSize() - => 1; + { + return 1; + } - public DXGI_FORMAT GetDxgiFormat() => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM; + public DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatR8Unorm; + } } - internal class RawRGEncoder : IRawEncoder + internal class RawRgEncoder : IRawEncoder { - public byte[] Encode(ReadOnlySpan pixels) { - byte[] output = new byte[pixels.Length * 2]; - for (int i = 0; i < pixels.Length; i++) { - output[i * 2] = pixels[i].R; - output[i * 2 + 1] = pixels[i].G; + public byte[] Encode(ReadOnlyMemory pixels) + { + var span = pixels.Span; + + var output = new byte[pixels.Length * 2]; + for (var i = 0; i < pixels.Length; i++) + { + output[i * 2] = span[i].r; + output[i * 2 + 1] = span[i].g; } return output; } public GlInternalFormat GetInternalFormat() - => GlInternalFormat.GL_RG8; + { + return GlInternalFormat.GlRg8; + } - public GLFormat GetBaseInternalFormat() - => GLFormat.GL_RG; + public GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRg; + } - public GLFormat GetGlFormat() => GLFormat.GL_RG; + public GlFormat GetGlFormat() + { + return GlFormat.GlRg; + } - public GLType GetGlType() - => GLType.GL_BYTE; + public GlType GetGlType() + { + return GlType.GlByte; + } public uint GetGlTypeSize() - => 1; + { + return 1; + } - public DXGI_FORMAT GetDxgiFormat() => DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM; + public DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatR8G8Unorm; + } } - internal class RawRGBEncoder : IRawEncoder + internal class RawRgbEncoder : IRawEncoder { - public byte[] Encode(ReadOnlySpan pixels) { - byte[] output = new byte[pixels.Length * 3]; - for (int i = 0; i < pixels.Length; i++) { - output[i * 3] = pixels[i].R; - output[i * 3 + 1] = pixels[i].G; - output[i * 3 + 2] = pixels[i].B; + public byte[] Encode(ReadOnlyMemory pixels) + { + var span = pixels.Span; + + var output = new byte[pixels.Length * 3]; + for (var i = 0; i < pixels.Length; i++) + { + output[i * 3] = span[i].r; + output[i * 3 + 1] = span[i].g; + output[i * 3 + 2] = span[i].b; } return output; } public GlInternalFormat GetInternalFormat() - => GlInternalFormat.GL_RGB8; + { + return GlInternalFormat.GlRgb8; + } - public GLFormat GetBaseInternalFormat() - => GLFormat.GL_RGB; + public GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgb; + } - public GLFormat GetGlFormat() => GLFormat.GL_RGB; + public GlFormat GetGlFormat() + { + return GlFormat.GlRgb; + } - public GLType GetGlType() - => GLType.GL_BYTE; + public GlType GetGlType() + { + return GlType.GlByte; + } public uint GetGlTypeSize() - => 1; + { + return 1; + } - public DXGI_FORMAT GetDxgiFormat() => throw new NotSupportedException("RGB format is not supported for dds files."); + public DxgiFormat GetDxgiFormat() + { + throw new NotSupportedException("RGB Format is not supported for dds files."); + } } - internal class RawRGBAEncoder : IRawEncoder + internal class RawRgbaEncoder : IRawEncoder { - public byte[] Encode(ReadOnlySpan pixels) { - byte[] output = new byte[pixels.Length * 4]; - for (int i = 0; i < pixels.Length; i++) { - output[i * 4] = pixels[i].R; - output[i * 4 + 1] = pixels[i].G; - output[i * 4 + 2] = pixels[i].B; - output[i * 4 + 3] = pixels[i].A; + public byte[] Encode(ReadOnlyMemory pixels) + { + var span = pixels.Span; + + var output = new byte[pixels.Length * 4]; + for (var i = 0; i < pixels.Length; i++) + { + output[i * 4] = span[i].r; + output[i * 4 + 1] = span[i].g; + output[i * 4 + 2] = span[i].b; + output[i * 4 + 3] = span[i].a; } return output; } public GlInternalFormat GetInternalFormat() - => GlInternalFormat.GL_RGBA8; + { + return GlInternalFormat.GlRgba8; + } - public GLFormat GetBaseInternalFormat() - => GLFormat.GL_RGBA; + public GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgba; + } - public GLFormat GetGlFormat() => GLFormat.GL_RGBA; + public GlFormat GetGlFormat() + { + return GlFormat.GlRgba; + } - public GLType GetGlType() - => GLType.GL_BYTE; + public GlType GetGlType() + { + return GlType.GlByte; + } public uint GetGlTypeSize() - => 1; + { + return 1; + } - public DXGI_FORMAT GetDxgiFormat() => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM; + public DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatR8G8B8A8Unorm; + } + } + + internal class RawBgraEncoder : IRawEncoder + { + public byte[] Encode(ReadOnlyMemory pixels) + { + var span = pixels.Span; + + var output = new byte[pixels.Length * 4]; + for (var i = 0; i < pixels.Length; i++) + { + output[i * 4] = span[i].b; + output[i * 4 + 1] = span[i].g; + output[i * 4 + 2] = span[i].r; + output[i * 4 + 3] = span[i].a; + } + return output; + } + + public GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlBgra8Extension; + } + + public GlFormat GetBaseInternalFormat() + { + return GlFormat.GlBgra; + } + + public GlFormat GetGlFormat() + { + return GlFormat.GlBgra; + } + + public GlType GetGlType() + { + return GlType.GlByte; + } + + public uint GetGlTypeSize() + { + return 1; + } + + public DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatB8G8R8A8Unorm; + } } } diff --git a/BCnEnc.Net/Shared/Bc6Block.cs b/BCnEnc.Net/Shared/Bc6Block.cs new file mode 100644 index 0000000..ad47b8f --- /dev/null +++ b/BCnEnc.Net/Shared/Bc6Block.cs @@ -0,0 +1,1405 @@ +using System; +using System.Diagnostics; +using BCnEncoder.Encoder.Bptc; + +namespace BCnEncoder.Shared +{ + + internal enum Bc6BlockType : uint + { + Type0 = 0, // Mode 1 + Type1 = 1, // Mode 2 + Type2 = 2, // Mode 3 + Type6 = 6, // Mode 4 + Type10 = 10, // Mode 5 + Type14 = 14, // Mode 6 + Type18 = 18, // Mode 7 + Type22 = 22, // Mode 8 + Type26 = 26, // Mode 9 + Type30 = 30, // Mode 10 + Type3 = 3, // Mode 11 + Type7 = 7, // Mode 12 + Type11 = 11, // Mode 13 + Type15 = 15, // Mode 14 + Unknown + } + + internal static class Bc6BlockTypeExtensions + { + public static bool HasSubsets(this Bc6BlockType Type) => Type switch + { + Bc6BlockType.Type3 => false, + Bc6BlockType.Type7 => false, + Bc6BlockType.Type11 => false, + Bc6BlockType.Type15 => false, + _ => true + }; + + public static bool HasTransformedEndpoints(this Bc6BlockType Type) => Type switch + { + Bc6BlockType.Type3 => false, + Bc6BlockType.Type30 => false, + _ => true + }; + + public static int EndpointBits(this Bc6BlockType Type) => Type switch + { + Bc6BlockType.Type0 => 10, + Bc6BlockType.Type1 => 7, + Bc6BlockType.Type2 => 11, + Bc6BlockType.Type6 => 11, + Bc6BlockType.Type10 => 11, + Bc6BlockType.Type14 => 9, + Bc6BlockType.Type18 => 8, + Bc6BlockType.Type22 => 8, + Bc6BlockType.Type26 => 8, + Bc6BlockType.Type30 => 6, + Bc6BlockType.Type3 => 10, + Bc6BlockType.Type7 => 11, + Bc6BlockType.Type11 => 12, + Bc6BlockType.Type15 => 16, + _ => 0 + }; + + public static (int, int, int) DeltaBits(this Bc6BlockType Type) => Type switch + { + Bc6BlockType.Type0 => (5, 5, 5), + Bc6BlockType.Type1 => (6, 6, 6), + Bc6BlockType.Type2 => (5, 4, 4), + Bc6BlockType.Type6 => (4, 5, 4), + Bc6BlockType.Type10 => (4, 4, 5), + Bc6BlockType.Type14 => (5, 5, 5), + Bc6BlockType.Type18 => (6, 5, 5), + Bc6BlockType.Type22 => (5, 6, 5), + Bc6BlockType.Type26 => (5, 5, 6), + Bc6BlockType.Type30 => (0, 0, 0), + Bc6BlockType.Type3 => (0, 0, 0), + Bc6BlockType.Type7 => (9, 9, 9), + Bc6BlockType.Type11 => (8, 8, 8), + Bc6BlockType.Type15 => (4, 4, 4), + _ => (0, 0, 0) + }; + } + + internal struct Bc6Block + { + public ulong lowBits; + public ulong highBits; + + public static readonly int[][] Subsets2PartitionTable = new int[32][]{ + new[] {0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1}, + new[] {0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}, + new[] {0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1}, + new[] {0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1}, + new[] {0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1}, + new[] {0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0}, + new[] {0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0}, + new[] {0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, + new[] {0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0}, + new[] {0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1}, + new[] {0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0}, + new[] {0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0}, + new[] {0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0}, + new[] {0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0}, + new[] {0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0}, + new[] {0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0}, + new[] {0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0} + }; + + public static readonly int[] Subsets2AnchorIndices = { + 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, + 15, 2, 8, 2, 2, 8, 8, 15, + 2, 8, 2, 2, 8, 8, 2, 2, + 15, 15, 6, 8, 2, 8, 15, 15, + 2, 8, 2, 2, 2, 15, 15, 6, + 6, 2, 6, 8, 15, 15, 2, 2, + 15, 15, 15, 15, 15, 2, 2, 15 + }; + + public static readonly RawBlock4X4RgbFloat ErrorBlock = new RawBlock4X4RgbFloat(new ColorRgbFloat(1, 0, 1)); + + public static readonly Bc6BlockType[] Subsets1Types = + { + Bc6BlockType.Type3, + Bc6BlockType.Type7, + Bc6BlockType.Type11, + Bc6BlockType.Type15 + }; + + public static readonly Bc6BlockType[] Subsets2Types = + { + Bc6BlockType.Type0 , + Bc6BlockType.Type1 , + Bc6BlockType.Type2 , + Bc6BlockType.Type6 , + Bc6BlockType.Type10, + Bc6BlockType.Type14, + Bc6BlockType.Type18, + Bc6BlockType.Type22, + Bc6BlockType.Type26, + Bc6BlockType.Type30 + }; + + public readonly Bc6BlockType Type + { + get + { + const ulong smallMask = 0b11; + const ulong bigMask = 0b11111; + // Type 0 or 1 + if ((lowBits & smallMask) < 2) + { + return (Bc6BlockType)(lowBits & smallMask); + } + else + { + var typeNum = (lowBits & bigMask); + switch (typeNum) + { + case 2: return Bc6BlockType.Type2; + case 3: return Bc6BlockType.Type3; + case 6: return Bc6BlockType.Type6; + case 7: return Bc6BlockType.Type7; + case 10: return Bc6BlockType.Type10; + case 11: return Bc6BlockType.Type11; + case 14: return Bc6BlockType.Type14; + case 15: return Bc6BlockType.Type15; + case 18: return Bc6BlockType.Type18; + case 22: return Bc6BlockType.Type22; + case 26: return Bc6BlockType.Type26; + case 30: return Bc6BlockType.Type30; + default: return Bc6BlockType.Unknown; + } + } + } + } + + public readonly bool HasSubsets => Type.HasSubsets(); + + public readonly int NumEndpoints => HasSubsets ? 4 : 2; + + public readonly bool HasTransformedEndpoints => Type.HasTransformedEndpoints(); + + public readonly int PartitionSetId => HasSubsets ? ByteHelper.Extract5(highBits, 13) : -1; + + public readonly int EndpointBits => Type.EndpointBits(); + + public readonly (int, int, int) DeltaBits => Type.DeltaBits(); + + public readonly int ColorIndexBitCount => HasSubsets ? 3 : 4; + + internal void StoreEp0((int, int, int) endpoint) + { + var r0 = (ulong)endpoint.Item1; + var g0 = (ulong)endpoint.Item2; + var b0 = (ulong)endpoint.Item3; + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 5, Math.Min(10, EndpointBits), r0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 15, Math.Min(10, EndpointBits), g0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 25, Math.Min(10, EndpointBits), b0); + + switch (Type) + { + case Bc6BlockType.Type2: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 49, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 59, 1, b0 >> 10); + + break; + case Bc6BlockType.Type6: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 39, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 59, 1, b0 >> 10); + + break; + case Bc6BlockType.Type10: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 39, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 49, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b0 >> 10); + + break; + case Bc6BlockType.Type7: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 44, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 54, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 64, 1, b0 >> 10); + + break; + case Bc6BlockType.Type11: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 44, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 54, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 64, 1, b0 >> 10); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 43, 1, r0 >> 11); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 53, 1, g0 >> 11); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 63, 1, b0 >> 11); + + break; + case Bc6BlockType.Type15: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 44, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 54, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 64, 1, b0 >> 10); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 43, 1, r0 >> 11); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 53, 1, g0 >> 11); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 63, 1, b0 >> 11); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 42, 1, r0 >> 12); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 52, 1, g0 >> 12); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 62, 1, b0 >> 12); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 41, 1, r0 >> 13); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 51, 1, g0 >> 13); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 61, 1, b0 >> 13); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, r0 >> 14); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, g0 >> 14); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b0 >> 14); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 39, 1, r0 >> 15); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 49, 1, g0 >> 15); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 59, 1, b0 >> 15); + + break; + } + } + + internal readonly (int, int, int) ExtractEp0() + { + ulong r0 = 0; + ulong g0 = 0; + ulong b0 = 0; + + r0 = ByteHelper.ExtractFrom128(lowBits, highBits, 5, Math.Min(10, EndpointBits)); + g0 = ByteHelper.ExtractFrom128(lowBits, highBits, 15, Math.Min(10, EndpointBits)); + b0 = ByteHelper.ExtractFrom128(lowBits, highBits, 25, Math.Min(10, EndpointBits)); + + switch (Type) + { + case Bc6BlockType.Type2: + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 49, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 59, 1) << 10; + break; + case Bc6BlockType.Type6: + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 39, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 59, 1) << 10; + break; + case Bc6BlockType.Type10: + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 39, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 49, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 10; + break; + case Bc6BlockType.Type7: + r0 = ByteHelper.ExtractFrom128(lowBits, highBits, 5, 10); + g0 = ByteHelper.ExtractFrom128(lowBits, highBits, 15, 10); + b0 = ByteHelper.ExtractFrom128(lowBits, highBits, 25, 10); + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 44, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 54, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 64, 1) << 10; + break; + case Bc6BlockType.Type11: + r0 = ByteHelper.ExtractFrom128(lowBits, highBits, 5, 10); + g0 = ByteHelper.ExtractFrom128(lowBits, highBits, 15, 10); + b0 = ByteHelper.ExtractFrom128(lowBits, highBits, 25, 10); + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 44, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 54, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 64, 1) << 10; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 43, 1) << 11; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 53, 1) << 11; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 63, 1) << 11; + break; + case Bc6BlockType.Type15: + r0 = ByteHelper.ExtractFrom128(lowBits, highBits, 5, 10); + g0 = ByteHelper.ExtractFrom128(lowBits, highBits, 15, 10); + b0 = ByteHelper.ExtractFrom128(lowBits, highBits, 25, 10); + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 44, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 54, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 64, 1) << 10; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 43, 1) << 11; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 53, 1) << 11; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 63, 1) << 11; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 42, 1) << 12; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 52, 1) << 12; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 62, 1) << 12; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 41, 1) << 13; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 51, 1) << 13; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 61, 1) << 13; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 14; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1) << 14; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 14; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 39, 1) << 15; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 49, 1) << 15; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 59, 1) << 15; + break; + } + + return ((int)r0, (int)g0, (int)b0); + } + + internal void StoreEp1((int, int, int) endpoint) + { + var r1 = (ulong)endpoint.Item1; + var g1 = (ulong)endpoint.Item2; + var b1 = (ulong)endpoint.Item3; + + if (HasTransformedEndpoints) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 35, Math.Min(5, DeltaBits.Item1), r1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 45, Math.Min(5, DeltaBits.Item2), g1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 55, Math.Min(5, DeltaBits.Item3), b1); + + } + + switch (Type) + { + case Bc6BlockType.Type1: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, r1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, g1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b1 >> 5); + + + break; + case Bc6BlockType.Type18: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, r1 >> 5); + + + break; + case Bc6BlockType.Type22: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, g1 >> 5); + + break; + case Bc6BlockType.Type26: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b1 >> 5); + + break; + case Bc6BlockType.Type30: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 35, 6, r1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 45, 6, g1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 55, 6, b1); + + break; + case Bc6BlockType.Type3: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 35, 10, r1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 45, 10, g1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 55, 10, b1); + + break; + case Bc6BlockType.Type7: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 4, r1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 4, g1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 4, b1 >> 5); + + break; + case Bc6BlockType.Type11: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 3, r1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 3, g1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 3, b1 >> 5); + + break; + } + } + + internal readonly (int, int, int) ExtractEp1() + { + ulong r1 = 0; + ulong g1 = 0; + ulong b1 = 0; + + if (HasTransformedEndpoints) + { + r1 = ByteHelper.ExtractFrom128(lowBits, highBits, 35, Math.Min(5, DeltaBits.Item1)); + g1 = ByteHelper.ExtractFrom128(lowBits, highBits, 45, Math.Min(5, DeltaBits.Item2)); + b1 = ByteHelper.ExtractFrom128(lowBits, highBits, 55, Math.Min(5, DeltaBits.Item3)); + } + + switch (Type) + { + case Bc6BlockType.Type1: + r1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 5; + g1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1) << 5; + b1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 5; + + break; + case Bc6BlockType.Type18: + r1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 5; + + break; + case Bc6BlockType.Type22: + g1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1) << 5; + + break; + case Bc6BlockType.Type26: + b1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 5; + + break; + case Bc6BlockType.Type30: + r1 = ByteHelper.ExtractFrom128(lowBits, highBits, 35, 6); + g1 = ByteHelper.ExtractFrom128(lowBits, highBits, 45, 6); + b1 = ByteHelper.ExtractFrom128(lowBits, highBits, 55, 6); + + break; + case Bc6BlockType.Type3: + r1 = ByteHelper.ExtractFrom128(lowBits, highBits, 35, 10); + g1 = ByteHelper.ExtractFrom128(lowBits, highBits, 45, 10); + b1 = ByteHelper.ExtractFrom128(lowBits, highBits, 55, 10); + + break; + case Bc6BlockType.Type7: + r1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 4) << 5; + g1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 4) << 5; + b1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 4) << 5; + + break; + case Bc6BlockType.Type11: + r1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 3) << 5; + g1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 3) << 5; + b1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 3) << 5; + + break; + } + + return ((int)r1, (int)g1, (int)b1); + } + + internal void StoreEp2((int, int, int) endpoint) + { + var r2 = (ulong) endpoint.Item1; + var g2 = (ulong) endpoint.Item2; + var b2 = (ulong) endpoint.Item3; + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 65, Math.Min(5, DeltaBits.Item1), r2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 41, 4, g2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 61, 4, b2); + + switch (Type) + { + case Bc6BlockType.Type0: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 2, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 3, 1, b2 >> 4); + + break; + case Bc6BlockType.Type1: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, r2 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 2, 1, g2 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 22, 1, b2 >> 5); + + break; + case Bc6BlockType.Type6: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 75, 1, g2 >> 4); + + break; + case Bc6BlockType.Type10: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, b2 >> 4); + + break; + case Bc6BlockType.Type14: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + + break; + case Bc6BlockType.Type18: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, r2 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + + break; + case Bc6BlockType.Type22: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 23, 1, g2 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + + break; + case Bc6BlockType.Type26: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 23, 1, b2 >> 5); + + break; + case Bc6BlockType.Type30: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 65, 6, r2); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 21, 1, g2 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 22, 1, b2 >> 5); + + break; + } + } + + internal readonly (int, int, int) ExtractEp2() + { + ulong r2 = 0; + ulong g2 = 0; + ulong b2 = 0; + + r2 = ByteHelper.ExtractFrom128(lowBits, highBits, 65, Math.Min(5, DeltaBits.Item1)); + g2 = ByteHelper.ExtractFrom128(lowBits, highBits, 41, 4); + b2 = ByteHelper.ExtractFrom128(lowBits, highBits, 61, 4); + + switch (Type) + { + case Bc6BlockType.Type0: + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 2, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 3, 1) << 4; + break; + case Bc6BlockType.Type1: + r2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 5; + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 2, 1) << 5; + + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 22, 1) << 5; + + break; + case Bc6BlockType.Type6: + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 75, 1) << 4; + + break; + case Bc6BlockType.Type10: + + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + + break; + case Bc6BlockType.Type14: + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + + break; + case Bc6BlockType.Type18: + r2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 5; + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + + break; + case Bc6BlockType.Type22: + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 23, 1) << 5; + + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + break; + case Bc6BlockType.Type26: + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 23, 1) << 5; + break; + case Bc6BlockType.Type30: + + r2 = ByteHelper.ExtractFrom128(lowBits, highBits, 65, 6); + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 21, 1) << 5; + + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 22, 1) << 5; + + break; + } + + return ((int)r2, (int)g2, (int)b2); + } + + internal void StoreEp3((int, int, int) endpoint) + { + var r3 = (ulong)endpoint.Item1; + var g3 = (ulong)endpoint.Item2; + var b3 = (ulong)endpoint.Item3; + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 71, Math.Min(5, DeltaBits.Item1), r3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 51, 4, g3); + + switch (Type) + { + case Bc6BlockType.Type0: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 4, 1, b3 >> 4); + + break; + case Bc6BlockType.Type1: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, r3 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 3, 2, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 12, 2, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 23, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 32, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 33, 1, b3 >> 5); + + break; + case Bc6BlockType.Type2: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + + break; + case Bc6BlockType.Type6: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 69, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + + break; + case Bc6BlockType.Type10: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 69, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 75, 1, b3 >> 4); + + break; + case Bc6BlockType.Type14: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + + break; + case Bc6BlockType.Type18: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, r3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 13, 1, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 23, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 33, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + + break; + case Bc6BlockType.Type22: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, g3 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 33, 1, g3 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 13, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + + break; + case Bc6BlockType.Type26: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 13, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 33, 1, b3 >> 5); + + break; + case Bc6BlockType.Type30: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 71, 6, r3); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 11, 1, g3 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 31, 1, g3 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 12, 2, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 23, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 32, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 33, 1, b3 >> 5); + + break; + } + } + + internal readonly (int, int, int) ExtractEp3() + { + ulong r3 = 0; + ulong g3 = 0; + ulong b3 = 0; + + r3 = ByteHelper.ExtractFrom128(lowBits, highBits, 71, Math.Min(5, DeltaBits.Item1)); + g3 = ByteHelper.ExtractFrom128(lowBits, highBits, 51, 4); + + switch (Type) + { + case Bc6BlockType.Type0: + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 4, 1) << 4; + break; + case Bc6BlockType.Type1: + r3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 5; + + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 3, 2) << 4; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 12, 2); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 23, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 32, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 33, 1) << 5; + + break; + case Bc6BlockType.Type2: + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + + break; + case Bc6BlockType.Type6: + + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 69, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + + break; + case Bc6BlockType.Type10: + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 69, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 75, 1) << 4; + + break; + case Bc6BlockType.Type14: + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + + break; + case Bc6BlockType.Type18: + r3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 5; + + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 13, 1) << 4; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 23, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 33, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + + break; + case Bc6BlockType.Type22: + + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 33, 1) << 5; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 13, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + + break; + case Bc6BlockType.Type26: + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 13, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 33, 1) << 5; + + break; + case Bc6BlockType.Type30: + + r3 = ByteHelper.ExtractFrom128(lowBits, highBits, 71, 6); + + + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 11, 1) << 4; + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 31, 1) << 5; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 12, 2); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 23, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 32, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 33, 1) << 5; + + break; + } + + return ((int)r3, (int)g3, (int)b3); + } + + private readonly (int, int, int)[] ExtractRawEndpoints(bool signedBc6) + { + var outEndpoints = new (int, int, int)[HasSubsets ? 4 : 2]; + var endpointBits = EndpointBits; + + var (r0, g0, b0) = ExtractEp0(); + + // If bc6h is in signed mode, sign extend the base endpoints + if (signedBc6) + { + r0 = IntHelper.SignExtend(r0, endpointBits); + g0 = IntHelper.SignExtend(g0, endpointBits); + b0 = IntHelper.SignExtend(b0, endpointBits); + } + + outEndpoints[0] = (r0, g0, b0); + + var (r1, g1, b1) = ExtractEp1(); + + if (HasTransformedEndpoints) + { + r1 = IntHelper.SignExtend(r1, DeltaBits.Item1); + g1 = IntHelper.SignExtend(g1, DeltaBits.Item2); + b1 = IntHelper.SignExtend(b1, DeltaBits.Item3); + + r1 = (r1 + r0) & ((1 << endpointBits) - 1); + g1 = (g1 + g0) & ((1 << endpointBits) - 1); + b1 = (b1 + b0) & ((1 << endpointBits) - 1); + } + if (signedBc6) + { + r1 = IntHelper.SignExtend(r1, endpointBits); + g1 = IntHelper.SignExtend(g1, endpointBits); + b1 = IntHelper.SignExtend(b1, endpointBits); + } + + outEndpoints[1] = (r1, g1, b1); + + if (HasSubsets) + { + var (r2, g2, b2) = ExtractEp2(); + var (r3, g3, b3) = ExtractEp3(); + + if (HasTransformedEndpoints) + { + r2 = IntHelper.SignExtend(r2, DeltaBits.Item1); + g2 = IntHelper.SignExtend(g2, DeltaBits.Item2); + b2 = IntHelper.SignExtend(b2, DeltaBits.Item3); + + r2 = (r2 + r0) & ((1 << endpointBits) - 1); + g2 = (g2 + g0) & ((1 << endpointBits) - 1); + b2 = (b2 + b0) & ((1 << endpointBits) - 1); + + r3 = IntHelper.SignExtend(r3, DeltaBits.Item1); + g3 = IntHelper.SignExtend(g3, DeltaBits.Item2); + b3 = IntHelper.SignExtend(b3, DeltaBits.Item3); + + r3 = (r3 + r0) & ((1 << endpointBits) - 1); + g3 = (g3 + g0) & ((1 << endpointBits) - 1); + b3 = (b3 + b0) & ((1 << endpointBits) - 1); + } + + if (signedBc6) + { + r2 = IntHelper.SignExtend(r2, endpointBits); + g2 = IntHelper.SignExtend(g2, endpointBits); + b2 = IntHelper.SignExtend(b2, endpointBits); + + r3 = IntHelper.SignExtend(r3, endpointBits); + g3 = IntHelper.SignExtend(g3, endpointBits); + b3 = IntHelper.SignExtend(b3, endpointBits); + } + + outEndpoints[2] = (r2, g2, b2); + outEndpoints[3] = (r3, g3, b3); + } + + return outEndpoints; + } + + internal static int UnQuantize(int component, int endpointBits, bool signedBc6) + { + int unq; + var sign = false; + + if (!signedBc6) + { + if (endpointBits >= 15) + unq = component; + else if (component == 0) + unq = 0; + else if (component == ((1 << endpointBits) - 1)) + unq = 0xFFFF; + else + //unq = ((component << 16) + 0x8000) >> endpointBits; + unq = ((component << 15) + 0x4000) >> (endpointBits - 1); + + } + else + { + if (endpointBits >= 16) + unq = component; + else + { + if (component < 0) + { + sign = true; + component = -component; + } + + if (component == 0) + unq = 0; + else if (component >= ((1 << (endpointBits - 1)) - 1)) + unq = 0x7FFF; + else + unq = ((component << 15) + 0x4000) >> (endpointBits - 1); + + if (sign) + unq = -unq; + } + } + return unq; + } + + internal static (int, int, int) UnQuantize((int, int, int) components, int endpointBits, bool signedBc6) + { + return ( + UnQuantize(components.Item1, endpointBits, signedBc6), + UnQuantize(components.Item2, endpointBits, signedBc6), + UnQuantize(components.Item3, endpointBits, signedBc6) + ); + } + + internal static Half FinishUnQuantize(int component, bool signedBc6) + { + if (!signedBc6) + { + component = (component * 31) >> 6; // scale the magnitude by 31/64 + return Half.ToHalf((ushort)component); + } + else // signed format + { + component = (component < 0) ? -(((-component) * 31) >> 5) : (component * 31) >> 5; // scale the magnitude by 31/32 + var s = 0; + if (component < 0) + { + s = 0x8000; + component = -component; + } + return Half.ToHalf((ushort)(s | component)); + } + } + + internal static (Half, Half, Half) FinishUnQuantize((int, int, int) components, bool signedBc6) + { + return ( + FinishUnQuantize(components.Item1, signedBc6), + FinishUnQuantize(components.Item2, signedBc6), + FinishUnQuantize(components.Item3, signedBc6) + ); + } + + private static int GetPartitionIndex(int numSubsets, int partitionSetId, int i) + { + switch (numSubsets) + { + case 1: + return 0; + case 2: + return Subsets2PartitionTable[partitionSetId][i]; + default: + throw new ArgumentOutOfRangeException(nameof(numSubsets), numSubsets, "Number of subsets can only be 1, 2 or 3"); + } + } + + private static int GetIndexOffset(int numSubsets, int partitionIndex, int bitCount, int index) + { + if (index == 0) return 0; + if (numSubsets == 1) + { + return bitCount * index - 1; + } + if (numSubsets == 2) + { + var anchorIndex = Subsets2AnchorIndices[partitionIndex]; + if (index <= anchorIndex) + { + return bitCount * index - 1; + } + else + { + return bitCount * index - 2; + } + } + throw new ArgumentOutOfRangeException(nameof(numSubsets), numSubsets, "Number of subsets can only be 1, 2 or 3"); + } + + /// + /// Decrements bitCount by one if index is one of the anchor indices. + /// + private static int GetIndexBitCount(int numSubsets, int partitionIndex, int bitCount, int index) + { + if (index == 0) return bitCount - 1; + if (numSubsets == 2) + { + var anchorIndex = Subsets2AnchorIndices[partitionIndex]; + if (index == anchorIndex) + { + return bitCount - 1; + } + } + return bitCount; + } + + private readonly int GetIndexBegin() + { + return HasSubsets ? 82 : 65; + } + + internal readonly int GetColorIndex(int numSubsets, int partitionIndex, int bitCount, int index) + { + var indexOffset = GetIndexOffset(numSubsets, partitionIndex, bitCount, index); + var indexBitCount = GetIndexBitCount(numSubsets, partitionIndex, bitCount, index); + var indexBegin = GetIndexBegin(); + return (int)ByteHelper.ExtractFrom128(lowBits, highBits, indexBegin + indexOffset, indexBitCount); + } + + internal static (int, int, int) InterpolateColor((int, int, int) endPointStart, (int, int, int) endPointEnd, + int colorIndex, int colorBitCount) + { + var result = ( + BptcEncodingHelpers.InterpolateInt(endPointStart.Item1, endPointEnd.Item1, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateInt(endPointStart.Item2, endPointEnd.Item2, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateInt(endPointStart.Item3, endPointEnd.Item3, colorIndex, colorBitCount) + ); + + return result; + } + + public readonly RawBlock4X4RgbFloat Decode(bool signed) + { + var output = new RawBlock4X4RgbFloat(); + var pixels = output.AsSpan; + + if (Type == Bc6BlockType.Unknown) + { + return ErrorBlock; + } + + var endpoints = ExtractRawEndpoints(signed); + var numSubsets = 1; + var partitionIndex = 0; + + if (HasSubsets) + { + numSubsets = 2; + partitionIndex = PartitionSetId; + } + + for (var i = 0; i < NumEndpoints; i++) + { + endpoints[i] = UnQuantize(endpoints[i], EndpointBits, signed); + } + + for (var i = 0; i < pixels.Length; i++) + { + var subsetIndex = GetPartitionIndex(numSubsets, partitionIndex, i); + + var endPointStart = endpoints[2 * subsetIndex]; + var endPointEnd = endpoints[2 * subsetIndex + 1]; + + var colorIndex = GetColorIndex(numSubsets, partitionIndex, ColorIndexBitCount, i); + + var (r, g, b) = FinishUnQuantize(InterpolateColor(endPointStart, endPointEnd, colorIndex, ColorIndexBitCount), signed); + + pixels[i] = new ColorRgbFloat(r, g, b); + } + + return output; + } + + #region pack methods + + private void StoreIndices(Span indices) + { + Debug.Assert(indices.Length == 16); + var numSubsets = HasSubsets ? 2 : 1; + var partSetId = PartitionSetId; + var colorBitCount = ColorIndexBitCount; + var colorIndexBegin = GetIndexBegin(); + for (var i = 0; i < indices.Length; i++) + { + var partitionIndex = GetPartitionIndex(numSubsets, partSetId, i); + var indexOffset = GetIndexOffset(numSubsets, partitionIndex, colorBitCount, i); + var indexBitCount = GetIndexBitCount(numSubsets, partitionIndex, colorBitCount, i); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + colorIndexBegin + indexOffset, indexBitCount, indices[i]); + } + } + + private void StorePartitionSetId(int partitionSetId) + { + highBits = ByteHelper.Store5(highBits, 13, (byte) partitionSetId); + } + + public static Bc6Block PackType0((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 0; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType1((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 1; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType2((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 2; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType6((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 6; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType10((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 10; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType14((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 14; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType18((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 18; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType22((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 22; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType26((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 26; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType30((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 30; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType3((int, int, int) endpoint0, (int, int, int) endpoint1, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 3; + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType7((int, int, int) endpoint0, (int, int, int) endpoint1, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 7; + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType11((int, int, int) endpoint0, (int, int, int) endpoint1, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 11; + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType15((int, int, int) endpoint0, (int, int, int) endpoint1, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 15; + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreIndices(indices); + + return block; + } + + #endregion + + + } +} diff --git a/BCnEnc.Net/Shared/Bc7Block.cs b/BCnEnc.Net/Shared/Bc7Block.cs index c63e1a8..def6762 100644 --- a/BCnEnc.Net/Shared/Bc7Block.cs +++ b/BCnEnc.Net/Shared/Bc7Block.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Linq; +using BCnEncoder.Encoder.Bptc; namespace BCnEncoder.Shared { @@ -23,10 +24,6 @@ internal struct Bc7Block public ulong lowBits; public ulong highBits; - public static ReadOnlySpan colorInterpolationWeights2 => new byte[] { 0, 21, 43, 64 }; - public static ReadOnlySpan colorInterpolationWeights3 => new byte[] { 0, 9, 18, 27, 37, 46, 55, 64 }; - public static ReadOnlySpan colorInterpolationWeights4 => new byte[] { 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64 }; - public static readonly int[][] Subsets2PartitionTable = { new[] {0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1}, @@ -195,13 +192,15 @@ internal struct Bc7Block 15, 15, 15, 15, 3, 15, 15, 8 }; + public static readonly RawBlock4X4Rgba32 ErrorBlock = new RawBlock4X4Rgba32(new ColorRgba32(255, 0, 255)); + public Bc7BlockType Type { get { - for (int i = 0; i < 8; i++) + for (var i = 0; i < 8; i++) { - ulong mask = (ulong)(1 << i); + var mask = (ulong)(1 << i); if ((lowBits & mask) == mask) { return (Bc7BlockType)i; @@ -531,7 +530,7 @@ private void FinalizeEndpoints(ColorRgba32[] endpoints) { if (HasPBits) { - for (int i = 0; i < endpoints.Length; i++) + for (var i = 0; i < endpoints.Length; i++) { endpoints[i] <<= 1; } @@ -546,7 +545,7 @@ private void FinalizeEndpoints(ColorRgba32[] endpoints) } else { - for (int i = 0; i < endpoints.Length; i++) + for (var i = 0; i < endpoints.Length; i++) { endpoints[i] |= pBits[i]; } @@ -555,7 +554,7 @@ private void FinalizeEndpoints(ColorRgba32[] endpoints) var colorPrecision = ColorComponentPrecision; var alphaPrecision = AlphaComponentPrecision; - for (int i = 0; i < endpoints.Length; i++) + for (var i = 0; i < endpoints.Length; i++) { // ColorComponentPrecision & AlphaComponentPrecision includes pbit // left shift endpoint components so that their MSB lies in bit 7 @@ -575,7 +574,7 @@ private void FinalizeEndpoints(ColorRgba32[] endpoints) //set alpha equal to 255 if (!HasAlpha) { - for (int i = 0; i < endpoints.Length; i++) + for (var i = 0; i < endpoints.Length; i++) { endpoints[i].a = 255; } @@ -584,7 +583,7 @@ private void FinalizeEndpoints(ColorRgba32[] endpoints) public ColorRgba32[] ExtractEndpoints() { - ColorRgba32[] endpoints = new ColorRgba32[NumSubsets * 2]; + var endpoints = new ColorRgba32[NumSubsets * 2]; ExtractRawEndpoints(endpoints); FinalizeEndpoints(endpoints); return endpoints; @@ -614,7 +613,7 @@ private int GetIndexOffset(Bc7BlockType type, int numSubsets, int partitionIndex } if (numSubsets == 2) { - int anchorIndex = Subsets2AnchorIndices[partitionIndex]; + var anchorIndex = Subsets2AnchorIndices[partitionIndex]; if (index <= anchorIndex) { return bitCount * index - 1; @@ -626,8 +625,8 @@ private int GetIndexOffset(Bc7BlockType type, int numSubsets, int partitionIndex } if (numSubsets == 3) { - int anchor2Index = Subsets3AnchorIndices2[partitionIndex]; - int anchor3Index = Subsets3AnchorIndices3[partitionIndex]; + var anchor2Index = Subsets3AnchorIndices2[partitionIndex]; + var anchor3Index = Subsets3AnchorIndices3[partitionIndex]; if (index <= anchor2Index && index <= anchor3Index) { @@ -653,7 +652,7 @@ private int GetIndexBitCount(int numSubsets, int partitionIndex, int bitCount, i if (index == 0) return bitCount - 1; if (numSubsets == 2) { - int anchorIndex = Subsets2AnchorIndices[partitionIndex]; + var anchorIndex = Subsets2AnchorIndices[partitionIndex]; if (index == anchorIndex) { return bitCount - 1; @@ -661,8 +660,8 @@ private int GetIndexBitCount(int numSubsets, int partitionIndex, int bitCount, i } else if (numSubsets == 3) { - int anchor2Index = Subsets3AnchorIndices2[partitionIndex]; - int anchor3Index = Subsets3AnchorIndices3[partitionIndex]; + var anchor2Index = Subsets3AnchorIndices2[partitionIndex]; + var anchor3Index = Subsets3AnchorIndices3[partitionIndex]; if (index == anchor2Index) { return bitCount - 1; @@ -675,7 +674,7 @@ private int GetIndexBitCount(int numSubsets, int partitionIndex, int bitCount, i return bitCount; } - private int GetIndexBegin(Bc7BlockType type, int bitCount, bool IsAlpha) + private int GetIndexBegin(Bc7BlockType type, int bitCount, bool isAlpha) { switch (type) { @@ -697,7 +696,7 @@ private int GetIndexBegin(Bc7BlockType type, int bitCount, bool IsAlpha) return 81; } case Bc7BlockType.Type5: - if (IsAlpha) + if (isAlpha) { return 97; } @@ -716,43 +715,28 @@ private int GetIndexBegin(Bc7BlockType type, int bitCount, bool IsAlpha) private int GetAlphaIndex(Bc7BlockType type, int numSubsets, int partitionIndex, int bitCount, int index) { if (bitCount == 0) return 0; // No Alpha - int indexOffset = GetIndexOffset(type, numSubsets, partitionIndex, bitCount, index); - int indexBitCount = GetIndexBitCount(numSubsets, partitionIndex, bitCount, index); - int indexBegin = GetIndexBegin(type, bitCount, true); + var indexOffset = GetIndexOffset(type, numSubsets, partitionIndex, bitCount, index); + var indexBitCount = GetIndexBitCount(numSubsets, partitionIndex, bitCount, index); + var indexBegin = GetIndexBegin(type, bitCount, true); return (int)ByteHelper.ExtractFrom128(lowBits, highBits, indexBegin + indexOffset, indexBitCount); } private int GetColorIndex(Bc7BlockType type, int numSubsets, int partitionIndex, int bitCount, int index) { - int indexOffset = GetIndexOffset(type, numSubsets, partitionIndex, bitCount, index); - int indexBitCount = GetIndexBitCount(numSubsets, partitionIndex, bitCount, index); - int indexBegin = GetIndexBegin(type, bitCount, false); + var indexOffset = GetIndexOffset(type, numSubsets, partitionIndex, bitCount, index); + var indexBitCount = GetIndexBitCount(numSubsets, partitionIndex, bitCount, index); + var indexBegin = GetIndexBegin(type, bitCount, false); return (int)ByteHelper.ExtractFrom128(lowBits, highBits, indexBegin + indexOffset, indexBitCount); } private ColorRgba32 InterpolateColor(ColorRgba32 endPointStart, ColorRgba32 endPointEnd, int colorIndex, int alphaIndex, int colorBitCount, int alphaBitCount) { - - byte InterpolateByte(byte e0, byte e1, int index, int indexPrecision) { - if (indexPrecision == 0) return e0; - ReadOnlySpan aWeights2 = colorInterpolationWeights2; - ReadOnlySpan aWeights3 = colorInterpolationWeights3; - ReadOnlySpan aWeights4 = colorInterpolationWeights4; - - if(indexPrecision == 2) - return (byte) (((64 - aWeights2[index])* (e0) + aWeights2[index]*(e1) + 32) >> 6); - else if(indexPrecision == 3) - return (byte) (((64 - aWeights3[index])*(e0) + aWeights3[index]*(e1) + 32) >> 6); - else // indexprecision == 4 - return (byte) (((64 - aWeights4[index])*(e0) + aWeights4[index]*(e1) + 32) >> 6); - } - - ColorRgba32 result = new ColorRgba32( - InterpolateByte(endPointStart.r, endPointEnd.r, colorIndex, colorBitCount), - InterpolateByte(endPointStart.g, endPointEnd.g, colorIndex, colorBitCount), - InterpolateByte(endPointStart.b, endPointEnd.b, colorIndex, colorBitCount), - InterpolateByte(endPointStart.a, endPointEnd.a, alphaIndex, alphaBitCount) + var result = new ColorRgba32( + BptcEncodingHelpers.InterpolateByte(endPointStart.r, endPointEnd.r, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.g, endPointEnd.g, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.b, endPointEnd.b, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.a, endPointEnd.a, alphaIndex, alphaBitCount) ); return result; @@ -781,52 +765,56 @@ private static ColorRgba32 SwapChannels(ColorRgba32 source, int rotation) { public RawBlock4X4Rgba32 Decode() { - RawBlock4X4Rgba32 output = new RawBlock4X4Rgba32(); + var output = new RawBlock4X4Rgba32(); var type = Type; + if (type == Bc7BlockType.Type8Reserved) + { + return ErrorBlock; + } + ////decode partition data from explicit partition bits //subset_index = 0; - int numSubsets = 1; - int partitionIndex = 0; + var numSubsets = 1; + var partitionIndex = 0; if (HasSubsets) { numSubsets = NumSubsets; partitionIndex = PartitionSetId; - //subset_index = get_partition_index(num_subsets, partition_set_id, x, y); } var pixels = output.AsSpan; - bool hasRotationBits = HasRotationBits; + var hasRotationBits = HasRotationBits; int rotation = RotationBits; - ColorRgba32[] endpoints = ExtractEndpoints(); - for (int i = 0; i < pixels.Length; i++) + var endpoints = ExtractEndpoints(); + for (var i = 0; i < pixels.Length; i++) { - int subsetIndex = GetPartitionIndex(numSubsets, partitionIndex, i); + var subsetIndex = GetPartitionIndex(numSubsets, partitionIndex, i); - ColorRgba32 endPointStart = endpoints[2 * subsetIndex]; - ColorRgba32 endPointEnd = endpoints[2 * subsetIndex + 1]; + var endPointStart = endpoints[2 * subsetIndex]; + var endPointEnd = endpoints[2 * subsetIndex + 1]; - int alphaBitCount = AlphaIndexBitCount; - int colorBitCount = ColorIndexBitCount; - int alphaIndex = GetAlphaIndex(type, numSubsets, partitionIndex, alphaBitCount, i); - int colorIndex = GetColorIndex(type, numSubsets, partitionIndex, colorBitCount, i); + var alphaBitCount = AlphaIndexBitCount; + var colorBitCount = ColorIndexBitCount; + var alphaIndex = GetAlphaIndex(type, numSubsets, partitionIndex, alphaBitCount, i); + var colorIndex = GetColorIndex(type, numSubsets, partitionIndex, colorBitCount, i); - ColorRgba32 outputColor = InterpolateColor(endPointStart, endPointEnd, colorIndex, alphaIndex, + var outputColor = InterpolateColor(endPointStart, endPointEnd, colorIndex, alphaIndex, colorBitCount, alphaBitCount); if (hasRotationBits) { //Decode the 2 color rotation bits as follows: - // 00 – Block format is Scalar(A) Vector(RGB) - no swapping - // 01 – Block format is Scalar(R) Vector(AGB) - swap A and R - // 10 – Block format is Scalar(G) Vector(RAB) - swap A and G - // 11 - Block format is Scalar(B) Vector(RGA) - swap A and B + // 00 – Block Format is Scalar(A) Vector(RGB) - no swapping + // 01 – Block Format is Scalar(R) Vector(AGB) - swap A and R + // 10 – Block Format is Scalar(G) Vector(RAB) - swap A and G + // 11 - Block Format is Scalar(B) Vector(RGA) - swap A and B outputColor = SwapChannels(outputColor, rotation); } - pixels[i] = outputColor.ToRgba32(); + pixels[i] = outputColor; } return output; @@ -837,37 +825,37 @@ public void PackType0(int partitionIndex4Bit, byte[][] subsetEndpoints, byte[] p Debug.Assert(partitionIndex4Bit < 16, "Mode 0 should have 4bit partition index"); Debug.Assert(subsetEndpoints.Length == 6, "Mode 0 should have 6 endpoints"); Debug.Assert(subsetEndpoints.All(x => x.Length == 3) , "Mode 0 should have RGB endpoints"); - Debug.Assert(subsetEndpoints.All(x => x.All(y => y < (1 << 4))) , "Mode 0 should have 4bit endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.All(y => y < 1 << 4)) , "Mode 0 should have 4bit endpoints"); Debug.Assert(pBits.Length == 6, "Mode 0 should have 6 pBits"); Debug.Assert(indices.Length == 16, "Provide 16 indices"); - Debug.Assert(indices.All(x => x < (1 << 3)) , "Mode 0 should have 3bit indices"); + Debug.Assert(indices.All(x => x < 1 << 3) , "Mode 0 should have 3bit indices"); lowBits = 1; // Set Mode 0 highBits = 0; lowBits = ByteHelper.Store4(lowBits, 1, (byte) partitionIndex4Bit); - int nextIdx = 5; + var nextIdx = 5; //Store endpoints - for (int i = 0; i < subsetEndpoints[0].Length; i++) { - for (int j = 0; j < subsetEndpoints.Length; j++) { + for (var i = 0; i < subsetEndpoints[0].Length; i++) { + for (var j = 0; j < subsetEndpoints.Length; j++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 4, subsetEndpoints[j][i]); nextIdx += 4; } } //Store pBits - for (int i = 0; i < pBits.Length; i++) { + for (var i = 0; i < pBits.Length; i++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 1, pBits[i]); nextIdx++; } Debug.Assert(nextIdx == 83); - int colorBitCount = ColorIndexBitCount; - int indexBegin = GetIndexBegin(Bc7BlockType.Type0, colorBitCount, false); - for (int i = 0; i < 16; i++) { - int indexOffset = GetIndexOffset(Bc7BlockType.Type0, NumSubsets, + var colorBitCount = ColorIndexBitCount; + var indexBegin = GetIndexBegin(Bc7BlockType.Type0, colorBitCount, false); + for (var i = 0; i < 16; i++) { + var indexOffset = GetIndexOffset(Bc7BlockType.Type0, NumSubsets, partitionIndex4Bit, colorBitCount, i); - int indexBitCount = GetIndexBitCount(NumSubsets, + var indexBitCount = GetIndexBitCount(NumSubsets, partitionIndex4Bit, colorBitCount, i); (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, @@ -879,37 +867,37 @@ public void PackType1(int partitionIndex6Bit, byte[][] subsetEndpoints, byte[] p Debug.Assert(partitionIndex6Bit < 64, "Mode 1 should have 6bit partition index"); Debug.Assert(subsetEndpoints.Length == 4, "Mode 1 should have 4 endpoints"); Debug.Assert(subsetEndpoints.All(x => x.Length == 3) , "Mode 1 should have RGB endpoints"); - Debug.Assert(subsetEndpoints.All(x => x.All(y => y < (1 << 6))) , "Mode 1 should have 6bit endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.All(y => y < 1 << 6)) , "Mode 1 should have 6bit endpoints"); Debug.Assert(pBits.Length == 2, "Mode 1 should have 2 pBits"); Debug.Assert(indices.Length == 16, "Provide 16 indices"); - Debug.Assert(indices.All(x => x < (1 << 3)) , "Mode 1 should have 3bit indices"); + Debug.Assert(indices.All(x => x < 1 << 3) , "Mode 1 should have 3bit indices"); lowBits = 2; // Set Mode 1 highBits = 0; lowBits = ByteHelper.Store6(lowBits, 2, (byte) partitionIndex6Bit); - int nextIdx = 8; + var nextIdx = 8; //Store endpoints - for (int i = 0; i < subsetEndpoints[0].Length; i++) { - for (int j = 0; j < subsetEndpoints.Length; j++) { + for (var i = 0; i < subsetEndpoints[0].Length; i++) { + for (var j = 0; j < subsetEndpoints.Length; j++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 6, subsetEndpoints[j][i]); nextIdx += 6; } } //Store pBits - for (int i = 0; i < pBits.Length; i++) { + for (var i = 0; i < pBits.Length; i++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 1, pBits[i]); nextIdx++; } Debug.Assert(nextIdx == 82); - int colorBitCount = ColorIndexBitCount; - int indexBegin = GetIndexBegin(Bc7BlockType.Type1, colorBitCount, false); - for (int i = 0; i < 16; i++) { - int indexOffset = GetIndexOffset(Bc7BlockType.Type1, NumSubsets, + var colorBitCount = ColorIndexBitCount; + var indexBegin = GetIndexBegin(Bc7BlockType.Type1, colorBitCount, false); + for (var i = 0; i < 16; i++) { + var indexOffset = GetIndexOffset(Bc7BlockType.Type1, NumSubsets, partitionIndex6Bit, colorBitCount, i); - int indexBitCount = GetIndexBitCount(NumSubsets, + var indexBitCount = GetIndexBitCount(NumSubsets, partitionIndex6Bit, colorBitCount, i); (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, @@ -921,19 +909,19 @@ public void PackType2(int partitionIndex6Bit, byte[][] subsetEndpoints, byte[] Debug.Assert(partitionIndex6Bit < 64, "Mode 2 should have 6bit partition index"); Debug.Assert(subsetEndpoints.Length == 6, "Mode 2 should have 6 endpoints"); Debug.Assert(subsetEndpoints.All(x => x.Length == 3) , "Mode 2 should have RGB endpoints"); - Debug.Assert(subsetEndpoints.All(x => x.All(y => y < (1 << 5))) , "Mode 2 should have 5bit endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.All(y => y < 1 << 5)) , "Mode 2 should have 5bit endpoints"); Debug.Assert(indices.Length == 16, "Provide 16 indices"); - Debug.Assert(indices.All(x => x < (1 << 2)) , "Mode 2 should have 2bit indices"); + Debug.Assert(indices.All(x => x < 1 << 2) , "Mode 2 should have 2bit indices"); lowBits = 0b100; // Set Mode 2 highBits = 0; lowBits = ByteHelper.Store6(lowBits, 3, (byte) partitionIndex6Bit); - int nextIdx = 9; + var nextIdx = 9; //Store endpoints - for (int i = 0; i < subsetEndpoints[0].Length; i++) { - for (int j = 0; j < subsetEndpoints.Length; j++) { + for (var i = 0; i < subsetEndpoints[0].Length; i++) { + for (var j = 0; j < subsetEndpoints.Length; j++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 5, subsetEndpoints[j][i]); nextIdx += 5; @@ -941,12 +929,12 @@ public void PackType2(int partitionIndex6Bit, byte[][] subsetEndpoints, byte[] } Debug.Assert(nextIdx == 99); - int colorBitCount = ColorIndexBitCount; - int indexBegin = GetIndexBegin(Bc7BlockType.Type2, colorBitCount, false); - for (int i = 0; i < 16; i++) { - int indexOffset = GetIndexOffset(Bc7BlockType.Type2, NumSubsets, + var colorBitCount = ColorIndexBitCount; + var indexBegin = GetIndexBegin(Bc7BlockType.Type2, colorBitCount, false); + for (var i = 0; i < 16; i++) { + var indexOffset = GetIndexOffset(Bc7BlockType.Type2, NumSubsets, partitionIndex6Bit, colorBitCount, i); - int indexBitCount = GetIndexBitCount(NumSubsets, + var indexBitCount = GetIndexBitCount(NumSubsets, partitionIndex6Bit, colorBitCount, i); (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, @@ -958,39 +946,39 @@ public void PackType3(int partitionIndex6Bit, byte[][] subsetEndpoints, byte[] p Debug.Assert(partitionIndex6Bit < 64, "Mode 3 should have 6bit partition index"); Debug.Assert(subsetEndpoints.Length == 4, "Mode 3 should have 4 endpoints"); Debug.Assert(subsetEndpoints.All(x => x.Length == 3) , "Mode 3 should have RGB endpoints"); - Debug.Assert(subsetEndpoints.All(x => x.All(y => y < (1 << 7))) , "Mode 3 should have 7bit endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.All(y => y < 1 << 7)) , "Mode 3 should have 7bit endpoints"); Debug.Assert(pBits.Length == 4, "Mode 3 should have 4 pBits"); Debug.Assert(indices.Length == 16, "Provide 16 indices"); - Debug.Assert(indices.All(x => x < (1 << 2)) , "Mode 3 should have 2bit indices"); + Debug.Assert(indices.All(x => x < 1 << 2) , "Mode 3 should have 2bit indices"); lowBits = 0b1000; // Set Mode 3 highBits = 0; lowBits = ByteHelper.Store6(lowBits, 4, (byte) partitionIndex6Bit); - int nextIdx = 10; + var nextIdx = 10; //Store endpoints - for (int i = 0; i < subsetEndpoints[0].Length; i++) { - for (int j = 0; j < subsetEndpoints.Length; j++) { + for (var i = 0; i < subsetEndpoints[0].Length; i++) { + for (var j = 0; j < subsetEndpoints.Length; j++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 7, subsetEndpoints[j][i]); nextIdx += 7; } } //Store pBits - for (int i = 0; i < pBits.Length; i++) { + for (var i = 0; i < pBits.Length; i++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 1, pBits[i]); nextIdx++; } Debug.Assert(nextIdx == 98); - int colorBitCount = ColorIndexBitCount; - int indexBegin = GetIndexBegin(Bc7BlockType.Type3, colorBitCount, false); - for (int i = 0; i < 16; i++) { - int indexOffset = GetIndexOffset(Bc7BlockType.Type3, NumSubsets, + var colorBitCount = ColorIndexBitCount; + var indexBegin = GetIndexBegin(Bc7BlockType.Type3, colorBitCount, false); + for (var i = 0; i < 16; i++) { + var indexOffset = GetIndexOffset(Bc7BlockType.Type3, NumSubsets, partitionIndex6Bit, colorBitCount, i); - int indexBitCount = GetIndexBitCount(NumSubsets, + var indexBitCount = GetIndexBitCount(NumSubsets, partitionIndex6Bit, colorBitCount, i); (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, @@ -1003,14 +991,14 @@ public void PackType4(int rotation, byte idxMode, byte[][] colorEndPoints, byte[ Debug.Assert(idxMode < 2, "IndexMode can only be 0 or 1"); Debug.Assert(colorEndPoints.Length == 2, "Mode 4 should have 2 endpoints"); Debug.Assert(colorEndPoints.All(x => x.Length == 3) , "Mode 4 should have RGB color endpoints"); - Debug.Assert(colorEndPoints.All(x => x.All(y => y < (1 << 5))) , "Mode 4 should have 5bit color endpoints"); + Debug.Assert(colorEndPoints.All(x => x.All(y => y < 1 << 5)) , "Mode 4 should have 5bit color endpoints"); Debug.Assert(alphaEndPoints.Length == 2, "Mode 4 should have 2 endpoints"); - Debug.Assert(alphaEndPoints.All(x => x < (1 << 6)) , "Mode 4 should have 6bit alpha endpoints"); + Debug.Assert(alphaEndPoints.All(x => x < 1 << 6) , "Mode 4 should have 6bit alpha endpoints"); Debug.Assert(indices2Bit.Length == 16, "Provide 16 indices"); - Debug.Assert(indices2Bit.All(x => x < (1 << 2)) , "Mode 4 should have 2bit indices"); + Debug.Assert(indices2Bit.All(x => x < 1 << 2) , "Mode 4 should have 2bit indices"); Debug.Assert(indices3Bit.Length == 16, "Provide 16 indices"); - Debug.Assert(indices3Bit.All(x => x < (1 << 3)) , "Mode 4 should have 3bit indices"); + Debug.Assert(indices3Bit.All(x => x < 1 << 3) , "Mode 4 should have 3bit indices"); lowBits = 0b10000; // Set Mode 4 highBits = 0; @@ -1018,29 +1006,29 @@ public void PackType4(int rotation, byte idxMode, byte[][] colorEndPoints, byte[ lowBits = ByteHelper.Store2(lowBits, 5, (byte) rotation); lowBits = ByteHelper.Store1(lowBits, 7, (byte) idxMode); - int nextIdx = 8; + var nextIdx = 8; //Store color endpoints - for (int i = 0; i < colorEndPoints[0].Length; i++) { - for (int j = 0; j < colorEndPoints.Length; j++) { + for (var i = 0; i < colorEndPoints[0].Length; i++) { + for (var j = 0; j < colorEndPoints.Length; j++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 5, colorEndPoints[j][i]); nextIdx += 5; } } //Store alpha endpoints - for (int i = 0; i < alphaEndPoints.Length; i++) { + for (var i = 0; i < alphaEndPoints.Length; i++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 6, alphaEndPoints[i]); nextIdx+=6; } Debug.Assert(nextIdx == 50); - int colorBitCount = ColorIndexBitCount; - int colorIndexBegin = GetIndexBegin(Bc7BlockType.Type4, colorBitCount, false); - for (int i = 0; i < 16; i++) { - int indexOffset = GetIndexOffset(Bc7BlockType.Type4, NumSubsets, + var colorBitCount = ColorIndexBitCount; + var colorIndexBegin = GetIndexBegin(Bc7BlockType.Type4, colorBitCount, false); + for (var i = 0; i < 16; i++) { + var indexOffset = GetIndexOffset(Bc7BlockType.Type4, NumSubsets, 0, colorBitCount, i); - int indexBitCount = GetIndexBitCount(NumSubsets, + var indexBitCount = GetIndexBitCount(NumSubsets, 0, colorBitCount, i); if (idxMode == 0) { @@ -1053,12 +1041,12 @@ public void PackType4(int rotation, byte idxMode, byte[][] colorEndPoints, byte[ } } - int alphaBitCount = AlphaIndexBitCount; - int alphaIndexBegin = GetIndexBegin(Bc7BlockType.Type4, alphaBitCount, true); - for (int i = 0; i < 16; i++) { - int indexOffset = GetIndexOffset(Bc7BlockType.Type4, NumSubsets, + var alphaBitCount = AlphaIndexBitCount; + var alphaIndexBegin = GetIndexBegin(Bc7BlockType.Type4, alphaBitCount, true); + for (var i = 0; i < 16; i++) { + var indexOffset = GetIndexOffset(Bc7BlockType.Type4, NumSubsets, 0, alphaBitCount, i); - int indexBitCount = GetIndexBitCount(NumSubsets, + var indexBitCount = GetIndexBitCount(NumSubsets, 0, alphaBitCount, i); if (idxMode == 0) { @@ -1076,42 +1064,42 @@ public void PackType5(int rotation, byte[][] colorEndPoints, byte[] alphaEndPoin Debug.Assert(rotation < 4, "Rotation can only be 0-3"); Debug.Assert(colorEndPoints.Length == 2, "Mode 5 should have 2 endpoints"); Debug.Assert(colorEndPoints.All(x => x.Length == 3) , "Mode 5 should have RGB color endpoints"); - Debug.Assert(colorEndPoints.All(x => x.All(y => y < (1 << 7))) , "Mode 5 should have 7bit color endpoints"); + Debug.Assert(colorEndPoints.All(x => x.All(y => y < 1 << 7)) , "Mode 5 should have 7bit color endpoints"); Debug.Assert(alphaEndPoints.Length == 2, "Mode 5 should have 2 endpoints"); Debug.Assert(colorIndices.Length == 16, "Provide 16 indices"); - Debug.Assert(colorIndices.All(x => x < (1 << 2)) , "Mode 5 should have 2bit color indices"); + Debug.Assert(colorIndices.All(x => x < 1 << 2) , "Mode 5 should have 2bit color indices"); Debug.Assert(alphaIndices.Length == 16, "Provide 16 indices"); - Debug.Assert(alphaIndices.All(x => x < (1 << 2)) , "Mode 5 should have 2bit alpha indices"); + Debug.Assert(alphaIndices.All(x => x < 1 << 2) , "Mode 5 should have 2bit alpha indices"); lowBits = 0b100000; // Set Mode 5 highBits = 0; lowBits = ByteHelper.Store2(lowBits, 6, (byte) rotation); - int nextIdx = 8; + var nextIdx = 8; //Store color endpoints - for (int i = 0; i < colorEndPoints[0].Length; i++) { - for (int j = 0; j < colorEndPoints.Length; j++) { + for (var i = 0; i < colorEndPoints[0].Length; i++) { + for (var j = 0; j < colorEndPoints.Length; j++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 7, colorEndPoints[j][i]); nextIdx += 7; } } //Store alpha endpoints - for (int i = 0; i < alphaEndPoints.Length; i++) { + for (var i = 0; i < alphaEndPoints.Length; i++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 8, alphaEndPoints[i]); nextIdx+=8; } Debug.Assert(nextIdx == 66); - int colorBitCount = ColorIndexBitCount; - int colorIndexBegin = GetIndexBegin(Bc7BlockType.Type5, colorBitCount, false); - for (int i = 0; i < 16; i++) { - int indexOffset = GetIndexOffset(Bc7BlockType.Type5, NumSubsets, + var colorBitCount = ColorIndexBitCount; + var colorIndexBegin = GetIndexBegin(Bc7BlockType.Type5, colorBitCount, false); + for (var i = 0; i < 16; i++) { + var indexOffset = GetIndexOffset(Bc7BlockType.Type5, NumSubsets, 0, colorBitCount, i); - int indexBitCount = GetIndexBitCount(NumSubsets, + var indexBitCount = GetIndexBitCount(NumSubsets, 0, colorBitCount, i); (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, @@ -1119,12 +1107,12 @@ public void PackType5(int rotation, byte[][] colorEndPoints, byte[] alphaEndPoin } - int alphaBitCount = AlphaIndexBitCount; - int alphaIndexBegin = GetIndexBegin(Bc7BlockType.Type5, alphaBitCount, true); - for (int i = 0; i < 16; i++) { - int indexOffset = GetIndexOffset(Bc7BlockType.Type5, NumSubsets, + var alphaBitCount = AlphaIndexBitCount; + var alphaIndexBegin = GetIndexBegin(Bc7BlockType.Type5, alphaBitCount, true); + for (var i = 0; i < 16; i++) { + var indexOffset = GetIndexOffset(Bc7BlockType.Type5, NumSubsets, 0, alphaBitCount, i); - int indexBitCount = GetIndexBitCount(NumSubsets, + var indexBitCount = GetIndexBitCount(NumSubsets, 0, alphaBitCount, i); @@ -1139,43 +1127,42 @@ public void PackType6(byte[][] colorAlphaEndPoints, byte[] pBits, byte[] indices "Mode 6 should have 2 endpoints"); Debug.Assert(colorAlphaEndPoints.All(x => x.Length == 4) , "Mode 6 should have RGBA color endpoints"); - Debug.Assert(colorAlphaEndPoints.All(x => x.All(y => y < (1 << 7))) , + Debug.Assert(colorAlphaEndPoints.All(x => x.All(y => y < 1 << 7)) , "Mode 6 should have 7bit color and alpha endpoints"); Debug.Assert(pBits.Length == 2, "Mode 6 should have 2 pBits"); Debug.Assert(indices.Length == 16, "Provide 16 indices"); - Debug.Assert(indices.All(x => x < (1 << 4)) , "Mode 6 should have 4bit color indices"); + Debug.Assert(indices.All(x => x < 1 << 4) , "Mode 6 should have 4bit color indices"); lowBits = 0b1000000; // Set Mode 6 highBits = 0; - int nextIdx = 7; + var nextIdx = 7; //Store color and alpha endpoints - for (int i = 0; i < colorAlphaEndPoints[0].Length; i++) { - for (int j = 0; j < colorAlphaEndPoints.Length; j++) { + for (var i = 0; i < colorAlphaEndPoints[0].Length; i++) { + for (var j = 0; j < colorAlphaEndPoints.Length; j++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 7, colorAlphaEndPoints[j][i]); nextIdx += 7; } } //Store pBits - for (int i = 0; i < pBits.Length; i++) { + for (var i = 0; i < pBits.Length; i++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 1, pBits[i]); nextIdx++; } Debug.Assert(nextIdx == 65); - int colorBitCount = ColorIndexBitCount; - int colorIndexBegin = GetIndexBegin(Bc7BlockType.Type6, colorBitCount, false); - for (int i = 0; i < 16; i++) { - int indexOffset = GetIndexOffset(Bc7BlockType.Type6, NumSubsets, + var colorBitCount = ColorIndexBitCount; + var colorIndexBegin = GetIndexBegin(Bc7BlockType.Type6, colorBitCount, false); + for (var i = 0; i < 16; i++) { + var indexOffset = GetIndexOffset(Bc7BlockType.Type6, NumSubsets, 0, colorBitCount, i); - int indexBitCount = GetIndexBitCount(NumSubsets, + var indexBitCount = GetIndexBitCount(NumSubsets, 0, colorBitCount, i); (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, colorIndexBegin + indexOffset, indexBitCount, indices[i]); - } } @@ -1183,39 +1170,39 @@ public void PackType7(int partitionIndex6Bit, byte[][] subsetEndpoints, byte[] p Debug.Assert(partitionIndex6Bit < 64, "Mode 7 should have 6bit partition index"); Debug.Assert(subsetEndpoints.Length == 4, "Mode 7 should have 4 endpoints"); Debug.Assert(subsetEndpoints.All(x => x.Length == 4) , "Mode 7 should have RGBA endpoints"); - Debug.Assert(subsetEndpoints.All(x => x.All(y => y < (1 << 5))) , "Mode 7 should have 5bit endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.All(y => y < 1 << 5)) , "Mode 7 should have 5bit endpoints"); Debug.Assert(pBits.Length == 4, "Mode 7 should have 4 pBits"); Debug.Assert(indices.Length == 16, "Provide 16 indices"); - Debug.Assert(indices.All(x => x < (1 << 2)) , "Mode 3 should have 2bit indices"); + Debug.Assert(indices.All(x => x < 1 << 2) , "Mode 3 should have 2bit indices"); lowBits = 0b10000000; // Set Mode 7 highBits = 0; lowBits = ByteHelper.Store6(lowBits, 8, (byte) partitionIndex6Bit); - int nextIdx = 14; + var nextIdx = 14; //Store endpoints - for (int i = 0; i < subsetEndpoints[0].Length; i++) { - for (int j = 0; j < subsetEndpoints.Length; j++) { + for (var i = 0; i < subsetEndpoints[0].Length; i++) { + for (var j = 0; j < subsetEndpoints.Length; j++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 5, subsetEndpoints[j][i]); nextIdx += 5; } } //Store pBits - for (int i = 0; i < pBits.Length; i++) { + for (var i = 0; i < pBits.Length; i++) { (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 1, pBits[i]); nextIdx++; } Debug.Assert(nextIdx == 98); - int colorBitCount = ColorIndexBitCount; - int indexBegin = GetIndexBegin(Bc7BlockType.Type7, colorBitCount, false); - for (int i = 0; i < 16; i++) { - int indexOffset = GetIndexOffset(Bc7BlockType.Type7, NumSubsets, + var colorBitCount = ColorIndexBitCount; + var indexBegin = GetIndexBegin(Bc7BlockType.Type7, colorBitCount, false); + for (var i = 0; i < 16; i++) { + var indexOffset = GetIndexOffset(Bc7BlockType.Type7, NumSubsets, partitionIndex6Bit, colorBitCount, i); - int indexBitCount = GetIndexBitCount(NumSubsets, + var indexBitCount = GetIndexBitCount(NumSubsets, partitionIndex6Bit, colorBitCount, i); (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, diff --git a/BCnEnc.Net/Shared/BinaryReaderWriterExtensions.cs b/BCnEnc.Net/Shared/BinaryReaderWriterExtensions.cs index 862980f..eccf7e6 100644 --- a/BCnEnc.Net/Shared/BinaryReaderWriterExtensions.cs +++ b/BCnEnc.Net/Shared/BinaryReaderWriterExtensions.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Runtime.CompilerServices; -using System.Text; namespace BCnEncoder.Shared { @@ -9,25 +8,25 @@ internal static class BinaryReaderWriterExtensions { public static unsafe void WriteStruct(this BinaryWriter bw, T t) where T : unmanaged { - int size = Unsafe.SizeOf(); - byte* bytes = stackalloc byte[size]; + var size = Unsafe.SizeOf(); + var bytes = stackalloc byte[size]; Unsafe.Write(bytes, t); - Span bSpan = new Span(bytes, size); + var bSpan = new Span(bytes, size); bw.Write(bSpan); } public static unsafe T ReadStruct(this BinaryReader br) where T : unmanaged { - int size = Unsafe.SizeOf(); - byte* bytes = stackalloc byte[size]; - Span bSpan = new Span(bytes, size); + var size = Unsafe.SizeOf(); + var bytes = stackalloc byte[size]; + var bSpan = new Span(bytes, size); br.Read(bSpan); return Unsafe.Read(bytes); } public static void AddPadding(this BinaryWriter bw, uint padding) { - for (int i = 0; i < padding; i++) + for (var i = 0; i < padding; i++) { bw.Write((byte)0); } @@ -42,28 +41,5 @@ public static void SkipPadding(this BinaryReader br, uint padding) public static void SkipPadding(this BinaryReader br, int padding) => SkipPadding(br, (uint) padding); - - public static void WriteString(this BinaryWriter writer, string input, Encoding encoding = null) { - encoding ??= Encoding.UTF8; - - var inputSpan = input.AsSpan(); - int inputByteCount = encoding.GetByteCount(input); - Span inputBytes = stackalloc byte[inputByteCount]; - - encoding.GetBytes(inputSpan, inputBytes); - - writer.Write(inputByteCount); - writer.Write(inputBytes); - } - - public static string ReadString(this BinaryReader reader, Encoding encoding = null) { - encoding ??= Encoding.UTF8; - - int byteCount = reader.ReadInt32(); - Span bytes = stackalloc byte[byteCount]; - reader.Read(bytes); - - return encoding.GetString(bytes); - } } } \ No newline at end of file diff --git a/BCnEnc.Net/Shared/ByteHelper.cs b/BCnEnc.Net/Shared/ByteHelper.cs index 4359cf2..7c60b60 100644 --- a/BCnEnc.Net/Shared/ByteHelper.cs +++ b/BCnEnc.Net/Shared/ByteHelper.cs @@ -128,8 +128,8 @@ public static ulong Extract(ulong source, int index, int bitCount) { unchecked { - ulong mask = (0b1UL << bitCount) - 1; - return ((source >> index) & mask); + var mask = (0b1UL << bitCount) - 1; + return (source >> index) & mask; } } @@ -137,7 +137,7 @@ public static ulong Store(ulong dest, int index, int bitCount, ulong value) { unchecked { - ulong mask = (0b1UL << bitCount) - 1; + var mask = (0b1UL << bitCount) - 1; dest &= ~(mask << index); dest |= (value & mask) << index; return dest; @@ -156,13 +156,13 @@ public static ulong ExtractFrom128(ulong low, ulong high, int index, int bitCoun } else { //handle boundary case - int lowIndex = index; - int lowBitCount = 64 - index; - int highBitCount = bitCount - lowBitCount; - int highIndex = 0; + var lowIndex = index; + var lowBitCount = 64 - index; + var highBitCount = bitCount - lowBitCount; + var highIndex = 0; - ulong value = Extract(low, lowIndex, lowBitCount); - ulong hVal = Extract(high, highIndex, highBitCount); + var value = Extract(low, lowIndex, lowBitCount); + var hVal = Extract(high, highIndex, highBitCount); value = Store(value, lowBitCount, highBitCount, hVal); return value; } @@ -180,10 +180,10 @@ public static (ulong, ulong) StoreTo128(ulong low, ulong high, int index, int bi } else { //handle boundary case - int lowIndex = index; - int lowBitCount = 64 - index; - int highBitCount = bitCount - lowBitCount; - int highIndex = 0; + var lowIndex = index; + var lowBitCount = 64 - index; + var highBitCount = bitCount - lowBitCount; + var highIndex = 0; var l = Store(low, lowIndex, lowBitCount, value); value >>= lowBitCount; diff --git a/BCnEnc.Net/Shared/ColorComponent.cs b/BCnEnc.Net/Shared/ColorComponent.cs new file mode 100644 index 0000000..81452b6 --- /dev/null +++ b/BCnEnc.Net/Shared/ColorComponent.cs @@ -0,0 +1,33 @@ +namespace BCnEncoder.Shared +{ + /// + /// The component to take from colors for BC4 and BC5. + /// + public enum ColorComponent + { + /// + /// The red component of an Rgba32 color. + /// + R, + + /// + /// The green component of an Rgba32 color. + /// + G, + + /// + /// The blue component of an Rgba32 color. + /// + B, + + /// + /// The alpha component of an Rgba32 color. + /// + A, + + /// + /// Use the color's luminance value as the component. + /// + Luminance + } +} diff --git a/BCnEnc.Net/Shared/Colors.cs b/BCnEnc.Net/Shared/Colors.cs index 620cff7..488d0f6 100644 --- a/BCnEnc.Net/Shared/Colors.cs +++ b/BCnEnc.Net/Shared/Colors.cs @@ -1,149 +1,12 @@ using System; using System.Numerics; -using SixLabors.ImageSharp.PixelFormats; namespace BCnEncoder.Shared { - internal struct ColorRgb565 : IEquatable - { - public bool Equals(ColorRgb565 other) - { - return data == other.data; - } - - public override bool Equals(object obj) - { - return obj is ColorRgb565 other && Equals(other); - } - - public override int GetHashCode() - { - return data.GetHashCode(); - } - - public static bool operator ==(ColorRgb565 left, ColorRgb565 right) - { - return left.Equals(right); - } - - public static bool operator !=(ColorRgb565 left, ColorRgb565 right) - { - return !left.Equals(right); - } - - private const ushort RedMask = 0b11111_000000_00000; - private const int RedShift = 11; - private const ushort GreenMask = 0b00000_111111_00000; - private const int GreenShift = 5; - private const ushort BlueMask = 0b00000_000000_11111; - - public ushort data; - - public byte R { - readonly get { - int r5 = ((data & RedMask) >> RedShift); - return (byte)((r5 << 3) | (r5 >> 2)); - } - set { - int r5 = value >> 3; - data = (ushort)(data & ~RedMask); - data = (ushort)(data | (r5 << RedShift)); - } - } - - public byte G { - readonly get { - int g6 = ((data & GreenMask) >> GreenShift); - return (byte)((g6 << 2) | (g6 >> 4)); - } - set { - int g6 = value >> 2; - data = (ushort)(data & ~GreenMask); - data = (ushort)(data | (g6 << GreenShift)); - } - } - - public byte B { - readonly get { - int b5 = (data & BlueMask); - return (byte)((b5 << 3) | (b5 >> 2)); - } - set { - int b5 = value >> 3; - data = (ushort)(data & ~BlueMask); - data = (ushort)(data | b5); - } - } - - public int RawR { - readonly get { return ((data & RedMask) >> RedShift); } - set { - if (value > 31) value = 31; - if (value < 0) value = 0; - data = (ushort)(data & ~RedMask); - data = (ushort)(data | ((value) << RedShift)); - } - } - - public int RawG { - readonly get { return ((data & GreenMask) >> GreenShift); } - set { - if (value > 63) value = 63; - if (value < 0) value = 0; - data = (ushort)(data & ~GreenMask); - data = (ushort)(data | ((value) << GreenShift)); - } - } - - public int RawB { - readonly get { return (data & BlueMask); } - set { - if (value > 31) value = 31; - if (value < 0) value = 0; - data = (ushort)(data & ~BlueMask); - data = (ushort)(data | value); - } - } - - public ColorRgb565(byte r, byte g, byte b) - { - data = 0; - R = r; - G = g; - B = b; - } - - public ColorRgb565(Vector3 colorVector) { - data = 0; - R = ByteHelper.ClampToByte(colorVector.X * 255); - G = ByteHelper.ClampToByte(colorVector.Y * 255); - B = ByteHelper.ClampToByte(colorVector.Z * 255); - } - - public ColorRgb565(ColorRgb24 color) { - data = 0; - R = color.r; - G = color.g; - B = color.b; - } - - public readonly ColorRgb24 ToColorRgb24() - { - return new ColorRgb24(R, G, B); - } - - public override string ToString() { - return $"r : {R} g : {G} b : {B}"; - } - - public ColorRgba32 ToColorRgba32() { - return new ColorRgba32(R, G, B, 255); - } - } - - internal struct ColorRgba32 : IEquatable + public struct ColorRgba32 : IEquatable { public byte r, g, b, a; + public ColorRgba32(byte r, byte g, byte b, byte a) { this.r = r; @@ -152,12 +15,12 @@ public ColorRgba32(byte r, byte g, byte b, byte a) this.a = a; } - public ColorRgba32(Rgba32 color) + public ColorRgba32(byte r, byte g, byte b) { - this.r = color.R; - this.g = color.G; - this.b = color.B; - this.a = color.A; + this.r = r; + this.g = g; + this.b = b; + this.a = 255; } public bool Equals(ColorRgba32 other) @@ -174,7 +37,7 @@ public override int GetHashCode() { unchecked { - int hashCode = r.GetHashCode(); + var hashCode = r.GetHashCode(); hashCode = (hashCode * 397) ^ g.GetHashCode(); hashCode = (hashCode * 397) ^ b.GetHashCode(); hashCode = (hashCode * 397) ^ a.GetHashCode(); @@ -236,10 +99,10 @@ public override int GetHashCode() public static ColorRgba32 operator <<(ColorRgba32 left, int right) { return new ColorRgba32( - ByteHelper.ClampToByte((left.r << right)), - ByteHelper.ClampToByte((left.g << right)), - ByteHelper.ClampToByte((left.b << right)), - ByteHelper.ClampToByte((left.a << right)) + ByteHelper.ClampToByte(left.r << right), + ByteHelper.ClampToByte(left.g << right), + ByteHelper.ClampToByte(left.b << right), + ByteHelper.ClampToByte(left.a << right) ); } @@ -249,10 +112,10 @@ public override int GetHashCode() public static ColorRgba32 operator >>(ColorRgba32 left, int right) { return new ColorRgba32( - ByteHelper.ClampToByte((left.r >> right)), - ByteHelper.ClampToByte((left.g >> right)), - ByteHelper.ClampToByte((left.b >> right)), - ByteHelper.ClampToByte((left.a >> right)) + ByteHelper.ClampToByte(left.r >> right), + ByteHelper.ClampToByte(left.g >> right), + ByteHelper.ClampToByte(left.b >> right), + ByteHelper.ClampToByte(left.a >> right) ); } @@ -262,10 +125,10 @@ public override int GetHashCode() public static ColorRgba32 operator |(ColorRgba32 left, ColorRgba32 right) { return new ColorRgba32( - ByteHelper.ClampToByte((left.r | right.r)), - ByteHelper.ClampToByte((left.g | right.g)), - ByteHelper.ClampToByte((left.b | right.b)), - ByteHelper.ClampToByte((left.a | right.a)) + ByteHelper.ClampToByte(left.r | right.r), + ByteHelper.ClampToByte(left.g | right.g), + ByteHelper.ClampToByte(left.b | right.b), + ByteHelper.ClampToByte(left.a | right.a) ); } @@ -275,10 +138,10 @@ public override int GetHashCode() public static ColorRgba32 operator |(ColorRgba32 left, int right) { return new ColorRgba32( - ByteHelper.ClampToByte((left.r | right)), - ByteHelper.ClampToByte((left.g | right)), - ByteHelper.ClampToByte((left.b | right)), - ByteHelper.ClampToByte((left.a | right)) + ByteHelper.ClampToByte(left.r | right), + ByteHelper.ClampToByte(left.g | right), + ByteHelper.ClampToByte(left.b | right), + ByteHelper.ClampToByte(left.a | right) ); } @@ -288,10 +151,10 @@ public override int GetHashCode() public static ColorRgba32 operator &(ColorRgba32 left, ColorRgba32 right) { return new ColorRgba32( - ByteHelper.ClampToByte((left.r & right.r)), - ByteHelper.ClampToByte((left.g & right.g)), - ByteHelper.ClampToByte((left.b & right.b)), - ByteHelper.ClampToByte((left.a & right.a)) + ByteHelper.ClampToByte(left.r & right.r), + ByteHelper.ClampToByte(left.g & right.g), + ByteHelper.ClampToByte(left.b & right.b), + ByteHelper.ClampToByte(left.a & right.a) ); } @@ -301,326 +164,957 @@ public override int GetHashCode() public static ColorRgba32 operator &(ColorRgba32 left, int right) { return new ColorRgba32( - ByteHelper.ClampToByte((left.r & right)), - ByteHelper.ClampToByte((left.g & right)), - ByteHelper.ClampToByte((left.b & right)), - ByteHelper.ClampToByte((left.a & right)) + ByteHelper.ClampToByte(left.r & right), + ByteHelper.ClampToByte(left.g & right), + ByteHelper.ClampToByte(left.b & right), + ByteHelper.ClampToByte(left.a & right) ); } - public override string ToString() { + public override string ToString() + { return $"r : {r} g : {g} b : {b} a : {a}"; } - public Rgba32 ToRgba32() { - return new Rgba32(r, g, b, a); + internal readonly ColorRgbaFloat ToFloat() + { + return new ColorRgbaFloat(this); + } + + public readonly ColorRgbFloat ToRgbFloat() + { + return new ColorRgbFloat(this); } } - internal struct ColorRgb24 : IEquatable + internal struct ColorRgbaFloat : IEquatable { - public byte r, g, b; - public ColorRgb24(byte r, byte g, byte b) + public float r, g, b, a; + + public ColorRgbaFloat(float r, float g, float b, float a) { this.r = r; this.g = g; this.b = b; + this.a = a; } - public ColorRgb24(ColorRgb565 color) { - this.r = color.R; - this.g = color.G; - this.b = color.B; - } - - public ColorRgb24(ColorRgba32 color) { - this.r = color.r; - this.g = color.g; - this.b = color.b; + public ColorRgbaFloat(ColorRgba32 other) + { + this.r = other.r / 255f; + this.g = other.g / 255f; + this.b = other.b / 255f; + this.a = other.a / 255f; } - public ColorRgb24(Rgba32 color) { - this.r = color.R; - this.g = color.G; - this.b = color.B; + public ColorRgbaFloat(float r, float g, float b) + { + this.r = r; + this.g = g; + this.b = b; + this.a = 1; } - public bool Equals(ColorRgb24 other) + public bool Equals(ColorRgbaFloat other) { - return r == other.r && g == other.g && b == other.b; + return r == other.r && g == other.g && b == other.b && a == other.a; } public override bool Equals(object obj) { - return obj is ColorRgb24 other && Equals(other); + return obj is ColorRgbaFloat other && Equals(other); } public override int GetHashCode() { unchecked { - int hashCode = r.GetHashCode(); + var hashCode = r.GetHashCode(); hashCode = (hashCode * 397) ^ g.GetHashCode(); hashCode = (hashCode * 397) ^ b.GetHashCode(); + hashCode = (hashCode * 397) ^ a.GetHashCode(); return hashCode; } } - public static bool operator ==(ColorRgb24 left, ColorRgb24 right) + public static bool operator ==(ColorRgbaFloat left, ColorRgbaFloat right) { return left.Equals(right); } - public static bool operator !=(ColorRgb24 left, ColorRgb24 right) + public static bool operator !=(ColorRgbaFloat left, ColorRgbaFloat right) { return !left.Equals(right); } - public static ColorRgb24 operator +(ColorRgb24 left, ColorRgb24 right) + public static ColorRgbaFloat operator +(ColorRgbaFloat left, ColorRgbaFloat right) { - return new ColorRgb24( - ByteHelper.ClampToByte(left.r + right.r), - ByteHelper.ClampToByte(left.g + right.g), - ByteHelper.ClampToByte(left.b + right.b)); + return new ColorRgbaFloat( + left.r + right.r, + left.g + right.g, + left.b + right.b, + left.a + right.a); } - public static ColorRgb24 operator -(ColorRgb24 left, ColorRgb24 right) + public static ColorRgbaFloat operator -(ColorRgbaFloat left, ColorRgbaFloat right) { - return new ColorRgb24( - ByteHelper.ClampToByte(left.r - right.r), - ByteHelper.ClampToByte(left.g - right.g), - ByteHelper.ClampToByte(left.b - right.b)); + return new ColorRgbaFloat( + left.r - right.r, + left.g - right.g, + left.b - right.b, + left.a - right.a); } - public static ColorRgb24 operator /(ColorRgb24 left, double right) + public static ColorRgbaFloat operator /(ColorRgbaFloat left, float right) { - return new ColorRgb24( - ByteHelper.ClampToByte((int)(left.r / right)), - ByteHelper.ClampToByte((int)(left.g / right)), - ByteHelper.ClampToByte((int)(left.b / right)) - ); + return new ColorRgbaFloat( + left.r / right, + left.g / right, + left.b / right, + left.a / right + ); } - public static ColorRgb24 operator *(ColorRgb24 left, double right) + public static ColorRgbaFloat operator *(ColorRgbaFloat left, float right) { - return new ColorRgb24( - ByteHelper.ClampToByte((int)(left.r * right)), - ByteHelper.ClampToByte((int)(left.g * right)), - ByteHelper.ClampToByte((int)(left.b * right)) + return new ColorRgbaFloat( + left.r * right, + left.g * right, + left.b * right, + left.a * right ); } - public override string ToString() { - return $"r : {r} g : {g} b : {b}"; + public static ColorRgbaFloat operator *(float left, ColorRgbaFloat right) + { + return new ColorRgbaFloat( + right.r * left, + right.g * left, + right.b * left, + right.a * left + ); + } + + public override string ToString() + { + return $"r : {r:0.00} g : {g:0.00} b : {b:0.00} a : {a:0.00}"; + } + + public ColorRgba32 ToRgba32() + { + return new ColorRgba32( + (byte)(ByteHelper.ClampToByte(r * 255)), + (byte)(ByteHelper.ClampToByte(g * 255)), + (byte)(ByteHelper.ClampToByte(b * 255)), + (byte)(ByteHelper.ClampToByte(a * 255)) + ); } + } - internal struct ColorYCbCr + public struct ColorRgbFloat : IEquatable { - public float y; - public float cb; - public float cr; + public float r, g, b; - public ColorYCbCr(float y, float cb, float cr) + public ColorRgbFloat(float r, float g, float b) { - this.y = y; - this.cb = cb; - this.cr = cr; + this.r = r; + this.g = g; + this.b = b; } - public ColorYCbCr(ColorRgb24 rgb) + public ColorRgbFloat(ColorRgba32 other) { - float fr = (float)rgb.r / 255; - float fg = (float)rgb.g / 255; - float fb = (float)rgb.b / 255; - - y = (0.2989f * fr + 0.5866f * fg + 0.1145f * fb); - cb = (-0.1687f * fr - 0.3313f * fg + 0.5000f * fb); - cr = (0.5000f * fr - 0.4184f * fg - 0.0816f * fb); + this.r = other.r / 255f; + this.g = other.g / 255f; + this.b = other.b / 255f; } - public ColorYCbCr(ColorRgb565 rgb) + public ColorRgbFloat(Vector3 vector) { - float fr = (float)rgb.R / 255; - float fg = (float)rgb.G / 255; - float fb = (float)rgb.B / 255; + r = vector.X; + g = vector.Y; + b = vector.Z; + } - y = (0.2989f * fr + 0.5866f * fg + 0.1145f * fb); - cb = (-0.1687f * fr - 0.3313f * fg + 0.5000f * fb); - cr = (0.5000f * fr - 0.4184f * fg - 0.0816f * fb); + public bool Equals(ColorRgbFloat other) + { + return r == other.r && g == other.g && b == other.b; } - public ColorYCbCr(ColorRgba32 rgba) + public override bool Equals(object obj) { - float fr = (float)rgba.r / 255; - float fg = (float)rgba.g / 255; - float fb = (float)rgba.b / 255; + return obj is ColorRgbFloat other && Equals(other); + } - y = (0.2989f * fr + 0.5866f * fg + 0.1145f * fb); - cb = (-0.1687f * fr - 0.3313f * fg + 0.5000f * fb); - cr = (0.5000f * fr - 0.4184f * fg - 0.0816f * fb); + public override int GetHashCode() + { + unchecked + { + var hashCode = r.GetHashCode(); + hashCode = (hashCode * 397) ^ g.GetHashCode(); + hashCode = (hashCode * 397) ^ b.GetHashCode(); + return hashCode; + } } - public ColorYCbCr(Rgba32 rgb) + public static bool operator ==(ColorRgbFloat left, ColorRgbFloat right) { - float fr = (float)rgb.R / 255; - float fg = (float)rgb.G / 255; - float fb = (float)rgb.B / 255; + return left.Equals(right); + } - y = (0.2989f * fr + 0.5866f * fg + 0.1145f * fb); - cb = (-0.1687f * fr - 0.3313f * fg + 0.5000f * fb); - cr = (0.5000f * fr - 0.4184f * fg - 0.0816f * fb); + public static bool operator !=(ColorRgbFloat left, ColorRgbFloat right) + { + return !left.Equals(right); } - public ColorYCbCr(Vector3 vec) { - float fr = (float) vec.X; - float fg = (float) vec.Y; - float fb = (float) vec.Z; + public static ColorRgbFloat operator +(ColorRgbFloat left, ColorRgbFloat right) + { + return new ColorRgbFloat( + left.r + right.r, + left.g + right.g, + left.b + right.b); + } - y = (0.2989f * fr + 0.5866f * fg + 0.1145f * fb); - cb = (-0.1687f * fr - 0.3313f * fg + 0.5000f * fb); - cr = (0.5000f * fr - 0.4184f * fg - 0.0816f * fb); + public static ColorRgbFloat operator -(ColorRgbFloat left, ColorRgbFloat right) + { + return new ColorRgbFloat( + left.r - right.r, + left.g - right.g, + left.b - right.b); } - public ColorRgb565 ToColorRgb565() { - float r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); - float g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); - float b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + public static ColorRgbFloat operator /(ColorRgbFloat left, float right) + { + return new ColorRgbFloat( + left.r / right, + left.g / right, + left.b / right + ); + } - return new ColorRgb565((byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + public static ColorRgbFloat operator *(ColorRgbFloat left, float right) + { + return new ColorRgbFloat( + left.r * right, + left.g * right, + left.b * right + ); } - public override string ToString() { - float r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); - float g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); - float b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + public static ColorRgbFloat operator *(float left, ColorRgbFloat right) + { + return new ColorRgbFloat( + right.r * left, + right.g * left, + right.b * left + ); + } - return $"r : {r * 255} g : {g * 255} b : {b * 255}"; + public override string ToString() + { + return $"r : {r:0.00} g : {g:0.00} b : {b:0.00}"; } - public float CalcDistWeighted(ColorYCbCr other, float yWeight = 4) { - float dy = (y - other.y) * (y - other.y) * yWeight; - float dcb = (cb - other.cb) * (cb - other.cb); - float dcr = (cr - other.cr) * (cr - other.cr); + public ColorRgba32 ToRgba32() + { + return new ColorRgba32( + (byte)(ByteHelper.ClampToByte(r * 255)), + (byte)(ByteHelper.ClampToByte(g * 255)), + (byte)(ByteHelper.ClampToByte(b * 255)), + 255 + ); + } - return MathF.Sqrt(dy + dcb + dcr); + public Vector3 ToVector3() + { + return new Vector3(r, g, b); } - public static ColorYCbCr operator+(ColorYCbCr left, ColorYCbCr right) + internal float CalcLogDist(ColorRgbFloat other) { - return new ColorYCbCr( - left.y + right.y, - left.cb + right.cb, - left.cr + right.cr); + var dr = Math.Sign(other.r) * MathF.Log(1 + MathF.Abs(other.r)) - Math.Sign(r) * MathF.Log(1 + MathF.Abs(r)); + var dg = Math.Sign(other.g) * MathF.Log(1 + MathF.Abs(other.g)) - Math.Sign(g) * MathF.Log(1 + MathF.Abs(g)); + var db = Math.Sign(other.b) * MathF.Log(1 + MathF.Abs(other.b)) - Math.Sign(b) * MathF.Log(1 + MathF.Abs(b)); + return MathF.Sqrt((dr * dr) + (dg * dg) + (db * db)); } - public static ColorYCbCr operator/(ColorYCbCr left, float right) + internal float CalcDist(ColorRgbFloat other) { - return new ColorYCbCr( - left.y / right, - left.cb / right, - left.cr / right); + var dr = other.r - r; + var dg = other.g - g; + var db = other.b - b; + return MathF.Sqrt((dr * dr) + (dg * dg) + (db * db)); } - public Rgba32 ToRgba32() { - float r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); - float g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); - float b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + internal void ClampToPositive() + { + if (r < 0) r = 0; + if (g < 0) g = 0; + if (b < 0) b = 0; + } - return new Rgba32((byte)(r * 255), (byte)(g * 255), (byte)(b * 255), 255); + internal void ClampToHalf() + { + if (r < Half.MinValue) r = Half.MinValue; + else if (g > Half.MaxValue) g = Half.MaxValue; + if (b < Half.MinValue) b = Half.MinValue; + else if (r > Half.MaxValue) r = Half.MaxValue; + if (g < Half.MinValue) g = Half.MinValue; + else if (b > Half.MaxValue) b = Half.MaxValue; } } - internal struct ColorYCbCrAlpha + internal struct ColorYCbCr { public float y; public float cb; public float cr; - public float alpha; - public ColorYCbCrAlpha(float y, float cb, float cr, float alpha) + public ColorYCbCr(float y, float cb, float cr) { this.y = y; this.cb = cb; this.cr = cr; - this.alpha = alpha; } - public ColorYCbCrAlpha(ColorRgb24 rgb) + internal ColorYCbCr(ColorRgb24 rgb) { - float fr = (float)rgb.r / 255; - float fg = (float)rgb.g / 255; - float fb = (float)rgb.b / 255; + var fr = (float)rgb.r / 255; + var fg = (float)rgb.g / 255; + var fb = (float)rgb.b / 255; - y = (0.2989f * fr + 0.5866f * fg + 0.1145f * fb); - cb = (-0.1687f * fr - 0.3313f * fg + 0.5000f * fb); - cr = (0.5000f * fr - 0.4184f * fg - 0.0816f * fb); - alpha = 1; + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; } - public ColorYCbCrAlpha(ColorRgb565 rgb) + internal ColorYCbCr(ColorRgbaFloat rgb) { - float fr = (float)rgb.R / 255; - float fg = (float)rgb.G / 255; - float fb = (float)rgb.B / 255; + var fr = rgb.r; + var fg = rgb.g; + var fb = rgb.b; - y = (0.2989f * fr + 0.5866f * fg + 0.1145f * fb); - cb = (-0.1687f * fr - 0.3313f * fg + 0.5000f * fb); - cr = (0.5000f * fr - 0.4184f * fg - 0.0816f * fb); - alpha = 1; + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; } - public ColorYCbCrAlpha(ColorRgba32 rgba) + internal ColorYCbCr(ColorRgbFloat rgb) { - float fr = (float)rgba.r / 255; - float fg = (float)rgba.g / 255; - float fb = (float)rgba.b / 255; + var fr = rgb.r; + var fg = rgb.g; + var fb = rgb.b; - y = (0.2989f * fr + 0.5866f * fg + 0.1145f * fb); - cb = (-0.1687f * fr - 0.3313f * fg + 0.5000f * fb); - cr = (0.5000f * fr - 0.4184f * fg - 0.0816f * fb); - alpha = rgba.a / 255f; + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; } - public ColorYCbCrAlpha(Rgba32 rgb) + internal ColorYCbCr(ColorRgb565 rgb) { - float fr = (float)rgb.R / 255; - float fg = (float)rgb.G / 255; - float fb = (float)rgb.B / 255; + var fr = (float)rgb.R / 255; + var fg = (float)rgb.G / 255; + var fb = (float)rgb.B / 255; - y = (0.2989f * fr + 0.5866f * fg + 0.1145f * fb); - cb = (-0.1687f * fr - 0.3313f * fg + 0.5000f * fb); - cr = (0.5000f * fr - 0.4184f * fg - 0.0816f * fb); - alpha = rgb.A / 255f; + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; } + public ColorYCbCr(ColorRgba32 rgba) + { + var fr = (float)rgba.r / 255; + var fg = (float)rgba.g / 255; + var fb = (float)rgba.b / 255; - public ColorRgb565 ToColorRgb565() { - float r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); - float g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); - float b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + } + + public ColorYCbCr(Vector3 vec) + { + var fr = (float)vec.X; + var fg = (float)vec.Y; + var fb = (float)vec.Z; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + } + + public ColorRgb565 ToColorRgb565() + { + var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); + var g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); + var b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + + return new ColorRgb565((byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + } + + public ColorRgba32 ToColorRgba32() + { + var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); + var g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); + var b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + + return new ColorRgba32((byte)(r * 255), (byte)(g * 255), (byte)(b * 255), 255); + } + + public override string ToString() + { + var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); + var g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); + var b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + + return $"r : {r * 255} g : {g * 255} b : {b * 255}"; + } + + public float CalcDistWeighted(ColorYCbCr other, float yWeight = 4) + { + var dy = (y - other.y) * (y - other.y) * yWeight; + var dcb = (cb - other.cb) * (cb - other.cb); + var dcr = (cr - other.cr) * (cr - other.cr); + + return MathF.Sqrt(dy + dcb + dcr); + } + + public static ColorYCbCr operator +(ColorYCbCr left, ColorYCbCr right) + { + return new ColorYCbCr( + left.y + right.y, + left.cb + right.cb, + left.cr + right.cr); + } + + public static ColorYCbCr operator /(ColorYCbCr left, float right) + { + return new ColorYCbCr( + left.y / right, + left.cb / right, + left.cr / right); + } + } + + internal struct ColorRgb555 : IEquatable + { + public bool Equals(ColorRgb555 other) + { + return data == other.data; + } + + public override bool Equals(object obj) + { + return obj is ColorRgb555 other && Equals(other); + } + + public override int GetHashCode() + { + return data.GetHashCode(); + } + + public static bool operator ==(ColorRgb555 left, ColorRgb555 right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgb555 left, ColorRgb555 right) + { + return !left.Equals(right); + } + + private const ushort ModeMask = 0b1_00000_00000_00000; + private const int ModeShift = 15; + private const ushort RedMask = 0b0_11111_00000_00000; + private const int RedShift = 10; + private const ushort GreenMask = 0b0_00000_11111_00000; + private const int GreenShift = 5; + private const ushort BlueMask = 0b0_00000_00000_11111; + + public ushort data; + + public byte Mode + { + readonly get + { + var mode = (data & ModeMask) >> ModeShift; + return (byte)mode; + } + set + { + var mode = value; + data = (ushort)(data & ~ModeMask); + data = (ushort)(data | (mode << ModeShift)); + } + } + + public byte R + { + readonly get + { + var r5 = (data & RedMask) >> RedShift; + return (byte)((r5 << 3) | (r5 >> 2)); + } + set + { + var r5 = value >> 3; + data = (ushort)(data & ~RedMask); + data = (ushort)(data | (r5 << RedShift)); + } + } + + public byte G + { + readonly get + { + var g5 = (data & GreenMask) >> GreenShift; + return (byte)((g5 << 3) | (g5 >> 2)); + } + set + { + var g5 = value >> 3; + data = (ushort)(data & ~GreenMask); + data = (ushort)(data | (g5 << GreenShift)); + } + } + + public byte B + { + readonly get + { + var b5 = data & BlueMask; + return (byte)((b5 << 3) | (b5 >> 2)); + } + set + { + var b5 = value >> 3; + data = (ushort)(data & ~BlueMask); + data = (ushort)(data | b5); + } + } + + public int RawR + { + readonly get => (data & RedMask) >> RedShift; + set + { + if (value > 31) value = 31; + if (value < 0) value = 0; + data = (ushort)(data & ~RedMask); + data = (ushort)(data | (value << RedShift)); + } + } + + public int RawG + { + readonly get => (data & GreenMask) >> GreenShift; + set + { + if (value > 31) value = 31; + if (value < 0) value = 0; + data = (ushort)(data & ~GreenMask); + data = (ushort)(data | (value << GreenShift)); + } + } + + public int RawB + { + readonly get => data & BlueMask; + set + { + if (value > 31) value = 31; + if (value < 0) value = 0; + data = (ushort)(data & ~BlueMask); + data = (ushort)(data | value); + } + } + + public ColorRgb555(byte r, byte g, byte b) + { + data = 0; + R = r; + G = g; + B = b; + } + + public ColorRgb555(Vector3 colorVector) + { + data = 0; + R = ByteHelper.ClampToByte(colorVector.X * 255); + G = ByteHelper.ClampToByte(colorVector.Y * 255); + B = ByteHelper.ClampToByte(colorVector.Z * 255); + } + + public ColorRgb555(ColorRgb24 color) + { + data = 0; + R = color.r; + G = color.g; + B = color.b; + } + + public readonly ColorRgb24 ToColorRgb24() + { + return new ColorRgb24(R, G, B); + } + + public override string ToString() + { + return $"r : {R} g : {G} b : {B}"; + } + + public ColorRgba32 ToColorRgba32() + { + return new ColorRgba32(R, G, B, 255); + } + } + + internal struct ColorRgb565 : IEquatable + { + public bool Equals(ColorRgb565 other) + { + return data == other.data; + } + + public override bool Equals(object obj) + { + return obj is ColorRgb565 other && Equals(other); + } + + public override int GetHashCode() + { + return data.GetHashCode(); + } + + public static bool operator ==(ColorRgb565 left, ColorRgb565 right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgb565 left, ColorRgb565 right) + { + return !left.Equals(right); + } + + private const ushort RedMask = 0b11111_000000_00000; + private const int RedShift = 11; + private const ushort GreenMask = 0b00000_111111_00000; + private const int GreenShift = 5; + private const ushort BlueMask = 0b00000_000000_11111; + + public ushort data; + + public byte R + { + readonly get + { + var r5 = (data & RedMask) >> RedShift; + return (byte)((r5 << 3) | (r5 >> 2)); + } + set + { + var r5 = value >> 3; + data = (ushort)(data & ~RedMask); + data = (ushort)(data | (r5 << RedShift)); + } + } + + public byte G + { + readonly get + { + var g6 = (data & GreenMask) >> GreenShift; + return (byte)((g6 << 2) | (g6 >> 4)); + } + set + { + var g6 = value >> 2; + data = (ushort)(data & ~GreenMask); + data = (ushort)(data | (g6 << GreenShift)); + } + } + + public byte B + { + readonly get + { + var b5 = data & BlueMask; + return (byte)((b5 << 3) | (b5 >> 2)); + } + set + { + var b5 = value >> 3; + data = (ushort)(data & ~BlueMask); + data = (ushort)(data | b5); + } + } + + public int RawR + { + readonly get { return (data & RedMask) >> RedShift; } + set + { + if (value > 31) value = 31; + if (value < 0) value = 0; + data = (ushort)(data & ~RedMask); + data = (ushort)(data | (value << RedShift)); + } + } + + public int RawG + { + readonly get { return (data & GreenMask) >> GreenShift; } + set + { + if (value > 63) value = 63; + if (value < 0) value = 0; + data = (ushort)(data & ~GreenMask); + data = (ushort)(data | (value << GreenShift)); + } + } + + public int RawB + { + readonly get { return data & BlueMask; } + set + { + if (value > 31) value = 31; + if (value < 0) value = 0; + data = (ushort)(data & ~BlueMask); + data = (ushort)(data | value); + } + } + + public ColorRgb565(byte r, byte g, byte b) + { + data = 0; + R = r; + G = g; + B = b; + } + + public ColorRgb565(Vector3 colorVector) + { + data = 0; + R = ByteHelper.ClampToByte(colorVector.X * 255); + G = ByteHelper.ClampToByte(colorVector.Y * 255); + B = ByteHelper.ClampToByte(colorVector.Z * 255); + } + + public ColorRgb565(ColorRgb24 color) + { + data = 0; + R = color.r; + G = color.g; + B = color.b; + } + + public readonly ColorRgb24 ToColorRgb24() + { + return new ColorRgb24(R, G, B); + } + + public override string ToString() + { + return $"r : {R} g : {G} b : {B}"; + } + + public ColorRgba32 ToColorRgba32() + { + return new ColorRgba32(R, G, B, 255); + } + } + + internal struct ColorRgb24 : IEquatable + { + public byte r, g, b; + + public ColorRgb24(byte r, byte g, byte b) + { + this.r = r; + this.g = g; + this.b = b; + } + + public ColorRgb24(ColorRgb565 color) + { + this.r = color.R; + this.g = color.G; + this.b = color.B; + } + + public ColorRgb24(ColorRgba32 color) + { + this.r = color.r; + this.g = color.g; + this.b = color.b; + } + + public bool Equals(ColorRgb24 other) + { + return r == other.r && g == other.g && b == other.b; + } + + public override bool Equals(object obj) + { + return obj is ColorRgb24 other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = r.GetHashCode(); + hashCode = (hashCode * 397) ^ g.GetHashCode(); + hashCode = (hashCode * 397) ^ b.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ColorRgb24 left, ColorRgb24 right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgb24 left, ColorRgb24 right) + { + return !left.Equals(right); + } + + public static ColorRgb24 operator +(ColorRgb24 left, ColorRgb24 right) + { + return new ColorRgb24( + ByteHelper.ClampToByte(left.r + right.r), + ByteHelper.ClampToByte(left.g + right.g), + ByteHelper.ClampToByte(left.b + right.b)); + } + + public static ColorRgb24 operator -(ColorRgb24 left, ColorRgb24 right) + { + return new ColorRgb24( + ByteHelper.ClampToByte(left.r - right.r), + ByteHelper.ClampToByte(left.g - right.g), + ByteHelper.ClampToByte(left.b - right.b)); + } + + public static ColorRgb24 operator /(ColorRgb24 left, double right) + { + return new ColorRgb24( + ByteHelper.ClampToByte((int)(left.r / right)), + ByteHelper.ClampToByte((int)(left.g / right)), + ByteHelper.ClampToByte((int)(left.b / right)) + ); + } + + public static ColorRgb24 operator *(ColorRgb24 left, double right) + { + return new ColorRgb24( + ByteHelper.ClampToByte((int)(left.r * right)), + ByteHelper.ClampToByte((int)(left.g * right)), + ByteHelper.ClampToByte((int)(left.b * right)) + ); + } + + public override string ToString() + { + return $"r : {r} g : {g} b : {b}"; + } + } + + internal struct ColorYCbCrAlpha + { + public float y; + public float cb; + public float cr; + public float alpha; + + public ColorYCbCrAlpha(float y, float cb, float cr, float alpha) + { + this.y = y; + this.cb = cb; + this.cr = cr; + this.alpha = alpha; + } + + public ColorYCbCrAlpha(ColorRgb24 rgb) + { + var fr = (float)rgb.r / 255; + var fg = (float)rgb.g / 255; + var fb = (float)rgb.b / 255; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + alpha = 1; + } + + public ColorYCbCrAlpha(ColorRgb565 rgb) + { + var fr = (float)rgb.R / 255; + var fg = (float)rgb.G / 255; + var fb = (float)rgb.B / 255; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + alpha = 1; + } + + public ColorYCbCrAlpha(ColorRgba32 rgba) + { + var fr = (float)rgba.r / 255; + var fg = (float)rgba.g / 255; + var fb = (float)rgba.b / 255; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + alpha = rgba.a / 255f; + } + + public ColorYCbCrAlpha(ColorRgbaFloat rgba) + { + var fr = rgba.r; + var fg = rgba.g; + var fb = rgba.b; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + alpha = rgba.a; + } + + + public ColorRgb565 ToColorRgb565() + { + var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); + var g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); + var b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); return new ColorRgb565((byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); } - public override string ToString() { - float r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); - float g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); - float b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + public override string ToString() + { + var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); + var g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); + var b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); return $"r : {r * 255} g : {g * 255} b : {b * 255}"; } - public float CalcDistWeighted(ColorYCbCrAlpha other, float yWeight = 4, float aWeight = 1) { - float dy = (y - other.y) * (y - other.y) * yWeight; - float dcb = (cb - other.cb) * (cb - other.cb); - float dcr = (cr - other.cr) * (cr - other.cr); - float da = (alpha - other.alpha) * (alpha - other.alpha) * aWeight; + public float CalcDistWeighted(ColorYCbCrAlpha other, float yWeight = 4, float aWeight = 1) + { + var dy = (y - other.y) * (y - other.y) * yWeight; + var dcb = (cb - other.cb) * (cb - other.cb); + var dcr = (cr - other.cr) * (cr - other.cr); + var da = (alpha - other.alpha) * (alpha - other.alpha) * aWeight; return MathF.Sqrt(dy + dcb + dcr + da); } - public static ColorYCbCrAlpha operator+(ColorYCbCrAlpha left, ColorYCbCrAlpha right) + public static ColorYCbCrAlpha operator +(ColorYCbCrAlpha left, ColorYCbCrAlpha right) { return new ColorYCbCrAlpha( left.y + right.y, @@ -629,7 +1123,7 @@ public float CalcDistWeighted(ColorYCbCrAlpha other, float yWeight = 4, float aW left.alpha + right.alpha); } - public static ColorYCbCrAlpha operator/(ColorYCbCrAlpha left, float right) + public static ColorYCbCrAlpha operator /(ColorYCbCrAlpha left, float right) { return new ColorYCbCrAlpha( left.y / right, @@ -637,90 +1131,208 @@ public float CalcDistWeighted(ColorYCbCrAlpha other, float yWeight = 4, float aW left.cr / right, left.alpha / right); } - - public Rgba32 ToRgba32() { - float r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); - float g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); - float b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); - - return new Rgba32((byte)(r * 255), (byte)(g * 255), (byte)(b * 255), (byte)(alpha * 255)); - } } - internal struct ColorXyz { + internal struct ColorXyz + { public float x; public float y; public float z; - public ColorXyz(float x, float y, float z) { + public ColorXyz(float x, float y, float z) + { this.x = x; this.y = y; this.z = z; } - public ColorXyz(ColorRgb24 color) { + public ColorXyz(ColorRgb24 color) + { this = ColorToXyz(color); } - public static ColorXyz ColorToXyz(ColorRgb24 color) { - float r = PivotRgb(color.r / 255.0f); - float g = PivotRgb(color.g / 255.0f); - float b = PivotRgb(color.b / 255.0f); + public ColorXyz(ColorRgbFloat color) + { + this = ColorToXyz(color); + } + + public ColorRgbFloat ToColorRgbFloat() + { + return new ColorRgbFloat( + 3.2404542f * x - 1.5371385f * y - 0.4985314f * z, + -0.9692660f * x + 1.8760108f * y + 0.0415560f * z, + 0.0556434f * x - 0.2040259f * y + 1.0572252f * z + ); + } + + public static ColorXyz ColorToXyz(ColorRgb24 color) + { + var r = PivotRgb(color.r / 255.0f); + var g = PivotRgb(color.g / 255.0f); + var b = PivotRgb(color.b / 255.0f); + + // Observer. = 2°, Illuminant = D65 + return new ColorXyz(r * 0.4124f + g * 0.3576f + b * 0.1805f, r * 0.2126f + g * 0.7152f + b * 0.0722f, r * 0.0193f + g * 0.1192f + b * 0.9505f); + } + + public static ColorXyz ColorToXyz(ColorRgbFloat color) + { + var r = PivotRgb(color.r); + var g = PivotRgb(color.g); + var b = PivotRgb(color.b); // Observer. = 2°, Illuminant = D65 return new ColorXyz(r * 0.4124f + g * 0.3576f + b * 0.1805f, r * 0.2126f + g * 0.7152f + b * 0.0722f, r * 0.0193f + g * 0.1192f + b * 0.9505f); } - private static float PivotRgb(float n) { + private static float PivotRgb(float n) + { return (n > 0.04045f ? MathF.Pow((n + 0.055f) / 1.055f, 2.4f) : n / 12.92f) * 100; } } - - internal struct ColorLab { + internal struct ColorLab + { public float l; public float a; public float b; - public ColorLab(float l, float a, float b) { + public ColorLab(float l, float a, float b) + { this.l = l; this.a = a; this.b = b; } - public ColorLab(ColorRgb24 color) { + public ColorLab(ColorRgb24 color) + { this = ColorToLab(color); } - public ColorLab(ColorRgba32 color) { + public ColorLab(ColorRgba32 color) + { this = ColorToLab(new ColorRgb24(color.r, color.g, color.b)); } - public ColorLab(Rgba32 color) { - this = ColorToLab(new ColorRgb24(color.R, color.G, color.B)); + public ColorLab(ColorRgbFloat color) + { + this = XyzToLab(new ColorXyz(color)); } - public static ColorLab ColorToLab(ColorRgb24 color) { - ColorXyz xyz = new ColorXyz(color); + public static ColorLab ColorToLab(ColorRgb24 color) + { + var xyz = new ColorXyz(color); return XyzToLab(xyz); } - public static ColorLab XyzToLab(ColorXyz xyz) { - float REF_X = 95.047f; // Observer= 2°, Illuminant= D65 - float REF_Y = 100.000f; - float REF_Z = 108.883f; + public static ColorLab XyzToLab(ColorXyz xyz) + { + var refX = 95.047f; // Observer= 2°, Illuminant= D65 + var refY = 100.000f; + var refZ = 108.883f; - float x = PivotXyz(xyz.x / REF_X); - float y = PivotXyz(xyz.y / REF_Y); - float z = PivotXyz(xyz.z / REF_Z); + var x = PivotXyz(xyz.x / refX); + var y = PivotXyz(xyz.y / refY); + var z = PivotXyz(xyz.z / refZ); return new ColorLab(116 * y - 16, 500 * (x - y), 200 * (y - z)); } - private static float PivotXyz(float n) { - float i = MathF.Cbrt(n); + private static float PivotXyz(float n) + { + var i = MathCbrt.Cbrt(n); return n > 0.008856f ? i : 7.787f * n + 16 / 116f; } } -} \ No newline at end of file + + internal struct ColorRgbe : IEquatable + { + public byte r; + public byte g; + public byte b; + public byte e; + + public ColorRgbe(byte r, byte g, byte b, byte e) + { + this.r = r; + this.g = g; + this.b = b; + this.e = e; + } + + public ColorRgbe(ColorRgbFloat color) + { + var max = MathF.Max(color.b, MathF.Max(color.g, color.r)); + if (max <= 1e-32f) + { + r = g = b = e = 0; + } + else + { + MathHelper.FrExp(max, out var exponent); + var scale = MathHelper.LdExp(1f, -exponent + 8); + r = (byte)(scale * color.r); + g = (byte)(scale * color.g); + b = (byte)(scale * color.b); + e = (byte)(exponent + 128); + } + } + + public ColorRgbFloat ToColorRgbFloat(float exposure = 1.0f) + { + if (e == 0) + { + return new ColorRgbFloat(0, 0, 0); + } + else + { + var fexp = MathHelper.LdExp(1f, e - (128 + 8)) / exposure; + + return new ColorRgbFloat( + (r + 0.5f) * fexp, + (g + 0.5f) * fexp, + (b + 0.5f) * fexp + ); + } + } + + + public bool Equals(ColorRgbe other) + { + return r == other.r && g == other.g && b == other.b && e == other.e; + } + + public override bool Equals(object obj) + { + return obj is ColorRgbe other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = r.GetHashCode(); + hashCode = (hashCode * 397) ^ g.GetHashCode(); + hashCode = (hashCode * 397) ^ b.GetHashCode(); + hashCode = (hashCode * 397) ^ e.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ColorRgbe left, ColorRgbe right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgbe left, ColorRgbe right) + { + return !left.Equals(right); + } + + public override string ToString() + { + return $"{nameof(r)}: {r}, {nameof(g)}: {g}, {nameof(b)}: {b}, {nameof(e)}: {e}"; + } + } +} diff --git a/BCnEnc.Net/Shared/ComponentHelper.cs b/BCnEnc.Net/Shared/ComponentHelper.cs new file mode 100644 index 0000000..268778c --- /dev/null +++ b/BCnEnc.Net/Shared/ComponentHelper.cs @@ -0,0 +1,87 @@ +using System; +using BCnEncoder.Encoder; + +namespace BCnEncoder.Shared +{ + internal static class ComponentHelper + { + public static ColorRgba32 ComponentToColor(ColorComponent component, byte componentValue) + { + switch (component) + { + case ColorComponent.R: + return new ColorRgba32(componentValue, 0, 0, 255); + + case ColorComponent.G: + return new ColorRgba32(0, componentValue, 0, 255); + + case ColorComponent.B: + return new ColorRgba32(0, 0, componentValue, 255); + + case ColorComponent.A: + return new ColorRgba32(0, 0, 0, componentValue); + + case ColorComponent.Luminance: + return new ColorRgba32(componentValue, componentValue, componentValue, 255); + + default: + throw new InvalidOperationException("Unsupported component."); + } + } + + public static ColorRgba32 ComponentToColor(ColorRgba32 existingColor, ColorComponent component, byte componentValue) + { + switch (component) + { + case ColorComponent.R: + existingColor.r = componentValue; + break; + + case ColorComponent.G: + existingColor.g = componentValue; + break; + + case ColorComponent.B: + existingColor.b = componentValue; + break; + + case ColorComponent.A: + existingColor.a = componentValue; + break; + + case ColorComponent.Luminance: + existingColor.r = existingColor.g = existingColor.b = componentValue; + break; + + default: + throw new InvalidOperationException("Unsupported component."); + } + + return existingColor; + } + + public static byte ColorToComponent(ColorRgba32 color, ColorComponent component) + { + switch (component) + { + case ColorComponent.R: + return color.r; + + case ColorComponent.G: + return color.g; + + case ColorComponent.B: + return color.b; + + case ColorComponent.A: + return color.a; + + case ColorComponent.Luminance: + return (byte)(new ColorYCbCr(color).y * 255); + + default: + throw new InvalidOperationException("Unsupported component."); + } + } + } +} diff --git a/BCnEnc.Net/Shared/CompressionFormat.cs b/BCnEnc.Net/Shared/CompressionFormat.cs index 04bf275..9d7545d 100644 --- a/BCnEnc.Net/Shared/CompressionFormat.cs +++ b/BCnEnc.Net/Shared/CompressionFormat.cs @@ -1,69 +1,109 @@ -namespace BCnEncoder.Shared +namespace BCnEncoder.Shared { - public enum CompressionFormat - { + public enum CompressionFormat + { + /// + /// Raw unsigned byte 8-bit Luminance data + /// + R, + /// + /// Raw unsigned byte 16-bit RG data + /// + Rg, + /// + /// Raw unsigned byte 24-bit RGB data + /// + Rgb, + /// + /// Raw unsigned byte 32-bit RGBA data + /// + Rgba, /// - /// Raw unsigned byte 8-bit Luminance data + /// Raw unsigned byte 32-bit BGRA data /// - R, - /// - /// Raw unsigned byte 16-bit RG data - /// - RG, - /// - /// Raw unsigned byte 24-bit RGB data - /// - RGB, - /// - /// Raw unsigned byte 32-bit RGBA data - /// - RGBA, + Bgra, /// /// BC1 / DXT1 with no alpha. Very widely supported and good compression ratio. /// - BC1, - /// - /// BC1 / DXT1 with 1-bit of alpha. - /// - BC1WithAlpha, + Bc1, + /// + /// BC1 / DXT1 with 1-bit of alpha. + /// + Bc1WithAlpha, + /// + /// BC2 / DXT3 encoding with alpha. Good for sharp alpha transitions. + /// + Bc2, + /// + /// BC3 / DXT5 encoding with alpha. Good for smooth alpha transitions. + /// + Bc3, + /// + /// BC4 single-channel encoding. Only luminance is encoded. + /// + Bc4, + /// + /// BC5 dual-channel encoding. Only red and green channels are encoded. + /// + Bc5, + /// + /// BC6H / BPTC unsigned float encoding. Can compress HDR textures without alpha. Does not support negative values. + /// + Bc6U, /// - /// BC2 / DXT3 encoding with alpha. Good for sharp alpha transitions. + /// BC6H / BPTC signed float encoding. Can compress HDR textures without alpha. Supports negative values. /// - BC2, + Bc6S, /// - /// BC3 / DXT5 encoding with alpha. Good for smooth alpha transitions. + /// BC7 / BPTC unorm encoding. Very high Quality rgba or rgb encoding. Also very slow. /// - BC3, + Bc7, /// - /// BC4 single-channel encoding. Only luminance is encoded. + /// ATC / Adreno Texture Compression encoding. Derivative of BC1. /// - BC4, + Atc, /// - /// BC5 dual-channel encoding. Only red and green channels are encoded. + /// ATC / Adreno Texture Compression encoding. Derivative of BC2. Good for sharp alpha transitions. /// - BC5, + AtcExplicitAlpha, /// - /// BC6H / BPTC float encoding. Can compress HDR textures without alpha. Currently not supported. + /// ATC / Adreno Texture Compression encoding. Derivative of BC3. Good for smooth alpha transitions. /// - BC6, + AtcInterpolatedAlpha, /// - /// BC7 / BPTC unorm encoding. Very high quality rgba or rgb encoding. Also very slow. + /// Unknown format /// - BC7 + Unknown } - public static class CompressionFormatExtensions { - public static bool IsCompressedFormat(this CompressionFormat format) + public static class CompressionFormatExtensions + { + public static bool IsCompressedFormat(this CompressionFormat format) + { + switch (format) + { + case CompressionFormat.R: + case CompressionFormat.Rg: + case CompressionFormat.Rgb: + case CompressionFormat.Rgba: + case CompressionFormat.Bgra: + return false; + + default: + return true; + } + } + + public static bool IsHdrFormat(this CompressionFormat format) { - switch (format) { - case CompressionFormat.R: - case CompressionFormat.RG: - case CompressionFormat.RGB: - case CompressionFormat.RGBA: - return false; - - default: + switch (format) + { + case CompressionFormat.Bc6S: + case CompressionFormat.Bc6U: return true; + + default: + return false; } } } diff --git a/BCnEnc.Net/Shared/DdsFile.cs b/BCnEnc.Net/Shared/DdsFile.cs deleted file mode 100644 index 229c529..0000000 --- a/BCnEnc.Net/Shared/DdsFile.cs +++ /dev/null @@ -1,992 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; - -namespace BCnEncoder.Shared -{ - public class DdsFile - { - public DdsHeader Header; - public DdsHeaderDxt10 Dxt10Header; - public List Faces { get; } = new List(); - - public DdsFile() { } - public DdsFile(DdsHeader header) - { - this.Header = header; - } - - public DdsFile(DdsHeader header, DdsHeaderDxt10 dxt10Header) - { - this.Header = header; - this.Dxt10Header = dxt10Header; - } - - public static DdsFile Load(Stream s) - { - using (BinaryReader br = new BinaryReader(s, Encoding.UTF8, true)) - { - var magic = br.ReadUInt32(); - if (magic != 0x20534444U) - { - throw new FormatException("The file does not contain a dds file."); - } - DdsHeader header = br.ReadStruct(); - DdsHeaderDxt10 dxt10Header = default; - if (header.dwSize != 124) - { - throw new FormatException("The file header contains invalid dwSize."); - } - - bool dxt10Format = header.ddsPixelFormat.IsDxt10Format; - - DdsFile output; - - if (dxt10Format) - { - dxt10Header = br.ReadStruct(); - output = new DdsFile(header, dxt10Header); - } - else - { - output = new DdsFile(header); - } - - uint mipMapCount = (header.dwCaps & HeaderCaps.DDSCAPS_MIPMAP) != 0 ? header.dwMipMapCount : 1; - uint faceCount = ((header.dwCaps2 & HeaderCaps2.DDSCAPS2_CUBEMAP) != 0) ? (uint)6 : (uint)1; - uint width = header.dwWidth; - uint height = header.dwHeight; - - for (int face = 0; face < faceCount; face++) - { - uint sizeInBytes = Math.Max(1, ((width + 3) / 4)) * Math.Max(1, ((height + 3) / 4)); - if (!dxt10Format) - { - if (header.ddsPixelFormat.IsDxt1To5CompressedFormat) - { - if (header.ddsPixelFormat.DxgiFormat == DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM) - { - sizeInBytes *= 8; - } - else - { - sizeInBytes *= 16; - } - } - else - { - sizeInBytes = header.dwPitchOrLinearSize * height; - } - } - else if (dxt10Header.dxgiFormat.IsCompressedFormat()) - { - sizeInBytes = (uint)(sizeInBytes * dxt10Header.dxgiFormat.GetByteSize()); - } - else - { - sizeInBytes = header.dwPitchOrLinearSize * height; - } - output.Faces.Add(new DdsFace(width, height, sizeInBytes, (int)mipMapCount)); - - for (int mip = 0; mip < mipMapCount; mip++) - { - uint mipWidth = header.dwWidth / (uint)(Math.Pow(2, mip)); - uint mipHeight = header.dwHeight / (uint)(Math.Pow(2, mip)); - - if (mip > 0) //Calculate new byteSize - { - sizeInBytes = Math.Max(1, ((mipWidth + 3) / 4)) * Math.Max(1, ((mipHeight + 3) / 4)); - if (!dxt10Format) - { - if (header.ddsPixelFormat.IsDxt1To5CompressedFormat) - { - if (header.ddsPixelFormat.DxgiFormat == DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM) - { - sizeInBytes *= 8; - } - else - { - sizeInBytes *= 16; - } - } - else - { - sizeInBytes = header.dwPitchOrLinearSize / (uint)(Math.Pow(2, mip)) * mipHeight; - } - } - else if (dxt10Header.dxgiFormat.IsCompressedFormat()) - { - sizeInBytes = (uint)(sizeInBytes * dxt10Header.dxgiFormat.GetByteSize()); - } - else - { - sizeInBytes = header.dwPitchOrLinearSize / (uint)(Math.Pow(2, mip)) * mipHeight; - } - } - byte[] data = new byte[sizeInBytes]; - br.Read(data); - output.Faces[face].MipMaps[mip] = new DdsMipMap(data, mipWidth, mipHeight); - } - } - - return output; - } - } - - public void Write(Stream outputStream) - { - if (Faces.Count < 1 || Faces[0].MipMaps.Length < 1) - { - throw new InvalidOperationException("The DDS structure should have at least 1 mipmap level and 1 Face before writing to file."); - } - - Header.dwFlags |= HeaderFlags.REQUIRED; - - Header.dwMipMapCount = (uint)Faces[0].MipMaps.Length; - if (Header.dwMipMapCount > 1) // MipMaps - { - Header.dwCaps |= HeaderCaps.DDSCAPS_MIPMAP | HeaderCaps.DDSCAPS_COMPLEX; - } - if (Faces.Count == 6) // CubeMap - { - Header.dwCaps |= HeaderCaps.DDSCAPS_COMPLEX; - Header.dwCaps2 |= HeaderCaps2.DDSCAPS2_CUBEMAP | - HeaderCaps2.DDSCAPS2_CUBEMAP_POSITIVEX | - HeaderCaps2.DDSCAPS2_CUBEMAP_NEGATIVEX | - HeaderCaps2.DDSCAPS2_CUBEMAP_POSITIVEY | - HeaderCaps2.DDSCAPS2_CUBEMAP_NEGATIVEY | - HeaderCaps2.DDSCAPS2_CUBEMAP_POSITIVEZ | - HeaderCaps2.DDSCAPS2_CUBEMAP_NEGATIVEZ; - } - - Header.dwWidth = Faces[0].Width; - Header.dwHeight = Faces[0].Height; - - for (int i = 0; i < Faces.Count; i++) - { - if (Faces[i].Width != Header.dwWidth || Faces[i].Height != Header.dwHeight) - { - throw new InvalidOperationException("Faces with different sizes are not supported."); - } - } - - int faceCount = Faces.Count; - int mipCount = (int)Header.dwMipMapCount; - - using (BinaryWriter bw = new BinaryWriter(outputStream, Encoding.UTF8, true)) - { - bw.Write(0x20534444U); // magic 'DDS ' - - bw.WriteStruct(Header); - - if (Header.ddsPixelFormat.IsDxt10Format) - { - bw.WriteStruct(Dxt10Header); - } - - for (int face = 0; face < faceCount; face++) - { - for (int mip = 0; mip < mipCount; mip++) - { - bw.Write(Faces[face].MipMaps[mip].Data); - } - } - } - } - } - - [StructLayout(LayoutKind.Sequential)] - public unsafe struct DdsHeader - { - /// - /// Has to be 124 - /// - public uint dwSize; - public HeaderFlags dwFlags; - public uint dwHeight; - public uint dwWidth; - public uint dwPitchOrLinearSize; - public uint dwDepth; - public uint dwMipMapCount; - public fixed uint dwReserved1[11]; - public DdsPixelFormat ddsPixelFormat; - public HeaderCaps dwCaps; - public HeaderCaps2 dwCaps2; - public uint dwCaps3; - public uint dwCaps4; - public uint dwReserved2; - - public static (DdsHeader, DdsHeaderDxt10) InitializeCompressed(int width, int height, DXGI_FORMAT format) - { - DdsHeader header = new DdsHeader(); - DdsHeaderDxt10 dxt10Header = new DdsHeaderDxt10(); - - header.dwSize = 124; - header.dwFlags = HeaderFlags.REQUIRED; - header.dwWidth = (uint)width; - header.dwHeight = (uint)height; - header.dwDepth = 1; - header.dwMipMapCount = 1; - header.dwCaps = HeaderCaps.DDSCAPS_TEXTURE; - - if (format == DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM) - { - header.ddsPixelFormat = new DdsPixelFormat() - { - dwSize = 32, - dwFlags = PixelFormatFlags.DDPF_FOURCC, - dwFourCC = DdsPixelFormat.DXT1 - }; - } - else if (format == DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM) - { - header.ddsPixelFormat = new DdsPixelFormat() - { - dwSize = 32, - dwFlags = PixelFormatFlags.DDPF_FOURCC, - dwFourCC = DdsPixelFormat.DXT3 - }; - } - else if (format == DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM) - { - header.ddsPixelFormat = new DdsPixelFormat() - { - dwSize = 32, - dwFlags = PixelFormatFlags.DDPF_FOURCC, - dwFourCC = DdsPixelFormat.DXT5 - }; - } - else - { - header.ddsPixelFormat = new DdsPixelFormat() - { - dwSize = 32, - dwFlags = PixelFormatFlags.DDPF_FOURCC, - dwFourCC = DdsPixelFormat.DX10 - }; - dxt10Header.arraySize = 1; - dxt10Header.dxgiFormat = format; - dxt10Header.resourceDimension = D3D10_RESOURCE_DIMENSION.D3D10_RESOURCE_DIMENSION_TEXTURE2D; - } - - return (header, dxt10Header); - } - - public static DdsHeader InitializeUncompressed(int width, int height, DXGI_FORMAT format) - { - DdsHeader header = new DdsHeader(); - - header.dwSize = 124; - header.dwFlags = HeaderFlags.REQUIRED | HeaderFlags.DDSD_PITCH; - header.dwWidth = (uint)width; - header.dwHeight = (uint)height; - header.dwDepth = 1; - header.dwMipMapCount = 1; - header.dwCaps = HeaderCaps.DDSCAPS_TEXTURE; - - if (format == DXGI_FORMAT.DXGI_FORMAT_R8_UNORM) - { - header.ddsPixelFormat = new DdsPixelFormat() - { - dwSize = 32, - dwFlags = PixelFormatFlags.DDPF_LUMINANCE, - dwRGBBitCount = 8, - dwRBitMask = 0xFF - }; - header.dwPitchOrLinearSize = (uint)((width * 8 + 7) / 8); - } - else if (format == DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM) - { - header.ddsPixelFormat = new DdsPixelFormat() - { - dwSize = 32, - dwFlags = PixelFormatFlags.DDPF_LUMINANCE | PixelFormatFlags.DDPF_ALPHAPIXELS, - dwRGBBitCount = 16, - dwRBitMask = 0xFF, - dwGBitMask = 0xFF00 - }; - header.dwPitchOrLinearSize = (uint)((width * 16 + 7) / 8); - } - else if (format == DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM) - { - header.ddsPixelFormat = new DdsPixelFormat() - { - dwSize = 32, - dwFlags = PixelFormatFlags.DDPF_RGB | PixelFormatFlags.DDPF_ALPHAPIXELS, - dwRGBBitCount = 32, - dwRBitMask = 0xFF, - dwGBitMask = 0xFF00, - dwBBitMask = 0xFF0000, - dwABitMask = 0xFF000000, - }; - header.dwPitchOrLinearSize = (uint)((width * 32 + 7) / 8); - } - else - { - throw new NotImplementedException("This format is not implemented in this method"); - } - - return header; - } - } - - public struct DdsPixelFormat - { - public const uint DXT1 = 0x31545844U; - public const uint DXT2 = 0x32545844U; - public const uint DXT3 = 0x33545844U; - public const uint DXT4 = 0x34545844U; - public const uint DXT5 = 0x35545844U; - public const uint DX10 = 0x30315844U; - - public uint dwSize; - public PixelFormatFlags dwFlags; - public uint dwFourCC; - public uint dwRGBBitCount; - public uint dwRBitMask; - public uint dwGBitMask; - public uint dwBBitMask; - public uint dwABitMask; - - public DXGI_FORMAT DxgiFormat - { - get - { - if ((dwFlags & PixelFormatFlags.DDPF_FOURCC) != 0) - { - switch (dwFourCC) - { - case 0x31545844U: - return DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM; - case 0x33545844U: - return DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM; - case 0x35545844U: - return DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM; - } - } - else - { - if ((dwFlags & PixelFormatFlags.DDPF_RGB) != 0) // RGB/A - { - if ((dwFlags & PixelFormatFlags.DDPF_ALPHAPIXELS) != 0) //RGBA - { - if (dwRGBBitCount == 32) - { - if (dwRBitMask == 0xff && dwGBitMask == 0xff00 && dwBBitMask == 0xff0000 && - dwABitMask == 0xff000000) - { - return DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM; - } - else if (dwRBitMask == 0x0000ff && dwGBitMask == 0xff00 && dwBBitMask == 0xff && - dwABitMask == 0xff000000) - { - return DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; - } - } - } - else //RGB - { - if (dwRGBBitCount == 32) - { - if (dwRBitMask == 0x0000ff && dwGBitMask == 0xff00 && dwBBitMask == 0xff) - { - return DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM; - } - } - } - } - else if ((dwFlags & PixelFormatFlags.DDPF_LUMINANCE) != 0) // R/RG - { - if ((dwFlags & PixelFormatFlags.DDPF_ALPHAPIXELS) != 0) // RG - { - if (dwRGBBitCount == 16) - { - if (dwRBitMask == 0x0000ff && dwGBitMask == 0xff00) - { - return DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM; - } - } - } - else // Luminance only - { - if (dwRGBBitCount == 8) - { - if (dwRBitMask == 0x0000ff && dwGBitMask == 0xff00) - { - return DXGI_FORMAT.DXGI_FORMAT_R8_UNORM; - } - } - } - } - } - return DXGI_FORMAT.DXGI_FORMAT_UNKNOWN; - } - } - - public bool IsDxt10Format => ((dwFlags & PixelFormatFlags.DDPF_FOURCC) == PixelFormatFlags.DDPF_FOURCC) - && dwFourCC == DX10; - - public bool IsDxt1To5CompressedFormat => ((dwFlags & PixelFormatFlags.DDPF_FOURCC) == PixelFormatFlags.DDPF_FOURCC) - && (dwFourCC == DXT1 - || dwFourCC == DXT2 - || dwFourCC == DXT3 - || dwFourCC == DXT4 - || dwFourCC == DXT5); - } - - public struct DdsHeaderDxt10 - { - public DXGI_FORMAT dxgiFormat; - public D3D10_RESOURCE_DIMENSION resourceDimension; - public uint miscFlag; - public uint arraySize; - public uint miscFlags2; - } - - - public class DdsFace - { - public uint Width { get; set; } - public uint Height { get; set; } - public uint SizeInBytes { get; } - public DdsMipMap[] MipMaps { get; } - - public DdsFace(uint width, uint height, uint sizeInBytes, int numMipMaps) - { - Width = width; - Height = height; - SizeInBytes = sizeInBytes; - MipMaps = new DdsMipMap[numMipMaps]; - } - } - - public class DdsMipMap - { - public uint Width { get; set; } - public uint Height { get; set; } - public uint SizeInBytes { get; } - public byte[] Data { get; } - public DdsMipMap(byte[] data, uint width, uint height) - { - Width = width; - Height = height; - SizeInBytes = (uint)data.Length; - Data = data; - } - } - - /// - /// Flags to indicate which members contain valid data. - /// - [Flags] - public enum HeaderFlags : uint - { - /// - /// Required in every .dds file. - /// - DDSD_CAPS = 0x1, - /// - /// Required in every .dds file. - /// - DDSD_HEIGHT = 0x2, - /// - /// Required in every .dds file. - /// - DDSD_WIDTH = 0x4, - /// - /// Required when pitch is provided for an uncompressed texture. - /// - DDSD_PITCH = 0x8, - /// - /// Required in every .dds file. - /// - DDSD_PIXELFORMAT = 0x1000, - /// - /// Required in a mipmapped texture. - /// - DDSD_MIPMAPCOUNT = 0x20000, - /// - /// Required when pitch is provided for a compressed texture. - /// - DDSD_LINEARSIZE = 0x80000, - /// - /// Required in a depth texture. - /// - DDSD_DEPTH = 0x800000, - - REQUIRED = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT - } - - /// - /// Specifies the complexity of the surfaces stored. - /// - [Flags] - public enum HeaderCaps : uint - { - /// - /// Optional; must be used on any file that contains more than one surface (a mipmap, a cubic environment map, or mipmapped volume texture). - /// - DDSCAPS_COMPLEX = 0x8, - /// - /// Optional; should be used for a mipmap. - /// - DDSCAPS_MIPMAP = 0x400000, - /// - /// Required - /// - DDSCAPS_TEXTURE = 0x1000 - } - - /// - /// Additional detail about the surfaces stored. - /// - [Flags] - public enum HeaderCaps2 : uint - { - /// - /// Required for a cube map. - /// - DDSCAPS2_CUBEMAP = 0x200, - /// - /// Required when these surfaces are stored in a cube map. - /// - DDSCAPS2_CUBEMAP_POSITIVEX = 0x400, - /// - /// Required when these surfaces are stored in a cube map. - /// - DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800, - /// - /// Required when these surfaces are stored in a cube map. - /// - DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000, - /// - /// Required when these surfaces are stored in a cube map. - /// - DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000, - /// - /// Required when these surfaces are stored in a cube map. - /// - DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000, - /// - /// Required when these surfaces are stored in a cube map. - /// - DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000, - /// - /// Required for a volume texture. - /// - DDSCAPS2_VOLUME = 0x200000 - } - - [Flags] - public enum PixelFormatFlags : uint - { - /// - /// Texture contains alpha data; dwRGBAlphaBitMask contains valid data. - /// - DDPF_ALPHAPIXELS = 0x1, - /// - /// Used in some older DDS files for alpha channel only uncompressed data (dwRGBBitCount contains the alpha channel bitcount; dwABitMask contains valid data) - /// - DDPF_ALPHA = 0x2, - /// - /// Texture contains compressed RGB data; dwFourCC contains valid data. - /// - DDPF_FOURCC = 0x4, - /// - /// Texture contains uncompressed RGB data; dwRGBBitCount and the RGB masks (dwRBitMask, dwGBitMask, dwBBitMask) contain valid data. - /// - DDPF_RGB = 0x40, - /// - /// Used in some older DDS files for YUV uncompressed data (dwRGBBitCount contains the YUV bit count; dwRBitMask contains the Y mask, dwGBitMask contains the U mask, dwBBitMask contains the V mask) - /// - DDPF_YUV = 0x200, - /// - /// Used in some older DDS files for single channel color uncompressed data (dwRGBBitCount contains the luminance channel bit count; dwRBitMask contains the channel mask). Can be combined with DDPF_ALPHAPIXELS for a two channel DDS file. - /// - DDPF_LUMINANCE = 0x20000 - } - - public enum D3D10_RESOURCE_DIMENSION : uint - { - D3D10_RESOURCE_DIMENSION_UNKNOWN, - D3D10_RESOURCE_DIMENSION_BUFFER, - D3D10_RESOURCE_DIMENSION_TEXTURE1D, - D3D10_RESOURCE_DIMENSION_TEXTURE2D, - D3D10_RESOURCE_DIMENSION_TEXTURE3D - }; - - public enum DXGI_FORMAT : uint - { - DXGI_FORMAT_UNKNOWN, - DXGI_FORMAT_R32G32B32A32_TYPELESS, - DXGI_FORMAT_R32G32B32A32_FLOAT, - DXGI_FORMAT_R32G32B32A32_UINT, - DXGI_FORMAT_R32G32B32A32_SINT, - DXGI_FORMAT_R32G32B32_TYPELESS, - DXGI_FORMAT_R32G32B32_FLOAT, - DXGI_FORMAT_R32G32B32_UINT, - DXGI_FORMAT_R32G32B32_SINT, - DXGI_FORMAT_R16G16B16A16_TYPELESS, - DXGI_FORMAT_R16G16B16A16_FLOAT, - DXGI_FORMAT_R16G16B16A16_UNORM, - DXGI_FORMAT_R16G16B16A16_UINT, - DXGI_FORMAT_R16G16B16A16_SNORM, - DXGI_FORMAT_R16G16B16A16_SINT, - DXGI_FORMAT_R32G32_TYPELESS, - DXGI_FORMAT_R32G32_FLOAT, - DXGI_FORMAT_R32G32_UINT, - DXGI_FORMAT_R32G32_SINT, - DXGI_FORMAT_R32G8X24_TYPELESS, - DXGI_FORMAT_D32_FLOAT_S8X24_UINT, - DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS, - DXGI_FORMAT_X32_TYPELESS_G8X24_UINT, - DXGI_FORMAT_R10G10B10A2_TYPELESS, - DXGI_FORMAT_R10G10B10A2_UNORM, - DXGI_FORMAT_R10G10B10A2_UINT, - DXGI_FORMAT_R11G11B10_FLOAT, - DXGI_FORMAT_R8G8B8A8_TYPELESS, - DXGI_FORMAT_R8G8B8A8_UNORM, - DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, - DXGI_FORMAT_R8G8B8A8_UINT, - DXGI_FORMAT_R8G8B8A8_SNORM, - DXGI_FORMAT_R8G8B8A8_SINT, - DXGI_FORMAT_R16G16_TYPELESS, - DXGI_FORMAT_R16G16_FLOAT, - DXGI_FORMAT_R16G16_UNORM, - DXGI_FORMAT_R16G16_UINT, - DXGI_FORMAT_R16G16_SNORM, - DXGI_FORMAT_R16G16_SINT, - DXGI_FORMAT_R32_TYPELESS, - DXGI_FORMAT_D32_FLOAT, - DXGI_FORMAT_R32_FLOAT, - DXGI_FORMAT_R32_UINT, - DXGI_FORMAT_R32_SINT, - DXGI_FORMAT_R24G8_TYPELESS, - DXGI_FORMAT_D24_UNORM_S8_UINT, - DXGI_FORMAT_R24_UNORM_X8_TYPELESS, - DXGI_FORMAT_X24_TYPELESS_G8_UINT, - DXGI_FORMAT_R8G8_TYPELESS, - DXGI_FORMAT_R8G8_UNORM, - DXGI_FORMAT_R8G8_UINT, - DXGI_FORMAT_R8G8_SNORM, - DXGI_FORMAT_R8G8_SINT, - DXGI_FORMAT_R16_TYPELESS, - DXGI_FORMAT_R16_FLOAT, - DXGI_FORMAT_D16_UNORM, - DXGI_FORMAT_R16_UNORM, - DXGI_FORMAT_R16_UINT, - DXGI_FORMAT_R16_SNORM, - DXGI_FORMAT_R16_SINT, - DXGI_FORMAT_R8_TYPELESS, - DXGI_FORMAT_R8_UNORM, - DXGI_FORMAT_R8_UINT, - DXGI_FORMAT_R8_SNORM, - DXGI_FORMAT_R8_SINT, - DXGI_FORMAT_A8_UNORM, - DXGI_FORMAT_R1_UNORM, - DXGI_FORMAT_R9G9B9E5_SHAREDEXP, - DXGI_FORMAT_R8G8_B8G8_UNORM, - DXGI_FORMAT_G8R8_G8B8_UNORM, - DXGI_FORMAT_BC1_TYPELESS, - DXGI_FORMAT_BC1_UNORM, - DXGI_FORMAT_BC1_UNORM_SRGB, - DXGI_FORMAT_BC2_TYPELESS, - DXGI_FORMAT_BC2_UNORM, - DXGI_FORMAT_BC2_UNORM_SRGB, - DXGI_FORMAT_BC3_TYPELESS, - DXGI_FORMAT_BC3_UNORM, - DXGI_FORMAT_BC3_UNORM_SRGB, - DXGI_FORMAT_BC4_TYPELESS, - DXGI_FORMAT_BC4_UNORM, - DXGI_FORMAT_BC4_SNORM, - DXGI_FORMAT_BC5_TYPELESS, - DXGI_FORMAT_BC5_UNORM, - DXGI_FORMAT_BC5_SNORM, - DXGI_FORMAT_B5G6R5_UNORM, - DXGI_FORMAT_B5G5R5A1_UNORM, - DXGI_FORMAT_B8G8R8A8_UNORM, - DXGI_FORMAT_B8G8R8X8_UNORM, - DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, - DXGI_FORMAT_B8G8R8A8_TYPELESS, - DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, - DXGI_FORMAT_B8G8R8X8_TYPELESS, - DXGI_FORMAT_B8G8R8X8_UNORM_SRGB, - DXGI_FORMAT_BC6H_TYPELESS, - DXGI_FORMAT_BC6H_UF16, - DXGI_FORMAT_BC6H_SF16, - DXGI_FORMAT_BC7_TYPELESS, - DXGI_FORMAT_BC7_UNORM, - DXGI_FORMAT_BC7_UNORM_SRGB, - DXGI_FORMAT_AYUV, - DXGI_FORMAT_Y410, - DXGI_FORMAT_Y416, - DXGI_FORMAT_NV12, - DXGI_FORMAT_P010, - DXGI_FORMAT_P016, - DXGI_FORMAT_420_OPAQUE, - DXGI_FORMAT_YUY2, - DXGI_FORMAT_Y210, - DXGI_FORMAT_Y216, - DXGI_FORMAT_NV11, - DXGI_FORMAT_AI44, - DXGI_FORMAT_IA44, - DXGI_FORMAT_P8, - DXGI_FORMAT_A8P8, - DXGI_FORMAT_B4G4R4A4_UNORM, - DXGI_FORMAT_P208, - DXGI_FORMAT_V208, - DXGI_FORMAT_V408, - DXGI_FORMAT_FORCE_UINT - }; - - public static class DxgiFormatExtensions - { - public static int GetByteSize(this DXGI_FORMAT format) - { - switch (format) - { - case DXGI_FORMAT.DXGI_FORMAT_UNKNOWN: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_TYPELESS: - return 4 * 4; - case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT: - return 4 * 4; - case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_UINT: - return 4 * 4; - case DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_SINT: - return 4 * 4; - case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_TYPELESS: - return 4 * 3; - case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT: - return 4 * 3; - case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_UINT: - return 4 * 3; - case DXGI_FORMAT.DXGI_FORMAT_R32G32B32_SINT: - return 4 * 3; - case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_TYPELESS: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UINT: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SNORM: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_SINT: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_R32G32_TYPELESS: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_R32G32_UINT: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_R32G32_SINT: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_R32G8X24_TYPELESS: - return 4 * 2; - case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT_S8X24_UINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_X32_TYPELESS_G8X24_UINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_TYPELESS: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R11G11B10_FLOAT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_TYPELESS: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SNORM: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_SINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R16G16_TYPELESS: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R16G16_FLOAT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R16G16_UNORM: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R16G16_UINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R16G16_SNORM: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R16G16_SINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R32_TYPELESS: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_D32_FLOAT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R32_UINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R32_SINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R24G8_TYPELESS: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_D24_UNORM_S8_UINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R24_UNORM_X8_TYPELESS: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_X24_TYPELESS_G8_UINT: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R8G8_TYPELESS: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R8G8_UNORM: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R8G8_UINT: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R8G8_SNORM: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R8G8_SINT: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R16_TYPELESS: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_D16_UNORM: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R16_UNORM: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R16_UINT: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R16_SNORM: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R16_SINT: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_R8_TYPELESS: - return 1; - case DXGI_FORMAT.DXGI_FORMAT_R8_UNORM: - return 1; - case DXGI_FORMAT.DXGI_FORMAT_R8_UINT: - return 1; - case DXGI_FORMAT.DXGI_FORMAT_R8_SNORM: - return 1; - case DXGI_FORMAT.DXGI_FORMAT_R8_SINT: - return 1; - case DXGI_FORMAT.DXGI_FORMAT_A8_UNORM: - return 1; - case DXGI_FORMAT.DXGI_FORMAT_R1_UNORM: - return 1; - case DXGI_FORMAT.DXGI_FORMAT_R9G9B9E5_SHAREDEXP: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R8G8_B8G8_UNORM: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_G8R8_G8B8_UNORM: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS: - return 8; - case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM: - return 8; - case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB: - return 8; - case DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC4_TYPELESS: - return 8; - case DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM: - return 8; - case DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM: - return 8; - case DXGI_FORMAT.DXGI_FORMAT_BC5_TYPELESS: - return 8; - case DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM: - return 8; - case DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM: - return 8; - case DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_TYPELESS: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_TYPELESS: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: - return 4; - case DXGI_FORMAT.DXGI_FORMAT_BC6H_TYPELESS: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC7_TYPELESS: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB: - return 16; - case DXGI_FORMAT.DXGI_FORMAT_P8: - return 1; - case DXGI_FORMAT.DXGI_FORMAT_A8P8: - return 2; - case DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM: - return 2; - } - return 4; - } - - public static bool IsCompressedFormat(this DXGI_FORMAT format) - { - switch (format) - { - case DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS: - case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB: - case DXGI_FORMAT.DXGI_FORMAT_BC2_TYPELESS: - case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM_SRGB: - case DXGI_FORMAT.DXGI_FORMAT_BC3_TYPELESS: - case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM_SRGB: - case DXGI_FORMAT.DXGI_FORMAT_BC4_TYPELESS: - case DXGI_FORMAT.DXGI_FORMAT_BC4_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC4_SNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC5_TYPELESS: - case DXGI_FORMAT.DXGI_FORMAT_BC5_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC5_SNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC6H_TYPELESS: - case DXGI_FORMAT.DXGI_FORMAT_BC6H_UF16: - case DXGI_FORMAT.DXGI_FORMAT_BC6H_SF16: - case DXGI_FORMAT.DXGI_FORMAT_BC7_TYPELESS: - case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM: - case DXGI_FORMAT.DXGI_FORMAT_BC7_UNORM_SRGB: - return true; - - default: - return false; - } - } - } -} diff --git a/BCnEnc.Net/Shared/EncodedBlocks.cs b/BCnEnc.Net/Shared/EncodedBlocks.cs index bf41884..e67b205 100644 --- a/BCnEnc.Net/Shared/EncodedBlocks.cs +++ b/BCnEnc.Net/Shared/EncodedBlocks.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.PixelFormats; +using BCnEncoder.Encoder; namespace BCnEncoder.Shared { [StructLayout(LayoutKind.Sequential)] - internal unsafe struct Bc1Block + internal struct Bc1Block { public ColorRgb565 color0; public ColorRgb565 color1; @@ -16,9 +16,9 @@ public int this[int index] readonly get => (int)(colorIndices >> (index * 2)) & 0b11; set { - colorIndices = (uint)(colorIndices & (~(0b11 << (index * 2)))); - int val = value & 0b11; - colorIndices = (colorIndices | ((uint)val << (index * 2))); + colorIndices = (uint)(colorIndices & ~(0b11 << (index * 2))); + var val = value & 0b11; + colorIndices = colorIndices | ((uint)val << (index * 2)); } } @@ -26,7 +26,7 @@ public int this[int index] public readonly RawBlock4X4Rgba32 Decode(bool useAlpha) { - RawBlock4X4Rgba32 output = new RawBlock4X4Rgba32(); + var output = new RawBlock4X4Rgba32(); var pixels = output.AsSpan; var color0 = this.color0.ToColorRgb24(); @@ -34,41 +34,40 @@ public readonly RawBlock4X4Rgba32 Decode(bool useAlpha) useAlpha = useAlpha && HasAlphaOrBlack; - Span colors = HasAlphaOrBlack ? + var colors = HasAlphaOrBlack ? stackalloc ColorRgb24[] { color0, color1, - color0 * (1.0 / 2.0) + color1 * (1.0 / 2.0), + color0.InterpolateHalf(color1), new ColorRgb24(0, 0, 0) } : stackalloc ColorRgb24[] { color0, color1, - color0 * (2.0 / 3.0) + color1 * (1.0 / 3.0), - color0 * (1.0 / 3.0) + color1 * (2.0 / 3.0) + color0.InterpolateThird(color1, 1), + color0.InterpolateThird(color1, 2) }; - for (int i = 0; i < pixels.Length; i++) + for (var i = 0; i < pixels.Length; i++) { - int colorIndex = (int)((colorIndices >> (i * 2)) & 0b11); + var colorIndex = (int)((colorIndices >> (i * 2)) & 0b11); var color = colors[colorIndex]; if (useAlpha && colorIndex == 3) { - pixels[i] = new Rgba32(0, 0, 0, 0); + pixels[i] = new ColorRgba32(0, 0, 0, 0); } else { - pixels[i] = new Rgba32(color.r, color.g, color.b, 255); + pixels[i] = new ColorRgba32(color.r, color.g, color.b, 255); } } return output; } } - [StructLayout(LayoutKind.Sequential)] - internal unsafe struct Bc2Block + internal struct Bc2Block { - public ulong alphaColors; + public Bc2AlphaBlock alphaBlock; public ColorRgb565 color0; public ColorRgb565 color1; public uint colorIndices; @@ -78,32 +77,19 @@ public int this[int index] readonly get => (int)(colorIndices >> (index * 2)) & 0b11; set { - colorIndices = (uint)(colorIndices & (~(0b11 << (index * 2)))); - int val = value & 0b11; - colorIndices = (colorIndices | ((uint)val << (index * 2))); + colorIndices = (uint)(colorIndices & ~(0b11 << (index * 2))); + var val = value & 0b11; + colorIndices = colorIndices | ((uint)val << (index * 2)); } } - public readonly byte GetAlpha(int index) - { - ulong mask = 0xFUL << (index * 4); - int shift = (index * 4); - ulong alphaUnscaled = (((alphaColors & mask) >> shift)); - return (byte)((alphaUnscaled / 15.0) * 255); - } + public readonly byte GetAlpha(int index) => alphaBlock.GetAlpha(index); - public void SetAlpha(int index, byte alpha) - { - ulong mask = 0xFUL << (index * 4); - int shift = (index * 4); - alphaColors &= ~mask; - byte a = (byte)((alpha / 255.0) * 15); - alphaColors |= (ulong)(a & 0xF) << shift; - } + public void SetAlpha(int index, byte alpha) => alphaBlock.SetAlpha(index, alpha); public readonly RawBlock4X4Rgba32 Decode() { - RawBlock4X4Rgba32 output = new RawBlock4X4Rgba32(); + var output = new RawBlock4X4Rgba32(); var pixels = output.AsSpan; var color0 = this.color0.ToColorRgb24(); @@ -112,25 +98,25 @@ public readonly RawBlock4X4Rgba32 Decode() Span colors = stackalloc ColorRgb24[] { color0, color1, - color0 * (2.0 / 3.0) + color1 * (1.0 / 3.0), - color0 * (1.0 / 3.0) + color1 * (2.0 / 3.0) + color0.InterpolateThird(color1, 1), + color0.InterpolateThird(color1, 2) }; - for (int i = 0; i < pixels.Length; i++) + for (var i = 0; i < pixels.Length; i++) { - int colorIndex = (int)((colorIndices >> (i * 2)) & 0b11); + var colorIndex = (int)((colorIndices >> (i * 2)) & 0b11); var color = colors[colorIndex]; - pixels[i] = new Rgba32(color.r, color.g, color.b, GetAlpha(i)); + pixels[i] = new ColorRgba32(color.r, color.g, color.b, GetAlpha(i)); } return output; } } [StructLayout(LayoutKind.Sequential)] - internal unsafe struct Bc3Block + internal struct Bc3Block { - public ulong alphaBlock; + public Bc4ComponentBlock alphaBlock; public ColorRgb565 color0; public ColorRgb565 color1; public uint colorIndices; @@ -140,180 +126,143 @@ public int this[int index] readonly get => (int)(colorIndices >> (index * 2)) & 0b11; set { - colorIndices = (uint)(colorIndices & (~(0b11 << (index * 2)))); - int val = value & 0b11; - colorIndices = (colorIndices | ((uint)val << (index * 2))); + colorIndices = (uint)(colorIndices & ~(0b11 << (index * 2))); + var val = value & 0b11; + colorIndices = colorIndices | ((uint)val << (index * 2)); } } public byte Alpha0 { - readonly get => (byte)(alphaBlock & 0xFFUL); - set - { - alphaBlock &= ~0xFFUL; - alphaBlock |= value; - } + get => alphaBlock.Endpoint0; + set => alphaBlock.Endpoint0 = value; } public byte Alpha1 { - readonly get => (byte)((alphaBlock >> 8) & 0xFFUL); - set - { - alphaBlock &= ~0xFF00UL; - alphaBlock |= (ulong)value << 8; - } + get => alphaBlock.Endpoint1; + set => alphaBlock.Endpoint1 = value; } - public readonly byte GetAlphaIndex(int pixelIndex) - { - ulong mask = 0b0111UL << (pixelIndex * 3 + 16); - int shift = (pixelIndex * 3 + 16); - ulong alphaIndex = (((alphaBlock & mask) >> shift)); - return (byte)alphaIndex; - } + public readonly byte GetAlphaIndex(int pixelIndex) => alphaBlock.GetComponentIndex(pixelIndex); - public void SetAlphaIndex(int pixelIndex, byte alphaIndex) - { - ulong mask = 0b0111UL << (pixelIndex * 3 + 16); - int shift = (pixelIndex * 3 + 16); - alphaBlock &= ~mask; - alphaBlock |= (ulong)(alphaIndex & 0b111) << shift; - } + public void SetAlphaIndex(int pixelIndex, byte alphaIndex) => alphaBlock.SetComponentIndex(pixelIndex, alphaIndex); public readonly RawBlock4X4Rgba32 Decode() { - RawBlock4X4Rgba32 output = new RawBlock4X4Rgba32(); + var output = new RawBlock4X4Rgba32(); var pixels = output.AsSpan; var color0 = this.color0.ToColorRgb24(); var color1 = this.color1.ToColorRgb24(); - var a0 = Alpha0; - var a1 = Alpha1; Span colors = stackalloc ColorRgb24[] { color0, color1, - color0 * (2.0 / 3.0) + color1 * (1.0 / 3.0), - color0 * (1.0 / 3.0) + color1 * (2.0 / 3.0) + color0.InterpolateThird(color1, 1), + color0.InterpolateThird(color1, 2) }; - Span alphas = a0 > a1 ? stackalloc byte[] { - a0, - a1, - (byte)(6 / 7.0 * a0 + 1 / 7.0 * a1), - (byte)(5 / 7.0 * a0 + 2 / 7.0 * a1), - (byte)(4 / 7.0 * a0 + 3 / 7.0 * a1), - (byte)(3 / 7.0 * a0 + 4 / 7.0 * a1), - (byte)(2 / 7.0 * a0 + 5 / 7.0 * a1), - (byte)(1 / 7.0 * a0 + 6 / 7.0 * a1), - } : stackalloc byte[] { - a0, - a1, - (byte)(4 / 5.0 * a0 + 1 / 5.0 * a1), - (byte)(3 / 5.0 * a0 + 2 / 5.0 * a1), - (byte)(2 / 5.0 * a0 + 3 / 5.0 * a1), - (byte)(1 / 5.0 * a0 + 4 / 5.0 * a1), - 0, - 255 - }; + var alphas = alphaBlock.Decode(); - for (int i = 0; i < pixels.Length; i++) + for (var i = 0; i < pixels.Length; i++) { - int colorIndex = (int)((colorIndices >> (i * 2)) & 0b11); + var colorIndex = (int)((colorIndices >> (i * 2)) & 0b11); var color = colors[colorIndex]; - pixels[i] = new Rgba32(color.r, color.g, color.b, alphas[GetAlphaIndex(i)]); + pixels[i] = new ColorRgba32(color.r, color.g, color.b, alphas[i]); } return output; } } [StructLayout(LayoutKind.Sequential)] - internal unsafe struct Bc4Block + internal struct Bc4Block { - public ulong redBlock; + public Bc4ComponentBlock componentBlock; - public byte Red0 + public byte Endpoint0 { - readonly get => (byte)(redBlock & 0xFFUL); - set + readonly get => componentBlock.Endpoint0; + set => componentBlock.Endpoint0 = value; + } + + public byte Endpoint1 + { + readonly get => componentBlock.Endpoint1; + set => componentBlock.Endpoint1 = value; + } + + public readonly byte GetComponentIndex(int pixelIndex) => componentBlock.GetComponentIndex(pixelIndex); + + public void SetComponentIndex(int pixelIndex, byte redIndex) => componentBlock.SetComponentIndex(pixelIndex, redIndex); + + public readonly RawBlock4X4Rgba32 Decode(ColorComponent component = ColorComponent.R) + { + var output = new RawBlock4X4Rgba32(); + var pixels = output.AsSpan; + + var components = componentBlock.Decode(); + + for (var i = 0; i < pixels.Length; i++) { - redBlock &= ~0xFFUL; - redBlock |= value; + pixels[i] = ComponentHelper.ComponentToColor(component, components[i]); } + + return output; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct Bc5Block + { + public Bc4ComponentBlock redBlock; + public Bc4ComponentBlock greenBlock; + + public byte Red0 + { + readonly get => redBlock.Endpoint0; + set => redBlock.Endpoint0 = value; } public byte Red1 { - readonly get => (byte)((redBlock >> 8) & 0xFFUL); - set - { - redBlock &= ~0xFF00UL; - redBlock |= (ulong)value << 8; - } + readonly get => redBlock.Endpoint1; + set => redBlock.Endpoint1 = value; } - public readonly byte GetRedIndex(int pixelIndex) + public byte Green0 { - ulong mask = 0b0111UL << (pixelIndex * 3 + 16); - int shift = (pixelIndex * 3 + 16); - ulong redIndex = (((redBlock & mask) >> shift)); - return (byte)redIndex; + readonly get => greenBlock.Endpoint0; + set => greenBlock.Endpoint0 = value; } - public void SetRedIndex(int pixelIndex, byte redIndex) + public byte Green1 { - ulong mask = 0b0111UL << (pixelIndex * 3 + 16); - int shift = (pixelIndex * 3 + 16); - redBlock &= ~mask; - redBlock |= (ulong)(redIndex & 0b111) << shift; + readonly get => greenBlock.Endpoint1; + set => greenBlock.Endpoint1 = value; } - public readonly RawBlock4X4Rgba32 Decode(bool redAsLuminance) + public readonly byte GetRedIndex(int pixelIndex) => redBlock.GetComponentIndex(pixelIndex); + + public void SetRedIndex(int pixelIndex, byte redIndex) => redBlock.SetComponentIndex(pixelIndex, redIndex); + + public readonly byte GetGreenIndex(int pixelIndex) => greenBlock.GetComponentIndex(pixelIndex); + + public void SetGreenIndex(int pixelIndex, byte greenIndex) => greenBlock.SetComponentIndex(pixelIndex, greenIndex); + + public readonly RawBlock4X4Rgba32 Decode(ColorComponent component1 = ColorComponent.R, ColorComponent component2 = ColorComponent.G) { - RawBlock4X4Rgba32 output = new RawBlock4X4Rgba32(); + var output = new RawBlock4X4Rgba32(); var pixels = output.AsSpan; - var r0 = Red0; - var r1 = Red1; - - Span reds = r0 > r1 ? stackalloc byte[] { - r0, - r1, - (byte)(6 / 7.0 * r0 + 1 / 7.0 * r1), - (byte)(5 / 7.0 * r0 + 2 / 7.0 * r1), - (byte)(4 / 7.0 * r0 + 3 / 7.0 * r1), - (byte)(3 / 7.0 * r0 + 4 / 7.0 * r1), - (byte)(2 / 7.0 * r0 + 5 / 7.0 * r1), - (byte)(1 / 7.0 * r0 + 6 / 7.0 * r1), - } : stackalloc byte[] { - r0, - r1, - (byte)(4 / 5.0 * r0 + 1 / 5.0 * r1), - (byte)(3 / 5.0 * r0 + 2 / 5.0 * r1), - (byte)(2 / 5.0 * r0 + 3 / 5.0 * r1), - (byte)(1 / 5.0 * r0 + 4 / 5.0 * r1), - 0, - 255 - }; + var reds = redBlock.Decode(); + var greens = greenBlock.Decode(); - if (redAsLuminance) + for (var i = 0; i < pixels.Length; i++) { - for (int i = 0; i < pixels.Length; i++) - { - var index = GetRedIndex(i); - pixels[i] = new Rgba32(reds[index], reds[index], reds[index], 255); - } - } - else - { - for (int i = 0; i < pixels.Length; i++) - { - var index = GetRedIndex(i); - pixels[i] = new Rgba32(reds[index], 0, 0, 255); - } + pixels[i] = ComponentHelper.ComponentToColor(component1, reds[i]); + pixels[i] = ComponentHelper.ComponentToColor(pixels[i], component2, greens[i]); } return output; @@ -321,143 +270,188 @@ public readonly RawBlock4X4Rgba32 Decode(bool redAsLuminance) } [StructLayout(LayoutKind.Sequential)] - internal unsafe struct Bc5Block + internal struct AtcBlock { - public ulong redBlock; - public ulong greenBlock; + public ColorRgb555 color0; + public ColorRgb565 color1; + public uint colorIndices; - public byte Red0 + public int this[int index] { - readonly get => (byte)(redBlock & 0xFFUL); + readonly get => (int)(colorIndices >> (index * 2)) & 0b11; set { - redBlock &= ~0xFFUL; - redBlock |= value; + colorIndices = (uint)(colorIndices & ~(0b11 << (index * 2))); + var val = value & 0b11; + colorIndices = colorIndices | ((uint)val << (index * 2)); } } - public byte Red1 + public readonly RawBlock4X4Rgba32 Decode() { - readonly get => (byte)((redBlock >> 8) & 0xFFUL); - set + var output = new RawBlock4X4Rgba32(); + var pixels = output.AsSpan; + + var color0 = this.color0.ToColorRgb24(); + var color1 = this.color1.ToColorRgb24(); + + Span colors = stackalloc ColorRgb24[] { + new ColorRgb24(0, 0, 0), + color0.InterpolateFourthAtc(color1, 1), + color0, + color1 + }; + + for (var i = 0; i < pixels.Length; i++) { - redBlock &= ~0xFF00UL; - redBlock |= (ulong)value << 8; + var colorIndex = this[i]; + + var color = this.color0.Mode == 0 ? color0.InterpolateThird(color1, colorIndex) : colors[colorIndex]; + + pixels[i] = new ColorRgba32(color.r, color.g, color.b, 255); } + return output; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct Bc2AlphaBlock + { + public ulong alphas; + + public readonly byte GetAlpha(int index) + { + var mask = 0xFUL << (index * 4); + var shift = index * 4; + var alphaUnscaled = (alphas & mask) >> shift; + return (byte)(alphaUnscaled * 17); } - public byte Green0 + public void SetAlpha(int index, byte alpha) { - readonly get => (byte)(greenBlock & 0xFFUL); + var mask = 0xFUL << (index * 4); + var shift = index * 4; + alphas &= ~mask; + var a = (byte)(alpha / 17); + alphas |= (ulong)(a & 0xF) << shift; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct Bc4ComponentBlock + { + public ulong componentBlock; + + public byte Endpoint0 + { + readonly get => (byte)(componentBlock & 0xFFUL); set { - greenBlock &= ~0xFFUL; - greenBlock |= value; + componentBlock &= ~0xFFUL; + componentBlock |= value; } } - public byte Green1 + public byte Endpoint1 { - readonly get => (byte)((greenBlock >> 8) & 0xFFUL); + readonly get => (byte)((componentBlock >> 8) & 0xFFUL); set { - greenBlock &= ~0xFF00UL; - greenBlock |= (ulong)value << 8; + componentBlock &= ~0xFF00UL; + componentBlock |= (ulong)value << 8; } } - public readonly byte GetRedIndex(int pixelIndex) + public readonly byte GetComponentIndex(int pixelIndex) { - ulong mask = 0b0111UL << (pixelIndex * 3 + 16); - int shift = (pixelIndex * 3 + 16); - ulong redIndex = (((redBlock & mask) >> shift)); + var mask = 0b0111UL << (pixelIndex * 3 + 16); + var shift = pixelIndex * 3 + 16; + var redIndex = (componentBlock & mask) >> shift; return (byte)redIndex; } - public void SetRedIndex(int pixelIndex, byte redIndex) + public void SetComponentIndex(int pixelIndex, byte redIndex) { - ulong mask = 0b0111UL << (pixelIndex * 3 + 16); - int shift = (pixelIndex * 3 + 16); - redBlock &= ~mask; - redBlock |= (ulong)(redIndex & 0b111) << shift; + var mask = 0b0111UL << (pixelIndex * 3 + 16); + var shift = pixelIndex * 3 + 16; + componentBlock &= ~mask; + componentBlock |= (ulong)(redIndex & 0b111) << shift; } - public readonly byte GetGreenIndex(int pixelIndex) + public readonly byte[] Decode() { - ulong mask = 0b0111UL << (pixelIndex * 3 + 16); - int shift = (pixelIndex * 3 + 16); - ulong greenIndex = (((greenBlock & mask) >> shift)); - return (byte)greenIndex; + var output = new byte[16]; + + var c0 = Endpoint0; + var c1 = Endpoint1; + + var components = c0 > c1 ? stackalloc byte[] { + c0, + c1, + c0.InterpolateSeventh(c1, 1), + c0.InterpolateSeventh(c1, 2), + c0.InterpolateSeventh(c1, 3), + c0.InterpolateSeventh(c1, 4), + c0.InterpolateSeventh(c1, 5), + c0.InterpolateSeventh(c1, 6), + } : stackalloc byte[] { + c0, + c1, + c0.InterpolateFifth(c1, 1), + c0.InterpolateFifth(c1, 2), + c0.InterpolateFifth(c1, 3), + c0.InterpolateFifth(c1, 4), + 0, + 255 + }; + + for (var i = 0; i < output.Length; i++) + { + var index = GetComponentIndex(i); + output[i] = components[index]; + } + + return output; } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AtcExplicitAlphaBlock + { + public Bc2AlphaBlock alphas; + public AtcBlock colors; - public void SetGreenIndex(int pixelIndex, byte greenIndex) + public readonly RawBlock4X4Rgba32 Decode() { - ulong mask = 0b0111UL << (pixelIndex * 3 + 16); - int shift = (pixelIndex * 3 + 16); - greenBlock &= ~mask; - greenBlock |= (ulong)(greenIndex & 0b111) << shift; + var output = colors.Decode(); + var pixels = output.AsSpan; + + for (var i = 0; i < pixels.Length; i++) + { + pixels[i].a = alphas.GetAlpha(i); + } + return output; } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AtcInterpolatedAlphaBlock + { + public Bc4ComponentBlock alphas; + public AtcBlock colors; public readonly RawBlock4X4Rgba32 Decode() { - RawBlock4X4Rgba32 output = new RawBlock4X4Rgba32(); + var output = colors.Decode(); var pixels = output.AsSpan; - var r0 = Red0; - var r1 = Red1; - - Span reds = r0 > r1 ? stackalloc byte[] { - r0, - r1, - (byte)(6 / 7.0 * r0 + 1 / 7.0 * r1), - (byte)(5 / 7.0 * r0 + 2 / 7.0 * r1), - (byte)(4 / 7.0 * r0 + 3 / 7.0 * r1), - (byte)(3 / 7.0 * r0 + 4 / 7.0 * r1), - (byte)(2 / 7.0 * r0 + 5 / 7.0 * r1), - (byte)(1 / 7.0 * r0 + 6 / 7.0 * r1), - } : stackalloc byte[] { - r0, - r1, - (byte)(4 / 5.0 * r0 + 1 / 5.0 * r1), - (byte)(3 / 5.0 * r0 + 2 / 5.0 * r1), - (byte)(2 / 5.0 * r0 + 3 / 5.0 * r1), - (byte)(1 / 5.0 * r0 + 4 / 5.0 * r1), - 0, - 255 - }; - - var g0 = Green0; - var g1 = Green1; - - Span greens = g0 > g1 ? stackalloc byte[] { - g0, - g1, - (byte)(6 / 7.0 * g0 + 1 / 7.0 * g1), - (byte)(5 / 7.0 * g0 + 2 / 7.0 * g1), - (byte)(4 / 7.0 * g0 + 3 / 7.0 * g1), - (byte)(3 / 7.0 * g0 + 4 / 7.0 * g1), - (byte)(2 / 7.0 * g0 + 5 / 7.0 * g1), - (byte)(1 / 7.0 * g0 + 6 / 7.0 * g1), - } : stackalloc byte[] { - g0, - g1, - (byte)(4 / 5.0 * g0 + 1 / 5.0 * g1), - (byte)(3 / 5.0 * g0 + 2 / 5.0 * g1), - (byte)(2 / 5.0 * g0 + 3 / 5.0 * g1), - (byte)(1 / 5.0 * g0 + 4 / 5.0 * g1), - 0, - 255 - }; + var componentValues = alphas.Decode(); - for (int i = 0; i < pixels.Length; i++) + for (var i = 0; i < pixels.Length; i++) { - var redIndex = GetRedIndex(i); - var greenIndex = GetGreenIndex(i); - pixels[i] = new Rgba32(reds[redIndex], greens[greenIndex], 0, 255); + pixels[i].a = componentValues[i]; } - return output; } } - } diff --git a/BCnEnc.Net/Shared/GLFormat.cs b/BCnEnc.Net/Shared/GLFormat.cs index 77e50ce..1a30b9c 100644 --- a/BCnEnc.Net/Shared/GLFormat.cs +++ b/BCnEnc.Net/Shared/GLFormat.cs @@ -1,193 +1,199 @@ -namespace BCnEncoder.Shared +namespace BCnEncoder.Shared { - public enum GLFormat : uint + public enum GlFormat : uint { - GL_RED = 0x1903, - GL_BGRA = 0x80E1, - GL_RGB = 0x1907, - GL_RGBA = 0x1908, - GL_RG = 0x8227, - GL_RED_INTEGER = 0x8D94, - GL_RG_INTEGER = 0x8228, - GL_RED_SNORM = 0x8F90, - GL_RG_SNORM = 0x8F91, - GL_RGB_SNORM = 0x8F92, - GL_RGBA_SNORM = 0x8F93, + GlRed = 0x1903, + GlBgra = 0x80E1, + GlRgb = 0x1907, + GlRgba = 0x1908, + GlRg = 0x8227, + GlRedInteger = 0x8D94, + GlRgInteger = 0x8228, + GlRedSnorm = 0x8F90, + GlRgSnorm = 0x8F91, + GlRgbSnorm = 0x8F92, + GlRgbaSnorm = 0x8F93, } - public enum GLType : uint + public enum GlType : uint { - GL_BYTE = 5120, - GL_UNSIGNED_BYTE = 5121, - GL_SHORT = 5122, - GL_UNSIGNED_SHORT = 5123, - GL_INT = 5124, - GL_UNSIGNED_INT = 5125, - GL_FLOAT = 5126, - GL_HALF_FLOAT = 5131, - GL_UNSIGNED_BYTE_2_3_3_REV = 33634, - GL_UNSIGNED_BYTE_3_3_2 = 32818, - GL_UNSIGNED_INT_10_10_10_2 = 32822, - GL_UNSIGNED_INT_2_10_10_10_REV = 33640, - GL_UNSIGNED_INT_8_8_8_8 = 32821, - GL_UNSIGNED_INT_8_8_8_8_REV = 33639, - GL_UNSIGNED_SHORT_1_5_5_5_REV = 33638, - GL_UNSIGNED_SHORT_4_4_4_4 = 32819, - GL_UNSIGNED_SHORT_4_4_4_4_REV = 33637, - GL_UNSIGNED_SHORT_5_5_5_1 = 32820, - GL_UNSIGNED_SHORT_5_6_5 = 33635, - GL_UNSIGNED_SHORT_5_6_5_REV = 33636 + GlByte = 5120, + GlUnsignedByte = 5121, + GlShort = 5122, + GlUnsignedShort = 5123, + GlInt = 5124, + GlUnsignedInt = 5125, + GlFloat = 5126, + GlHalfFloat = 5131, + GlUnsignedByte233Rev = 33634, + GlUnsignedByte332 = 32818, + GlUnsignedInt1010102 = 32822, + GlUnsignedInt2101010Rev = 33640, + GlUnsignedInt8888 = 32821, + GlUnsignedInt8888Rev = 33639, + GlUnsignedShort1555Rev = 33638, + GlUnsignedShort4444 = 32819, + GlUnsignedShort4444Rev = 33637, + GlUnsignedShort5551 = 32820, + GlUnsignedShort565 = 33635, + GlUnsignedShort565Rev = 33636 } public enum GlInternalFormat : uint { - GL_RGBA4 = 0x8056, - GL_RGB5 = 0x8050, - GL_RGB565 = 0x8D62, - GL_RGBA8 = 0x8058, - GL_RGB5_A1 = 0x8057, - GL_RGBA16 = 0x805B, - GL_DEPTH_COMPONENT16 = 0x81A5, - GL_DEPTH_COMPONENT24 = 0x81A6, - GL_DEPTH_COMPONENT32F = 36012, - GL_STENCIL_INDEX8 = 36168, - GL_DEPTH24_STENCIL8 = 0x88F0, - GL_DEPTH32F_STENCIL8 = 36013, - - GL_R8 = 0x8229, - GL_RG8 = 0x822B, - GL_RG16 = 0x822C, - GL_R16F = 0x822D, - GL_R32F = 0x822E, - GL_RG16F = 0x822F, - GL_RG32F = 0x8230, - GL_RGBA32F = 0x8814, - GL_RGBA16F = 0x881A, - - GL_R8UI = 33330, - GL_R8I = 33329, - GL_R16 = 33322, - GL_R16I = 33331, - GL_R16UI = 33332, - GL_R32I = 33333, - GL_R32UI = 33334, - - - GL_RG8I = 33335, - GL_RG8UI = 33336, - GL_RG16I = 33337, - GL_RG16UI = 33338, - GL_RG32I = 33339, - GL_RG32UI = 33340, - - GL_RGB8 = 32849, - GL_RGB8I = 36239, - GL_RGB8UI = 36221, - - GL_RGBA12 = 32858, - GL_RGBA2 = 32853, - GL_RGBA8I = 36238, - GL_RGBA8UI = 36220, - - GL_RGBA16I = 36232, - GL_RGBA16UI = 36214, - GL_RGBA32I = 36226, - GL_RGBA32UI = 36208, - - - GL_R8_SNORM = 0x8F94, - GL_RG8_SNORM = 0x8F95, - GL_RGB8_SNORM = 0x8F96, - GL_RGBA8_SNORM = 0x8F97, - GL_R16_SNORM = 0x8F98, - GL_RG16_SNORM = 0x8F99, - GL_RGB16_SNORM = 0x8F9A, - GL_RGBA16_SNORM = 0x8F9B, - - GL_RGB10_A2 = 32857, - GL_RGB10_A2UI = 36975, - - GL_RGB16 = 32852, - GL_RGB16F = 34843, - GL_RGB16I = 36233, - GL_RGB16UI = 36215, - - GL_RGB32F = 34837, - GL_RGB32I = 36227, - GL_RGB32UI = 36209, + GlRgba4 = 0x8056, + GlRgb5 = 0x8050, + GlRgb565 = 0x8D62, + GlRgba8 = 0x8058, + GlRgb5A1 = 0x8057, + GlRgba16 = 0x805B, + GlDepthComponent16 = 0x81A5, + GlDepthComponent24 = 0x81A6, + GlDepthComponent32F = 36012, + GlStencilIndex8 = 36168, + GlDepth24Stencil8 = 0x88F0, + GlDepth32FStencil8 = 36013, + + GlR8 = 0x8229, + GlRg8 = 0x822B, + GlRg16 = 0x822C, + GlR16F = 0x822D, + GlR32F = 0x822E, + GlRg16F = 0x822F, + GlRg32F = 0x8230, + GlRgba32F = 0x8814, + GlRgba16F = 0x881A, + + GlR8Ui = 33330, + GlR8I = 33329, + GlR16 = 33322, + GlR16I = 33331, + GlR16Ui = 33332, + GlR32I = 33333, + GlR32Ui = 33334, + + + GlRg8I = 33335, + GlRg8Ui = 33336, + GlRg16I = 33337, + GlRg16Ui = 33338, + GlRg32I = 33339, + GlRg32Ui = 33340, + + GlRgb8 = 32849, + GlRgb8I = 36239, + GlRgb8Ui = 36221, + + GlRgba12 = 32858, + GlRgba2 = 32853, + GlRgba8I = 36238, + GlRgba8Ui = 36220, + + GlRgba16I = 36232, + GlRgba16Ui = 36214, + GlRgba32I = 36226, + GlRgba32Ui = 36208, + + + GlR8Snorm = 0x8F94, + GlRg8Snorm = 0x8F95, + GlRgb8Snorm = 0x8F96, + GlRgba8Snorm = 0x8F97, + GlR16Snorm = 0x8F98, + GlRg16Snorm = 0x8F99, + GlRgb16Snorm = 0x8F9A, + GlRgba16Snorm = 0x8F9B, + + GlRgb10A2 = 32857, + GlRgb10A2Ui = 36975, + + GlRgb16 = 32852, + GlRgb16F = 34843, + GlRgb16I = 36233, + GlRgb16Ui = 36215, + + GlRgb32F = 34837, + GlRgb32I = 36227, + GlRgb32Ui = 36209, //BC1 - GL_COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0, - GL_COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C, - GL_COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1, - GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8C4D, + GlCompressedRgbS3TcDxt1Ext = 0x83F0, + GlCompressedSrgbS3TcDxt1Ext = 0x8C4C, + GlCompressedRgbaS3TcDxt1Ext = 0x83F1, + GlCompressedSrgbAlphaS3TcDxt1Ext = 0x8C4D, //BC2 - GL_COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2, - GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8C4E, + GlCompressedRgbaS3TcDxt3Ext = 0x83F2, + GlCompressedSrgbAlphaS3TcDxt3Ext = 0x8C4E, //BC3 - GL_COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3, - GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F, + GlCompressedRgbaS3TcDxt5Ext = 0x83F3, + GlCompressedSrgbAlphaS3TcDxt5Ext = 0x8C4F, //BC4 & BC5 - GL_COMPRESSED_RED_GREEN_RGTC2_EXT = 36285, - GL_COMPRESSED_RED_RGTC1_EXT = 36283, - GL_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT = 36286, - GL_COMPRESSED_SIGNED_RED_RGTC1_EXT = 36284, + GlCompressedRedGreenRgtc2Ext = 36285, + GlCompressedRedRgtc1Ext = 36283, + GlCompressedSignedRedGreenRgtc2Ext = 36286, + GlCompressedSignedRedRgtc1Ext = 36284, //BC6 & BC7 - GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT_ARB = 36494, - GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB = 36495, - GL_COMPRESSED_RGBA_BPTC_UNORM_ARB = 36492, - GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB = 36493, + GlCompressedRgbBptcSignedFloatArb = 36494, + GlCompressedRgbBptcUnsignedFloatArb = 36495, + GlCompressedRgbaBptcUnormArb = 36492, + GlCompressedSrgbAlphaBptcUnormArb = 36493, + GlCompressedRgbAtc = 0x8C92, + GlCompressedRgbaAtcExplicitAlpha = 0x8C93, + GlCompressedRgbaAtcInterpolatedAlpha = 0x87EE, // ETC1 & 2 - GL_ETC1_RGB8_OES = 0x8D64, + GlEtc1Rgb8Oes = 0x8D64, - GL_COMPRESSED_R11_EAC = 0x9270, - GL_COMPRESSED_SIGNED_R11_EAC = 0x9271, - GL_COMPRESSED_RG11_EAC = 0x9272, - GL_COMPRESSED_SIGNED_RG11_EAC = 0x9273, + GlCompressedR11Eac = 0x9270, + GlCompressedSignedR11Eac = 0x9271, + GlCompressedRg11Eac = 0x9272, + GlCompressedSignedRg11Eac = 0x9273, - GL_COMPRESSED_RGB8_ETC2 = 0x9274, - GL_COMPRESSED_SRGB8_ETC2 = 0x9275, - GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276, - GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277, - GL_COMPRESSED_RGBA8_ETC2_EAC = 0x9278, - GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279, + GlCompressedRgb8Etc2 = 0x9274, + GlCompressedSrgb8Etc2 = 0x9275, + GlCompressedRgb8PunchthroughAlpha1Etc2 = 0x9276, + GlCompressedSrgb8PunchthroughAlpha1Etc2 = 0x9277, + GlCompressedRgba8Etc2Eac = 0x9278, + GlCompressedSrgb8Alpha8Etc2Eac = 0x9279, + + // Apple extension BGRA8 + GlBgra8Extension = 0x93A1, // ASTC - GL_COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0, - GL_COMPRESSED_RGBA_ASTC_5x4_KHR = 0x93B1, - GL_COMPRESSED_RGBA_ASTC_5x5_KHR = 0x93B2, - GL_COMPRESSED_RGBA_ASTC_6x5_KHR = 0x93B3, - GL_COMPRESSED_RGBA_ASTC_6x6_KHR = 0x93B4, - GL_COMPRESSED_RGBA_ASTC_8x5_KHR = 0x93B5, - GL_COMPRESSED_RGBA_ASTC_8x6_KHR = 0x93B6, - GL_COMPRESSED_RGBA_ASTC_8x8_KHR = 0x93B7, - GL_COMPRESSED_RGBA_ASTC_10x5_KHR = 0x93B8, - GL_COMPRESSED_RGBA_ASTC_10x6_KHR = 0x93B9, - GL_COMPRESSED_RGBA_ASTC_10x8_KHR = 0x93BA, - GL_COMPRESSED_RGBA_ASTC_10x10_KHR = 0x93BB, - GL_COMPRESSED_RGBA_ASTC_12x10_KHR = 0x93BC, - GL_COMPRESSED_RGBA_ASTC_12x12_KHR = 0x93BD, - - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR = 0x93D0, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR = 0x93D1, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR = 0x93D2, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR = 0x93D3, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR = 0x93D4, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR = 0x93D5, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR = 0x93D6, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR = 0x93D7, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR = 0x93D8, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR = 0x93D9, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR = 0x93DA, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR = 0x93DB, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR = 0x93DC, - GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR = 0x93DD + GlCompressedRgbaAstc4X4Khr = 0x93B0, + GlCompressedRgbaAstc5X4Khr = 0x93B1, + GlCompressedRgbaAstc5X5Khr = 0x93B2, + GlCompressedRgbaAstc6X5Khr = 0x93B3, + GlCompressedRgbaAstc6X6Khr = 0x93B4, + GlCompressedRgbaAstc8X5Khr = 0x93B5, + GlCompressedRgbaAstc8X6Khr = 0x93B6, + GlCompressedRgbaAstc8X8Khr = 0x93B7, + GlCompressedRgbaAstc10X5Khr = 0x93B8, + GlCompressedRgbaAstc10X6Khr = 0x93B9, + GlCompressedRgbaAstc10X8Khr = 0x93BA, + GlCompressedRgbaAstc10X10Khr = 0x93BB, + GlCompressedRgbaAstc12X10Khr = 0x93BC, + GlCompressedRgbaAstc12X12Khr = 0x93BD, + + GlCompressedSrgb8Alpha8Astc4X4Khr = 0x93D0, + GlCompressedSrgb8Alpha8Astc5X4Khr = 0x93D1, + GlCompressedSrgb8Alpha8Astc5X5Khr = 0x93D2, + GlCompressedSrgb8Alpha8Astc6X5Khr = 0x93D3, + GlCompressedSrgb8Alpha8Astc6X6Khr = 0x93D4, + GlCompressedSrgb8Alpha8Astc8X5Khr = 0x93D5, + GlCompressedSrgb8Alpha8Astc8X6Khr = 0x93D6, + GlCompressedSrgb8Alpha8Astc8X8Khr = 0x93D7, + GlCompressedSrgb8Alpha8Astc10X5Khr = 0x93D8, + GlCompressedSrgb8Alpha8Astc10X6Khr = 0x93D9, + GlCompressedSrgb8Alpha8Astc10X8Khr = 0x93DA, + GlCompressedSrgb8Alpha8Astc10X10Khr = 0x93DB, + GlCompressedSrgb8Alpha8Astc12X10Khr = 0x93DC, + GlCompressedSrgb8Alpha8Astc12X12Khr = 0x93DD } } diff --git a/BCnEnc.Net/Shared/Half.cs b/BCnEnc.Net/Shared/Half.cs new file mode 100644 index 0000000..d1c0cdd --- /dev/null +++ b/BCnEnc.Net/Shared/Half.cs @@ -0,0 +1,1090 @@ +/// ================ Half.cs ==================== +/// The code is free to use for any reason without any restrictions. +/// Ladislav Lang (2009), Joannes Vermorel (2017) + +using System; +using System.Diagnostics; +using System.Globalization; + +namespace BCnEncoder.Shared +{ + /// + /// Represents a half-precision floating point number. + /// + /// + /// Note: + /// Half is not fast enought and precision is also very bad, + /// so is should not be used for mathematical computation (use Single instead). + /// The main advantage of Half type is lower memory cost: two bytes per number. + /// Half is typically used in graphical applications. + /// + /// Note: + /// All functions, where is used conversion half->float/float->half, + /// are approx. ten times slower than float->double/double->float, i.e. ~3ns on 2GHz CPU. + /// + /// References: + /// - Code retrieved from http://sourceforge.net/p/csharp-half/code/HEAD/tree/ on 2015-12-04 + /// - Fast Half Float Conversions, Jeroen van der Zijp, link: http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf + /// - IEEE 754 revision, link: http://grouper.ieee.org/groups/754/ + /// + [Serializable] + public struct Half : IComparable, IFormattable, IConvertible, IComparable, IEquatable + { + /// + /// Internal representation of the half-precision floating-point number. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal ushort Value; + + #region Constants + /// + /// Represents the smallest positive System.Half value greater than zero. This field is constant. + /// + public static readonly Half Epsilon = ToHalf(0x0001); + /// + /// Represents the largest possible value of System.Half. This field is constant. + /// + public static readonly Half MaxValue = ToHalf(0x7bff); + /// + /// Represents the smallest possible value of System.Half. This field is constant. + /// + public static readonly Half MinValue = ToHalf(0xfbff); + /// + /// Represents not a number (NaN). This field is constant. + /// + public static readonly Half NaN = ToHalf(0xfe00); + /// + /// Represents negative infinity. This field is constant. + /// + public static readonly Half NegativeInfinity = ToHalf(0xfc00); + /// + /// Represents positive infinity. This field is constant. + /// + public static readonly Half PositiveInfinity = ToHalf(0x7c00); + #endregion + + #region Constructors + /// + /// Initializes a new instance of System.Half to the value of the specified single-precision floating-point number. + /// + /// The value to represent as a System.Half. + public Half(float value) { this = HalfHelper.SingleToHalf(value); } + /// + /// Initializes a new instance of System.Half to the value of the specified 32-bit signed integer. + /// + /// The value to represent as a System.Half. + public Half(int value) : this((float)value) { } + /// + /// Initializes a new instance of System.Half to the value of the specified 64-bit signed integer. + /// + /// The value to represent as a System.Half. + public Half(long value) : this((float)value) { } + /// + /// Initializes a new instance of System.Half to the value of the specified double-precision floating-point number. + /// + /// The value to represent as a System.Half. + public Half(double value) : this((float)value) { } + /// + /// Initializes a new instance of System.Half to the value of the specified decimal number. + /// + /// The value to represent as a System.Half. + public Half(decimal value) : this((float)value) { } + /// + /// Initializes a new instance of System.Half to the value of the specified 32-bit unsigned integer. + /// + /// The value to represent as a System.Half. + public Half(uint value) : this((float)value) { } + /// + /// Initializes a new instance of System.Half to the value of the specified 64-bit unsigned integer. + /// + /// The value to represent as a System.Half. + public Half(ulong value) : this((float)value) { } + #endregion + + #region Numeric operators + + /// + /// Returns the result of multiplying the specified System.Half value by negative one. + /// + /// A System.Half. + /// A System.Half with the value of half, but the opposite sign. -or- Zero, if half is zero. + public static Half Negate(Half half) { return -half; } + /// + /// Adds two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// A System.Half value that is the sum of half1 and half2. + public static Half Add(Half half1, Half half2) { return half1 + half2; } + /// + /// Subtracts one specified System.Half value from another. + /// + /// A System.Half (the minuend). + /// A System.Half (the subtrahend). + /// The System.Half result of subtracting half2 from half1. + public static Half Subtract(Half half1, Half half2) { return half1 - half2; } + /// + /// Multiplies two specified System.Half values. + /// + /// A System.Half (the multiplicand). + /// A System.Half (the multiplier). + /// A System.Half that is the result of multiplying half1 and half2. + public static Half Multiply(Half half1, Half half2) { return half1 * half2; } + /// + /// Divides two specified System.Half values. + /// + /// A System.Half (the dividend). + /// A System.Half (the divisor). + /// The System.Half that is the result of dividing half1 by half2. + /// half2 is zero. + public static Half Divide(Half half1, Half half2) { return half1 / half2; } + + /// + /// Returns the value of the System.Half operand (the sign of the operand is unchanged). + /// + /// The System.Half operand. + /// The value of the operand, half. + public static Half operator +(Half half) { return half; } + /// + /// Negates the value of the specified System.Half operand. + /// + /// The System.Half operand. + /// The result of half multiplied by negative one (-1). + public static Half operator -(Half half) { return HalfHelper.Negate(half); } + /// + /// Increments the System.Half operand by 1. + /// + /// The System.Half operand. + /// The value of half incremented by 1. + public static Half operator ++(Half half) { return (Half)(half + 1f); } + /// + /// Decrements the System.Half operand by one. + /// + /// The System.Half operand. + /// The value of half decremented by 1. + public static Half operator --(Half half) { return (Half)(half - 1f); } + /// + /// Adds two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// The System.Half result of adding half1 and half2. + public static Half operator +(Half half1, Half half2) { return (Half)(half1 + (float)half2); } + /// + /// Subtracts two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// The System.Half result of subtracting half1 and half2. + public static Half operator -(Half half1, Half half2) { return (Half)(half1 - (float)half2); } + /// + /// Multiplies two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// The System.Half result of multiplying half1 by half2. + public static Half operator *(Half half1, Half half2) { return (Half)(half1 * (float)half2); } + /// + /// Divides two specified System.Half values. + /// + /// A System.Half (the dividend). + /// A System.Half (the divisor). + /// The System.Half result of half1 by half2. + public static Half operator /(Half half1, Half half2) { return (Half)(half1 / (float)half2); } + /// + /// Returns a value indicating whether two instances of System.Half are equal. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 and half2 are equal; otherwise, false. + public static bool operator ==(Half half1, Half half2) { return (!IsNaN(half1) && (half1.Value == half2.Value)); } + /// + /// Returns a value indicating whether two instances of System.Half are not equal. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 and half2 are not equal; otherwise, false. + public static bool operator !=(Half half1, Half half2) { return half1.Value != half2.Value; } + /// + /// Returns a value indicating whether a specified System.Half is less than another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is less than half1; otherwise, false. + public static bool operator <(Half half1, Half half2) { return half1 < (float)half2; } + /// + /// Returns a value indicating whether a specified System.Half is greater than another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is greater than half2; otherwise, false. + public static bool operator >(Half half1, Half half2) { return half1 > (float)half2; } + /// + /// Returns a value indicating whether a specified System.Half is less than or equal to another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is less than or equal to half2; otherwise, false. + public static bool operator <=(Half half1, Half half2) { return (half1 == half2) || (half1 < half2); } + /// + /// Returns a value indicating whether a specified System.Half is greater than or equal to another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is greater than or equal to half2; otherwise, false. + public static bool operator >=(Half half1, Half half2) { return (half1 == half2) || (half1 > half2); } + #endregion + + #region Type casting operators + /// + /// Converts an 8-bit unsigned integer to a System.Half. + /// + /// An 8-bit unsigned integer. + /// A System.Half that represents the converted 8-bit unsigned integer. + public static implicit operator Half(byte value) { return new Half((float)value); } + /// + /// Converts a 16-bit signed integer to a System.Half. + /// + /// A 16-bit signed integer. + /// A System.Half that represents the converted 16-bit signed integer. + public static implicit operator Half(short value) { return new Half((float)value); } + /// + /// Converts a Unicode character to a System.Half. + /// + /// A Unicode character. + /// A System.Half that represents the converted Unicode character. + public static implicit operator Half(char value) { return new Half((float)value); } + /// + /// Converts a 32-bit signed integer to a System.Half. + /// + /// A 32-bit signed integer. + /// A System.Half that represents the converted 32-bit signed integer. + public static implicit operator Half(int value) { return new Half((float)value); } + /// + /// Converts a 64-bit signed integer to a System.Half. + /// + /// A 64-bit signed integer. + /// A System.Half that represents the converted 64-bit signed integer. + public static implicit operator Half(long value) { return new Half((float)value); } + /// + /// Converts a single-precision floating-point number to a System.Half. + /// + /// A single-precision floating-point number. + /// A System.Half that represents the converted single-precision floating point number. + public static explicit operator Half(float value) { return new Half(value); } + /// + /// Converts a double-precision floating-point number to a System.Half. + /// + /// A double-precision floating-point number. + /// A System.Half that represents the converted double-precision floating point number. + public static explicit operator Half(double value) { return new Half((float)value); } + /// + /// Converts a decimal number to a System.Half. + /// + /// decimal number + /// A System.Half that represents the converted decimal number. + public static explicit operator Half(decimal value) { return new Half((float)value); } + /// + /// Converts a System.Half to an 8-bit unsigned integer. + /// + /// A System.Half to convert. + /// An 8-bit unsigned integer that represents the converted System.Half. + public static explicit operator byte(Half value) { return (byte)(float)value; } + /// + /// Converts a System.Half to a Unicode character. + /// + /// A System.Half to convert. + /// A Unicode character that represents the converted System.Half. + public static explicit operator char(Half value) { return (char)(float)value; } + /// + /// Converts a System.Half to a 16-bit signed integer. + /// + /// A System.Half to convert. + /// A 16-bit signed integer that represents the converted System.Half. + public static explicit operator short(Half value) { return (short)(float)value; } + /// + /// Converts a System.Half to a 32-bit signed integer. + /// + /// A System.Half to convert. + /// A 32-bit signed integer that represents the converted System.Half. + public static explicit operator int(Half value) { return (int)(float)value; } + /// + /// Converts a System.Half to a 64-bit signed integer. + /// + /// A System.Half to convert. + /// A 64-bit signed integer that represents the converted System.Half. + public static explicit operator long(Half value) { return (long)(float)value; } + /// + /// Converts a System.Half to a single-precision floating-point number. + /// + /// A System.Half to convert. + /// A single-precision floating-point number that represents the converted System.Half. + public static implicit operator float(Half value) { return HalfHelper.HalfToSingle(value); } + /// + /// Converts a System.Half to a double-precision floating-point number. + /// + /// A System.Half to convert. + /// A double-precision floating-point number that represents the converted System.Half. + public static implicit operator double(Half value) { return (float)value; } + /// + /// Converts a System.Half to a decimal number. + /// + /// A System.Half to convert. + /// A decimal number that represents the converted System.Half. + public static explicit operator decimal(Half value) { return (decimal)(float)value; } + /// + /// Converts an 8-bit signed integer to a System.Half. + /// + /// An 8-bit signed integer. + /// A System.Half that represents the converted 8-bit signed integer. + public static implicit operator Half(sbyte value) { return new Half((float)value); } + /// + /// Converts a 16-bit unsigned integer to a System.Half. + /// + /// A 16-bit unsigned integer. + /// A System.Half that represents the converted 16-bit unsigned integer. + public static implicit operator Half(ushort value) { return new Half((float)value); } + /// + /// Converts a 32-bit unsigned integer to a System.Half. + /// + /// A 32-bit unsigned integer. + /// A System.Half that represents the converted 32-bit unsigned integer. + public static implicit operator Half(uint value) { return new Half((float)value); } + /// + /// Converts a 64-bit unsigned integer to a System.Half. + /// + /// A 64-bit unsigned integer. + /// A System.Half that represents the converted 64-bit unsigned integer. + public static implicit operator Half(ulong value) { return new Half((float)value); } + /// + /// Converts a System.Half to an 8-bit signed integer. + /// + /// A System.Half to convert. + /// An 8-bit signed integer that represents the converted System.Half. + public static explicit operator sbyte(Half value) { return (sbyte)(float)value; } + /// + /// Converts a System.Half to a 16-bit unsigned integer. + /// + /// A System.Half to convert. + /// A 16-bit unsigned integer that represents the converted System.Half. + public static explicit operator ushort(Half value) { return (ushort)(float)value; } + /// + /// Converts a System.Half to a 32-bit unsigned integer. + /// + /// A System.Half to convert. + /// A 32-bit unsigned integer that represents the converted System.Half. + public static explicit operator uint(Half value) { return (uint)(float)value; } + /// + /// Converts a System.Half to a 64-bit unsigned integer. + /// + /// A System.Half to convert. + /// A 64-bit unsigned integer that represents the converted System.Half. + public static explicit operator ulong(Half value) { return (ulong)(float)value; } + #endregion + + /// + /// Compares this instance to a specified System.Half object. + /// + /// A System.Half object. + /// + /// A signed number indicating the relative values of this instance and value. + /// Return Value Meaning Less than zero This instance is less than value. Zero + /// This instance is equal to value. Greater than zero This instance is greater than value. + /// + public int CompareTo(Half other) + { + var result = 0; + if (this < other) + { + result = -1; + } + else if (this > other) + { + result = 1; + } + else if (this != other) + { + if (!IsNaN(this)) + { + result = 1; + } + else if (!IsNaN(other)) + { + result = -1; + } + } + + return result; + } + /// + /// Compares this instance to a specified System.Object. + /// + /// An System.Object or null. + /// + /// A signed number indicating the relative values of this instance and value. + /// Return Value Meaning Less than zero This instance is less than value. Zero + /// This instance is equal to value. Greater than zero This instance is greater + /// than value. -or- value is null. + /// + /// value is not a System.Half + public int CompareTo(object obj) + { + var result = 0; + if (obj == null) + { + result = 1; + } + else + { + if (obj is Half) + { + result = CompareTo((Half)obj); + } + else + { + throw new ArgumentException("Object must be of type Half."); + } + } + + return result; + } + /// + /// Returns a value indicating whether this instance and a specified System.Half object represent the same value. + /// + /// A System.Half object to compare to this instance. + /// true if value is equal to this instance; otherwise, false. + public bool Equals(Half other) + { + return ((other == this) || (IsNaN(other) && IsNaN(this))); + } + /// + /// Returns a value indicating whether this instance and a specified System.Object + /// represent the same type and value. + /// + /// An System.Object. + /// true if value is a System.Half and equal to this instance; otherwise, false. + public override bool Equals(object obj) + { + var result = false; + if (obj is Half) + { + var half = (Half)obj; + if ((half == this) || (IsNaN(half) && IsNaN(this))) + { + result = true; + } + } + + return result; + } + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + { + return Value.GetHashCode(); + } + /// + /// Returns the System.TypeCode for value type System.Half. + /// + /// The enumerated constant (TypeCode)255. + public TypeCode GetTypeCode() + { + return (TypeCode)255; + } + + #region BitConverter & Math methods for Half + /// + /// Returns the specified half-precision floating point value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 2. + public static byte[] GetBytes(Half value) + { + return BitConverter.GetBytes(value.Value); + } + /// + /// Converts the value of a specified instance of System.Half to its equivalent binary representation. + /// + /// A System.Half value. + /// A 16-bit unsigned integer that contain the binary representation of value. + public static ushort GetBits(Half value) + { + return value.Value; + } + /// + /// Returns a half-precision floating point number converted from two bytes + /// at a specified position in a byte array. + /// + /// An array of bytes. + /// The starting position within value. + /// A half-precision floating point number formed by two bytes beginning at startIndex. + /// + /// startIndex is greater than or equal to the length of value minus 1, and is + /// less than or equal to the length of value minus 1. + /// + /// value is null. + /// startIndex is less than zero or greater than the length of value minus 1. + public static Half ToHalf(byte[] value, int startIndex) + { + return ToHalf((ushort)BitConverter.ToInt16(value, startIndex)); + } + /// + /// Returns a half-precision floating point number converted from its binary representation. + /// + /// Binary representation of System.Half value + /// A half-precision floating point number formed by its binary representation. + public static Half ToHalf(ushort bits) + { + return new Half { Value = bits }; + } + + /// + /// Returns a value indicating the sign of a half-precision floating-point number. + /// + /// A signed number. + /// + /// A number indicating the sign of value. Number Description -1 value is less + /// than zero. 0 value is equal to zero. 1 value is greater than zero. + /// + /// value is equal to System.Half.NaN. + public static int Sign(Half value) + { + if (value < 0) + { + return -1; + } + else if (value > 0) + { + return 1; + } + else + { + if (value != 0) + { + throw new ArithmeticException("Function does not accept floating point Not-a-Number values."); + } + } + + return 0; + } + /// + /// Returns the absolute value of a half-precision floating-point number. + /// + /// A number in the range System.Half.MinValue ≤ value ≤ System.Half.MaxValue. + /// A half-precision floating-point number, x, such that 0 ≤ x ≤System.Half.MaxValue. + public static Half Abs(Half value) + { + return HalfHelper.Abs(value); + } + /// + /// Returns the larger of two half-precision floating-point numbers. + /// + /// The first of two half-precision floating-point numbers to compare. + /// The second of two half-precision floating-point numbers to compare. + /// + /// Parameter value1 or value2, whichever is larger. If value1, or value2, or both val1 + /// and value2 are equal to System.Half.NaN, System.Half.NaN is returned. + /// + public static Half Max(Half value1, Half value2) + { + return (value1 < value2) ? value2 : value1; + } + /// + /// Returns the smaller of two half-precision floating-point numbers. + /// + /// The first of two half-precision floating-point numbers to compare. + /// The second of two half-precision floating-point numbers to compare. + /// + /// Parameter value1 or value2, whichever is smaller. If value1, or value2, or both val1 + /// and value2 are equal to System.Half.NaN, System.Half.NaN is returned. + /// + public static Half Min(Half value1, Half value2) + { + return (value1 < value2) ? value1 : value2; + } + #endregion + + /// + /// Returns a value indicating whether the specified number evaluates to not a number (System.Half.NaN). + /// + /// A half-precision floating-point number. + /// true if value evaluates to not a number (System.Half.NaN); otherwise, false. + public static bool IsNaN(Half half) + { + return HalfHelper.IsNaN(half); + } + /// + /// Returns a value indicating whether the specified number evaluates to negative or positive infinity. + /// + /// A half-precision floating-point number. + /// true if half evaluates to System.Half.PositiveInfinity or System.Half.NegativeInfinity; otherwise, false. + public static bool IsInfinity(Half half) + { + return HalfHelper.IsInfinity(half); + } + /// + /// Returns a value indicating whether the specified number evaluates to negative infinity. + /// + /// A half-precision floating-point number. + /// true if half evaluates to System.Half.NegativeInfinity; otherwise, false. + public static bool IsNegativeInfinity(Half half) + { + return HalfHelper.IsNegativeInfinity(half); + } + /// + /// Returns a value indicating whether the specified number evaluates to positive infinity. + /// + /// A half-precision floating-point number. + /// true if half evaluates to System.Half.PositiveInfinity; otherwise, false. + public static bool IsPositiveInfinity(Half half) + { + return HalfHelper.IsPositiveInfinity(half); + } + + #region String operations (Parse and ToString) + /// + /// Converts the string representation of a number to its System.Half equivalent. + /// + /// The string representation of the number to convert. + /// The System.Half number equivalent to the number contained in value. + /// value is null. + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value) + { + return (Half)float.Parse(value, CultureInfo.InvariantCulture); + } + /// + /// Converts the string representation of a number to its System.Half equivalent + /// using the specified culture-specific format information. + /// + /// The string representation of the number to convert. + /// An System.IFormatProvider that supplies culture-specific parsing information about value. + /// The System.Half number equivalent to the number contained in s as specified by provider. + /// value is null. + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value, IFormatProvider provider) + { + return (Half)float.Parse(value, provider); + } + /// + /// Converts the string representation of a number in a specified style to its System.Half equivalent. + /// + /// The string representation of the number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates + /// the style elements that can be present in value. A typical value to specify is + /// System.Globalization.NumberStyles.Number. + /// + /// The System.Half number equivalent to the number contained in s as specified by style. + /// value is null. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is the + /// System.Globalization.NumberStyles.AllowHexSpecifier value. + /// + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value, NumberStyles style) + { + return (Half)float.Parse(value, style, CultureInfo.InvariantCulture); + } + /// + /// Converts the string representation of a number to its System.Half equivalent + /// using the specified style and culture-specific format. + /// + /// The string representation of the number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates + /// the style elements that can be present in value. A typical value to specify is + /// System.Globalization.NumberStyles.Number. + /// + /// An System.IFormatProvider object that supplies culture-specific information about the format of value. + /// The System.Half number equivalent to the number contained in s as specified by style and provider. + /// value is null. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is the + /// System.Globalization.NumberStyles.AllowHexSpecifier value. + /// + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value, NumberStyles style, IFormatProvider provider) + { + return (Half)float.Parse(value, style, provider); + } + /// + /// Converts the string representation of a number to its System.Half equivalent. + /// A return value indicates whether the conversion succeeded or failed. + /// + /// The string representation of the number to convert. + /// + /// When this method returns, contains the System.Half number that is equivalent + /// to the numeric value contained in value, if the conversion succeeded, or is zero + /// if the conversion failed. The conversion fails if the s parameter is null, + /// is not a number in a valid format, or represents a number less than System.Half.MinValue + /// or greater than System.Half.MaxValue. This parameter is passed uninitialized. + /// + /// true if s was converted successfully; otherwise, false. + public static bool TryParse(string value, out Half result) + { + float f; + if (float.TryParse(value, out f)) + { + result = (Half)f; + return true; + } + + result = new Half(); + return false; + } + /// + /// Converts the string representation of a number to its System.Half equivalent + /// using the specified style and culture-specific format. A return value indicates + /// whether the conversion succeeded or failed. + /// + /// The string representation of the number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates + /// the permitted format of value. A typical value to specify is System.Globalization.NumberStyles.Number. + /// + /// An System.IFormatProvider object that supplies culture-specific parsing information about value. + /// + /// When this method returns, contains the System.Half number that is equivalent + /// to the numeric value contained in value, if the conversion succeeded, or is zero + /// if the conversion failed. The conversion fails if the s parameter is null, + /// is not in a format compliant with style, or represents a number less than + /// System.Half.MinValue or greater than System.Half.MaxValue. This parameter is passed uninitialized. + /// + /// true if s was converted successfully; otherwise, false. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style + /// is the System.Globalization.NumberStyles.AllowHexSpecifier value. + /// + public static bool TryParse(string value, NumberStyles style, IFormatProvider provider, out Half result) + { + var parseResult = false; + float f; + if (float.TryParse(value, style, provider, out f)) + { + result = (Half)f; + parseResult = true; + } + else + { + result = new Half(); + } + + return parseResult; + } + /// + /// Converts the numeric value of this instance to its equivalent string representation. + /// + /// A string that represents the value of this instance. + public override string ToString() + { + return ((float)this).ToString(CultureInfo.InvariantCulture); + } + /// + /// Converts the numeric value of this instance to its equivalent string representation + /// using the specified culture-specific format information. + /// + /// An System.IFormatProvider that supplies culture-specific formatting information. + /// The string representation of the value of this instance as specified by provider. + public string ToString(IFormatProvider formatProvider) + { + return ((float)this).ToString(formatProvider); + } + /// + /// Converts the numeric value of this instance to its equivalent string representation, using the specified format. + /// + /// A numeric format string. + /// The string representation of the value of this instance as specified by format. + public string ToString(string format) + { + return ((float)this).ToString(format, CultureInfo.InvariantCulture); + } + /// + /// Converts the numeric value of this instance to its equivalent string representation + /// using the specified format and culture-specific format information. + /// + /// A numeric format string. + /// An System.IFormatProvider that supplies culture-specific formatting information. + /// The string representation of the value of this instance as specified by format and provider. + /// format is invalid. + public string ToString(string format, IFormatProvider formatProvider) + { + return ((float)this).ToString(format, formatProvider); + } + #endregion + + #region IConvertible Members + float IConvertible.ToSingle(IFormatProvider provider) + { + return this; + } + TypeCode IConvertible.GetTypeCode() + { + return GetTypeCode(); + } + bool IConvertible.ToBoolean(IFormatProvider provider) + { + return Convert.ToBoolean(this); + } + byte IConvertible.ToByte(IFormatProvider provider) + { + return Convert.ToByte(this); + } + char IConvertible.ToChar(IFormatProvider provider) + { + throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "Invalid cast from '{0}' to '{1}'.", "Half", "Char")); + } + DateTime IConvertible.ToDateTime(IFormatProvider provider) + { + throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "Invalid cast from '{0}' to '{1}'.", "Half", "DateTime")); + } + decimal IConvertible.ToDecimal(IFormatProvider provider) + { + return Convert.ToDecimal(this); + } + double IConvertible.ToDouble(IFormatProvider provider) + { + return Convert.ToDouble(this); + } + short IConvertible.ToInt16(IFormatProvider provider) + { + return Convert.ToInt16(this); + } + int IConvertible.ToInt32(IFormatProvider provider) + { + return Convert.ToInt32(this); + } + long IConvertible.ToInt64(IFormatProvider provider) + { + return Convert.ToInt64(this); + } + sbyte IConvertible.ToSByte(IFormatProvider provider) + { + return Convert.ToSByte(this); + } + string IConvertible.ToString(IFormatProvider provider) + { + return Convert.ToString(this, CultureInfo.InvariantCulture); + } + object IConvertible.ToType(Type conversionType, IFormatProvider provider) + { + return (((float)this) as IConvertible).ToType(conversionType, provider); + } + ushort IConvertible.ToUInt16(IFormatProvider provider) + { + return Convert.ToUInt16(this); + } + uint IConvertible.ToUInt32(IFormatProvider provider) + { + return Convert.ToUInt32(this); + } + ulong IConvertible.ToUInt64(IFormatProvider provider) + { + return Convert.ToUInt64(this); + } + #endregion + } +} + +namespace BCnEncoder.Shared +{ + /// + /// Helper class for Half conversions and some low level operations. + /// This class is internally used in the Half class. + /// + /// + /// References: + /// - Code retrieved from http://sourceforge.net/p/csharp-half/code/HEAD/tree/ on 2015-12-04 + /// - Fast Half Float Conversions, Jeroen van der Zijp, link: http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf + /// + internal static class HalfHelper + { + private static readonly uint[] MantissaTable = GenerateMantissaTable(); + private static readonly uint[] ExponentTable = GenerateExponentTable(); + private static readonly ushort[] OffsetTable = GenerateOffsetTable(); + private static readonly ushort[] BaseTable = GenerateBaseTable(); + private static readonly sbyte[] ShiftTable = GenerateShiftTable(); + + // Transforms the subnormal representation to a normalized one. + private static uint ConvertMantissa(int i) + { + var m = (uint)(i << 13); // Zero pad mantissa bits + uint e = 0; // Zero exponent + + // While not normalized + while ((m & 0x00800000) == 0) + { + e -= 0x00800000; // Decrement exponent (1<<23) + m <<= 1; // Shift mantissa + } + m &= unchecked((uint)~0x00800000); // Clear leading 1 bit + e += 0x38800000; // Adjust bias ((127-14)<<23) + return m | e; // Return combined number + } + + private static uint[] GenerateMantissaTable() + { + var mantissaTable = new uint[2048]; + mantissaTable[0] = 0; + for (var i = 1; i < 1024; i++) + { + mantissaTable[i] = ConvertMantissa(i); + } + for (var i = 1024; i < 2048; i++) + { + mantissaTable[i] = (uint)(0x38000000 + ((i - 1024) << 13)); + } + + return mantissaTable; + } + private static uint[] GenerateExponentTable() + { + var exponentTable = new uint[64]; + exponentTable[0] = 0; + for (var i = 1; i < 31; i++) + { + exponentTable[i] = (uint)(i << 23); + } + exponentTable[31] = 0x47800000; + exponentTable[32] = 0x80000000; + for (var i = 33; i < 63; i++) + { + exponentTable[i] = (uint)(0x80000000 + ((i - 32) << 23)); + } + exponentTable[63] = 0xc7800000; + + return exponentTable; + } + private static ushort[] GenerateOffsetTable() + { + var offsetTable = new ushort[64]; + offsetTable[0] = 0; + for (var i = 1; i < 32; i++) + { + offsetTable[i] = 1024; + } + offsetTable[32] = 0; + for (var i = 33; i < 64; i++) + { + offsetTable[i] = 1024; + } + + return offsetTable; + } + private static ushort[] GenerateBaseTable() + { + var baseTable = new ushort[512]; + for (var i = 0; i < 256; ++i) + { + var e = (sbyte)(127 - i); + if (e > 24) + { // Very small numbers map to zero + baseTable[i | 0x000] = 0x0000; + baseTable[i | 0x100] = 0x8000; + } + else if (e > 14) + { // Small numbers map to denorms + baseTable[i | 0x000] = (ushort)(0x0400 >> (18 + e)); + baseTable[i | 0x100] = (ushort)((0x0400 >> (18 + e)) | 0x8000); + } + else if (e >= -15) + { // Normal numbers just lose precision + baseTable[i | 0x000] = (ushort)((15 - e) << 10); + baseTable[i | 0x100] = (ushort)(((15 - e) << 10) | 0x8000); + } + else if (e > -128) + { // Large numbers map to Infinity + baseTable[i | 0x000] = 0x7c00; + baseTable[i | 0x100] = 0xfc00; + } + else + { // Infinity and NaN's stay Infinity and NaN's + baseTable[i | 0x000] = 0x7c00; + baseTable[i | 0x100] = 0xfc00; + } + } + + return baseTable; + } + private static sbyte[] GenerateShiftTable() + { + var shiftTable = new sbyte[512]; + for (var i = 0; i < 256; ++i) + { + var e = (sbyte)(127 - i); + if (e > 24) + { // Very small numbers map to zero + shiftTable[i | 0x000] = 24; + shiftTable[i | 0x100] = 24; + } + else if (e > 14) + { // Small numbers map to denorms + shiftTable[i | 0x000] = (sbyte)(e - 1); + shiftTable[i | 0x100] = (sbyte)(e - 1); + } + else if (e >= -15) + { // Normal numbers just lose precision + shiftTable[i | 0x000] = 13; + shiftTable[i | 0x100] = 13; + } + else if (e > -128) + { // Large numbers map to Infinity + shiftTable[i | 0x000] = 24; + shiftTable[i | 0x100] = 24; + } + else + { // Infinity and NaN's stay Infinity and NaN's + shiftTable[i | 0x000] = 13; + shiftTable[i | 0x100] = 13; + } + } + + return shiftTable; + } + + public static unsafe float HalfToSingle(Half half) + { + var result = MantissaTable[OffsetTable[half.Value >> 10] + (half.Value & 0x3ff)] + ExponentTable[half.Value >> 10]; + return *(float*)&result; + } + public static unsafe Half SingleToHalf(float single) + { + var value = *(uint*)&single; + + var result = (ushort)(BaseTable[(value >> 23) & 0x1ff] + ((value & 0x007fffff) >> ShiftTable[value >> 23])); + return Half.ToHalf(result); + } + + public static Half Negate(Half half) + { + return Half.ToHalf((ushort)(half.Value ^ 0x8000)); + } + public static Half Abs(Half half) + { + return Half.ToHalf((ushort)(half.Value & 0x7fff)); + } + + public static bool IsNaN(Half half) + { + return (half.Value & 0x7fff) > 0x7c00; + } + public static bool IsInfinity(Half half) + { + return (half.Value & 0x7fff) == 0x7c00; + } + public static bool IsPositiveInfinity(Half half) + { + return half.Value == 0x7c00; + } + public static bool IsNegativeInfinity(Half half) + { + return half.Value == 0xfc00; + } + } +} diff --git a/BCnEnc.Net/Shared/HdrImage.cs b/BCnEnc.Net/Shared/HdrImage.cs new file mode 100644 index 0000000..a4143de --- /dev/null +++ b/BCnEnc.Net/Shared/HdrImage.cs @@ -0,0 +1,344 @@ +using System; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using CommunityToolkit.HighPerformance; + +namespace BCnEncoder.Shared +{ + /// + /// Reads and writes .hdr RGBE/Radiance HDR files. File format by Gregory Ward. + /// This class is experimental, incomplete and probably going to be removed in a future version. + /// Use only if you don't have anything better. + /// + public class HdrImage + { + public enum ColorSpace + { + Rgbe, + Xyze + } + + public float exposure = -1; + public float gamma = -1; + public int width; + public int height; + + public ColorRgbFloat[] pixels; + + /// + /// Gets a span2D over the array. + /// + public Span2D PixelSpan => new Span2D(pixels, height, width); + /// + /// Gets a span2D over the array. + /// + public Memory2D PixelMemory => new Memory2D(pixels, height, width); + + public HdrImage() {} + + public HdrImage(int width, int height, float exposure = 0, float gamma = 0) + { + this.width = width; + this.height = height; + this.exposure = exposure; + this.gamma = gamma; + this.pixels = new ColorRgbFloat[width * height]; + } + + public HdrImage(Span2D pixels, float exposure = 0, float gamma = 0) + { + this.width = pixels.Width; + this.height = pixels.Height; + this.exposure = exposure; + this.gamma = gamma; + this.pixels = new ColorRgbFloat[width * height]; + pixels.CopyTo(this.pixels); + } + + // StreamReader class does not work. Have to use custom string reading. + private static string ReadFromStream(Stream stream) + { + var i = 0; + var buffer = new char[512]; + char c; + do + { + var b = stream.ReadByte(); + if (b == -1) + { + return null; + } + c = (char)b; + buffer[i++] = c; + } while (c != (char)10); + return new string(buffer, 0, i).Trim(); + } + + private static void WriteLineToStream(BinaryWriter br, string s) + { + foreach (var c in s) + { + var b = (byte) c; + br.Write(b); + } + br.Write((byte)10); + } + + /// + /// Read a Radiance HDR image by filename. + /// Just calls internally. + /// + /// The filename or path of the image + /// A new HdrImage with the data + public static HdrImage Read(string filename) + { + using var fs = File.OpenRead(filename); + return Read(fs); + } + + /// + /// Read a Radiance HDR image from a stream + /// + /// The stream to read from + /// A new HdrImage with the data + public static HdrImage Read(Stream stream) + { + var image = new HdrImage(); + + var line = ReadFromStream(stream); + + if (!(line == "#?RGBE" || line == "#?RADIANCE" || line == "#?AUTOPANO")) + { + throw new FileLoadException("Correct file type specifier was not found."); + } + + var colorSpace = ColorSpace.Rgbe; + + do + { + line = ReadFromStream(stream); + + if (line == null) + { + throw new FileLoadException("Reached end of stream."); + } + + line = line.Trim(); + + if (line == "") + { + break; + } + + if (line.StartsWith("#")) // Found comment + { + continue; + } + + if (line == "FORMAT=32-bit_rle_rgbe") + { + colorSpace = ColorSpace.Rgbe; + } + + else if (line == "FORMAT=32-bit_rle_xyze") + { + colorSpace = ColorSpace.Xyze; + } + + else if (line.StartsWith("EXPOSURE=")) + { + image.exposure = float.Parse(line.Replace("EXPOSURE=", "").Trim(), CultureInfo.InvariantCulture); + } + + else if (line.StartsWith("GAMMA=")) + { + image.gamma = float.Parse(line.Replace("GAMMA=", "").Trim(), CultureInfo.InvariantCulture); + } + + } while (true); + + if (image.exposure < 0.000001) + { + image.exposure = 1.0f; + } + + if (image.gamma < 0.000001) + { + image.gamma = 1.0f; + } + + var imgSize = ReadFromStream(stream).Split(' '); + + var yStr = imgSize[0]; + image.height = int.Parse(imgSize[1]); + var xStr = imgSize[2]; + image.width = int.Parse(imgSize[3]); + + ReadPixels(image, stream); + + if (colorSpace == ColorSpace.Xyze) + { + // Transform colorspace + var xyzColors = MemoryMarshal.Cast(image.pixels.AsSpan()); + for (var i = 0; i < xyzColors.Length; i++) + { + image.pixels[i] = xyzColors[i].ToColorRgbFloat(); + } + } + + return image; + } + + + private static void RleReadChannel(BinaryReader br, Span dest, int width) + { + var i = 0; + var data = new byte[2]; + while (i < width) + { + if (br.Read(data) == 0) + { + throw new FileLoadException("Not enough data in RLE"); + } + if (data[0] > 128) + { + // same byte is repeated many times + var len = data[0] - 128; + for (; len > 0; len--) + { + dest[i++] = data[1]; + } + } + else + { + // different byte sequence + dest[i++] = data[1]; + + var len = data[0] - 1; + if (len > 0) + { + if (br.Read(dest.Slice(i, len)) == 0) + { + throw new FileLoadException("Not enough data in RLE"); + } + i += len; + } + } + } + + if (i != width) + { + throw new FileLoadException("Scanline size was different from width"); + } + } + + + private static void ReadPixels(HdrImage destImage, Stream stream) + { + var height = destImage.height; + var width = destImage.width; + destImage.pixels = new ColorRgbFloat[destImage.height * destImage.width]; + Span bytes = new byte[destImage.width * 4]; + + using var br = new BinaryReader(stream, Encoding.ASCII, true); + + var header = new byte[4]; + + for (var y = 0; y < height; y++) + { + br.Read(header); + + var isRle = header[0] == 2 && header[1] == 2 && + (header[2] << 8) + header[3] == width; // whether the scanline is rle or not + + if (isRle) + { + // for each channel + for (var i = 0; i < 4; i++) + { + RleReadChannel(br, bytes.Slice(width * i, width), width); + } + + for (var x = 0; x < width; x++) + { + var color = new ColorRgbe( + bytes[x + width * 0], + bytes[x + width * 1], + bytes[x + width * 2], + bytes[x + width * 3] + ); + + destImage.pixels[y * width + x] = color.ToColorRgbFloat(destImage.exposure); + } + } + else + { + br.Read(bytes.Slice(4)); + header.CopyTo(bytes); + + for (var x = 0; x < width; x++) + { + var color = new ColorRgbe( + bytes[4 * x + 0], + bytes[4 * x + 1], + bytes[4 * x + 2], + bytes[4 * x + 3] + ); + + destImage.pixels[y * width + x] = color.ToColorRgbFloat(destImage.exposure); + } + } + } + } + + /// + /// Write this file to a stream. + /// + /// The stream to write it to. + public void Write(Stream stream) + { + using var br = new BinaryWriter(stream, Encoding.ASCII, true); + + WriteLineToStream(br, "#?RADIANCE"); + WriteLineToStream(br, "# BCnEncoder.Net HdrImage"); + WriteLineToStream(br, "FORMAT=32-bit_rle_rgbe"); + if (exposure > 0) + { + WriteLineToStream(br, "EXPOSURE=" + exposure.ToString(CultureInfo.InvariantCulture)); + } + if (gamma > 0) + { + WriteLineToStream(br, "GAMMA=" + gamma.ToString(CultureInfo.InvariantCulture)); + } + + WriteLineToStream(br, ""); // Start data with empty row + WriteLineToStream(br, $"-Y {height} +X {width}"); + + WritePixels(br); + } + + private void WritePixels(BinaryWriter br) + { + var buffer = new byte[4]; + var span = PixelSpan; + + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + var pixel = span[y, x]; + var rgbe = new ColorRgbe(pixel); + + buffer[0] = rgbe.r; + buffer[1] = rgbe.g; + buffer[2] = rgbe.b; + buffer[3] = rgbe.e; + + br.Write(buffer); + } + } + } + } +} diff --git a/BCnEnc.Net/Shared/ImageFiles/DdsFile.cs b/BCnEnc.Net/Shared/ImageFiles/DdsFile.cs new file mode 100644 index 0000000..4134a37 --- /dev/null +++ b/BCnEnc.Net/Shared/ImageFiles/DdsFile.cs @@ -0,0 +1,1103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace BCnEncoder.Shared.ImageFiles +{ + public class DdsFile + { + public DdsHeader header; + public DdsHeaderDx10 dx10Header; + public List Faces { get; } = new List(); + + public DdsFile() { } + public DdsFile(DdsHeader header) + { + this.header = header; + } + + public DdsFile(DdsHeader header, DdsHeaderDx10 dx10Header) + { + this.header = header; + this.dx10Header = dx10Header; + } + + public static DdsFile Load(Stream s) + { + using (var br = new BinaryReader(s, Encoding.UTF8, true)) + { + var magic = br.ReadUInt32(); + if (magic != 0x20534444U) + { + throw new FormatException("The file does not contain a dds file."); + } + var header = br.ReadStruct(); + DdsHeaderDx10 dx10Header = default; + if (header.dwSize != 124) + { + throw new FormatException("The file header contains invalid dwSize."); + } + + var dx10Format = header.ddsPixelFormat.IsDxt10Format; + + DdsFile output; + + if (dx10Format) + { + dx10Header = br.ReadStruct(); + output = new DdsFile(header, dx10Header); + } + else + { + output = new DdsFile(header); + } + + var mipMapCount = (header.dwCaps & HeaderCaps.DdscapsMipmap) != 0 ? header.dwMipMapCount : 1; + + // Assume at least 1 mip level + mipMapCount = Math.Max(mipMapCount, 1); + + var faceCount = (header.dwCaps2 & HeaderCaps2.Ddscaps2Cubemap) != 0 ? 6u : 1u; + var width = header.dwWidth; + var height = header.dwHeight; + + for (var face = 0; face < faceCount; face++) + { + var format = dx10Format ? dx10Header.dxgiFormat : header.ddsPixelFormat.DxgiFormat; + var sizeInBytes = GetSizeInBytes(format, width, height); + + output.Faces.Add(new DdsFace(width, height, sizeInBytes, (int)mipMapCount)); + + for (var mip = 0; mip < mipMapCount; mip++) + { + MipMapper.CalculateMipLevelSize( + (int)header.dwWidth, + (int)header.dwHeight, + mip, + out var mipWidth, + out var mipHeight); + + if (mip > 0) //Calculate new byteSize + { + sizeInBytes = GetSizeInBytes(format, (uint)mipWidth, (uint)mipHeight); + } + + var data = new byte[sizeInBytes]; + br.Read(data); + output.Faces[face].MipMaps[mip] = new DdsMipMap(data, (uint)mipWidth, (uint)mipHeight); + } + } + + return output; + } + } + + public void Write(Stream outputStream) + { + if (Faces.Count < 1 || Faces[0].MipMaps.Length < 1) + { + throw new InvalidOperationException("The DDS structure should have at least 1 mipmap level and 1 Face before writing to file."); + } + + header.dwFlags |= HeaderFlags.Required; + + header.dwMipMapCount = (uint)Faces[0].MipMaps.Length; + if (header.dwMipMapCount > 1) // MipMaps + { + header.dwCaps |= HeaderCaps.DdscapsMipmap | HeaderCaps.DdscapsComplex; + } + if (Faces.Count == 6) // CubeMap + { + header.dwCaps |= HeaderCaps.DdscapsComplex; + header.dwCaps2 |= HeaderCaps2.Ddscaps2Cubemap | + HeaderCaps2.Ddscaps2CubemapPositivex | + HeaderCaps2.Ddscaps2CubemapNegativex | + HeaderCaps2.Ddscaps2CubemapPositivey | + HeaderCaps2.Ddscaps2CubemapNegativey | + HeaderCaps2.Ddscaps2CubemapPositivez | + HeaderCaps2.Ddscaps2CubemapNegativez; + } + + header.dwWidth = Faces[0].Width; + header.dwHeight = Faces[0].Height; + + for (var i = 0; i < Faces.Count; i++) + { + if (Faces[i].Width != header.dwWidth || Faces[i].Height != header.dwHeight) + { + throw new InvalidOperationException("Faces with different sizes are not supported."); + } + } + + var faceCount = Faces.Count; + var mipCount = (int)header.dwMipMapCount; + + using (var bw = new BinaryWriter(outputStream, Encoding.UTF8, true)) + { + bw.Write(0x20534444U); // magic 'DDS ' + + bw.WriteStruct(header); + + if (header.ddsPixelFormat.IsDxt10Format) + { + bw.WriteStruct(dx10Header); + } + + for (var face = 0; face < faceCount; face++) + { + for (var mip = 0; mip < mipCount; mip++) + { + bw.Write(Faces[face].MipMaps[mip].Data); + } + } + } + } + + private static uint GetSizeInBytes(DxgiFormat format, uint width, uint height) + { + uint sizeInBytes; + if (format.IsCompressedFormat()) + { + sizeInBytes = (uint)ImageToBlocks.CalculateNumOfBlocks((int)width, (int)height); + sizeInBytes *= (uint)format.GetByteSize(); + } + else + { + sizeInBytes = width * height; + sizeInBytes = (uint)(sizeInBytes * format.GetByteSize()); + } + + return sizeInBytes; + } + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct DdsHeader + { + /// + /// Has to be 124 + /// + public uint dwSize; + public HeaderFlags dwFlags; + public uint dwHeight; + public uint dwWidth; + public uint dwPitchOrLinearSize; + public uint dwDepth; + public uint dwMipMapCount; + public fixed uint dwReserved1[11]; + public DdsPixelFormat ddsPixelFormat; + public HeaderCaps dwCaps; + public HeaderCaps2 dwCaps2; + public uint dwCaps3; + public uint dwCaps4; + public uint dwReserved2; + + public static (DdsHeader, DdsHeaderDx10) InitializeCompressed(int width, int height, DxgiFormat format, bool preferDxt10Header) + { + var header = new DdsHeader(); + var dxt10Header = new DdsHeaderDx10(); + + header.dwSize = 124; + header.dwFlags = HeaderFlags.Required; + header.dwWidth = (uint)width; + header.dwHeight = (uint)height; + header.dwDepth = 1; + header.dwMipMapCount = 1; + header.dwCaps = HeaderCaps.DdscapsTexture; + + if (preferDxt10Header) + { + // ATC formats cannot be written to DXT10 header due to lack of a DxgiFormat enum + switch (format) + { + case DxgiFormat.DxgiFormatAtcExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atc + }; + break; + + case DxgiFormat.DxgiFormatAtcExplicitAlphaExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atci + }; + break; + + case DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atca + }; + break; + + default: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Dx10 + }; + dxt10Header.arraySize = 1; + dxt10Header.dxgiFormat = format; + dxt10Header.resourceDimension = D3D10ResourceDimension.D3D10ResourceDimensionTexture2D; + break; + } + } + else + { + switch (format) + { + case DxgiFormat.DxgiFormatBc1Unorm: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Dxt1 + }; + break; + + case DxgiFormat.DxgiFormatBc2Unorm: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Dxt3 + }; + break; + + case DxgiFormat.DxgiFormatBc3Unorm: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Dxt5 + }; + break; + + case DxgiFormat.DxgiFormatBc4Unorm: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Bc4U + }; + break; + + case DxgiFormat.DxgiFormatBc5Unorm: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Ati2 + }; + break; + + case DxgiFormat.DxgiFormatAtcExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atc + }; + break; + + case DxgiFormat.DxgiFormatAtcExplicitAlphaExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atci + }; + break; + + case DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atca + }; + break; + + default: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Dx10 + }; + dxt10Header.arraySize = 1; + dxt10Header.dxgiFormat = format; + dxt10Header.resourceDimension = D3D10ResourceDimension.D3D10ResourceDimensionTexture2D; + break; + } + } + + return (header, dxt10Header); + } + + public static DdsHeader InitializeUncompressed(int width, int height, DxgiFormat format) + { + var header = new DdsHeader(); + + header.dwSize = 124; + header.dwFlags = HeaderFlags.Required | HeaderFlags.DdsdPitch; + header.dwWidth = (uint)width; + header.dwHeight = (uint)height; + header.dwDepth = 1; + header.dwMipMapCount = 1; + header.dwCaps = HeaderCaps.DdscapsTexture; + + if (format == DxgiFormat.DxgiFormatR8Unorm) + { + header.ddsPixelFormat = new DdsPixelFormat() + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfLuminance, + dwRgbBitCount = 8, + dwRBitMask = 0xFF + }; + header.dwPitchOrLinearSize = (uint)((width * 8 + 7) / 8); + } + else if (format == DxgiFormat.DxgiFormatR8G8Unorm) + { + header.ddsPixelFormat = new DdsPixelFormat() + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfLuminance | PixelFormatFlags.DdpfAlphaPixels, + dwRgbBitCount = 16, + dwRBitMask = 0xFF, + dwGBitMask = 0xFF00 + }; + header.dwPitchOrLinearSize = (uint)((width * 16 + 7) / 8); + } + else if (format == DxgiFormat.DxgiFormatR8G8B8A8Unorm) + { + header.ddsPixelFormat = new DdsPixelFormat() + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfRgb | PixelFormatFlags.DdpfAlphaPixels, + dwRgbBitCount = 32, + dwRBitMask = 0xFF, + dwGBitMask = 0xFF00, + dwBBitMask = 0xFF0000, + dwABitMask = 0xFF000000, + }; + header.dwPitchOrLinearSize = (uint)((width * 32 + 7) / 8); + } + else if (format == DxgiFormat.DxgiFormatB8G8R8A8Unorm) + { + header.ddsPixelFormat = new DdsPixelFormat() + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfRgb | PixelFormatFlags.DdpfAlphaPixels, + dwRgbBitCount = 32, + dwRBitMask = 0xFF0000, + dwGBitMask = 0xFF00, + dwBBitMask = 0xFF, + dwABitMask = 0xFF000000, + }; + header.dwPitchOrLinearSize = (uint)((width * 32 + 7) / 8); + } + else + { + throw new NotImplementedException("This Format is not implemented in this method"); + } + + return header; + } + } + + public struct DdsPixelFormat + { + public static readonly uint Dx10 = MakeFourCc('D', 'X', '1', '0'); + + public static readonly uint Dxt1 = MakeFourCc('D', 'X', 'T', '1'); + public static readonly uint Dxt2 = MakeFourCc('D', 'X', 'T', '2'); + public static readonly uint Dxt3 = MakeFourCc('D', 'X', 'T', '3'); + public static readonly uint Dxt4 = MakeFourCc('D', 'X', 'T', '4'); + public static readonly uint Dxt5 = MakeFourCc('D', 'X', 'T', '5'); + public static readonly uint Ati1 = MakeFourCc('A', 'T', 'I', '1'); + public static readonly uint Ati2 = MakeFourCc('A', 'T', 'I', '2'); + public static readonly uint Atc = MakeFourCc('A', 'T', 'C', ' '); + public static readonly uint Atci = MakeFourCc('A', 'T', 'C', 'I'); + public static readonly uint Atca = MakeFourCc('A', 'T', 'C', 'A'); + + public static readonly uint Bc4S = MakeFourCc('B', 'C', '4', 'S'); + public static readonly uint Bc4U = MakeFourCc('B', 'C', '4', 'U'); + public static readonly uint Bc5S = MakeFourCc('B', 'C', '5', 'S'); + public static readonly uint Bc5U = MakeFourCc('B', 'C', '5', 'U'); + + private static uint MakeFourCc(char c0, char c1, char c2, char c3) + { + uint result = c0; + result |= (uint)c1 << 8; + result |= (uint)c2 << 16; + result |= (uint)c3 << 24; + return result; + } + + public uint dwSize; + public PixelFormatFlags dwFlags; + public uint dwFourCc; + public uint dwRgbBitCount; + public uint dwRBitMask; + public uint dwGBitMask; + public uint dwBBitMask; + public uint dwABitMask; + + public DxgiFormat DxgiFormat + { + get + { + if (dwFlags.HasFlag(PixelFormatFlags.DdpfFourcc)) + { + if (dwFourCc == Dxt1) return DxgiFormat.DxgiFormatBc1Unorm; + if (dwFourCc == Dxt2 || dwFourCc == Dxt3) return DxgiFormat.DxgiFormatBc2Unorm; + if (dwFourCc == Dxt4 || dwFourCc == Dxt5) return DxgiFormat.DxgiFormatBc3Unorm; + if (dwFourCc == Ati1 || dwFourCc == Bc4S || dwFourCc == Bc4U) return DxgiFormat.DxgiFormatBc4Unorm; + if (dwFourCc == Ati2 || dwFourCc == Bc5S || dwFourCc == Bc5U) return DxgiFormat.DxgiFormatBc5Unorm; + if (dwFourCc == Atc) return DxgiFormat.DxgiFormatAtcExt; + if (dwFourCc == Atci) return DxgiFormat.DxgiFormatAtcExplicitAlphaExt; + if (dwFourCc == Atca) return DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt; + } + else + { + if (dwFlags.HasFlag(PixelFormatFlags.DdpfRgb)) // RGB/A + { + if (dwFlags.HasFlag(PixelFormatFlags.DdpfAlphaPixels)) //RGBA + { + if (dwRgbBitCount == 32) + { + if (dwRBitMask == 0xff && dwGBitMask == 0xff00 && dwBBitMask == 0xff0000 && + dwABitMask == 0xff000000) + { + return DxgiFormat.DxgiFormatR8G8B8A8Unorm; + } + + if (dwRBitMask == 0xff0000 && dwGBitMask == 0xff00 && dwBBitMask == 0xff && + dwABitMask == 0xff000000) + { + return DxgiFormat.DxgiFormatB8G8R8A8Unorm; + } + } + } + else //RGB + { + if (dwRgbBitCount == 32) + { + if (dwRBitMask == 0xff0000 && dwGBitMask == 0xff00 && dwBBitMask == 0xff) + { + return DxgiFormat.DxgiFormatB8G8R8X8Unorm; + } + } + } + } + else if (dwFlags.HasFlag(PixelFormatFlags.DdpfLuminance)) // R/RG + { + if (dwFlags.HasFlag(PixelFormatFlags.DdpfAlphaPixels)) // RG + { + if (dwRgbBitCount == 16) + { + if (dwRBitMask == 0xff && dwGBitMask == 0xff00) + { + return DxgiFormat.DxgiFormatR8G8Unorm; + } + } + } + else // Luminance only + { + if (dwRgbBitCount == 8) + { + if (dwRBitMask == 0xff) + { + return DxgiFormat.DxgiFormatR8Unorm; + } + } + } + } + } + return DxgiFormat.DxgiFormatUnknown; + } + } + + public bool IsDxt10Format => (dwFlags & PixelFormatFlags.DdpfFourcc) == PixelFormatFlags.DdpfFourcc + && dwFourCc == Dx10; + } + + public struct DdsHeaderDx10 + { + public DxgiFormat dxgiFormat; + public D3D10ResourceDimension resourceDimension; + public uint miscFlag; + public uint arraySize; + public uint miscFlags2; + } + + public class DdsFace + { + public uint Width { get; set; } + public uint Height { get; set; } + public uint SizeInBytes { get; } + public DdsMipMap[] MipMaps { get; } + + public DdsFace(uint width, uint height, uint sizeInBytes, int numMipMaps) + { + Width = width; + Height = height; + SizeInBytes = sizeInBytes; + MipMaps = new DdsMipMap[numMipMaps]; + } + } + + public class DdsMipMap + { + public uint Width { get; set; } + public uint Height { get; set; } + public uint SizeInBytes { get; } + public byte[] Data { get; } + public DdsMipMap(byte[] data, uint width, uint height) + { + Width = width; + Height = height; + SizeInBytes = (uint)data.Length; + Data = data; + } + } + + /// + /// Flags to indicate which members contain valid data. + /// + [Flags] + public enum HeaderFlags : uint + { + /// + /// Required in every .dds file. + /// + DdsdCaps = 0x1, + /// + /// Required in every .dds file. + /// + DdsdHeight = 0x2, + /// + /// Required in every .dds file. + /// + DdsdWidth = 0x4, + /// + /// Required when pitch is provided for an uncompressed texture. + /// + DdsdPitch = 0x8, + /// + /// Required in every .dds file. + /// + DdsdPixelformat = 0x1000, + /// + /// Required in a mipmapped texture. + /// + DdsdMipmapcount = 0x20000, + /// + /// Required when pitch is provided for a compressed texture. + /// + DdsdLinearsize = 0x80000, + /// + /// Required in a depth texture. + /// + DdsdDepth = 0x800000, + + Required = DdsdCaps | DdsdHeight | DdsdWidth | DdsdPixelformat + } + + /// + /// Specifies the complexity of the surfaces stored. + /// + [Flags] + public enum HeaderCaps : uint + { + /// + /// Optional; must be used on any file that contains more than one surface (a mipmap, a cubic environment map, or mipmapped volume texture). + /// + DdscapsComplex = 0x8, + /// + /// Optional; should be used for a mipmap. + /// + DdscapsMipmap = 0x400000, + /// + /// Required + /// + DdscapsTexture = 0x1000 + } + + /// + /// Additional detail about the surfaces stored. + /// + [Flags] + public enum HeaderCaps2 : uint + { + /// + /// Required for a cube map. + /// + Ddscaps2Cubemap = 0x200, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapPositivex = 0x400, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapNegativex = 0x800, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapPositivey = 0x1000, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapNegativey = 0x2000, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapPositivez = 0x4000, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapNegativez = 0x8000, + /// + /// Required for a volume texture. + /// + Ddscaps2Volume = 0x200000 + } + + [Flags] + public enum PixelFormatFlags : uint + { + /// + /// Texture contains alpha data; dwRGBAlphaBitMask contains valid data. + /// + DdpfAlphaPixels = 0x1, + /// + /// Used in some older DDS files for alpha channel only uncompressed data (dwRGBBitCount contains the alpha channel bitcount; dwABitMask contains valid data) + /// + DdpfAlpha = 0x2, + /// + /// Texture contains compressed RGB data; dwFourCC contains valid data. + /// + DdpfFourcc = 0x4, + /// + /// Texture contains uncompressed RGB data; dwRGBBitCount and the RGB masks (dwRBitMask, dwGBitMask, dwBBitMask) contain valid data. + /// + DdpfRgb = 0x40, + /// + /// Used in some older DDS files for YUV uncompressed data (dwRGBBitCount contains the YUV bit count; dwRBitMask contains the Y mask, dwGBitMask contains the U mask, dwBBitMask contains the V mask) + /// + DdpfYuv = 0x200, + /// + /// Used in some older DDS files for single channel color uncompressed data (dwRGBBitCount contains the luminance channel bit count; dwRBitMask contains the channel mask). Can be combined with DDPF_ALPHAPIXELS for a two channel DDS file. + /// + DdpfLuminance = 0x20000 + } + + public enum D3D10ResourceDimension : uint + { + D3D10ResourceDimensionUnknown, + D3D10ResourceDimensionBuffer, + D3D10ResourceDimensionTexture1D, + D3D10ResourceDimensionTexture2D, + D3D10ResourceDimensionTexture3D + }; + + public enum DxgiFormat : uint + { + DxgiFormatUnknown, + DxgiFormatR32G32B32A32Typeless, + DxgiFormatR32G32B32A32Float, + DxgiFormatR32G32B32A32Uint, + DxgiFormatR32G32B32A32Sint, + DxgiFormatR32G32B32Typeless, + DxgiFormatR32G32B32Float, + DxgiFormatR32G32B32Uint, + DxgiFormatR32G32B32Sint, + DxgiFormatR16G16B16A16Typeless, + DxgiFormatR16G16B16A16Float, + DxgiFormatR16G16B16A16Unorm, + DxgiFormatR16G16B16A16Uint, + DxgiFormatR16G16B16A16Snorm, + DxgiFormatR16G16B16A16Sint, + DxgiFormatR32G32Typeless, + DxgiFormatR32G32Float, + DxgiFormatR32G32Uint, + DxgiFormatR32G32Sint, + DxgiFormatR32G8X24Typeless, + DxgiFormatD32FloatS8X24Uint, + DxgiFormatR32FloatX8X24Typeless, + DxgiFormatX32TypelessG8X24Uint, + DxgiFormatR10G10B10A2Typeless, + DxgiFormatR10G10B10A2Unorm, + DxgiFormatR10G10B10A2Uint, + DxgiFormatR11G11B10Float, + DxgiFormatR8G8B8A8Typeless, + DxgiFormatR8G8B8A8Unorm, + DxgiFormatR8G8B8A8UnormSrgb, + DxgiFormatR8G8B8A8Uint, + DxgiFormatR8G8B8A8Snorm, + DxgiFormatR8G8B8A8Sint, + DxgiFormatR16G16Typeless, + DxgiFormatR16G16Float, + DxgiFormatR16G16Unorm, + DxgiFormatR16G16Uint, + DxgiFormatR16G16Snorm, + DxgiFormatR16G16Sint, + DxgiFormatR32Typeless, + DxgiFormatD32Float, + DxgiFormatR32Float, + DxgiFormatR32Uint, + DxgiFormatR32Sint, + DxgiFormatR24G8Typeless, + DxgiFormatD24UnormS8Uint, + DxgiFormatR24UnormX8Typeless, + DxgiFormatX24TypelessG8Uint, + DxgiFormatR8G8Typeless, + DxgiFormatR8G8Unorm, + DxgiFormatR8G8Uint, + DxgiFormatR8G8Snorm, + DxgiFormatR8G8Sint, + DxgiFormatR16Typeless, + DxgiFormatR16Float, + DxgiFormatD16Unorm, + DxgiFormatR16Unorm, + DxgiFormatR16Uint, + DxgiFormatR16Snorm, + DxgiFormatR16Sint, + DxgiFormatR8Typeless, + DxgiFormatR8Unorm, + DxgiFormatR8Uint, + DxgiFormatR8Snorm, + DxgiFormatR8Sint, + DxgiFormatA8Unorm, + DxgiFormatR1Unorm, + DxgiFormatR9G9B9E5Sharedexp, + DxgiFormatR8G8B8G8Unorm, + DxgiFormatG8R8G8B8Unorm, + DxgiFormatBc1Typeless, + DxgiFormatBc1Unorm, + DxgiFormatBc1UnormSrgb, + DxgiFormatBc2Typeless, + DxgiFormatBc2Unorm, + DxgiFormatBc2UnormSrgb, + DxgiFormatBc3Typeless, + DxgiFormatBc3Unorm, + DxgiFormatBc3UnormSrgb, + DxgiFormatBc4Typeless, + DxgiFormatBc4Unorm, + DxgiFormatBc4Snorm, + DxgiFormatBc5Typeless, + DxgiFormatBc5Unorm, + DxgiFormatBc5Snorm, + DxgiFormatB5G6R5Unorm, + DxgiFormatB5G5R5A1Unorm, + DxgiFormatB8G8R8A8Unorm, + DxgiFormatB8G8R8X8Unorm, + DxgiFormatR10G10B10XrBiasA2Unorm, + DxgiFormatB8G8R8A8Typeless, + DxgiFormatB8G8R8A8UnormSrgb, + DxgiFormatB8G8R8X8Typeless, + DxgiFormatB8G8R8X8UnormSrgb, + DxgiFormatBc6HTypeless, + DxgiFormatBc6HUf16, + DxgiFormatBc6HSf16, + DxgiFormatBc7Typeless, + DxgiFormatBc7Unorm, + DxgiFormatBc7UnormSrgb, + DxgiFormatAyuv, + DxgiFormatY410, + DxgiFormatY416, + DxgiFormatNv12, + DxgiFormatP010, + DxgiFormatP016, + DxgiFormat420Opaque, + DxgiFormatYuy2, + DxgiFormatY210, + DxgiFormatY216, + DxgiFormatNv11, + DxgiFormatAi44, + DxgiFormatIa44, + DxgiFormatP8, + DxgiFormatA8P8, + DxgiFormatB4G4R4A4Unorm, + DxgiFormatP208, + DxgiFormatV208, + DxgiFormatV408, + DxgiFormatForceUint, + + // Added here due to lack of an official value + DxgiFormatAtcExt = 300, + DxgiFormatAtcExplicitAlphaExt, + DxgiFormatAtcInterpolatedAlphaExt + }; + + public static class DxgiFormatExtensions + { + public static int GetByteSize(this DxgiFormat format) + { + switch (format) + { + case DxgiFormat.DxgiFormatUnknown: + return 4; + case DxgiFormat.DxgiFormatR32G32B32A32Typeless: + return 4 * 4; + case DxgiFormat.DxgiFormatR32G32B32A32Float: + return 4 * 4; + case DxgiFormat.DxgiFormatR32G32B32A32Uint: + return 4 * 4; + case DxgiFormat.DxgiFormatR32G32B32A32Sint: + return 4 * 4; + case DxgiFormat.DxgiFormatR32G32B32Typeless: + return 4 * 3; + case DxgiFormat.DxgiFormatR32G32B32Float: + return 4 * 3; + case DxgiFormat.DxgiFormatR32G32B32Uint: + return 4 * 3; + case DxgiFormat.DxgiFormatR32G32B32Sint: + return 4 * 3; + case DxgiFormat.DxgiFormatR16G16B16A16Typeless: + return 4 * 2; + case DxgiFormat.DxgiFormatR16G16B16A16Float: + return 4 * 2; + case DxgiFormat.DxgiFormatR16G16B16A16Unorm: + return 4 * 2; + case DxgiFormat.DxgiFormatR16G16B16A16Uint: + return 4 * 2; + case DxgiFormat.DxgiFormatR16G16B16A16Snorm: + return 4 * 2; + case DxgiFormat.DxgiFormatR16G16B16A16Sint: + return 4 * 2; + case DxgiFormat.DxgiFormatR32G32Typeless: + return 4 * 2; + case DxgiFormat.DxgiFormatR32G32Float: + return 4 * 2; + case DxgiFormat.DxgiFormatR32G32Uint: + return 4 * 2; + case DxgiFormat.DxgiFormatR32G32Sint: + return 4 * 2; + case DxgiFormat.DxgiFormatR32G8X24Typeless: + return 4 * 2; + case DxgiFormat.DxgiFormatD32FloatS8X24Uint: + return 4; + case DxgiFormat.DxgiFormatR32FloatX8X24Typeless: + return 4; + case DxgiFormat.DxgiFormatX32TypelessG8X24Uint: + return 4; + case DxgiFormat.DxgiFormatR10G10B10A2Typeless: + return 4; + case DxgiFormat.DxgiFormatR10G10B10A2Unorm: + return 4; + case DxgiFormat.DxgiFormatR10G10B10A2Uint: + return 4; + case DxgiFormat.DxgiFormatR11G11B10Float: + return 4; + case DxgiFormat.DxgiFormatR8G8B8A8Typeless: + return 4; + case DxgiFormat.DxgiFormatR8G8B8A8Unorm: + return 4; + case DxgiFormat.DxgiFormatR8G8B8A8UnormSrgb: + return 4; + case DxgiFormat.DxgiFormatR8G8B8A8Uint: + return 4; + case DxgiFormat.DxgiFormatR8G8B8A8Snorm: + return 4; + case DxgiFormat.DxgiFormatR8G8B8A8Sint: + return 4; + case DxgiFormat.DxgiFormatR16G16Typeless: + return 4; + case DxgiFormat.DxgiFormatR16G16Float: + return 4; + case DxgiFormat.DxgiFormatR16G16Unorm: + return 4; + case DxgiFormat.DxgiFormatR16G16Uint: + return 4; + case DxgiFormat.DxgiFormatR16G16Snorm: + return 4; + case DxgiFormat.DxgiFormatR16G16Sint: + return 4; + case DxgiFormat.DxgiFormatR32Typeless: + return 4; + case DxgiFormat.DxgiFormatD32Float: + return 4; + case DxgiFormat.DxgiFormatR32Float: + return 4; + case DxgiFormat.DxgiFormatR32Uint: + return 4; + case DxgiFormat.DxgiFormatR32Sint: + return 4; + case DxgiFormat.DxgiFormatR24G8Typeless: + return 4; + case DxgiFormat.DxgiFormatD24UnormS8Uint: + return 4; + case DxgiFormat.DxgiFormatR24UnormX8Typeless: + return 4; + case DxgiFormat.DxgiFormatX24TypelessG8Uint: + return 4; + case DxgiFormat.DxgiFormatR8G8Typeless: + return 2; + case DxgiFormat.DxgiFormatR8G8Unorm: + return 2; + case DxgiFormat.DxgiFormatR8G8Uint: + return 2; + case DxgiFormat.DxgiFormatR8G8Snorm: + return 2; + case DxgiFormat.DxgiFormatR8G8Sint: + return 2; + case DxgiFormat.DxgiFormatR16Typeless: + return 2; + case DxgiFormat.DxgiFormatR16Float: + return 2; + case DxgiFormat.DxgiFormatD16Unorm: + return 2; + case DxgiFormat.DxgiFormatR16Unorm: + return 2; + case DxgiFormat.DxgiFormatR16Uint: + return 2; + case DxgiFormat.DxgiFormatR16Snorm: + return 2; + case DxgiFormat.DxgiFormatR16Sint: + return 2; + case DxgiFormat.DxgiFormatR8Typeless: + return 1; + case DxgiFormat.DxgiFormatR8Unorm: + return 1; + case DxgiFormat.DxgiFormatR8Uint: + return 1; + case DxgiFormat.DxgiFormatR8Snorm: + return 1; + case DxgiFormat.DxgiFormatR8Sint: + return 1; + case DxgiFormat.DxgiFormatA8Unorm: + return 1; + case DxgiFormat.DxgiFormatR1Unorm: + return 1; + case DxgiFormat.DxgiFormatR9G9B9E5Sharedexp: + return 4; + case DxgiFormat.DxgiFormatR8G8B8G8Unorm: + return 4; + case DxgiFormat.DxgiFormatG8R8G8B8Unorm: + return 4; + case DxgiFormat.DxgiFormatBc1Typeless: + return 8; + case DxgiFormat.DxgiFormatBc1Unorm: + return 8; + case DxgiFormat.DxgiFormatBc1UnormSrgb: + return 8; + case DxgiFormat.DxgiFormatBc2Typeless: + return 16; + case DxgiFormat.DxgiFormatBc2Unorm: + return 16; + case DxgiFormat.DxgiFormatBc2UnormSrgb: + return 16; + case DxgiFormat.DxgiFormatBc3Typeless: + return 16; + case DxgiFormat.DxgiFormatBc3Unorm: + return 16; + case DxgiFormat.DxgiFormatBc3UnormSrgb: + return 16; + case DxgiFormat.DxgiFormatBc4Typeless: + return 8; + case DxgiFormat.DxgiFormatBc4Unorm: + return 8; + case DxgiFormat.DxgiFormatBc4Snorm: + return 8; + case DxgiFormat.DxgiFormatBc5Typeless: + return 16; + case DxgiFormat.DxgiFormatBc5Unorm: + return 16; + case DxgiFormat.DxgiFormatBc5Snorm: + return 16; + case DxgiFormat.DxgiFormatB5G6R5Unorm: + return 2; + case DxgiFormat.DxgiFormatB5G5R5A1Unorm: + return 2; + case DxgiFormat.DxgiFormatB8G8R8A8Unorm: + return 4; + case DxgiFormat.DxgiFormatB8G8R8X8Unorm: + return 4; + case DxgiFormat.DxgiFormatR10G10B10XrBiasA2Unorm: + return 4; + case DxgiFormat.DxgiFormatB8G8R8A8Typeless: + return 4; + case DxgiFormat.DxgiFormatB8G8R8A8UnormSrgb: + return 4; + case DxgiFormat.DxgiFormatB8G8R8X8Typeless: + return 4; + case DxgiFormat.DxgiFormatB8G8R8X8UnormSrgb: + return 4; + case DxgiFormat.DxgiFormatBc6HTypeless: + return 16; + case DxgiFormat.DxgiFormatBc6HUf16: + return 16; + case DxgiFormat.DxgiFormatBc6HSf16: + return 16; + case DxgiFormat.DxgiFormatBc7Typeless: + return 16; + case DxgiFormat.DxgiFormatBc7Unorm: + return 16; + case DxgiFormat.DxgiFormatBc7UnormSrgb: + return 16; + case DxgiFormat.DxgiFormatP8: + return 1; + case DxgiFormat.DxgiFormatA8P8: + return 2; + case DxgiFormat.DxgiFormatB4G4R4A4Unorm: + return 2; + case DxgiFormat.DxgiFormatAtcExt: + return 8; + case DxgiFormat.DxgiFormatAtcExplicitAlphaExt: + return 16; + case DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt: + return 16; + } + return 4; + } + + public static bool IsCompressedFormat(this DxgiFormat format) + { + switch (format) + { + case DxgiFormat.DxgiFormatBc1Typeless: + case DxgiFormat.DxgiFormatBc1Unorm: + case DxgiFormat.DxgiFormatBc1UnormSrgb: + case DxgiFormat.DxgiFormatBc2Typeless: + case DxgiFormat.DxgiFormatBc2Unorm: + case DxgiFormat.DxgiFormatBc2UnormSrgb: + case DxgiFormat.DxgiFormatBc3Typeless: + case DxgiFormat.DxgiFormatBc3Unorm: + case DxgiFormat.DxgiFormatBc3UnormSrgb: + case DxgiFormat.DxgiFormatBc4Typeless: + case DxgiFormat.DxgiFormatBc4Unorm: + case DxgiFormat.DxgiFormatBc4Snorm: + case DxgiFormat.DxgiFormatBc5Typeless: + case DxgiFormat.DxgiFormatBc5Unorm: + case DxgiFormat.DxgiFormatBc5Snorm: + case DxgiFormat.DxgiFormatBc6HTypeless: + case DxgiFormat.DxgiFormatBc6HUf16: + case DxgiFormat.DxgiFormatBc6HSf16: + case DxgiFormat.DxgiFormatBc7Typeless: + case DxgiFormat.DxgiFormatBc7Unorm: + case DxgiFormat.DxgiFormatBc7UnormSrgb: + case DxgiFormat.DxgiFormatAtcExt: + case DxgiFormat.DxgiFormatAtcExplicitAlphaExt: + case DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt: + return true; + + default: + return false; + } + } + } +} diff --git a/BCnEnc.Net/Shared/ImageFiles/ImageFile.cs b/BCnEnc.Net/Shared/ImageFiles/ImageFile.cs new file mode 100644 index 0000000..b3882d3 --- /dev/null +++ b/BCnEnc.Net/Shared/ImageFiles/ImageFile.cs @@ -0,0 +1,76 @@ +using System.IO; +using System.Linq; +using System.Text; + +namespace BCnEncoder.Shared.ImageFiles +{ + /// + /// The format identifier of an image file. + /// + public enum ImageFileFormat + { + /// + /// Represents the KTX image file format. + /// + Ktx, + + /// + /// Represents the DDS image file format. + /// + Dds, + + /// + /// Represents an unknown image file format. + /// + Unknown + } + + /// + /// Static helper class to determine the format of an image file. + /// + public static class ImageFile + { + private static readonly byte[] ktx1Identifier = { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; + + /// + /// Determines the image file format of the given stream. + /// + /// The stream of data to identify. + /// The format this image file may contain. + public static ImageFileFormat DetermineImageFormat(Stream stream) + { + if (IsDds(stream)) + { + return ImageFileFormat.Dds; + } + + if (IsKtx(stream)) + { + return ImageFileFormat.Ktx; + } + + return ImageFileFormat.Unknown; + } + + private static bool IsDds(Stream stream) + { + using var br = new BinaryReader(stream, Encoding.UTF8, true); + + var magic = br.ReadUInt32(); + stream.Position -= 4; + + return magic == 0x20534444U; + } + + private static bool IsKtx(Stream stream) + { + // Only checks for version 1 + using var br = new BinaryReader(stream, Encoding.ASCII, true); + + var identifier = br.ReadBytes(12); + stream.Position -= 12; + + return identifier.SequenceEqual(ktx1Identifier); + } + } +} diff --git a/BCnEnc.Net/Shared/KtxFile.cs b/BCnEnc.Net/Shared/ImageFiles/KtxFile.cs similarity index 66% rename from BCnEnc.Net/Shared/KtxFile.cs rename to BCnEnc.Net/Shared/ImageFiles/KtxFile.cs index 27ce05f..b090986 100644 --- a/BCnEnc.Net/Shared/KtxFile.cs +++ b/BCnEnc.Net/Shared/ImageFiles/KtxFile.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using static System.Text.Encoding; -namespace BCnEncoder.Shared +namespace BCnEncoder.Shared.ImageFiles { /// /// A representation of a ktx file. @@ -15,14 +15,14 @@ namespace BCnEncoder.Shared public class KtxFile { - public KtxHeader Header; + public KtxHeader header; public List KeyValuePairs { get; } = new List(); public List MipMaps { get; } = new List(); public KtxFile() { } public KtxFile(KtxHeader header) { - Header = header; + this.header = header; } /// @@ -35,44 +35,44 @@ public void Write(Stream s) throw new InvalidOperationException("The KTX structure should have at least 1 mipmap level and 1 Face before writing to file."); } - using (BinaryWriter bw = new BinaryWriter(s, UTF8, true)) + using (var bw = new BinaryWriter(s, UTF8, true)) { - uint bytesOfKeyValueData = (uint)KeyValuePairs.Sum(x => x.GetSizeWithPadding()); + var bytesOfKeyValueData = (uint)KeyValuePairs.Sum(x => x.GetSizeWithPadding()); - Header.BytesOfKeyValueData = bytesOfKeyValueData; - Header.NumberOfFaces = MipMaps[0].NumberOfFaces; - Header.NumberOfMipmapLevels = (uint)MipMaps.Count; - Header.NumberOfArrayElements = 0; + header.BytesOfKeyValueData = bytesOfKeyValueData; + header.NumberOfFaces = MipMaps[0].NumberOfFaces; + header.NumberOfMipmapLevels = (uint)MipMaps.Count; + header.NumberOfArrayElements = 0; - if (!Header.VerifyHeader()) + if (!header.VerifyHeader()) { throw new InvalidOperationException("Please verify the header validity before writing to file."); } - bw.WriteStruct(Header); + bw.WriteStruct(header); - foreach (KtxKeyValuePair keyValuePair in KeyValuePairs) + foreach (var keyValuePair in KeyValuePairs) { KtxKeyValuePair.WriteKeyValuePair(bw, keyValuePair); } - for (int mip = 0; mip < Header.NumberOfMipmapLevels; mip++) + for (var mip = 0; mip < header.NumberOfMipmapLevels; mip++) { - uint imageSize = MipMaps[mip].SizeInBytes; + var imageSize = MipMaps[mip].SizeInBytes; bw.Write(imageSize); - bool isCubemap = Header.NumberOfFaces == 6 && Header.NumberOfArrayElements == 0; - for (int f = 0; f < Header.NumberOfFaces; f++) + var isCubemap = header.NumberOfFaces == 6 && header.NumberOfArrayElements == 0; + for (var f = 0; f < header.NumberOfFaces; f++) { bw.Write(MipMaps[mip].Faces[f].Data); - uint cubePadding = 0u; + var cubePadding = 0u; if (isCubemap) { - cubePadding = 3 - ((imageSize + 3) % 4); + cubePadding = 3 - (imageSize + 3) % 4; } bw.AddPadding(cubePadding); } - uint mipPaddingBytes = 3 - ((imageSize + 3) % 4); + var mipPaddingBytes = 3 - (imageSize + 3) % 4; bw.AddPadding(mipPaddingBytes); } @@ -85,49 +85,49 @@ public void Write(Stream s) public static KtxFile Load(Stream s) { - using (BinaryReader br = new BinaryReader(s, UTF8, true)) + using (var br = new BinaryReader(s, UTF8, true)) { - KtxHeader header = br.ReadStruct(); + var header = br.ReadStruct(); if (header.NumberOfArrayElements > 0) { throw new NotSupportedException("KTX files with arrays are not supported."); } - KtxFile ktx = new KtxFile(header); + var ktx = new KtxFile(header); - int keyValuePairBytesRead = 0; + var keyValuePairBytesRead = 0; while (keyValuePairBytesRead < header.BytesOfKeyValueData) { - KtxKeyValuePair kvp = KtxKeyValuePair.ReadKeyValuePair(br, out int read); + var kvp = KtxKeyValuePair.ReadKeyValuePair(br, out var read); keyValuePairBytesRead += read; ktx.KeyValuePairs.Add(kvp); } - uint numberOfFaces = Math.Max(1, header.NumberOfFaces); + var numberOfFaces = Math.Max(1, header.NumberOfFaces); ktx.MipMaps.Capacity = (int)header.NumberOfMipmapLevels; for (uint mipLevel = 0; mipLevel < header.NumberOfMipmapLevels; mipLevel++) { - uint imageSize = br.ReadUInt32(); - uint mipWidth = header.PixelWidth / (uint)(Math.Pow(2, mipLevel)); - uint mipHeight = header.PixelHeight / (uint)(Math.Pow(2, mipLevel)); + var imageSize = br.ReadUInt32(); + var mipWidth = header.PixelWidth / (uint)Math.Pow(2, mipLevel); + var mipHeight = header.PixelHeight / (uint)Math.Pow(2, mipLevel); ktx.MipMaps.Add(new KtxMipmap(imageSize, mipWidth, mipHeight, numberOfFaces)); - bool cubemap = header.NumberOfFaces > 1 && header.NumberOfArrayElements == 0; + var cubemap = header.NumberOfFaces > 1 && header.NumberOfArrayElements == 0; for (uint face = 0; face < numberOfFaces; face++) { - byte[] faceData = br.ReadBytes((int)imageSize); + var faceData = br.ReadBytes((int)imageSize); ktx.MipMaps[(int)mipLevel].Faces[(int)face] = new KtxMipFace(faceData, mipWidth, mipHeight); if (cubemap) { - uint cubePadding = 0u; - cubePadding = 3 - ((imageSize + 3) % 4); + var cubePadding = 0u; + cubePadding = 3 - (imageSize + 3) % 4; br.SkipPadding(cubePadding); } } - uint mipPaddingBytes = 3 - ((imageSize + 3) % 4); + var mipPaddingBytes = 3 - (imageSize + 3) % 4; br.SkipPadding(mipPaddingBytes); } @@ -142,11 +142,11 @@ public ulong GetTotalSize() { ulong totalSize = 0; - for (int mipLevel = 0; mipLevel < Header.NumberOfMipmapLevels; mipLevel++) + for (var mipLevel = 0; mipLevel < header.NumberOfMipmapLevels; mipLevel++) { - for (int face = 0; face < Header.NumberOfFaces; face++) + for (var face = 0; face < header.NumberOfFaces; face++) { - KtxMipFace ktxface = MipMaps[mipLevel].Faces[face]; + var ktxface = MipMaps[mipLevel].Faces[face]; totalSize += ktxface.SizeInBytes; } } @@ -159,13 +159,13 @@ public ulong GetTotalSize() /// public byte[] GetAllTextureDataFaceMajor() { - byte[] result = new byte[GetTotalSize()]; + var result = new byte[GetTotalSize()]; uint start = 0; - for (int face = 0; face < Header.NumberOfFaces; face++) + for (var face = 0; face < header.NumberOfFaces; face++) { - for (int mipLevel = 0; mipLevel < Header.NumberOfMipmapLevels; mipLevel++) + for (var mipLevel = 0; mipLevel < header.NumberOfMipmapLevels; mipLevel++) { - KtxMipFace ktxMipFace = MipMaps[mipLevel].Faces[face]; + var ktxMipFace = MipMaps[mipLevel].Faces[face]; ktxMipFace.Data.CopyTo(result, (int)start); start += ktxMipFace.SizeInBytes; } @@ -179,13 +179,13 @@ public byte[] GetAllTextureDataFaceMajor() /// public byte[] GetAllTextureDataMipMajor() { - byte[] result = new byte[GetTotalSize()]; + var result = new byte[GetTotalSize()]; uint start = 0; - for (int mipLevel = 0; mipLevel < Header.NumberOfMipmapLevels; mipLevel++) + for (var mipLevel = 0; mipLevel < header.NumberOfMipmapLevels; mipLevel++) { - for (int face = 0; face < Header.NumberOfFaces; face++) + for (var face = 0; face < header.NumberOfFaces; face++) { - KtxMipFace ktxMipFace = MipMaps[mipLevel].Faces[face]; + var ktxMipFace = MipMaps[mipLevel].Faces[face]; ktxMipFace.Data.CopyTo(result, (int)start); start += ktxMipFace.SizeInBytes; } @@ -207,16 +207,16 @@ public KtxKeyValuePair(string key, byte[] value) public uint GetSizeWithPadding() { - int keySpanLength = UTF8.GetByteCount(Key); - uint totalSize = (uint)(keySpanLength + 1 + Value.Length); - int paddingBytes = (int)(3 - ((totalSize + 3) % 4)); + var keySpanLength = UTF8.GetByteCount(Key); + var totalSize = (uint)(keySpanLength + 1 + Value.Length); + var paddingBytes = (int)(3 - (totalSize + 3) % 4); return (uint)(totalSize + paddingBytes); } public static KtxKeyValuePair ReadKeyValuePair(BinaryReader br, out int bytesRead) { - uint totalSize = br.ReadUInt32(); + var totalSize = br.ReadUInt32(); Span keyValueBytes = stackalloc byte[(int)totalSize]; br.Read(keyValueBytes); @@ -236,15 +236,15 @@ public static KtxKeyValuePair ReadKeyValuePair(BinaryReader br, out int bytesRea } - int keySize = i; - string key = UTF8.GetString(keyValueBytes.Slice(0, keySize)); + var keySize = i; + var key = UTF8.GetString(keyValueBytes.Slice(0, keySize)); - int valueSize = (int)(totalSize - keySize - 1); - Span valueBytes = keyValueBytes.Slice(i + 1, valueSize); - byte[] value = new byte[valueSize]; + var valueSize = (int)(totalSize - keySize - 1); + var valueBytes = keyValueBytes.Slice(i + 1, valueSize); + var value = new byte[valueSize]; valueBytes.CopyTo(value); - int paddingBytes = (int)(3 - ((totalSize + 3) % 4)); + var paddingBytes = (int)(3 - (totalSize + 3) % 4); br.SkipPadding(paddingBytes); bytesRead = (int)(totalSize + paddingBytes + sizeof(uint)); @@ -253,12 +253,12 @@ public static KtxKeyValuePair ReadKeyValuePair(BinaryReader br, out int bytesRea public static uint WriteKeyValuePair(BinaryWriter bw, KtxKeyValuePair pair) { - int keySpanLength = UTF8.GetByteCount(pair.Key); + var keySpanLength = UTF8.GetByteCount(pair.Key); Span keySpan = stackalloc byte[keySpanLength]; Span valueSpan = pair.Value; - uint totalSize = (uint)(keySpan.Length + 1 + valueSpan.Length); - int paddingBytes = (int)(3 - ((totalSize + 3) % 4)); + var totalSize = (uint)(keySpan.Length + 1 + valueSpan.Length); + var paddingBytes = (int)(3 - (totalSize + 3) % 4); bw.Write(totalSize); bw.Write(keySpan); @@ -274,11 +274,11 @@ public unsafe struct KtxHeader { public fixed byte Identifier[12]; public uint Endianness; - public GLType GlType; + public GlType GlType; public uint GlTypeSize; - public GLFormat GlFormat; + public GlFormat GlFormat; public GlInternalFormat GlInternalFormat; - public GLFormat GlBaseInternalFormat; + public GlFormat GlBaseInternalFormat; public uint PixelWidth; public uint PixelHeight; @@ -291,18 +291,18 @@ public unsafe struct KtxHeader public bool VerifyHeader() { Span id = stackalloc byte[] { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; - for (int i = 0; i < id.Length; i++) + for (var i = 0; i < id.Length; i++) { if (Identifier[i] != id[i]) return false; } return true; } - public static KtxHeader InitializeCompressed(int width, int height, GlInternalFormat internalFormat, GLFormat baseInternalFormat) + public static KtxHeader InitializeCompressed(int width, int height, GlInternalFormat internalFormat, GlFormat baseInternalFormat) { - KtxHeader header = new KtxHeader(); + var header = new KtxHeader(); Span id = stackalloc byte[] { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; - for (int i = 0; i < id.Length; i++) + for (var i = 0; i < id.Length; i++) { header.Identifier[i] = id[i]; } @@ -318,11 +318,11 @@ public static KtxHeader InitializeCompressed(int width, int height, GlInternalFo return header; } - public static KtxHeader InitializeUncompressed(int width, int height, GLType type, GLFormat format, uint glTypeSize, GlInternalFormat internalFormat, GLFormat baseInternalFormat) + public static KtxHeader InitializeUncompressed(int width, int height, GlType type, GlFormat format, uint glTypeSize, GlInternalFormat internalFormat, GlFormat baseInternalFormat) { - KtxHeader header = new KtxHeader(); + var header = new KtxHeader(); Span id = stackalloc byte[] { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; - for (int i = 0; i < id.Length; i++) + for (var i = 0; i < id.Length; i++) { header.Identifier[i] = id[i]; } diff --git a/BCnEnc.Net/Shared/ImageQuality.cs b/BCnEnc.Net/Shared/ImageQuality.cs index 1def66f..b97fb55 100644 --- a/BCnEnc.Net/Shared/ImageQuality.cs +++ b/BCnEnc.Net/Shared/ImageQuality.cs @@ -1,23 +1,22 @@ -using System; -using SixLabors.ImageSharp.PixelFormats; +using System; namespace BCnEncoder.Shared { public class ImageQuality { - public static float PeakSignalToNoiseRatio(ReadOnlySpan original, ReadOnlySpan other, bool countAlpha = true) { + public static float PeakSignalToNoiseRatio(ReadOnlySpan original, ReadOnlySpan other, bool countAlpha = true) { if (original.Length != other.Length) { throw new ArgumentException("Both spans should be the same length"); } float error = 0; - for (int i = 0; i < original.Length; i++) { + for (var i = 0; i < original.Length; i++) { var o = new ColorYCbCr(original[i]); var c = new ColorYCbCr(other[i]); error += (o.y - c.y) * (o.y - c.y); error += (o.cb - c.cb) * (o.cb - c.cb); error += (o.cr - c.cr) * (o.cr - c.cr); if (countAlpha) { - error += ((original[i].A - other[i].A) / 255.0f) * ((original[i].A - other[i].A) / 255.0f); + error += (original[i].a - other[i].a) / 255.0f * ((original[i].a - other[i].a) / 255.0f); } } @@ -35,17 +34,41 @@ public static float PeakSignalToNoiseRatio(ReadOnlySpan original, ReadOn return 20 * MathF.Log10(1 / MathF.Sqrt(error)); } - public static float PeakSignalToNoiseRatioLuminance(ReadOnlySpan original, ReadOnlySpan other, bool countAlpha = true) { + public static float CalculateLogRMSE(ReadOnlySpan original, ReadOnlySpan other) + { + if (original.Length != other.Length) + { + throw new ArgumentException("Both spans should be the same length"); + } + float error = 0; + for (var i = 0; i < original.Length; i++) + { + //var dr = MathF.Log(other[i].r + 1.0f) - MathF.Log(original[i].r + 1.0f); + //var dg = MathF.Log(other[i].g + 1.0f) - MathF.Log(original[i].g + 1.0f); + //var db = MathF.Log(other[i].b + 1.0f) - MathF.Log(original[i].b + 1.0f); + var dr = Math.Sign(other[i].r) * MathF.Log(1 + MathF.Abs(other[i].r)) - Math.Sign(original[i].r) * MathF.Log(1 + MathF.Abs(original[i].r)); + var dg = Math.Sign(other[i].g) * MathF.Log(1 + MathF.Abs(other[i].g)) - Math.Sign(original[i].g) * MathF.Log(1 + MathF.Abs(original[i].g)); + var db = Math.Sign(other[i].b) * MathF.Log(1 + MathF.Abs(other[i].b)) - Math.Sign(original[i].b) * MathF.Log(1 + MathF.Abs(original[i].b)); + + error += dr * dr; + error += dg * dg; + error += db * db; + + } + return MathF.Sqrt(error / (3.0f * original.Length)); + } + + public static float PeakSignalToNoiseRatioLuminance(ReadOnlySpan original, ReadOnlySpan other, bool countAlpha = true) { if (original.Length != other.Length) { throw new ArgumentException("Both spans should be the same length"); } float error = 0; - for (int i = 0; i < original.Length; i++) { + for (var i = 0; i < original.Length; i++) { var o = new ColorYCbCr(original[i]); var c = new ColorYCbCr(other[i]); error += (o.y - c.y) * (o.y - c.y); if (countAlpha) { - error += ((original[i].A - other[i].A) / 255.0f) * ((original[i].A - other[i].A) / 255.0f); + error += (original[i].a - other[i].a) / 255.0f * ((original[i].a - other[i].a) / 255.0f); } } diff --git a/BCnEnc.Net/Shared/ImageToBlocks.cs b/BCnEnc.Net/Shared/ImageToBlocks.cs index 2285ebf..49acb21 100644 --- a/BCnEnc.Net/Shared/ImageToBlocks.cs +++ b/BCnEnc.Net/Shared/ImageToBlocks.cs @@ -1,44 +1,115 @@ -using System; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; +using CommunityToolkit.HighPerformance; namespace BCnEncoder.Shared { internal static class ImageToBlocks { + #region Blocks to colors - internal static RawBlock4X4Rgba32[] ImageTo4X4(ImageFrame image, out int blocksWidth, out int blocksHeight) + internal static ColorRgba32[] ColorsFromRawBlocks(RawBlock4X4Rgba32[,] blocks, int pixelWidth, int pixelHeight) { - blocksWidth = (int)MathF.Ceiling(image.Width / 4.0f); - blocksHeight = (int)MathF.Ceiling(image.Height / 4.0f); - RawBlock4X4Rgba32[] output = new RawBlock4X4Rgba32[blocksWidth * blocksHeight]; - Span pixels = image.GetPixelSpan(); + var output = new ColorRgba32[pixelWidth * pixelHeight]; - for (int y = 0; y < image.Height; y++) + for (var y = 0; y < pixelHeight; y++) { - for (int x = 0; x < image.Width; x++) + for (var x = 0; x < pixelWidth; x++) { - Rgba32 color = pixels[x + y * image.Width]; - int blockIndexX = (int)MathF.Floor(x / 4.0f); - int blockIndexY = (int)MathF.Floor(y / 4.0f); - int blockInternalIndexX = x % 4; - int blockInternalIndexY = y % 4; + var blockIndexX = x >> 2; + var blockIndexY = y >> 2; + var blockInternalIndexX = x & 3; + var blockInternalIndexY = y & 3; + + output[x + y * pixelWidth] = blocks[blockIndexX, blockIndexY][blockInternalIndexX, blockInternalIndexY]; + } + } + + return output; + } + + internal static ColorRgba32[] ColorsFromRawBlocks(RawBlock4X4Rgba32[] blocks, int pixelWidth, int pixelHeight) + { + var blocksWidth = ((pixelWidth + 3) & ~3) >> 2; + var output = new ColorRgba32[pixelWidth * pixelHeight]; + + for (var y = 0; y < pixelHeight; y++) + { + for (var x = 0; x < pixelWidth; x++) + { + var blockIndexX = x >> 2; + var blockIndexY = y >> 2; + var blockInternalIndexX = x & 3; + var blockInternalIndexY = y & 3; + + var blockIndex = blockIndexX + blockIndexY * blocksWidth; + + output[x + y * pixelWidth] = blocks[blockIndex][blockInternalIndexX, blockInternalIndexY]; + } + } + + return output; + } + + internal static ColorRgbFloat[] ColorsFromRawBlocks(RawBlock4X4RgbFloat[] blocks, int pixelWidth, int pixelHeight) + { + var blocksWidth = ((pixelWidth + 3) & ~3) >> 2; + var output = new ColorRgbFloat[pixelWidth * pixelHeight]; + + for (var y = 0; y < pixelHeight; y++) + { + for (var x = 0; x < pixelWidth; x++) + { + var blockIndexX = x >> 2; + var blockIndexY = y >> 2; + var blockInternalIndexX = x & 3; + var blockInternalIndexY = y & 3; + + var blockIndex = blockIndexX + blockIndexY * blocksWidth; + + output[x + y * pixelWidth] = blocks[blockIndex][blockInternalIndexX, blockInternalIndexY]; + } + } + + return output; + } + + #endregion + + #region Image to blocks + + internal static RawBlock4X4Rgba32[] ImageTo4X4(ReadOnlyMemory2D image, out int blocksWidth, out int blocksHeight) + { + blocksWidth = ((image.Width + 3) & ~3) >> 2; + blocksHeight = ((image.Height + 3) & ~3) >> 2; + + var output = new RawBlock4X4Rgba32[blocksWidth * blocksHeight]; + + var span = image.Span; + + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + var color = span[y, x]; + + var blockIndexX = x >> 2; + var blockIndexY = y >> 2; + var blockInternalIndexX = x & 3; + var blockInternalIndexY = y & 3; output[blockIndexX + blockIndexY * blocksWidth][blockInternalIndexX, blockInternalIndexY] = color; } } - //Fill in block y with edge color - if (image.Height % 4 != 0) + // Fill in block y with edge color + if ((image.Height & 3) != 0) { - int yPaddingStart = image.Height % 4; - for (int i = 0; i < blocksWidth; i++) + var yPaddingStart = image.Height & 3; + for (var i = 0; i < blocksWidth; i++) { var lastBlock = output[i + blocksWidth * (blocksHeight - 1)]; - for (int y = yPaddingStart; y < 4; y++) + for (var y = yPaddingStart; y < 4; y++) { - for (int x = 0; x < 4; x++) + for (var x = 0; x < 4; x++) { lastBlock[x, y] = lastBlock[x, y - 1]; } @@ -47,16 +118,16 @@ internal static RawBlock4X4Rgba32[] ImageTo4X4(ImageFrame image, out int } } - //Fill in block x with edge color - if (image.Width % 4 != 0) + // Fill in block x with edge color + if ((image.Width & 3) != 0) { - int xPaddingStart = image.Width % 4; - for (int i = 0; i < blocksHeight; i++) + var xPaddingStart = image.Width & 3; + for (var i = 0; i < blocksHeight; i++) { var lastBlock = output[blocksWidth - 1 + i * blocksWidth]; - for (int x = xPaddingStart; x < 4; x++) + for (var x = xPaddingStart; x < 4; x++) { - for (int y = 0; y < 4; y++) + for (var y = 0; y < 4; y++) { lastBlock[x, y] = lastBlock[x - 1, y]; } @@ -68,58 +139,82 @@ internal static RawBlock4X4Rgba32[] ImageTo4X4(ImageFrame image, out int return output; } - internal static Image ImageFromRawBlocks(RawBlock4X4Rgba32[,] blocks, int blocksWidth, int blocksHeight) - => ImageFromRawBlocks(blocks, blocksWidth, blocksHeight, blocksWidth * 4, blocksHeight * 4); + internal static RawBlock4X4RgbFloat[] ImageTo4X4(ReadOnlyMemory2D image, out int blocksWidth, out int blocksHeight) + { + blocksWidth = ((image.Width + 3) & ~3) >> 2; + blocksHeight = ((image.Height + 3) & ~3) >> 2; + var output = new RawBlock4X4RgbFloat[blocksWidth * blocksHeight]; - internal static Image ImageFromRawBlocks(RawBlock4X4Rgba32[,] blocks, int blocksWidth, int blocksHeight, int pixelWidth, int pixelHeight) - { - Image output = new Image(pixelWidth, pixelHeight); - Span pixels = output.GetPixelSpan(); + var span = image.Span; - for (int y = 0; y < output.Height; y++) + for (var y = 0; y < image.Height; y++) { - for (int x = 0; x < output.Width; x++) + for (var x = 0; x < image.Width; x++) { - int blockIndexX = (int)MathF.Floor(x / 4.0f); - int blockIndexY = (int)MathF.Floor(y / 4.0f); - int blockInternalIndexX = x % 4; - int blockInternalIndexY = y % 4; - - pixels[x + y * output.Width] = - blocks[blockIndexX, blockIndexY] - [blockInternalIndexX, blockInternalIndexY]; - } - } + var color = span[y, x]; - return output; - } - - internal static Image ImageFromRawBlocks(RawBlock4X4Rgba32[] blocks, int blocksWidth, int blocksHeight) - => ImageFromRawBlocks(blocks, blocksWidth, blocksHeight, blocksWidth * 4, blocksHeight * 4); + var blockIndexX = x >> 2; + var blockIndexY = y >> 2; + var blockInternalIndexX = x & 3; + var blockInternalIndexY = y & 3; + output[blockIndexX + blockIndexY * blocksWidth][blockInternalIndexX, blockInternalIndexY] = color; + } + } - internal static Image ImageFromRawBlocks(RawBlock4X4Rgba32[] blocks, int blocksWidth, int blocksHeight, int pixelWidth, int pixelHeight) - { - Image output = new Image(pixelWidth, pixelHeight); - Span pixels = output.GetPixelSpan(); + // Fill in block y with edge color + if ((image.Height & 3) != 0) + { + var yPaddingStart = image.Height & 3; + for (var i = 0; i < blocksWidth; i++) + { + var lastBlock = output[i + blocksWidth * (blocksHeight - 1)]; + for (var y = yPaddingStart; y < 4; y++) + { + for (var x = 0; x < 4; x++) + { + lastBlock[x, y] = lastBlock[x, y - 1]; + } + } + output[i + blocksWidth * (blocksHeight - 1)] = lastBlock; + } + } - for (int y = 0; y < output.Height; y++) + // Fill in block x with edge color + if ((image.Width & 3) != 0) { - for (int x = 0; x < output.Width; x++) + var xPaddingStart = image.Width & 3; + for (var i = 0; i < blocksHeight; i++) { - int blockIndexX = (int)MathF.Floor(x / 4.0f); - int blockIndexY = (int)MathF.Floor(y / 4.0f); - int blockInternalIndexX = x % 4; - int blockInternalIndexY = y % 4; - - pixels[x + y * output.Width] = - blocks[blockIndexX + blockIndexY * blocksWidth] - [blockInternalIndexX, blockInternalIndexY]; + var lastBlock = output[blocksWidth - 1 + i * blocksWidth]; + for (var x = xPaddingStart; x < 4; x++) + { + for (var y = 0; y < 4; y++) + { + lastBlock[x, y] = lastBlock[x - 1, y]; + } + } + output[blocksWidth - 1 + i * blocksWidth] = lastBlock; } } return output; } + + #endregion + + public static int CalculateNumOfBlocks(int pixelWidth, int pixelHeight) + { + var blocksWidth = ((pixelWidth + 3) & ~3) >> 2; + var blocksHeight = ((pixelHeight + 3) & ~3) >> 2; + + return blocksWidth * blocksHeight; + } + public static void CalculateNumOfBlocks(int pixelWidth, int pixelHeight, out int blocksWidth, out int blocksHeight) + { + blocksWidth = ((pixelWidth + 3) & ~3) >> 2; + blocksHeight = ((pixelHeight + 3) & ~3) >> 2; + } } } diff --git a/BCnEnc.Net/Shared/IntHelper.cs b/BCnEnc.Net/Shared/IntHelper.cs new file mode 100644 index 0000000..6c2f562 --- /dev/null +++ b/BCnEnc.Net/Shared/IntHelper.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BCnEncoder.Shared +{ + internal static class IntHelper + { + + public static int SignExtend(int orig, int precision) + { + var signMask = (1 << (precision - 1)); + var numberMask = signMask - 1; + + if ((orig & signMask) != 0) + { + return (~numberMask) | (orig & numberMask); + } + + return (orig & numberMask); + } + } +} diff --git a/BCnEnc.Net/Shared/InternalUtils.cs b/BCnEnc.Net/Shared/InternalUtils.cs new file mode 100644 index 0000000..01d6ba3 --- /dev/null +++ b/BCnEnc.Net/Shared/InternalUtils.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BCnEncoder.Shared +{ + internal static class InternalUtils + { + public static void Swap(ref T lhs, ref T rhs) + { + var temp = lhs; + lhs = rhs; + rhs = temp; + } + } +} diff --git a/BCnEnc.Net/Shared/Interpolation.cs b/BCnEnc.Net/Shared/Interpolation.cs new file mode 100644 index 0000000..8f09723 --- /dev/null +++ b/BCnEnc.Net/Shared/Interpolation.cs @@ -0,0 +1,103 @@ +namespace BCnEncoder.Shared +{ + internal static class Interpolation + { + /// + /// Interpolates two colors by half. + /// + /// The first color endpoint. + /// The second color endpoint. + /// The interpolated color. + public static ColorRgb24 InterpolateHalf(this ColorRgb24 c0, ColorRgb24 c1) => + InterpolateColor(c0, c1, 1, 2); + + /// + /// Interpolates two colors by third. + /// + /// The first color endpoint. + /// The second color endpoint. + /// The dividend in the third. + /// The interpolated color. + public static ColorRgb24 InterpolateThird(this ColorRgb24 c0, ColorRgb24 c1, int num) => + InterpolateColor(c0, c1, num, 3); + + /// + /// Interpolates two colors by fourth with ATC interpolation. + /// + /// The first color endpoint. + /// The second color endpoint. + /// The dividend in the fourth. + /// The interpolated color. + public static ColorRgb24 InterpolateFourthAtc(this ColorRgb24 c0, ColorRgb24 c1, int num) => + InterpolateColorAtc(c0, c1, num, 4); + + /// + /// Interpolates two colors by fifth. + /// + /// The first component. + /// The second component. + /// The dividend in the fifth. + /// The interpolated component. + public static byte InterpolateFifth(this byte a0, byte a1, int num) => + (byte)Interpolate(a0, a1, num, 5, 2); + + /// + /// Interpolates two colors by seventh. + /// + /// The first component. + /// The second component. + /// The dividend in the seventh. + /// The interpolated component. + public static byte InterpolateSeventh(this byte a0, byte a1, int num) => + (byte)Interpolate(a0, a1, num, 7, 3); + + /// + /// Interpolates two colors. + /// + /// The first color. + /// The second color. + /// The dividend on each color component. + /// The divisor on each color component. + /// The interpolated color. + private static ColorRgb24 InterpolateColor(ColorRgb24 c0, ColorRgb24 c1, int num, int den) => new ColorRgb24( + (byte)Interpolate(c0.r, c1.r, num, den), + (byte)Interpolate(c0.g, c1.g, num, den), + (byte)Interpolate(c0.b, c1.b, num, den)); + + /// + /// Interpolates two colors with the ATC interpolation. + /// + /// The first color. + /// The second color. + /// The dividend on each color component. + /// The divisor on each color component. + /// The interpolated color. + private static ColorRgb24 InterpolateColorAtc(ColorRgb24 c0, ColorRgb24 c1, int num, int den) => new ColorRgb24( + (byte)InterpolateAtc(c0.r, c1.r, num, den), + (byte)InterpolateAtc(c0.g, c1.g, num, den), + (byte)InterpolateAtc(c0.b, c1.b, num, den)); + + /// + /// Interpolates two components. + /// + /// The first component. + /// The second component. + /// The dividend. + /// The divisor. + /// A correction value for increased accuracy when working with integer interpolated values. + /// The interpolated component. + private static int Interpolate(int a, int b, int num, int den, int correction = 0) => + (int)(((den - num) * a + num * b + correction) / (float)den); + + /// + /// Interpolates two components with the ATC interpolation. + /// + /// The first component. + /// The second component. + /// The dividend. + /// The divisor. + /// The interpolated component. + private static int InterpolateAtc(int a, int b, int num, int den) => + (int)(a - num / (float)den * b); + } +} diff --git a/BCnEnc.Net/Shared/LinearClustering.cs b/BCnEnc.Net/Shared/LinearClustering.cs index 5df2b97..7e23f5b 100644 --- a/BCnEnc.Net/Shared/LinearClustering.cs +++ b/BCnEnc.Net/Shared/LinearClustering.cs @@ -1,13 +1,16 @@ -using System; +using System; using System.Collections.Generic; -using SixLabors.ImageSharp.PixelFormats; + +#if NETSTANDARD2_0 +using Array = BCnEncoder.Shared.ArrayPolyfills; +#endif namespace BCnEncoder.Shared { /// /// Simple Linear Iterative Clustering. /// - public static class LinearClustering + internal static class LinearClustering { private struct LabXy @@ -65,25 +68,25 @@ public ClusterCenter(LabXy labxy) public readonly float Distance(LabXy other, float m, float s) { - float dLab = MathF.Sqrt( + var dLab = MathF.Sqrt( MathF.Pow(l - other.l, 2) + MathF.Pow(a - other.a, 2) + MathF.Pow(b - other.b, 2)); - float dXy = MathF.Sqrt( + var dXy = MathF.Sqrt( MathF.Pow(x - other.x, 2) + MathF.Pow(y - other.y, 2)); - return dLab + (m / s) * dXy; + return dLab + m / s * dXy; } public readonly float Distance(ClusterCenter other, float m, float s) { - float dLab = MathF.Sqrt( + var dLab = MathF.Sqrt( (l - other.l) * (l - other.l) + (a - other.a) * (a - other.a) + (b - other.b) * (b - other.b)); - float dXy = MathF.Sqrt( + var dXy = MathF.Sqrt( (x - other.x) * (x - other.x) + (y - other.y) * (y - other.y)); return dLab + m / s * dXy; @@ -121,7 +124,90 @@ public readonly float Distance(ClusterCenter other, float m, float s) /// the more spatial proximity is emphasized and the more compact the cluster, /// M should be in range of 1 to 20. /// - public static int[] ClusterPixels(ReadOnlySpan pixels, int width, int height, + public static int[] ClusterPixels(ReadOnlySpan pixels, int width, int height, + int clusters, float m = 10, int maxIterations = 10, bool enforceConnectivity = true) + { + + var floats = new ColorRgbFloat[pixels.Length]; + for (var i = 0; i < pixels.Length; i++) + { + floats[i] = pixels[i].ToRgbFloat(); + } + + return ClusterPixels(floats, width, height, clusters, m, maxIterations, enforceConnectivity); + + //Grid interval S + var s = MathF.Sqrt(pixels.Length / (float)clusters); + var clusterIndices = new int[pixels.Length]; + + var labXys = ConvertToLabXy(pixels, width, height); + + + Span clusterCenters = InitialClusterCenters(width, height, clusters, s, labXys); + Span previousCenters = new ClusterCenter[clusters]; + + float error = 999; + const float threshold = 0.1f; + var iter = 0; + while (error > threshold) + { + if (maxIterations > 0 && iter >= maxIterations) + { + break; + } + iter++; + + clusterCenters.CopyTo(previousCenters); + + Array.Fill(clusterIndices, -1); + + // Find closest cluster for pixels + for (var j = 0; j < clusters; j++) + { + var xL = Math.Max(0, (int)(clusterCenters[j].x - s)); + var xH = Math.Min(width, (int)(clusterCenters[j].x + s)); + var yL = Math.Max(0, (int)(clusterCenters[j].y - s)); + var yH = Math.Min(height, (int)(clusterCenters[j].y + s)); + + for (var x = xL; x < xH; x++) + { + for (var y = yL; y < yH; y++) + { + var i = x + y * width; + + if (clusterIndices[i] == -1) + { + clusterIndices[i] = j; + } + else + { + var prevDistance = clusterCenters[clusterIndices[i]].Distance(labXys[i], m, s); + var distance = clusterCenters[j].Distance(labXys[i], m, s); + if (distance < prevDistance) + { + clusterIndices[i] = j; + } + } + } + } + } + + error = RecalculateCenters(clusters, m, labXys, clusterIndices, previousCenters, s, ref clusterCenters); + } + + if (enforceConnectivity) { + clusterIndices = EnforceConnectivity(clusterIndices, width, height, clusters); + } + + return clusterIndices; + } + + /// + /// The greater the value of M, + /// the more spatial proximity is emphasized and the more compact the cluster, + /// M should be in range of 1 to 20. + /// + public static int[] ClusterPixels(ReadOnlySpan pixels, int width, int height, int clusters, float m = 10, int maxIterations = 10, bool enforceConnectivity = true) { @@ -131,19 +217,19 @@ public static int[] ClusterPixels(ReadOnlySpan pixels, int width, int he } //Grid interval S - float S = MathF.Sqrt(pixels.Length / (float)clusters); - int[] clusterIndices = new int[pixels.Length]; + var s = MathF.Sqrt(pixels.Length / (float)clusters); + var clusterIndices = new int[pixels.Length]; var labXys = ConvertToLabXy(pixels, width, height); - Span clusterCenters = InitialClusterCenters(width, height, clusters, S, labXys); + Span clusterCenters = InitialClusterCenters(width, height, clusters, s, labXys); Span previousCenters = new ClusterCenter[clusters]; - float Error = 999; + float error = 999; const float threshold = 0.1f; - int iter = 0; - while (Error > threshold) + var iter = 0; + while (error > threshold) { if (maxIterations > 0 && iter >= maxIterations) { @@ -156,18 +242,18 @@ public static int[] ClusterPixels(ReadOnlySpan pixels, int width, int he Array.Fill(clusterIndices, -1); // Find closest cluster for pixels - for (int j = 0; j < clusters; j++) + for (var j = 0; j < clusters; j++) { - int xL = Math.Max(0, (int)(clusterCenters[j].x - S)); - int xH = Math.Min(width, (int)(clusterCenters[j].x + S)); - int yL = Math.Max(0, (int)(clusterCenters[j].y - S)); - int yH = Math.Min(height, (int)(clusterCenters[j].y + S)); + var xL = Math.Max(0, (int)(clusterCenters[j].x - s)); + var xH = Math.Min(width, (int)(clusterCenters[j].x + s)); + var yL = Math.Max(0, (int)(clusterCenters[j].y - s)); + var yH = Math.Min(height, (int)(clusterCenters[j].y + s)); - for (int x = xL; x < xH; x++) + for (var x = xL; x < xH; x++) { - for (int y = yL; y < yH; y++) + for (var y = yL; y < yH; y++) { - int i = x + y * width; + var i = x + y * width; if (clusterIndices[i] == -1) { @@ -175,8 +261,8 @@ public static int[] ClusterPixels(ReadOnlySpan pixels, int width, int he } else { - float prevDistance = clusterCenters[clusterIndices[i]].Distance(labXys[i], m, S); - float distance = clusterCenters[j].Distance(labXys[i], m, S); + var prevDistance = clusterCenters[clusterIndices[i]].Distance(labXys[i], m, s); + var distance = clusterCenters[j].Distance(labXys[i], m, s); if (distance < prevDistance) { clusterIndices[i] = j; @@ -186,31 +272,32 @@ public static int[] ClusterPixels(ReadOnlySpan pixels, int width, int he } } - Error = RecalculateCenters(clusters, m, labXys, clusterIndices, previousCenters, S, ref clusterCenters); + error = RecalculateCenters(clusters, m, labXys, clusterIndices, previousCenters, s, ref clusterCenters); } - if (enforceConnectivity) { + if (enforceConnectivity) + { clusterIndices = EnforceConnectivity(clusterIndices, width, height, clusters); } - + return clusterIndices; } private static float RecalculateCenters(int clusters, float m, LabXy[] labXys, int[] clusterIndices, - Span previousCenters, float S, ref Span clusterCenters) + Span previousCenters, float s, ref Span clusterCenters) { clusterCenters.Clear(); - for (int i = 0; i < labXys.Length; i++) + for (var i = 0; i < labXys.Length; i++) { - int clusterIndex = clusterIndices[i]; + var clusterIndex = clusterIndices[i]; // Sometimes a pixel is out of the range of any cluster, // in that case, find the nearest cluster and add it to it if (clusterIndex == -1) { - int bestCluster = 0; - float bestDistance = previousCenters[0].Distance(labXys[i], m, S); - for (int j = 1; j < clusters; j++) { - float dist = previousCenters[j].Distance(labXys[i], m, S); + var bestCluster = 0; + var bestDistance = previousCenters[0].Distance(labXys[i], m, s); + for (var j = 1; j < clusters; j++) { + var dist = previousCenters[j].Distance(labXys[i], m, s); if (dist < bestDistance) { bestDistance = dist; bestCluster = j; @@ -225,12 +312,12 @@ private static float RecalculateCenters(int clusters, float m, LabXy[] labXys, i } float error = 0; - for (int i = 0; i < clusters; i++) + for (var i = 0; i < clusters; i++) { if (clusterCenters[i].count > 0) { clusterCenters[i] /= clusterCenters[i].count; - error += clusterCenters[i].Distance(previousCenters[i], m, S); + error += clusterCenters[i].Distance(previousCenters[i], m, s); } } @@ -238,52 +325,52 @@ private static float RecalculateCenters(int clusters, float m, LabXy[] labXys, i return error; } - private static ClusterCenter[] InitialClusterCenters(int width, int height, int clusters, float S, LabXy[] labXys) + private static ClusterCenter[] InitialClusterCenters(int width, int height, int clusters, float s, LabXy[] labXys) { - ClusterCenter[] clusterCenters = new ClusterCenter[clusters]; + var clusterCenters = new ClusterCenter[clusters]; if (clusters == 2) { - int x0 = (int)MathF.Floor(width * 0.333f); - int y0 = (int)MathF.Floor(height * 0.333f); + var x0 = (int)MathF.Floor(width * 0.333f); + var y0 = (int)MathF.Floor(height * 0.333f); - int x1 = (int)MathF.Floor(width * 0.666f); - int y1 = (int)MathF.Floor(height * 0.666f); + var x1 = (int)MathF.Floor(width * 0.666f); + var y1 = (int)MathF.Floor(height * 0.666f); - int i0 = x0 + y0 * width; + var i0 = x0 + y0 * width; clusterCenters[0] = new ClusterCenter(labXys[i0]); - int i1 = x1 + y1 * width; + var i1 = x1 + y1 * width; clusterCenters[1] = new ClusterCenter(labXys[i1]); }else if(clusters == 3) { - int x0 = (int)MathF.Floor(width * 0.333f); - int y0 = (int)MathF.Floor(height * 0.333f); - int i0 = x0 + y0 * width; + var x0 = (int)MathF.Floor(width * 0.333f); + var y0 = (int)MathF.Floor(height * 0.333f); + var i0 = x0 + y0 * width; clusterCenters[0] = new ClusterCenter(labXys[i0]); - int x1 = (int)MathF.Floor(width * 0.666f); - int y1 = (int)MathF.Floor(height * 0.333f); - int i1 = x1 + y1 * width; + var x1 = (int)MathF.Floor(width * 0.666f); + var y1 = (int)MathF.Floor(height * 0.333f); + var i1 = x1 + y1 * width; clusterCenters[1] = new ClusterCenter(labXys[i1]); - int x2 = (int)MathF.Floor(width * 0.5f); - int y2 = (int)MathF.Floor(height * 0.666f); - int i2 = x2 + y2 * width; + var x2 = (int)MathF.Floor(width * 0.5f); + var y2 = (int)MathF.Floor(height * 0.666f); + var i2 = x2 + y2 * width; clusterCenters[2] = new ClusterCenter(labXys[i2]); } else { - int cIdx = 0; + var cIdx = 0; //Choose initial centers - for (float x = S / 2; x < width; x += S) + for (var x = s / 2; x < width; x += s) { - for (float y = S / 2; y < height; y += S) + for (var y = s / 2; y < height; y += s) { if (cIdx >= clusterCenters.Length) { break; } - int i = (int)x + (int)y * width; + var i = (int)x + (int)y * width; clusterCenters[cIdx] = new ClusterCenter(labXys[i]); cIdx++; } @@ -292,17 +379,41 @@ private static ClusterCenter[] InitialClusterCenters(int width, int height, int return clusterCenters; } - private static LabXy[] ConvertToLabXy(ReadOnlySpan pixels, int width, int height) + private static LabXy[] ConvertToLabXy(ReadOnlySpan pixels, int width, int height) { - LabXy[] labXys = new LabXy[pixels.Length]; + var labXys = new LabXy[pixels.Length]; //Convert pixels to LabXy - for (int x = 0; x < width; x++) + for (var x = 0; x < width; x++) { - for (int y = 0; y < height; y++) + for (var y = 0; y < height; y++) { - int i = x + y * width; + var i = x + y * width; var lab = new ColorLab(pixels[i]); - labXys[i] = new LabXy() + labXys[i] = new LabXy + { + l = lab.l, + a = lab.a, + b = lab.b, + x = x, + y = y + }; + } + } + + return labXys; + } + + private static LabXy[] ConvertToLabXy(ReadOnlySpan pixels, int width, int height) + { + var labXys = new LabXy[pixels.Length]; + //Convert pixels to LabXy + for (var x = 0; x < width; x++) + { + for (var y = 0; y < height; y++) + { + var i = x + y * width; + var lab = new ColorLab(pixels[i]); + labXys[i] = new LabXy { l = lab.l, a = lab.a, @@ -320,24 +431,24 @@ private static int[] EnforceConnectivity(int[] oldLabels, int width, int height, { ReadOnlySpan neighborX = new[] { -1, 0, 1, 0 }; ReadOnlySpan neighborY = new[] { 0, -1, 0, 1 }; - - int sSquared = (width * height) / clusters; - List clusterX = new List(sSquared); - List clusterY = new List(sSquared); + var sSquared = width * height / clusters; + + var clusterX = new List(sSquared); + var clusterY = new List(sSquared); - int adjacentLabel = 0; - int[] newLabels = new int[oldLabels.Length]; - bool[] usedLabels = new bool[clusters]; + var adjacentLabel = 0; + var newLabels = new int[oldLabels.Length]; + var usedLabels = new bool[clusters]; Array.Fill(newLabels, -1); - for (int y = 0; y < height; ++y) + for (var y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) + for (var x = 0; x < width; ++x) { - int xyIndex = x + y * width; + var xyIndex = x + y * width; if (newLabels[xyIndex] < 0) { - int label = oldLabels[xyIndex]; + var label = oldLabels[xyIndex]; newLabels[xyIndex] = label; //New cluster @@ -345,11 +456,11 @@ private static int[] EnforceConnectivity(int[] oldLabels, int width, int height, clusterY.Add(y); //Search neighbors for already completed clusters - for (int i = 0; i < neighborX.Length; ++i) + for (var i = 0; i < neighborX.Length; ++i) { - int nX = x + neighborX[i]; - int nY = y + neighborY[i]; - int nI = nX + nY * width; + var nX = x + neighborX[i]; + var nY = y + neighborY[i]; + var nI = nX + nY * width; if (nX < width && nX >= 0 && nY < height && nY >= 0) { if (newLabels[nI] >= 0) @@ -361,13 +472,13 @@ private static int[] EnforceConnectivity(int[] oldLabels, int width, int height, } //Count pixels in this cluster - for (int c = 0; c < clusterX.Count; ++c) + for (var c = 0; c < clusterX.Count; ++c) { - for (int i = 0; i < neighborX.Length; ++i) + for (var i = 0; i < neighborX.Length; ++i) { - int nX = clusterX[c] + neighborX[i]; - int nY = clusterY[c] + neighborY[i]; - int nI = nX + nY * width; + var nX = clusterX[c] + neighborX[i]; + var nY = clusterY[c] + neighborY[i]; + var nI = nX + nY * width; if (nX < width && nX >= 0 && nY < height && nY >= 0) { if (newLabels[nI] == -1 && label == oldLabels[nI]) @@ -382,11 +493,11 @@ private static int[] EnforceConnectivity(int[] oldLabels, int width, int height, // If this is unusually small cluster or this label is already used, // merge with adjacent cluster - if (clusterX.Count < (sSquared / 4) || usedLabels[label]) + if (clusterX.Count < sSquared / 4 || usedLabels[label]) { - for (int i = 0; i < clusterX.Count; ++i) + for (var i = 0; i < clusterX.Count; ++i) { - newLabels[(clusterY[i] * width + clusterX[i])] = adjacentLabel; + newLabels[clusterY[i] * width + clusterX[i]] = adjacentLabel; } } else { diff --git a/BCnEnc.Net/Shared/MathHelper.cs b/BCnEnc.Net/Shared/MathHelper.cs new file mode 100644 index 0000000..d5a47d9 --- /dev/null +++ b/BCnEnc.Net/Shared/MathHelper.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BCnEncoder.Shared +{ + public static unsafe class MathHelper + { + private static double two54 = 1.80143985094819840000e+16; /* 0x43500000, 0x00000000 */ + + // FrExp modified to C# from http://www.netlib.org + /* @(#)fdlibm.h 1.5 04/04/22 */ + /* + * ==================================================== + * Copyright (C) 2004 by Sun Microsystems, Inc. All rights reserved. + * + * Permission to use, copy, modify, and distribute this + * software is freely granted, provided that this notice + * is preserved. + * ==================================================== + */ + /// + /// Breaks down the floating-point value x into a component m for the normalized fraction component and another term n for the exponent, such that the absolute value of m is greater than or equal to 0.5 and less than 1.0 or equal to 0, and x = m * 2n. The function stores the integer exponent n at the location to which expptr points. + /// + /// + /// + /// Returns the normalized fraction m. If x is 0, the function returns 0 for both the fraction and exponent. The fraction has the same sign as the argument x. The result of the function cannot have a range error. + public static double FrExp(double x, out int eptr) + { + unchecked + { + int hx, ix, lx; + hx = *(1 + (int*) &x); + ix = 0x7fffffff & hx; + lx = *(int*) &x; + eptr = 0; + if (ix >= 0x7ff00000 || ((ix | lx) == 0)) return x; /* 0,inf,nan */ + if (ix < 0x00100000) + { + /* subnormal */ + x *= two54; + hx = *(1 + (int*) &x); + ix = hx & 0x7fffffff; + eptr = -54; + } + + eptr += (ix >> 20) - 1022; + hx = (hx & (int) 0x800fffff) | (int) 0x3fe00000; + *(1 + (int*) &x) = hx; + return x; + } + } + + /// + /// Multiplies a floating point value arg by the number 2 raised to the exp power. + /// + /// + /// + /// + public static float LdExp(float arg, int exp) + { + return arg * MathF.Pow(2, exp); + } + } +} diff --git a/BCnEnc.Net/Shared/MipMapper.cs b/BCnEnc.Net/Shared/MipMapper.cs index 23329e2..ddbcea7 100644 --- a/BCnEnc.Net/Shared/MipMapper.cs +++ b/BCnEnc.Net/Shared/MipMapper.cs @@ -1,53 +1,122 @@ -using System; -using System.Collections.Generic; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; +using System; +using CommunityToolkit.HighPerformance; namespace BCnEncoder.Shared { - public static class MipMapper + internal static class MipMapper { + /// + /// Generate a chain of elements. + /// + /// The original image to scale down. + /// The original image width. + /// The original image height. + /// The number of mipmaps to generate. + /// Will generate as many mipmaps as possible until a mipmap of 1x1 is reached for 0 or smaller. + public static ReadOnlyMemory2D[] GenerateMipChain(ReadOnlyMemory input, int width, int height, ref int numMipMaps) + { + return GenerateMipChain(input.AsMemory2D(height, width), ref numMipMaps); + } - public static uint CalculateMipChainLength(int width, int height, uint maxNumMipMaps) { - if (maxNumMipMaps == 1) { - return 1; + /// + /// Generate a chain of elements. + /// + /// The original image to scale down. + /// The number of mipmaps to generate. + /// Will generate as many mipmaps as possible until a mipmap of 1x1 is reached for 0 or smaller. + public static ReadOnlyMemory2D[] GenerateMipChain(ReadOnlyMemory2D pixels, ref int numMipMaps) + { + var width = pixels.Width; + var height = pixels.Height; + var mipChainLength = CalculateMipChainLength(width, height, numMipMaps); + + var result = new ReadOnlyMemory2D[mipChainLength]; + result[0] = pixels; + + // If only one mipmap was requested, return original image only + if (numMipMaps == 1) + { + return result; } - if (maxNumMipMaps == 0) { - maxNumMipMaps = 999; + + // If number of mipmaps is "marked as boundless", do as many mipmaps as it takes to reach a size of 1x1 + if (numMipMaps <= 0) + { + numMipMaps = int.MaxValue; } - uint output = 0; - for (uint mipLevel = 1; mipLevel < maxNumMipMaps; mipLevel++) { - int mipWidth = Math.Max(1, width / (int)(Math.Pow(2, mipLevel))); - int mipHeight = Math.Max(1, height / (int)(Math.Pow(2, mipLevel))); - if (mipWidth == 1 && mipHeight == 1) { - output = mipLevel + 1; + + // Generate mipmaps + for (var mipLevel = 1; mipLevel < numMipMaps; mipLevel++) + { + var mipWidth = Math.Max(1, width >> mipLevel); + var mipHeight = Math.Max(1, height >> mipLevel); + + var newMip = ResizeToHalf(result[mipLevel - 1].Span); + result[mipLevel] = newMip; + + // Stop generating if last generated mipmap was of size 1x1 + if (mipWidth == 1 && mipHeight == 1) + { + numMipMaps = mipLevel + 1; break; } } - return output; + + return result; } - public static List> GenerateMipChain(Image sourceImage, ref uint numMipMaps) { - List> result = new List>(); - result.Add(sourceImage.Clone()); + /// + /// Generate a chain of elements. + /// + /// The original image to scale down. + /// The original image width. + /// The original image height. + /// The number of mipmaps to generate. + /// Will generate as many mipmaps as possible until a mipmap of 1x1 is reached for 0 or smaller. + public static ReadOnlyMemory2D[] GenerateMipChain(ReadOnlyMemory input, int width, int height, ref int numMipMaps) + { + return GenerateMipChain(input.AsMemory2D(height, width), ref numMipMaps); + } + + /// + /// Generate a chain of elements. + /// + /// The original image to scale down. + /// The number of mipmaps to generate. + /// Will generate as many mipmaps as possible until a mipmap of 1x1 is reached for 0 or smaller. + public static ReadOnlyMemory2D[] GenerateMipChain(ReadOnlyMemory2D pixels, ref int numMipMaps) + { + var width = pixels.Width; + var height = pixels.Height; + var mipChainLength = CalculateMipChainLength(width, height, numMipMaps); - if (numMipMaps == 1) { + var result = new ReadOnlyMemory2D[mipChainLength]; + result[0] = pixels; + + // If only one mipmap was requested, return original image only + if (numMipMaps == 1) + { return result; } - if (numMipMaps == 0) { - numMipMaps = 999; + // If number of mipmaps is "marked as boundless", do as many mipmaps as it takes to reach a size of 1x1 + if (numMipMaps <= 0) + { + numMipMaps = int.MaxValue; } - for (uint mipLevel = 1; mipLevel < numMipMaps; mipLevel++) { - int mipWidth = Math.Max(1, sourceImage.Width / (int)(Math.Pow(2, mipLevel))); - int mipHeight = Math.Max(1, sourceImage.Height / (int)(Math.Pow(2, mipLevel))); + // Generate mipmaps + for (var mipLevel = 1; mipLevel < numMipMaps; mipLevel++) + { + var mipWidth = Math.Max(1, width >> mipLevel); + var mipHeight = Math.Max(1, height >> mipLevel); - var newImage = sourceImage.Clone(x => x.Resize(mipWidth, mipHeight)); - result.Add(newImage); + var newMip = ResizeToHalf(result[mipLevel - 1].Span); + result[mipLevel] = newMip; - if (mipWidth == 1 && mipHeight == 1) { + // Stop generating if last generated mipmap was of size 1x1 + if (mipWidth == 1 && mipHeight == 1) + { numMipMaps = mipLevel + 1; break; } @@ -55,5 +124,100 @@ public static List> GenerateMipChain(Image sourceImage, re return result; } + + public static int CalculateMipChainLength(int width, int height, int maxNumMipMaps) + { + if (maxNumMipMaps == 1) + { + return 1; + } + + if (maxNumMipMaps <= 0) + { + maxNumMipMaps = int.MaxValue; + } + + var output = 0; + for (var mipLevel = 1; mipLevel <= maxNumMipMaps; mipLevel++) + { + var mipWidth = Math.Max(1, width >> mipLevel); + var mipHeight = Math.Max(1, height >> mipLevel); + + if (mipLevel == maxNumMipMaps) + { + return maxNumMipMaps; + } + + if (mipWidth == 1 && mipHeight == 1) + { + output = mipLevel + 1; + break; + } + } + + return output; + } + + public static void CalculateMipLevelSize(int width, int height, int mipIdx, out int mipWidth, out int mipHeight) + { + mipWidth = Math.Max(1, width >> mipIdx); + mipHeight = Math.Max(1, height >> mipIdx); + } + + 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]; + + int ClampW(int x) => Math.Max(0, Math.Min(oldWidth - 1, x)); + int ClampH(int y) => Math.Max(0, Math.Min(oldHeight - 1, y)); + + for (var y2 = 0; y2 < newHeight; y2++) + { + for (var x2 = 0; x2 < newWidth; x2++) + { + var ul = pixelsRgba[ClampH(y2 * 2), ClampW(x2 * 2)].ToFloat(); + var ur = pixelsRgba[ClampH(y2 * 2), ClampW(x2 * 2 + 1)].ToFloat(); + 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 * newWidth + x2] = ((ul + ur + ll + lr) / 4).ToRgba32(); + } + } + + return ((Memory)result).AsMemory2D(newHeight, newWidth); + } + + 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]; + + int ClampW(int x) => Math.Max(0, Math.Min(oldWidth - 1, x)); + int ClampH(int y) => Math.Max(0, Math.Min(oldHeight - 1, y)); + + for (var y2 = 0; y2 < newHeight; y2++) + { + for (var x2 = 0; x2 < newWidth; x2++) + { + var ul = pixelsRgba[ClampH(y2 * 2), ClampW(x2 * 2)]; + var ur = pixelsRgba[ClampH(y2 * 2), ClampW(x2 * 2 + 1)]; + var ll = pixelsRgba[ClampH(y2 * 2 + 1), ClampW(x2 * 2)]; + var lr = pixelsRgba[ClampH(y2 * 2 + 1), ClampW(x2 * 2 + 1)]; + + result[y2 * newWidth + x2] = ((ul + ur + ll + lr) / 4); + } + } + + return ((Memory)result).AsMemory2D(newHeight, newWidth); + } } } diff --git a/BCnEnc.Net/Shared/NetStdPolyfills.cs b/BCnEnc.Net/Shared/NetStdPolyfills.cs new file mode 100644 index 0000000..3936f96 --- /dev/null +++ b/BCnEnc.Net/Shared/NetStdPolyfills.cs @@ -0,0 +1,155 @@ + +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using CommunityToolkit.HighPerformance; + + +namespace BCnEncoder.Shared +{ +#if NETSTANDARD2_0 + internal static class MemoryPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory2D AsMemory2D(this Memory memory, int height, int width) + { + if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment)) + { + T[] array = segment.Array; + ref T value = ref array.DangerousGetReference(); + return Memory2D.DangerousCreate(array, ref value, height, width, 0); + } + else + { + throw new NotSupportedException(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory2D AsMemory2D(this ReadOnlyMemory memory, int height, int width) + { + if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment)) + { + T[] array = segment.Array; + ref T value = ref array.DangerousGetReference(); + return ReadOnlyMemory2D.DangerousCreate(array, ref value, height, width, 0); + } + else + { + throw new NotSupportedException(); + } + } + } + + internal static class SpanPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe ReadOnlySpan2D AsSpan2D(this ReadOnlySpan span, int height, int width) + { + ref T value = ref span.DangerousGetReference(); + void* pointer = Unsafe.AsPointer(ref value); + return new ReadOnlySpan2D(pointer, height, width, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span GetRowSpan(this Span2D span, int row) + { + ref T value = ref span.DangerousGetReferenceAt(row, 0); + void* pointer = Unsafe.AsPointer(ref value); + return new Span(pointer, span.Width); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe ReadOnlySpan GetRowSpan(this ReadOnlySpan2D span, int row) + { + ref T value = ref span.DangerousGetReferenceAt(row, 0); + void* pointer = Unsafe.AsPointer(ref value); + return new Span(pointer, span.Width); + } + } + + internal static class BinaryWriterPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Write(this BinaryWriter writer, ReadOnlySpan buffer) + { + writer.BaseStream.Write(buffer); + } + } + + internal static class BinaryReaderPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Read(this BinaryReader reader, Span buffer) + { + return reader.BaseStream.Read(buffer); + } + } + + internal static class EncodingPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe string GetString(this Encoding encoding, ReadOnlySpan buffer) + { + ref byte value = ref buffer.DangerousGetReference(); + byte* pointer = (byte*)Unsafe.AsPointer(ref value); + return encoding.GetString(pointer, buffer.Length); + } + } + + internal static class MemoryMarshalPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span CreateSpan(ref T reference, int length) + { + return new Span(Unsafe.AsPointer(ref reference), length); + } + } + + internal static class ArrayPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Fill(T[] array, T value) + { + array.AsSpan().Fill(value); + } + } + + internal static class MathPolyfills + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Clamp(float value, float min, float max) + { + if (min > max) + { + throw new ArgumentException(); + } + + if (value < min) + { + return min; + } + else if (value > max) + { + return max; + } + + return value; + } + } +#endif + + internal static class MathCbrt + { + public static float Cbrt(float f) + { + #if NETSTANDARD2_0 + return MathF.Pow(f, 1 / 3.0f); + #else + return MathF.Cbrt(f); + #endif + } + } +} diff --git a/BCnEnc.Net/Shared/OperationContext.cs b/BCnEnc.Net/Shared/OperationContext.cs new file mode 100644 index 0000000..bef999e --- /dev/null +++ b/BCnEnc.Net/Shared/OperationContext.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; + +namespace BCnEncoder.Shared +{ + /// + /// The operation context. + /// + public class OperationContext + { + /// + /// Whether the blocks should be decoded in parallel. + /// + public bool IsParallel { get; set; } + + /// + /// Determines how many tasks should be used for parallel processing. + /// + public int TaskCount { get; set; } = Environment.ProcessorCount; + + /// + /// The cancellation token to check if the asynchronous operation was cancelled. + /// + public CancellationToken CancellationToken { get; set; } + + /// + /// The progress context for the operation. + /// + public OperationProgress Progress { get; set; } + } +} diff --git a/BCnEnc.Net/Shared/OperationProgress.cs b/BCnEnc.Net/Shared/OperationProgress.cs new file mode 100644 index 0000000..7d9e87e --- /dev/null +++ b/BCnEnc.Net/Shared/OperationProgress.cs @@ -0,0 +1,27 @@ +using System; + +namespace BCnEncoder.Shared +{ + public class OperationProgress + { + private readonly IProgress progress; + private readonly int totalBlocks; + private int processedBlocks; + + public OperationProgress(IProgress progress, int totalBlocks) + { + this.progress = progress; + this.totalBlocks = totalBlocks; + } + + public void SetProcessedBlocks(int processedBlocks) + { + this.processedBlocks = processedBlocks; + } + + public void Report(int currentBlock) + { + progress?.Report(new ProgressElement(processedBlocks + currentBlock, totalBlocks)); + } + } +} diff --git a/BCnEnc.Net/Shared/OutputFileFormat.cs b/BCnEnc.Net/Shared/OutputFileFormat.cs index 157974f..cc63b8d 100644 --- a/BCnEnc.Net/Shared/OutputFileFormat.cs +++ b/BCnEnc.Net/Shared/OutputFileFormat.cs @@ -3,11 +3,11 @@ public enum OutputFileFormat { /// - /// Khronos texture format https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/ + /// Khronos texture Format https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/ /// Ktx, /// - /// Direct draw surface format https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds + /// Direct draw surface Format https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds /// Dds } diff --git a/BCnEnc.Net/Shared/PcaVectors.cs b/BCnEnc.Net/Shared/PcaVectors.cs index 999e5c8..63c2685 100644 --- a/BCnEnc.Net/Shared/PcaVectors.cs +++ b/BCnEnc.Net/Shared/PcaVectors.cs @@ -1,24 +1,32 @@ -using System; -using SixLabors.ImageSharp.PixelFormats; -using Vector3 = System.Numerics.Vector3; -using Vector4 = System.Numerics.Vector4; -using Matrix4x4 = System.Numerics.Matrix4x4; +using System; +using System.Numerics; namespace BCnEncoder.Shared { internal static class PcaVectors { - private const int c565_5_mask = 0xF8; - private const int c565_6_mask = 0xFC; + private const int C565_5Mask = 0xF8; + private const int C565_6Mask = 0xFC; - private static void ConvertToVector4(ReadOnlySpan colors, Span vectors) + private static void ConvertToVector4(ReadOnlySpan colors, Span vectors) { - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { - vectors[i].X += colors[i].R / 255f; - vectors[i].Y += colors[i].G / 255f; - vectors[i].Z += colors[i].B / 255f; - vectors[i].W += colors[i].A / 255f; + vectors[i].X += colors[i].r / 255f; + vectors[i].Y += colors[i].g / 255f; + vectors[i].Z += colors[i].b / 255f; + vectors[i].W += colors[i].a / 255f; + } + } + + private static void ConvertToVector4(ReadOnlySpan colors, Span vectors) + { + for (var i = 0; i < colors.Length; i++) + { + vectors[i].X += colors[i].r; + vectors[i].Y += colors[i].g; + vectors[i].Z += colors[i].b; + vectors[i].W = 0; } } @@ -30,7 +38,7 @@ private static Vector4 CalculateMean(Span colors) float b = 0; float a = 0; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { r += colors[i].X; g += colors[i].Y; @@ -48,15 +56,15 @@ private static Vector4 CalculateMean(Span colors) internal static Matrix4x4 CalculateCovariance(Span values, out Vector4 mean) { mean = CalculateMean(values); - for (int i = 0; i < values.Length; i++) + for (var i = 0; i < values.Length; i++) { values[i] -= mean; } //4x4 matrix - Matrix4x4 mat = new Matrix4x4(); + var mat = new Matrix4x4(); - for (int i = 0; i < values.Length; i++) + for (var i = 0; i < values.Length; i++) { mat.M11 += values[i].X * values[i].X; mat.M12 += values[i].X * values[i].Y; @@ -91,9 +99,9 @@ internal static Matrix4x4 CalculateCovariance(Span values, out Vector4 /// /// internal static Vector4 CalculatePrincipalAxis(Matrix4x4 covarianceMatrix) { - Vector4 lastDa = Vector4.UnitY; + var lastDa = Vector4.UnitY; - for (int i = 0; i < 30; i++) { + for (var i = 0; i < 30; i++) { var dA = Vector4.Transform(lastDa, covarianceMatrix); if(dA.LengthSquared() == 0) { @@ -112,13 +120,13 @@ internal static Vector4 CalculatePrincipalAxis(Matrix4x4 covarianceMatrix) { return lastDa; } - public static void Create(Span colors, out Vector3 mean, out Vector3 principalAxis) + public static void Create(Span colors, out Vector3 mean, out Vector3 principalAxis) { Span vectors = stackalloc Vector4[colors.Length]; ConvertToVector4(colors, vectors); - var cov = CalculateCovariance(vectors, out Vector4 v4Mean); + var cov = CalculateCovariance(vectors, out var v4Mean); mean = new Vector3(v4Mean.X, v4Mean.Y, v4Mean.Z); var pa = CalculatePrincipalAxis(cov); @@ -132,7 +140,29 @@ public static void Create(Span colors, out Vector3 mean, out Vector3 pri } - public static void CreateWithAlpha(Span colors, out Vector4 mean, out Vector4 principalAxis) + public static void Create(Span colors, out Vector3 mean, out Vector3 principalAxis) + { + Span vectors = stackalloc Vector4[colors.Length]; + ConvertToVector4(colors, vectors); + + + var cov = CalculateCovariance(vectors, out var v4Mean); + mean = new Vector3(v4Mean.X, v4Mean.Y, v4Mean.Z); + + var pa = CalculatePrincipalAxis(cov); + principalAxis = new Vector3(pa.X, pa.Y, pa.Z); + if (principalAxis.LengthSquared() == 0) + { + principalAxis = Vector3.UnitY; + } + else + { + principalAxis = Vector3.Normalize(principalAxis); + } + + } + + public static void CreateWithAlpha(Span colors, out Vector4 mean, out Vector4 principalAxis) { Span vectors = stackalloc Vector4[colors.Length]; ConvertToVector4(colors, vectors); @@ -142,16 +172,16 @@ public static void CreateWithAlpha(Span colors, out Vector4 mean, out Ve } - public static void GetExtremePoints(Span colors, Vector3 mean, Vector3 principalAxis, out ColorRgb24 min, + public static void GetExtremePoints(Span colors, Vector3 mean, Vector3 principalAxis, out ColorRgb24 min, out ColorRgb24 max) { float minD = 0; float maxD = 0; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { - var colorVec = new Vector3(colors[i].R / 255f, colors[i].G / 255f, colors[i].B / 255f); + var colorVec = new Vector3(colors[i].r / 255f, colors[i].g / 255f, colors[i].b / 255f); var v = colorVec - mean; var d = Vector3.Dot(v, principalAxis); @@ -159,39 +189,39 @@ public static void GetExtremePoints(Span colors, Vector3 mean, Vector3 p if (d > maxD) maxD = d; } - Vector3 minVec = mean + (principalAxis * minD); - Vector3 maxVec = mean + (principalAxis * maxD); + var minVec = mean + principalAxis * minD; + var maxVec = mean + principalAxis * maxD; - int minR = (int) (minVec.X * 255); - int minG = (int) (minVec.Y * 255); - int minB = (int) (minVec.Z * 255); + var minR = (int) (minVec.X * 255); + var minG = (int) (minVec.Y * 255); + var minB = (int) (minVec.Z * 255); - int maxR = (int) (maxVec.X * 255); - int maxG = (int) (maxVec.Y * 255); - int maxB = (int) (maxVec.Z * 255); + var maxR = (int) (maxVec.X * 255); + var maxG = (int) (maxVec.Y * 255); + var maxB = (int) (maxVec.Z * 255); - minR = (minR >= 0) ? minR : 0; - minG = (minG >= 0) ? minG : 0; - minB = (minB >= 0) ? minB : 0; + minR = minR >= 0 ? minR : 0; + minG = minG >= 0 ? minG : 0; + minB = minB >= 0 ? minB : 0; - maxR = (maxR <= 255) ? maxR : 255; - maxG = (maxG <= 255) ? maxG : 255; - maxB = (maxB <= 255) ? maxB : 255; + maxR = maxR <= 255 ? maxR : 255; + maxG = maxG <= 255 ? maxG : 255; + maxB = maxB <= 255 ? maxB : 255; min = new ColorRgb24((byte)minR, (byte)minG, (byte)minB); max = new ColorRgb24((byte)maxR, (byte)maxG, (byte)maxB); } - public static void GetMinMaxColor565(Span colors, Vector3 mean, Vector3 principalAxis, + public static void GetMinMaxColor565(Span colors, Vector3 mean, Vector3 principalAxis, out ColorRgb565 min, out ColorRgb565 max) { float minD = 0; float maxD = 0; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { - var colorVec = new Vector3(colors[i].R / 255f, colors[i].G / 255f, colors[i].B / 255f); + var colorVec = new Vector3(colors[i].r / 255f, colors[i].g / 255f, colors[i].b / 255f); var v = colorVec - mean; var d = Vector3.Dot(v, principalAxis); @@ -203,49 +233,49 @@ public static void GetMinMaxColor565(Span colors, Vector3 mean, Vector3 minD *= 15 / 16f; maxD *= 15 / 16f; - Vector3 minVec = mean + (principalAxis * minD); - Vector3 maxVec = mean + (principalAxis * maxD); + var minVec = mean + principalAxis * minD; + var maxVec = mean + principalAxis * maxD; - int minR = (int) (minVec.X * 255); - int minG = (int) (minVec.Y * 255); - int minB = (int) (minVec.Z * 255); + var minR = (int) (minVec.X * 255); + var minG = (int) (minVec.Y * 255); + var minB = (int) (minVec.Z * 255); - int maxR = (int) (maxVec.X * 255); - int maxG = (int) (maxVec.Y * 255); - int maxB = (int) (maxVec.Z * 255); + var maxR = (int) (maxVec.X * 255); + var maxG = (int) (maxVec.Y * 255); + var maxB = (int) (maxVec.Z * 255); - minR = (minR >= 0) ? minR : 0; - minG = (minG >= 0) ? minG : 0; - minB = (minB >= 0) ? minB : 0; + minR = minR >= 0 ? minR : 0; + minG = minG >= 0 ? minG : 0; + minB = minB >= 0 ? minB : 0; - maxR = (maxR <= 255) ? maxR : 255; - maxG = (maxG <= 255) ? maxG : 255; - maxB = (maxB <= 255) ? maxB : 255; + maxR = maxR <= 255 ? maxR : 255; + maxG = maxG <= 255 ? maxG : 255; + maxB = maxB <= 255 ? maxB : 255; // Optimal round - minR = (minR & c565_5_mask) | (minR >> 5); - minG = (minG & c565_6_mask) | (minG >> 6); - minB = (minB & c565_5_mask) | (minB >> 5); + minR = (minR & C565_5Mask) | (minR >> 5); + minG = (minG & C565_6Mask) | (minG >> 6); + minB = (minB & C565_5Mask) | (minB >> 5); - maxR = (maxR & c565_5_mask) | (maxR >> 5); - maxG = (maxG & c565_6_mask) | (maxG >> 6); - maxB = (maxB & c565_5_mask) | (maxB >> 5); + maxR = (maxR & C565_5Mask) | (maxR >> 5); + maxG = (maxG & C565_6Mask) | (maxG >> 6); + maxB = (maxB & C565_5Mask) | (maxB >> 5); min = new ColorRgb565((byte)minR, (byte)minG, (byte)minB); max = new ColorRgb565((byte)maxR, (byte)maxG, (byte)maxB); } - public static void GetExtremePointsWithAlpha(Span colors, Vector4 mean, Vector4 principalAxis, out Vector4 min, + public static void GetExtremePointsWithAlpha(Span colors, Vector4 mean, Vector4 principalAxis, out Vector4 min, out Vector4 max) { float minD = 0; float maxD = 0; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { - var colorVec = new Vector4(colors[i].R / 255f, colors[i].G / 255f, colors[i].B / 255f, colors[i].A / 255f); + var colorVec = new Vector4(colors[i].r / 255f, colors[i].g / 255f, colors[i].b / 255f, colors[i].a / 255f); var v = colorVec - mean; var d = Vector4.Dot(v, principalAxis); @@ -253,444 +283,33 @@ public static void GetExtremePointsWithAlpha(Span colors, Vector4 mean, if (d > maxD) maxD = d; } - min = mean + (principalAxis * minD); - max = mean + (principalAxis * maxD); + min = mean + principalAxis * minD; + max = mean + principalAxis * maxD; } - public static void GetOptimizedEndpoints565(Span colors, Vector3 mean, Vector3 principalAxis, out ColorRgb565 min, out ColorRgb565 max, - float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f) + public static void GetExtremePoints(Span colors, Vector3 mean, Vector3 principalAxis, out Vector3 min, + out Vector3 max) { - int length = colors.Length; - Vector3[] vectorColors = new Vector3[length]; - for (int i = 0; i < colors.Length; i++) - { - vectorColors[i] = new Vector3(colors[i].R / 255f, colors[i].G / 255f, colors[i].B / 255f); - } float minD = 0; float maxD = 0; - Vector3 Clamp565(Vector3 vec) - { - if (vec.X < 0) vec.X = 0; - if (vec.X > 31) vec.X = 31; - if (vec.Y < 0) vec.Y = 0; - if (vec.Y > 63) vec.Y = 63; - if (vec.Z < 0) vec.Z = 0; - if (vec.Z > 31) vec.Z = 31; - return new Vector3(MathF.Round(vec.X), MathF.Round(vec.Y), MathF.Round(vec.Z)); - } - - float Distance(Vector3 v, Vector3 p) - { - return (v.X - p.X) * (v.X - p.X) * rWeight - + (v.Y - p.Y) * (v.Y - p.Y) * gWeight - + (v.Z - p.Z) * (v.Z - p.Z) * bWeight; - ; - } - - float selectClosestDistance(Vector3 selector, Vector3 f0, Vector3 f1, Vector3 f2, Vector3 f3) + for (var i = 0; i < colors.Length; i++) { - float d0 = Distance(selector, f0); - float d1 = Distance(selector, f1); - float d2 = Distance(selector, f2); - float d3 = Distance(selector, f3); - - if (d0 < d1 && d0 < d2 && d0 < d3) return d0; - if (d1 < d0 && d1 < d2 && d1 < d3) return d1; - if (d2 < d0 && d2 < d1 && d2 < d3) return d2; - else return d3; - } - - Vector3 endPoint0; - Vector3 endPoint1; + var colorVec = new Vector3(colors[i].r, colors[i].g, colors[i].b); - double calculateError() - { - double cumulativeError = 0; - Vector3 ep0 = new Vector3(endPoint0.X / 31, endPoint0.Y / 63, endPoint0.Z / 31); - Vector3 ep1 = new Vector3(endPoint1.X / 31, endPoint1.Y / 63, endPoint1.Z / 31); - Vector3 ep2 = ep0 + ((ep1 - ep0) * 1 / 3f); - Vector3 ep3 = ep0 + ((ep1 - ep0) * 2 / 3f); - - for (int i = 0; i < length; i++) - { - double distance = selectClosestDistance(vectorColors[i], ep0, ep1, ep2, ep3); - cumulativeError += distance; - } - return cumulativeError; - } - - - for (int i = 0; i < vectorColors.Length; i++) - { - float d = ProjectPointOnLine(vectorColors[i], mean, principalAxis); + var v = colorVec - mean; + var d = Vector3.Dot(v, principalAxis); if (d < minD) minD = d; if (d > maxD) maxD = d; } - endPoint0 = mean + (principalAxis * minD); - endPoint1 = mean + (principalAxis * maxD); - - endPoint0 = new Vector3(MathF.Round(endPoint0.X * 31), MathF.Round(endPoint0.Y * 63), MathF.Round(endPoint0.Z * 31)); - endPoint1 = new Vector3(MathF.Round(endPoint1.X * 31), MathF.Round(endPoint1.Y * 63), MathF.Round(endPoint1.Z * 31)); - endPoint0 = Clamp565(endPoint0); - endPoint1 = Clamp565(endPoint1); - - double best = calculateError(); - int increment = 5; - bool foundBetter = true; - int rounds = 0; - // Variate color and look for better endpoints - while (increment > 1 || foundBetter) - { - rounds++; - foundBetter = false; - { // decrement ep0 - var prev = endPoint0; - endPoint0 -= principalAxis * increment * 2; - endPoint0 = Clamp565(endPoint0); - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint0 = prev; - } - } - - { // decrement ep1 - var prev = endPoint1; - endPoint1 -= principalAxis * increment * 2; - endPoint1 = Clamp565(endPoint1); - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint1 = prev; - } - } - - { // increment ep0 - var prev = endPoint0; - endPoint0 += principalAxis * increment * 2; - endPoint0 = Clamp565(endPoint0); - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint0 = prev; - } - } - - { // increment ep1 - var prev = endPoint1; - endPoint1 += principalAxis * increment * 2; - endPoint1 = Clamp565(endPoint1); - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint1 = prev; - } - } - - { // scaleUp - var prev0 = endPoint0; - var prev1 = endPoint1; - endPoint0 -= principalAxis * increment * 2; - endPoint1 += principalAxis * increment * 2; - endPoint0 = Clamp565(endPoint0); - endPoint1 = Clamp565(endPoint1); - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint0 = prev0; - endPoint1 = prev1; - } - } - - { // scaleDown - var prev0 = endPoint0; - var prev1 = endPoint1; - endPoint0 += principalAxis * increment * 2; - endPoint1 -= principalAxis * increment * 2; - endPoint0 = Clamp565(endPoint0); - endPoint1 = Clamp565(endPoint1); - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint0 = prev0; - endPoint1 = prev1; - } - } - - #region G - if (endPoint0.Y - increment >= 0) - { // decrement ep0 G - float prevY = endPoint0.Y; - endPoint0.Y -= increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint0.Y = prevY; - } - } - - if (endPoint1.Y - increment >= 0) - { // decrement ep1 G - float prevY = endPoint1.Y; - endPoint1.Y -= increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint1.Y = prevY; - } - } - - if (foundBetter && increment > 1) - { - increment--; - } - - if (endPoint1.Y + increment <= 63) - { // increment ep1 G - float prevY = endPoint1.Y; - endPoint1.Y += increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint1.Y = prevY; - } - } - - if (endPoint0.Y + increment <= 63) - { // increment ep0 G - float prevY = endPoint0.Y; - endPoint0.Y += increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint0.Y = prevY; - } - } - - #endregion - - #region R - if (endPoint0.X - increment >= 0) - { // decrement ep0 R - float prevX = endPoint0.X; - endPoint0.X -= increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint0.X = prevX; - } - } - - if (endPoint1.X - increment >= 0) - { // decrement ep1 R - float prevX = endPoint1.X; - endPoint1.X -= increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint1.X = prevX; - } - } - - if (foundBetter && increment > 1) - { - increment--; - } - - if (endPoint1.X + increment <= 31) - { // increment ep1 R - float prevX = endPoint1.X; - endPoint1.X += increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint1.X = prevX; - } - } - - if (endPoint0.X + increment <= 31) - { // increment ep0 R - float prevX = endPoint0.X; - endPoint0.X += increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint0.X = prevX; - } - } - #endregion - - #region B - - if (endPoint0.Z - increment >= 0) - { // decrement ep0 B - float prevZ = endPoint0.Z; - endPoint0.Z -= increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint0.Z = prevZ; - } - } - - if (endPoint1.Z - increment >= 0) - { // decrement ep1 B - float prevZ = endPoint1.Z; - endPoint1.Z -= increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint1.Z = prevZ; - } - } - - if (foundBetter && increment > 1) - { - increment--; - } - - if (endPoint1.Z + increment <= 31) - { // increment ep1 B - float prevZ = endPoint1.Z; - endPoint1.Z += increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint1.Z = prevZ; - } - } - - if (endPoint0.Z + increment <= 31) - { // increment ep0 B - float prevZ = endPoint0.Z; - endPoint0.Z += increment; - double error = calculateError(); - if (error < best) - { - foundBetter = true; - best = error; - } - else - { - endPoint0.Z = prevZ; - } - } - - #endregion - - endPoint0 = Clamp565(endPoint0); - endPoint1 = Clamp565(endPoint1); - - if (!foundBetter && increment > 1) - { - increment--; - } - } - - min = new ColorRgb565(); - min.RawR = (int)endPoint0.X; - min.RawG = (int)endPoint0.Y; - min.RawB = (int)endPoint0.Z; + minD *= 15 / 16f; + maxD *= 15 / 16f; - max = new ColorRgb565(); - max.RawR = (int)endPoint1.X; - max.RawG = (int)endPoint1.Y; - max.RawB = (int)endPoint1.Z; + min = mean + principalAxis * minD; + max = mean + principalAxis * maxD; } - private static float ProjectPointOnLine(Vector3 point, Vector3 linePoint, Vector3 lineDir) - { - var v = point - linePoint; - var d = Vector3.Dot(v, lineDir); - return d; - } } } diff --git a/BCnEnc.Net/Shared/ProgressElement.cs b/BCnEnc.Net/Shared/ProgressElement.cs new file mode 100644 index 0000000..77bf43e --- /dev/null +++ b/BCnEnc.Net/Shared/ProgressElement.cs @@ -0,0 +1,31 @@ +namespace BCnEncoder.Shared +{ + public struct ProgressElement + { + /// + /// Current block being processed + /// + public int CurrentBlock { get; } + + /// + /// The total amount of blocks to be processed + /// + public int TotalBlocks { get; } + + /// + /// Returns the progress percentage as a float from 0 to 1 + /// + public float Percentage => CurrentBlock / (float) TotalBlocks; + + public ProgressElement(int currentBlock, int totalBlocks) + { + CurrentBlock = currentBlock; + TotalBlocks = totalBlocks; + } + + public override string ToString() + { + return $"{nameof(CurrentBlock)}: {CurrentBlock}, {nameof(TotalBlocks)}: {TotalBlocks}, {nameof(Percentage)}: {Percentage}"; + } + } +} diff --git a/BCnEnc.Net/Shared/RawBlocks.cs b/BCnEnc.Net/Shared/RawBlocks.cs index 551e330..dadb4cc 100644 --- a/BCnEnc.Net/Shared/RawBlocks.cs +++ b/BCnEnc.Net/Shared/RawBlocks.cs @@ -1,45 +1,64 @@ -using System; +using System; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.PixelFormats; +using BCnEncoder.Encoder.Bptc; + +#if NETSTANDARD2_0 +using MemoryMarshal = BCnEncoder.Shared.MemoryMarshalPolyfills; +#endif namespace BCnEncoder.Shared { - internal struct RawBlock4X4Rgba32 { - public Rgba32 p00, p10, p20, p30; - public Rgba32 p01, p11, p21, p31; - public Rgba32 p02, p12, p22, p32; - public Rgba32 p03, p13, p23, p33; - public Span AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); + public struct RawBlock4X4Rgba32 + { + public ColorRgba32 p00, p10, p20, p30; + public ColorRgba32 p01, p11, p21, p31; + public ColorRgba32 p02, p12, p22, p32; + public ColorRgba32 p03, p13, p23, p33; + + public RawBlock4X4Rgba32(ColorRgba32 fillColor) + { + p00 = p01 = p02 = p03 = + p10 = p11 = p12 = p13 = + p20 = p21 = p22 = p23 = + p30 = p31 = p32 = p33 = fillColor; + } - public Rgba32 this[int x, int y] { + public Span AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); + + public ColorRgba32 this[int x, int y] + { get => AsSpan[x + y * 4]; set => AsSpan[x + y * 4] = value; } - public Rgba32 this[int index] { + public ColorRgba32 this[int index] + { get => AsSpan[index]; set => AsSpan[index] = value; } - public int CalculateError(RawBlock4X4Rgba32 other, bool useAlpha = false) { + internal int CalculateError(RawBlock4X4Rgba32 other, bool useAlpha = false) + { float error = 0; var pix1 = AsSpan; var pix2 = other.AsSpan; - for (int i = 0; i < pix1.Length; i++) { + for (var i = 0; i < pix1.Length; i++) + { var col1 = pix1[i]; var col2 = pix2[i]; - var re = col1.R - col2.R; - var ge = col1.G - col2.G; - var be = col1.B - col2.B; + var re = col1.r - col2.r; + var ge = col1.g - col2.g; + var be = col1.b - col2.b; error += re * re; error += ge * ge; error += be * be; - if (useAlpha) { - var ae = col1.A - col2.A; + if (useAlpha) + { + var ae = col1.a - col2.a; error += ae * ae * 4; } } @@ -50,13 +69,15 @@ public int CalculateError(RawBlock4X4Rgba32 other, bool useAlpha = false) { return (int)error; } - public float CalculateYCbCrError(RawBlock4X4Rgba32 other) { + internal float CalculateYCbCrError(RawBlock4X4Rgba32 other) + { float yError = 0; float cbError = 0; float crError = 0; var pix1 = AsSpan; var pix2 = other.AsSpan; - for (int i = 0; i < pix1.Length; i++) { + for (var i = 0; i < pix1.Length; i++) + { var col1 = new ColorYCbCr(pix1[i]); var col2 = new ColorYCbCr(pix2[i]); @@ -74,14 +95,16 @@ public float CalculateYCbCrError(RawBlock4X4Rgba32 other) { return error; } - public float CalculateYCbCrAlphaError(RawBlock4X4Rgba32 other, float yMultiplier = 2, float alphaMultiplier = 1) { + internal float CalculateYCbCrAlphaError(RawBlock4X4Rgba32 other, float yMultiplier = 2, float alphaMultiplier = 1) + { float yError = 0; float cbError = 0; float crError = 0; float alphaError = 0; var pix1 = AsSpan; var pix2 = other.AsSpan; - for (int i = 0; i < pix1.Length; i++) { + for (var i = 0; i < pix1.Length; i++) + { var col1 = new ColorYCbCrAlpha(pix1[i]); var col2 = new ColorYCbCrAlpha(pix2[i]); @@ -100,45 +123,183 @@ public float CalculateYCbCrAlphaError(RawBlock4X4Rgba32 other, float yMultiplier return error; } - public RawBlock4X4Ycbcr ToRawBlockYcbcr() { - RawBlock4X4Ycbcr rawYcbcr = new RawBlock4X4Ycbcr(); + internal RawBlock4X4Ycbcr ToRawBlockYcbcr() + { + var rawYcbcr = new RawBlock4X4Ycbcr(); var pixels = AsSpan; var ycbcrPs = rawYcbcr.AsSpan; - for (int i = 0; i < pixels.Length; i++) { + for (var i = 0; i < pixels.Length; i++) + { ycbcrPs[i] = new ColorYCbCr(pixels[i]); } return rawYcbcr; } - public bool HasTransparentPixels() { + public bool HasTransparentPixels() + { var pixels = AsSpan; - for (int i = 0; i < pixels.Length; i++) { - if (pixels[i].A < 255) return true; + for (var i = 0; i < pixels.Length; i++) + { + if (pixels[i].a < 255) return true; } return false; } } + public struct RawBlock4X4RgbFloat + { + public ColorRgbFloat p00, p10, p20, p30; + public ColorRgbFloat p01, p11, p21, p31; + public ColorRgbFloat p02, p12, p22, p32; + public ColorRgbFloat p03, p13, p23, p33; + + public RawBlock4X4RgbFloat(ColorRgbFloat fillColor) + { + p00 = p01 = p02 = p03 = + p10 = p11 = p12 = p13 = + p20 = p21 = p22 = p23 = + p30 = p31 = p32 = p33 = fillColor; + } + + public Span AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); + + public ColorRgbFloat this[int x, int y] + { + get => AsSpan[x + y * 4]; + set => AsSpan[x + y * 4] = value; + } + + public ColorRgbFloat this[int index] + { + get => AsSpan[index]; + set => AsSpan[index] = value; + } + + internal float CalculateError(RawBlock4X4RgbFloat other) + { + float error = 0; + var pix1 = AsSpan; + var pix2 = other.AsSpan; + for (var i = 0; i < pix1.Length; i++) + { + var col1 = pix1[i]; + var col2 = pix2[i]; + + 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; + + } + + error /= pix1.Length * 3; + error = MathF.Sqrt(error); + + return error; + } + + internal float CalculateYCbCrError(RawBlock4X4RgbFloat other) + { + float yError = 0; + float cbError = 0; + float crError = 0; + var pix1 = AsSpan; + var pix2 = other.AsSpan; + for (var i = 0; i < pix1.Length; i++) + { + var col1 = new ColorYCbCr(pix1[i]); + var col2 = new ColorYCbCr(pix2[i]); + + var ye = col1.y - col2.y; + var cbe = col1.cb - col2.cb; + var cre = col1.cr - col2.cr; + + yError += ye * ye; + cbError += cbe * cbe; + crError += cre * cre; + } + + var error = yError * 2 + cbError / 2 + crError / 2; + + return error; + } + + + internal RawBlock4X4Ycbcr ToRawBlockYcbcr() + { + var rawYcbcr = new RawBlock4X4Ycbcr(); + var pixels = AsSpan; + var ycbcrPs = rawYcbcr.AsSpan; + for (var i = 0; i < pixels.Length; i++) + { + ycbcrPs[i] = new ColorYCbCr(pixels[i]); + } + return rawYcbcr; + } + } + + //Used for Bc6H + internal struct RawBlock4X4RgbHalfInt + { + public (int, int, int) p00, p10, p20, p30; + public (int, int, int) p01, p11, p21, p31; + public (int, int, int) p02, p12, p22, p32; + public (int, int, int) p03, p13, p23, p33; + public Span<(int, int, int)> AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); + + public (int, int, int) this[int x, int y] + { + get => AsSpan[x + y * 4]; + set => AsSpan[x + y * 4] = value; + } + + public (int, int, int) this[int index] + { + get => AsSpan[index]; + set => AsSpan[index] = value; + } + + public static RawBlock4X4RgbHalfInt FromRawFloats(RawBlock4X4RgbFloat other, bool signed) + { + var output = new RawBlock4X4RgbHalfInt(); + var span = output.AsSpan; + var floats = other.AsSpan; + for (var i = 0; i < 16; i++) + { + span[i] = Bc6EncodingHelpers.PreQuantizeRawEndpoint(floats[i], signed); + } + return output; + } + + } + - internal struct RawBlock4X4Ycbcr { + internal struct RawBlock4X4Ycbcr + { public ColorYCbCr p00, p10, p20, p30; public ColorYCbCr p01, p11, p21, p31; public ColorYCbCr p02, p12, p22, p32; public ColorYCbCr p03, p13, p23, p33; public Span AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); - public ColorYCbCr this[int x, int y] { + public ColorYCbCr this[int x, int y] + { get => AsSpan[x + y * 4]; set => AsSpan[x + y * 4] = value; } - public float CalculateError(RawBlock4X4Rgba32 other) { + public float CalculateError(RawBlock4X4Rgba32 other) + { float yError = 0; float cbError = 0; float crError = 0; var pix1 = AsSpan; var pix2 = other.AsSpan; - for (int i = 0; i < pix1.Length; i++) { + for (var i = 0; i < pix1.Length; i++) + { var col1 = pix1[i]; var col2 = new ColorYCbCr(pix2[i]); diff --git a/BCnEnc.Net/Shared/RgbBoundingBox.cs b/BCnEnc.Net/Shared/RgbBoundingBox.cs index 3e72628..b6b2f44 100644 --- a/BCnEnc.Net/Shared/RgbBoundingBox.cs +++ b/BCnEnc.Net/Shared/RgbBoundingBox.cs @@ -1,5 +1,5 @@ -using System; -using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Runtime.InteropServices; namespace BCnEncoder.Shared { @@ -11,11 +11,11 @@ namespace BCnEncoder.Shared internal static class RgbBoundingBox { - public static void Create565(ReadOnlySpan colors, out ColorRgb565 min, out ColorRgb565 max) + public static void Create565(ReadOnlySpan colors, out ColorRgb565 min, out ColorRgb565 max) { const int colorInsetShift = 4; - const int c565_5_mask = 0xF8; - const int c565_6_mask = 0xFC; + const int c5655Mask = 0xF8; + const int c5656Mask = 0xFC; int minR = 255, minG = 255, @@ -24,22 +24,22 @@ public static void Create565(ReadOnlySpan colors, out ColorRgb565 min, o maxG = 0, maxB = 0; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { var c = colors[i]; - if (c.R < minR) minR = c.R; - if (c.G < minG) minG = c.G; - if (c.B < minB) minB = c.B; + if (c.r < minR) minR = c.r; + if (c.g < minG) minG = c.g; + if (c.b < minB) minB = c.b; - if (c.R > maxR) maxR = c.R; - if (c.G > maxG) maxG = c.G; - if (c.B > maxB) maxB = c.B; + if (c.r > maxR) maxR = c.r; + if (c.g > maxG) maxG = c.g; + if (c.b > maxB) maxB = c.b; } - int insetR = (maxR - minR) >> colorInsetShift; - int insetG = (maxG - minG) >> colorInsetShift; - int insetB = (maxB - minB) >> colorInsetShift; + var insetR = (maxR - minR) >> colorInsetShift; + var insetG = (maxG - minG) >> colorInsetShift; + var insetB = (maxB - minB) >> colorInsetShift; // Inset by 1/16th minR = ((minR << colorInsetShift) + insetR) >> colorInsetShift; @@ -50,32 +50,32 @@ public static void Create565(ReadOnlySpan colors, out ColorRgb565 min, o maxG = ((maxG << colorInsetShift) - insetG) >> colorInsetShift; maxB = ((maxB << colorInsetShift) - insetB) >> colorInsetShift; - minR = (minR >= 0) ? minR : 0; - minG = (minG >= 0) ? minG : 0; - minB = (minB >= 0) ? minB : 0; + minR = minR >= 0 ? minR : 0; + minG = minG >= 0 ? minG : 0; + minB = minB >= 0 ? minB : 0; - maxR = (maxR <= 255) ? maxR : 255; - maxG = (maxG <= 255) ? maxG : 255; - maxB = (maxB <= 255) ? maxB : 255; + maxR = maxR <= 255 ? maxR : 255; + maxG = maxG <= 255 ? maxG : 255; + maxB = maxB <= 255 ? maxB : 255; // Optimal rounding - minR = (minR & c565_5_mask) | (minR >> 5); - minG = (minG & c565_6_mask) | (minG >> 6); - minB = (minB & c565_5_mask) | (minB >> 5); + minR = (minR & c5655Mask) | (minR >> 5); + minG = (minG & c5656Mask) | (minG >> 6); + minB = (minB & c5655Mask) | (minB >> 5); - maxR = (maxR & c565_5_mask) | (maxR >> 5); - maxG = (maxG & c565_6_mask) | (maxG >> 6); - maxB = (maxB & c565_5_mask) | (maxB >> 5); + maxR = (maxR & c5655Mask) | (maxR >> 5); + maxG = (maxG & c5656Mask) | (maxG >> 6); + maxB = (maxB & c5655Mask) | (maxB >> 5); min = new ColorRgb565((byte)minR, (byte)minG, (byte)minB); max = new ColorRgb565((byte)maxR, (byte)maxG, (byte)maxB); } - public static void Create565AlphaCutoff(ReadOnlySpan colors, out ColorRgb565 min, out ColorRgb565 max, int alphaCutoff = 128) + public static void Create565AlphaCutoff(ReadOnlySpan colors, out ColorRgb565 min, out ColorRgb565 max, int alphaCutoff = 128) { const int colorInsetShift = 4; - const int c565_5_mask = 0xF8; - const int c565_6_mask = 0xFC; + const int c5655Mask = 0xF8; + const int c5656Mask = 0xFC; int minR = 255, minG = 255, @@ -84,22 +84,22 @@ public static void Create565AlphaCutoff(ReadOnlySpan colors, out ColorRg maxG = 0, maxB = 0; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { var c = colors[i]; - if (c.A < alphaCutoff) continue; - if (c.R < minR) minR = c.R; - if (c.G < minG) minG = c.G; - if (c.B < minB) minB = c.B; - - if (c.R > maxR) maxR = c.R; - if (c.G > maxG) maxG = c.G; - if (c.B > maxB) maxB = c.B; + if (c.a < alphaCutoff) continue; + if (c.r < minR) minR = c.r; + if (c.g < minG) minG = c.g; + if (c.b < minB) minB = c.b; + + if (c.r > maxR) maxR = c.r; + if (c.g > maxG) maxG = c.g; + if (c.b > maxB) maxB = c.b; } - int insetR = (maxR - minR) >> colorInsetShift; - int insetG = (maxG - minG) >> colorInsetShift; - int insetB = (maxB - minB) >> colorInsetShift; + var insetR = (maxR - minR) >> colorInsetShift; + var insetG = (maxG - minG) >> colorInsetShift; + var insetB = (maxB - minB) >> colorInsetShift; minR = ((minR << colorInsetShift) + insetR) >> colorInsetShift; minG = ((minG << colorInsetShift) + insetG) >> colorInsetShift; @@ -109,32 +109,32 @@ public static void Create565AlphaCutoff(ReadOnlySpan colors, out ColorRg maxG = ((maxG << colorInsetShift) - insetG) >> colorInsetShift; maxB = ((maxB << colorInsetShift) - insetB) >> colorInsetShift; - minR = (minR >= 0) ? minR : 0; - minG = (minG >= 0) ? minG : 0; - minB = (minB >= 0) ? minB : 0; + minR = minR >= 0 ? minR : 0; + minG = minG >= 0 ? minG : 0; + minB = minB >= 0 ? minB : 0; - maxR = (maxR <= 255) ? maxR : 255; - maxG = (maxG <= 255) ? maxG : 255; - maxB = (maxB <= 255) ? maxB : 255; + maxR = maxR <= 255 ? maxR : 255; + maxG = maxG <= 255 ? maxG : 255; + maxB = maxB <= 255 ? maxB : 255; - minR = (minR & c565_5_mask) | (minR >> 5); - minG = (minG & c565_6_mask) | (minG >> 6); - minB = (minB & c565_5_mask) | (minB >> 5); + minR = (minR & c5655Mask) | (minR >> 5); + minG = (minG & c5656Mask) | (minG >> 6); + minB = (minB & c5655Mask) | (minB >> 5); - maxR = (maxR & c565_5_mask) | (maxR >> 5); - maxG = (maxG & c565_6_mask) | (maxG >> 6); - maxB = (maxB & c565_5_mask) | (maxB >> 5); + maxR = (maxR & c5655Mask) | (maxR >> 5); + maxG = (maxG & c5656Mask) | (maxG >> 6); + maxB = (maxB & c5655Mask) | (maxB >> 5); min = new ColorRgb565((byte)minR, (byte)minG, (byte)minB); max = new ColorRgb565((byte)maxR, (byte)maxG, (byte)maxB); } - public static void Create565a(ReadOnlySpan colors, out ColorRgb565 min, out ColorRgb565 max, out byte minAlpha, out byte maxAlpha) + public static void Create565A(ReadOnlySpan colors, out ColorRgb565 min, out ColorRgb565 max, out byte minAlpha, out byte maxAlpha) { const int colorInsetShift = 4; const int alphaInsetShift = 5; - const int c565_5_mask = 0xF8; - const int c565_6_mask = 0xFC; + const int c5655Mask = 0xF8; + const int c5656Mask = 0xFC; int minR = 255, minG = 255, @@ -145,25 +145,25 @@ public static void Create565a(ReadOnlySpan colors, out ColorRgb565 min, maxB = 0, maxA = 0; - for (int i = 0; i < colors.Length; i++) + for (var i = 0; i < colors.Length; i++) { var c = colors[i]; - if (c.R < minR) minR = c.R; - if (c.G < minG) minG = c.G; - if (c.B < minB) minB = c.B; - if (c.A < minA) minA = c.A; - - if (c.R > maxR) maxR = c.R; - if (c.G > maxG) maxG = c.G; - if (c.B > maxB) maxB = c.B; - if (c.A > maxA) maxA = c.A; + if (c.r < minR) minR = c.r; + if (c.g < minG) minG = c.g; + if (c.b < minB) minB = c.b; + if (c.a < minA) minA = c.a; + + if (c.r > maxR) maxR = c.r; + if (c.g > maxG) maxG = c.g; + if (c.b > maxB) maxB = c.b; + if (c.a > maxA) maxA = c.a; } - int insetR = (maxR - minR) >> colorInsetShift; - int insetG = (maxG - minG) >> colorInsetShift; - int insetB = (maxB - minB) >> colorInsetShift; - int insetA = (maxA - minA) >> alphaInsetShift; + var insetR = (maxR - minR) >> colorInsetShift; + var insetG = (maxG - minG) >> colorInsetShift; + var insetB = (maxB - minB) >> colorInsetShift; + var insetA = (maxA - minA) >> alphaInsetShift; minR = ((minR << colorInsetShift) + insetR) >> colorInsetShift; minG = ((minG << colorInsetShift) + insetG) >> colorInsetShift; @@ -175,28 +175,103 @@ public static void Create565a(ReadOnlySpan colors, out ColorRgb565 min, maxB = ((maxB << colorInsetShift) - insetB) >> colorInsetShift; maxA = ((maxA << alphaInsetShift) - insetA) >> alphaInsetShift; - minR = (minR >= 0) ? minR : 0; - minG = (minG >= 0) ? minG : 0; - minB = (minB >= 0) ? minB : 0; - minA = (minA >= 0) ? minA : 0; + minR = minR >= 0 ? minR : 0; + minG = minG >= 0 ? minG : 0; + minB = minB >= 0 ? minB : 0; + minA = minA >= 0 ? minA : 0; - maxR = (maxR <= 255) ? maxR : 255; - maxG = (maxG <= 255) ? maxG : 255; - maxB = (maxB <= 255) ? maxB : 255; - maxA = (maxA <= 255) ? maxA : 255; + maxR = maxR <= 255 ? maxR : 255; + maxG = maxG <= 255 ? maxG : 255; + maxB = maxB <= 255 ? maxB : 255; + maxA = maxA <= 255 ? maxA : 255; - minR = (minR & c565_5_mask) | (minR >> 5); - minG = (minG & c565_6_mask) | (minG >> 6); - minB = (minB & c565_5_mask) | (minB >> 5); + minR = (minR & c5655Mask) | (minR >> 5); + minG = (minG & c5656Mask) | (minG >> 6); + minB = (minB & c5655Mask) | (minB >> 5); - maxR = (maxR & c565_5_mask) | (maxR >> 5); - maxG = (maxG & c565_6_mask) | (maxG >> 6); - maxB = (maxB & c565_5_mask) | (maxB >> 5); + maxR = (maxR & c5655Mask) | (maxR >> 5); + maxG = (maxG & c5656Mask) | (maxG >> 6); + maxB = (maxB & c5655Mask) | (maxB >> 5); min = new ColorRgb565((byte)minR, (byte)minG, (byte)minB); max = new ColorRgb565((byte)maxR, (byte)maxG, (byte)maxB); minAlpha = (byte)minA; maxAlpha = (byte)maxA; } + + /// + /// Hdr rgb bounding box inset by Krzysztof Narkowicz. https://github.com/knarkowicz/GPURealTimeBC6H + /// Code is public domain. + /// + private static void InsetHdrChannel(ReadOnlySpan colors, int channel, ref float blockMax, ref float blockMin) + { + var offset = 0f; + if (blockMin < 0) + { + offset = -blockMin; + blockMin += offset; + blockMax += offset; + } + + float Select(ReadOnlySpan span, int i) + { + return span[i * 3 + channel] + offset; + } + + var floats = MemoryMarshal.Cast(colors); + + var refinedBlockMin = blockMax; + var refinedBlockMax = blockMin; + + for (var i = 0; i < 16; ++i) + { + refinedBlockMin = MathF.Min(refinedBlockMin, Select(floats, i) == blockMin ? refinedBlockMin : Select(floats, i)); + refinedBlockMax = MathF.Max(refinedBlockMax, Select(floats, i) == blockMax ? refinedBlockMax : Select(floats, i)); + } + + var logRefinedBlockMax = MathF.Log(refinedBlockMax + 1.0f, 2); + var logRefinedBlockMin = MathF.Log(refinedBlockMin + 1.0f, 2); + + var logBlockMax = MathF.Log(blockMax + 1.0f, 2); + var logBlockMin = MathF.Log(blockMin + 1.0f, 2); + var logBlockMaxExt = (logBlockMax - logBlockMin) * (1.0f / 32.0f); + + logBlockMin += MathF.Min(logRefinedBlockMin - logBlockMin, logBlockMaxExt); + logBlockMax -= MathF.Min(logBlockMax - logRefinedBlockMax, logBlockMaxExt); + + blockMin = MathF.Pow(2, logBlockMin) - 1.0f - offset; + blockMax = MathF.Pow(2, logBlockMax) - 1.0f - offset; + } + + public static void CreateFloat(ReadOnlySpan colors, out ColorRgbFloat min, out ColorRgbFloat max) + { + + float minR = float.MaxValue, + minG = float.MaxValue, + minB = float.MaxValue; + float maxR = float.MinValue, + maxG = float.MinValue, + maxB = float.MinValue; + + for (var i = 0; i < colors.Length; i++) + { + var c = colors[i]; + + if (c.r < minR) minR = c.r; + if (c.g < minG) minG = c.g; + if (c.b < minB) minB = c.b; + + if (c.r > maxR) maxR = c.r; + if (c.g > maxG) maxG = c.g; + if (c.b > maxB) maxB = c.b; + } + + InsetHdrChannel(colors, 0, ref maxR, ref minR); + InsetHdrChannel(colors, 1, ref maxG, ref minG); + InsetHdrChannel(colors, 2, ref maxB, ref minB); + + min = new ColorRgbFloat(minR, minG, minB); + max = new ColorRgbFloat(maxR, maxG, maxB); + } } } diff --git a/BCnEncNet.sln b/BCnEncNet.sln index 96b2490..dca5cf7 100644 --- a/BCnEncNet.sln +++ b/BCnEncNet.sln @@ -7,6 +7,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BCnEncoder", "BCnEnc.Net\BC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BCnEncTests", "BCnEncTests\BCnEncTests.csproj", "{8E5E366B-9782-4194-AC85-602E8387496A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5B2FBB0B-E3B9-44EB-BBE6-E7EC9F3E56AD}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +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 @@ -21,6 +32,14 @@ Global {8E5E366B-9782-4194-AC85-602E8387496A}.Debug|Any CPU.Build.0 = Debug|Any CPU {8E5E366B-9782-4194-AC85-602E8387496A}.Release|Any CPU.ActiveCfg = Release|Any CPU {8E5E366B-9782-4194-AC85-602E8387496A}.Release|Any CPU.Build.0 = Release|Any CPU + {7D884C56-B982-4B8C-907E-68F231CF3D89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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/BCnEncNet.sln.DotSettings b/BCnEncNet.sln.DotSettings new file mode 100644 index 0000000..7ba4779 --- /dev/null +++ b/BCnEncNet.sln.DotSettings @@ -0,0 +1,4 @@ + + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> \ No newline at end of file diff --git a/BCnEncTests.Framework/BCnEncTests.Framework.csproj b/BCnEncTests.Framework/BCnEncTests.Framework.csproj new file mode 100644 index 0000000..c928b90 --- /dev/null +++ b/BCnEncTests.Framework/BCnEncTests.Framework.csproj @@ -0,0 +1,33 @@ + + + + net481 + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/BCnEncTests.Framework/HdrImageTests.cs b/BCnEncTests.Framework/HdrImageTests.cs new file mode 100644 index 0000000..75e9c8e --- /dev/null +++ b/BCnEncTests.Framework/HdrImageTests.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class HdrImageTests + { + [Fact] + public void LoadHdr() + { + using (var stream = File.OpenRead("../../../../BCnEncTests/testImages/test_hdr_kiara.hdr")) + { + var hdrImg = HdrImage.Read(stream); + Assert.True(hdrImg.width > 0); + Assert.True(hdrImg.height > 0); + Assert.True(hdrImg.pixels.Length == hdrImg.width * hdrImg.height); + + var rgba = new ColorRgba32[hdrImg.pixels.Length]; + for (var i = 0; i < hdrImg.pixels.Length; i++) + { + var p = hdrImg.pixels[i]; + rgba[i] = new ColorRgba32( + (byte)(Math.Max(0, Math.Min(1, p.r)) * 255 + 0.5f), + (byte)(Math.Max(0, Math.Min(1, p.g)) * 255 + 0.5f), + (byte)(Math.Max(0, Math.Min(1, p.b)) * 255 + 0.5f), + 255); + } + var converted = new Memory2D(rgba, hdrImg.height, hdrImg.width); + var reference = ImageLoader.LoadTestImage("../../../../BCnEncTests/testImages/test_hdr_kiara.png"); + TestHelper.AssertImagesEqual(reference, converted, CompressionQuality.BestQuality); + } + } + + [Fact] + public async Task DecodeAllMipMapsHdrStreamAsync() + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Format = CompressionFormat.Bc6U; + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + + var decoder = new BcDecoder(); + var input = HdrLoader.TestHdrKiara; + var ktxWithMips = encoder.EncodeToKtxHdr(new Memory2D(input.pixels, input.height, input.width)); + using (var ms = new MemoryStream()) + { + ktxWithMips.Write(ms); + ms.Position = 0; + + var images = await decoder.DecodeAllMipMapsHdr2DAsync(ms); + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + Assert.True(images.Length > 1); + } + } + } +} diff --git a/BCnEncTests.Framework/MipMapperTests.cs b/BCnEncTests.Framework/MipMapperTests.cs new file mode 100644 index 0000000..8b9e292 --- /dev/null +++ b/BCnEncTests.Framework/MipMapperTests.cs @@ -0,0 +1,83 @@ +using System; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class MipMapperTests + { + [Fact] + public void MipChainHasCorrectDimensions() + { + var image = ImageLoader.TestGradient1; // 512x416 + var numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + Assert.Equal(chain.Length, numMips); + Assert.True(numMips > 1); + + for (var i = 0; i < numMips; i++) + { + Assert.Equal(Math.Max(1, image.Width >> i), chain[i].Width); + Assert.Equal(Math.Max(1, image.Height >> i), chain[i].Height); + } + + // Last level must be 1x1 + Assert.Equal(1, chain[numMips - 1].Width); + Assert.Equal(1, chain[numMips - 1].Height); + } + + /// + /// A solid-color image must downsample to exactly the same color at + /// every mip level, regardless of how many times the box filter is applied. + /// + [Fact] + public void SolidColorMipChainIsExact() + { + var color = new ColorRgba32(200, 100, 50, 255); + var pixels = new ColorRgba32[16 * 16]; + for (var i = 0; i < pixels.Length; i++) pixels[i] = color; + + var image = new Memory2D(pixels, 16, 16); + var numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + // 16x16 -> 8x8 -> 4x4 -> 2x2 -> 1x1 = 5 levels + Assert.Equal(5, numMips); + + for (var level = 0; level < numMips; level++) + { + var span = chain[level].Span; + for (var y = 0; y < chain[level].Height; y++) + for (var x = 0; x < chain[level].Width; x++) + Assert.Equal(color, span[y, x]); + } + } + + /// + /// Verifies the exact 2x2 box-filter arithmetic with a known input. + /// A 2x2 image of two values repeated both rows should average exactly. + /// + [Fact] + public void KnownPatternDownsamplesCorrectly() + { + // 2-wide, 2-tall: left column = 100 red, right column = 200 red. + // Expected 1x1 average: r = (100+200+100+200)/4 = 150, g=b=0, a=255. + var pixels = new[] + { + new ColorRgba32(100, 0, 0, 255), new ColorRgba32(200, 0, 0, 255), + new ColorRgba32(100, 0, 0, 255), new ColorRgba32(200, 0, 0, 255), + }; + var image = new Memory2D(pixels, 2, 2); + var numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + Assert.Equal(2, numMips); // 2x2 -> 1x1 + Assert.Equal(1, chain[1].Width); + Assert.Equal(1, chain[1].Height); + Assert.Equal(new ColorRgba32(150, 0, 0, 255), chain[1].Span[0, 0]); + } + } +} diff --git a/BCnEncTests.Framework/Support/HdrLoader.cs b/BCnEncTests.Framework/Support/HdrLoader.cs new file mode 100644 index 0000000..51f7d5b --- /dev/null +++ b/BCnEncTests.Framework/Support/HdrLoader.cs @@ -0,0 +1,15 @@ +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; + +namespace BCnEncTests.Support +{ + public static class HdrLoader + { + public static HdrImage TestHdrKiara { get; } = HdrImage.Read("../../../../BCnEncTests/testImages/test_hdr_kiara.hdr"); + public static HdrImage TestHdrProbe { get; } = HdrImage.Read("../../../../BCnEncTests/testImages/test_hdr_probe.hdr"); + public static DdsFile TestHdrKiaraDds { get; } = + DdsLoader.LoadDdsFile("../../../../BCnEncTests/testImages/test_hdr_kiara_bc6h.dds"); + public static KtxFile TestHdrKiaraKtx { get; } = + KtxLoader.LoadKtxFile("../../../../BCnEncTests/testImages/test_hdr_kiara_bc6h_ktx.ktx"); + } +} diff --git a/BCnEncTests.Framework/Support/ImageLoader.cs b/BCnEncTests.Framework/Support/ImageLoader.cs new file mode 100644 index 0000000..c73eea2 --- /dev/null +++ b/BCnEncTests.Framework/Support/ImageLoader.cs @@ -0,0 +1,104 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; + +namespace BCnEncTests.Support +{ + public static class ImageLoader + { + public static Memory2D TestDiffuse1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_diffuse_1_512.jpg"); + public static Memory2D TestBlur1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_blur_1_512.jpg"); + public static Memory2D TestNormal1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_normal_1_512.jpg"); + public static Memory2D TestHeight1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_height_1_512.jpg"); + public static Memory2D TestGradient1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_gradient_1_512.jpg"); + + public static Memory2D TestTransparentSprite1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_transparent.png"); + public static Memory2D TestAlphaGradient1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_alphagradient_1_512.png"); + public static Memory2D TestAlpha1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_alpha_1_512.png"); + public static Memory2D TestRedGreen1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_red_green_1_64.png"); + public static Memory2D TestRgbHard1 { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_rgb_hard_1.png"); + public static Memory2D TestLenna { get; } = LoadTestImage("../../../../BCnEncTests/testImages/test_lenna_512.png"); + public static Memory2D TestDecodingBc5Reference { get; } = LoadTestImage("../../../../BCnEncTests/testImages/decoding_dds_bc5_reference.png"); + + public static Memory2D[] TestCubemap { get; } = { + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/right.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/left.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/top.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/bottom.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/back.png"), + LoadTestImage("../../../../BCnEncTests/testImages/cubemap/forward.png") + }; + + internal static Memory2D LoadTestImage(string filename) + { + using (var bmp = new Bitmap(filename)) + { + return FromBitmap(bmp); + } + } + + internal static unsafe Memory2D FromBitmap(Bitmap bmp) + { + var pixels = new ColorRgba32[bmp.Width * bmp.Height]; + var data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), + ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + byte* ptr = (byte*)data.Scan0; + for (int i = 0; i < pixels.Length; i++) + { + // GDI+ Format32bppArgb memory order: B,G,R,A + pixels[i] = new ColorRgba32(ptr[2], ptr[1], ptr[0], ptr[3]); + ptr += 4; + } + bmp.UnlockBits(data); + return new Memory2D(pixels, bmp.Width, bmp.Height); + } + } + + public static class DdsLoader + { + public const string TestDecompressBc1Name = "../../../../BCnEncTests/testImages/test_decompress_bc1.dds"; + public const string TestDecompressBc1AName = "../../../../BCnEncTests/testImages/test_decompress_bc1a.dds"; + public const string TestDecompressBc7Name = "../../../../BCnEncTests/testImages/test_decompress_bc7.dds"; + public const string TestDecompressBc5Name = "../../../../BCnEncTests/testImages/decoding_dds_bc5.dds"; + public const string TestDecompressRgbaName = "../../../../BCnEncTests/testImages/test_decompress_rgba.dds"; + + public static DdsFile TestDecompressBc1 { get; } = LoadDdsFile(TestDecompressBc1Name); + public static DdsFile TestDecompressBc1A { get; } = LoadDdsFile(TestDecompressBc1AName); + public static DdsFile TestDecompressBc5 { get; } = LoadDdsFile(TestDecompressBc5Name); + public static DdsFile TestDecompressBc7 { get; } = LoadDdsFile(TestDecompressBc7Name); + public static DdsFile TestDecompressRgba { get; } = LoadDdsFile(TestDecompressRgbaName); + + internal static DdsFile LoadDdsFile(string filename) + { + using (var fs = File.OpenRead(filename)) + { + return DdsFile.Load(fs); + } + } + } + + public static class KtxLoader + { + public static KtxFile TestDecompressBc1 { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc1.ktx"); + public static KtxFile TestDecompressBc1A { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc1a.ktx"); + public static KtxFile TestDecompressBc2 { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc2.ktx"); + public static KtxFile TestDecompressBc3 { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc3.ktx"); + public static KtxFile TestDecompressBc4Unorm { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc4_unorm.ktx"); + public static KtxFile TestDecompressBc5Unorm { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc5_unorm.ktx"); + public static KtxFile TestDecompressBc7Rgb { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc7_rgb.ktx"); + public static KtxFile TestDecompressBc7Types { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc7_types.ktx"); + public static KtxFile TestDecompressBc7Unorm { get; } = LoadKtxFile("../../../../BCnEncTests/testImages/test_decompress_bc7_unorm.ktx"); + + internal static KtxFile LoadKtxFile(string filename) + { + using (var fs = File.OpenRead(filename)) + { + return KtxFile.Load(fs); + } + } + } +} diff --git a/BCnEncTests.Framework/Support/TestHelper.cs b/BCnEncTests.Framework/Support/TestHelper.cs new file mode 100644 index 0000000..24ad009 --- /dev/null +++ b/BCnEncTests.Framework/Support/TestHelper.cs @@ -0,0 +1,284 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; +using Xunit; +using Xunit.Abstractions; + +namespace BCnEncTests.Support +{ + public static class TestHelper + { + #region Assertions + + public static void AssertPixelsEqual(Span originalPixels, Span pixels, CompressionQuality quality, ITestOutputHelper output = null) + { + var psnr = ImageQuality.PeakSignalToNoiseRatio(originalPixels, pixels); + AssertPSNR(psnr, quality, output); + } + + public static void AssertPixelsEqual(Span originalPixels, Span pixels, CompressionQuality quality, ITestOutputHelper output = null) + { + var rmse = ImageQuality.CalculateLogRMSE(originalPixels, pixels); + AssertRMSE(rmse, quality, output); + } + + public static void AssertImagesEqual(Memory2D original, Memory2D image, CompressionQuality quality, bool countAlpha = true) + { + var psnr = CalculatePSNR(original, image, countAlpha); + AssertPSNR(psnr, quality); + } + + #endregion + + #region Execute methods + + public static void ExecuteDecodingTest(KtxFile file, string outputFile) + { + Assert.True(file.header.VerifyHeader()); + Assert.Equal((uint)1, file.header.NumberOfFaces); + + var decoder = new BcDecoder(); + var pixels = decoder.Decode2D(file); + + Assert.Equal((uint)pixels.Width, file.header.PixelWidth); + Assert.Equal((uint)pixels.Height, file.header.PixelHeight); + + using (var outFs = File.OpenWrite(outputFile)) + { + SaveAsPng(pixels, outFs); + } + } + + #region Dds + + public static void ExecuteDdsWritingTest(Memory2D image, CompressionFormat format, string outputFile) + { + ExecuteDdsWritingTest(new[] { image }, format, outputFile); + } + + public static void ExecuteDdsWritingTest(Memory2D[] images, CompressionFormat format, string outputFile) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + encoder.OutputOptions.FileFormat = OutputFileFormat.Dds; + + using (var fs = File.OpenWrite(outputFile)) + { + if (images.Length == 1) + { + encoder.EncodeToStream(images[0], fs); + } + else + { + encoder.EncodeCubeMapToStream(images[0], images[1], images[2], images[3], images[4], images[5], fs); + } + } + } + + public static void ExecuteDdsReadingTest(DdsFile file, DxgiFormat format, string outputFile, bool assertAlpha = false) + { + Assert.Equal(format, file.header.ddsPixelFormat.DxgiFormat); + Assert.Equal(file.header.dwMipMapCount, (uint)file.Faces[0].MipMaps.Length); + + var decoder = new BcDecoder(); + decoder.InputOptions.DdsBc1ExpectAlpha = assertAlpha; + var images = decoder.DecodeAllMipMaps2D(file); + + Assert.Equal((uint)images[0].Width, file.header.dwWidth); + Assert.Equal((uint)images[0].Height, file.header.dwHeight); + + for (var i = 0; i < images.Length; i++) + { + if (assertAlpha) + { + var pixels = GetSinglePixelArrayAsColors(images[0]); + Assert.Contains(pixels, x => x.a == 0); + } + + using (var outFs = File.OpenWrite(string.Format(outputFile, i))) + { + SaveAsPng(images[i], outFs); + } + } + } + + #endregion + + #region Cancellation + + public static async Task ExecuteCancellationTest(Memory2D image, bool isParallel) + { + var encoder = new BcEncoder(CompressionFormat.Bc7); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.Options.IsParallel = isParallel; + + var source = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); + await Assert.ThrowsAnyAsync(() => + encoder.EncodeToRawBytesAsync(image, source.Token)); + } + + #endregion + + #endregion + + public static float DecodeKtxCheckPSNR(string filename, Memory2D original) + { + using (var fs = File.OpenRead(filename)) + { + var ktx = KtxFile.Load(fs); + var decoder = new BcDecoder() + { + OutputOptions = { Bc4Component = ColorComponent.Luminance } + }; + var decoded = decoder.Decode2D(ktx); + + return CalculatePSNR(original, decoded); + } + } + + public static float DecodeKtxCheckRMSEHdr(string filename, HdrImage original) + { + using (var fs = File.OpenRead(filename)) + { + var ktx = KtxFile.Load(fs); + var decoder = new BcDecoder(); + + var decoded = decoder.DecodeHdr(ktx); + + return ImageQuality.CalculateLogRMSE(original.pixels, decoded); + } + } + + public static void ExecuteEncodingTest(Memory2D image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = quality; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + + var fs = File.OpenWrite(filename); + encoder.EncodeToStream(image, fs); + fs.Close(); + + var psnr = DecodeKtxCheckPSNR(filename, image); + output.WriteLine("RGBA PSNR: " + psnr + "db"); + AssertPSNR(psnr, encoder.OutputOptions.Quality); + } + + public static void ExecuteHdrEncodingTest(HdrImage image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = quality; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + + var fs = File.OpenWrite(filename); + encoder.EncodeToStreamHdr(new Memory2D(image.pixels, image.height, image.width), fs); + fs.Close(); + + var rmse = DecodeKtxCheckRMSEHdr(filename, image); + output.WriteLine("RGBFloat RMSE: " + rmse); + AssertRMSE(rmse, encoder.OutputOptions.Quality); + } + + private static float CalculatePSNR(Memory2D original, Memory2D decoded, bool countAlpha = true) + { + var pixels = GetSinglePixelArrayAsColors(original); + var pixels2 = GetSinglePixelArrayAsColors(decoded); + + return ImageQuality.PeakSignalToNoiseRatio(pixels, pixels2, countAlpha); + } + + public static void AssertPSNR(float psnr, CompressionQuality quality, ITestOutputHelper output = null) + { + output?.WriteLine($"PSNR: {psnr} , quality: {quality}"); + if (quality == CompressionQuality.Fast) + { + Assert.True(psnr > 25, $"PSNR was less than 25: {psnr} , quality: {quality}"); + } + else + { + Assert.True(psnr > 30, $"PSNR was less than 30: {psnr} , quality: {quality}"); + } + } + + public static void AssertRMSE(float rmse, CompressionQuality quality, ITestOutputHelper output = null) + { + output?.WriteLine($"RMSE: {rmse} , quality: {quality}"); + if (quality == CompressionQuality.Fast) + { + Assert.True(rmse < 0.1); + } + else + { + Assert.True(rmse < 0.04); + } + } + + public static ColorRgba32[] GetSinglePixelArrayAsColors(ReadOnlyMemory2D image) + { + var pixels = new ColorRgba32[image.Width * image.Height]; + var span = image.Span; + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + pixels[y * image.Width + x] = span[y, x]; + } + } + return pixels; + } + + public static unsafe void SaveAsPng(Memory2D image, Stream stream) + { + int width = image.Width; + int height = image.Height; + using (var bmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) + { + var data = bmp.LockBits(new Rectangle(0, 0, width, height), + ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + byte* ptr = (byte*)data.Scan0; + var span = image.Span; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + var c = span[y, x]; + ptr[0] = c.b; + ptr[1] = c.g; + ptr[2] = c.r; + ptr[3] = c.a; + ptr += 4; + } + } + bmp.UnlockBits(data); + bmp.Save(stream, ImageFormat.Png); + } + } + + public static void SaveAsPng(ColorRgbFloat[] pixels, int width, int height, Stream stream) + { + var rgba = new ColorRgba32[pixels.Length]; + for (var i = 0; i < pixels.Length; i++) + { + var p = pixels[i]; + byte r = (byte)(Math.Max(0, Math.Min(1, p.r)) * 255 + 0.5f); + byte g = (byte)(Math.Max(0, Math.Min(1, p.g)) * 255 + 0.5f); + byte b = (byte)(Math.Max(0, Math.Min(1, p.b)) * 255 + 0.5f); + rgba[i] = new ColorRgba32(r, g, b, 255); + } + var mem = new Memory2D(rgba, height, width); + SaveAsPng(mem, stream); + } + } +} diff --git a/BCnEncTests.Shared/AtcTests.cs b/BCnEncTests.Shared/AtcTests.cs new file mode 100644 index 0000000..eff342c --- /dev/null +++ b/BCnEncTests.Shared/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.Shared/BC1Tests.cs b/BCnEncTests.Shared/BC1Tests.cs new file mode 100644 index 0000000..314c284 --- /dev/null +++ b/BCnEncTests.Shared/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.Shared/BCnEncTests.Shared.projitems b/BCnEncTests.Shared/BCnEncTests.Shared.projitems new file mode 100644 index 0000000..a221845 --- /dev/null +++ b/BCnEncTests.Shared/BCnEncTests.Shared.projitems @@ -0,0 +1,34 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + D954291E-2A0B-460D-934E-DC6B0785DB48 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BCnEncTests.Shared/BCnEncTests.Shared.shproj b/BCnEncTests.Shared/BCnEncTests.Shared.shproj new file mode 100644 index 0000000..be4f3fb --- /dev/null +++ b/BCnEncTests.Shared/BCnEncTests.Shared.shproj @@ -0,0 +1,10 @@ + + + + {D954291E-2A0B-460D-934E-DC6B0785DB48} + 14.0 + + + + + diff --git a/BCnEncTests.Shared/Bc5Tests.cs b/BCnEncTests.Shared/Bc5Tests.cs new file mode 100644 index 0000000..4e361ef --- /dev/null +++ b/BCnEncTests.Shared/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.Shared/Bc6EncoderTests.cs b/BCnEncTests.Shared/Bc6EncoderTests.cs new file mode 100644 index 0000000..ced39c6 --- /dev/null +++ b/BCnEncTests.Shared/Bc6EncoderTests.cs @@ -0,0 +1,402 @@ +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(new Memory2D(image.pixels, 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.Shared/Bc6HDecoderTests.cs b/BCnEncTests.Shared/Bc6HDecoderTests.cs new file mode 100644 index 0000000..2cbb609 --- /dev/null +++ b/BCnEncTests.Shared/Bc6HDecoderTests.cs @@ -0,0 +1,173 @@ +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 width = (int)HdrLoader.TestHdrKiaraDds.header.dwWidth; + var height = (int)HdrLoader.TestHdrKiaraDds.header.dwHeight; + var hdr = new HdrImage(width, height); + + Assert.Equal(hdr.pixels.Length, decoded.Length); + + hdr.pixels = decoded; + using (var sfs = File.OpenWrite("decoding_test_dds_bc6h.hdr")) + { + hdr.Write(sfs); + } + + using (var pngFs = File.OpenWrite("decoding_test_dds_bc6h.png")) + { + TestHelper.SaveAsPng(decoded, width, height, pngFs); + } + + TestHelper.AssertPixelsEqual(HdrLoader.TestHdrKiara.pixels, decoded, CompressionQuality.Fast, output); + } + + [Fact] + public void DecodeKtx() + { + var decoder = new BcDecoder(); + var decoded = decoder.DecodeHdr(HdrLoader.TestHdrKiaraKtx); + + var width = (int)HdrLoader.TestHdrKiaraKtx.header.PixelWidth; + var height = (int)HdrLoader.TestHdrKiaraKtx.header.PixelHeight; + var hdr = new HdrImage(width, height); + + Assert.Equal(hdr.pixels.Length, decoded.Length); + + hdr.pixels = decoded; + using (var sfs = File.OpenWrite("decoding_test_ktx_bc6h.hdr")) + { + hdr.Write(sfs); + } + + using (var pngFs = File.OpenWrite("decoding_test_ktx_bc6h.png")) + { + TestHelper.SaveAsPng(decoded, width, height, pngFs); + } + + 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); + + Stream fs; +#if NETCOREAPP + fs = File.OpenRead("../../../testImages/test_hdr_kiara_dds_float16_data.bin"); +#else + fs = File.OpenRead("../../../../BCnEncTests/testImages/test_hdr_kiara_dds_float16_data.bin"); +#endif + using (fs) + using (var ms = new MemoryStream()) + { + 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/Bc7BlockTests.cs b/BCnEncTests.Shared/Bc7BlockTests.cs similarity index 58% rename from BCnEncTests/Bc7BlockTests.cs rename to BCnEncTests.Shared/Bc7BlockTests.cs index 8489102..8ac34ea 100644 --- a/BCnEncTests/Bc7BlockTests.cs +++ b/BCnEncTests.Shared/Bc7BlockTests.cs @@ -1,20 +1,103 @@ -using System; -using System.Collections.Generic; +using System; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Security.Cryptography.X509Certificates; -using System.Text; +using BCnEncoder.Decoder; using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using BCnEncTests.Support; using Xunit; +using CommunityToolkit.HighPerformance; + 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); + } - private void Type0Pack(Span output) { - byte[][] subsetEndpoints = new[] { + [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 = new Memory2D(pixels, 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[] { //subset 1 new byte[]{0b1111, 0, 0}, new byte[]{0b1000, 0, 0}, @@ -25,72 +108,66 @@ private void Type0Pack(Span output) { new byte[]{0, 0, 0b1111}, new byte[]{0, 0, 0b1000} }; - byte[] pBits = new byte[] { - 1, 0, 1, 0, 1, 1 - }; - byte[] indices = new byte[] { + 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 (int i = 0; i < 16; i++) { + 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 - }; + 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 (int i = 0; i < 16; i++) { + 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 - }; + 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 (int i = 0; i < 16; i++) { + 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 - }; + 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 (int i = 0; i < 16; i++) { + 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) { - byte[][] subsetEndpoints = new[] { + private void Type1Pack(Span output) + { + var subsetEndpoints = new[] { //subset 1 new byte[]{0b111111, 0b0100, 0}, new byte[]{0b0100, 0b111111, 0}, @@ -98,24 +175,24 @@ private void Type1Pack(Span output) { new byte[]{0, 0, 0b111111}, new byte[]{0, 0, 0b1010} }; - byte[] pBits = new byte[] { - 1, 1 - }; - byte[] indices = new byte[] { + 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 (int i = 0; i < 64; i++) { + 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) { - byte[][] subsetEndpoints = new[] { + private void Type2Pack(Span output) + { + var subsetEndpoints = new[] { //subset 1 new byte[]{0b11111, 0, 0}, new byte[]{0b01000, 0, 0}, @@ -126,21 +203,23 @@ private void Type2Pack(Span output) { new byte[]{0, 0, 0b11111}, new byte[]{0, 0, 0b01000} }; - byte[] indices = new byte[] { + var indices = new byte[] { 0, 1, 2, 3, 0, 1, 2, 3, 3, 2, 1, 0, 3, 2, 1, 0 }; - for (int i = 0; i < 64; i++) { + 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) { - byte[][] subsetEndpoints = new[] { + private void Type3Pack(Span output) + { + var subsetEndpoints = new[] { //subset 1 new byte[]{0b1111111, 0b10100, 0}, new byte[]{0b10100, 0b1111111, 0}, @@ -148,48 +227,45 @@ private void Type3Pack(Span output) { new byte[]{0, 0, 0b1111111}, new byte[]{0, 0, 0b11010} }; - byte[] pBits = new byte[] { - 1, 0, 0, 1 - }; - byte[] indices = new byte[] { + 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 (int i = 0; i < 64; i++) { + 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) { - byte[][] colorEndpoints = new[] { + private void Type4Pack(Span output) + { + var colorEndpoints = new[] { //subset 1 new byte[]{0b11111, 0, 0b0100}, new byte[]{0, 0b11111, 0b0100} }; - byte[] alphaEndPoints = new byte[]{ - 0b111111, - 0 - }; - byte[] indices2Bit = new byte[] { + 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 }; - - byte[] indices3Bit = new byte[] { + var indices3Bit = new byte[] { 0, 1, 2, 3, 7, 6, 5, 4, 7, 6, 5, 4, 3, 2, 1, 0 }; - int rotation = 0; + var rotation = 0; byte idxMode = 0; - for (int i = 0; i < 64; i++) { + 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); @@ -199,30 +275,29 @@ private void Type4Pack(Span output) { } } - private void Type5Pack(Span output) { - byte[][] colorEndpoints = new[] { + private void Type5Pack(Span output) + { + var colorEndpoints = new[] { //subset 1 - new byte[]{0b1111111, 0b0100, 0}, + new byte[]{0b1111111, 0b0100, 0}, new byte[]{0, 0, 0b1111111} }; - byte[] alphaEndPoints = new byte[]{ - 255, - 100 - }; - byte[] colorIndices = new byte[] { + 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 }; - byte[] alphaIndices = new byte[] { + var alphaIndices = new byte[] { 1, 2, 3, 0, 0, 1, 2, 3, 2, 3, 0, 1, 3, 2, 1, 0 }; - int rotation = 0; - for (int i = 0; i < 64; i++) { + 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); @@ -230,115 +305,62 @@ private void Type5Pack(Span output) { } } - private void Type6Pack(Span output) { - byte[][] colorEndpoints = new[] { + private void Type6Pack(Span output) + { + var colorEndpoints = new[] { //subset 1 - new byte[]{0b1111111, 0b0100, 0, 0b1111111}, + new byte[]{0b1111111, 0b0100, 0, 0b1111111}, new byte[]{0, 0, 0b1111111, 0b1111} }; - byte[] pBits = new byte[] { - 1, 0 - }; - byte[] colorIndices = new byte[] { + 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 (int i = 0; i < 64; i++) { + 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) { - byte[][] colorEndpoints = new[] { + private void Type7Pack(Span output) + { + var colorEndpoints = new[] { //subset 1 - new byte[]{0b11111, 0b0100, 0, 0b11111}, + new byte[]{0b11111, 0b0100, 0, 0b11111}, new byte[]{0, 0b11111, 0, 0b111}, //subset 2 - new byte[]{0b11111, 0b0100, 0, 0b1111}, + new byte[]{0b11111, 0b0100, 0, 0b1111}, new byte[]{0b10100, 0, 0b11111, 0b11111} }; - byte[] pBits = new byte[] { - 1, 0, 1, 0 - }; - byte[] indices = new byte[] { + 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 (int i = 0; i < 64; i++) { + 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 (int i = 0; i < 8; i++) { - int xStart =((x * 8) + (y * 8 + i) * 8 * destBlockWidth); + 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)); } } - - [Fact] - public void PackTypes() { - int numWidthBlocks = 4; - int numHeightBlocks = 2; - Bc7Block[] outputBlocks = new Bc7Block[64 * numWidthBlocks * numHeightBlocks]; - byte[] encoded = new byte[64 * numWidthBlocks * numHeightBlocks * Unsafe.SizeOf()]; - - KtxFile output = new KtxFile( - KtxHeader.InitializeCompressed(numWidthBlocks * 8 * 4, numHeightBlocks * 8 * 4, - GlInternalFormat.GL_COMPRESSED_RGBA_BPTC_UNORM_ARB, - GLFormat.GL_RGBA)); - - - Span type0 = new Span(outputBlocks, 0 , 64); - Type0Pack(type0); - PlaceBlock(0, 0, type0, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - Span type1 = new Span(outputBlocks, 64 * 1 , 64); - Type1Pack(type1); - PlaceBlock(1, 0, type1, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - Span type2 = new Span(outputBlocks, 64 * 2 , 64); - Type2Pack(type2); - PlaceBlock(2, 0, type2, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - Span type3 = new Span(outputBlocks, 64 * 3 , 64); - Type3Pack(type3); - PlaceBlock(3, 0, type3, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - Span type4 = new Span(outputBlocks, 64 * 4 , 64); - Type4Pack(type4); - PlaceBlock(0, 1, type4, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - Span type5 = new Span(outputBlocks, 64 * 5 , 64); - Type5Pack(type5); - PlaceBlock(1, 1, type5, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - Span type6 = new Span(outputBlocks, 64 * 6 , 64); - Type6Pack(type6); - PlaceBlock(2, 1, type6, MemoryMarshal.Cast(new Span(encoded)), numWidthBlocks); - - Span 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)); - - FileStream fs = File.OpenWrite("bc7_blocktests.ktx"); - output.Write(fs); - } + #endregion } } diff --git a/BCnEncTests.Shared/BgraTests.cs b/BCnEncTests.Shared/BgraTests.cs new file mode 100644 index 0000000..6f0e8bf --- /dev/null +++ b/BCnEncTests.Shared/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.Shared/BlockTests.cs b/BCnEncTests.Shared/BlockTests.cs new file mode 100644 index 0000000..14d6621 --- /dev/null +++ b/BCnEncTests.Shared/BlockTests.cs @@ -0,0 +1,117 @@ +using System; +using BCnEncoder.Shared; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class BlockTests + { + [Fact] + public void CreateBlocksExact() + { + var testImage = new ColorRgba32[16, 16]; + + var blocks = ImageToBlocks.ImageTo4X4(testImage, out var blocksWidth, out var blocksHeight); + + Assert.Equal(16, blocks.Length); + Assert.Equal(4, blocksWidth); + Assert.Equal(4, blocksHeight); + } + + [Fact] + public void CreateBlocksPadding() + { + var testImage = new ColorRgba32[15, 11]; + + var blocks = ImageToBlocks.ImageTo4X4(testImage, out var blocksWidth, out var blocksHeight); + + Assert.Equal(12, blocks.Length); + Assert.Equal(3, blocksWidth); + Assert.Equal(4, blocksHeight); + } + + [Fact] + public void PaddingColor() + { + var testImage = new ColorRgba32[15, 11]; + + 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); + + Assert.Equal(12, blocks.Length); + Assert.Equal(3, blocksWidth); + Assert.Equal(4, blocksHeight); + + for (var x = 0; x < blocksWidth; x++) { + for (var y = 0; y < blocksHeight; y++) { + foreach (var color in blocks[x + y * blocksWidth].AsSpan) { + Assert.Equal(new ColorRgba32(0, 125, 125), color); + } + } + } + } + + [Fact] + public void BlocksToImage() + { + var r = new Random(0); + var testImage = new ColorRgba32[16, 16]; + + 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); + + Assert.Equal(16, blocks.Length); + Assert.Equal(4, blocksWidth); + Assert.Equal(4, blocksHeight); + + var output = ImageToBlocks.ColorsFromRawBlocks(blocks, 16, 16); + + var pixels2 = output.AsSpan(); + + 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]); + } + } + } + + [Fact] + public void BlockError() + { + var testImage = new ColorRgba32[16, 16]; + + var blocks = ImageToBlocks.ImageTo4X4(testImage, out var blocksWidth, out var blocksHeight); + + var block1 = blocks[2 + 2 * blocksWidth]; + var block2 = blocks[2 + 2 * blocksWidth]; + + Assert.Equal(0, block1.CalculateError(block2)); + + for (var i = 0; i < block2.AsSpan.Length; i++) { + block2.AsSpan[i].r = (byte) (block2.AsSpan[i].r + 2); + } + Assert.Equal(2, block1.CalculateError(block2)); + + for (var i = 0; i < block2.AsSpan.Length; i++) { + block2.AsSpan[i].g = (byte) (block2.AsSpan[i].r + 20); + } + Assert.Equal(22, block1.CalculateError(block2)); + } + } +} diff --git a/BCnEncTests.Shared/CancellationTests.cs b/BCnEncTests.Shared/CancellationTests.cs new file mode 100644 index 0000000..9932c6c --- /dev/null +++ b/BCnEncTests.Shared/CancellationTests.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using BCnEncTests.Support; +using Xunit; + +namespace BCnEncTests +{ + public class CancellationTests + { + [Fact] + public async Task EncodeParallelCancellation() + { + await TestHelper.ExecuteCancellationTest(ImageLoader.TestAlphaGradient1, true); + } + + [Fact] + public async Task EncodeNonParallelCancellation() + { + await TestHelper.ExecuteCancellationTest(ImageLoader.TestAlphaGradient1, false); + } + } +} diff --git a/BCnEncTests.Shared/ClusterTests.cs b/BCnEncTests.Shared/ClusterTests.cs new file mode 100644 index 0000000..a30ab52 --- /dev/null +++ b/BCnEncTests.Shared/ClusterTests.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class ClusterTests + { + [Fact] + public void Clusterize() + { + var original = ImageLoader.TestBlur1; + int height = original.Height; + int width = original.Width; + + // Copy pixels from the source image + var pixels = new ColorRgba32[height * width]; + var srcSpan = original.Span; + for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) + pixels[y * width + x] = srcSpan[y, x]; + + var numClusters = (width / 32) * (height / 32); + var clusters = LinearClustering.ClusterPixels(pixels, width, height, numClusters, 10, 10); + + var pixC = new ColorYCbCr[numClusters]; + var counts = new int[numClusters]; + + for (var i = 0; i < pixels.Length; i++) + { + pixC[clusters[i]] += new ColorYCbCr(pixels[i]); + counts[clusters[i]]++; + } + + for (var i = 0; i < numClusters; i++) + { + pixC[i] /= counts[i]; + } + + for (var i = 0; i < pixels.Length; i++) + { + pixels[i] = pixC[clusters[i]].ToColorRgba32(); + } + + var result = new Memory2D(pixels, height, width); + using (var fs = File.OpenWrite("test_cluster.png")) + { + TestHelper.SaveAsPng(result, fs); + } + } + } +} diff --git a/BCnEncTests/ColorTest.cs b/BCnEncTests.Shared/ColorTest.cs similarity index 86% rename from BCnEncTests/ColorTest.cs rename to BCnEncTests.Shared/ColorTest.cs index e8f8be5..eefa12f 100644 --- a/BCnEncTests/ColorTest.cs +++ b/BCnEncTests.Shared/ColorTest.cs @@ -1,4 +1,4 @@ -using BCnEncoder.Shared; +using BCnEncoder.Shared; using Xunit; namespace BCnEncTests @@ -7,8 +7,9 @@ public class ColorTest { [Fact] - public void Rgb565Test() { - ColorRgb565 color = new ColorRgb565(255,255,255); + public void Rgb565Test() + { + var color = new ColorRgb565(255, 255, 255); Assert.Equal(255, color.R); Assert.Equal(255, color.G); @@ -29,7 +30,7 @@ public void Rgb565Test() { Assert.Equal(0, color.G); Assert.Equal(0, color.B); - color = new ColorRgb565(255,255,255); + color = new ColorRgb565(255, 255, 255); color.B = 0; Assert.Equal(255, color.R); diff --git a/BCnEncTests.Shared/DdsReadTests.cs b/BCnEncTests.Shared/DdsReadTests.cs new file mode 100644 index 0000000..3c2a35e --- /dev/null +++ b/BCnEncTests.Shared/DdsReadTests.cs @@ -0,0 +1,54 @@ +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.Shared/DdsWritingTests.cs b/BCnEncTests.Shared/DdsWritingTests.cs new file mode 100644 index 0000000..44a0616 --- /dev/null +++ b/BCnEncTests.Shared/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.Shared/DecodingAsyncTests.cs b/BCnEncTests.Shared/DecodingAsyncTests.cs new file mode 100644 index 0000000..cba775b --- /dev/null +++ b/BCnEncTests.Shared/DecodingAsyncTests.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class 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 new Memory2D(pixels, mipHeight, mipWidth); + }); + + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + } +} diff --git a/BCnEncTests.Shared/DecodingTests.cs b/BCnEncTests.Shared/DecodingTests.cs new file mode 100644 index 0000000..c2fc08c --- /dev/null +++ b/BCnEncTests.Shared/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.Shared/EncoderOptionsTests.cs b/BCnEncTests.Shared/EncoderOptionsTests.cs new file mode 100644 index 0000000..f73ecd3 --- /dev/null +++ b/BCnEncTests.Shared/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.Shared/EncodingAsyncTest.cs b/BCnEncTests.Shared/EncodingAsyncTest.cs new file mode 100644 index 0000000..88cfb3f --- /dev/null +++ b/BCnEncTests.Shared/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 = new Memory2D(rawPixels, (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 = 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); + } + } + + [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 = new Memory2D(rawPixels, originalImage.Height, originalImage.Width); + + TestHelper.AssertImagesEqual(originalImage, image, encoder.OutputOptions.Quality); + } + } +} diff --git a/BCnEncTests.Shared/EncodingTest.cs b/BCnEncTests.Shared/EncodingTest.cs new file mode 100644 index 0000000..e4f3edd --- /dev/null +++ b/BCnEncTests.Shared/EncodingTest.cs @@ -0,0 +1,317 @@ +using System.IO; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; +using Xunit.Abstractions; + +namespace BCnEncTests +{ + public class Bc1GradientTest + { + private readonly ITestOutputHelper output; + + public Bc1GradientTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc1GradientBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestGradient1, CompressionFormat.Bc1, CompressionQuality.BestQuality, "encoding_bc1_gradient_bestQuality.ktx", output); + } + + [Fact] + public void Bc1GradientBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestGradient1, CompressionFormat.Bc1, CompressionQuality.Balanced, "encoding_bc1_gradient_balanced.ktx", output); + } + + [Fact] + public void Bc1GradientFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestGradient1, CompressionFormat.Bc1, CompressionQuality.Fast, "encoding_bc1_gradient_fast.ktx", output); + } + } + + public class Bc1DiffuseTest + { + private readonly ITestOutputHelper output; + + public Bc1DiffuseTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc1DiffuseBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestDiffuse1, CompressionFormat.Bc1, CompressionQuality.BestQuality, "encoding_bc1_diffuse_bestQuality.ktx", output); + } + + [Fact] + public void Bc1DiffuseBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestDiffuse1, CompressionFormat.Bc1, CompressionQuality.Balanced, "encoding_bc1_diffuse_balanced.ktx", output); + } + + [Fact] + public void Bc1DiffuseFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestDiffuse1, CompressionFormat.Bc1, CompressionQuality.Fast, "encoding_bc1_diffuse_fast.ktx", output); + } + } + + public class Bc1BlurryTest + { + private readonly ITestOutputHelper output; + + public Bc1BlurryTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc1BlurBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestBlur1, CompressionFormat.Bc1, CompressionQuality.BestQuality, "encoding_bc1_blur_bestQuality.ktx", output); + } + + [Fact] + public void Bc1BlurBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestBlur1, CompressionFormat.Bc1, CompressionQuality.Balanced, "encoding_bc1_blur_balanced.ktx", output); + } + + [Fact] + public void Bc1BlurFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestBlur1, CompressionFormat.Bc1, CompressionQuality.Fast, "encoding_bc1_blur_fast.ktx", output); + } + } + + public class Bc1ASpriteTest + { + private readonly ITestOutputHelper output; + + public Bc1ASpriteTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc1ASpriteBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestTransparentSprite1, CompressionFormat.Bc1WithAlpha, CompressionQuality.BestQuality, "encoding_bc1a_sprite_bestQuality.ktx", output); + } + + [Fact] + public void Bc1ASpriteBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestTransparentSprite1, CompressionFormat.Bc1WithAlpha, CompressionQuality.Balanced, "encoding_bc1a_sprite_balanced.ktx", output); + } + + [Fact] + public void Bc1ASpriteFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestTransparentSprite1, CompressionFormat.Bc1WithAlpha, CompressionQuality.Fast, "encoding_bc1a_sprite_fast.ktx", output); + } + } + + public class Bc2GradientTest + { + private readonly ITestOutputHelper output; + + public Bc2GradientTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc2GradientBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc2, CompressionQuality.BestQuality, "encoding_bc2_gradient_bestQuality.ktx", output); + } + + [Fact] + public void Bc2GradientBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc2, CompressionQuality.Balanced, "encoding_bc2_gradient_balanced.ktx", output); + } + + [Fact] + public void Bc2GradientFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc2, CompressionQuality.Fast, "encoding_bc2_gradient_fast.ktx", output); + } + } + + public class Bc3GradientTest + { + private readonly ITestOutputHelper output; + + public Bc3GradientTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc3GradientBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc3, CompressionQuality.BestQuality, "encoding_bc3_gradient_bestQuality.ktx", output); + } + + [Fact] + public void Bc3GradientBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc3, CompressionQuality.Balanced, "encoding_bc3_gradient_balanced.ktx", output); + } + + [Fact] + public void Bc3GradientFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlphaGradient1, CompressionFormat.Bc3, CompressionQuality.Fast, "encoding_bc3_gradient_fast.ktx", output); + } + } + + public class Bc4RedTest + { + private readonly ITestOutputHelper output; + + public Bc4RedTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc4RedBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestHeight1, CompressionFormat.Bc4, CompressionQuality.BestQuality, "encoding_bc4_red_bestQuality.ktx", output); + } + + [Fact] + public void Bc4RedBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestHeight1, CompressionFormat.Bc4, CompressionQuality.Balanced, "encoding_bc4_red_balanced.ktx", output); + } + + [Fact] + public void Bc4RedFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestHeight1, CompressionFormat.Bc4, CompressionQuality.Fast, "encoding_bc4_red_fast.ktx", output); + } + } + + public class Bc5RedGreenTest + { + private readonly ITestOutputHelper output; + + public Bc5RedGreenTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc5RedGreenBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRedGreen1, CompressionFormat.Bc5, CompressionQuality.BestQuality, "encoding_bc5_red_green_bestQuality.ktx", output); + } + + [Fact] + public void Bc5RedGreenBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRedGreen1, CompressionFormat.Bc5, CompressionQuality.Balanced, "encoding_bc5_red_green_balanced.ktx", output); + } + + [Fact] + public void Bc5RedGreenFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRedGreen1, CompressionFormat.Bc5, CompressionQuality.Fast, "encoding_bc5_red_green_fast.ktx", output); + } + } + + public class Bc7RgbTest + { + private readonly ITestOutputHelper output; + + public Bc7RgbTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc7RgbBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRgbHard1, CompressionFormat.Bc7, CompressionQuality.BestQuality, "encoding_bc7_rgb_bestQuality.ktx", output); + } + + [Fact] + public void Bc7RgbBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRgbHard1, CompressionFormat.Bc7, CompressionQuality.Balanced, "encoding_bc7_rgb_balanced.ktx", output); + } + + [Fact] + public void Bc7LennaBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestLenna, CompressionFormat.Bc7, CompressionQuality.Balanced, "encoding_bc7_lenna_balanced.ktx", output); + } + + [Fact] + public void Bc7RgbFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestRgbHard1, CompressionFormat.Bc7, CompressionQuality.Fast, "encoding_bc7_rgb_fast.ktx", output); + } + } + + public class Bc7RgbaTest + { + private readonly ITestOutputHelper output; + + public Bc7RgbaTest(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void Bc7RgbaBestQuality() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlpha1, CompressionFormat.Bc7, CompressionQuality.BestQuality, "encoding_bc7_rgba_bestQuality.ktx", output); + } + + [Fact] + public void Bc7RgbaBalanced() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlpha1, CompressionFormat.Bc7, CompressionQuality.Balanced, "encoding_bc7_rgba_balanced.ktx", output); + } + + [Fact] + public void Bc7RgbaFast() + { + TestHelper.ExecuteEncodingTest(ImageLoader.TestAlpha1, CompressionFormat.Bc7, CompressionQuality.Fast, "encoding_bc7_rgba_fast.ktx", output); + } + } + + public class CubemapTest + { + [Fact] + public void WriteCubeMapFile() + { + var images = ImageLoader.TestCubemap; + + var filename = "encoding_bc1_cubemap.ktx"; + + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = CompressionFormat.Bc1; + + using (var fs = File.OpenWrite(filename)) + { + encoder.EncodeCubeMapToStream(images[0], images[1], images[2], images[3], images[4], images[5], fs); + } + } + } +} diff --git a/BCnEncTests.Shared/IntHelperTests.cs b/BCnEncTests.Shared/IntHelperTests.cs new file mode 100644 index 0000000..a8e2e49 --- /dev/null +++ b/BCnEncTests.Shared/IntHelperTests.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using BCnEncoder.Shared; +using Xunit; + +namespace BCnEncTests +{ + public class IntHelperTests + { + + [Theory] + [InlineData(0, 2, 0)] + [InlineData(0b0111, 4, 0b0111)] + [InlineData(0b1111, 4, -1)] + [InlineData(0b1111_1110, 8, -2)] + [InlineData(0b11110011, 8, unchecked((int)0b1111_1111_1111_1111_1111_1111_1111_0011))] + [InlineData(0b10100011, 8, unchecked((int)0b1111_1111_1111_1111_1111_1111_1010_0011))] + [InlineData(0b10100011, 7, 0b010_0011)] + public void SignExtend(int orig, int precision, int expected) + { + var result = IntHelper.SignExtend(orig, precision); + Assert.Equal(expected, result); + } + } +} diff --git a/BCnEncTests.Shared/MathHelperTests.cs b/BCnEncTests.Shared/MathHelperTests.cs new file mode 100644 index 0000000..0aeac1c --- /dev/null +++ b/BCnEncTests.Shared/MathHelperTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text; +using BCnEncoder.Shared; +using Xunit; + +namespace BCnEncTests +{ + public class MathHelperTests + { + [Theory] + [InlineData(16.4, 0.512500, 5)] + public void FrExp(double val, double expectedMantissa, int expectedExponent) + { + int exp; + var mantissa = MathHelper.FrExp(val, out exp); + Assert.Equal(expectedExponent, exp); + Assert.Equal(expectedMantissa, mantissa, 5); + } + + [Theory] + [InlineData(16.4f, 0.512500f, 5)] + public void FrExpFloat(float val, float expectedMantissa, int expectedExponent) + { + var mantissa = (float)MathHelper.FrExp(val, out var exp); + Assert.Equal(expectedExponent, exp); + Assert.Equal(expectedMantissa, mantissa, 5); + } + + [Theory] + [InlineData(7f, -4, 0.437500)] + public void LdExpFloat(float val, int exponent, float expectedValue) + { + var value = (float)MathHelper.LdExp(val, exponent); + Assert.Equal(expectedValue, value); + } + } +} diff --git a/BCnEncTests/PcaTests.cs b/BCnEncTests.Shared/PcaTests.cs similarity index 89% rename from BCnEncTests/PcaTests.cs rename to BCnEncTests.Shared/PcaTests.cs index a3b500a..9c2e66d 100644 --- a/BCnEncTests/PcaTests.cs +++ b/BCnEncTests.Shared/PcaTests.cs @@ -1,16 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System; using BCnEncoder.Shared; using Xunit; using Vector4 = System.Numerics.Vector4; -namespace BCnEncTests { - public class PcaTests { - +namespace BCnEncTests +{ + public class PcaTests + { [Fact] - public void PrincipalAxisTest() { - Vector4[] testData = new[] { + public void PrincipalAxisTest() + { + var testData = new[] { new Vector4(0.4f, 0.3f, 0.5f, 0.1f), new Vector4(0.3f, 0.3f, 0.5f, 0.3f), new Vector4(0.5f, 0.3f, 0.6f, 0.2f), @@ -18,7 +18,7 @@ public void PrincipalAxisTest() { new Vector4(0.456f, 0.34f, 0.23f, 0.45f) }; - var refMean = new[] { 0.3712, 0.308, 0.48600000000000004, 0.25}; + var refMean = new[] { 0.3712, 0.308, 0.48600000000000004, 0.25 }; var refCovar = new[] { new[] {0.014747200000000002, 0.00084800000000000088, -0.0067839999999999992, 0.0028000000000000004}, new[] {0.00084800000000000088, 0.00032000000000000062, -0.0025600000000000024, 0.0020000000000000018}, @@ -26,11 +26,10 @@ public void PrincipalAxisTest() { new[] {0.0028000000000000004, 0.0020000000000000018, -0.015999999999999997, 0.0174999} }; - var covarianceMatrix = PcaVectors.CalculateCovariance(testData, out Vector4 mean); + var covarianceMatrix = PcaVectors.CalculateCovariance(testData, out var mean); var pa = PcaVectors.CalculatePrincipalAxis(covarianceMatrix); - var refPa = new double[4] - {0.282, 0.087, -0.744, 0.603}; + var refPa = new[] { 0.282, 0.087, -0.744, 0.603 }; Assert.Equal(refMean[0], mean.X, 2); Assert.Equal(refMean[1], mean.Y, 2); @@ -62,6 +61,5 @@ public void PrincipalAxisTest() { Assert.True(Math.Abs(pa.Z - refPa[2]) < 0.1f, $"actual: {pa.Z} expected: {refPa[2]}"); Assert.True(Math.Abs(pa.W - refPa[3]) < 0.1f, $"actual: {pa.W} expected: {refPa[3]}"); } - } } diff --git a/BCnEncTests.Shared/ProgressTests.cs b/BCnEncTests.Shared/ProgressTests.cs new file mode 100644 index 0000000..83dab93 --- /dev/null +++ b/BCnEncTests.Shared/ProgressTests.cs @@ -0,0 +1,392 @@ +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.Shared/RawTests.cs b/BCnEncTests.Shared/RawTests.cs new file mode 100644 index 0000000..ebd71e2 --- /dev/null +++ b/BCnEncTests.Shared/RawTests.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using Xunit; + +namespace BCnEncTests +{ + public class RawTests + { + [Fact] + public void EncodeDecode() + { + var inputImage = ImageLoader.TestGradient1; + var decoder = new BcDecoder(); + var encoder = new BcEncoder + { + OutputOptions = { Quality = CompressionQuality.BestQuality } + }; + + var encodedRawBytes = encoder.EncodeToRawBytes(inputImage); + var decodedPixels = decoder.DecodeRaw(encodedRawBytes[0], inputImage.Width, inputImage.Height, CompressionFormat.Bc1); + var decodedImage = new Memory2D(decodedPixels, inputImage.Height, inputImage.Width); + + var originalColors = TestHelper.GetSinglePixelArrayAsColors(inputImage); + var decodedColors = TestHelper.GetSinglePixelArrayAsColors(decodedImage); + + TestHelper.AssertPixelsEqual(originalColors, decodedColors, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeDecodeStream() + { + var inputImage = ImageLoader.TestGradient1; + var decoder = new BcDecoder(); + var encoder = new BcEncoder + { + OutputOptions = { Quality = CompressionQuality.BestQuality } + }; + + var encodedRawBytes = encoder.EncodeToRawBytes(inputImage); + + using (var ms = new MemoryStream(encodedRawBytes[0])) + { + Assert.Equal(0, ms.Position); + + var rawPixels = decoder.DecodeRaw(ms, inputImage.Width, inputImage.Height, CompressionFormat.Bc1); + var decodedImage = new Memory2D(rawPixels, inputImage.Height, inputImage.Width); + + var originalColors = TestHelper.GetSinglePixelArrayAsColors(inputImage); + var decodedColors = TestHelper.GetSinglePixelArrayAsColors(decodedImage); + + TestHelper.AssertPixelsEqual(originalColors, decodedColors, encoder.OutputOptions.Quality); + } + } + + [Fact] + public void EncodeDecodeAllMipMapsStream() + { + var inputImage = ImageLoader.TestGradient1; + var decoder = new BcDecoder(); + var encoder = new BcEncoder + { + OutputOptions = + { + Quality = CompressionQuality.BestQuality, + GenerateMipMaps = true, + MaxMipMapLevel = 0 + } + }; + + using (var ms = new MemoryStream()) + { + var encodedRawBytes = encoder.EncodeToRawBytes(inputImage); + + var mipLevels = encoder.CalculateNumberOfMipLevels(inputImage.Width, inputImage.Height); + Assert.True(mipLevels > 1); + + for (var i = 0; i < mipLevels; i++) + { + ms.Write(encodedRawBytes[i], 0, encodedRawBytes[i].Length); + } + + ms.Position = 0; + Assert.Equal(0, ms.Position); + var resized = ResizeImageToMips(inputImage); + + + for (var i = 0; i < mipLevels; i++) + { + encoder.CalculateMipMapSize(inputImage.Width, inputImage.Height, i, out var mipWidth, out var mipHeight); + + var blockSize = decoder.GetBlockSize(CompressionFormat.Bc1); + var blockCount = decoder.GetBlockCount(mipWidth, mipHeight); + var buffer = new byte[blockSize * blockCount]; + ms.Read(buffer, 0, buffer.Length); + + var rawPixels = decoder.DecodeRaw(buffer, mipWidth, mipHeight, CompressionFormat.Bc1); + var decodedImage = new Memory2D(rawPixels, mipHeight, mipWidth); + + var originalColors = TestHelper.GetSinglePixelArrayAsColors(resized[i]); + var decodedColors = TestHelper.GetSinglePixelArrayAsColors(decodedImage); + + TestHelper.AssertPixelsEqual(originalColors, decodedColors, encoder.OutputOptions.Quality); + } + + encoder.CalculateMipMapSize(inputImage.Width, inputImage.Height, mipLevels - 1, out var lastMWidth, out var lastMHeight); + Assert.Equal(1, lastMWidth); + Assert.Equal(1, lastMHeight); + } + } + + private static ReadOnlyMemory2D[] ResizeImageToMips(Memory2D image) + { + int numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + return chain; + } + } +} diff --git a/BCnEncTests.Shared/SingleBlockTests.cs b/BCnEncTests.Shared/SingleBlockTests.cs new file mode 100644 index 0000000..74691ed --- /dev/null +++ b/BCnEncTests.Shared/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/BC1Tests.cs b/BCnEncTests/BC1Tests.cs deleted file mode 100644 index 189f9ac..0000000 --- a/BCnEncTests/BC1Tests.cs +++ /dev/null @@ -1,158 +0,0 @@ -using BCnEncoder.Shared; -using SixLabors.ImageSharp.PixelFormats; -using Xunit; - -namespace BCnEncTests -{ - public class BC1Tests - { - - [Fact] - public void Decode() { - Bc1Block block = new Bc1Block(); - block.color0 = new ColorRgb565(255, 255, 255); - block.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(Rgba32.Black, raw.p00); - Assert.Equal(Rgba32.Black, raw.p10); - Assert.Equal(Rgba32.Black, raw.p20); - Assert.Equal(Rgba32.Black, raw.p30); - - Assert.Equal(new Rgba32(85, 85, 85), raw.p01); - Assert.Equal(new Rgba32(85, 85, 85), raw.p11); - Assert.Equal(new Rgba32(85, 85, 85), raw.p21); - Assert.Equal(new Rgba32(85, 85, 85), raw.p31); - - Assert.Equal(new Rgba32(170, 170, 170), raw.p02); - Assert.Equal(new Rgba32(170, 170, 170), raw.p12); - Assert.Equal(new Rgba32(170, 170, 170), raw.p22); - Assert.Equal(new Rgba32(170, 170, 170), raw.p32); - - Assert.Equal(Rgba32.White, raw.p03); - Assert.Equal(Rgba32.White, raw.p13); - Assert.Equal(Rgba32.White, raw.p23); - Assert.Equal(Rgba32.White, raw.p33); - } - - [Fact] - public void DecodeBlack() { - Bc1Block block = new Bc1Block(); - block.color0 = new ColorRgb565(200, 200, 200); - block.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 Rgba32(206, 203, 206), raw.p00); - Assert.Equal(new Rgba32(206, 203, 206), raw.p10); - Assert.Equal(new Rgba32(206, 203, 206), raw.p20); - Assert.Equal(new Rgba32(206, 203, 206), raw.p30); - - Assert.Equal(Rgba32.Black, raw.p01); - Assert.Equal(Rgba32.Black, raw.p11); - Assert.Equal(Rgba32.Black, raw.p21); - Assert.Equal(Rgba32.Black, raw.p31); - - Assert.Equal(new Rgba32(230, 228, 230), raw.p02); - Assert.Equal(new Rgba32(230, 228, 230), raw.p12); - Assert.Equal(new Rgba32(230, 228, 230), raw.p22); - Assert.Equal(new Rgba32(230, 228, 230), raw.p32); - - Assert.Equal(Rgba32.White, raw.p03); - Assert.Equal(Rgba32.White, raw.p13); - Assert.Equal(Rgba32.White, raw.p23); - Assert.Equal(Rgba32.White, raw.p33); - } - - [Fact] - public void DecodeAlpha() { - Bc1Block block = new Bc1Block(); - block.color0 = new ColorRgb565(200, 200, 200); - block.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 Rgba32(206, 203, 206), raw.p00); - Assert.Equal(new Rgba32(206, 203, 206), raw.p10); - Assert.Equal(new Rgba32(206, 203, 206), raw.p20); - Assert.Equal(new Rgba32(206, 203, 206), raw.p30); - - Assert.Equal(new Rgba32(0,0,0,0), raw.p01); - Assert.Equal(new Rgba32(0,0,0,0), raw.p11); - Assert.Equal(new Rgba32(0,0,0,0), raw.p21); - Assert.Equal(new Rgba32(0,0,0,0), raw.p31); - - Assert.Equal(new Rgba32(230, 228, 230), raw.p02); - Assert.Equal(new Rgba32(230, 228, 230), raw.p12); - Assert.Equal(new Rgba32(230, 228, 230), raw.p22); - Assert.Equal(new Rgba32(230, 228, 230), raw.p32); - - Assert.Equal(Rgba32.White, raw.p03); - Assert.Equal(Rgba32.White, raw.p13); - Assert.Equal(Rgba32.White, raw.p23); - Assert.Equal(Rgba32.White, raw.p33); - } - - } -} diff --git a/BCnEncTests/BCnEncTests.csproj b/BCnEncTests/BCnEncTests.csproj index 9770be3..74aa454 100644 --- a/BCnEncTests/BCnEncTests.csproj +++ b/BCnEncTests/BCnEncTests.csproj @@ -1,20 +1,30 @@  - netcoreapp3.1 + net9.0 false - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/BCnEncTests/BlockTests.cs b/BCnEncTests/BlockTests.cs deleted file mode 100644 index 7cfd6e1..0000000 --- a/BCnEncTests/BlockTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using BCnEncoder.Shared; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using Xunit; - -namespace BCnEncTests -{ - public class BlockTests - { - [Fact] - public void CreateBlocksExact() - { - using Image testImage = new Image(16, 16); - - var blocks = ImageToBlocks.ImageTo4X4(testImage.Frames[0], out var blocksWidth, out var blocksHeight); - - Assert.Equal(16, blocks.Length); - Assert.Equal(4, blocksWidth); - Assert.Equal(4, blocksHeight); - } - - [Fact] - public void CreateBlocksPadding() - { - using Image testImage = new Image(11, 15); - - var blocks = ImageToBlocks.ImageTo4X4(testImage.Frames[0], out var blocksWidth, out var blocksHeight); - - Assert.Equal(12, blocks.Length); - Assert.Equal(3, blocksWidth); - Assert.Equal(4, blocksHeight); - } - - [Fact] - public void PaddingColor() - { - using Image testImage = new Image(13, 13); - - var pixels = testImage.GetPixelSpan(); - for (int i = 0; i < pixels.Length; i++) { - pixels[i] = Rgba32.Aquamarine; - } - - var blocks = ImageToBlocks.ImageTo4X4(testImage.Frames[0], out var blocksWidth, out var blocksHeight); - - Assert.Equal(16, blocks.Length); - Assert.Equal(4, blocksWidth); - Assert.Equal(4, blocksHeight); - - for (int x = 0; x < blocksWidth; x++) { - for (int y = 0; y < blocksHeight; y++) { - foreach (var color in blocks[x + y * blocksWidth].AsSpan) { - Assert.Equal(Rgba32.Aquamarine, color); - } - } - } - } - - [Fact] - public void BlocksToImage() - { - Random r = new Random(0); - using Image testImage = new Image(16, 16); - - var pixels = testImage.GetPixelSpan(); - for (int i = 0; i < pixels.Length; i++) { - pixels[i] = new Rgba32( - (byte)r.Next(255), - (byte)r.Next(255), - (byte)r.Next(255), - (byte)r.Next(255)); - } - - var blocks = ImageToBlocks.ImageTo4X4(testImage.Frames[0], out var blocksWidth, out var blocksHeight); - - Assert.Equal(16, blocks.Length); - Assert.Equal(4, blocksWidth); - Assert.Equal(4, blocksHeight); - - using var output = ImageToBlocks.ImageFromRawBlocks(blocks, blocksWidth, blocksHeight); - var pixels2 = output.GetPixelSpan(); - - Assert.Equal(pixels.Length, pixels2.Length); - for (int i = 0; i < pixels.Length; i++) { - Assert.Equal(pixels[i], pixels2[i]); - } - } - - [Fact] - public void BlockError() - { - using Image testImage = new Image(16, 16); - - var blocks = ImageToBlocks.ImageTo4X4(testImage.Frames[0], out var blocksWidth, out var blocksHeight); - - var block1 = blocks[2 + 2 * blocksWidth]; - var block2 = blocks[2 + 2 * blocksWidth]; - - Assert.Equal(0, block1.CalculateError(block2)); - - for (int i = 0; i < block2.AsSpan.Length; i++) { - block2.AsSpan[i].R = (byte) (block2.AsSpan[i].R + 2); - } - Assert.Equal(2, block1.CalculateError(block2)); - - for (int i = 0; i < block2.AsSpan.Length; i++) { - block2.AsSpan[i].G = (byte) (block2.AsSpan[i].R + 20); - } - Assert.Equal(22, block1.CalculateError(block2)); - } - } -} diff --git a/BCnEncTests/ClusterTests.cs b/BCnEncTests/ClusterTests.cs deleted file mode 100644 index dbd5ab8..0000000 --- a/BCnEncTests/ClusterTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using BCnEncoder.Shared; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using Xunit; - -namespace BCnEncTests -{ - public class ClusterTests - { - [Fact] - public void Clusterize() { - using var testImage = ImageLoader.testBlur1.Clone(); - var pixels = testImage.GetPixelSpan(); - int numClusters = (testImage.Width / 32) * (testImage.Height / 32); - - var clusters = LinearClustering.ClusterPixels(pixels, testImage.Width, testImage.Height, numClusters, 10, 10); - - ColorYCbCr[] pixC = new ColorYCbCr[numClusters]; - int[] counts = new int[numClusters]; - for (int i = 0; i < pixels.Length; i++) { - pixC[clusters[i]] += new ColorYCbCr(pixels[i]); - counts[clusters[i]]++; - } - for (int i = 0; i < numClusters; i++) { - pixC[i] /= counts[i]; - } - for (int i = 0; i < pixels.Length; i++) { - pixels[i] = pixC[clusters[i]].ToRgba32(); - } - - 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 d1423ef..0000000 --- a/BCnEncTests/DdsReadTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using BCnEncoder.Decoder; -using BCnEncoder.Shared; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using Xunit; - -namespace BCnEncTests -{ - public class DdsReadTests - { - [Fact] - public void ReadRgba() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_rgba.dds"); - DdsFile file = DdsFile.Load(fs); - Assert.Equal(DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, file.Header.ddsPixelFormat.DxgiFormat); - Assert.Equal(file.Header.dwMipMapCount, (uint)file.Faces[0].MipMaps.Length); - - BcDecoder decoder = new BcDecoder(); - var images = decoder.DecodeAllMipMaps(file); - - Assert.Equal((uint)images[0].Width, file.Header.dwWidth); - Assert.Equal((uint)images[0].Height, file.Header.dwHeight); - - for (int i = 0; i < images.Length; i++) { - using FileStream outFs = File.OpenWrite($"decoding_test_dds_rgba_mip{i}.png"); - images[i].SaveAsPng(outFs); - images[i].Dispose(); - } - } - - [Fact] - public void ReadBc1() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc1.dds"); - DdsFile file = DdsFile.Load(fs); - Assert.Equal(DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, file.Header.ddsPixelFormat.DxgiFormat); - Assert.Equal(file.Header.dwMipMapCount, (uint)file.Faces[0].MipMaps.Length); - - - BcDecoder decoder = new BcDecoder(); - var images = decoder.DecodeAllMipMaps(file); - - Assert.Equal((uint)images[0].Width, file.Header.dwWidth); - Assert.Equal((uint)images[0].Height, file.Header.dwHeight); - - for (int i = 0; i < images.Length; i++) { - using FileStream outFs = File.OpenWrite($"decoding_test_dds_bc1_mip{i}.png"); - images[i].SaveAsPng(outFs); - images[i].Dispose(); - } - } - - [Fact] - public void ReadBc1a() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc1a.dds"); - DdsFile file = DdsFile.Load(fs); - Assert.Equal(DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, file.Header.ddsPixelFormat.DxgiFormat); - Assert.Equal(file.Header.dwMipMapCount, (uint)file.Faces[0].MipMaps.Length); - - - BcDecoder decoder = new BcDecoder(); - decoder.InputOptions.ddsBc1ExpectAlpha = true; - var image = decoder.Decode(file); - - Assert.Equal((uint)image.Width, file.Header.dwWidth); - Assert.Equal((uint)image.Height, file.Header.dwHeight); - - Assert.Contains(image.GetPixelSpan().ToArray(), x => x.A == 0); - - using FileStream outFs = File.OpenWrite($"decoding_test_dds_bc1a.png"); - image.SaveAsPng(outFs); - image.Dispose(); - } - - [Fact] - public void ReadBc7() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc7.dds"); - BcDecoder decoder = new BcDecoder(); - var images = decoder.DecodeAllMipMaps(fs); - - for (int i = 0; i < images.Length; i++) { - using FileStream outFs = File.OpenWrite($"decoding_test_dds_bc7_mip{i}.png"); - images[i].SaveAsPng(outFs); - images[i].Dispose(); - } - } - - [Fact] - public void ReadFromStream() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc1.dds"); - - BcDecoder decoder = new BcDecoder(); - var images = decoder.DecodeAllMipMaps(fs); - - for (int i = 0; i < images.Length; i++) { - using FileStream 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 f4103c4..0000000 --- a/BCnEncTests/DdsWritingTests.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using BCnEncoder.Encoder; -using BCnEncoder.Shared; -using Xunit; - -namespace BCnEncTests -{ - public class DdsWritingTests - { - [Fact] - public void DdsWriteRgba() { - var image = ImageLoader.testLenna; - - BcEncoder encoder = new BcEncoder(); - encoder.OutputOptions.quality = EncodingQuality.Fast; - encoder.OutputOptions.generateMipMaps = true; - encoder.OutputOptions.format = CompressionFormat.RGBA; - encoder.OutputOptions.fileFormat = OutputFileFormat.Dds; - - using FileStream fs = File.OpenWrite("encoding_dds_rgba.dds"); - encoder.Encode(image, fs); - fs.Close(); - } - - [Fact] - public void DdsWriteBc1() { - var image = ImageLoader.testLenna; - - BcEncoder encoder = new BcEncoder(); - encoder.OutputOptions.quality = EncodingQuality.Fast; - encoder.OutputOptions.generateMipMaps = true; - encoder.OutputOptions.format = CompressionFormat.BC1; - encoder.OutputOptions.fileFormat = OutputFileFormat.Dds; - - using FileStream fs = File.OpenWrite("encoding_dds_bc1.dds"); - encoder.Encode(image, fs); - fs.Close(); - } - - [Fact] - public void DdsWriteBc2() { - var image = ImageLoader.testAlpha1; - - BcEncoder encoder = new BcEncoder(); - encoder.OutputOptions.quality = EncodingQuality.Fast; - encoder.OutputOptions.generateMipMaps = true; - encoder.OutputOptions.format = CompressionFormat.BC2; - encoder.OutputOptions.fileFormat = OutputFileFormat.Dds; - - using FileStream fs = File.OpenWrite("encoding_dds_bc2.dds"); - encoder.Encode(image, fs); - fs.Close(); - } - - [Fact] - public void DdsWriteBc3() { - var image = ImageLoader.testAlpha1; - - BcEncoder encoder = new BcEncoder(); - encoder.OutputOptions.quality = EncodingQuality.Fast; - encoder.OutputOptions.generateMipMaps = true; - encoder.OutputOptions.format = CompressionFormat.BC3; - encoder.OutputOptions.fileFormat = OutputFileFormat.Dds; - - using FileStream fs = File.OpenWrite("encoding_dds_bc3.dds"); - encoder.Encode(image, fs); - fs.Close(); - } - - [Fact] - public void DdsWriteBc4() { - var image = ImageLoader.testHeight1; - - BcEncoder encoder = new BcEncoder(); - encoder.OutputOptions.quality = EncodingQuality.Fast; - encoder.OutputOptions.generateMipMaps = true; - encoder.OutputOptions.format = CompressionFormat.BC4; - encoder.OutputOptions.fileFormat = OutputFileFormat.Dds; - - using FileStream fs = File.OpenWrite("encoding_dds_bc4.dds"); - encoder.Encode(image, fs); - fs.Close(); - } - - [Fact] - public void DdsWriteBc5() { - var image = ImageLoader.testRedGreen1; - - BcEncoder encoder = new BcEncoder(); - encoder.OutputOptions.quality = EncodingQuality.Fast; - encoder.OutputOptions.generateMipMaps = true; - encoder.OutputOptions.format = CompressionFormat.BC5; - encoder.OutputOptions.fileFormat = OutputFileFormat.Dds; - - using FileStream fs = File.OpenWrite("encoding_dds_bc5.dds"); - encoder.Encode(image, fs); - fs.Close(); - } - - [Fact] - public void DdsWriteBc7() { - var image = ImageLoader.testLenna; - - BcEncoder encoder = new BcEncoder(); - encoder.OutputOptions.quality = EncodingQuality.Fast; - encoder.OutputOptions.generateMipMaps = true; - encoder.OutputOptions.format = CompressionFormat.BC7; - encoder.OutputOptions.fileFormat = OutputFileFormat.Dds; - - using FileStream fs = File.OpenWrite("encoding_dds_bc7.dds"); - encoder.Encode(image, fs); - fs.Close(); - } - - [Fact] - public void DdsWriteCubemap() { - var images = ImageLoader.testCubemap; - - BcEncoder encoder = new BcEncoder(); - encoder.OutputOptions.quality = EncodingQuality.Fast; - encoder.OutputOptions.generateMipMaps = true; - encoder.OutputOptions.format = CompressionFormat.BC1; - encoder.OutputOptions.fileFormat = OutputFileFormat.Dds; - - using FileStream fs = File.OpenWrite("encoding_dds_cubemap_bc1.dds"); - encoder.EncodeCubeMap(images[0],images[1],images[2],images[3],images[4],images[5], fs); - fs.Close(); - } - } -} diff --git a/BCnEncTests/DecodingTests.cs b/BCnEncTests/DecodingTests.cs deleted file mode 100644 index 3cf65e7..0000000 --- a/BCnEncTests/DecodingTests.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using BCnEncoder.Decoder; -using BCnEncoder.Shared; -using SixLabors.ImageSharp; -using Xunit; - -namespace BCnEncTests -{ - public class DecodingTests - { - [Fact] - public void Bc1Decode() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc1.ktx"); - KtxFile file = KtxFile.Load(fs); - Assert.True(file.Header.VerifyHeader()); - Assert.Equal((uint)1, file.Header.NumberOfFaces); - - BcDecoder decoder = new BcDecoder(); - using var image = decoder.Decode(file); - - Assert.Equal((uint)image.Width, file.Header.PixelWidth); - Assert.Equal((uint)image.Height, file.Header.PixelHeight); - - using FileStream outFs = File.OpenWrite("decoding_test_bc1.png"); - image.SaveAsPng(outFs); - } - - [Fact] - public void Bc1AlphaDecode() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc1a.ktx"); - KtxFile file = KtxFile.Load(fs); - Assert.True(file.Header.VerifyHeader()); - Assert.Equal((uint)1, file.Header.NumberOfFaces); - - BcDecoder decoder = new BcDecoder(); - using var image = decoder.Decode(file); - - Assert.Equal((uint)image.Width, file.Header.PixelWidth); - Assert.Equal((uint)image.Height, file.Header.PixelHeight); - - using FileStream outFs = File.OpenWrite("decoding_test_bc1a.png"); - image.SaveAsPng(outFs); - } - - [Fact] - public void Bc2Decode() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc2.ktx"); - KtxFile file = KtxFile.Load(fs); - Assert.True(file.Header.VerifyHeader()); - Assert.Equal((uint)1, file.Header.NumberOfFaces); - - BcDecoder decoder = new BcDecoder(); - using var image = decoder.Decode(file); - - Assert.Equal((uint)image.Width, file.Header.PixelWidth); - Assert.Equal((uint)image.Height, file.Header.PixelHeight); - - using FileStream outFs = File.OpenWrite("decoding_test_bc2.png"); - image.SaveAsPng(outFs); - } - - [Fact] - public void Bc3Decode() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc3.ktx"); - KtxFile file = KtxFile.Load(fs); - Assert.True(file.Header.VerifyHeader()); - Assert.Equal((uint)1, file.Header.NumberOfFaces); - - BcDecoder decoder = new BcDecoder(); - using var image = decoder.Decode(file); - - Assert.Equal((uint)image.Width, file.Header.PixelWidth); - Assert.Equal((uint)image.Height, file.Header.PixelHeight); - - using FileStream outFs = File.OpenWrite("decoding_test_bc3.png"); - image.SaveAsPng(outFs); - } - - [Fact] - public void Bc4Decode() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc4_unorm.ktx"); - KtxFile file = KtxFile.Load(fs); - Assert.True(file.Header.VerifyHeader()); - Assert.Equal((uint)1, file.Header.NumberOfFaces); - - BcDecoder decoder = new BcDecoder(); - using var image = decoder.Decode(file); - - Assert.Equal((uint)image.Width, file.Header.PixelWidth); - Assert.Equal((uint)image.Height, file.Header.PixelHeight); - - using FileStream outFs = File.OpenWrite("decoding_test_bc4.png"); - image.SaveAsPng(outFs); - } - - [Fact] - public void Bc5Decode() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc5_unorm.ktx"); - KtxFile file = KtxFile.Load(fs); - Assert.True(file.Header.VerifyHeader()); - Assert.Equal((uint)1, file.Header.NumberOfFaces); - - BcDecoder decoder = new BcDecoder(); - using var image = decoder.Decode(file); - - Assert.Equal((uint)image.Width, file.Header.PixelWidth); - Assert.Equal((uint)image.Height, file.Header.PixelHeight); - - using FileStream outFs = File.OpenWrite("decoding_test_bc5.png"); - image.SaveAsPng(outFs); - } - - [Fact] - public void Bc7DecodeRgb() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc7_rgb.ktx"); - KtxFile file = KtxFile.Load(fs); - Assert.True(file.Header.VerifyHeader()); - Assert.Equal((uint)1, file.Header.NumberOfFaces); - - BcDecoder decoder = new BcDecoder(); - using var image = decoder.Decode(file); - - Assert.Equal((uint)image.Width, file.Header.PixelWidth); - Assert.Equal((uint)image.Height, file.Header.PixelHeight); - - using FileStream outFs = File.OpenWrite("decoding_test_bc7_rgb.png"); - image.SaveAsPng(outFs); - } - - [Fact] - public void Bc7DecodeUnorm() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc7_unorm.ktx"); - KtxFile file = KtxFile.Load(fs); - Assert.True(file.Header.VerifyHeader()); - Assert.Equal((uint)1, file.Header.NumberOfFaces); - - BcDecoder decoder = new BcDecoder(); - using var image = decoder.Decode(file); - - Assert.Equal((uint)image.Width, file.Header.PixelWidth); - Assert.Equal((uint)image.Height, file.Header.PixelHeight); - - using FileStream outFs = File.OpenWrite("decoding_test_bc7_unorm.png"); - image.SaveAsPng(outFs); - } - - [Fact] - public void Bc7DecodeEveryBlockType() { - using FileStream fs = File.OpenRead(@"../../../testImages/test_decompress_bc7_types.ktx"); - KtxFile file = KtxFile.Load(fs); - Assert.True(file.Header.VerifyHeader()); - Assert.Equal((uint)1, file.Header.NumberOfFaces); - - BcDecoder decoder = new BcDecoder(); - using var image = decoder.Decode(file); - - Assert.Equal((uint)image.Width, file.Header.PixelWidth); - Assert.Equal((uint)image.Height, file.Header.PixelHeight); - - using FileStream outFs = File.OpenWrite("decoding_test_bc7_types.png"); - image.SaveAsPng(outFs); - } - } -} diff --git a/BCnEncTests/EncodeByteOrderTests.cs b/BCnEncTests/EncodeByteOrderTests.cs new file mode 100644 index 0000000..9b1d042 --- /dev/null +++ b/BCnEncTests/EncodeByteOrderTests.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.ImageSharp; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace BCnEncTests +{ + public class EncodeByteOrderTests + { + + private void EncodeByteOrderTest(PixelFormat pixelFormat) where T : unmanaged, IPixel + { + using var imageOrig = ImageLoader.LoadTestImageSharp("../../../testImages/test_alpha_1_512.png"); + var testImage = imageOrig.CloneAs(); + + var pixels = testImage.GetPixelMemoryGroup()[0]; + var bytes = MemoryMarshal.AsBytes(pixels.Span); + + var encoder = new BcEncoder(CompressionFormat.Bc3); + var decoder = new BcDecoder(); + + using var ms = new MemoryStream(); + + encoder.EncodeToStream(bytes, testImage.Width, testImage.Height, pixelFormat, ms); + + ms.Position = 0; + + var decoded = decoder.DecodeToImageRgba32(ms); + + var hasNoAlpha = pixelFormat == PixelFormat.Rgb24 || pixelFormat == PixelFormat.Bgr24; + + TestHelper.AssertImagesEqual(imageOrig, decoded, encoder.OutputOptions.Quality, + !hasNoAlpha); + } + + [Fact] + public void EncodeByteOrderBgra() + { + EncodeByteOrderTest(PixelFormat.Bgra32); + } + + [Fact] + public void EncodeByteOrderBgr() + { + EncodeByteOrderTest(PixelFormat.Bgr24); + } + + [Fact] + public void EncodeByteOrderArgb() + { + EncodeByteOrderTest(PixelFormat.Argb32); + } + + [Fact] + public void EncodeByteOrderRgba() + { + EncodeByteOrderTest(PixelFormat.Rgba32); + } + + [Fact] + public void EncodeByteOrderRgb24() + { + EncodeByteOrderTest(PixelFormat.Rgb24); + } + } +} diff --git a/BCnEncTests/EncodingTest.cs b/BCnEncTests/EncodingTest.cs deleted file mode 100644 index f00e31c..0000000 --- a/BCnEncTests/EncodingTest.cs +++ /dev/null @@ -1,529 +0,0 @@ -using System.IO; -using BCnEncoder.Encoder; -using BCnEncoder.Shared; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -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, - EncodingQuality.BestQuality, - "encoding_bc1_gradient_bestQuality.ktx", - output); - } - - [Fact] - public void Bc1GradientBalanced() - { - var image = ImageLoader.testGradient1; - - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC1, - EncodingQuality.Balanced, - "encoding_bc1_gradient_balanced.ktx", - output); - } - - [Fact] - public void Bc1GradientFast() - { - var image = ImageLoader.testGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC1, - EncodingQuality.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, - EncodingQuality.BestQuality, - "encoding_bc1_diffuse_bestQuality.ktx", - output); - } - - [Fact] - public void Bc1DiffuseBalanced() - { - var image = ImageLoader.testDiffuse1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC1, - EncodingQuality.Balanced, - "encoding_bc1_diffuse_balanced.ktx", - output); - } - - [Fact] - public void Bc1DiffuseFast() - { - var image = ImageLoader.testDiffuse1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC1, - EncodingQuality.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, - EncodingQuality.BestQuality, - "encoding_bc1_blur_bestQuality.ktx", - output); - } - - [Fact] - public void Bc1BlurBalanced() - { - var image = ImageLoader.testBlur1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC1, - EncodingQuality.Balanced, - "encoding_bc1_blur_balanced.ktx", - output); - } - - [Fact] - public void Bc1BlurFast() - { - var image = ImageLoader.testBlur1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC1, - EncodingQuality.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, - EncodingQuality.BestQuality, - "encoding_bc1a_sprite_bestQuality.ktx", - output); - } - - [Fact] - public void Bc1aSpriteBalanced() - { - var image = ImageLoader.testTransparentSprite1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC1WithAlpha, - EncodingQuality.Balanced, - "encoding_bc1a_sprite_balanced.ktx", - output); - } - - [Fact] - public void Bc1aSpriteFast() - { - var image = ImageLoader.testTransparentSprite1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC1WithAlpha, - EncodingQuality.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, - EncodingQuality.BestQuality, - "encoding_bc2_gradient_bestQuality.ktx", - output); - } - - [Fact] - public void Bc2GradientBalanced() - { - var image = ImageLoader.testAlphaGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC2, - EncodingQuality.Balanced, - "encoding_bc2_gradient_balanced.ktx", - output); - } - - [Fact] - public void Bc2GradientFast() - { - var image = ImageLoader.testAlphaGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC2, - EncodingQuality.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, - EncodingQuality.BestQuality, - "encoding_bc3_gradient_bestQuality.ktx", - output); - } - - [Fact] - public void Bc3GradientBalanced() - { - var image = ImageLoader.testAlphaGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC3, - EncodingQuality.Balanced, - "encoding_bc3_gradient_balanced.ktx", - output); - } - - [Fact] - public void Bc3GradientFast() - { - var image = ImageLoader.testAlphaGradient1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC3, - EncodingQuality.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, - EncodingQuality.BestQuality, - "encoding_bc4_red_bestQuality.ktx", - output); - } - - [Fact] - public void Bc4RedBalanced() - { - var image = ImageLoader.testHeight1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC4, - EncodingQuality.Balanced, - "encoding_bc4_red_balanced.ktx", - output); - } - - [Fact] - public void Bc4RedFast() - { - var image = ImageLoader.testHeight1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC4, - EncodingQuality.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, - EncodingQuality.BestQuality, - "encoding_bc5_red_green_bestQuality.ktx", - output); - } - - [Fact] - public void Bc5RedGreenBalanced() - { - var image = ImageLoader.testRedGreen1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC5, - EncodingQuality.Balanced, - "encoding_bc5_red_green_balanced.ktx", - output); - } - - [Fact] - public void Bc5RedGreenFast() - { - var image = ImageLoader.testRedGreen1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC5, - EncodingQuality.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, - EncodingQuality.BestQuality, - "encoding_bc7_rgb_bestQuality.ktx", - output); - } - - [Fact] - public void Bc7RgbBalanced() - { - var image = ImageLoader.testRgbHard1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC7, - EncodingQuality.Balanced, - "encoding_bc7_rgb_balanced.ktx", - output); - } - - [Fact] - public void Bc7LennaBalanced() - { - var image = ImageLoader.testLenna; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC7, - EncodingQuality.Balanced, - "encoding_bc7_lenna_balanced.ktx", - output); - } - - [Fact] - public void Bc7RgbFast() - { - var image = ImageLoader.testRgbHard1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC7, - EncodingQuality.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, - EncodingQuality.BestQuality, - "encoding_bc7_rgba_bestQuality.ktx", - output); - } - - [Fact] - public void Bc7RgbaBalanced() - { - var image = ImageLoader.testAlpha1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC7, - EncodingQuality.Balanced, - "encoding_bc7_rgba_balanced.ktx", - output); - } - - [Fact] - public void Bc7RgbaFast() - { - var image = ImageLoader.testAlpha1; - - TestHelper.ExecuteEncodingTest(image, - CompressionFormat.BC7, - EncodingQuality.Fast, - "encoding_bc7_rgba_fast.ktx", - output); - } - } - - public class CubemapTest - { - - [Fact] - public void WriteCubeMapFile() - { - var images = ImageLoader.testCubemap; - - string filename = "encoding_bc1_cubemap.ktx"; - - BcEncoder encoder = new BcEncoder(); - encoder.OutputOptions.quality = EncodingQuality.Fast; - encoder.OutputOptions.generateMipMaps = true; - encoder.OutputOptions.format = CompressionFormat.BC1; - - using FileStream fs = File.OpenWrite(filename); - encoder.EncodeCubeMap(images[0],images[1],images[2],images[3],images[4],images[5], fs); - fs.Close(); - } - } -} diff --git a/BCnEncTests/Examples.cs b/BCnEncTests/Examples.cs new file mode 100644 index 0000000..210d8a9 --- /dev/null +++ b/BCnEncTests/Examples.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.ImageSharp; +using BCnEncoder.Shared; +using CommunityToolkit.HighPerformance; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace BCnEncTests +{ + public class Examples + { + + public void EncodeImageSharp() + { + using Image image = Image.Load("example.png"); + + BcEncoder encoder = new BcEncoder(); + + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Quality = CompressionQuality.Balanced; + encoder.OutputOptions.Format = CompressionFormat.Bc1; + encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; //Change to Dds for a dds file. + + using FileStream fs = File.OpenWrite("example.ktx"); + encoder.EncodeToStream(image, fs); + } + + public void DecodeImageSharp() + { + using FileStream fs = File.OpenRead("compressed_bc1.ktx"); + + BcDecoder decoder = new BcDecoder(); + using Image image = decoder.DecodeToImageRgba32(fs); + + using FileStream outFs = File.OpenWrite("decoding_test_bc1.png"); + image.SaveAsPng(outFs); + } + + public void EncodeHdr() + { + HdrImage image = HdrImage.Read("example.hdr"); + + + BcEncoder encoder = new BcEncoder(); + + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Quality = CompressionQuality.Balanced; + encoder.OutputOptions.Format = CompressionFormat.Bc6U; + encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; //Change to Dds for a dds file. + + using FileStream fs = File.OpenWrite("example.ktx"); + encoder.EncodeToStreamHdr(image.PixelMemory, fs); + } + + public void DecodeHdr() + { + using FileStream fs = File.OpenRead("compressed_bc6.ktx"); + + BcDecoder decoder = new BcDecoder(); + Memory2D pixels = decoder.DecodeHdr2D(fs); + + HdrImage image = new HdrImage(pixels.Span); + + using FileStream outFs = File.OpenWrite("decoded.hdr"); + image.Write(outFs); + } + } +} diff --git a/BCnEncTests/HdrImageTests.cs b/BCnEncTests/HdrImageTests.cs new file mode 100644 index 0000000..14dc3d1 --- /dev/null +++ b/BCnEncTests/HdrImageTests.cs @@ -0,0 +1,66 @@ +using System; +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; + +namespace BCnEncTests +{ + public class HdrImageTests + { + [Fact] + public void LoadHdr() + { + using var stream = File.OpenRead("../../../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 img = new Image(hdrImg.width, hdrImg.height); + + for (var y = 0; y < hdrImg.height; y++) + { + for (var x = 0; x < hdrImg.width; x++) + { + var i = y * hdrImg.width + x; + img[x, y] = new RgbaVector(hdrImg.pixels[i].r, hdrImg.pixels[i].g, hdrImg.pixels[i].b); + } + } + + img.SaveAsPng("test_hdr_load.png"); + + var img2 = img.CloneAs(); + + TestHelper.AssertImagesEqual(HdrLoader.ReferenceKiara, img2, CompressionQuality.BestQuality); + } + + [Fact] + public async Task DecodeAllMipMapsHdrStreamAsync() + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Format = CompressionFormat.Bc6U; + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + + var decoder = new BcDecoder(); + var input = HdrLoader.TestHdrKiara; + var ktxWithMips = encoder.EncodeToKtxHdr(new Memory2D(input.pixels, input.height, input.width)); + using var ms = new MemoryStream(); + ktxWithMips.Write(ms); + ms.Position = 0; + + var images = await decoder.DecodeAllMipMapsHdr2DAsync(ms); + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + Assert.True(images.Length > 1); + } + } +} diff --git a/BCnEncTests/ImageLoader.cs b/BCnEncTests/ImageLoader.cs deleted file mode 100644 index c840eb7..0000000 --- a/BCnEncTests/ImageLoader.cs +++ /dev/null @@ -1,32 +0,0 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -namespace BCnEncTests -{ - 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 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[] testCubemap { get; } = new [] { - LoadTestImage("../../../testImages/cubemap/right.png"), - LoadTestImage("../../../testImages/cubemap/left.png"), - LoadTestImage("../../../testImages/cubemap/top.png"), - LoadTestImage("../../../testImages/cubemap/bottom.png"), - LoadTestImage("../../../testImages/cubemap/back.png"), - LoadTestImage("../../../testImages/cubemap/forward.png") - }; - - private static Image LoadTestImage(string filename) { - return Image.Load(filename); - } - } -} diff --git a/BCnEncTests/ImageSharpTests.cs b/BCnEncTests/ImageSharpTests.cs new file mode 100644 index 0000000..855a051 --- /dev/null +++ b/BCnEncTests/ImageSharpTests.cs @@ -0,0 +1,388 @@ +using System.IO; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.ImageSharp; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using BCnEncTests.Support; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace BCnEncTests +{ + public class ImageSharpEncodingTests + { + private readonly BcEncoder encoder; + private readonly BcDecoder decoder; + private readonly Image original; + private readonly Image[] cubemap; + + public ImageSharpEncodingTests() + { + encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + decoder = new BcDecoder(); + original = ImageLoader.LoadTestImageSharp("../../../testImages/test_gradient_1_512.jpg"); + cubemap = new[] + { + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/right.png"), + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/left.png"), + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/top.png"), + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/bottom.png"), + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/back.png"), + ImageLoader.LoadTestImageSharp("../../../testImages/cubemap/forward.png"), + }; + } + + [Fact] + public void EncodeToKtx() + { + var ktx = encoder.EncodeToKtx(original); + using var decoded = decoder.DecodeToImageRgba32(ktx); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeToDds() + { + var dds = encoder.EncodeToDds(original); + using var decoded = decoder.DecodeToImageRgba32(dds); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeToStreamKtx() + { + encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; + using var ms = new MemoryStream(); + encoder.EncodeToStream(original, ms); + ms.Position = 0; + using var decoded = decoder.DecodeToImageRgba32(ms); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeToStreamDds() + { + encoder.OutputOptions.FileFormat = OutputFileFormat.Dds; + using var ms = new MemoryStream(); + encoder.EncodeToStream(original, ms); + ms.Position = 0; + using var decoded = decoder.DecodeToImageRgba32(ms); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeToRawBytes() + { + var rawBytes = encoder.EncodeToRawBytes(original); + using var decoded = decoder.DecodeRawToImageRgba32(rawBytes[0], original.Width, original.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void EncodeToRawBytesSingleMip() + { + var mip0 = encoder.EncodeToRawBytes(original, 0, out var mipWidth, out var mipHeight); + Assert.Equal(original.Width, mipWidth); + Assert.Equal(original.Height, mipHeight); + Assert.True(mip0.Length > 0); + } + + [Fact] + public void CalculateMipLevelCount() + { + var fromImage = encoder.CalculateNumberOfMipLevels(original); + var fromDims = encoder.CalculateNumberOfMipLevels(original.Width, original.Height); + Assert.Equal(fromDims, fromImage); + } + + [Fact] + public void CalculateMipMapSize() + { + encoder.CalculateMipMapSize(original, 0, out var w0, out var h0); + Assert.Equal(original.Width, w0); + Assert.Equal(original.Height, h0); + + encoder.CalculateMipMapSize(original, 1, out var w1, out var h1); + Assert.Equal(original.Width / 2, w1); + Assert.Equal(original.Height / 2, h1); + } + + [Fact] + public void EncodeCubeMapToKtx() + { + var ktx = encoder.EncodeCubeMapToKtx(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5]); + Assert.Equal(6, ktx.MipMaps[0].Faces.Length); + for (var i = 0; i < 6; i++) + { + var face = ktx.MipMaps[0].Faces[i]; + using var decoded = decoder.DecodeRawToImageRgba32(face.Data, (int)face.Width, (int)face.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(cubemap[i], decoded, encoder.OutputOptions.Quality); + } + } + + [Fact] + public void EncodeCubeMapToDds() + { + var dds = encoder.EncodeCubeMapToDds(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5]); + Assert.Equal(6, dds.Faces.Count); + for (var i = 0; i < 6; i++) + { + var face = dds.Faces[i].MipMaps[0]; + using var decoded = decoder.DecodeRawToImageRgba32(face.Data, (int)face.Width, (int)face.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(cubemap[i], decoded, encoder.OutputOptions.Quality); + } + } + + [Fact] + public void EncodeCubeMapToStream() + { + encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; + using var ms = new MemoryStream(); + encoder.EncodeCubeMapToStream(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5], ms); + ms.Position = 0; + var ktx = KtxFile.Load(ms); + Assert.Equal(6, ktx.MipMaps[0].Faces.Length); + } + + [Fact] + public async Task EncodeToKtxAsync() + { + var ktx = await encoder.EncodeToKtxAsync(original); + using var decoded = decoder.DecodeToImageRgba32(ktx); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task EncodeToDdsAsync() + { + var dds = await encoder.EncodeToDdsAsync(original); + using var decoded = decoder.DecodeToImageRgba32(dds); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task EncodeToStreamAsync() + { + encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; + using var ms = new MemoryStream(); + await encoder.EncodeToStreamAsync(original, ms); + ms.Position = 0; + using var decoded = decoder.DecodeToImageRgba32(ms); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task EncodeToRawBytesAsync() + { + var rawBytes = await encoder.EncodeToRawBytesAsync(original); + using var decoded = decoder.DecodeRawToImageRgba32(rawBytes[0], original.Width, original.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task EncodeCubeMapToKtxAsync() + { + var ktx = await encoder.EncodeCubeMapToKtxAsync(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5]); + Assert.Equal(6, ktx.MipMaps[0].Faces.Length); + } + + [Fact] + public async Task EncodeCubeMapToDdsAsync() + { + var dds = await encoder.EncodeCubeMapToDdsAsync(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5]); + Assert.Equal(6, dds.Faces.Count); + } + + [Fact] + public async Task EncodeCubeMapToStreamAsync() + { + encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; + using var ms = new MemoryStream(); + await encoder.EncodeCubeMapToStreamAsync(cubemap[0], cubemap[1], cubemap[2], cubemap[3], cubemap[4], cubemap[5], ms); + ms.Position = 0; + var ktx = KtxFile.Load(ms); + Assert.Equal(6, ktx.MipMaps[0].Faces.Length); + } + } + + public class ImageSharpDecodingTests + { + private readonly BcEncoder encoder; + private readonly BcDecoder decoder; + private readonly Image original; + private readonly KtxFile encodedKtx; + private readonly DdsFile encodedDds; + private readonly byte[] rawEncoded; + + public ImageSharpDecodingTests() + { + encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + decoder = new BcDecoder(); + original = ImageLoader.LoadTestImageSharp("../../../testImages/test_gradient_1_512.jpg"); + encodedKtx = encoder.EncodeToKtx(ImageLoader.TestGradient1); + encodedDds = encoder.EncodeToDds(ImageLoader.TestGradient1); + rawEncoded = encoder.EncodeToRawBytes(ImageLoader.TestGradient1)[0]; + } + + [Fact] + public void DecodeKtxFile() + { + using var decoded = decoder.DecodeToImageRgba32(encodedKtx); + Assert.Equal(original.Width, decoded.Width); + Assert.Equal(original.Height, decoded.Height); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void DecodeDdsFile() + { + using var decoded = decoder.DecodeToImageRgba32(encodedDds); + Assert.Equal(original.Width, decoded.Width); + Assert.Equal(original.Height, decoded.Height); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void DecodeStream() + { + using var ms = new MemoryStream(); + encodedKtx.Write(ms); + ms.Position = 0; + using var decoded = decoder.DecodeToImageRgba32(ms); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void DecodeAllMipMapsKtx() + { + encoder.OutputOptions.GenerateMipMaps = true; + var ktxWithMips = encoder.EncodeToKtx(ImageLoader.TestGradient1); + var images = decoder.DecodeAllMipMapsToImageRgba32(ktxWithMips); + + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + Assert.True(images.Length > 1); + TestHelper.AssertImagesEqual(original, images[0], encoder.OutputOptions.Quality); + + foreach (var img in images) img.Dispose(); + } + + [Fact] + public void DecodeAllMipMapsDds() + { + encoder.OutputOptions.GenerateMipMaps = true; + var ddsWithMips = encoder.EncodeToDds(ImageLoader.TestGradient1); + var images = decoder.DecodeAllMipMapsToImageRgba32(ddsWithMips); + + Assert.Equal((int)ddsWithMips.header.dwMipMapCount, images.Length); + Assert.True(images.Length > 1); + + foreach (var img in images) img.Dispose(); + } + + [Fact] + public void DecodeAllMipMapsStream() + { + encoder.OutputOptions.GenerateMipMaps = true; + var ktxWithMips = encoder.EncodeToKtx(ImageLoader.TestGradient1); + using var ms = new MemoryStream(); + ktxWithMips.Write(ms); + ms.Position = 0; + + var images = decoder.DecodeAllMipMapsToImageRgba32(ms); + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + + foreach (var img in images) img.Dispose(); + } + + [Fact] + public void DecodeRaw() + { + using var decoded = decoder.DecodeRawToImageRgba32(rawEncoded, original.Width, original.Height, CompressionFormat.Bc1); + Assert.Equal(original.Width, decoded.Width); + Assert.Equal(original.Height, decoded.Height); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public void DecodeRawStream() + { + using var ms = new MemoryStream(rawEncoded); + using var decoded = decoder.DecodeRawToImageRgba32(ms, original.Width, original.Height, CompressionFormat.Bc1); + Assert.Equal(original.Width, decoded.Width); + Assert.Equal(original.Height, decoded.Height); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeKtxFileAsync() + { + using var decoded = await decoder.DecodeToImageRgba32Async(encodedKtx); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeDdsFileAsync() + { + using var decoded = await decoder.DecodeToImageRgba32Async(encodedDds); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeStreamAsync() + { + using var ms = new MemoryStream(); + encodedKtx.Write(ms); + ms.Position = 0; + using var decoded = await decoder.DecodeToImageRgba32Async(ms); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeAllMipMapsKtxAsync() + { + encoder.OutputOptions.GenerateMipMaps = true; + var ktxWithMips = encoder.EncodeToKtx(ImageLoader.TestGradient1); + var images = await decoder.DecodeAllMipMapsToImageRgba32Async(ktxWithMips); + + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + Assert.True(images.Length > 1); + + foreach (var img in images) img.Dispose(); + } + + [Fact] + public async Task DecodeAllMipMapsStreamAsync() + { + encoder.OutputOptions.GenerateMipMaps = true; + var ktxWithMips = encoder.EncodeToKtx(ImageLoader.TestGradient1); + using var ms = new MemoryStream(); + ktxWithMips.Write(ms); + ms.Position = 0; + + var images = await decoder.DecodeAllMipMapsToImageRgba32Async(ms); + Assert.Equal((int)ktxWithMips.header.NumberOfMipmapLevels, images.Length); + + foreach (var img in images) img.Dispose(); + } + + [Fact] + public async Task DecodeRawAsync() + { + using var decoded = await decoder.DecodeRawToImageRgba32Async(rawEncoded, original.Width, original.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + + [Fact] + public async Task DecodeRawStreamAsync() + { + using var ms = new MemoryStream(rawEncoded); + using var decoded = await decoder.DecodeRawToImageRgba32Async(ms, original.Width, original.Height, CompressionFormat.Bc1); + TestHelper.AssertImagesEqual(original, decoded, encoder.OutputOptions.Quality); + } + } +} diff --git a/BCnEncTests/MipMapperTests.cs b/BCnEncTests/MipMapperTests.cs new file mode 100644 index 0000000..5da9c7e --- /dev/null +++ b/BCnEncTests/MipMapperTests.cs @@ -0,0 +1,79 @@ +using System; +using BCnEncoder.Shared; +using BCnEncTests.Support; +using CommunityToolkit.HighPerformance; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using Xunit; +using Xunit.Abstractions; + +namespace BCnEncTests +{ + public class MipMapperTests + { + private readonly ITestOutputHelper output; + + public MipMapperTests(ITestOutputHelper output) => this.output = output; + + [Fact] + public void MipChainHasCorrectDimensions() + { + var image = ImageLoader.TestGradient1; // 512x416 + var numMips = 0; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + Assert.Equal(chain.Length, numMips); + Assert.True(numMips > 1); + + for (var i = 0; i < numMips; i++) + { + Assert.Equal(Math.Max(1, image.Width >> i), chain[i].Width); + Assert.Equal(Math.Max(1, image.Height >> i), chain[i].Height); + } + + // Last level must be 1x1 + Assert.Equal(1, chain[numMips - 1].Width); + Assert.Equal(1, chain[numMips - 1].Height); + } + + /// + /// Compares each mip level produced by MipMapper against the equivalent + /// ImageSharp Box-filter resize (Compand=false so both operate in sRGB + /// byte space without gamma correction). The PSNR should be very high + /// because both algorithms perform the same 2x2 box average. + /// + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + public void MipLevelMatchesImageSharpBoxFilter(int mipLevel) + { + var image = ImageLoader.TestGradient1; + var numMips = mipLevel + 1; + var chain = MipMapper.GenerateMipChain(image, ref numMips); + + var mipW = chain[mipLevel].Width; + var mipH = chain[mipLevel].Height; + + // ImageSharp Box sampler + Compand=false: no gamma correction, + // averages pixel values in sRGB space — matches MipMapper exactly. + using var imageSharp = ImageLoader.LoadTestImageSharp("../../../testImages/test_gradient_1_512.jpg"); + using var reference = imageSharp.Clone(x => x.Resize(new ResizeOptions + { + Size = new Size(mipW, mipH), + Sampler = KnownResamplers.Box, + Compand = false + })); + + var mipColors = TestHelper.GetSinglePixelArrayAsColors(chain[mipLevel]); + var refColors = TestHelper.GetSinglePixelArrayAsColors(reference); + + var psnr = ImageQuality.PeakSignalToNoiseRatio(mipColors, refColors); + output.WriteLine($"Mip level {mipLevel} ({mipW}x{mipH}): PSNR vs ImageSharp Box = {psnr:F2} dB"); + + Assert.True(psnr > 40, + $"Mip level {mipLevel}: PSNR vs ImageSharp Box was {psnr:F2} dB (expected > 40)"); + } + } +} diff --git a/BCnEncTests/Support/HdrLoader.cs b/BCnEncTests/Support/HdrLoader.cs new file mode 100644 index 0000000..6080cf4 --- /dev/null +++ b/BCnEncTests/Support/HdrLoader.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace BCnEncTests.Support +{ + 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.LoadTestImageSharp("../../../testImages/test_hdr_kiara.png"); + public static DdsFile TestHdrKiaraDds { get; } = + DdsLoader.LoadDdsFile("../../../testImages/test_hdr_kiara_bc6h.dds"); + public static KtxFile TestHdrKiaraKtx { get; } = + KtxLoader.LoadKtxFile("../../../testImages/test_hdr_kiara_bc6h_ktx.ktx"); + + } +} diff --git a/BCnEncTests/Support/ImageLoader.cs b/BCnEncTests/Support/ImageLoader.cs new file mode 100644 index 0000000..37d410b --- /dev/null +++ b/BCnEncTests/Support/ImageLoader.cs @@ -0,0 +1,98 @@ +using System.IO; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace BCnEncTests.Support +{ + public static class ImageLoader + { + 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 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 Memory2D[] TestCubemap { get; } = { + LoadTestImage("../../../testImages/cubemap/right.png"), + LoadTestImage("../../../testImages/cubemap/left.png"), + LoadTestImage("../../../testImages/cubemap/top.png"), + LoadTestImage("../../../testImages/cubemap/bottom.png"), + LoadTestImage("../../../testImages/cubemap/back.png"), + LoadTestImage("../../../testImages/cubemap/forward.png") + }; + + 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 + { + public const string TestDecompressBc1Name = "../../../testImages/test_decompress_bc1.dds"; + public const string TestDecompressBc1AName = "../../../testImages/test_decompress_bc1a.dds"; + public const string TestDecompressBc7Name = "../../../testImages/test_decompress_bc7.dds"; + public const string TestDecompressBc5Name = "../../../testImages/decoding_dds_bc5.dds"; + public const string TestDecompressRgbaName = "../../../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("../../../testImages/test_decompress_bc1.ktx"); + public static KtxFile TestDecompressBc1A { get; } = LoadKtxFile("../../../testImages/test_decompress_bc1a.ktx"); + public static KtxFile TestDecompressBc2 { get; } = LoadKtxFile("../../../testImages/test_decompress_bc2.ktx"); + public static KtxFile TestDecompressBc3 { get; } = LoadKtxFile("../../../testImages/test_decompress_bc3.ktx"); + public static KtxFile TestDecompressBc4Unorm { get; } = LoadKtxFile("../../../testImages/test_decompress_bc4_unorm.ktx"); + public static KtxFile TestDecompressBc5Unorm { get; } = LoadKtxFile("../../../testImages/test_decompress_bc5_unorm.ktx"); + public static KtxFile TestDecompressBc7Rgb { get; } = LoadKtxFile("../../../testImages/test_decompress_bc7_rgb.ktx"); + public static KtxFile TestDecompressBc7Types { get; } = LoadKtxFile("../../../testImages/test_decompress_bc7_types.ktx"); + public static KtxFile TestDecompressBc7Unorm { get; } = LoadKtxFile("../../../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/Support/TestHelper.cs b/BCnEncTests/Support/TestHelper.cs new file mode 100644 index 0000000..9d6349a --- /dev/null +++ b/BCnEncTests/Support/TestHelper.cs @@ -0,0 +1,320 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Encoder; +using BCnEncoder.ImageSharp; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +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(Image original, Image image, CompressionQuality quality, bool countAlpha = true) + { + var psnr = CalculatePSNR(original, image, countAlpha); + 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 + + public static void ExecuteDecodingTest(KtxFile file, string outputFile) + { + Assert.True(file.header.VerifyHeader()); + Assert.Equal((uint)1, file.header.NumberOfFaces); + + var decoder = new BcDecoder(); + var pixels = decoder.Decode2D(file); + + Assert.Equal((uint)pixels.Width, file.header.PixelWidth); + Assert.Equal((uint)pixels.Height, file.header.PixelHeight); + + using var outFs = File.OpenWrite(outputFile); + SaveAsPng(pixels, outFs); + } + + #region Dds + + public static void ExecuteDdsWritingTest(Memory2D image, CompressionFormat format, string outputFile) + { + ExecuteDdsWritingTest(new[] { image }, format, outputFile); + } + + public static void ExecuteDdsWritingTest(Memory2D[] images, CompressionFormat format, string outputFile) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + encoder.OutputOptions.FileFormat = OutputFileFormat.Dds; + + using var fs = File.OpenWrite(outputFile); + + if (images.Length == 1) + { + encoder.EncodeToStream(images[0], fs); + } + else + { + encoder.EncodeCubeMapToStream(images[0], images[1], images[2], images[3], images[4], images[5], fs); + } + } + + public static void ExecuteDdsReadingTest(DdsFile file, DxgiFormat format, string outputFile, bool assertAlpha = false) + { + Assert.Equal(format, file.header.ddsPixelFormat.DxgiFormat); + Assert.Equal(file.header.dwMipMapCount, (uint)file.Faces[0].MipMaps.Length); + + var decoder = new BcDecoder(); + decoder.InputOptions.DdsBc1ExpectAlpha = assertAlpha; + var images = decoder.DecodeAllMipMaps2D(file); + + Assert.Equal((uint)images[0].Width, file.header.dwWidth); + Assert.Equal((uint)images[0].Height, file.header.dwHeight); + + for (var i = 0; i < images.Length; i++) + { + if (assertAlpha) + { + var pixels = GetSinglePixelArrayAsColors(images[0]); + Assert.Contains(pixels, x => x.a == 0); + } + + using var outFs = File.OpenWrite(string.Format(outputFile, i)); + SaveAsPng(images[i], outFs); + } + } + + #endregion + + #region Cancellation + + public static async Task ExecuteCancellationTest(Memory2D image, bool isParallel) + { + var encoder = new BcEncoder(CompressionFormat.Bc7); + encoder.OutputOptions.Quality = CompressionQuality.Fast; + encoder.Options.IsParallel = isParallel; + + var source = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); + await Assert.ThrowsAnyAsync(() => + encoder.EncodeToRawBytesAsync(image, source.Token)); + } + + #endregion + + #endregion + + public static float DecodeKtxCheckPSNR(string filename, Memory2D original) + { + using var fs = File.OpenRead(filename); + var ktx = KtxFile.Load(fs); + var decoder = new BcDecoder() + { + OutputOptions = { Bc4Component = ColorComponent.Luminance } + }; + var decoded = decoder.Decode2D(ktx); + + return CalculatePSNR(original, decoded); + } + + public static float DecodeKtxCheckRMSEHdr(string filename, HdrImage original) + { + using var fs = File.OpenRead(filename); + var ktx = KtxFile.Load(fs); + var decoder = new BcDecoder() + { + }; + + var decoded = decoder.DecodeHdr(ktx); + + return ImageQuality.CalculateLogRMSE(original.pixels, decoded); + } + + public static void ExecuteEncodingTest(Memory2D image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = quality; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + + var fs = File.OpenWrite(filename); + encoder.EncodeToStream(image, fs); + fs.Close(); + + var psnr = DecodeKtxCheckPSNR(filename, image); + output.WriteLine("RGBA PSNR: " + psnr + "db"); + AssertPSNR(psnr, encoder.OutputOptions.Quality); + } + + public static void ExecuteHdrEncodingTest(HdrImage image, CompressionFormat format, CompressionQuality quality, string filename, ITestOutputHelper output) + { + var encoder = new BcEncoder(); + encoder.OutputOptions.Quality = quality; + encoder.OutputOptions.GenerateMipMaps = true; + encoder.OutputOptions.Format = format; + + var fs = File.OpenWrite(filename); + encoder.EncodeToStreamHdr(new Memory2D(image.pixels, image.height, image.width), fs); + fs.Close(); + + var rmse = DecodeKtxCheckRMSEHdr(filename, image); + output.WriteLine("RGBFloat RMSE: " + rmse); + AssertRMSE(rmse, encoder.OutputOptions.Quality); + } + + private static float CalculatePSNR(Image original, Image decoded, bool countAlpha = true) + { + var pixels = GetSinglePixelArrayAsColors(original); + var pixels2 = GetSinglePixelArrayAsColors(decoded); + + return ImageQuality.PeakSignalToNoiseRatio(pixels, pixels2, countAlpha); + } + + 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(Image original) + { + ColorRgba32[] pixels = new ColorRgba32[original.Width * original.Height]; + for (var y = 0; y < original.Height; y++) + { + for (var x = 0; x < original.Width; x++) + { + var oPixel = original[x, y]; + pixels[y * original.Width + x] = new ColorRgba32(oPixel.R, oPixel.G, oPixel.B, oPixel.A); + } + } + 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]; + for (var y = 0; y < original.Height; y++) + { + for (var x = 0; x < original.Width; x++) + { + var oPixel = original[x, y]; + pixels[y * original.Width + x] = oPixel; + } + } + return pixels; + } + + public static void SetSinglePixelArray(Image dest, T[] pixels) where T : unmanaged, IPixel + { + for (var y = 0; y < dest.Height; y++) + { + for (var x = 0; x < dest.Width; x++) + { + dest[x, y] = pixels[y * dest.Width + x]; + } + } + } + + public static void SaveAsPng(Memory2D img, Stream fs) + { + var imageRgba = new Image(img.Width, img.Height); + var span = img.Span; + for (var y = 0; y < img.Height; y++) + { + for (var x = 0; x < img.Width; x++) + { + var col = span[y, x]; + imageRgba[x, y] = new Rgba32(col.r, col.g, col.b, col.a); + } + } + imageRgba.SaveAsPng(fs); + } + + public static void SaveAsPng(ColorRgbFloat[] pixels, int width, int height, Stream stream) + { + var rgba = new ColorRgba32[pixels.Length]; + for (var i = 0; i < pixels.Length; i++) + { + var p = pixels[i]; + byte r = (byte)(Math.Max(0, Math.Min(1, p.r)) * 255 + 0.5f); + byte g = (byte)(Math.Max(0, Math.Min(1, p.g)) * 255 + 0.5f); + byte b = (byte)(Math.Max(0, Math.Min(1, p.b)) * 255 + 0.5f); + rgba[i] = new ColorRgba32(r, g, b, 255); + } + var mem = new Memory2D(rgba, height, width); + SaveAsPng(mem, stream); + } + } +} diff --git a/BCnEncTests/TestHelper.cs b/BCnEncTests/TestHelper.cs deleted file mode 100644 index c34e00c..0000000 --- a/BCnEncTests/TestHelper.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using BCnEncoder.Decoder; -using BCnEncoder.Encoder; -using BCnEncoder.Shared; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using Xunit; -using Xunit.Abstractions; - -namespace BCnEncTests -{ - public static class TestHelper - { - public static float DecodeCheckPSNR(string filename, Image original) { - using FileStream fs = File.OpenRead(filename); - var ktx = KtxFile.Load(fs); - var decoder = new BcDecoder(); - using var img = decoder.Decode(ktx); - var pixels = original.GetPixelSpan(); - var pixels2 = img.GetPixelSpan(); - - return ImageQuality.PeakSignalToNoiseRatio(pixels, pixels2, true); - } - - public static void ExecuteEncodingTest(Image image, CompressionFormat format, EncodingQuality quality, string filename, ITestOutputHelper output) { - BcEncoder encoder = new BcEncoder(); - encoder.OutputOptions.quality = quality; - encoder.OutputOptions.generateMipMaps = true; - encoder.OutputOptions.format = format; - - using FileStream fs = File.OpenWrite(filename); - encoder.Encode(image, fs); - fs.Close(); - var psnr = TestHelper.DecodeCheckPSNR(filename, image); - output.WriteLine("RGBA PSNR: " + psnr + "db"); - if(quality == EncodingQuality.Fast) - { - Assert.True(psnr > 25); - } - else - { - Assert.True(psnr > 30); - } - } - } -} diff --git a/BCnEncTests/testImages/NOTICE b/BCnEncTests/testImages/NOTICE index 312ce71..5539ab7 100644 --- a/BCnEncTests/testImages/NOTICE +++ b/BCnEncTests/testImages/NOTICE @@ -11,3 +11,6 @@ Cat photo blurry by Karina Vorozheeva on Unsplash Cat photo gradient by Borna Bevanda on Unsplash Pixel art birds by Refuzzle from opengameart.org + +HDR Kiara from https://hdrihaven.com +test_hdr_probe CC0 from http://graphicslearning.com/downloads/hdr-light-probe-02-cc0/ \ No newline at end of file diff --git a/BCnEncTests/testImages/decoding_dds_bc5.dds b/BCnEncTests/testImages/decoding_dds_bc5.dds new file mode 100644 index 0000000..813742d Binary files /dev/null and b/BCnEncTests/testImages/decoding_dds_bc5.dds differ diff --git a/BCnEncTests/testImages/decoding_dds_bc5_reference.png b/BCnEncTests/testImages/decoding_dds_bc5_reference.png new file mode 100644 index 0000000..cb981d9 Binary files /dev/null and b/BCnEncTests/testImages/decoding_dds_bc5_reference.png differ diff --git a/BCnEncTests/testImages/test_bc6h_ufloat.ktx b/BCnEncTests/testImages/test_bc6h_ufloat.ktx new file mode 100644 index 0000000..5414498 Binary files /dev/null and b/BCnEncTests/testImages/test_bc6h_ufloat.ktx differ diff --git a/BCnEncTests/testImages/test_decompress_bc4_unorm_reference.bmp b/BCnEncTests/testImages/test_decompress_bc4_unorm_reference.bmp new file mode 100644 index 0000000..75d1242 Binary files /dev/null and b/BCnEncTests/testImages/test_decompress_bc4_unorm_reference.bmp differ diff --git a/BCnEncTests/testImages/test_decompress_bc5_unorm_reference.bmp b/BCnEncTests/testImages/test_decompress_bc5_unorm_reference.bmp new file mode 100644 index 0000000..80a0328 Binary files /dev/null and b/BCnEncTests/testImages/test_decompress_bc5_unorm_reference.bmp differ diff --git a/BCnEncTests/testImages/test_hdr_kiara.hdr b/BCnEncTests/testImages/test_hdr_kiara.hdr new file mode 100644 index 0000000..82ced05 Binary files /dev/null and b/BCnEncTests/testImages/test_hdr_kiara.hdr differ diff --git a/BCnEncTests/testImages/test_hdr_kiara.png b/BCnEncTests/testImages/test_hdr_kiara.png new file mode 100644 index 0000000..2f4264d Binary files /dev/null and b/BCnEncTests/testImages/test_hdr_kiara.png differ diff --git a/BCnEncTests/testImages/test_hdr_kiara_bc6h.dds b/BCnEncTests/testImages/test_hdr_kiara_bc6h.dds new file mode 100644 index 0000000..3723121 Binary files /dev/null and b/BCnEncTests/testImages/test_hdr_kiara_bc6h.dds differ diff --git a/BCnEncTests/testImages/test_hdr_kiara_bc6h_ktx.ktx b/BCnEncTests/testImages/test_hdr_kiara_bc6h_ktx.ktx new file mode 100644 index 0000000..646bcf1 Binary files /dev/null and b/BCnEncTests/testImages/test_hdr_kiara_bc6h_ktx.ktx differ diff --git a/BCnEncTests/testImages/test_hdr_kiara_dds_float16_data.bin b/BCnEncTests/testImages/test_hdr_kiara_dds_float16_data.bin new file mode 100644 index 0000000..5cf3652 Binary files /dev/null and b/BCnEncTests/testImages/test_hdr_kiara_dds_float16_data.bin differ diff --git a/BCnEncTests/testImages/test_hdr_probe.hdr b/BCnEncTests/testImages/test_hdr_probe.hdr new file mode 100644 index 0000000..d67034d Binary files /dev/null and b/BCnEncTests/testImages/test_hdr_probe.hdr differ diff --git a/BCnEncoder.NET.ImageSharp/BCnDecoderExtensions.cs b/BCnEncoder.NET.ImageSharp/BCnDecoderExtensions.cs new file mode 100644 index 0000000..ea88bed --- /dev/null +++ b/BCnEncoder.NET.ImageSharp/BCnDecoderExtensions.cs @@ -0,0 +1,260 @@ +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using BCnEncoder.Decoder; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace BCnEncoder.ImageSharp +{ + public static class BCnDecoderExtensions + { + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the encoded image. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The format the encoded data is in. + /// The decoded Rgba32 image. + public static Image DecodeRawToImageRgba32(this BcDecoder decoder, Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format) + { + return ColorMemoryToImage(decoder.DecodeRaw2D(inputStream, pixelWidth, pixelHeight, format)); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The array containing the encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The format the encoded data is in. + /// The decoded Rgba32 image. + public static Image DecodeRawToImageRgba32(this BcDecoder decoder, byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format) + { + return ColorMemoryToImage(decoder.DecodeRaw2D(input, pixelWidth, pixelHeight, format)); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// + /// The stream that contains either ktx or dds file. + /// The decoded Rgba32 image. + public static Image DecodeToImageRgba32(this BcDecoder decoder, Stream inputStream) + { + return ColorMemoryToImage(decoder.Decode2D(inputStream)); + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// + /// The stream that contains either ktx or dds file. + /// An array of decoded Rgba32 images. + public static Image[] DecodeAllMipMapsToImageRgba32(this BcDecoder decoder, Stream inputStream) + { + var decoded = decoder.DecodeAllMipMaps2D(inputStream); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The decoded Rgba32 image. + public static Image DecodeToImageRgba32(this BcDecoder decoder, KtxFile file) + { + return ColorMemoryToImage(decoder.Decode2D(file)); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// An array of decoded Rgba32 images. + public static Image[] DecodeAllMipMapsToImageRgba32(this BcDecoder decoder, KtxFile file) + { + var decoded = decoder.DecodeAllMipMaps2D(file); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The decoded Rgba32 image. + public static Image DecodeToImageRgba32(this BcDecoder decoder, DdsFile file) + { + return ColorMemoryToImage(decoder.Decode2D(file)); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// An array of decoded Rgba32 images. + public static Image[] DecodeAllMipMapsToImageRgba32(this BcDecoder decoder, DdsFile file) + { + var decoded = decoder.DecodeAllMipMaps2D(file); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The cancellation token for this asynchronous operation. + /// The decoded Rgba32 image. + public static async Task> DecodeRawToImageRgba32Async(this BcDecoder decoder, Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + return ColorMemoryToImage(await decoder.DecodeRaw2DAsync(inputStream, pixelWidth, pixelHeight, format, token)); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The array containing the encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The cancellation token for this asynchronous operation. + /// The decoded Rgba32 image. + public static async Task> DecodeRawToImageRgba32Async(this BcDecoder decoder, byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + return ColorMemoryToImage(await decoder.DecodeRaw2DAsync(input, pixelWidth, pixelHeight, format, token)); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// + /// The stream that contains either ktx or dds file. + /// The cancellation token for this asynchronous operation. + /// The decoded Rgba32 image. + public static async Task> DecodeToImageRgba32Async(this BcDecoder decoder, Stream inputStream, CancellationToken token = default) + { + return ColorMemoryToImage(await decoder.Decode2DAsync(inputStream, token)); + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// + /// The stream that contains either ktx or dds file. + /// The cancellation token for this asynchronous operation. + /// An array of decoded Rgba32 images. + public static async Task[]> DecodeAllMipMapsToImageRgba32Async(this BcDecoder decoder, Stream inputStream, CancellationToken token = default) + { + var decoded = await decoder.DecodeAllMipMaps2DAsync(inputStream, token); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The decoded Rgba32 image. + public static async Task> DecodeToImageRgba32Async(this BcDecoder decoder, KtxFile file, CancellationToken token = default) + { + return ColorMemoryToImage(await decoder.Decode2DAsync(file, token)); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// An array of decoded Rgba32 images. + public static async Task[]> DecodeAllMipMapsToImageRgba32Async(this BcDecoder decoder, KtxFile file, CancellationToken token = default) + { + var decoded = await decoder.DecodeAllMipMaps2DAsync(file, token); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The decoded Rgba32 image. + public static async Task> DecodeToImageRgba32Async(this BcDecoder decoder, DdsFile file, CancellationToken token = default) + { + return ColorMemoryToImage(await decoder.Decode2DAsync(file, token)); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// An array of decoded Rgba32 images. + public static async Task[]> DecodeAllMipMapsToImageRgba32Async(this BcDecoder decoder, DdsFile file, CancellationToken token = default) + { + var decoded = await decoder.DecodeAllMipMaps2DAsync(file, token); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + + + private static Image ColorMemoryToImage(Memory2D colors) + { + var output = new Image(colors.Width, colors.Height); + for (var y = 0; y < colors.Height; y++) + { + var yPixels = output.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + var yColors = colors.Span.GetRowSpan(y); + + MemoryMarshal.Cast(yColors).CopyTo(yPixels); + } + return output; + } + } +} diff --git a/BCnEncoder.NET.ImageSharp/BCnEncoder.NET.ImageSharp.csproj b/BCnEncoder.NET.ImageSharp/BCnEncoder.NET.ImageSharp.csproj new file mode 100644 index 0000000..ef8673c --- /dev/null +++ b/BCnEncoder.NET.ImageSharp/BCnEncoder.NET.ImageSharp.csproj @@ -0,0 +1,36 @@ + + + + net6.0 + + true + true + snupkg + true + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + BCnEncoder.Net.ImageSharp + BCnEncoder.Net.ImageSharp + Nominom + Adds ImageSharp apis to BCnEncoder.Net + MIT OR Unlicense + https://github.com/Nominom/BCnEncoder.NET + https://github.com/Nominom/BCnEncoder.NET + git + Update to ImageSharp 3.1.12 + 1.1.3 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/BCnEncoder.NET.ImageSharp/BCnEncoderExtensions.cs b/BCnEncoder.NET.ImageSharp/BCnEncoderExtensions.cs new file mode 100644 index 0000000..15c4640 --- /dev/null +++ b/BCnEncoder.NET.ImageSharp/BCnEncoderExtensions.cs @@ -0,0 +1,319 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using BCnEncoder.Encoder; +using BCnEncoder.Shared; +using BCnEncoder.Shared.ImageFiles; +using CommunityToolkit.HighPerformance; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace BCnEncoder.ImageSharp +{ + public static class BCnEncoderExtensions + { + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream. + /// + /// The image to encode. + /// The stream to write the encoded image to. + public static void EncodeToStream(this BcEncoder encoder, Image inputImage, Stream outputStream) + { + encoder.EncodeToStream(ImageToMemory2D(inputImage), outputStream); + } + + /// + /// Encodes all mipmap levels into a Ktx file. + /// + /// The image to encode. + /// The Ktx file containing the encoded image. + public static KtxFile EncodeToKtx(this BcEncoder encoder, Image inputImage) + { + return encoder.EncodeToKtx(ImageToMemory2D(inputImage)); + } + + /// + /// Encodes all mipmap levels into a Dds file. + /// + /// The image to encode. + /// The Dds file containing the encoded image. + public static DdsFile EncodeToDds(this BcEncoder encoder, Image inputImage) + { + return encoder.EncodeToDds(ImageToMemory2D(inputImage)); + } + + /// + /// Encodes all mipmap levels into an array of byte buffers. This data does not contain any file headers, just the raw encoded data. + /// + /// The image to encode. + /// A list of raw encoded data. + public static byte[][] EncodeToRawBytes(this BcEncoder encoder, Image inputImage) + { + return encoder.EncodeToRawBytes(ImageToMemory2D(inputImage)); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer. This data does not contain any file headers, just the raw encoded data. + /// + /// The image to encode. + /// The mipmap to encode. + /// The width of the mipmap. + /// The height of the mipmap. + /// The raw encoded data. + public static byte[] EncodeToRawBytes(this BcEncoder encoder, Image inputImage, int mipLevel, out int mipWidth, out int mipHeight) + { + return encoder.EncodeToRawBytes(ImageToMemory2D(inputImage), mipLevel, out mipWidth, out mipHeight); + } + + /// + /// Encodes all cubemap faces and mipmap levels into either a ktx or a dds file and writes it to the output stream. + /// Order is +X, -X, +Y, -Y, +Z, -Z + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The stream to write the encoded image to. + public static void EncodeCubeMapToStream(this BcEncoder encoder, Image right, Image left, Image top, Image down, + Image back, Image front, Stream outputStream) + { + encoder.EncodeCubeMapToStream( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front), + outputStream + ); + } + + /// + /// Encodes all cubemap faces and mipmap levels into a Ktx file. + /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The Ktx file containing the encoded image. + public static KtxFile EncodeCubeMapToKtx(this BcEncoder encoder, Image right, Image left, Image top, Image down, + Image back, Image front) + { + return encoder.EncodeCubeMapToKtx( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front) + ); + } + + /// + /// Encodes all cubemap faces and mipmap levels into a Dds file. + /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The Dds file containing the encoded image. + public static DdsFile EncodeCubeMapToDds(this BcEncoder encoder, Image right, Image left, Image top, Image down, + Image back, Image front) + { + return encoder.EncodeCubeMapToDds( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front) + ); + } + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously. + /// + /// The image to encode. + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + public static Task EncodeToStreamAsync(this BcEncoder encoder, Image inputImage, Stream outputStream, CancellationToken token = default) + { + return encoder.EncodeToStreamAsync(ImageToMemory2D(inputImage), outputStream, token); + } + + /// + /// Encodes all mipmap levels into a Ktx file asynchronously. + /// + /// The image to encode. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The Ktx file containing the encoded image. + public static Task EncodeToKtxAsync(this BcEncoder encoder, Image inputImage, CancellationToken token = default) + { + return encoder.EncodeToKtxAsync(ImageToMemory2D(inputImage), token); + } + + /// + /// Encodes all mipmap levels into a Dds file asynchronously. + /// + /// The image to encode. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The Dds file containing the encoded image. + public static Task EncodeToDdsAsync(this BcEncoder encoder, Image inputImage, CancellationToken token = default) + { + return encoder.EncodeToDdsAsync(ImageToMemory2D(inputImage), token); + } + + /// + /// Encodes all mipmap levels into an array of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded data. + /// + /// The image to encode. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// A list of raw encoded data. + public static Task EncodeToRawBytesAsync(this BcEncoder encoder, Image inputImage, CancellationToken token = default) + { + return encoder.EncodeToRawBytesAsync(ImageToMemory2D(inputImage), token); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded data. + /// + /// The image to encode. + /// The mipmap to encode. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The raw encoded data. + /// To get the width and height of the encoded mipLevel, see . + public static Task EncodeToRawBytesAsync(this BcEncoder encoder, Image inputImage, int mipLevel, CancellationToken token = default) + { + return encoder.EncodeToRawBytesAsync(ImageToMemory2D(inputImage), mipLevel, token); + } + + /// + /// Encodes all cubemap faces and mipmap levels into either a ktx or a dds file and writes it to the output stream asynchronously. + /// Order is +X, -X, +Y, -Y, +Z, -Z + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + public static Task EncodeCubeMapToStreamAsync(this BcEncoder encoder, Image right, Image left, Image top, Image down, + Image back, Image front, Stream outputStream, CancellationToken token = default) + { + return encoder.EncodeCubeMapToStreamAsync( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front), + outputStream, + token + ); + } + + /// + /// Encodes all cubemap faces and mipmap levels into a Ktx file asynchronously. + /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The Ktx file containing the encoded image. + public static Task EncodeCubeMapToKtxAsync(this BcEncoder encoder, Image right, Image left, Image top, + Image down, Image back, Image front, CancellationToken token = default) + { + return encoder.EncodeCubeMapToKtxAsync( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front), + token + ); + } + + /// + /// Encodes all cubemap faces and mipmap levels into a Dds file asynchronously. + /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The Dds file containing the encoded image. + public static Task EncodeCubeMapToDdsAsync(this BcEncoder encoder, Image right, Image left, Image top, + Image down, Image back, Image front, CancellationToken token = default) + { + return encoder.EncodeCubeMapToDdsAsync( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front), + token + ); + } + + /// + /// Calculates the number of mipmap levels that will be generated for the given input image. + /// + /// The image to use for the calculation. + /// The number of mipmap levels that will be generated for the input image. + public static int CalculateNumberOfMipLevels(this BcEncoder encoder, Image inputImage) + { + return encoder.CalculateNumberOfMipLevels(inputImage.Width, inputImage.Height); + } + + /// + /// Calculates the size of a given mipmap level. + /// + /// The image to use for the calculation. + /// The mipLevel to calculate (0 is original image) + /// The mipmap width calculated + /// The mipmap height calculated + public static void CalculateMipMapSize(this BcEncoder encoder, Image inputImage, int mipLevel, out int mipWidth, out int mipHeight) + { + encoder.CalculateMipMapSize(inputImage.Width, inputImage.Height, + mipLevel, out mipWidth, out mipHeight); + } + + + private static Memory2D ImageToMemory2D(Image inputImage) + { + var pixels = inputImage.GetPixelMemoryGroup()[0]; + var colors = new ColorRgba32[inputImage.Width * inputImage.Height]; + for (var y = 0; y < inputImage.Height; y++) + { + var yPixels = inputImage.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + var yColors = colors.AsSpan(y * inputImage.Width, inputImage.Width); + + MemoryMarshal.Cast(yPixels).CopyTo(yColors); + } + var memory = colors.AsMemory().AsMemory2D(inputImage.Height, inputImage.Width); + return memory; + } + } +} diff --git a/README.md b/README.md index 04c2c38..2e157c7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ [![Nuget](https://img.shields.io/nuget/v/BCnEncoder.Net)](https://www.nuget.org/packages/BCnEncoder.Net/) -![Tests](https://github.com/Nominom/BCnEncoder.NET/workflows/Tests/badge.svg) +[![Tests](https://github.com/Nominom/BCnEncoder.NET/actions/workflows/dotnetcore.yml/badge.svg)](https://github.com/Nominom/BCnEncoder.NET/actions/workflows/dotnetcore.yml) # BCnEncoder.NET A Cross-platform BCn / DXT encoding libary for .NET # What is it? -BCnEncoder.NET is a library for compressing rgba images to different block-compressed formats. It has no native dependencies and is .NET Standard 2.1 compatible. +BCnEncoder.NET is a library for compressing rgba images to different block-compressed formats. It has no native dependencies and is .NET Standard 2.1 & 2.0 compatible. Supported formats are: - Raw unsigned byte R, RG, RGB and RGBA formats @@ -14,18 +14,48 @@ Supported formats are: - BC3 (S3TC DXT5) - BC4 (RGTC1) - BC5 (RGTC2) + - BC6 (BPTC_FLOAT) - BC7 (BPTC) # Current state The current state of this library is in development but quite usable. I'm planning on implementing support for more codecs and -different algorithms. The current version is capable of encoding and decoding BC1-BC5 and BC7 images in both KTX or DDS formats. +different algorithms. The current version is capable of encoding and decoding BC1-BC7 images in both KTX or DDS formats. + +.NET Standard 2.0 support is experimental, and relies on [PolySharp](https://www.nuget.org/packages/PolySharp) & [Microsoft.Bcl.Numerics](https://www.nuget.org/packages/Microsoft.Bcl.Numerics). + +Please note, that the API might change between versions. # Dependencies Current dependencies are: -* [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) licenced under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) licence for image loading and saving +* [CommunityToolkit.HighPerformance](https://www.nuget.org/packages/CommunityToolkit.HighPerformance/) licensed under the [MIT](https://opensource.org/licenses/MIT) license for Span2D and Memory2D types. + +Additionally, for .NET Standard 2.0: +* [PolySharp](https://www.nuget.org/packages/PolySharp) licensed under the [MIT](https://opensource.org/licenses/MIT) license. +* [Microsoft.Bcl.Numerics](https://www.nuget.org/packages/Microsoft.Bcl.Numerics) licensed under the [MIT](https://opensource.org/licenses/MIT) license. + +# Image library extensions +This library has extension packages available for the following image libraries: + +[ImageSharp](https://www.nuget.org/packages/BCnEncoder.Net.ImageSharp/) + +The extension packages provide extension methods for ease of use with the image library. + +# Upgrading to 2.0 + +If you're upgrading from 1.X.X to version 2, expect some of your exsting code to be broken. ImageSharp was removed as a core dependency in version 2.0, so the code will no longer work with ImageSharp's Image types by default. You can install the extension package for ImageSharp to continue using this library easily with ImageSharp apis. # API -For more detailed usage examples, you can go look at the unit tests. +The below examples are using the ImageSharp extension package. For more detailed usage examples, you can go look at the unit tests. + +Remember add the following usings to the top of the file: +```CSharp +using BCnEncoder.Encoder; +using BCnEncoder.Decoder; +using BCnEncoder.Shared; +using BCnEncoder.ImageSharp; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +``` Here's an example on how to encode a png image to BC1 without alpha, and save it to a file. ```CSharp @@ -33,13 +63,13 @@ using Image image = Image.Load("example.png"); BcEncoder encoder = new BcEncoder(); -encoder.OutputOptions.generateMipMaps = true; -encoder.OutputOptions.quality = EncodingQuality.Balanced; -encoder.OutputOptions.format = CompressionFormat.BC1; -encoder.OutputOptions.fileFormat = OutputFileFormat.Ktx; //Change to Dds for a dds file. +encoder.OutputOptions.GenerateMipMaps = true; +encoder.OutputOptions.Quality = CompressionQuality.Balanced; +encoder.OutputOptions.Format = CompressionFormat.Bc1; +encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; //Change to Dds for a dds file. using FileStream fs = File.OpenWrite("example.ktx"); -encoder.Encode(image, fs); +encoder.EncodeToStream(image, fs); ``` And how to decode a compressed image from a KTX file and save it to png format. @@ -47,12 +77,41 @@ And how to decode a compressed image from a KTX file and save it to png format. using FileStream fs = File.OpenRead("compressed_bc1.ktx"); BcDecoder decoder = new BcDecoder(); -using Image image = decoder.Decode(fs); +using Image image = decoder.DecodeToImageRgba32(fs); using FileStream outFs = File.OpenWrite("decoding_test_bc1.png"); image.SaveAsPng(outFs); ``` +How to encode an HDR image with BC6H. +(HdrImage class reads and writes Radiance HDR files. This class is experimental and subject to be removed) +```CSharp +HdrImage image = HdrImage.Read("example.hdr"); + +BcEncoder encoder = new BcEncoder(); + +encoder.OutputOptions.GenerateMipMaps = true; +encoder.OutputOptions.Quality = CompressionQuality.Balanced; +encoder.OutputOptions.Format = CompressionFormat.Bc6U; +encoder.OutputOptions.FileFormat = OutputFileFormat.Ktx; //Change to Dds for a dds file. + +using FileStream fs = File.OpenWrite("example.ktx"); +encoder.EncodeToStreamHdr(image.PixelMemory, fs); +``` + +How to decode a BC6H encoded file. +```CSharp +using FileStream fs = File.OpenRead("compressed_bc6.ktx"); + +BcDecoder decoder = new BcDecoder(); +Memory2D pixels = decoder.DecodeHdr2D(fs); + +HdrImage image = new HdrImage(pixels.Span); + +using FileStream outFs = File.OpenWrite("decoded.hdr"); +image.Write(outFs); +``` + # TO-DO - [x] BC1 / DXT1 Encoding Without Alpha @@ -64,14 +123,15 @@ image.SaveAsPng(outFs); - [x] BC7 / BPTC Encoding - [x] DDS file support - [x] Implement PCA to remove Accord.Statistics dependency +- [x] BC6H HDR Encoding +- [ ] Performance improvements - [ ] ETC / ETC2 Encoding? -- [ ] Implement saving and loading basic image formats to remove ImageSharp dependency # Contributing All contributions are welcome. I'll try to respond to bug reports and feature requests as fast as possible, but you can also fix things yourself and submit a pull request. Please note, that by submitting a pull request you accept that your code will be dual licensed under MIT and public domain Unlicense. # License -This library is dual-licensed under the [Unlicense](https://unlicense.org/), and [MIT](https://opensource.org/licenses/MIT) licenses. +This library is dual-licensed under the [Unlicense](https://unlicense.org/) and [MIT](https://opensource.org/licenses/MIT) licenses. You may use this code under the terms of either license.