diff --git a/ImageSharp.Textures.sln b/ImageSharp.Textures.sln
index 636514f8..3f610385 100644
--- a/ImageSharp.Textures.sln
+++ b/ImageSharp.Textures.sln
@@ -49,35 +49,92 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Textures.Astc", "src\ImageSharp.Textures.Astc\ImageSharp.Textures.Astc.csproj", "{AE37301B-3635-4C61-A026-DEB2E1328DD1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Textures.Astc.Reference.Tests", "tests\ImageSharp.Textures.Astc.Reference.Tests\ImageSharp.Textures.Astc.Reference.Tests.csproj", "{E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}"
+EndProject
Global
- GlobalSection(SharedMSBuildProjectFiles) = preSolution
- shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{1588f6c4-2186-4a35-9693-e9f296791393}*SharedItemsImports = 5
- tests\Images\Images.projitems*{17fcbd4d-d232-45e8-876f-dfbc2fad52cf}*SharedItemsImports = 5
- tests\Images\Images.projitems*{18be79b6-6b95-4ed7-a963-ad75f6cb9f3c}*SharedItemsImports = 5
- tests\Images\Images.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13
- tests\Images\Images.projitems*{b159ffd1-e646-42d0-892c-4abf69103712}*SharedItemsImports = 5
- EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1588F6C4-2186-4A35-9693-E9F296791393}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1588F6C4-2186-4A35-9693-E9F296791393}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1588F6C4-2186-4A35-9693-E9F296791393}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1588F6C4-2186-4A35-9693-E9F296791393}.Debug|x64.Build.0 = Debug|Any CPU
+ {1588F6C4-2186-4A35-9693-E9F296791393}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1588F6C4-2186-4A35-9693-E9F296791393}.Debug|x86.Build.0 = Debug|Any CPU
{1588F6C4-2186-4A35-9693-E9F296791393}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1588F6C4-2186-4A35-9693-E9F296791393}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1588F6C4-2186-4A35-9693-E9F296791393}.Release|x64.ActiveCfg = Release|Any CPU
+ {1588F6C4-2186-4A35-9693-E9F296791393}.Release|x64.Build.0 = Release|Any CPU
+ {1588F6C4-2186-4A35-9693-E9F296791393}.Release|x86.ActiveCfg = Release|Any CPU
+ {1588F6C4-2186-4A35-9693-E9F296791393}.Release|x86.Build.0 = Release|Any CPU
{B159FFD1-E646-42D0-892C-4ABF69103712}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B159FFD1-E646-42D0-892C-4ABF69103712}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B159FFD1-E646-42D0-892C-4ABF69103712}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B159FFD1-E646-42D0-892C-4ABF69103712}.Debug|x64.Build.0 = Debug|Any CPU
+ {B159FFD1-E646-42D0-892C-4ABF69103712}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B159FFD1-E646-42D0-892C-4ABF69103712}.Debug|x86.Build.0 = Debug|Any CPU
{B159FFD1-E646-42D0-892C-4ABF69103712}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B159FFD1-E646-42D0-892C-4ABF69103712}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B159FFD1-E646-42D0-892C-4ABF69103712}.Release|x64.ActiveCfg = Release|Any CPU
+ {B159FFD1-E646-42D0-892C-4ABF69103712}.Release|x64.Build.0 = Release|Any CPU
+ {B159FFD1-E646-42D0-892C-4ABF69103712}.Release|x86.ActiveCfg = Release|Any CPU
+ {B159FFD1-E646-42D0-892C-4ABF69103712}.Release|x86.Build.0 = Release|Any CPU
{18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Debug|x64.Build.0 = Debug|Any CPU
+ {18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Debug|x86.Build.0 = Debug|Any CPU
{18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Release|x64.ActiveCfg = Release|Any CPU
+ {18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Release|x64.Build.0 = Release|Any CPU
+ {18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Release|x86.ActiveCfg = Release|Any CPU
+ {18BE79B6-6B95-4ED7-A963-AD75F6CB9F3C}.Release|x86.Build.0 = Release|Any CPU
{17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Debug|x64.Build.0 = Debug|Any CPU
+ {17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Debug|x86.Build.0 = Debug|Any CPU
{17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Release|x64.ActiveCfg = Release|Any CPU
+ {17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Release|x64.Build.0 = Release|Any CPU
+ {17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Release|x86.ActiveCfg = Release|Any CPU
+ {17FCBD4D-D232-45E8-876F-DFBC2FAD52CF}.Release|x86.Build.0 = Release|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Debug|x64.Build.0 = Debug|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Debug|x86.Build.0 = Debug|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Release|x64.ActiveCfg = Release|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Release|x64.Build.0 = Release|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Release|x86.ActiveCfg = Release|Any CPU
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1}.Release|x86.Build.0 = Release|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Debug|x64.Build.0 = Debug|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Debug|x86.Build.0 = Debug|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Release|x64.ActiveCfg = Release|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Release|x64.Build.0 = Release|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Release|x86.ActiveCfg = Release|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -90,8 +147,17 @@ Global
{17FCBD4D-D232-45E8-876F-DFBC2FAD52CF} = {6DF92068-B792-4038-8E3F-5FDF2E026BE7}
{E6224AB7-6987-4BA1-B2A6-ECFB7DA281DE} = {9F1F0B0F-704F-4B71-89EF-EE36042A27C9}
{9F19EBB4-32DB-4AFE-A5E4-722EDFAAE04B} = {E6224AB7-6987-4BA1-B2A6-ECFB7DA281DE}
+ {AE37301B-3635-4C61-A026-DEB2E1328DD1} = {5E6840D2-9CBB-4FDE-8378-33086CF5A8D8}
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720} = {6DF92068-B792-4038-8E3F-5FDF2E026BE7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F1762A0D-74C4-454A-BCB7-C010BB067E58}
EndGlobalSection
+ GlobalSection(SharedMSBuildProjectFiles) = preSolution
+ shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{1588f6c4-2186-4a35-9693-e9f296791393}*SharedItemsImports = 5
+ tests\Images\Images.projitems*{17fcbd4d-d232-45e8-876f-dfbc2fad52cf}*SharedItemsImports = 5
+ tests\Images\Images.projitems*{18be79b6-6b95-4ed7-a963-ad75f6cb9f3c}*SharedItemsImports = 5
+ tests\Images\Images.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13
+ tests\Images\Images.projitems*{b159ffd1-e646-42d0-892c-4abf69103712}*SharedItemsImports = 5
+ EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index 5be8e659..6fb3d82e 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ SixLabors.ImageSharp.Textures
[](https://github.com/SixLabors/ImageSharp.Textures/actions)
[](https://codecov.io/gh/SixLabors/ImageSharp)
-[](https://opensource.org/licenses/Apache-2.0)
+[](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
[](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors)
@@ -33,6 +33,7 @@ with the following compressions:
- BC5
- BC6H
- BC7
+- ASTC
Encoding textures is **not** yet supported. PR are of course very welcome.
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 2813cc4b..7060cf45 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -11,7 +11,7 @@
-->
-
+
@@ -25,6 +25,7 @@
+
diff --git a/src/ImageSharp.Textures.Astc/AstcDecoder.cs b/src/ImageSharp.Textures.Astc/AstcDecoder.cs
new file mode 100644
index 00000000..c243265e
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/AstcDecoder.cs
@@ -0,0 +1,447 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Buffers.Binary;
+using SixLabors.ImageSharp.Textures.Astc.BlockDecoder;
+using SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+using SixLabors.ImageSharp.Textures.Astc.IO;
+using SixLabors.ImageSharp.Textures.Astc.TexelBlock;
+
+namespace SixLabors.ImageSharp.Textures.Astc;
+
+///
+/// Provides methods to decode ASTC-compressed texture data into uncompressed pixel formats.
+///
+public static class AstcDecoder
+{
+ private static readonly ArrayPool ArrayPool = ArrayPool.Shared;
+ private const int BytesPerPixelUnorm8 = 4;
+
+ ///
+ /// Decompresses ASTC-compressed data to uncompressed RGBA8 format (4 bytes per pixel).
+ ///
+ /// The ASTC-compressed texture data
+ /// Image width in pixels
+ /// Image height in pixels
+ /// The ASTC block footprint (e.g., 4x4, 5x5)
+ /// Array of bytes in RGBA8 format (width * height * 4 bytes total)
+ /// If decompression fails for any block
+ public static Span DecompressImage(ReadOnlySpan astcData, int width, int height, Footprint footprint)
+ {
+ byte[] imageBuffer = new byte[width * height * BytesPerPixelUnorm8];
+
+ return DecompressImage(astcData, width, height, footprint, imageBuffer)
+ ? imageBuffer
+ : [];
+ }
+
+ ///
+ /// Decompresses ASTC-compressed data to uncompressed RGBA8 format into a caller-provided buffer.
+ ///
+ /// The ASTC-compressed texture data
+ /// Image width in pixels
+ /// Image height in pixels
+ /// The ASTC block footprint (e.g., 4x4, 5x5)
+ /// Output buffer. Must be at least width * height * 4 bytes.
+ /// True if decompression succeeded, false if input was invalid.
+ /// If decompression fails for any block
+ public static bool DecompressImage(ReadOnlySpan astcData, int width, int height, Footprint footprint, Span imageBuffer)
+ {
+ if (!TryGetBlockLayout(astcData, width, height, footprint, out int blocksWide, out int blocksHigh))
+ {
+ return false;
+ }
+
+ byte[] decodedBlock = [];
+
+ try
+ {
+ // Create a buffer once for fallback blocks; fast path writes directly to image
+ decodedBlock = ArrayPool.Rent(footprint.Width * footprint.Height * BytesPerPixelUnorm8);
+ Span decodedPixels = decodedBlock.AsSpan();
+ int blockIndex = 0;
+ int footprintWidth = footprint.Width;
+ int footprintHeight = footprint.Height;
+
+ for (int blockY = 0; blockY < blocksHigh; blockY++)
+ {
+ for (int blockX = 0; blockX < blocksWide; blockX++)
+ {
+ int blockDataOffset = blockIndex++ * PhysicalBlock.SizeInBytes;
+ if (blockDataOffset + PhysicalBlock.SizeInBytes > astcData.Length)
+ {
+ continue;
+ }
+
+ ulong low = BinaryPrimitives.ReadUInt64LittleEndian(astcData[blockDataOffset..]);
+ ulong high = BinaryPrimitives.ReadUInt64LittleEndian(astcData[(blockDataOffset + 8)..]);
+ UInt128 blockBits = new(high, low);
+
+ int dstBaseX = blockX * footprintWidth;
+ int dstBaseY = blockY * footprintHeight;
+ int copyWidth = Math.Min(footprintWidth, width - dstBaseX);
+ int copyHeight = Math.Min(footprintHeight, height - dstBaseY);
+
+ BlockInfo info = BlockInfo.Decode(blockBits);
+ if (!info.IsValid)
+ {
+ continue;
+ }
+
+ // Fast path: fuse decode directly into image buffer for interior full blocks
+ if (!info.IsVoidExtent && info.PartitionCount == 1 && !info.IsDualPlane
+ && !info.EndpointMode0.IsHdr()
+ && copyWidth == footprintWidth && copyHeight == footprintHeight)
+ {
+ FusedLdrBlockDecoder.DecompressBlockFusedLdrToImage(
+ blockBits,
+ in info,
+ footprint,
+ dstBaseX,
+ dstBaseY,
+ width,
+ imageBuffer);
+ continue;
+ }
+
+ // Fallback: decode to temp buffer, then copy
+ if (!info.IsVoidExtent && info.PartitionCount == 1 && !info.IsDualPlane
+ && !info.EndpointMode0.IsHdr())
+ {
+ FusedLdrBlockDecoder.DecompressBlockFusedLdr(blockBits, in info, footprint, decodedPixels);
+ }
+ else
+ {
+ LogicalBlock? logicalBlock = LogicalBlock.UnpackLogicalBlock(footprint, blockBits, in info);
+ if (logicalBlock is null)
+ {
+ continue;
+ }
+
+ logicalBlock.WriteAllPixelsLdr(footprint, decodedPixels);
+ }
+
+ int copyBytes = copyWidth * BytesPerPixelUnorm8;
+ for (int pixelY = 0; pixelY < copyHeight; pixelY++)
+ {
+ int srcOffset = pixelY * footprintWidth * BytesPerPixelUnorm8;
+ int dstOffset = (((dstBaseY + pixelY) * width) + dstBaseX) * BytesPerPixelUnorm8;
+ decodedPixels.Slice(srcOffset, copyBytes)
+ .CopyTo(imageBuffer.Slice(dstOffset, copyBytes));
+ }
+ }
+ }
+ }
+ finally
+ {
+ ArrayPool.Return(decodedBlock);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Decompress a single ASTC block to RGBA8 pixel data
+ ///
+ /// The data to decode
+ /// The type of ASTC block footprint e.g. 4x4, 5x5, etc.
+ /// The decoded block of pixels as RGBA values
+ public static Span DecompressBlock(ReadOnlySpan blockData, Footprint footprint)
+ {
+ byte[] decodedPixels = [];
+ try
+ {
+ decodedPixels = ArrayPool.Rent(footprint.Width * footprint.Height * BytesPerPixelUnorm8);
+ Span decodedPixelBuffer = decodedPixels.AsSpan();
+
+ DecompressBlock(blockData, footprint, decodedPixelBuffer);
+ }
+ finally
+ {
+ ArrayPool.Return(decodedPixels);
+ }
+
+ return decodedPixels;
+ }
+
+ ///
+ /// Decompresses a single ASTC block to RGBA8 pixel data
+ ///
+ /// The data to decode
+ /// The type of ASTC block footprint e.g. 4x4, 5x5, etc.
+ /// The buffer to write the decoded pixels into
+ public static void DecompressBlock(ReadOnlySpan blockData, Footprint footprint, Span buffer)
+ {
+ // Read the 16 bytes that make up the ASTC block as a 128-bit value
+ ulong low = BinaryPrimitives.ReadUInt64LittleEndian(blockData);
+ ulong high = BinaryPrimitives.ReadUInt64LittleEndian(blockData[8..]);
+ UInt128 blockBits = new(high, low);
+
+ BlockInfo info = BlockInfo.Decode(blockBits);
+ if (!info.IsValid)
+ {
+ return;
+ }
+
+ // Fully fused fast path for single-partition, non-dual-plane, LDR blocks
+ if (!info.IsVoidExtent && info.PartitionCount == 1 && !info.IsDualPlane
+ && !info.EndpointMode0.IsHdr())
+ {
+ FusedLdrBlockDecoder.DecompressBlockFusedLdr(blockBits, in info, footprint, buffer);
+ return;
+ }
+
+ // Fallback for void extent, multi-partition, dual plane, HDR
+ LogicalBlock? logicalBlock = LogicalBlock.UnpackLogicalBlock(footprint, blockBits, in info);
+ if (logicalBlock is null)
+ {
+ return;
+ }
+
+ logicalBlock.WriteAllPixelsLdr(footprint, buffer);
+ }
+
+ ///
+ /// Decompresses ASTC-compressed data to RGBA values.
+ ///
+ /// The ASTC-compressed texture data
+ /// Image width in pixels
+ /// Image height in pixels
+ /// The ASTC block footprint (e.g., 4x4, 5x5)
+ ///
+ /// Values in RGBA order. For HDR content, values may exceed 1.0.
+ ///
+ public static Span DecompressHdrImage(ReadOnlySpan astcData, int width, int height, Footprint footprint)
+ {
+ const int channelsPerPixel = 4;
+ float[] imageBuffer = new float[width * height * channelsPerPixel];
+ if (!DecompressHdrImage(astcData, width, height, footprint, imageBuffer))
+ {
+ return [];
+ }
+
+ return imageBuffer;
+ }
+
+ ///
+ /// Decompresses ASTC-compressed data to RGBA float values into a caller-provided buffer.
+ ///
+ /// The ASTC-compressed texture data
+ /// Image width in pixels
+ /// Image height in pixels
+ /// The ASTC block footprint (e.g., 4x4, 5x5)
+ /// Output buffer. Must be at least width * height * 4 floats.
+ /// True if decompression succeeded, false if input was invalid.
+ /// If decompression fails for any block
+ public static bool DecompressHdrImage(ReadOnlySpan astcData, int width, int height, Footprint footprint, Span imageBuffer)
+ {
+ if (!TryGetBlockLayout(astcData, width, height, footprint, out int blocksWide, out int blocksHigh))
+ {
+ return false;
+ }
+
+ const int channelsPerPixel = 4;
+ float[] decodedBlock = [];
+
+ try
+ {
+ // Create a buffer once for fallback blocks; fast path writes directly to image
+ decodedBlock = ArrayPool.Shared.Rent(footprint.Width * footprint.Height * channelsPerPixel);
+ Span decodedPixels = decodedBlock.AsSpan();
+ int blockIndex = 0;
+ int footprintWidth = footprint.Width;
+ int footprintHeight = footprint.Height;
+
+ for (int blockY = 0; blockY < blocksHigh; blockY++)
+ {
+ for (int blockX = 0; blockX < blocksWide; blockX++)
+ {
+ int blockDataOffset = blockIndex++ * PhysicalBlock.SizeInBytes;
+ if (blockDataOffset + PhysicalBlock.SizeInBytes > astcData.Length)
+ {
+ continue;
+ }
+
+ ulong low = BinaryPrimitives.ReadUInt64LittleEndian(astcData[blockDataOffset..]);
+ ulong high = BinaryPrimitives.ReadUInt64LittleEndian(astcData[(blockDataOffset + 8)..]);
+ UInt128 blockBits = new(high, low);
+
+ int dstBaseX = blockX * footprintWidth;
+ int dstBaseY = blockY * footprintHeight;
+ int copyWidth = Math.Min(footprintWidth, width - dstBaseX);
+ int copyHeight = Math.Min(footprintHeight, height - dstBaseY);
+
+ BlockInfo info = BlockInfo.Decode(blockBits);
+ if (!info.IsValid)
+ {
+ continue;
+ }
+
+ // Fast path: fuse decode directly into image buffer for interior full blocks
+ if (!info.IsVoidExtent && info.PartitionCount == 1 && !info.IsDualPlane
+ && copyWidth == footprintWidth && copyHeight == footprintHeight)
+ {
+ FusedHdrBlockDecoder.DecompressBlockFusedHdrToImage(
+ blockBits,
+ in info,
+ footprint,
+ dstBaseX,
+ dstBaseY,
+ width,
+ imageBuffer);
+ continue;
+ }
+
+ // Fused decode to temp buffer for single-partition non-dual-plane
+ if (!info.IsVoidExtent && info.PartitionCount == 1 && !info.IsDualPlane)
+ {
+ FusedHdrBlockDecoder.DecompressBlockFusedHdr(blockBits, in info, footprint, decodedPixels);
+ }
+ else
+ {
+ // Fallback: LogicalBlock path for void extent, multi-partition, dual plane
+ LogicalBlock? logicalBlock = LogicalBlock.UnpackLogicalBlock(footprint, blockBits, in info);
+ if (logicalBlock is null)
+ {
+ continue;
+ }
+
+ for (int row = 0; row < footprintHeight; row++)
+ {
+ for (int column = 0; column < footprintWidth; ++column)
+ {
+ int pixelOffset = (footprintWidth * row * channelsPerPixel) + (column * channelsPerPixel);
+ logicalBlock.WriteHdrPixel(column, row, decodedPixels.Slice(pixelOffset, channelsPerPixel));
+ }
+ }
+ }
+
+ int copyFloats = copyWidth * channelsPerPixel;
+ for (int pixelY = 0; pixelY < copyHeight; pixelY++)
+ {
+ int srcOffset = pixelY * footprintWidth * channelsPerPixel;
+ int dstOffset = (((dstBaseY + pixelY) * width) + dstBaseX) * channelsPerPixel;
+ decodedPixels.Slice(srcOffset, copyFloats)
+ .CopyTo(imageBuffer.Slice(dstOffset, copyFloats));
+ }
+ }
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(decodedBlock);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Decompresses ASTC-compressed data to RGBA values.
+ ///
+ /// The ASTC-compressed texture data
+ /// Image width in pixels
+ /// Image height in pixels
+ /// The ASTC block footprint type
+ ///
+ /// Values in RGBA order. For HDR content, values may exceed 1.0.
+ ///
+ public static Span DecompressHdrImage(ReadOnlySpan astcData, int width, int height, FootprintType footprint)
+ {
+ Footprint footPrint = Footprint.FromFootprintType(footprint);
+ return DecompressHdrImage(astcData, width, height, footPrint);
+ }
+
+ ///
+ /// Decompresses a single ASTC block to float RGBA values.
+ ///
+ /// The 16-byte ASTC block to decode
+ /// The ASTC block footprint
+ /// The buffer to write decoded values into (must be at least footprint.Width * footprint.Height * 4 elements)
+ public static void DecompressHdrBlock(ReadOnlySpan blockData, Footprint footprint, Span buffer)
+ {
+ // Read the 16 bytes that make up the ASTC block as a 128-bit value
+ ulong low = BinaryPrimitives.ReadUInt64LittleEndian(blockData);
+ ulong high = BinaryPrimitives.ReadUInt64LittleEndian(blockData[8..]);
+ UInt128 blockBits = new(high, low);
+
+ BlockInfo info = BlockInfo.Decode(blockBits);
+ if (!info.IsValid)
+ {
+ return;
+ }
+
+ // Fused fast path for single-partition, non-dual-plane blocks
+ if (!info.IsVoidExtent && info.PartitionCount == 1 && !info.IsDualPlane)
+ {
+ FusedHdrBlockDecoder.DecompressBlockFusedHdr(blockBits, in info, footprint, buffer);
+ return;
+ }
+
+ // Fallback for void extent, multi-partition, dual plane
+ LogicalBlock? logicalBlock = LogicalBlock.UnpackLogicalBlock(footprint, blockBits, in info);
+ if (logicalBlock is null)
+ {
+ return;
+ }
+
+ const int channelsPerPixel = 4;
+ for (int row = 0; row < footprint.Height; row++)
+ {
+ for (int column = 0; column < footprint.Width; ++column)
+ {
+ int pixelOffset = (footprint.Width * row * channelsPerPixel) + (column * channelsPerPixel);
+ logicalBlock.WriteHdrPixel(column, row, buffer.Slice(pixelOffset, channelsPerPixel));
+ }
+ }
+ }
+
+ internal static Span DecompressImage(AstcFile file)
+ {
+ ArgumentNullException.ThrowIfNull(file);
+
+ return DecompressImage(file.Blocks, file.Width, file.Height, file.Footprint);
+ }
+
+ internal static Span DecompressImage(ReadOnlySpan astcData, int width, int height, FootprintType footprint)
+ {
+ Footprint footPrint = Footprint.FromFootprintType(footprint);
+
+ return DecompressImage(astcData, width, height, footPrint);
+ }
+
+ private static bool TryGetBlockLayout(
+ ReadOnlySpan astcData,
+ int width,
+ int height,
+ Footprint footprint,
+ out int blocksWide,
+ out int blocksHigh)
+ {
+ int blockWidth = footprint.Width;
+ int blockHeight = footprint.Height;
+ blocksWide = 0;
+ blocksHigh = 0;
+
+ if (blockWidth == 0 || blockHeight == 0 || width == 0 || height == 0)
+ {
+ return false;
+ }
+
+ blocksWide = (width + blockWidth - 1) / blockWidth;
+ if (blocksWide == 0)
+ {
+ return false;
+ }
+
+ blocksHigh = (height + blockHeight - 1) / blockHeight;
+ int expectedBlockCount = blocksWide * blocksHigh;
+ if (astcData.Length % PhysicalBlock.SizeInBytes != 0 || astcData.Length / PhysicalBlock.SizeInBytes != expectedBlockCount)
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BiseEncoding/BiseEncodingMode.cs b/src/ImageSharp.Textures.Astc/BiseEncoding/BiseEncodingMode.cs
new file mode 100644
index 00000000..ae168b4b
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BiseEncoding/BiseEncodingMode.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.BiseEncoding;
+
+///
+/// The encoding modes supported by BISE.
+///
+///
+/// Note that the values correspond to the number of symbols in each alphabet.
+///
+internal enum BiseEncodingMode
+{
+ Unknown = 0,
+ BitEncoding = 1,
+ TritEncoding = 3,
+ QuintEncoding = 5,
+}
diff --git a/src/ImageSharp.Textures.Astc/BiseEncoding/BoundedIntegerSequenceCodec.cs b/src/ImageSharp.Textures.Astc/BiseEncoding/BoundedIntegerSequenceCodec.cs
new file mode 100644
index 00000000..8ee2c9f9
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BiseEncoding/BoundedIntegerSequenceCodec.cs
@@ -0,0 +1,264 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.BiseEncoding;
+
+///
+/// The Bounded Integer Sequence Encoding (BISE) allows storage of character sequences using
+/// arbitrary alphabets of up to 256 symbols. Each alphabet size is encoded in the most
+/// space-efficient choice of bits, trits, and quints.
+///
+internal partial class BoundedIntegerSequenceCodec
+{
+ ///
+ /// The maximum number of bits needed to encode an ISE value.
+ ///
+ ///
+ /// The ASTC specification does not give a maximum number, however unquantized color
+ /// values have a maximum range of 255, meaning that we can't feasibly have more
+ /// than eight bits per value.
+ ///
+ private const int Log2MaxRangeForBits = 8;
+
+ ///
+ /// The number of bits used after each value to store the interleaved quint block.
+ ///
+ protected static readonly int[] InterleavedQuintBits = [3, 2, 2];
+
+ ///
+ /// The number of bits used after each value to store the interleaved trit block.
+ ///
+ protected static readonly int[] InterleavedTritBits = [2, 2, 1, 2, 1];
+
+ ///
+ /// Trit encodings for BISE blocks.
+ ///
+ ///
+ ///
+ /// These tables are used to decode the blocks of values encoded using the ASTC
+ /// integer sequence encoding. The theory is that five trits (values that can
+ /// take any number in the range [0, 2]) can take on a total of 3^5 = 243 total
+ /// values, which can be stored in eight bits. These eight bits are used to
+ /// decode the five trits based on the ASTC specification in Section C.2.12.
+ ///
+ ///
+ /// For simplicity, we have stored a look-up table here so that we don't need
+ /// to implement the decoding logic. Similarly, seven bits are used to decode
+ /// three quints.
+ ///
+ ///
+ protected static readonly int[][] TritEncodings =
+ [
+ [0, 0, 0, 0, 0], [1, 0, 0, 0, 0], [2, 0, 0, 0, 0], [0, 0, 2, 0, 0], [0, 1, 0, 0, 0], [1, 1, 0, 0, 0], [2, 1, 0, 0, 0], [1, 0, 2, 0, 0], [0, 2, 0, 0, 0],
+ [1, 2, 0, 0, 0], [2, 2, 0, 0, 0], [2, 0, 2, 0, 0], [0, 2, 2, 0, 0], [1, 2, 2, 0, 0], [2, 2, 2, 0, 0], [2, 0, 2, 0, 0], [0, 0, 1, 0, 0], [1, 0, 1, 0, 0],
+ [2, 0, 1, 0, 0], [0, 1, 2, 0, 0], [0, 1, 1, 0, 0], [1, 1, 1, 0, 0], [2, 1, 1, 0, 0], [1, 1, 2, 0, 0], [0, 2, 1, 0, 0], [1, 2, 1, 0, 0], [2, 2, 1, 0, 0],
+ [2, 1, 2, 0, 0], [0, 0, 0, 2, 2], [1, 0, 0, 2, 2], [2, 0, 0, 2, 2], [0, 0, 2, 2, 2], [0, 0, 0, 1, 0], [1, 0, 0, 1, 0], [2, 0, 0, 1, 0], [0, 0, 2, 1, 0],
+ [0, 1, 0, 1, 0], [1, 1, 0, 1, 0], [2, 1, 0, 1, 0], [1, 0, 2, 1, 0], [0, 2, 0, 1, 0], [1, 2, 0, 1, 0], [2, 2, 0, 1, 0], [2, 0, 2, 1, 0], [0, 2, 2, 1, 0],
+ [1, 2, 2, 1, 0], [2, 2, 2, 1, 0], [2, 0, 2, 1, 0], [0, 0, 1, 1, 0], [1, 0, 1, 1, 0], [2, 0, 1, 1, 0], [0, 1, 2, 1, 0], [0, 1, 1, 1, 0], [1, 1, 1, 1, 0],
+ [2, 1, 1, 1, 0], [1, 1, 2, 1, 0], [0, 2, 1, 1, 0], [1, 2, 1, 1, 0], [2, 2, 1, 1, 0], [2, 1, 2, 1, 0], [0, 1, 0, 2, 2], [1, 1, 0, 2, 2], [2, 1, 0, 2, 2],
+ [1, 0, 2, 2, 2], [0, 0, 0, 2, 0], [1, 0, 0, 2, 0], [2, 0, 0, 2, 0], [0, 0, 2, 2, 0], [0, 1, 0, 2, 0], [1, 1, 0, 2, 0], [2, 1, 0, 2, 0], [1, 0, 2, 2, 0],
+ [0, 2, 0, 2, 0], [1, 2, 0, 2, 0], [2, 2, 0, 2, 0], [2, 0, 2, 2, 0], [0, 2, 2, 2, 0], [1, 2, 2, 2, 0], [2, 2, 2, 2, 0], [2, 0, 2, 2, 0], [0, 0, 1, 2, 0],
+ [1, 0, 1, 2, 0], [2, 0, 1, 2, 0], [0, 1, 2, 2, 0], [0, 1, 1, 2, 0], [1, 1, 1, 2, 0], [2, 1, 1, 2, 0], [1, 1, 2, 2, 0], [0, 2, 1, 2, 0], [1, 2, 1, 2, 0],
+ [2, 2, 1, 2, 0], [2, 1, 2, 2, 0], [0, 2, 0, 2, 2], [1, 2, 0, 2, 2], [2, 2, 0, 2, 2], [2, 0, 2, 2, 2], [0, 0, 0, 0, 2], [1, 0, 0, 0, 2], [2, 0, 0, 0, 2],
+ [0, 0, 2, 0, 2], [0, 1, 0, 0, 2], [1, 1, 0, 0, 2], [2, 1, 0, 0, 2], [1, 0, 2, 0, 2], [0, 2, 0, 0, 2], [1, 2, 0, 0, 2], [2, 2, 0, 0, 2], [2, 0, 2, 0, 2],
+ [0, 2, 2, 0, 2], [1, 2, 2, 0, 2], [2, 2, 2, 0, 2], [2, 0, 2, 0, 2], [0, 0, 1, 0, 2], [1, 0, 1, 0, 2], [2, 0, 1, 0, 2], [0, 1, 2, 0, 2], [0, 1, 1, 0, 2],
+ [1, 1, 1, 0, 2], [2, 1, 1, 0, 2], [1, 1, 2, 0, 2], [0, 2, 1, 0, 2], [1, 2, 1, 0, 2], [2, 2, 1, 0, 2], [2, 1, 2, 0, 2], [0, 2, 2, 2, 2], [1, 2, 2, 2, 2],
+ [2, 2, 2, 2, 2], [2, 0, 2, 2, 2], [0, 0, 0, 0, 1], [1, 0, 0, 0, 1], [2, 0, 0, 0, 1], [0, 0, 2, 0, 1], [0, 1, 0, 0, 1], [1, 1, 0, 0, 1], [2, 1, 0, 0, 1],
+ [1, 0, 2, 0, 1], [0, 2, 0, 0, 1], [1, 2, 0, 0, 1], [2, 2, 0, 0, 1], [2, 0, 2, 0, 1], [0, 2, 2, 0, 1], [1, 2, 2, 0, 1], [2, 2, 2, 0, 1], [2, 0, 2, 0, 1],
+ [0, 0, 1, 0, 1], [1, 0, 1, 0, 1], [2, 0, 1, 0, 1], [0, 1, 2, 0, 1], [0, 1, 1, 0, 1], [1, 1, 1, 0, 1], [2, 1, 1, 0, 1], [1, 1, 2, 0, 1], [0, 2, 1, 0, 1],
+ [1, 2, 1, 0, 1], [2, 2, 1, 0, 1], [2, 1, 2, 0, 1], [0, 0, 1, 2, 2], [1, 0, 1, 2, 2], [2, 0, 1, 2, 2], [0, 1, 2, 2, 2], [0, 0, 0, 1, 1], [1, 0, 0, 1, 1],
+ [2, 0, 0, 1, 1], [0, 0, 2, 1, 1], [0, 1, 0, 1, 1], [1, 1, 0, 1, 1], [2, 1, 0, 1, 1], [1, 0, 2, 1, 1], [0, 2, 0, 1, 1], [1, 2, 0, 1, 1], [2, 2, 0, 1, 1],
+ [2, 0, 2, 1, 1], [0, 2, 2, 1, 1], [1, 2, 2, 1, 1], [2, 2, 2, 1, 1], [2, 0, 2, 1, 1], [0, 0, 1, 1, 1], [1, 0, 1, 1, 1], [2, 0, 1, 1, 1], [0, 1, 2, 1, 1],
+ [0, 1, 1, 1, 1], [1, 1, 1, 1, 1], [2, 1, 1, 1, 1], [1, 1, 2, 1, 1], [0, 2, 1, 1, 1], [1, 2, 1, 1, 1], [2, 2, 1, 1, 1], [2, 1, 2, 1, 1], [0, 1, 1, 2, 2],
+ [1, 1, 1, 2, 2], [2, 1, 1, 2, 2], [1, 1, 2, 2, 2], [0, 0, 0, 2, 1], [1, 0, 0, 2, 1], [2, 0, 0, 2, 1], [0, 0, 2, 2, 1], [0, 1, 0, 2, 1], [1, 1, 0, 2, 1],
+ [2, 1, 0, 2, 1], [1, 0, 2, 2, 1], [0, 2, 0, 2, 1], [1, 2, 0, 2, 1], [2, 2, 0, 2, 1], [2, 0, 2, 2, 1], [0, 2, 2, 2, 1], [1, 2, 2, 2, 1], [2, 2, 2, 2, 1],
+ [2, 0, 2, 2, 1], [0, 0, 1, 2, 1], [1, 0, 1, 2, 1], [2, 0, 1, 2, 1], [0, 1, 2, 2, 1], [0, 1, 1, 2, 1], [1, 1, 1, 2, 1], [2, 1, 1, 2, 1], [1, 1, 2, 2, 1],
+ [0, 2, 1, 2, 1], [1, 2, 1, 2, 1], [2, 2, 1, 2, 1], [2, 1, 2, 2, 1], [0, 2, 1, 2, 2], [1, 2, 1, 2, 2], [2, 2, 1, 2, 2], [2, 1, 2, 2, 2], [0, 0, 0, 1, 2],
+ [1, 0, 0, 1, 2], [2, 0, 0, 1, 2], [0, 0, 2, 1, 2], [0, 1, 0, 1, 2], [1, 1, 0, 1, 2], [2, 1, 0, 1, 2], [1, 0, 2, 1, 2], [0, 2, 0, 1, 2], [1, 2, 0, 1, 2],
+ [2, 2, 0, 1, 2], [2, 0, 2, 1, 2], [0, 2, 2, 1, 2], [1, 2, 2, 1, 2], [2, 2, 2, 1, 2], [2, 0, 2, 1, 2], [0, 0, 1, 1, 2], [1, 0, 1, 1, 2], [2, 0, 1, 1, 2],
+ [0, 1, 2, 1, 2], [0, 1, 1, 1, 2], [1, 1, 1, 1, 2], [2, 1, 1, 1, 2], [1, 1, 2, 1, 2], [0, 2, 1, 1, 2], [1, 2, 1, 1, 2], [2, 2, 1, 1, 2], [2, 1, 2, 1, 2],
+ [0, 2, 2, 2, 2], [1, 2, 2, 2, 2], [2, 2, 2, 2, 2], [2, 1, 2, 2, 2]
+ ];
+
+ ///
+ /// Quint encodings for BISE blocks.
+ ///
+ ///
+ /// See for more details.
+ ///
+ protected static readonly int[][] QuintEncodings =
+ [
+ [0, 0, 0], [1, 0, 0], [2, 0, 0], [3, 0, 0], [4, 0, 0], [0, 4, 0], [4, 4, 0], [4, 4, 4], [0, 1, 0], [1, 1, 0], [2, 1, 0], [3, 1, 0], [4, 1, 0],
+ [1, 4, 0], [4, 4, 1], [4, 4, 4], [0, 2, 0], [1, 2, 0], [2, 2, 0], [3, 2, 0], [4, 2, 0], [2, 4, 0], [4, 4, 2], [4, 4, 4], [0, 3, 0], [1, 3, 0],
+ [2, 3, 0], [3, 3, 0], [4, 3, 0], [3, 4, 0], [4, 4, 3], [4, 4, 4], [0, 0, 1], [1, 0, 1], [2, 0, 1], [3, 0, 1], [4, 0, 1], [0, 4, 1], [4, 0, 4],
+ [0, 4, 4], [0, 1, 1], [1, 1, 1], [2, 1, 1], [3, 1, 1], [4, 1, 1], [1, 4, 1], [4, 1, 4], [1, 4, 4], [0, 2, 1], [1, 2, 1], [2, 2, 1], [3, 2, 1],
+ [4, 2, 1], [2, 4, 1], [4, 2, 4], [2, 4, 4], [0, 3, 1], [1, 3, 1], [2, 3, 1], [3, 3, 1], [4, 3, 1], [3, 4, 1], [4, 3, 4], [3, 4, 4], [0, 0, 2],
+ [1, 0, 2], [2, 0, 2], [3, 0, 2], [4, 0, 2], [0, 4, 2], [2, 0, 4], [3, 0, 4], [0, 1, 2], [1, 1, 2], [2, 1, 2], [3, 1, 2], [4, 1, 2], [1, 4, 2],
+ [2, 1, 4], [3, 1, 4], [0, 2, 2], [1, 2, 2], [2, 2, 2], [3, 2, 2], [4, 2, 2], [2, 4, 2], [2, 2, 4], [3, 2, 4], [0, 3, 2], [1, 3, 2], [2, 3, 2],
+ [3, 3, 2], [4, 3, 2], [3, 4, 2], [2, 3, 4], [3, 3, 4], [0, 0, 3], [1, 0, 3], [2, 0, 3], [3, 0, 3], [4, 0, 3], [0, 4, 3], [0, 0, 4], [1, 0, 4],
+ [0, 1, 3], [1, 1, 3], [2, 1, 3], [3, 1, 3], [4, 1, 3], [1, 4, 3], [0, 1, 4], [1, 1, 4], [0, 2, 3], [1, 2, 3], [2, 2, 3], [3, 2, 3], [4, 2, 3],
+ [2, 4, 3], [0, 2, 4], [1, 2, 4], [0, 3, 3], [1, 3, 3], [2, 3, 3], [3, 3, 3], [4, 3, 3], [3, 4, 3], [0, 3, 4], [1, 3, 4]
+ ];
+
+ ///
+ /// The maximum ranges for BISE encoding.
+ ///
+ ///
+ /// These are the numbers between 1 and
+ /// that can be represented exactly as a number in the ranges
+ /// [0, 2^k), [0, 3 * 2^k), and [0, 5 * 2^k).
+ ///
+ internal static readonly int[] MaxRanges = [1, 2, 3, 4, 5, 7, 9, 11, 15, 19, 23, 31, 39, 47, 63, 79, 95, 127, 159, 191, 255];
+
+ // Flat encoding tables: eliminates jagged array indirection
+ protected static readonly int[] FlatTritEncodings = FlattenEncodings(TritEncodings, 5);
+ protected static readonly int[] FlatQuintEncodings = FlattenEncodings(QuintEncodings, 3);
+
+ private static readonly (BiseEncodingMode Mode, int BitCount)[] PackingModeCache = InitPackingModeCache();
+
+ ///
+ /// Initializes a new instance of the class.
+ /// operate on sequences of integers and produce bit patterns that pack the
+ /// integers based on the encoding scheme specified in the ASTC specification
+ /// Section C.2.12.
+ ///
+ ///
+ ///
+ /// The resulting bit pattern is a sequence of encoded blocks.
+ /// All blocks in a sequence are one of the following encodings:
+ ///
+ ///
+ /// - Bit encoding: one encoded value of the form 2^k
+ /// - Trit encoding: five encoded values of the form 3*2^k
+ /// - Quint encoding: three encoded values of the form 5*2^k
+ ///
+ /// The layouts of each block are designed such that the blocks can be truncated
+ /// during encoding in order to support variable length input sequences (i.e. a
+ /// sequence of values that are encoded using trit encoded blocks does not
+ /// need to have a multiple-of-five length).
+ ///
+ /// Creates a decoder that decodes values within [0, ] (inclusive).
+ protected BoundedIntegerSequenceCodec(int range)
+ {
+ (BiseEncodingMode encodingMode, int bitCount) = GetPackingModeBitCount(range);
+ this.Encoding = encodingMode;
+ this.BitCount = bitCount;
+ }
+
+ protected BiseEncodingMode Encoding { get; }
+
+ protected int BitCount { get; }
+
+ ///
+ /// The number of bits needed to encode the given number of values with respect to the
+ /// number of trits, quints, and bits specified by .
+ ///
+ public static (BiseEncodingMode Mode, int BitCount) GetPackingModeBitCount(int range)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(range, 0);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(range, 1 << Log2MaxRangeForBits);
+
+ return PackingModeCache[range];
+ }
+
+ ///
+ /// Returns the overall bit count for a range of values encoded
+ ///
+ public static int GetBitCount(BiseEncodingMode encodingMode, int valuesCount, int bitCount)
+ {
+ int encodingBitCount = encodingMode switch
+ {
+ BiseEncodingMode.TritEncoding => ((valuesCount * 8) + 4) / 5,
+ BiseEncodingMode.QuintEncoding => ((valuesCount * 7) + 2) / 3,
+ BiseEncodingMode.BitEncoding => 0,
+ _ => throw new ArgumentOutOfRangeException(nameof(encodingMode), "Invalid encoding mode"),
+ };
+ int baseBitCount = valuesCount * bitCount;
+
+ return encodingBitCount + baseBitCount;
+ }
+
+ ///
+ /// The number of bits needed to encode a given number of values within the range [0, ] (inclusive).
+ ///
+ public static int GetBitCountForRange(int valuesCount, int range)
+ {
+ (BiseEncodingMode mode, int bitCount) = GetPackingModeBitCount(range);
+
+ return GetBitCount(mode, valuesCount, bitCount);
+ }
+
+ ///
+ /// The size of a single ISE block in bits
+ ///
+ protected int GetEncodedBlockSize()
+ {
+ (int blockSize, int extraBlockSize) = this.Encoding switch
+ {
+ BiseEncodingMode.TritEncoding => (5, 8),
+ BiseEncodingMode.QuintEncoding => (3, 7),
+ BiseEncodingMode.BitEncoding => (1, 0),
+ _ => (0, 0),
+ };
+
+ return extraBlockSize + (blockSize * this.BitCount);
+ }
+
+ private static int[] FlattenEncodings(int[][] jagged, int stride)
+ {
+ int[] flat = new int[jagged.Length * stride];
+ for (int i = 0; i < jagged.Length; i++)
+ {
+ for (int j = 0; j < stride; j++)
+ {
+ flat[(i * stride) + j] = jagged[i][j];
+ }
+ }
+
+ return flat;
+ }
+
+ private static (BiseEncodingMode, int)[] InitPackingModeCache()
+ {
+ (BiseEncodingMode, int)[] cache = new (BiseEncodingMode, int)[1 << Log2MaxRangeForBits];
+
+ // Precompute for all valid ranges [1, 255]
+ for (int range = 1; range < cache.Length; range++)
+ {
+ int index = -1;
+ for (int i = 0; i < MaxRanges.Length; i++)
+ {
+ if (MaxRanges[i] >= range)
+ {
+ index = i;
+ break;
+ }
+ }
+
+ int maxValue = index < 0
+ ? MaxRanges[^1] + 1
+ : MaxRanges[index] + 1;
+
+ // Check QuintEncoding (5), TritEncoding (3), BitEncoding (1) in descending order
+ BiseEncodingMode encodingMode = BiseEncodingMode.Unknown;
+ ReadOnlySpan modes = [BiseEncodingMode.QuintEncoding, BiseEncodingMode.TritEncoding, BiseEncodingMode.BitEncoding];
+ foreach (BiseEncodingMode em in modes)
+ {
+ if (maxValue % (int)em == 0 && int.IsPow2(maxValue / (int)em))
+ {
+ encodingMode = em;
+ break;
+ }
+ }
+
+ if (encodingMode == BiseEncodingMode.Unknown)
+ {
+ throw new InvalidOperationException($"Invalid range for BISE encoding: {range}");
+ }
+
+ cache[range] = (encodingMode, int.Log2(maxValue / (int)encodingMode));
+ }
+
+ return cache;
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BiseEncoding/BoundedIntegerSequenceDecoder.cs b/src/ImageSharp.Textures.Astc/BiseEncoding/BoundedIntegerSequenceDecoder.cs
new file mode 100644
index 00000000..bf9ec0f9
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BiseEncoding/BoundedIntegerSequenceDecoder.cs
@@ -0,0 +1,159 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.IO;
+
+namespace SixLabors.ImageSharp.Textures.Astc.BiseEncoding;
+
+internal sealed class BoundedIntegerSequenceDecoder : BoundedIntegerSequenceCodec
+{
+ private static readonly BoundedIntegerSequenceDecoder?[] Cache = new BoundedIntegerSequenceDecoder?[256];
+
+ public BoundedIntegerSequenceDecoder(int range)
+ : base(range)
+ {
+ }
+
+ public static BoundedIntegerSequenceDecoder GetCached(int range)
+ {
+ BoundedIntegerSequenceDecoder? decoder = Cache[range];
+ if (decoder is null)
+ {
+ decoder = new BoundedIntegerSequenceDecoder(range);
+ Cache[range] = decoder;
+ }
+
+ return decoder;
+ }
+
+ ///
+ /// Decode a sequence of bounded integers into a caller-provided span.
+ ///
+ /// The number of values to decode.
+ /// The source of values to decode from.
+ /// The span to write decoded values into.
+ /// Thrown when the encoded block size is too large.
+ /// Thrown when there are not enough bits to decode.
+ public void Decode(int valuesCount, ref BitStream bitSource, Span result)
+ {
+ int totalBitCount = GetBitCount(this.Encoding, valuesCount, this.BitCount);
+ int bitsPerBlock = this.GetEncodedBlockSize();
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(bitsPerBlock, 64);
+
+ Span blockResult = stackalloc int[5];
+ int resultIndex = 0;
+ int bitsRemaining = totalBitCount;
+
+ while (bitsRemaining > 0)
+ {
+ int bitsToRead = Math.Min(bitsRemaining, bitsPerBlock);
+ if (!bitSource.TryGetBits(bitsToRead, out ulong blockBits))
+ {
+ throw new InvalidOperationException("Not enough bits in BitStream to decode BISE block");
+ }
+
+ if (this.Encoding == BiseEncodingMode.BitEncoding)
+ {
+ if (resultIndex < valuesCount)
+ {
+ result[resultIndex++] = (int)blockBits;
+ }
+ }
+ else
+ {
+ int decoded = DecodeISEBlock(this.Encoding, blockBits, this.BitCount, blockResult);
+ for (int i = 0; i < decoded && resultIndex < valuesCount; ++i)
+ {
+ result[resultIndex++] = blockResult[i];
+ }
+ }
+
+ bitsRemaining -= bitsPerBlock;
+ }
+
+ if (resultIndex < valuesCount)
+ {
+ throw new InvalidOperationException("Decoded fewer values than expected from BISE block");
+ }
+ }
+
+ ///
+ /// Decode a sequence of bounded integers. The number of bits read is dependent on the number
+ /// of bits required to encode based on the calculation provided
+ /// in Section C.2.22 of the ASTC specification.
+ ///
+ /// The number of values to decode.
+ /// The source of values to decode from.
+ /// The decoded values. The collection always contains exactly elements.
+ /// Thrown when the encoded block size is too large.
+ /// Thrown when there are not enough bits to decode.
+ public int[] Decode(int valuesCount, ref BitStream bitSource)
+ {
+ int[] result = new int[valuesCount];
+ this.Decode(valuesCount, ref bitSource, result);
+ return result;
+ }
+
+ ///
+ /// Decode a trit/quint block into a caller-provided span.
+ /// Returns the number of values written.
+ /// Uses direct bit extraction (no BitStream) and flat encoding tables.
+ ///
+ public static int DecodeISEBlock(BiseEncodingMode mode, ulong encodedBlock, int encodedBitCount, Span result)
+ {
+ ulong mantissaMask = (1UL << encodedBitCount) - 1;
+
+ if (mode == BiseEncodingMode.TritEncoding)
+ {
+ // 5 values, interleaved bits = [2, 2, 1, 2, 1] = 8 bits total
+ int bitPosition = 0;
+ int mantissa0 = (int)((encodedBlock >> bitPosition) & mantissaMask);
+ bitPosition += encodedBitCount;
+ ulong encodedTrits = (encodedBlock >> bitPosition) & 0x3;
+ bitPosition += 2;
+ int mantissa1 = (int)((encodedBlock >> bitPosition) & mantissaMask);
+ bitPosition += encodedBitCount;
+ encodedTrits |= ((encodedBlock >> bitPosition) & 0x3) << 2;
+ bitPosition += 2;
+ int mantissa2 = (int)((encodedBlock >> bitPosition) & mantissaMask);
+ bitPosition += encodedBitCount;
+ encodedTrits |= ((encodedBlock >> bitPosition) & 0x1) << 4;
+ bitPosition += 1;
+ int mantissa3 = (int)((encodedBlock >> bitPosition) & mantissaMask);
+ bitPosition += encodedBitCount;
+ encodedTrits |= ((encodedBlock >> bitPosition) & 0x3) << 5;
+ bitPosition += 2;
+ int mantissa4 = (int)((encodedBlock >> bitPosition) & mantissaMask);
+ encodedTrits |= ((encodedBlock >> (bitPosition + encodedBitCount)) & 0x1) << 7;
+
+ int tritTableBase = (int)encodedTrits * 5;
+ result[0] = (FlatTritEncodings[tritTableBase] << encodedBitCount) | mantissa0;
+ result[1] = (FlatTritEncodings[tritTableBase + 1] << encodedBitCount) | mantissa1;
+ result[2] = (FlatTritEncodings[tritTableBase + 2] << encodedBitCount) | mantissa2;
+ result[3] = (FlatTritEncodings[tritTableBase + 3] << encodedBitCount) | mantissa3;
+ result[4] = (FlatTritEncodings[tritTableBase + 4] << encodedBitCount) | mantissa4;
+ return 5;
+ }
+ else
+ {
+ // 3 values, interleaved bits = [3, 2, 2] = 7 bits total
+ int bitPosition = 0;
+ int mantissa0 = (int)((encodedBlock >> bitPosition) & mantissaMask);
+ bitPosition += encodedBitCount;
+ ulong encodedQuints = (encodedBlock >> bitPosition) & 0x7;
+ bitPosition += 3;
+ int mantissa1 = (int)((encodedBlock >> bitPosition) & mantissaMask);
+ bitPosition += encodedBitCount;
+ encodedQuints |= ((encodedBlock >> bitPosition) & 0x3) << 3;
+ bitPosition += 2;
+ int mantissa2 = (int)((encodedBlock >> bitPosition) & mantissaMask);
+ encodedQuints |= ((encodedBlock >> (bitPosition + encodedBitCount)) & 0x3) << 5;
+
+ int quintTableBase = (int)encodedQuints * 3;
+ result[0] = (FlatQuintEncodings[quintTableBase] << encodedBitCount) | mantissa0;
+ result[1] = (FlatQuintEncodings[quintTableBase + 1] << encodedBitCount) | mantissa1;
+ result[2] = (FlatQuintEncodings[quintTableBase + 2] << encodedBitCount) | mantissa2;
+ return 3;
+ }
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BiseEncoding/BoundedIntegerSequenceEncoder.cs b/src/ImageSharp.Textures.Astc/BiseEncoding/BoundedIntegerSequenceEncoder.cs
new file mode 100644
index 00000000..341e2c29
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BiseEncoding/BoundedIntegerSequenceEncoder.cs
@@ -0,0 +1,168 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.IO;
+
+namespace SixLabors.ImageSharp.Textures.Astc.BiseEncoding;
+
+internal sealed class BoundedIntegerSequenceEncoder : BoundedIntegerSequenceCodec
+{
+ private readonly List values = [];
+
+ public BoundedIntegerSequenceEncoder(int range)
+ : base(range)
+ {
+ }
+
+ ///
+ /// Adds a value to the encoding sequence.
+ ///
+ public void AddValue(int val) => this.values.Add(val);
+
+ ///
+ /// Encodes and writes the stored values encoding to the sink. Repeated calls will produce the same result.
+ ///
+ public void Encode(ref BitStream bitSink)
+ {
+ int totalBitCount = GetBitCount(this.Encoding, this.values.Count, this.BitCount);
+
+ int index = 0;
+ int bitsWrittenCount = 0;
+ while (index < this.values.Count)
+ {
+ switch (this.Encoding)
+ {
+ case BiseEncodingMode.TritEncoding:
+ List trits = [];
+ for (int i = 0; i < 5; ++i)
+ {
+ if (index < this.values.Count)
+ {
+ trits.Add(this.values[index++]);
+ }
+ else
+ {
+ trits.Add(0);
+ }
+ }
+
+ EncodeISEBlock(trits, this.BitCount, ref bitSink, ref bitsWrittenCount, totalBitCount);
+ break;
+ case BiseEncodingMode.QuintEncoding:
+ List quints = [];
+ for (int i = 0; i < 3; ++i)
+ {
+ int value = index < this.values.Count
+ ? this.values[index++]
+ : 0;
+ quints.Add(value);
+ }
+
+ EncodeISEBlock(quints, this.BitCount, ref bitSink, ref bitsWrittenCount, totalBitCount);
+ break;
+ case BiseEncodingMode.BitEncoding:
+ bitSink.PutBits((uint)this.values[index++], this.GetEncodedBlockSize());
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Clear the stored values.
+ ///
+ public void Reset() => this.values.Clear();
+
+ private static void EncodeISEBlock(List values, int bitsPerValue, ref BitStream bitSink, ref int bitsWritten, int totalBitCount)
+ where T : unmanaged
+ {
+ int valueCount = values.Count;
+ int valueRange = (valueCount == 3) ? 5 : 3;
+ int bitsPerBlock = (valueRange == 5) ? 7 : 8;
+ int[] interleavedBits = (valueRange == 5)
+ ? InterleavedQuintBits
+ : InterleavedTritBits;
+
+ int[] nonBitComponents = new int[valueCount];
+ int[] bitComponents = new int[valueCount];
+ for (int i = 0; i < valueCount; ++i)
+ {
+ bitComponents[i] = values[i] & ((1 << bitsPerValue) - 1);
+ nonBitComponents[i] = values[i] >> bitsPerValue;
+ }
+
+ // Determine how many interleaved bits for this block given the global
+ // totalBitCount and how many bits have already been written.
+ int tempBitsAdded = bitsWritten;
+ int encodedBitCount = 0;
+ for (int i = 0; i < valueCount; ++i)
+ {
+ tempBitsAdded += bitsPerValue;
+ if (tempBitsAdded >= totalBitCount)
+ {
+ break;
+ }
+
+ encodedBitCount += interleavedBits[i];
+ tempBitsAdded += interleavedBits[i];
+ if (tempBitsAdded >= totalBitCount)
+ {
+ break;
+ }
+ }
+
+ int nonBitEncoding = -1;
+ for (int j = (1 << encodedBitCount) - 1; j >= 0; --j)
+ {
+ bool matches = true;
+ for (int i = 0; i < valueCount; ++i)
+ {
+ if (valueRange == 5)
+ {
+ if (QuintEncodings[j][i] != nonBitComponents[i])
+ {
+ matches = false;
+ break;
+ }
+ }
+ else
+ {
+ if (TritEncodings[j][i] != nonBitComponents[i])
+ {
+ matches = false;
+ break;
+ }
+ }
+ }
+
+ if (matches)
+ {
+ nonBitEncoding = j;
+ break;
+ }
+ }
+
+ if (nonBitEncoding < 0)
+ {
+ throw new InvalidOperationException();
+ }
+
+ int nonBitEncodingCopy = nonBitEncoding;
+ for (int i = 0; i < valueCount; ++i)
+ {
+ if (bitsWritten + bitsPerValue <= totalBitCount)
+ {
+ bitSink.PutBits((uint)bitComponents[i], bitsPerValue);
+ bitsWritten += bitsPerValue;
+ }
+
+ int interleavedBitCount = interleavedBits[i];
+ int interleavedBitsValue = nonBitEncodingCopy & ((1 << interleavedBitCount) - 1);
+ if (bitsWritten + interleavedBitCount <= totalBitCount)
+ {
+ bitSink.PutBits((uint)interleavedBitsValue, interleavedBitCount);
+ bitsWritten += interleavedBitCount;
+ nonBitEncodingCopy >>= interleavedBitCount;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/BitQuantizationMap.cs b/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/BitQuantizationMap.cs
new file mode 100644
index 00000000..a6b442af
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/BitQuantizationMap.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.BiseEncoding.Quantize;
+
+internal sealed class BitQuantizationMap : QuantizationMap
+{
+ // TotalUnquantizedBits is 8 for endpoint values and 6 for weights
+ public BitQuantizationMap(int range, int totalUnquantizedBits)
+ {
+ // ensure range+1 is power of two
+ ArgumentOutOfRangeException.ThrowIfNotEqual(CountOnes(range + 1), 1);
+
+ int bitCount = Log2Floor(range + 1);
+
+ for (int bits = 0; bits <= range; bits++)
+ {
+ int unquantized = bits;
+ int unquantizedBitCount = bitCount;
+ while (unquantizedBitCount < totalUnquantizedBits)
+ {
+ int destinationShiftUp = Math.Min(bitCount, totalUnquantizedBits - unquantizedBitCount);
+ int sourceShiftDown = bitCount - destinationShiftUp;
+ unquantized <<= destinationShiftUp;
+ unquantized |= bits >> sourceShiftDown;
+ unquantizedBitCount += destinationShiftUp;
+ }
+
+ if (unquantizedBitCount != totalUnquantizedBits)
+ {
+ throw new InvalidOperationException();
+ }
+
+ this.UnquantizationMapBuilder.Add(unquantized);
+
+ if (bits > 0)
+ {
+ int previousUnquantized = this.UnquantizationMapBuilder[bits - 1];
+ while (this.QuantizationMapBuilder.Count <= (previousUnquantized + unquantized) / 2)
+ {
+ this.QuantizationMapBuilder.Add(bits - 1);
+ }
+ }
+
+ while (this.QuantizationMapBuilder.Count <= unquantized)
+ {
+ this.QuantizationMapBuilder.Add(bits);
+ }
+ }
+
+ this.Freeze();
+ }
+
+ private static int CountOnes(int value)
+ {
+ int count = 0;
+ while (value != 0)
+ {
+ count += value & 1;
+ value >>= 1;
+ }
+
+ return count;
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/Quantization.cs b/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/Quantization.cs
new file mode 100644
index 00000000..d97c2bea
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/Quantization.cs
@@ -0,0 +1,245 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.BiseEncoding.Quantize;
+
+internal static class Quantization
+{
+ public const int EndpointRangeMinValue = 5;
+ public const int WeightRangeMaxValue = 31;
+
+ private static readonly SortedDictionary EndpointMaps = InitEndpointMaps();
+ private static readonly SortedDictionary WeightMaps = InitWeightMaps();
+
+ // Flat lookup tables indexed by range value for O(1) access.
+ // Each slot maps to the QuantizationMap for the greatest supported range <= that index.
+ private static readonly QuantizationMap?[] EndpointMapByRange = InitEndpointMapFlat();
+ private static readonly QuantizationMap?[] WeightMapByRange = InitWeightMapFlat();
+
+ // Pre-computed flat tables for weight unquantization: entry[quantizedValue] = final unquantized weight.
+ // Includes the dq > 32 -> dq + 1 adjustment. Indexed by weight range.
+ // Valid ranges: 1, 2, 3, 4, 5, 7, 9, 11, 15, 19, 23, 31
+ private static readonly int[]?[] UnquantizeWeightsFlat = InitializeUnquantizeWeightsFlat();
+
+ // Pre-computed flat tables for endpoint unquantization.
+ // Indexed by range value. Valid ranges: 5, 7, 9, 11, 15, 19, 23, 31, 39, 47, 63, 79, 95, 127, 159, 191, 255
+ private static readonly int[]?[] UnquantizeEndpointsFlat = InitializeUnquantizeEndpointsFlat();
+
+ public static int QuantizeCEValueToRange(int value, int rangeMaxValue)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(rangeMaxValue, EndpointRangeMinValue);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(rangeMaxValue, byte.MaxValue);
+ ArgumentOutOfRangeException.ThrowIfLessThan(value, 0);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(value, byte.MaxValue);
+
+ QuantizationMap? map = GetQuantMapForValueRange(rangeMaxValue);
+ return map != null ? map.Quantize(value) : 0;
+ }
+
+ public static int UnquantizeCEValueFromRange(int value, int rangeMaxValue)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(rangeMaxValue, EndpointRangeMinValue);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(rangeMaxValue, byte.MaxValue);
+ ArgumentOutOfRangeException.ThrowIfLessThan(value, 0);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(value, rangeMaxValue);
+
+ QuantizationMap? map = GetQuantMapForValueRange(rangeMaxValue);
+ return map != null ? map.Unquantize(value) : 0;
+ }
+
+ public static int QuantizeWeightToRange(int weight, int rangeMaxValue)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(rangeMaxValue, 1);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(rangeMaxValue, WeightRangeMaxValue);
+ ArgumentOutOfRangeException.ThrowIfLessThan(weight, 0);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(weight, 64);
+
+ if (weight > 33)
+ {
+ weight -= 1;
+ }
+
+ QuantizationMap? map = GetQuantMapForWeightRange(rangeMaxValue);
+ return map != null ? map.Quantize(weight) : 0;
+ }
+
+ public static int UnquantizeWeightFromRange(int weight, int rangeMaxValue)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(rangeMaxValue, 1);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(rangeMaxValue, WeightRangeMaxValue);
+ ArgumentOutOfRangeException.ThrowIfLessThan(weight, 0);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(weight, rangeMaxValue);
+
+ QuantizationMap? map = GetQuantMapForWeightRange(rangeMaxValue);
+ int dequantized = map != null ? map.Unquantize(weight) : 0;
+ if (dequantized > 32)
+ {
+ dequantized += 1;
+ }
+
+ return dequantized;
+ }
+
+ ///
+ /// Batch unquantize: uses pre-computed flat table for O(1) lookup per value.
+ /// No per-call validation, no conditional branch per weight.
+ ///
+ internal static void UnquantizeWeightsBatch(Span weights, int count, int range)
+ {
+ int[]? table = UnquantizeWeightsFlat[range];
+ if (table == null)
+ {
+ return;
+ }
+
+ for (int i = 0; i < count; i++)
+ {
+ weights[i] = table[weights[i]];
+ }
+ }
+
+ ///
+ /// Batch unquantize color endpoint values: uses pre-computed flat table.
+ /// No per-call validation, single array lookup per value.
+ ///
+ internal static void UnquantizeCEValuesBatch(Span values, int count, int rangeMaxValue)
+ {
+ int[]? table = UnquantizeEndpointsFlat[rangeMaxValue];
+ if (table == null)
+ {
+ return;
+ }
+
+ for (int i = 0; i < count; i++)
+ {
+ values[i] = table[values[i]];
+ }
+ }
+
+ private static SortedDictionary InitEndpointMaps()
+ {
+ SortedDictionary d = new()
+ {
+ { 5, new TritQuantizationMap(5, TritQuantizationMap.GetUnquantizedValue) },
+ { 7, new BitQuantizationMap(7, 8) },
+ { 9, new QuintQuantizationMap(9, QuintQuantizationMap.GetUnquantizedValue) },
+ { 11, new TritQuantizationMap(11, TritQuantizationMap.GetUnquantizedValue) },
+ { 15, new BitQuantizationMap(15, 8) },
+ { 19, new QuintQuantizationMap(19, QuintQuantizationMap.GetUnquantizedValue) },
+ { 23, new TritQuantizationMap(23, TritQuantizationMap.GetUnquantizedValue) },
+ { 31, new BitQuantizationMap(31, 8) },
+ { 39, new QuintQuantizationMap(39, QuintQuantizationMap.GetUnquantizedValue) },
+ { 47, new TritQuantizationMap(47, TritQuantizationMap.GetUnquantizedValue) },
+ { 63, new BitQuantizationMap(63, 8) },
+ { 79, new QuintQuantizationMap(79, QuintQuantizationMap.GetUnquantizedValue) },
+ { 95, new TritQuantizationMap(95, TritQuantizationMap.GetUnquantizedValue) },
+ { 127, new BitQuantizationMap(127, 8) },
+ { 159, new QuintQuantizationMap(159, QuintQuantizationMap.GetUnquantizedValue) },
+ { 191, new TritQuantizationMap(191, TritQuantizationMap.GetUnquantizedValue) },
+ { 255, new BitQuantizationMap(255, 8) }
+ };
+ return d;
+ }
+
+ private static SortedDictionary InitWeightMaps()
+ {
+ SortedDictionary d = new()
+ {
+ { 1, new BitQuantizationMap(1, 6) },
+ { 2, new TritQuantizationMap(2, TritQuantizationMap.GetUnquantizedWeight) },
+ { 3, new BitQuantizationMap(3, 6) },
+ { 4, new QuintQuantizationMap(4, QuintQuantizationMap.GetUnquantizedWeight) },
+ { 5, new TritQuantizationMap(5, TritQuantizationMap.GetUnquantizedWeight) },
+ { 7, new BitQuantizationMap(7, 6) },
+ { 9, new QuintQuantizationMap(9, QuintQuantizationMap.GetUnquantizedWeight) },
+ { 11, new TritQuantizationMap(11, TritQuantizationMap.GetUnquantizedWeight) },
+ { 15, new BitQuantizationMap(15, 6) },
+ { 19, new QuintQuantizationMap(19, QuintQuantizationMap.GetUnquantizedWeight) },
+ { 23, new TritQuantizationMap(23, TritQuantizationMap.GetUnquantizedWeight) },
+ { 31, new BitQuantizationMap(31, 6) }
+ };
+ return d;
+ }
+
+ private static QuantizationMap?[] BuildFlatLookup(SortedDictionary maps, int size)
+ {
+ QuantizationMap?[] flat = new QuantizationMap?[size];
+ QuantizationMap? current = null;
+ for (int i = 0; i < size; i++)
+ {
+ if (maps.TryGetValue(i, out QuantizationMap? map))
+ {
+ current = map;
+ }
+
+ flat[i] = current;
+ }
+
+ return flat;
+ }
+
+ private static QuantizationMap?[] InitEndpointMapFlat()
+ => BuildFlatLookup(InitEndpointMaps(), 256);
+
+ private static QuantizationMap?[] InitWeightMapFlat()
+ => BuildFlatLookup(InitWeightMaps(), 32);
+
+ private static QuantizationMap? GetQuantMapForValueRange(int r)
+ {
+ if ((uint)r >= (uint)EndpointMapByRange.Length)
+ {
+ return null;
+ }
+
+ return EndpointMapByRange[r];
+ }
+
+ private static QuantizationMap? GetQuantMapForWeightRange(int r)
+ {
+ if ((uint)r >= (uint)WeightMapByRange.Length)
+ {
+ return null;
+ }
+
+ return WeightMapByRange[r];
+ }
+
+ private static int[]?[] InitializeUnquantizeWeightsFlat()
+ {
+ int[]?[] tables = new int[]?[WeightRangeMaxValue + 1];
+ foreach (KeyValuePair kvp in WeightMaps)
+ {
+ int range = kvp.Key;
+ QuantizationMap map = kvp.Value;
+ int[] table = new int[range + 1];
+ for (int i = 0; i <= range; i++)
+ {
+ int dequantized = map.Unquantize(i);
+ table[i] = dequantized > 32 ? dequantized + 1 : dequantized;
+ }
+
+ tables[range] = table;
+ }
+
+ return tables;
+ }
+
+ private static int[]?[] InitializeUnquantizeEndpointsFlat()
+ {
+ int[]?[] tables = new int[]?[256];
+ foreach (KeyValuePair kvp in EndpointMaps)
+ {
+ int range = kvp.Key;
+ QuantizationMap map = kvp.Value;
+ int[] table = new int[range + 1];
+ for (int i = 0; i <= range; i++)
+ {
+ table[i] = map.Unquantize(i);
+ }
+
+ tables[range] = table;
+ }
+
+ return tables;
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/QuantizationMap.cs b/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/QuantizationMap.cs
new file mode 100644
index 00000000..ffeb8146
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/QuantizationMap.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.BiseEncoding.Quantize;
+
+internal class QuantizationMap
+{
+ // Flat arrays for O(1) lookup on the hot path (set by Freeze)
+ private int[] quantizationMap = [];
+ private int[] unquantizationMap = [];
+
+ protected List QuantizationMapBuilder { get; set; } = [];
+
+ protected List UnquantizationMapBuilder { get; set; } = [];
+
+ public int Quantize(int x)
+ => (uint)x < (uint)this.quantizationMap.Length
+ ? this.quantizationMap[x]
+ : 0;
+
+ public int Unquantize(int x)
+ => (uint)x < (uint)this.unquantizationMap.Length
+ ? this.unquantizationMap[x]
+ : 0;
+
+ internal static int Log2Floor(int value)
+ {
+ int result = 0;
+ while ((1 << (result + 1)) <= value)
+ {
+ result++;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Converts builder lists to flat arrays. Called after construction is complete.
+ ///
+ protected void Freeze()
+ {
+ this.unquantizationMap = [.. this.UnquantizationMapBuilder];
+ this.quantizationMap = [.. this.QuantizationMapBuilder];
+ this.UnquantizationMapBuilder = [];
+ this.QuantizationMapBuilder = [];
+ }
+
+ protected void GenerateQuantizationMap()
+ {
+ if (this.UnquantizationMapBuilder.Count <= 1)
+ {
+ return;
+ }
+
+ this.QuantizationMapBuilder.Clear();
+ for (int i = 0; i < 256; ++i)
+ {
+ int bestIndex = 0;
+ int bestScore = int.MaxValue;
+ for (int index = 0; index < this.UnquantizationMapBuilder.Count; ++index)
+ {
+ int diff = i - this.UnquantizationMapBuilder[index];
+ int score = diff * diff;
+ if (score < bestScore)
+ {
+ bestIndex = index;
+ bestScore = score;
+ }
+ }
+
+ this.QuantizationMapBuilder.Add(bestIndex);
+ }
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/QuintQuantizationMap.cs b/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/QuintQuantizationMap.cs
new file mode 100644
index 00000000..3c77263f
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/QuintQuantizationMap.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.BiseEncoding.Quantize;
+
+internal sealed class QuintQuantizationMap : QuantizationMap
+{
+ public QuintQuantizationMap(int range, Func unquantFunc)
+ {
+ ArgumentOutOfRangeException.ThrowIfNotEqual((range + 1) % 5, 0);
+
+ int bitsPowerOfTwo = (range + 1) / 5;
+ int bitCount = bitsPowerOfTwo == 0 ? 0 : Log2Floor(bitsPowerOfTwo);
+
+ for (int quint = 0; quint < 5; ++quint)
+ {
+ for (int bits = 0; bits < (1 << bitCount); ++bits)
+ {
+ this.UnquantizationMapBuilder.Add(unquantFunc(quint, bits, range));
+ }
+ }
+
+ this.GenerateQuantizationMap();
+ this.Freeze();
+ }
+
+ internal static int GetUnquantizedValue(int quint, int bits, int range)
+ {
+ int a = (bits & 1) != 0 ? 0x1FF : 0;
+ (int b, int c) = range switch
+ {
+ 9 => (0, 113),
+ 19 => ((bits >> 1) & 0x1) is var x ? ((x << 2) | (x << 3) | (x << 8), 54) : default,
+ 39 => ((bits >> 1) & 0x3) is var x ? ((x >> 1) | (x << 1) | (x << 7), 26) : default,
+ 79 => ((bits >> 1) & 0x7) is var x ? ((x >> 1) | (x << 6), 13) : default,
+ 159 => ((bits >> 1) & 0xF) is var x ? ((x >> 3) | (x << 5), 6) : default,
+ _ => throw new ArgumentException("Illegal quint encoding")
+ };
+ int t = (quint * c) + b;
+ t ^= a;
+ t = (a & 0x80) | (t >> 2);
+ return t;
+ }
+
+ internal static int GetUnquantizedWeight(int quint, int bits, int range)
+ {
+ if (range == 4)
+ {
+ int[] weights = [0, 16, 32, 47, 63];
+ return weights[quint];
+ }
+
+ int a = (bits & 1) != 0 ? 0x7F : 0;
+ (int b, int c) = range switch
+ {
+ 9 => (0, 28),
+ 19 => ((bits >> 1) & 0x1) is var x ? ((x << 1) | (x << 6), 13) : default,
+ _ => throw new ArgumentException("Illegal quint encoding")
+ };
+ int t = (quint * c) + b;
+ t ^= a;
+ t = (a & 0x20) | (t >> 2);
+ return t;
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/TritQuantizationMap.cs b/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/TritQuantizationMap.cs
new file mode 100644
index 00000000..9daa76d7
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BiseEncoding/Quantize/TritQuantizationMap.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.BiseEncoding.Quantize;
+
+internal sealed class TritQuantizationMap : QuantizationMap
+{
+ public TritQuantizationMap(int range, Func unquantFunc)
+ {
+ ArgumentOutOfRangeException.ThrowIfNotEqual((range + 1) % 3, 0);
+
+ int bitsPowerOfTwo = (range + 1) / 3;
+ int bitCount = bitsPowerOfTwo == 0 ? 0 : Log2Floor(bitsPowerOfTwo);
+
+ for (int trit = 0; trit < 3; ++trit)
+ {
+ for (int bits = 0; bits < (1 << bitCount); ++bits)
+ {
+ this.UnquantizationMapBuilder.Add(unquantFunc(trit, bits, range));
+ }
+ }
+
+ this.GenerateQuantizationMap();
+ this.Freeze();
+ }
+
+ internal static int GetUnquantizedValue(int trit, int bits, int range)
+ {
+ int a = (bits & 1) != 0 ? 0x1FF : 0;
+ (int b, int c) = range switch
+ {
+ 5 => (0, 204),
+ 11 => ((bits >> 1) & 0x1) is var x ? ((x << 1) | (x << 2) | (x << 4) | (x << 8), 93) : default,
+ 23 => ((bits >> 1) & 0x3) is var x ? (x | (x << 2) | (x << 7), 44) : default,
+ 47 => ((bits >> 1) & 0x7) is var x ? (x | (x << 6), 22) : default,
+ 95 => ((bits >> 1) & 0xF) is var x ? ((x >> 2) | (x << 5), 11) : default,
+ 191 => ((bits >> 1) & 0x1F) is var x ? ((x >> 4) | (x << 4), 5) : default,
+ _ => throw new ArgumentException("Illegal trit encoding")
+ };
+ int t = (trit * c) + b;
+ t ^= a;
+ t = (a & 0x80) | (t >> 2);
+ return t;
+ }
+
+ internal static int GetUnquantizedWeight(int trit, int bits, int range)
+ {
+ if (range == 2)
+ {
+ return trit switch
+ {
+ 0 => 0,
+ 1 => 32,
+ _ => 63
+ };
+ }
+
+ int a = (bits & 1) != 0 ? 0x7F : 0;
+ (int b, int c) = range switch
+ {
+ 5 => (0, 50),
+ 11 => ((bits >> 1) & 1) is var x
+ ? (x | (x << 2) | (x << 6), 23)
+ : default,
+ 23 => ((bits >> 1) & 0x3) is var x
+ ? (x | (x << 5), 11)
+ : default,
+ _ => throw new ArgumentException("Illegal trit encoding")
+ };
+ int t = (trit * c) + b;
+ t ^= a;
+ return (a & 0x20) | (t >> 2);
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BlockDecoder/FusedBlockDecoder.cs b/src/ImageSharp.Textures.Astc/BlockDecoder/FusedBlockDecoder.cs
new file mode 100644
index 00000000..8ca24c51
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BlockDecoder/FusedBlockDecoder.cs
@@ -0,0 +1,183 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Textures.Astc.BiseEncoding;
+using SixLabors.ImageSharp.Textures.Astc.BiseEncoding.Quantize;
+using SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+using SixLabors.ImageSharp.Textures.Astc.IO;
+using SixLabors.ImageSharp.Textures.Astc.TexelBlock;
+
+namespace SixLabors.ImageSharp.Textures.Astc.BlockDecoder;
+
+///
+/// Shared decode core for the fused (zero-allocation) ASTC block decode pipeline.
+/// Contains BISE extraction and weight infill used by both LDR and HDR decoders.
+///
+internal static class FusedBlockDecoder
+{
+ ///
+ /// Shared decode core: BISE decode, unquantize, and infill.
+ /// Populates and returns the decoded endpoint pair.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ internal static ColorEndpointPair DecodeFusedCore(
+ UInt128 bits, in BlockInfo info, Footprint footprint, Span texelWeights)
+ {
+ // 1. BISE decode color endpoint values
+ int colorCount = info.EndpointMode0.GetColorValuesCount();
+ Span colors = stackalloc int[colorCount];
+ DecodeBiseValues(bits, info.ColorStartBit, info.ColorBitCount, info.ColorValuesRange, colorCount, colors);
+
+ // 2. Batch unquantize color values, then decode endpoint pair
+ Quantization.UnquantizeCEValuesBatch(colors, colorCount, info.ColorValuesRange);
+ ColorEndpointPair endpointPair = EndpointCodec.DecodeColorsForModePolymorphicUnquantized(colors, info.EndpointMode0);
+
+ // 3. BISE decode weights
+ int gridSize = info.GridWidth * info.GridHeight;
+ Span gridWeights = stackalloc int[gridSize];
+ DecodeBiseWeights(bits, info.WeightBitCount, info.WeightRange, gridSize, gridWeights);
+
+ // 4. Batch unquantize weights
+ Quantization.UnquantizeWeightsBatch(gridWeights, gridSize, info.WeightRange);
+
+ // 5. Infill weights from grid to texels (or pass through if identity mapping)
+ if (info.GridWidth == footprint.Width && info.GridHeight == footprint.Height)
+ {
+ gridWeights[..footprint.PixelCount].CopyTo(texelWeights);
+ }
+ else
+ {
+ DecimationInfo decimationInfo = DecimationTable.Get(footprint, info.GridWidth, info.GridHeight);
+ DecimationTable.InfillWeights(gridWeights, decimationInfo, texelWeights);
+ }
+
+ return endpointPair;
+ }
+
+ ///
+ /// Decodes BISE-encoded values from the specified bit region of the block.
+ /// For bit-only encoding with small total bit count, extracts directly from ulong
+ /// without creating a BitStream (avoids per-value ShiftBuffer overhead).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void DecodeBiseValues(UInt128 bits, int startBit, int bitCount, int range, int valuesCount, Span result)
+ {
+ (BiseEncodingMode encMode, int bitsPerValue) = BoundedIntegerSequenceCodec.GetPackingModeBitCount(range);
+
+ if (encMode == BiseEncodingMode.BitEncoding)
+ {
+ // Fast path: extract N-bit values directly via shifts
+ int totalBits = valuesCount * bitsPerValue;
+ ulong mask = (1UL << bitsPerValue) - 1;
+
+ if (startBit + totalBits <= 64)
+ {
+ // All color data fits in the low 64 bits
+ ulong data = bits.Low() >> startBit;
+ for (int i = 0; i < valuesCount; i++)
+ {
+ result[i] = (int)(data & mask);
+ data >>= bitsPerValue;
+ }
+ }
+ else
+ {
+ // Spans both halves — use UInt128 shift then extract from low
+ UInt128 shifted = (bits >> startBit) & UInt128Extensions.OnesMask(totalBits);
+ ulong lowBits = shifted.Low();
+ ulong highBits = shifted.High();
+ int bitPos = 0;
+ for (int i = 0; i < valuesCount; i++)
+ {
+ if (bitPos < 64)
+ {
+ ulong val = (lowBits >> bitPos) & mask;
+ if (bitPos + bitsPerValue > 64)
+ {
+ val |= (highBits << (64 - bitPos)) & mask;
+ }
+
+ result[i] = (int)val;
+ }
+ else
+ {
+ result[i] = (int)((highBits >> (bitPos - 64)) & mask);
+ }
+
+ bitPos += bitsPerValue;
+ }
+ }
+
+ return;
+ }
+
+ // Trit/quint encoding: fall back to full BISE decoder
+ UInt128 colorBitMask = UInt128Extensions.OnesMask(bitCount);
+ UInt128 colorBits = (bits >> startBit) & colorBitMask;
+ BitStream colorBitStream = new(colorBits, 128);
+ BoundedIntegerSequenceDecoder decoder = BoundedIntegerSequenceDecoder.GetCached(range);
+ decoder.Decode(valuesCount, ref colorBitStream, result);
+ }
+
+ ///
+ /// Decodes BISE-encoded weight values from the reversed high-end of the block.
+ /// For bit-only encoding, extracts directly from the reversed bits without BitStream.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void DecodeBiseWeights(UInt128 bits, int weightBitCount, int weightRange, int count, Span result)
+ {
+ (BiseEncodingMode encMode, int bitsPerValue) = BoundedIntegerSequenceCodec.GetPackingModeBitCount(weightRange);
+ UInt128 weightBits = UInt128Extensions.ReverseBits(bits) & UInt128Extensions.OnesMask(weightBitCount);
+
+ if (encMode == BiseEncodingMode.BitEncoding)
+ {
+ // Fast path: extract N-bit values directly via shifts
+ int totalBits = count * bitsPerValue;
+ ulong mask = (1UL << bitsPerValue) - 1;
+
+ if (totalBits <= 64)
+ {
+ ulong data = weightBits.Low();
+ for (int i = 0; i < count; i++)
+ {
+ result[i] = (int)(data & mask);
+ data >>= bitsPerValue;
+ }
+ }
+ else
+ {
+ ulong lowBits = weightBits.Low();
+ ulong highBits = weightBits.High();
+ int bitPos = 0;
+ for (int i = 0; i < count; i++)
+ {
+ if (bitPos < 64)
+ {
+ ulong val = (lowBits >> bitPos) & mask;
+ if (bitPos + bitsPerValue > 64)
+ {
+ val |= (highBits << (64 - bitPos)) & mask;
+ }
+
+ result[i] = (int)val;
+ }
+ else
+ {
+ result[i] = (int)((highBits >> (bitPos - 64)) & mask);
+ }
+
+ bitPos += bitsPerValue;
+ }
+ }
+
+ return;
+ }
+
+ // Trit/quint encoding: fall back to full BISE decoder
+ BitStream weightBitStream = new(weightBits, 128);
+ BoundedIntegerSequenceDecoder decoder = BoundedIntegerSequenceDecoder.GetCached(weightRange);
+ decoder.Decode(count, ref weightBitStream, result);
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BlockDecoder/FusedHdrBlockDecoder.cs b/src/ImageSharp.Textures.Astc/BlockDecoder/FusedHdrBlockDecoder.cs
new file mode 100644
index 00000000..d88b2eee
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BlockDecoder/FusedHdrBlockDecoder.cs
@@ -0,0 +1,223 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+using SixLabors.ImageSharp.Textures.Astc.TexelBlock;
+
+namespace SixLabors.ImageSharp.Textures.Astc.BlockDecoder;
+
+///
+/// HDR pixel writers and entry points for the fused decode pipeline.
+/// All methods handle single-partition, non-dual-plane blocks.
+///
+internal static class FusedHdrBlockDecoder
+{
+ ///
+ /// Fused HDR decode to contiguous float buffer.
+ /// Handles single-partition, non-dual-plane blocks with both LDR and HDR endpoints.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ internal static void DecompressBlockFusedHdr(UInt128 bits, in BlockInfo info, Footprint footprint, Span buffer)
+ {
+ Span texelWeights = stackalloc int[footprint.PixelCount];
+ ColorEndpointPair endpointPair = FusedBlockDecoder.DecodeFusedCore(bits, in info, footprint, texelWeights);
+ WriteHdrOutputPixels(buffer, footprint.PixelCount, in endpointPair, texelWeights);
+ }
+
+ ///
+ /// Fused HDR decode writing directly to image buffer at strided positions.
+ /// Handles single-partition, non-dual-plane blocks with both LDR and HDR endpoints.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ internal static void DecompressBlockFusedHdrToImage(
+ UInt128 bits,
+ in BlockInfo info,
+ Footprint footprint,
+ int dstBaseX,
+ int dstBaseY,
+ int imageWidth,
+ Span imageBuffer)
+ {
+ Span texelWeights = stackalloc int[footprint.PixelCount];
+ ColorEndpointPair endpointPair = FusedBlockDecoder.DecodeFusedCore(bits, in info, footprint, texelWeights);
+ WriteHdrOutputPixelsToImage(imageBuffer, footprint, dstBaseX, dstBaseY, imageWidth, in endpointPair, texelWeights);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteHdrOutputPixels(
+ Span buffer, int pixelCount, in ColorEndpointPair endpointPair, Span texelWeights)
+ {
+ if (endpointPair.IsHdr)
+ {
+ WriteHdrPixels(buffer, pixelCount, in endpointPair, texelWeights);
+ }
+ else
+ {
+ WriteLdrAsHdrPixels(buffer, pixelCount, in endpointPair, texelWeights);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteHdrOutputPixelsToImage(
+ Span imageBuffer,
+ Footprint footprint,
+ int dstBaseX,
+ int dstBaseY,
+ int imageWidth,
+ in ColorEndpointPair endpointPair,
+ Span texelWeights)
+ {
+ if (endpointPair.IsHdr)
+ {
+ WriteHdrPixelsToImage(imageBuffer, footprint, dstBaseX, dstBaseY, imageWidth, in endpointPair, texelWeights);
+ }
+ else
+ {
+ WriteLdrAsHdrPixelsToImage(imageBuffer, footprint, dstBaseX, dstBaseY, imageWidth, in endpointPair, texelWeights);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteLdrAsHdrPixels(Span buffer, int pixelCount, in ColorEndpointPair endpointPair, Span texelWeights)
+ {
+ int lowR = endpointPair.LdrLow.R, lowG = endpointPair.LdrLow.G, lowB = endpointPair.LdrLow.B, lowA = endpointPair.LdrLow.A;
+ int highR = endpointPair.LdrHigh.R, highG = endpointPair.LdrHigh.G, highB = endpointPair.LdrHigh.B, highA = endpointPair.LdrHigh.A;
+
+ for (int i = 0; i < pixelCount; i++)
+ {
+ int weight = texelWeights[i];
+ int offset = i * 4;
+ buffer[offset + 0] = InterpolateLdrAsFloat(lowR, highR, weight);
+ buffer[offset + 1] = InterpolateLdrAsFloat(lowG, highG, weight);
+ buffer[offset + 2] = InterpolateLdrAsFloat(lowB, highB, weight);
+ buffer[offset + 3] = InterpolateLdrAsFloat(lowA, highA, weight);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteLdrAsHdrPixelsToImage(
+ Span imageBuffer,
+ Footprint footprint,
+ int dstBaseX,
+ int dstBaseY,
+ int imageWidth,
+ in ColorEndpointPair endpointPair,
+ Span texelWeights)
+ {
+ int lowR = endpointPair.LdrLow.R, lowG = endpointPair.LdrLow.G, lowB = endpointPair.LdrLow.B, lowA = endpointPair.LdrLow.A;
+ int highR = endpointPair.LdrHigh.R, highG = endpointPair.LdrHigh.G, highB = endpointPair.LdrHigh.B, highA = endpointPair.LdrHigh.A;
+
+ const int channelsPerPixel = 4;
+ int footprintWidth = footprint.Width;
+ int footprintHeight = footprint.Height;
+ int rowStride = imageWidth * channelsPerPixel;
+
+ for (int pixelY = 0; pixelY < footprintHeight; pixelY++)
+ {
+ int dstRowOffset = ((dstBaseY + pixelY) * rowStride) + (dstBaseX * channelsPerPixel);
+ int srcRowBase = pixelY * footprintWidth;
+
+ for (int pixelX = 0; pixelX < footprintWidth; pixelX++)
+ {
+ int weight = texelWeights[srcRowBase + pixelX];
+ int dstOffset = dstRowOffset + (pixelX * channelsPerPixel);
+ imageBuffer[dstOffset + 0] = InterpolateLdrAsFloat(lowR, highR, weight);
+ imageBuffer[dstOffset + 1] = InterpolateLdrAsFloat(lowG, highG, weight);
+ imageBuffer[dstOffset + 2] = InterpolateLdrAsFloat(lowB, highB, weight);
+ imageBuffer[dstOffset + 3] = InterpolateLdrAsFloat(lowA, highA, weight);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteHdrPixels(Span buffer, int pixelCount, in ColorEndpointPair endpointPair, Span texelWeights)
+ {
+ bool alphaIsLdr = endpointPair.AlphaIsLdr;
+ int lowR = endpointPair.HdrLow.R, lowG = endpointPair.HdrLow.G, lowB = endpointPair.HdrLow.B, lowA = endpointPair.HdrLow.A;
+ int highR = endpointPair.HdrHigh.R, highG = endpointPair.HdrHigh.G, highB = endpointPair.HdrHigh.B, highA = endpointPair.HdrHigh.A;
+
+ for (int i = 0; i < pixelCount; i++)
+ {
+ int weight = texelWeights[i];
+ int offset = i * 4;
+ buffer[offset + 0] = InterpolateHdrAsFloat(lowR, highR, weight);
+ buffer[offset + 1] = InterpolateHdrAsFloat(lowG, highG, weight);
+ buffer[offset + 2] = InterpolateHdrAsFloat(lowB, highB, weight);
+
+ if (alphaIsLdr)
+ {
+ int interpolated = ((lowA * (64 - weight)) + (highA * weight) + 32) / 64;
+ buffer[offset + 3] = (ushort)Math.Clamp(interpolated, 0, 0xFFFF) / 65535.0f;
+ }
+ else
+ {
+ buffer[offset + 3] = InterpolateHdrAsFloat(lowA, highA, weight);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteHdrPixelsToImage(
+ Span imageBuffer,
+ Footprint footprint,
+ int dstBaseX,
+ int dstBaseY,
+ int imageWidth,
+ in ColorEndpointPair endpointPair,
+ Span texelWeights)
+ {
+ bool alphaIsLdr = endpointPair.AlphaIsLdr;
+ int lowR = endpointPair.HdrLow.R, lowG = endpointPair.HdrLow.G, lowB = endpointPair.HdrLow.B, lowA = endpointPair.HdrLow.A;
+ int highR = endpointPair.HdrHigh.R, highG = endpointPair.HdrHigh.G, highB = endpointPair.HdrHigh.B, highA = endpointPair.HdrHigh.A;
+
+ const int channelsPerPixel = 4;
+ int footprintWidth = footprint.Width;
+ int footprintHeight = footprint.Height;
+ int rowStride = imageWidth * channelsPerPixel;
+
+ for (int pixelY = 0; pixelY < footprintHeight; pixelY++)
+ {
+ int dstRowOffset = ((dstBaseY + pixelY) * rowStride) + (dstBaseX * channelsPerPixel);
+ int srcRowBase = pixelY * footprintWidth;
+
+ for (int pixelX = 0; pixelX < footprintWidth; pixelX++)
+ {
+ int weight = texelWeights[srcRowBase + pixelX];
+ int dstOffset = dstRowOffset + (pixelX * channelsPerPixel);
+ imageBuffer[dstOffset + 0] = InterpolateHdrAsFloat(lowR, highR, weight);
+ imageBuffer[dstOffset + 1] = InterpolateHdrAsFloat(lowG, highG, weight);
+ imageBuffer[dstOffset + 2] = InterpolateHdrAsFloat(lowB, highB, weight);
+
+ if (alphaIsLdr)
+ {
+ int interpolated = ((lowA * (64 - weight)) + (highA * weight) + 32) / 64;
+ imageBuffer[dstOffset + 3] = (ushort)Math.Clamp(interpolated, 0, 0xFFFF) / 65535.0f;
+ }
+ else
+ {
+ imageBuffer[dstOffset + 3] = InterpolateHdrAsFloat(lowA, highA, weight);
+ }
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static float InterpolateLdrAsFloat(int p0, int p1, int weight)
+ {
+ int c0 = (p0 << 8) | p0;
+ int c1 = (p1 << 8) | p1;
+ int interpolated = ((c0 * (64 - weight)) + (c1 * weight) + 32) / 64;
+ return Math.Clamp(interpolated, 0, 0xFFFF) / 65535.0f;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static float InterpolateHdrAsFloat(int p0, int p1, int weight)
+ {
+ int interpolated = ((p0 * (64 - weight)) + (p1 * weight) + 32) / 64;
+ ushort clamped = (ushort)Math.Clamp(interpolated, 0, 0xFFFF);
+ ushort halfFloatBits = LogicalBlock.LnsToSf16(clamped);
+ return (float)BitConverter.UInt16BitsToHalf(halfFloatBits);
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/BlockDecoder/FusedLdrBlockDecoder.cs b/src/ImageSharp.Textures.Astc/BlockDecoder/FusedLdrBlockDecoder.cs
new file mode 100644
index 00000000..5391c4b3
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/BlockDecoder/FusedLdrBlockDecoder.cs
@@ -0,0 +1,172 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
+using SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+using SixLabors.ImageSharp.Textures.Astc.TexelBlock;
+
+namespace SixLabors.ImageSharp.Textures.Astc.BlockDecoder;
+
+///
+/// LDR pixel writers and entry points for the fused decode pipeline.
+/// All methods handle single-partition, non-dual-plane blocks.
+///
+internal static class FusedLdrBlockDecoder
+{
+ private const int BytesPerPixelUnorm8 = 4;
+
+ ///
+ /// Fused LDR decode to contiguous buffer.
+ /// Only handles single-partition, non-dual-plane, LDR blocks.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ internal static void DecompressBlockFusedLdr(UInt128 bits, in BlockInfo info, Footprint footprint, Span buffer)
+ {
+ Span texelWeights = stackalloc int[footprint.PixelCount];
+ ColorEndpointPair endpointPair = FusedBlockDecoder.DecodeFusedCore(bits, in info, footprint, texelWeights);
+ WriteLdrPixels(buffer, footprint.PixelCount, in endpointPair, texelWeights);
+ }
+
+ ///
+ /// Fused LDR decode writing directly to image buffer at strided positions.
+ /// Only handles single-partition, non-dual-plane, LDR blocks.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ internal static void DecompressBlockFusedLdrToImage(
+ UInt128 bits,
+ in BlockInfo info,
+ Footprint footprint,
+ int dstBaseX,
+ int dstBaseY,
+ int imageWidth,
+ Span imageBuffer)
+ {
+ Span texelWeights = stackalloc int[footprint.PixelCount];
+ ColorEndpointPair endpointPair = FusedBlockDecoder.DecodeFusedCore(bits, in info, footprint, texelWeights);
+ WriteLdrPixelsToImage(imageBuffer, footprint, dstBaseX, dstBaseY, imageWidth, in endpointPair, texelWeights);
+ }
+
+ ///
+ /// Writes all pixels for a single-partition LDR block using SIMD where possible.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteLdrPixels(Span buffer, int pixelCount, in ColorEndpointPair endpointPair, Span texelWeights)
+ {
+ int lowR = endpointPair.LdrLow.R, lowG = endpointPair.LdrLow.G, lowB = endpointPair.LdrLow.B, lowA = endpointPair.LdrLow.A;
+ int highR = endpointPair.LdrHigh.R, highG = endpointPair.LdrHigh.G, highB = endpointPair.LdrHigh.B, highA = endpointPair.LdrHigh.A;
+
+ int i = 0;
+ if (Vector128.IsHardwareAccelerated)
+ {
+ int limit = pixelCount - 3;
+ for (; i < limit; i += 4)
+ {
+ Vector128 weights = Vector128.Create(
+ texelWeights[i],
+ texelWeights[i + 1],
+ texelWeights[i + 2],
+ texelWeights[i + 3]);
+ SimdHelpers.Write4PixelLdr(
+ buffer,
+ i * 4,
+ lowR,
+ lowG,
+ lowB,
+ lowA,
+ highR,
+ highG,
+ highB,
+ highA,
+ weights);
+ }
+ }
+
+ for (; i < pixelCount; i++)
+ {
+ SimdHelpers.WriteSinglePixelLdr(
+ buffer,
+ i * 4,
+ lowR,
+ lowG,
+ lowB,
+ lowA,
+ highR,
+ highG,
+ highB,
+ highA,
+ texelWeights[i]);
+ }
+ }
+
+ ///
+ /// Writes LDR pixels directly to image buffer at strided positions.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteLdrPixelsToImage(
+ Span imageBuffer,
+ Footprint footprint,
+ int dstBaseX,
+ int dstBaseY,
+ int imageWidth,
+ in ColorEndpointPair endpointPair,
+ Span texelWeights)
+ {
+ int lowR = endpointPair.LdrLow.R, lowG = endpointPair.LdrLow.G, lowB = endpointPair.LdrLow.B, lowA = endpointPair.LdrLow.A;
+ int highR = endpointPair.LdrHigh.R, highG = endpointPair.LdrHigh.G, highB = endpointPair.LdrHigh.B, highA = endpointPair.LdrHigh.A;
+
+ int footprintWidth = footprint.Width;
+ int footprintHeight = footprint.Height;
+ int rowStride = imageWidth * BytesPerPixelUnorm8;
+
+ for (int pixelY = 0; pixelY < footprintHeight; pixelY++)
+ {
+ int dstRowOffset = ((dstBaseY + pixelY) * rowStride) + (dstBaseX * BytesPerPixelUnorm8);
+ int srcRowBase = pixelY * footprintWidth;
+ int pixelX = 0;
+
+ if (Vector128.IsHardwareAccelerated)
+ {
+ int limit = footprintWidth - 3;
+ for (; pixelX < limit; pixelX += 4)
+ {
+ int texelIndex = srcRowBase + pixelX;
+ Vector128 weights = Vector128.Create(
+ texelWeights[texelIndex],
+ texelWeights[texelIndex + 1],
+ texelWeights[texelIndex + 2],
+ texelWeights[texelIndex + 3]);
+ SimdHelpers.Write4PixelLdr(
+ imageBuffer,
+ dstRowOffset + (pixelX * BytesPerPixelUnorm8),
+ lowR,
+ lowG,
+ lowB,
+ lowA,
+ highR,
+ highG,
+ highB,
+ highA,
+ weights);
+ }
+ }
+
+ for (; pixelX < footprintWidth; pixelX++)
+ {
+ SimdHelpers.WriteSinglePixelLdr(
+ imageBuffer,
+ dstRowOffset + (pixelX * BytesPerPixelUnorm8),
+ lowR,
+ lowG,
+ lowB,
+ lowA,
+ highR,
+ highG,
+ highB,
+ highA,
+ texelWeights[srcRowBase + pixelX]);
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/ColorEncoding/ColorEndpointMode.cs b/src/ImageSharp.Textures.Astc/ColorEncoding/ColorEndpointMode.cs
new file mode 100644
index 00000000..14c9bc37
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ColorEncoding/ColorEndpointMode.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+
+///
+/// ASTC supports 16 color endpoint encoding schemes, known as endpoint modes
+///
+///
+/// The options for endpoint modes let you vary the following:
+///
+/// - The number of color channels. For example, luminance, luminance+alpha, rgb, or rgba
+/// - The encoding method. For example, direct, base+offset, base+scale, or quantization level
+/// - The data range. For example, low dynamic range or High Dynamic Range
+///
+///
+internal enum ColorEndpointMode
+{
+ LdrLumaDirect = 0,
+ LdrLumaBaseOffset,
+ HdrLumaLargeRange,
+ HdrLumaSmallRange,
+ LdrLumaAlphaDirect,
+ LdrLumaAlphaBaseOffset,
+ LdrRgbBaseScale,
+ HdrRgbBaseScale,
+ LdrRgbDirect,
+ LdrRgbBaseOffset,
+ LdrRgbBaseScaleTwoA,
+ HdrRgbDirect,
+ LdrRgbaDirect,
+ LdrRgbaBaseOffset,
+ HdrRgbDirectLdrAlpha,
+ HdrRgbDirectHdrAlpha,
+
+ // Number of endpoint modes defined by the ASTC specification.
+ ColorEndpointModeCount
+}
diff --git a/src/ImageSharp.Textures.Astc/ColorEncoding/ColorEndpointModeExtensions.cs b/src/ImageSharp.Textures.Astc/ColorEncoding/ColorEndpointModeExtensions.cs
new file mode 100644
index 00000000..c1a6e077
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ColorEncoding/ColorEndpointModeExtensions.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+
+internal static class ColorEndpointModeExtensions
+{
+ public static int GetEndpointModeClass(this ColorEndpointMode mode)
+ => (int)mode / 4;
+
+ public static int GetColorValuesCount(this ColorEndpointMode mode)
+ => (mode.GetEndpointModeClass() + 1) * 2;
+
+ ///
+ /// Determines whether the specified endpoint mode uses HDR (High Dynamic Range) encoding.
+ ///
+ ///
+ /// True if the mode is one of the 6 HDR modes (2, 3, 7, 11, 14, 15), false otherwise.
+ ///
+ public static bool IsHdr(this ColorEndpointMode mode)
+ => mode switch
+ {
+ ColorEndpointMode.HdrLumaLargeRange => true, // Mode 2
+ ColorEndpointMode.HdrLumaSmallRange => true, // Mode 3
+ ColorEndpointMode.HdrRgbBaseScale => true, // Mode 7
+ ColorEndpointMode.HdrRgbDirect => true, // Mode 11
+ ColorEndpointMode.HdrRgbDirectLdrAlpha => true, // Mode 14
+ ColorEndpointMode.HdrRgbDirectHdrAlpha => true, // Mode 15
+ _ => false
+ };
+}
diff --git a/src/ImageSharp.Textures.Astc/ColorEncoding/ColorEndpointPair.cs b/src/ImageSharp.Textures.Astc/ColorEncoding/ColorEndpointPair.cs
new file mode 100644
index 00000000..45cae3a1
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ColorEncoding/ColorEndpointPair.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+
+///
+/// A value-type discriminated union representing either an LDR or HDR color endpoint pair.
+///
+[StructLayout(LayoutKind.Auto)]
+internal struct ColorEndpointPair
+{
+ public bool IsHdr;
+
+ // LDR fields (used when IsHdr == false)
+ public RgbaColor LdrLow;
+ public RgbaColor LdrHigh;
+
+ // HDR fields (used when IsHdr == true)
+ public RgbaHdrColor HdrLow;
+ public RgbaHdrColor HdrHigh;
+ public bool AlphaIsLdr;
+ public bool ValuesAreLns;
+
+ public static ColorEndpointPair Ldr(RgbaColor low, RgbaColor high)
+ => new() { IsHdr = false, LdrLow = low, LdrHigh = high };
+
+ public static ColorEndpointPair Hdr(RgbaHdrColor low, RgbaHdrColor high, bool alphaIsLdr = false, bool valuesAreLns = true)
+ => new() { IsHdr = true, HdrLow = low, HdrHigh = high, AlphaIsLdr = alphaIsLdr, ValuesAreLns = valuesAreLns };
+}
diff --git a/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointCodec.cs b/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointCodec.cs
new file mode 100644
index 00000000..3263ac79
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointCodec.cs
@@ -0,0 +1,249 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.BiseEncoding.Quantize;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+
+internal static class EndpointCodec
+{
+ ///
+ /// Decodes color endpoints for the specified mode, returning a polymorphic endpoint pair
+ /// that supports both LDR and HDR modes.
+ ///
+ /// Quantized integer values from the ASTC block
+ /// Maximum quantization value
+ /// The color endpoint mode
+ /// A ColorEndpointPair representing either LDR or HDR endpoints
+ public static ColorEndpointPair DecodeColorsForModePolymorphic(ReadOnlySpan values, int maxValue, ColorEndpointMode mode)
+ {
+ if (mode.IsHdr())
+ {
+ (RgbaHdrColor low, RgbaHdrColor high) = HdrEndpointDecoder.DecodeHdrMode(values, maxValue, mode);
+ bool alphaIsLdr = mode == ColorEndpointMode.HdrRgbDirectLdrAlpha;
+ return ColorEndpointPair.Hdr(low, high, alphaIsLdr);
+ }
+ else
+ {
+ (RgbaColor low, RgbaColor high) = DecodeColorsForMode(values, maxValue, mode);
+ return ColorEndpointPair.Ldr(low, high);
+ }
+ }
+
+ public static (RgbaColor EndpointLowRgba, RgbaColor EndpointHighRgba) DecodeColorsForMode(ReadOnlySpan values, int maxValue, ColorEndpointMode mode)
+ {
+ int count = mode.GetColorValuesCount();
+ Span unquantizedValues = stackalloc int[count];
+ int copyLen = Math.Min(count, values.Length);
+ for (int i = 0; i < copyLen; i++)
+ {
+ unquantizedValues[i] = values[i];
+ }
+
+ UnquantizeInline(unquantizedValues, maxValue);
+ ColorEndpointPair pair = DecodeColorsForModeUnquantized(unquantizedValues, mode);
+ return (pair.LdrLow, pair.LdrHigh);
+ }
+
+ ///
+ /// Decodes color endpoints from already-unquantized values, supporting both LDR and HDR modes.
+ /// Called from the fused HDR decode path where BISE decode + batch unquantize
+ /// have already been performed. Returns a ColorEndpointPair (LDR or HDR).
+ ///
+ internal static ColorEndpointPair DecodeColorsForModePolymorphicUnquantized(ReadOnlySpan unquantizedValues, ColorEndpointMode mode)
+ {
+ if (mode.IsHdr())
+ {
+ (RgbaHdrColor low, RgbaHdrColor high) = HdrEndpointDecoder.DecodeHdrModeUnquantized(unquantizedValues, mode);
+ bool alphaIsLdr = mode == ColorEndpointMode.HdrRgbDirectLdrAlpha;
+ return ColorEndpointPair.Hdr(low, high, alphaIsLdr);
+ }
+
+ return DecodeColorsForModeUnquantized(unquantizedValues, mode);
+ }
+
+ ///
+ /// Decodes color endpoints from already-unquantized values.
+ /// Called from the fused decode path where BISE decode + batch unquantize
+ /// have already been performed. Returns an LDR ColorEndpointPair.
+ ///
+ internal static ColorEndpointPair DecodeColorsForModeUnquantized(ReadOnlySpan unquantizedValues, ColorEndpointMode mode)
+ {
+ RgbaColor endpointLowRgba, endpointHighRgba;
+
+ switch (mode)
+ {
+ case ColorEndpointMode.LdrLumaDirect:
+ endpointLowRgba = new RgbaColor(unquantizedValues[0], unquantizedValues[0], unquantizedValues[0]);
+ endpointHighRgba = new RgbaColor(unquantizedValues[1], unquantizedValues[1], unquantizedValues[1]);
+ break;
+ case ColorEndpointMode.LdrLumaBaseOffset:
+ {
+ int l0 = (unquantizedValues[0] >> 2) | (unquantizedValues[1] & 0xC0);
+ int l1 = Math.Min(l0 + (unquantizedValues[1] & 0x3F), 0xFF);
+ endpointLowRgba = new RgbaColor(l0, l0, l0);
+ endpointHighRgba = new RgbaColor(l1, l1, l1);
+ break;
+ }
+
+ case ColorEndpointMode.LdrLumaAlphaDirect:
+ endpointLowRgba = new RgbaColor(unquantizedValues[0], unquantizedValues[0], unquantizedValues[0], unquantizedValues[2]);
+ endpointHighRgba = new RgbaColor(unquantizedValues[1], unquantizedValues[1], unquantizedValues[1], unquantizedValues[3]);
+ break;
+ case ColorEndpointMode.LdrLumaAlphaBaseOffset:
+ {
+ (int b0, int a0) = BitOperations.TransferPrecision(unquantizedValues[1], unquantizedValues[0]);
+ (int b2, int a2) = BitOperations.TransferPrecision(unquantizedValues[3], unquantizedValues[2]);
+ endpointLowRgba = new RgbaColor(a0, a0, a0, a2);
+ int highLuma = a0 + b0;
+ endpointHighRgba = new RgbaColor(highLuma, highLuma, highLuma, a2 + b2);
+ break;
+ }
+
+ case ColorEndpointMode.LdrRgbBaseScale:
+ endpointLowRgba = new RgbaColor(
+ (unquantizedValues[0] * unquantizedValues[3]) >> 8,
+ (unquantizedValues[1] * unquantizedValues[3]) >> 8,
+ (unquantizedValues[2] * unquantizedValues[3]) >> 8);
+ endpointHighRgba = new RgbaColor(unquantizedValues[0], unquantizedValues[1], unquantizedValues[2]);
+ break;
+ case ColorEndpointMode.LdrRgbDirect:
+ {
+ int sum0 = unquantizedValues[0] + unquantizedValues[2] + unquantizedValues[4];
+ int sum1 = unquantizedValues[1] + unquantizedValues[3] + unquantizedValues[5];
+ if (sum1 < sum0)
+ {
+ endpointLowRgba = new RgbaColor(
+ r: (unquantizedValues[1] + unquantizedValues[5]) >> 1,
+ g: (unquantizedValues[3] + unquantizedValues[5]) >> 1,
+ b: unquantizedValues[5]);
+ endpointHighRgba = new RgbaColor(
+ r: (unquantizedValues[0] + unquantizedValues[4]) >> 1,
+ g: (unquantizedValues[2] + unquantizedValues[4]) >> 1,
+ b: unquantizedValues[4]);
+ }
+ else
+ {
+ endpointLowRgba = new RgbaColor(unquantizedValues[0], unquantizedValues[2], unquantizedValues[4]);
+ endpointHighRgba = new RgbaColor(unquantizedValues[1], unquantizedValues[3], unquantizedValues[5]);
+ }
+
+ break;
+ }
+
+ case ColorEndpointMode.LdrRgbBaseOffset:
+ {
+ (int b0, int a0) = BitOperations.TransferPrecision(unquantizedValues[1], unquantizedValues[0]);
+ (int b1, int a1) = BitOperations.TransferPrecision(unquantizedValues[3], unquantizedValues[2]);
+ (int b2, int a2) = BitOperations.TransferPrecision(unquantizedValues[5], unquantizedValues[4]);
+ if (b0 + b1 + b2 < 0)
+ {
+ endpointLowRgba = new RgbaColor(
+ r: (a0 + b0 + a2 + b2) >> 1,
+ g: (a1 + b1 + a2 + b2) >> 1,
+ b: a2 + b2);
+ endpointHighRgba = new RgbaColor(
+ r: (a0 + a2) >> 1,
+ g: (a1 + a2) >> 1,
+ b: a2);
+ }
+ else
+ {
+ endpointLowRgba = new RgbaColor(a0, a1, a2);
+ endpointHighRgba = new RgbaColor(a0 + b0, a1 + b1, a2 + b2);
+ }
+
+ break;
+ }
+
+ case ColorEndpointMode.LdrRgbBaseScaleTwoA:
+ endpointLowRgba = new RgbaColor(
+ r: (unquantizedValues[0] * unquantizedValues[3]) >> 8,
+ g: (unquantizedValues[1] * unquantizedValues[3]) >> 8,
+ b: (unquantizedValues[2] * unquantizedValues[3]) >> 8,
+ a: unquantizedValues[4]);
+ endpointHighRgba = new RgbaColor(unquantizedValues[0], unquantizedValues[1], unquantizedValues[2], unquantizedValues[5]);
+ break;
+ case ColorEndpointMode.LdrRgbaDirect:
+ {
+ int sum0 = unquantizedValues[0] + unquantizedValues[2] + unquantizedValues[4];
+ int sum1 = unquantizedValues[1] + unquantizedValues[3] + unquantizedValues[5];
+ if (sum1 >= sum0)
+ {
+ endpointLowRgba = new RgbaColor(unquantizedValues[0], unquantizedValues[2], unquantizedValues[4], unquantizedValues[6]);
+ endpointHighRgba = new RgbaColor(unquantizedValues[1], unquantizedValues[3], unquantizedValues[5], unquantizedValues[7]);
+ }
+ else
+ {
+ endpointLowRgba = new RgbaColor(
+ r: (unquantizedValues[1] + unquantizedValues[5]) >> 1,
+ g: (unquantizedValues[3] + unquantizedValues[5]) >> 1,
+ b: unquantizedValues[5],
+ a: unquantizedValues[7]);
+ endpointHighRgba = new RgbaColor(
+ r: (unquantizedValues[0] + unquantizedValues[4]) >> 1,
+ g: (unquantizedValues[2] + unquantizedValues[4]) >> 1,
+ b: unquantizedValues[4],
+ a: unquantizedValues[6]);
+ }
+
+ break;
+ }
+
+ case ColorEndpointMode.LdrRgbaBaseOffset:
+ {
+ (int b0, int a0) = BitOperations.TransferPrecision(unquantizedValues[1], unquantizedValues[0]);
+ (int b1, int a1) = BitOperations.TransferPrecision(unquantizedValues[3], unquantizedValues[2]);
+ (int b2, int a2) = BitOperations.TransferPrecision(unquantizedValues[5], unquantizedValues[4]);
+ (int b3, int a3) = BitOperations.TransferPrecision(unquantizedValues[7], unquantizedValues[6]);
+ if (b0 + b1 + b2 < 0)
+ {
+ endpointLowRgba = new RgbaColor(
+ r: (a0 + b0 + a2 + b2) >> 1,
+ g: (a1 + b1 + a2 + b2) >> 1,
+ b: a2 + b2,
+ a: a3 + b3);
+ endpointHighRgba = new RgbaColor(
+ r: (a0 + a2) >> 1,
+ g: (a1 + a2) >> 1,
+ b: a2,
+ a: a3);
+ }
+ else
+ {
+ endpointLowRgba = new RgbaColor(a0, a1, a2, a3);
+ endpointHighRgba = new RgbaColor(a0 + b0, a1 + b1, a2 + b2, a3 + b3);
+ }
+
+ break;
+ }
+
+ default:
+ endpointLowRgba = RgbaColor.Empty;
+ endpointHighRgba = RgbaColor.Empty;
+ break;
+ }
+
+ return ColorEndpointPair.Ldr(endpointLowRgba, endpointHighRgba);
+ }
+
+ internal static int[] UnquantizeArray(int[] values, int maxValue)
+ {
+ int[] result = new int[values.Length];
+ for (int i = 0; i < values.Length; ++i)
+ {
+ result[i] = Quantization.UnquantizeCEValueFromRange(values[i], maxValue);
+ }
+
+ return result;
+ }
+
+ private static void UnquantizeInline(Span values, int maxValue)
+ {
+ for (int i = 0; i < values.Length; ++i)
+ {
+ values[i] = Quantization.UnquantizeCEValueFromRange(values[i], maxValue);
+ }
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointEncoder.cs b/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointEncoder.cs
new file mode 100644
index 00000000..1cb40a93
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointEncoder.cs
@@ -0,0 +1,549 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.BiseEncoding.Quantize;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+
+internal static class EndpointEncoder
+{
+ public static bool UsesBlueContract(int maxValue, ColorEndpointMode mode, List values)
+ {
+ int valueCount = mode.GetColorValuesCount();
+ ArgumentOutOfRangeException.ThrowIfLessThan(values.Count, valueCount);
+
+ switch (mode)
+ {
+ case ColorEndpointMode.LdrRgbDirect:
+ case ColorEndpointMode.LdrRgbaDirect:
+ {
+ int maxValueCount = Math.Max(ColorEndpointMode.LdrRgbDirect.GetColorValuesCount(), ColorEndpointMode.LdrRgbaDirect.GetColorValuesCount());
+ int[] v = new int[maxValueCount];
+ for (int i = 0; i < maxValueCount; ++i)
+ {
+ v[i] = i < values.Count ? values[i] : 0;
+ }
+
+ int[] unquantizedValues = EndpointCodec.UnquantizeArray(v, maxValue);
+ int s0 = unquantizedValues[0] + unquantizedValues[2] + unquantizedValues[4];
+ int s1 = unquantizedValues[1] + unquantizedValues[3] + unquantizedValues[5];
+ return s0 > s1;
+ }
+
+ case ColorEndpointMode.LdrRgbBaseOffset:
+ case ColorEndpointMode.LdrRgbaBaseOffset:
+ {
+ int maxValueCount = Math.Max(ColorEndpointMode.LdrRgbBaseOffset.GetColorValuesCount(), ColorEndpointMode.LdrRgbaBaseOffset.GetColorValuesCount());
+ int[] v = new int[maxValueCount];
+ for (int i = 0; i < maxValueCount; ++i)
+ {
+ v[i] = i < values.Count ? values[i] : 0;
+ }
+
+ int[] unquantizedValues = EndpointCodec.UnquantizeArray(v, maxValue);
+ (int b0, int a0) = BitOperations.TransferPrecision(unquantizedValues[1], unquantizedValues[0]);
+ (int b1, int a1) = BitOperations.TransferPrecision(unquantizedValues[3], unquantizedValues[2]);
+ (int b2, int a2) = BitOperations.TransferPrecision(unquantizedValues[5], unquantizedValues[4]);
+ return (b0 + b1 + b2) < 0;
+ }
+
+ default:
+ return false;
+ }
+ }
+
+ // TODO: Extract an interface and implement instances for each encoding mode
+ public static bool EncodeColorsForMode(RgbaColor endpointLowRgba, RgbaColor endpointHighRgba, int maxValue, EndpointEncodingMode encodingMode, out ColorEndpointMode astcMode, List values)
+ {
+ bool needsWeightSwap = false;
+ astcMode = ColorEndpointMode.LdrLumaDirect;
+ int valueCount = encodingMode.GetValuesCount();
+ for (int i = values.Count; i < valueCount; ++i)
+ {
+ values.Add(0);
+ }
+
+ switch (encodingMode)
+ {
+ case EndpointEncodingMode.DirectLuma:
+ return EncodeColorsLuma(endpointLowRgba, endpointHighRgba, maxValue, out astcMode, values);
+ case EndpointEncodingMode.DirectLumaAlpha:
+ {
+ int avg1 = endpointLowRgba.Average;
+ int avg2 = endpointHighRgba.Average;
+ values[0] = Quantization.QuantizeCEValueToRange(avg1, maxValue);
+ values[1] = Quantization.QuantizeCEValueToRange(avg2, maxValue);
+ values[2] = Quantization.QuantizeCEValueToRange(endpointLowRgba[3], maxValue);
+ values[3] = Quantization.QuantizeCEValueToRange(endpointHighRgba[3], maxValue);
+ astcMode = ColorEndpointMode.LdrLumaAlphaDirect;
+ break;
+ }
+
+ case EndpointEncodingMode.BaseScaleRgb:
+ case EndpointEncodingMode.BaseScaleRgba:
+ {
+ RgbaColor baseColor = endpointHighRgba;
+ RgbaColor scaled = endpointLowRgba;
+
+ int numChannelsGe = 0;
+ for (int i = 0; i < 3; ++i)
+ {
+ numChannelsGe += endpointHighRgba[i] >= endpointLowRgba[i] ? 1 : 0;
+ }
+
+ if (numChannelsGe < 2)
+ {
+ needsWeightSwap = true;
+ (scaled, baseColor) = (baseColor, scaled);
+ }
+
+ int[] quantizedBase = QuantizeColorArray(baseColor, maxValue);
+ int[] unquantizedBase = EndpointCodec.UnquantizeArray(quantizedBase, maxValue);
+
+ int numSamples = 0;
+ int scaleSum = 0;
+ for (int i = 0; i < 3; ++i)
+ {
+ int x = unquantizedBase[i];
+ if (x != 0)
+ {
+ ++numSamples;
+ scaleSum += (scaled[i] * 256) / x;
+ }
+ }
+
+ values[0] = quantizedBase[0];
+ values[1] = quantizedBase[1];
+ values[2] = quantizedBase[2];
+ if (numSamples > 0)
+ {
+ int avgScale = Math.Clamp(scaleSum / numSamples, 0, 255);
+ values[3] = Quantization.QuantizeCEValueToRange(avgScale, maxValue);
+ }
+ else
+ {
+ values[3] = maxValue;
+ }
+
+ astcMode = ColorEndpointMode.LdrRgbBaseScale;
+
+ if (encodingMode == EndpointEncodingMode.BaseScaleRgba)
+ {
+ values[4] = Quantization.QuantizeCEValueToRange(scaled[3], maxValue);
+ values[5] = Quantization.QuantizeCEValueToRange(baseColor[3], maxValue);
+ astcMode = ColorEndpointMode.LdrRgbBaseScaleTwoA;
+ }
+
+ break;
+ }
+
+ case EndpointEncodingMode.DirectRbg:
+ case EndpointEncodingMode.DirectRgba:
+ return EncodeColorsRGBA(endpointLowRgba, endpointHighRgba, maxValue, encodingMode == EndpointEncodingMode.DirectRgba, out astcMode, values);
+ default:
+ throw new InvalidOperationException("Unimplemented color encoding.");
+ }
+
+ return needsWeightSwap;
+ }
+
+ private static int[] QuantizeColorArray(RgbaColor c, int maxValue)
+ {
+ int[] array = new int[RgbaColor.BytesPerPixel];
+ for (int i = 0; i < RgbaColor.BytesPerPixel; ++i)
+ {
+ array[i] = Quantization.QuantizeCEValueToRange(c[i], maxValue);
+ }
+
+ return array;
+ }
+
+ private static bool EncodeColorsLuma(RgbaColor endpointLow, RgbaColor endpointHigh, int maxValue, out ColorEndpointMode astcMode, List values)
+ {
+ astcMode = ColorEndpointMode.LdrLumaDirect;
+ ArgumentOutOfRangeException.ThrowIfLessThan(values.Count, 2);
+
+ int avg1 = endpointLow.Average;
+ int avg2 = endpointHigh.Average;
+
+ bool needsWeightSwap = false;
+ if (avg1 > avg2)
+ {
+ needsWeightSwap = true;
+ (avg2, avg1) = (avg1, avg2);
+ }
+
+ int offset = Math.Min(avg2 - avg1, 0x3F);
+ int quantOffLow = Quantization.QuantizeCEValueToRange((avg1 & 0x3F) << 2, maxValue);
+ int quantOffHigh = Quantization.QuantizeCEValueToRange((avg1 & 0xC0) | offset, maxValue);
+
+ int quantLow = Quantization.QuantizeCEValueToRange(avg1, maxValue);
+ int quantHigh = Quantization.QuantizeCEValueToRange(avg2, maxValue);
+
+ values[0] = quantOffLow;
+ values[1] = quantOffHigh;
+ (RgbaColor decLowOff, RgbaColor decHighOff) = EndpointCodec.DecodeColorsForMode(values.ToArray(), maxValue, ColorEndpointMode.LdrLumaBaseOffset);
+
+ values[0] = quantLow;
+ values[1] = quantHigh;
+ (RgbaColor decLowDir, RgbaColor decHighDir) = EndpointCodec.DecodeColorsForMode(values.ToArray(), maxValue, ColorEndpointMode.LdrLumaDirect);
+
+ int calculateErrorOff = 0;
+ int calculateErrorDir = 0;
+ if (needsWeightSwap)
+ {
+ calculateErrorDir = RgbaColor.SquaredError(decLowDir, endpointHigh) + RgbaColor.SquaredError(decHighDir, endpointLow);
+ calculateErrorOff = RgbaColor.SquaredError(decLowOff, endpointHigh) + RgbaColor.SquaredError(decHighOff, endpointLow);
+ }
+ else
+ {
+ calculateErrorDir = RgbaColor.SquaredError(decLowDir, endpointLow) + RgbaColor.SquaredError(decHighDir, endpointHigh);
+ calculateErrorOff = RgbaColor.SquaredError(decLowOff, endpointLow) + RgbaColor.SquaredError(decHighOff, endpointHigh);
+ }
+
+ if (calculateErrorDir <= calculateErrorOff)
+ {
+ values[0] = quantLow;
+ values[1] = quantHigh;
+ astcMode = ColorEndpointMode.LdrLumaDirect;
+ }
+ else
+ {
+ values[0] = quantOffLow;
+ values[1] = quantOffHigh;
+ astcMode = ColorEndpointMode.LdrLumaBaseOffset;
+ }
+
+ return needsWeightSwap;
+ }
+
+ private static bool EncodeColorsRGBA(RgbaColor endpointLowRgba, RgbaColor endpointHighRgba, int maxValue, bool withAlpha, out ColorEndpointMode astcMode, List values)
+ {
+ astcMode = ColorEndpointMode.LdrRgbDirect;
+ int numChannels = withAlpha ? 4 : 3;
+
+ RgbaColor invertedBlueContractLow = endpointLowRgba.WithInvertedBlueContract();
+ RgbaColor invertedBlueContractHigh = endpointHighRgba.WithInvertedBlueContract();
+
+ int[] directBase = new int[4];
+ int[] directOffset = new int[4];
+ for (int i = 0; i < 4; ++i)
+ {
+ directBase[i] = endpointLowRgba[i];
+ directOffset[i] = Math.Clamp(endpointHighRgba[i] - endpointLowRgba[i], -32, 31);
+ (directOffset[i], directBase[i]) = BitOperations.TransferPrecisionInverse(directOffset[i], directBase[i]);
+ }
+
+ int[] invertedBlueContractBase = new int[4];
+ int[] invertedBlueContractOffset = new int[4];
+ for (int i = 0; i < 4; ++i)
+ {
+ invertedBlueContractBase[i] = invertedBlueContractHigh[i];
+ invertedBlueContractOffset[i] = Math.Clamp(invertedBlueContractLow[i] - invertedBlueContractHigh[i], -32, 31);
+ (invertedBlueContractOffset[i], invertedBlueContractBase[i]) = BitOperations.TransferPrecisionInverse(invertedBlueContractOffset[i], invertedBlueContractBase[i]);
+ }
+
+ int[] directBaseSwapped = new int[4];
+ int[] directOffsetSwapped = new int[4];
+ for (int i = 0; i < 4; ++i)
+ {
+ directBaseSwapped[i] = endpointHighRgba[i];
+ directOffsetSwapped[i] = Math.Clamp(endpointLowRgba[i] - endpointHighRgba[i], -32, 31);
+ (directOffsetSwapped[i], directBaseSwapped[i]) = BitOperations.TransferPrecisionInverse(directOffsetSwapped[i], directBaseSwapped[i]);
+ }
+
+ int[] invertedBlueContractBaseSwapped = new int[4];
+ int[] invertedBlueContractOffsetSwapped = new int[4];
+ for (int i = 0; i < 4; ++i)
+ {
+ invertedBlueContractBaseSwapped[i] = invertedBlueContractLow[i];
+ invertedBlueContractOffsetSwapped[i] = Math.Clamp(invertedBlueContractHigh[i] - invertedBlueContractLow[i], -32, 31);
+ (invertedBlueContractOffsetSwapped[i], invertedBlueContractBaseSwapped[i]) = BitOperations.TransferPrecisionInverse(invertedBlueContractOffsetSwapped[i], invertedBlueContractBaseSwapped[i]);
+ }
+
+ QuantizedEndpointPair directQuantized = new(endpointLowRgba, endpointHighRgba, maxValue);
+ QuantizedEndpointPair bcQuantized = new(invertedBlueContractLow, invertedBlueContractHigh, maxValue);
+
+ QuantizedEndpointPair offsetQuantized = new(new RgbaColor(directBase[0], directBase[1], directBase[2], directBase[3]), new RgbaColor(directOffset[0], directOffset[1], directOffset[2], directOffset[3]), maxValue);
+ QuantizedEndpointPair bcOffsetQuantized = new(new RgbaColor(invertedBlueContractBase[0], invertedBlueContractBase[1], invertedBlueContractBase[2], invertedBlueContractBase[3]), new RgbaColor(invertedBlueContractOffset[0], invertedBlueContractOffset[1], invertedBlueContractOffset[2], invertedBlueContractOffset[3]), maxValue);
+
+ QuantizedEndpointPair offsetSwappedQuantized = new(new RgbaColor(directBaseSwapped[0], directBaseSwapped[1], directBaseSwapped[2], directBaseSwapped[3]), new RgbaColor(directOffsetSwapped[0], directOffsetSwapped[1], directOffsetSwapped[2], directOffsetSwapped[3]), maxValue);
+ QuantizedEndpointPair bcOffsetSwappedQuantized = new(new RgbaColor(invertedBlueContractBaseSwapped[0], invertedBlueContractBaseSwapped[1], invertedBlueContractBaseSwapped[2], invertedBlueContractBaseSwapped[3]), new RgbaColor(invertedBlueContractOffsetSwapped[0], invertedBlueContractOffsetSwapped[1], invertedBlueContractOffsetSwapped[2], invertedBlueContractOffsetSwapped[3]), maxValue);
+
+ List errors = new(6);
+
+ // 3.1 regular unquantized error
+ {
+ int[] rgbaLow = directQuantized.UnquantizedLow();
+ int[] rgbaHigh = directQuantized.UnquantizedHigh();
+ RgbaColor lowColor = new(rgbaLow[0], rgbaLow[1], rgbaLow[2], rgbaLow[3]);
+ RgbaColor highColor = new(rgbaHigh[0], rgbaHigh[1], rgbaHigh[2], rgbaHigh[3]);
+ int squaredRgbError = withAlpha
+ ? RgbaColor.SquaredError(lowColor, endpointLowRgba) + RgbaColor.SquaredError(highColor, endpointHighRgba)
+ : RgbColor.SquaredError(lowColor, endpointLowRgba) + RgbColor.SquaredError(highColor, endpointHighRgba);
+ errors.Add(new CEEncodingOption(squaredRgbError, directQuantized, false, false, false));
+ }
+
+ // 3.2 blue-contract
+ {
+ int[] blueContractUnquantizedLow = bcQuantized.UnquantizedLow();
+ int[] blueContractUnquantizedHigh = bcQuantized.UnquantizedHigh();
+ RgbaColor blueContractLow = RgbaColorExtensions.WithBlueContract(blueContractUnquantizedLow[0], blueContractUnquantizedLow[1], blueContractUnquantizedLow[2], blueContractUnquantizedLow[3]);
+ RgbaColor blueContractHigh = RgbaColorExtensions.WithBlueContract(blueContractUnquantizedHigh[0], blueContractUnquantizedHigh[1], blueContractUnquantizedHigh[2], blueContractUnquantizedHigh[3]);
+
+ // TODO: How to handle alpha for this entire functions??
+ int blueContractSquaredError = withAlpha
+ ? RgbaColor.SquaredError(blueContractLow, endpointLowRgba) + RgbaColor.SquaredError(blueContractHigh, endpointHighRgba)
+ : RgbColor.SquaredError(blueContractLow, endpointLowRgba) + RgbColor.SquaredError(blueContractHigh, endpointHighRgba);
+
+ errors.Add(new CEEncodingOption(blueContractSquaredError, bcQuantized, swapEndpoints: false, blueContract: true, useOffsetMode: false));
+ }
+
+ // 3.3 base/offset
+ void ComputeBaseOffsetError(QuantizedEndpointPair pair, bool swapped)
+ {
+ int[] baseArr = pair.UnquantizedLow();
+ int[] offsetArr = pair.UnquantizedHigh();
+
+ RgbaColor baseColor = new(baseArr[0], baseArr[1], baseArr[2], baseArr[3]);
+ RgbaColor offsetColor = new RgbaColor(offsetArr[0], offsetArr[1], offsetArr[2], offsetArr[3]).AsOffsetFrom(baseColor);
+
+ int baseOffsetError = 0;
+ if (swapped)
+ {
+ baseOffsetError = withAlpha
+ ? RgbaColor.SquaredError(baseColor, endpointHighRgba) + RgbaColor.SquaredError(offsetColor, endpointLowRgba)
+ : RgbColor.SquaredError(baseColor, endpointHighRgba) + RgbColor.SquaredError(offsetColor, endpointLowRgba);
+ }
+ else
+ {
+ baseOffsetError = withAlpha
+ ? RgbaColor.SquaredError(baseColor, endpointLowRgba) + RgbaColor.SquaredError(offsetColor, endpointHighRgba)
+ : RgbColor.SquaredError(baseColor, endpointLowRgba) + RgbColor.SquaredError(offsetColor, endpointHighRgba);
+ }
+
+ errors.Add(new CEEncodingOption(baseOffsetError, pair, swapped, false, true));
+ }
+
+ ComputeBaseOffsetError(offsetQuantized, false);
+
+ void ComputeBaseOffsetBlueContractError(QuantizedEndpointPair pair, bool swapped)
+ {
+ int[] baseArr = pair.UnquantizedLow();
+ int[] offsetArr = pair.UnquantizedHigh();
+
+ RgbaColor baseColor = new(baseArr[0], baseArr[1], baseArr[2], baseArr[3]);
+ RgbaColor offsetColor = new RgbaColor(offsetArr[0], offsetArr[1], offsetArr[2], offsetArr[3]).AsOffsetFrom(baseColor);
+
+ baseColor = baseColor.WithBlueContract();
+ offsetColor = offsetColor.WithBlueContract();
+
+ int squaredBlueContractError = 0;
+ if (swapped)
+ {
+ squaredBlueContractError = withAlpha
+ ? RgbaColor.SquaredError(baseColor, endpointLowRgba) + RgbaColor.SquaredError(offsetColor, endpointHighRgba)
+ : RgbColor.SquaredError(baseColor, endpointLowRgba) + RgbColor.SquaredError(offsetColor, endpointHighRgba);
+ }
+ else
+ {
+ squaredBlueContractError = withAlpha
+ ? RgbaColor.SquaredError(baseColor, endpointHighRgba) + RgbaColor.SquaredError(offsetColor, endpointLowRgba)
+ : RgbColor.SquaredError(baseColor, endpointHighRgba) + RgbColor.SquaredError(offsetColor, endpointLowRgba);
+ }
+
+ errors.Add(new CEEncodingOption(squaredBlueContractError, pair, swapped, true, true));
+ }
+
+ ComputeBaseOffsetBlueContractError(bcOffsetQuantized, false);
+ ComputeBaseOffsetError(offsetSwappedQuantized, true);
+ ComputeBaseOffsetBlueContractError(bcOffsetSwappedQuantized, true);
+
+ errors.Sort((a, b) => a.Error().CompareTo(b.Error()));
+
+ foreach (CEEncodingOption measurement in errors)
+ {
+ bool needsWeightSwap = false;
+ if (measurement.Pack(withAlpha, out ColorEndpointMode modeUnused, values, ref needsWeightSwap))
+ {
+ return needsWeightSwap;
+ }
+ }
+
+ throw new InvalidOperationException("Shouldn't have reached this point");
+ }
+
+ private class QuantizedEndpointPair
+ {
+ private readonly RgbaColor originalLow;
+ private readonly RgbaColor originalHigh;
+ private readonly int[] quantizedLow;
+ private readonly int[] quantizedHigh;
+ private readonly int[] unquantizedLow;
+ private readonly int[] unquantizedHigh;
+
+ public QuantizedEndpointPair(RgbaColor low, RgbaColor high, int maxValue)
+ {
+ this.originalLow = low;
+ this.originalHigh = high;
+ this.quantizedLow = QuantizeColorArray(low, maxValue);
+ this.quantizedHigh = QuantizeColorArray(high, maxValue);
+ this.unquantizedLow = EndpointCodec.UnquantizeArray(this.quantizedLow, maxValue);
+ this.unquantizedHigh = EndpointCodec.UnquantizeArray(this.quantizedHigh, maxValue);
+ }
+
+ public int[] QuantizedLow() => this.quantizedLow;
+
+ public int[] QuantizedHigh() => this.quantizedHigh;
+
+ public int[] UnquantizedLow() => this.unquantizedLow;
+
+ public int[] UnquantizedHigh() => this.unquantizedHigh;
+
+ public RgbaColor OriginalLow() => this.originalLow;
+
+ public RgbaColor OriginalHigh() => this.originalHigh;
+ }
+
+ private class CEEncodingOption
+ {
+ private readonly int squaredError;
+ private readonly QuantizedEndpointPair quantizedEndpoints;
+ private readonly bool swapEndpoints;
+ private readonly bool blueContract;
+ private readonly bool useOffsetMode;
+
+ public CEEncodingOption(
+ int squaredError,
+ QuantizedEndpointPair quantizedEndpoints,
+ bool swapEndpoints,
+ bool blueContract,
+ bool useOffsetMode)
+ {
+ this.squaredError = squaredError;
+ this.quantizedEndpoints = quantizedEndpoints;
+ this.swapEndpoints = swapEndpoints;
+ this.blueContract = blueContract;
+ this.useOffsetMode = useOffsetMode;
+ }
+
+ public bool Pack(bool hasAlpha, out ColorEndpointMode endpointMode, List values, ref bool needsWeightSwap)
+ {
+ endpointMode = ColorEndpointMode.LdrLumaDirect;
+ int[] unquantizedLowOriginal = this.quantizedEndpoints.UnquantizedLow();
+ int[] unquantizedHighOriginal = this.quantizedEndpoints.UnquantizedHigh();
+
+ int[] unquantizedLow = (int[])unquantizedLowOriginal.Clone();
+ int[] unquantizedHigh = (int[])unquantizedHighOriginal.Clone();
+
+ if (this.useOffsetMode)
+ {
+ for (int i = 0; i < 4; ++i)
+ {
+ (unquantizedHigh[i], unquantizedLow[i]) = BitOperations.TransferPrecision(unquantizedHigh[i], unquantizedLow[i]);
+ }
+ }
+
+ int sum0 = 0, sum1 = 0;
+ for (int i = 0; i < 3; ++i)
+ {
+ sum0 += unquantizedLow[i];
+ sum1 += unquantizedHigh[i];
+ }
+
+ bool swapVals = false;
+ if (this.useOffsetMode)
+ {
+ if (this.blueContract)
+ {
+ swapVals = sum1 >= 0;
+ }
+ else
+ {
+ swapVals = sum1 < 0;
+ }
+
+ if (swapVals)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (this.blueContract)
+ {
+ if (sum1 == sum0)
+ {
+ return false;
+ }
+
+ swapVals = sum1 > sum0;
+ needsWeightSwap = !needsWeightSwap;
+ }
+ else
+ {
+ swapVals = sum1 < sum0;
+ }
+ }
+
+ int[] quantizedLowOriginal = this.quantizedEndpoints.QuantizedLow();
+ int[] quantizedHighOriginal = this.quantizedEndpoints.QuantizedHigh();
+
+ int[] quantizedLow = (int[])quantizedLowOriginal.Clone();
+ int[] quantizedHigh = (int[])quantizedHighOriginal.Clone();
+
+ if (swapVals)
+ {
+ if (this.useOffsetMode)
+ {
+ throw new InvalidOperationException();
+ }
+
+ (quantizedHigh, quantizedLow) = (quantizedLow, quantizedHigh);
+ needsWeightSwap = !needsWeightSwap;
+ }
+
+ values[0] = quantizedLow[0];
+ values[1] = quantizedHigh[0];
+ values[2] = quantizedLow[1];
+ values[3] = quantizedHigh[1];
+ values[4] = quantizedLow[2];
+ values[5] = quantizedHigh[2];
+
+ if (this.useOffsetMode)
+ {
+ endpointMode = ColorEndpointMode.LdrRgbBaseOffset;
+ }
+ else
+ {
+ endpointMode = ColorEndpointMode.LdrRgbDirect;
+ }
+
+ if (hasAlpha)
+ {
+ values[6] = quantizedLow[3];
+ values[7] = quantizedHigh[3];
+ if (this.useOffsetMode)
+ {
+ endpointMode = ColorEndpointMode.LdrRgbaBaseOffset;
+ }
+ else
+ {
+ endpointMode = ColorEndpointMode.LdrRgbaDirect;
+ }
+ }
+
+ if (this.swapEndpoints)
+ {
+ needsWeightSwap = !needsWeightSwap;
+ }
+
+ return true;
+ }
+
+ public bool BlueContract() => this.blueContract;
+
+ public int Error() => this.squaredError;
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointEncodingMode.cs b/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointEncodingMode.cs
new file mode 100644
index 00000000..08f2f58e
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointEncodingMode.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+
+internal enum EndpointEncodingMode
+{
+ DirectLuma,
+ DirectLumaAlpha,
+ BaseScaleRgb,
+ BaseScaleRgba,
+ DirectRbg,
+ DirectRgba
+}
diff --git a/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointEncodingModeExtensions.cs b/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointEncodingModeExtensions.cs
new file mode 100644
index 00000000..1b81a1ac
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ColorEncoding/EndpointEncodingModeExtensions.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+
+internal static class EndpointEncodingModeExtensions
+{
+ public static int GetValuesCount(this EndpointEncodingMode mode) => mode switch
+ {
+ EndpointEncodingMode.DirectLuma => 2,
+ EndpointEncodingMode.DirectLumaAlpha or EndpointEncodingMode.BaseScaleRgb => 4,
+ EndpointEncodingMode.DirectRbg or EndpointEncodingMode.BaseScaleRgba => 6,
+ _ => 8
+ };
+}
diff --git a/src/ImageSharp.Textures.Astc/ColorEncoding/HdrEndpointDecoder.cs b/src/ImageSharp.Textures.Astc/ColorEncoding/HdrEndpointDecoder.cs
new file mode 100644
index 00000000..68040c57
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ColorEncoding/HdrEndpointDecoder.cs
@@ -0,0 +1,478 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.BiseEncoding.Quantize;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+
+///
+/// Decodes HDR (High Dynamic Range) color endpoints for ASTC texture compression.
+///
+///
+/// HDR modes produce 12-bit intermediate values (0-4095) which are shifted left by 4
+/// to produce the final 16-bit values (0-65520) stored as FP16 bit patterns.
+///
+internal static class HdrEndpointDecoder
+{
+ public static (RgbaHdrColor Low, RgbaHdrColor High) DecodeHdrMode(ReadOnlySpan values, int maxValue, ColorEndpointMode mode)
+ {
+ int count = mode.GetColorValuesCount();
+ Span unquantizedValues = stackalloc int[count];
+ int copyLength = Math.Min(count, values.Length);
+ for (int i = 0; i < copyLength; i++)
+ {
+ unquantizedValues[i] = Quantization.UnquantizeCEValueFromRange(values[i], maxValue);
+ }
+
+ return DecodeHdrModeUnquantized(unquantizedValues, mode);
+ }
+
+ ///
+ /// Decodes HDR endpoints from already-unquantized values.
+ /// Called from the fused decode path where BISE decode + batch unquantize
+ /// have already been performed.
+ ///
+ public static (RgbaHdrColor Low, RgbaHdrColor High) DecodeHdrModeUnquantized(ReadOnlySpan value, ColorEndpointMode mode) => mode switch
+ {
+ ColorEndpointMode.HdrLumaLargeRange => UnpackHdrLuminanceLargeRangeCore(value[0], value[1]),
+ ColorEndpointMode.HdrLumaSmallRange => UnpackHdrLuminanceSmallRangeCore(value[0], value[1]),
+ ColorEndpointMode.HdrRgbBaseScale => UnpackHdrRgbBaseScaleCore(value[0], value[1], value[2], value[3]),
+ ColorEndpointMode.HdrRgbDirect => UnpackHdrRgbDirectCore(value[0], value[1], value[2], value[3], value[4], value[5]),
+ ColorEndpointMode.HdrRgbDirectLdrAlpha => UnpackHdrRgbDirectLdrAlphaCore(value),
+ ColorEndpointMode.HdrRgbDirectHdrAlpha => UnpackHdrRgbDirectHdrAlphaCore(value),
+ _ => throw new InvalidOperationException($"Mode {mode} is not an HDR mode")
+ };
+
+ ///
+ /// Performs an unsigned left shift of a signed value, avoiding undefined behavior
+ /// that would occur with signed left shift of negative values.
+ ///
+ private static int SafeSignedLeftShift(int value, int shift) => (int)((uint)value << shift);
+
+ private static (RgbaHdrColor Low, RgbaHdrColor High) UnpackHdrLuminanceLargeRangeCore(int v0, int v1)
+ {
+ int y0, y1;
+ if (v1 >= v0)
+ {
+ y0 = v0 << 4;
+ y1 = v1 << 4;
+ }
+ else
+ {
+ y0 = (v1 << 4) + 8;
+ y1 = (v0 << 4) - 8;
+ }
+
+ RgbaHdrColor low = new((ushort)(y0 << 4), (ushort)(y0 << 4), (ushort)(y0 << 4), 0x7800);
+ RgbaHdrColor high = new((ushort)(y1 << 4), (ushort)(y1 << 4), (ushort)(y1 << 4), 0x7800);
+ return (low, high);
+ }
+
+ private static (RgbaHdrColor Low, RgbaHdrColor High) UnpackHdrLuminanceSmallRangeCore(int v0, int v1)
+ {
+ int y0, y1;
+ if ((v0 & 0x80) != 0)
+ {
+ y0 = ((v1 & 0xE0) << 4) | ((v0 & 0x7F) << 2);
+ y1 = (v1 & 0x1F) << 2;
+ }
+ else
+ {
+ y0 = ((v1 & 0xF0) << 4) | ((v0 & 0x7F) << 1);
+ y1 = (v1 & 0x0F) << 1;
+ }
+
+ y1 += y0;
+ if (y1 > 0xFFF)
+ {
+ y1 = 0xFFF;
+ }
+
+ RgbaHdrColor low = new((ushort)(y0 << 4), (ushort)(y0 << 4), (ushort)(y0 << 4), 0x7800);
+ RgbaHdrColor high = new((ushort)(y1 << 4), (ushort)(y1 << 4), (ushort)(y1 << 4), 0x7800);
+ return (low, high);
+ }
+
+ private static (RgbaHdrColor Low, RgbaHdrColor High) UnpackHdrRgbBaseScaleCore(int v0, int v1, int v2, int v3)
+ {
+ int modeValue = ((v0 & 0xC0) >> 6) | (((v1 & 0x80) >> 7) << 2) | (((v2 & 0x80) >> 7) << 3);
+
+ int majorComponent;
+ int mode;
+
+ (majorComponent, mode) = modeValue switch
+ {
+ _ when (modeValue & 0xC) != 0xC => (modeValue >> 2, modeValue & 3),
+ not 0xF => (modeValue & 3, 4),
+ _ => (0, 5)
+ };
+
+ int red = v0 & 0x3F;
+ int green = v1 & 0x1F;
+ int blue = v2 & 0x1F;
+ int scale = v3 & 0x1F;
+
+ int bit0 = (v1 >> 6) & 1;
+ int bit1 = (v1 >> 5) & 1;
+ int bit2 = (v2 >> 6) & 1;
+ int bit3 = (v2 >> 5) & 1;
+ int bit4 = (v3 >> 7) & 1;
+ int bit5 = (v3 >> 6) & 1;
+ int bit6 = (v3 >> 5) & 1;
+
+ int oneHotMode = 1 << mode;
+
+ if ((oneHotMode & 0x30) != 0)
+ {
+ green |= bit0 << 6;
+ }
+
+ if ((oneHotMode & 0x3A) != 0)
+ {
+ green |= bit1 << 5;
+ }
+
+ if ((oneHotMode & 0x30) != 0)
+ {
+ blue |= bit2 << 6;
+ }
+
+ if ((oneHotMode & 0x3A) != 0)
+ {
+ blue |= bit3 << 5;
+ }
+
+ if ((oneHotMode & 0x3D) != 0)
+ {
+ scale |= bit6 << 5;
+ }
+
+ if ((oneHotMode & 0x2D) != 0)
+ {
+ scale |= bit5 << 6;
+ }
+
+ if ((oneHotMode & 0x04) != 0)
+ {
+ scale |= bit4 << 7;
+ }
+
+ if ((oneHotMode & 0x3B) != 0)
+ {
+ red |= bit4 << 6;
+ }
+
+ if ((oneHotMode & 0x04) != 0)
+ {
+ red |= bit3 << 6;
+ }
+
+ if ((oneHotMode & 0x10) != 0)
+ {
+ red |= bit5 << 7;
+ }
+
+ if ((oneHotMode & 0x0F) != 0)
+ {
+ red |= bit2 << 7;
+ }
+
+ if ((oneHotMode & 0x05) != 0)
+ {
+ red |= bit1 << 8;
+ }
+
+ if ((oneHotMode & 0x0A) != 0)
+ {
+ red |= bit0 << 8;
+ }
+
+ if ((oneHotMode & 0x05) != 0)
+ {
+ red |= bit0 << 9;
+ }
+
+ if ((oneHotMode & 0x02) != 0)
+ {
+ red |= bit6 << 9;
+ }
+
+ if ((oneHotMode & 0x01) != 0)
+ {
+ red |= bit3 << 10;
+ }
+
+ if ((oneHotMode & 0x02) != 0)
+ {
+ red |= bit5 << 10;
+ }
+
+ // Shift amounts per mode (from ARM reference)
+ ReadOnlySpan shiftAmounts = [1, 1, 2, 3, 4, 5];
+ int shiftAmount = shiftAmounts[mode];
+
+ red <<= shiftAmount;
+ green <<= shiftAmount;
+ blue <<= shiftAmount;
+ scale <<= shiftAmount;
+
+ if (mode != 5)
+ {
+ green = red - green;
+ blue = red - blue;
+ }
+
+ // Swap components based on major component
+ (red, green, blue) = majorComponent switch
+ {
+ 1 => (green, red, blue),
+ 2 => (blue, green, red),
+ _ => (red, green, blue)
+ };
+
+ // Low endpoint is base minus scale offset
+ int red0 = red - scale;
+ int green0 = green - scale;
+ int blue0 = blue - scale;
+
+ // Clamp to [0, 0xFFF]
+ red = Math.Max(red, 0);
+ green = Math.Max(green, 0);
+ blue = Math.Max(blue, 0);
+ red0 = Math.Max(red0, 0);
+ green0 = Math.Max(green0, 0);
+ blue0 = Math.Max(blue0, 0);
+
+ RgbaHdrColor low = new((ushort)(red0 << 4), (ushort)(green0 << 4), (ushort)(blue0 << 4), 0x7800);
+ RgbaHdrColor high = new((ushort)(red << 4), (ushort)(green << 4), (ushort)(blue << 4), 0x7800);
+ return (low, high);
+ }
+
+ private static (RgbaHdrColor Low, RgbaHdrColor High) UnpackHdrRgbDirectCore(int v0, int v1, int v2, int v3, int v4, int v5)
+ {
+ int modeValue = ((v1 & 0x80) >> 7) | (((v2 & 0x80) >> 7) << 1) | (((v3 & 0x80) >> 7) << 2);
+ int majorComponent = ((v4 & 0x80) >> 7) | (((v5 & 0x80) >> 7) << 1);
+
+ // Special case: majorComponent == 3 (direct passthrough)
+ if (majorComponent == 3)
+ {
+ RgbaHdrColor low = new(
+ (ushort)(v0 << 8),
+ (ushort)(v2 << 8),
+ (ushort)((v4 & 0x7F) << 9),
+ 0x7800);
+ RgbaHdrColor high = new(
+ (ushort)(v1 << 8),
+ (ushort)(v3 << 8),
+ (ushort)((v5 & 0x7F) << 9),
+ 0x7800);
+ return (low, high);
+ }
+
+ int a = v0 | ((v1 & 0x40) << 2);
+ int b0 = v2 & 0x3F;
+ int b1 = v3 & 0x3F;
+ int c = v1 & 0x3F;
+ int d0 = v4 & 0x7F;
+ int d1 = v5 & 0x7F;
+
+ // Data bits table from ARM reference
+ ReadOnlySpan dataBitsTable = [7, 6, 7, 6, 5, 6, 5, 6];
+ int dataBits = dataBitsTable[modeValue];
+
+ int bit0 = (v2 >> 6) & 1;
+ int bit1 = (v3 >> 6) & 1;
+ int bit2 = (v4 >> 6) & 1;
+ int bit3 = (v5 >> 6) & 1;
+ int bit4 = (v4 >> 5) & 1;
+ int bit5 = (v5 >> 5) & 1;
+
+ int oneHotModeValue = 1 << modeValue;
+
+ // Bit placement for 'a'
+ if ((oneHotModeValue & 0xA4) != 0)
+ {
+ a |= bit0 << 9;
+ }
+
+ if ((oneHotModeValue & 0x8) != 0)
+ {
+ a |= bit2 << 9;
+ }
+
+ if ((oneHotModeValue & 0x50) != 0)
+ {
+ a |= bit4 << 9;
+ }
+
+ if ((oneHotModeValue & 0x50) != 0)
+ {
+ a |= bit5 << 10;
+ }
+
+ if ((oneHotModeValue & 0xA0) != 0)
+ {
+ a |= bit1 << 10;
+ }
+
+ if ((oneHotModeValue & 0xC0) != 0)
+ {
+ a |= bit2 << 11;
+ }
+
+ // Bit placement for 'c'
+ if ((oneHotModeValue & 0x4) != 0)
+ {
+ c |= bit1 << 6;
+ }
+
+ if ((oneHotModeValue & 0xE8) != 0)
+ {
+ c |= bit3 << 6;
+ }
+
+ if ((oneHotModeValue & 0x20) != 0)
+ {
+ c |= bit2 << 7;
+ }
+
+ // Bit placement for 'b0' and 'b1'
+ if ((oneHotModeValue & 0x5B) != 0)
+ {
+ b0 |= bit0 << 6;
+ b1 |= bit1 << 6;
+ }
+
+ if ((oneHotModeValue & 0x12) != 0)
+ {
+ b0 |= bit2 << 7;
+ b1 |= bit3 << 7;
+ }
+
+ // Bit placement for 'd0' and 'd1'
+ if ((oneHotModeValue & 0xAF) != 0)
+ {
+ d0 |= bit4 << 5;
+ d1 |= bit5 << 5;
+ }
+
+ if ((oneHotModeValue & 0x5) != 0)
+ {
+ d0 |= bit2 << 6;
+ d1 |= bit3 << 6;
+ }
+
+ // Sign-extend d0 and d1 based on dataBits
+ int signExtendShift = 32 - dataBits;
+ d0 = (d0 << signExtendShift) >> signExtendShift;
+ d1 = (d1 << signExtendShift) >> signExtendShift;
+
+ // Expand to 12 bits
+ int valueShift = (modeValue >> 1) ^ 3;
+ a = SafeSignedLeftShift(a, valueShift);
+ b0 = SafeSignedLeftShift(b0, valueShift);
+ b1 = SafeSignedLeftShift(b1, valueShift);
+ c = SafeSignedLeftShift(c, valueShift);
+ d0 = SafeSignedLeftShift(d0, valueShift);
+ d1 = SafeSignedLeftShift(d1, valueShift);
+
+ // Compute color values per ARM reference
+ int red1 = a;
+ int green1 = a - b0;
+ int blue1 = a - b1;
+ int red0 = a - c;
+ int green0 = a - b0 - c - d0;
+ int blue0 = a - b1 - c - d1;
+
+ // Clamp to [0, 4095]
+ red0 = Math.Clamp(red0, 0, 0xFFF);
+ green0 = Math.Clamp(green0, 0, 0xFFF);
+ blue0 = Math.Clamp(blue0, 0, 0xFFF);
+ red1 = Math.Clamp(red1, 0, 0xFFF);
+ green1 = Math.Clamp(green1, 0, 0xFFF);
+ blue1 = Math.Clamp(blue1, 0, 0xFFF);
+
+ // Swap components based on major component
+ (red0, green0, blue0, red1, green1, blue1) = majorComponent switch
+ {
+ 1 => (green0, red0, blue0, green1, red1, blue1),
+ 2 => (blue0, green0, red0, blue1, green1, red1),
+ _ => (red0, green0, blue0, red1, green1, blue1)
+ };
+
+ RgbaHdrColor lowResult = new((ushort)(red0 << 4), (ushort)(green0 << 4), (ushort)(blue0 << 4), 0x7800);
+ RgbaHdrColor highResult = new((ushort)(red1 << 4), (ushort)(green1 << 4), (ushort)(blue1 << 4), 0x7800);
+ return (lowResult, highResult);
+ }
+
+ private static (RgbaHdrColor Low, RgbaHdrColor High) UnpackHdrRgbDirectLdrAlphaCore(ReadOnlySpan unquantizedValues)
+ {
+ (RgbaHdrColor rgbLow, RgbaHdrColor rgbHigh) = UnpackHdrRgbDirectCore(unquantizedValues[0], unquantizedValues[1], unquantizedValues[2], unquantizedValues[3], unquantizedValues[4], unquantizedValues[5]);
+
+ ushort alpha0 = (ushort)(unquantizedValues[6] * 257);
+ ushort alpha1 = (ushort)(unquantizedValues[7] * 257);
+
+ RgbaHdrColor low = new(rgbLow.R, rgbLow.G, rgbLow.B, alpha0);
+ RgbaHdrColor high = new(rgbHigh.R, rgbHigh.G, rgbHigh.B, alpha1);
+ return (low, high);
+ }
+
+ private static (RgbaHdrColor Low, RgbaHdrColor High) UnpackHdrRgbDirectHdrAlphaCore(ReadOnlySpan unquantizedValues)
+ {
+ (RgbaHdrColor rgbLow, RgbaHdrColor rgbHigh) = UnpackHdrRgbDirectCore(unquantizedValues[0], unquantizedValues[1], unquantizedValues[2], unquantizedValues[3], unquantizedValues[4], unquantizedValues[5]);
+
+ (ushort alpha0, ushort alpha1) = UnpackHdrAlpha(unquantizedValues[6], unquantizedValues[7]);
+
+ RgbaHdrColor low = new(rgbLow.R, rgbLow.G, rgbLow.B, alpha0);
+ RgbaHdrColor high = new(rgbHigh.R, rgbHigh.G, rgbHigh.B, alpha1);
+ return (low, high);
+ }
+
+ ///
+ /// Decodes HDR alpha values
+ ///
+ private static (ushort Low, ushort High) UnpackHdrAlpha(int v6, int v7)
+ {
+ int selector = ((v6 >> 7) & 1) | ((v7 >> 6) & 2);
+ v6 &= 0x7F;
+ v7 &= 0x7F;
+
+ int a0, a1;
+
+ if (selector == 3)
+ {
+ // Simple mode: direct 7-bit values shifted to 12-bit
+ a0 = v6 << 5;
+ a1 = v7 << 5;
+ }
+ else
+ {
+ // Complex mode: base + sign-extended offset
+ v6 |= (v7 << (selector + 1)) & 0x780;
+ v7 &= 0x3F >> selector;
+ v7 ^= 32 >> selector;
+ v7 -= 32 >> selector;
+ v6 <<= 4 - selector;
+ v7 <<= 4 - selector;
+ v7 += v6;
+
+ if (v7 < 0)
+ {
+ v7 = 0;
+ }
+ else if (v7 > 0xFFF)
+ {
+ v7 = 0xFFF;
+ }
+
+ a0 = v6;
+ a1 = v7;
+ }
+
+ a0 = Math.Clamp(a0, 0, 0xFFF);
+ a1 = Math.Clamp(a1, 0, 0xFFF);
+
+ return ((ushort)(a0 << 4), (ushort)(a1 << 4));
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/ColorEncoding/Partition.cs b/src/ImageSharp.Textures.Astc/ColorEncoding/Partition.cs
new file mode 100644
index 00000000..1ea3df1d
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ColorEncoding/Partition.cs
@@ -0,0 +1,248 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+
+internal sealed class Partition
+{
+ private static readonly System.Collections.Concurrent.ConcurrentDictionary<(Footprint, int, int), Partition> PartitionCache = new();
+
+ public Partition(Footprint footprint, int partitionCount, int? id = null)
+ {
+ this.Footprint = footprint;
+ this.PartitionCount = partitionCount;
+ this.PartitionId = id;
+ this.Assignment = [];
+ }
+
+ public Footprint Footprint { get; set; }
+
+ public int PartitionCount { get; set; }
+
+ public int? PartitionId { get; set; }
+
+ public int[] Assignment { get; set; }
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not Partition other)
+ {
+ return false;
+ }
+
+ return PartitionMetric(this, other) == 0;
+ }
+
+ public override int GetHashCode() => HashCode.Combine(this.Footprint, this.PartitionCount, this.PartitionId);
+
+ public static int PartitionMetric(Partition a, Partition b)
+ {
+ ArgumentOutOfRangeException.ThrowIfNotEqual(a.Footprint, b.Footprint);
+
+ const int maxNumSubsets = 4;
+ int width = a.Footprint.Width;
+ int height = a.Footprint.Height;
+
+ List<(int A, int B, int Count)> pairCounts = [];
+ for (int y = 0; y < 4; ++y)
+ {
+ for (int x = 0; x < 4; ++x)
+ {
+ pairCounts.Add((x, y, 0));
+ }
+ }
+
+ for (int y = 0; y < height; ++y)
+ {
+ for (int x = 0; x < width; ++x)
+ {
+ int idx = (y * width) + x;
+ int aVal = a.Assignment[idx];
+ int bVal = b.Assignment[idx];
+ pairCounts[(bVal * 4) + aVal] = (aVal, bVal, pairCounts[(bVal * 4) + aVal].Count + 1);
+ }
+ }
+
+ List<(int A, int B, int Count)> sorted = [.. pairCounts.OrderByDescending(p => p.Count)];
+ bool[,] assigned = new bool[maxNumSubsets, maxNumSubsets];
+ int pixelsMatched = 0;
+ foreach ((int pairA, int pairB, int count) in sorted)
+ {
+ bool isAssigned = false;
+ for (int i = 0; i < maxNumSubsets; ++i)
+ {
+ if (assigned[pairA, i] || assigned[i, pairB])
+ {
+ isAssigned = true;
+ break;
+ }
+ }
+
+ if (!isAssigned)
+ {
+ assigned[pairA, pairB] = true;
+ pixelsMatched += count;
+ }
+ }
+
+ return (width * height) - pixelsMatched;
+ }
+
+ // Basic GetASTCPartition implementation using selection function from C++
+ public static Partition GetASTCPartition(Footprint footprint, int partitionCount, int partitionId)
+ {
+ (Footprint Footprint, int PartitionCount, int PartitionId) key = (footprint, partitionCount, partitionId);
+ if (PartitionCache.TryGetValue(key, out Partition? cached))
+ {
+ return cached;
+ }
+
+ Partition part = new(footprint, partitionCount, partitionId);
+ int w = footprint.Width;
+ int h = footprint.Height;
+ int[] assignment = new int[w * h];
+ int idx = 0;
+ for (int y = 0; y < h; ++y)
+ {
+ for (int x = 0; x < w; ++x)
+ {
+ assignment[idx++] = SelectASTCPartition(partitionId, x, y, 0, partitionCount, footprint.PixelCount);
+ }
+ }
+
+ part.Assignment = assignment;
+ PartitionCache.TryAdd(key, part);
+ return part;
+ }
+
+ // For now, implement a naive FindClosestASTCPartition that just checks a
+ // small set of candidate partitions generated by enum footprints. We'll
+ // return the same footprint size partitions with ID=0 for simplicity.
+ public static Partition FindClosestASTCPartition(Partition candidate)
+ {
+ // Search a few partitions and pick the one with minimal PartitionMetric
+ Partition best = GetASTCPartition(candidate.Footprint, Math.Max(1, candidate.PartitionCount), 0);
+ int bestDist = PartitionMetric(best, candidate);
+ return best;
+ }
+
+ // Very small port of selection function; behavior taken from C++ file.
+ private static int SelectASTCPartition(int seed, int x, int y, int z, int partitionCount, int pixelCount)
+ {
+ if (partitionCount <= 1)
+ {
+ return 0;
+ }
+
+ if (pixelCount < 31)
+ {
+ x <<= 1;
+ y <<= 1;
+ z <<= 1;
+ }
+
+ seed += (partitionCount - 1) * 1024;
+ uint randomNumber = (uint)seed;
+ randomNumber ^= randomNumber >> 15;
+ randomNumber -= randomNumber << 17;
+ randomNumber += randomNumber << 7;
+ randomNumber += randomNumber << 4;
+ randomNumber ^= randomNumber >> 5;
+ randomNumber += randomNumber << 16;
+ randomNumber ^= randomNumber >> 7;
+ randomNumber ^= randomNumber >> 3;
+ randomNumber ^= randomNumber << 6;
+ randomNumber ^= randomNumber >> 17;
+
+ uint seed1 = randomNumber & 0xF;
+ uint seed2 = (randomNumber >> 4) & 0xF;
+ uint seed3 = (randomNumber >> 8) & 0xF;
+ uint seed4 = (randomNumber >> 12) & 0xF;
+ uint seed5 = (randomNumber >> 16) & 0xF;
+ uint seed6 = (randomNumber >> 20) & 0xF;
+ uint seed7 = (randomNumber >> 24) & 0xF;
+ uint seed8 = (randomNumber >> 28) & 0xF;
+ uint seed9 = (randomNumber >> 18) & 0xF;
+ uint seed10 = (randomNumber >> 22) & 0xF;
+ uint seed11 = (randomNumber >> 26) & 0xF;
+ uint seed12 = ((randomNumber >> 30) | (randomNumber << 2)) & 0xF;
+
+ seed1 *= seed1;
+ seed2 *= seed2;
+ seed3 *= seed3;
+ seed4 *= seed4;
+ seed5 *= seed5;
+ seed6 *= seed6;
+ seed7 *= seed7;
+ seed8 *= seed8;
+ seed9 *= seed9;
+ seed10 *= seed10;
+ seed11 *= seed11;
+ seed12 *= seed12;
+
+ int sh1, sh2, sh3;
+ if ((seed & 1) != 0)
+ {
+ sh1 = (seed & 2) != 0 ? 4 : 5;
+ sh2 = (partitionCount == 3) ? 6 : 5;
+ }
+ else
+ {
+ sh1 = (partitionCount == 3) ? 6 : 5;
+ sh2 = (seed & 2) != 0 ? 4 : 5;
+ }
+
+ sh3 = (seed & 0x10) != 0 ? sh1 : sh2;
+
+ seed1 >>= sh1;
+ seed2 >>= sh2;
+ seed3 >>= sh1;
+ seed4 >>= sh2;
+ seed5 >>= sh1;
+ seed6 >>= sh2;
+ seed7 >>= sh1;
+ seed8 >>= sh2;
+ seed9 >>= sh3;
+ seed10 >>= sh3;
+ seed11 >>= sh3;
+ seed12 >>= sh3;
+
+ int a = (int)((seed1 * x) + (seed2 * y) + (seed11 * z) + (randomNumber >> 14));
+ int b = (int)((seed3 * x) + (seed4 * y) + (seed12 * z) + (randomNumber >> 10));
+ int c = (int)((seed5 * x) + (seed6 * y) + (seed9 * z) + (randomNumber >> 6));
+ int d = (int)((seed7 * x) + (seed8 * y) + (seed10 * z) + (randomNumber >> 2));
+
+ a &= 0x3F;
+ b &= 0x3F;
+ c &= 0x3F;
+ d &= 0x3F;
+ if (partitionCount <= 3)
+ {
+ d = 0;
+ }
+
+ if (partitionCount <= 2)
+ {
+ c = 0;
+ }
+
+ if (a >= b && a >= c && a >= d)
+ {
+ return 0;
+ }
+ else if (b >= c && b >= d)
+ {
+ return 1;
+ }
+ else if (c >= d)
+ {
+ return 2;
+ }
+ else
+ {
+ return 3;
+ }
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/ColorEncoding/RgbaColorExtensions.cs b/src/ImageSharp.Textures.Astc/ColorEncoding/RgbaColorExtensions.cs
new file mode 100644
index 00000000..c8b7740e
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ColorEncoding/RgbaColorExtensions.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+
+internal static class RgbaColorExtensions
+{
+ ///
+ /// Uses the value in the blue channel to tint the red and green
+ ///
+ ///
+ /// Applies the 'blue_contract' function defined in Section C.2.14 of the ASTC specification.
+ ///
+ public static RgbaColor WithBlueContract(int red, int green, int blue, int alpha)
+ => new(
+ r: (red + blue) >> 1,
+ g: (green + blue) >> 1,
+ b: blue,
+ a: alpha);
+
+ ///
+ /// Uses the value in the blue channel to tint the red and green
+ ///
+ ///
+ /// Applies the 'blue_contract' function defined in Section C.2.14 of the ASTC specification.
+ ///
+ public static RgbaColor WithBlueContract(this RgbaColor color)
+ => WithBlueContract(color.R, color.G, color.B, color.A);
+
+ ///
+ /// The inverse of
+ ///
+ public static RgbaColor WithInvertedBlueContract(this RgbaColor color)
+ => new(
+ r: (2 * color.R) - color.B,
+ g: (2 * color.G) - color.B,
+ b: color.B,
+ a: color.A);
+
+ public static RgbaColor AsOffsetFrom(this RgbaColor color, RgbaColor baseColor)
+ {
+ int[] offset = [color.R, color.G, color.B, color.A];
+
+ for (int i = 0; i < RgbaColor.BytesPerPixel; ++i)
+ {
+ (int a, int b) = BitOperations.TransferPrecision(offset[i], baseColor[i]);
+ offset[i] = Math.Clamp(baseColor[i] + a, byte.MinValue, byte.MaxValue);
+ }
+
+ return new RgbaColor(offset[0], offset[1], offset[2], offset[3]);
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/Core/BitOperations.cs b/src/ImageSharp.Textures.Astc/Core/BitOperations.cs
new file mode 100644
index 00000000..4f6f8fbc
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/Core/BitOperations.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.Core;
+
+internal static class BitOperations
+{
+ ///
+ /// Return the specified range as a (low bits in lower 64 bits)
+ ///
+ public static UInt128 GetBits(UInt128 value, int start, int length)
+ {
+ if (length <= 0)
+ {
+ return UInt128.Zero;
+ }
+
+ UInt128 shifted = value >> start;
+ if (length >= 128)
+ {
+ return shifted;
+ }
+
+ if (length >= 64)
+ {
+ ulong lowMask = ~0UL;
+ int highBits = length - 64;
+ ulong highMask = (highBits == 64)
+ ? ~0UL
+ : ((1UL << highBits) - 1UL);
+
+ return new UInt128(shifted.High() & highMask, shifted.Low() & lowMask);
+ }
+ else
+ {
+ ulong mask = (length == 64)
+ ? ~0UL
+ : ((1UL << length) - 1UL);
+
+ return new UInt128(0, shifted.Low() & mask);
+ }
+ }
+
+ ///
+ /// Return the specified range as a ulong
+ ///
+ public static ulong GetBits(ulong value, int start, int length)
+ {
+ if (length <= 0)
+ {
+ return 0UL;
+ }
+
+ int totalBits = sizeof(ulong) * 8;
+ ulong mask = length == totalBits
+ ? ~0UL
+ : ~0UL >> (totalBits - length);
+
+ return (value >> start) & mask;
+ }
+
+ ///
+ /// Transfers a few bits of precision from one value to another.
+ ///
+ ///
+ /// The 'bit_transfer_signed' function defined in Section C.2.14 of the ASTC specification
+ ///
+ public static (int A, int B) TransferPrecision(int a, int b)
+ {
+ b >>= 1;
+ b |= a & 0x80;
+ a >>= 1;
+ a &= 0x3F;
+
+ if ((a & 0x20) != 0)
+ {
+ a -= 0x40;
+ }
+
+ return (a, b);
+ }
+
+ ///
+ /// Takes two values, |a| in the range [-32, 31], and |b| in the range [0, 255],
+ /// and returns the two values in [0, 255] that will reconstruct |a| and |b| when
+ /// passed to the function.
+ ///
+ public static (int A, int B) TransferPrecisionInverse(int a, int b)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(a, -32);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(a, 31);
+ ArgumentOutOfRangeException.ThrowIfLessThan(b, byte.MinValue);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(b, byte.MaxValue);
+
+ if (a < 0)
+ {
+ a += 0x40;
+ }
+
+ a <<= 1;
+ a |= b & 0x80;
+ b <<= 1;
+ b &= 0xff;
+
+ return (a, b);
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/Core/DecimationInfo.cs b/src/ImageSharp.Textures.Astc/Core/DecimationInfo.cs
new file mode 100644
index 00000000..d513b353
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/Core/DecimationInfo.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.Core;
+
+///
+/// Pre-computed weight infill data for a specific (footprint, weightGridX, weightGridY) combination.
+/// Stores bilinear interpolation indices and factors in a transposed layout.
+///
+internal sealed class DecimationInfo
+{
+ // Transposed layout: [contribution * TexelCount + texel]
+ // 4 contributions per texel (bilinear interpolation from weight grid).
+ // For edge texels where some grid points are out of bounds, factor is 0 and index is 0.
+ public DecimationInfo(int texelCount, int[] weightIndices, int[] weightFactors)
+ {
+ this.TexelCount = texelCount;
+ this.WeightIndices = weightIndices;
+ this.WeightFactors = weightFactors;
+ }
+
+ public int TexelCount { get; }
+
+ public int[] WeightIndices { get; }
+
+ public int[] WeightFactors { get; }
+}
diff --git a/src/ImageSharp.Textures.Astc/Core/DecimationTable.cs b/src/ImageSharp.Textures.Astc/Core/DecimationTable.cs
new file mode 100644
index 00000000..5be931ff
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/Core/DecimationTable.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Textures.Astc.Core;
+
+///
+/// Caches pre-computed DecimationInfo tables and provides weight infill.
+/// For each unique (footprint, gridX, gridY) combination, the bilinear interpolation
+/// indices and factors are computed once and reused for every block with that configuration.
+/// Uses a flat array indexed by (footprintType, gridX, gridY) for O(1) lookup.
+///
+internal static class DecimationTable
+{
+ // Grid dimensions range from 2 to 12 inclusive
+ private const int GridMin = 2;
+ private const int GridRange = 11; // 12 - 2 + 1
+ private const int FootprintCount = 14;
+ private static readonly DecimationInfo?[] Table = new DecimationInfo?[FootprintCount * GridRange * GridRange];
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static DecimationInfo Get(Footprint footprint, int gridX, int gridY)
+ {
+ int index = ((int)footprint.Type * GridRange * GridRange) + ((gridX - GridMin) * GridRange) + (gridY - GridMin);
+ DecimationInfo? decimationInfo = Table[index];
+ if (decimationInfo is null)
+ {
+ decimationInfo = Compute(footprint.Width, footprint.Height, gridX, gridY);
+ Table[index] = decimationInfo;
+ }
+
+ return decimationInfo;
+ }
+
+ ///
+ /// Performs weight infill using pre-computed tables.
+ /// Maps unquantized grid weights to per-texel weights via bilinear interpolation
+ /// with pre-computed indices and factors.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ public static void InfillWeights(ReadOnlySpan gridWeights, DecimationInfo decimationInfo, Span result)
+ {
+ int texelCount = decimationInfo.TexelCount;
+ int[] weightIndices = decimationInfo.WeightIndices;
+ int[] weightFactors = decimationInfo.WeightFactors;
+ int offset1 = texelCount, offset2 = texelCount * 2, offset3 = texelCount * 3;
+
+ for (int i = 0; i < texelCount; i++)
+ {
+ result[i] = (8
+ + (gridWeights[weightIndices[i]] * weightFactors[i])
+ + (gridWeights[weightIndices[offset1 + i]] * weightFactors[offset1 + i])
+ + (gridWeights[weightIndices[offset2 + i]] * weightFactors[offset2 + i])
+ + (gridWeights[weightIndices[offset3 + i]] * weightFactors[offset3 + i])) >> 4;
+ }
+ }
+
+ private static int GetScaleFactorD(int blockDimensions) => (int)((1024f + (blockDimensions >> 1)) / (blockDimensions - 1));
+
+ private static DecimationInfo Compute(int footprintWidth, int footprintHeight, int gridWidth, int gridHeight)
+ {
+ int texelCount = footprintWidth * footprintHeight;
+
+ int[] indices = new int[4 * texelCount];
+ int[] factors = new int[4 * texelCount];
+
+ int scaleHorizontal = GetScaleFactorD(footprintWidth);
+ int scaleVertical = GetScaleFactorD(footprintHeight);
+ int gridLimit = gridWidth * gridHeight;
+ int maxGridX = gridWidth - 1;
+ int maxGridY = gridHeight - 1;
+
+ int texelIndex = 0;
+ for (int texelY = 0; texelY < footprintHeight; ++texelY)
+ {
+ int scaledY = scaleVertical * texelY;
+ int gridY = ((scaledY * maxGridY) + 32) >> 6;
+ int gridRowIndex = gridY >> 4;
+ int fractionY = gridY & 0xF;
+
+ for (int texelX = 0; texelX < footprintWidth; ++texelX)
+ {
+ int scaledX = scaleHorizontal * texelX;
+ int gridX = ((scaledX * maxGridX) + 32) >> 6;
+ int gridColIndex = gridX >> 4;
+ int fractionX = gridX & 0xF;
+
+ int gridPoint0 = gridColIndex + (gridWidth * gridRowIndex);
+ int gridPoint1 = gridPoint0 + 1;
+ int gridPoint2 = gridColIndex + (gridWidth * (gridRowIndex + 1));
+ int gridPoint3 = gridPoint2 + 1;
+
+ int factor3 = ((fractionX * fractionY) + 8) >> 4;
+ int factor2 = fractionY - factor3;
+ int factor1 = fractionX - factor3;
+ int factor0 = 16 - fractionX - fractionY + factor3;
+
+ // For out-of-bounds grid points, zero the factor and use index 0 (safe dummy)
+ if (gridPoint3 >= gridLimit)
+ {
+ factor3 = 0;
+ gridPoint3 = 0;
+ }
+
+ if (gridPoint2 >= gridLimit)
+ {
+ factor2 = 0;
+ gridPoint2 = 0;
+ }
+
+ if (gridPoint1 >= gridLimit)
+ {
+ factor1 = 0;
+ gridPoint1 = 0;
+ }
+
+ if (gridPoint0 >= gridLimit)
+ {
+ factor0 = 0;
+ gridPoint0 = 0;
+ }
+
+ indices[(0 * texelCount) + texelIndex] = gridPoint0;
+ indices[(1 * texelCount) + texelIndex] = gridPoint1;
+ indices[(2 * texelCount) + texelIndex] = gridPoint2;
+ indices[(3 * texelCount) + texelIndex] = gridPoint3;
+
+ factors[(0 * texelCount) + texelIndex] = factor0;
+ factors[(1 * texelCount) + texelIndex] = factor1;
+ factors[(2 * texelCount) + texelIndex] = factor2;
+ factors[(3 * texelCount) + texelIndex] = factor3;
+
+ texelIndex++;
+ }
+ }
+
+ return new DecimationInfo(texelCount, indices, factors);
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/Core/Footprint.cs b/src/ImageSharp.Textures.Astc/Core/Footprint.cs
new file mode 100644
index 00000000..3ccd7027
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/Core/Footprint.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.Core;
+
+///
+/// Represents the dimensions of an ASTC block footprint.
+///
+public readonly record struct Footprint
+{
+ private Footprint(FootprintType type, int width, int height)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(width);
+ ArgumentOutOfRangeException.ThrowIfNegative(height);
+
+ this.Type = type;
+ this.Width = width;
+ this.Height = height;
+ this.PixelCount = width * height;
+ }
+
+ /// Gets the block width in texels.
+ public int Width { get; }
+
+ /// Gets the block height in texels.
+ public int Height { get; }
+
+ /// Gets the footprint type enum value.
+ public FootprintType Type { get; }
+
+ /// Gets the total number of texels in the block (Width * Height).
+ public int PixelCount { get; }
+
+ ///
+ /// Creates a from the specified .
+ ///
+ /// The footprint type to create a footprint from.
+ /// A matching the specified type.
+ public static Footprint FromFootprintType(FootprintType type) => type switch
+ {
+ FootprintType.Footprint4x4 => Get4x4(),
+ FootprintType.Footprint5x4 => Get5x4(),
+ FootprintType.Footprint5x5 => Get5x5(),
+ FootprintType.Footprint6x5 => Get6x5(),
+ FootprintType.Footprint6x6 => Get6x6(),
+ FootprintType.Footprint8x5 => Get8x5(),
+ FootprintType.Footprint8x6 => Get8x6(),
+ FootprintType.Footprint8x8 => Get8x8(),
+ FootprintType.Footprint10x5 => Get10x5(),
+ FootprintType.Footprint10x6 => Get10x6(),
+ FootprintType.Footprint10x8 => Get10x8(),
+ FootprintType.Footprint10x10 => Get10x10(),
+ FootprintType.Footprint12x10 => Get12x10(),
+ FootprintType.Footprint12x12 => Get12x12(),
+ _ => throw new ArgumentOutOfRangeException($"Invalid FootprintType: {type}"),
+ };
+
+ internal static Footprint Get4x4() => new(FootprintType.Footprint4x4, 4, 4);
+
+ internal static Footprint Get5x4() => new(FootprintType.Footprint5x4, 5, 4);
+
+ internal static Footprint Get5x5() => new(FootprintType.Footprint5x5, 5, 5);
+
+ internal static Footprint Get6x5() => new(FootprintType.Footprint6x5, 6, 5);
+
+ internal static Footprint Get6x6() => new(FootprintType.Footprint6x6, 6, 6);
+
+ internal static Footprint Get8x5() => new(FootprintType.Footprint8x5, 8, 5);
+
+ internal static Footprint Get8x6() => new(FootprintType.Footprint8x6, 8, 6);
+
+ internal static Footprint Get8x8() => new(FootprintType.Footprint8x8, 8, 8);
+
+ internal static Footprint Get10x5() => new(FootprintType.Footprint10x5, 10, 5);
+
+ internal static Footprint Get10x6() => new(FootprintType.Footprint10x6, 10, 6);
+
+ internal static Footprint Get10x8() => new(FootprintType.Footprint10x8, 10, 8);
+
+ internal static Footprint Get10x10() => new(FootprintType.Footprint10x10, 10, 10);
+
+ internal static Footprint Get12x10() => new(FootprintType.Footprint12x10, 12, 10);
+
+ internal static Footprint Get12x12() => new(FootprintType.Footprint12x12, 12, 12);
+}
diff --git a/src/ImageSharp.Textures.Astc/Core/FootprintType.cs b/src/ImageSharp.Textures.Astc/Core/FootprintType.cs
new file mode 100644
index 00000000..65d983cb
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/Core/FootprintType.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.Core;
+
+///
+/// The supported ASTC block footprint sizes.
+///
+public enum FootprintType
+{
+ /// 4x4 texel block.
+ Footprint4x4,
+
+ /// 5x4 texel block.
+ Footprint5x4,
+
+ /// 5x5 texel block.
+ Footprint5x5,
+
+ /// 6x5 texel block.
+ Footprint6x5,
+
+ /// 6x6 texel block.
+ Footprint6x6,
+
+ /// 8x5 texel block.
+ Footprint8x5,
+
+ /// 8x6 texel block.
+ Footprint8x6,
+
+ /// 8x8 texel block.
+ Footprint8x8,
+
+ /// 10x5 texel block.
+ Footprint10x5,
+
+ /// 10x6 texel block.
+ Footprint10x6,
+
+ /// 10x8 texel block.
+ Footprint10x8,
+
+ /// 10x10 texel block.
+ Footprint10x10,
+
+ /// 12x10 texel block.
+ Footprint12x10,
+
+ /// 12x12 texel block.
+ Footprint12x12,
+}
diff --git a/src/ImageSharp.Textures.Astc/Core/RgbColor.cs b/src/ImageSharp.Textures.Astc/Core/RgbColor.cs
new file mode 100644
index 00000000..19d5e4c4
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/Core/RgbColor.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.Core;
+
+internal readonly record struct RgbColor(byte R, byte G, byte B)
+{
+ public RgbColor(int r, int g, int b)
+ : this(
+ (byte)Math.Clamp(r, byte.MinValue, byte.MaxValue),
+ (byte)Math.Clamp(g, byte.MinValue, byte.MaxValue),
+ (byte)Math.Clamp(b, byte.MinValue, byte.MaxValue))
+ {
+ }
+
+ public static int BytesPerPixel => 3;
+
+ public static RgbColor Empty => default;
+
+ ///
+ /// Gets the rounded arithmetic mean of the R, G, and B channels.
+ ///
+ public byte Average
+ {
+ get
+ {
+ int sum = this.R + this.G + this.B;
+ return (byte)(((sum * 256) + 384) / 768);
+ }
+ }
+
+ public int this[int i]
+ => i switch
+ {
+ 0 => this.R,
+ 1 => this.G,
+ 2 => this.B,
+ _ => throw new ArgumentOutOfRangeException(nameof(i), $"Index must be between 0 and {BytesPerPixel - 1}. Actual value: {i}.")
+ };
+
+ public static int SquaredError(RgbColor a, RgbColor b)
+ {
+ int result = 0;
+ for (int i = 0; i < BytesPerPixel; i++)
+ {
+ int diff = a[i] - b[i];
+ result += diff * diff;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Computes the squared error comparing only the RGB channels of two RgbaColors.
+ ///
+ public static int SquaredError(RgbaColor a, RgbaColor b)
+ {
+ int dr = a.R - b.R;
+ int dg = a.G - b.G;
+ int db = a.B - b.B;
+ return (dr * dr) + (dg * dg) + (db * db);
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/Core/RgbaColor.cs b/src/ImageSharp.Textures.Astc/Core/RgbaColor.cs
new file mode 100644
index 00000000..69824fdf
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/Core/RgbaColor.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.Core;
+
+internal readonly record struct RgbaColor(byte R, byte G, byte B, byte A)
+{
+ public RgbaColor(int r, int g, int b, int a = byte.MaxValue)
+ : this(
+ (byte)Math.Clamp(r, byte.MinValue, byte.MaxValue),
+ (byte)Math.Clamp(g, byte.MinValue, byte.MaxValue),
+ (byte)Math.Clamp(b, byte.MinValue, byte.MaxValue),
+ (byte)Math.Clamp(a, byte.MinValue, byte.MaxValue))
+ {
+ }
+
+ public static int BytesPerPixel => 4;
+
+ public static RgbaColor Empty => default;
+
+ ///
+ /// Gets the rounded arithmetic mean of the R, G, and B channels.
+ ///
+ public byte Average
+ {
+ get
+ {
+ int sum = this.R + this.G + this.B;
+ return (byte)(((sum * 256) + 384) / 768);
+ }
+ }
+
+ public int this[int i]
+ => i switch
+ {
+ 0 => this.R,
+ 1 => this.G,
+ 2 => this.B,
+ 3 => this.A,
+ _ => throw new ArgumentOutOfRangeException(nameof(i), $"Index must be between 0 and {BytesPerPixel - 1}. Actual value: {i}.")
+ };
+
+ public static int SquaredError(RgbaColor a, RgbaColor b)
+ {
+ int result = 0;
+ for (int i = 0; i < BytesPerPixel; i++)
+ {
+ int diff = a[i] - b[i];
+ result += diff * diff;
+ }
+
+ return result;
+ }
+
+ public bool IsCloseTo(RgbaColor other, int tolerance)
+ => Math.Abs(this.R - other.R) <= tolerance &&
+ Math.Abs(this.G - other.G) <= tolerance &&
+ Math.Abs(this.B - other.B) <= tolerance &&
+ Math.Abs(this.A - other.A) <= tolerance;
+}
diff --git a/src/ImageSharp.Textures.Astc/Core/RgbaHdrColor.cs b/src/ImageSharp.Textures.Astc/Core/RgbaHdrColor.cs
new file mode 100644
index 00000000..f73b810d
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/Core/RgbaHdrColor.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.Core;
+
+///
+/// Represents an HDR (High Dynamic Range) color with 16-bit per-channel precision.
+///
+///
+/// HDR colors use ushort values (0-65535) for each channel, allowing representation
+/// of values beyond the standard 0-255 LDR range. This enables encoding of High Dynamic
+/// Range content that can represent brightness values exceeding the typical white point.
+///
+internal readonly record struct RgbaHdrColor(ushort R, ushort G, ushort B, ushort A)
+{
+ public static RgbaHdrColor Empty => default;
+
+ ///
+ /// Indexer to access channels by index: 0=R, 1=G, 2=B, 3=A
+ ///
+ public ushort this[int i] => i switch
+ {
+ 0 => this.R,
+ 1 => this.G,
+ 2 => this.B,
+ 3 => this.A,
+ _ => throw new ArgumentOutOfRangeException(nameof(i), $"Index must be between 0 and 3. Actual value: {i}.")
+ };
+
+ ///
+ /// Converts an LDR color (0-255) to HDR range (0-65535).
+ ///
+ public static RgbaHdrColor FromRgba(RgbaColor ldr)
+ => new((ushort)(ldr.R * 257), (ushort)(ldr.G * 257), (ushort)(ldr.B * 257), (ushort)(ldr.A * 257));
+
+ ///
+ /// Converts an HDR color (0-65535) to LDR range (0-255).
+ ///
+ ///
+ /// Values are clamped to 0-255 range, so HDR values exceeding
+ /// the standard white point will be clipped.
+ ///
+ public RgbaColor ToLowDynamicRange()
+ => new((byte)(this.R >> 8), (byte)(this.G >> 8), (byte)(this.B >> 8), (byte)(this.A >> 8));
+
+ public bool IsCloseTo(RgbaHdrColor other, int tolerance)
+ => Math.Abs(this.R - other.R) <= tolerance &&
+ Math.Abs(this.G - other.G) <= tolerance &&
+ Math.Abs(this.B - other.B) <= tolerance &&
+ Math.Abs(this.A - other.A) <= tolerance;
+}
diff --git a/src/ImageSharp.Textures.Astc/Core/SimdHelpers.cs b/src/ImageSharp.Textures.Astc/Core/SimdHelpers.cs
new file mode 100644
index 00000000..bc6f7448
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/Core/SimdHelpers.cs
@@ -0,0 +1,175 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
+
+namespace SixLabors.ImageSharp.Textures.Astc.Core;
+
+internal static class SimdHelpers
+{
+ private static readonly Vector128 Vec32 = Vector128.Create(32);
+ private static readonly Vector128 Vec64 = Vector128.Create(64);
+ private static readonly Vector128 Vec255 = Vector128.Create(255);
+ private static readonly Vector128 Vec32767 = Vector128.Create(32767);
+
+ ///
+ /// Interpolates one channel for 4 pixels simultaneously.
+ /// All 4 pixels share the same endpoint values but have different weights.
+ /// Returns 4 byte results packed into the lower bytes of a .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Interpolate4ChannelPixels(int p0, int p1, Vector128 weights)
+ {
+ // Bit-replicate endpoint bytes to 16-bit
+ Vector128 c0 = Vector128.Create((p0 << 8) | p0);
+ Vector128 c1 = Vector128.Create((p1 << 8) | p1);
+
+ // c = (c0 * (64 - w) + c1 * w + 32) >> 6
+ // NOTE: Using >> 6 instead of / 64 because Vector128 division
+ // has no hardware support and decomposes to scalar operations.
+ Vector128 w64 = Vec64 - weights;
+ Vector128 c = ((c0 * w64) + (c1 * weights) + Vec32) >> 6;
+
+ // Quantize: (c * 255 + 32767) >> 16, clamped to [0, 255]
+ Vector128 result = ((c * Vec255) + Vec32767) >>> 16;
+ return Vector128.Min(Vector128.Max(result, Vector128.Zero), Vec255);
+ }
+
+ ///
+ /// Writes 4 LDR pixels directly to output buffer using SIMD.
+ /// Processes each channel across 4 pixels in parallel, then interleaves to RGBA output.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Write4PixelLdr(
+ Span output,
+ int offset,
+ int lowR,
+ int lowG,
+ int lowB,
+ int lowA,
+ int highR,
+ int highG,
+ int highB,
+ int highA,
+ Vector128 weights)
+ {
+ Vector128 r = Interpolate4ChannelPixels(lowR, highR, weights);
+ Vector128 g = Interpolate4ChannelPixels(lowG, highG, weights);
+ Vector128 b = Interpolate4ChannelPixels(lowB, highB, weights);
+ Vector128 a = Interpolate4ChannelPixels(lowA, highA, weights);
+
+ // Pack 4 RGBA pixels into 16 bytes via vector OR+shift.
+ // Each int element has its channel value in bits [0:7].
+ // Combine: element[i] = R[i] | (G[i] << 8) | (B[i] << 16) | (A[i] << 24)
+ // On little-endian, storing this int32 writes bytes [R, G, B, A].
+ Vector128 rgba = r | (g << 8) | (b << 16) | (a << 24);
+ rgba.AsByte().CopyTo(output.Slice(offset, 16));
+ }
+
+ ///
+ /// Scalar single-pixel LDR interpolation, writing directly to buffer.
+ /// No RgbaColor allocation.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteSinglePixelLdr(
+ Span output,
+ int offset,
+ int lowR,
+ int lowG,
+ int lowB,
+ int lowA,
+ int highR,
+ int highG,
+ int highB,
+ int highA,
+ int weight)
+ {
+ output[offset + 0] = (byte)InterpolateChannelScalar(lowR, highR, weight);
+ output[offset + 1] = (byte)InterpolateChannelScalar(lowG, highG, weight);
+ output[offset + 2] = (byte)InterpolateChannelScalar(lowB, highB, weight);
+ output[offset + 3] = (byte)InterpolateChannelScalar(lowA, highA, weight);
+ }
+
+ ///
+ /// Scalar single-pixel dual-plane LDR interpolation, writing directly to buffer.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteSinglePixelLdrDualPlane(
+ Span output,
+ int offset,
+ int lowR,
+ int lowG,
+ int lowB,
+ int lowA,
+ int highR,
+ int highG,
+ int highB,
+ int highA,
+ int weight,
+ int dpChannel,
+ int dpWeight)
+ {
+ output[offset + 0] = (byte)InterpolateChannelScalar(
+ lowR,
+ highR,
+ dpChannel == 0 ? dpWeight : weight);
+ output[offset + 1] = (byte)InterpolateChannelScalar(
+ lowG,
+ highG,
+ dpChannel == 1 ? dpWeight : weight);
+ output[offset + 2] = (byte)InterpolateChannelScalar(
+ lowB,
+ highB,
+ dpChannel == 2 ? dpWeight : weight);
+ output[offset + 3] = (byte)InterpolateChannelScalar(
+ lowA,
+ highA,
+ dpChannel == 3 ? dpWeight : weight);
+ }
+
+ // Keep the old API for ColorAt() (used by tests and non-hot paths)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static RgbaColor InterpolateColorLdr(RgbaColor low, RgbaColor high, int weight)
+ => new(
+ r: InterpolateChannelScalar(low.R, high.R, weight),
+ g: InterpolateChannelScalar(low.G, high.G, weight),
+ b: InterpolateChannelScalar(low.B, high.B, weight),
+ a: InterpolateChannelScalar(low.A, high.A, weight));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static RgbaColor InterpolateColorLdrDualPlane(
+ RgbaColor low,
+ RgbaColor high,
+ int weight,
+ int dualPlaneChannel,
+ int dualPlaneWeight)
+ => new(
+ r: InterpolateChannelScalar(
+ low.R,
+ high.R,
+ dualPlaneChannel == 0 ? dualPlaneWeight : weight),
+ g: InterpolateChannelScalar(
+ low.G,
+ high.G,
+ dualPlaneChannel == 1 ? dualPlaneWeight : weight),
+ b: InterpolateChannelScalar(
+ low.B,
+ high.B,
+ dualPlaneChannel == 2 ? dualPlaneWeight : weight),
+ a: InterpolateChannelScalar(
+ low.A,
+ high.A,
+ dualPlaneChannel == 3 ? dualPlaneWeight : weight));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static int InterpolateChannelScalar(int p0, int p1, int weight)
+ {
+ int c0 = (p0 << 8) | p0;
+ int c1 = (p1 << 8) | p1;
+ int c = ((c0 * (64 - weight)) + (c1 * weight) + 32) / 64;
+ int quantized = ((c * 255) + 32767) / 65536;
+
+ return Math.Clamp(quantized, 0, 255);
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/Core/UInt128Extensions.cs b/src/ImageSharp.Textures.Astc/Core/UInt128Extensions.cs
new file mode 100644
index 00000000..4df89401
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/Core/UInt128Extensions.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.Core;
+
+internal static class UInt128Extensions
+{
+ ///
+ /// The lower 64 bits of the value
+ ///
+ public static ulong Low(this UInt128 value)
+ => (ulong)(value & 0xFFFFFFFFFFFFFFFFUL);
+
+ ///
+ /// The upper 64 bits of the value
+ ///
+ public static ulong High(this UInt128 value)
+ => (ulong)(value >> 64);
+
+ ///
+ /// A mask with the lowest n bits set to 1
+ ///
+ public static UInt128 OnesMask(int n)
+ {
+ if (n <= 0)
+ {
+ return UInt128.Zero;
+ }
+
+ if (n >= 128)
+ {
+ return new UInt128(~0UL, ~0UL);
+ }
+
+ if (n <= 64)
+ {
+ ulong low = (n == 64)
+ ? ~0UL
+ : ((1UL << n) - 1UL);
+
+ return new UInt128(0UL, low);
+ }
+ else
+ {
+ int highBits = n - 64;
+ ulong low = ~0UL;
+ ulong high = (highBits == 64)
+ ? ~0UL
+ : ((1UL << highBits) - 1UL);
+
+ return new UInt128(high, low);
+ }
+ }
+
+ ///
+ /// Reverse bits across the full 128-bit value
+ ///
+ public static UInt128 ReverseBits(this UInt128 value)
+ {
+ ulong revLow = ReverseBits(value.Low());
+ ulong revHigh = ReverseBits(value.High());
+
+ return new UInt128(revLow, revHigh);
+ }
+
+ private static ulong ReverseBits(ulong x)
+ {
+ x = ((x >> 1) & 0x5555555555555555UL) | ((x & 0x5555555555555555UL) << 1);
+ x = ((x >> 2) & 0x3333333333333333UL) | ((x & 0x3333333333333333UL) << 2);
+ x = ((x >> 4) & 0x0F0F0F0F0F0F0F0FUL) | ((x & 0x0F0F0F0F0F0F0F0FUL) << 4);
+ x = ((x >> 8) & 0x00FF00FF00FF00FFUL) | ((x & 0x00FF00FF00FF00FFUL) << 8);
+ x = ((x >> 16) & 0x0000FFFF0000FFFFUL) | ((x & 0x0000FFFF0000FFFFUL) << 16);
+ x = (x >> 32) | (x << 32);
+
+ return x;
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/IO/AstcFile.cs b/src/ImageSharp.Textures.Astc/IO/AstcFile.cs
new file mode 100644
index 00000000..84a46481
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/IO/AstcFile.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.IO;
+
+///
+/// A very simple format consisting of a small header followed immediately
+/// by the binary payload for a single image surface.
+///
+///
+/// See https://github.com/ARM-software/astc-encoder/blob/main/Docs/FileFormat.md
+///
+internal record AstcFile
+{
+ private readonly AstcFileHeader header;
+ private readonly byte[] blocks;
+
+ internal AstcFile(AstcFileHeader header, byte[] blocks)
+ {
+ this.header = header;
+ this.blocks = blocks;
+ this.Footprint = this.GetFootprint();
+ }
+
+ public ReadOnlySpan Blocks => this.blocks;
+
+ public Footprint Footprint { get; }
+
+ public int Width => this.header.ImageWidth;
+
+ public int Height => this.header.ImageHeight;
+
+ public int Depth => this.header.ImageDepth;
+
+ public static AstcFile FromMemory(byte[] data)
+ {
+ AstcFileHeader header = AstcFileHeader.FromMemory(data.AsSpan(0, AstcFileHeader.SizeInBytes));
+
+ // Remaining bytes are blocks; C++ reference keeps them as string; here we keep as byte[]
+ byte[] blocks = new byte[data.Length - AstcFileHeader.SizeInBytes];
+ Array.Copy(data, AstcFileHeader.SizeInBytes, blocks, 0, blocks.Length);
+
+ return new AstcFile(header, blocks);
+ }
+
+ ///
+ /// Map the block dimensions in the header to a Footprint, if possible.
+ ///
+ private Footprint GetFootprint() => (this.header.BlockWidth, this.header.BlockHeight) switch
+ {
+ (4, 4) => Footprint.FromFootprintType(FootprintType.Footprint4x4),
+ (5, 4) => Footprint.FromFootprintType(FootprintType.Footprint5x4),
+ (5, 5) => Footprint.FromFootprintType(FootprintType.Footprint5x5),
+ (6, 5) => Footprint.FromFootprintType(FootprintType.Footprint6x5),
+ (6, 6) => Footprint.FromFootprintType(FootprintType.Footprint6x6),
+ (8, 5) => Footprint.FromFootprintType(FootprintType.Footprint8x5),
+ (8, 6) => Footprint.FromFootprintType(FootprintType.Footprint8x6),
+ (8, 8) => Footprint.FromFootprintType(FootprintType.Footprint8x8),
+ (10, 5) => Footprint.FromFootprintType(FootprintType.Footprint10x5),
+ (10, 6) => Footprint.FromFootprintType(FootprintType.Footprint10x6),
+ (10, 8) => Footprint.FromFootprintType(FootprintType.Footprint10x8),
+ (10, 10) => Footprint.FromFootprintType(FootprintType.Footprint10x10),
+ (12, 10) => Footprint.FromFootprintType(FootprintType.Footprint12x10),
+ (12, 12) => Footprint.FromFootprintType(FootprintType.Footprint12x12),
+ _ => throw new ArgumentOutOfRangeException($"Unsupported block dimensions: {this.header.BlockWidth}x{this.header.BlockHeight}"),
+ };
+}
diff --git a/src/ImageSharp.Textures.Astc/IO/AstcFileHeader.cs b/src/ImageSharp.Textures.Astc/IO/AstcFileHeader.cs
new file mode 100644
index 00000000..497a6ceb
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/IO/AstcFileHeader.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.IO;
+
+///
+/// The 16 byte ASTC file header
+///
+///
+/// ASTC block and decoded image dimensions in texels.
+///
+/// For 2D images the Z dimension must be set to 1.
+///
+/// Note that the image is not required to be an exact multiple of the compressed block
+/// size; the compressed data may include padding that is discarded during decompression.
+///
+internal readonly record struct AstcFileHeader(byte BlockWidth, byte BlockHeight, byte BlockDepth, int ImageWidth, int ImageHeight, int ImageDepth)
+{
+ public const uint Magic = 0x5CA1AB13;
+ public const int SizeInBytes = 16;
+
+ public static AstcFileHeader FromMemory(Span data)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(data.Length, SizeInBytes);
+
+ // ASTC header is 16 bytes:
+ // - magic (4),
+ // - blockdim (3),
+ // - xsize,y,z (each 3 little-endian bytes)
+ uint magic = BitConverter.ToUInt32(data);
+ ArgumentOutOfRangeException.ThrowIfNotEqual(magic, Magic);
+
+ return new AstcFileHeader(
+ BlockWidth: data[4],
+ BlockHeight: data[5],
+ BlockDepth: data[6],
+ ImageWidth: data[7] | (data[8] << 8) | (data[9] << 16),
+ ImageHeight: data[10] | (data[11] << 8) | (data[12] << 16),
+ ImageDepth: data[13] | (data[14] << 8) | (data[15] << 16));
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/IO/BitStream.cs b/src/ImageSharp.Textures.Astc/IO/BitStream.cs
new file mode 100644
index 00000000..e111b1eb
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/IO/BitStream.cs
@@ -0,0 +1,188 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Globalization;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.IO;
+
+///
+/// A simple bit stream used for reading/writing arbitrary-sized chunks.
+///
+internal struct BitStream
+{
+ private ulong low;
+ private ulong high;
+ private uint dataSize; // number of valid bits in the 128-bit buffer
+
+ public BitStream(ulong data = 0, uint dataSize = 0)
+ {
+ this.low = data;
+ this.high = 0;
+ this.dataSize = dataSize;
+ }
+
+ public BitStream(UInt128 data, uint dataSize)
+ {
+ this.low = data.Low();
+ this.high = data.High();
+ this.dataSize = dataSize;
+ }
+
+ public readonly uint Bits => this.dataSize;
+
+ public void PutBits(T x, int size)
+ where T : unmanaged
+ {
+ ulong value = x switch
+ {
+ uint ui => ui,
+ ulong ul => ul,
+ ushort us => us,
+ byte b => b,
+ _ => Convert.ToUInt64(x, CultureInfo.InvariantCulture)
+ };
+
+ if (this.dataSize + (uint)size > 128)
+ {
+ throw new InvalidOperationException("Not enough space in BitStream");
+ }
+
+ if (this.dataSize < 64)
+ {
+ int lowFree = (int)(64 - this.dataSize);
+ if (size <= lowFree)
+ {
+ this.low |= (value & MaskFor(size)) << (int)this.dataSize;
+ }
+ else
+ {
+ this.low |= (value & MaskFor(lowFree)) << (int)this.dataSize;
+ this.high |= (value >> lowFree) & MaskFor(size - lowFree);
+ }
+ }
+ else
+ {
+ int shift = (int)(this.dataSize - 64);
+ this.high |= (value & MaskFor(size)) << shift;
+ }
+
+ this.dataSize += (uint)size;
+ }
+
+ ///
+ /// Attempt to retrieve the specified number of bits from the buffer.
+ /// The buffer is shifted accordingly if successful.
+ ///
+ public bool TryGetBits(int count, out T bits)
+ where T : unmanaged
+ {
+ T? result = null;
+
+ if (typeof(T) == typeof(UInt128))
+ {
+ result = (T?)(object?)this.GetBitsUInt128(count);
+ }
+ else if (count <= this.dataSize)
+ {
+ ulong value = count switch
+ {
+ 0 => 0,
+ <= 64 => this.low & MaskFor(count),
+ _ => this.low
+ };
+
+ this.ShiftBuffer(count);
+ object boxed = Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
+ result = (T)boxed;
+ }
+
+ bits = result ?? default;
+
+ return result is not null;
+ }
+
+ public bool TryGetBits(int count, out ulong bits)
+ {
+ if (count > this.dataSize)
+ {
+ bits = 0;
+ return false;
+ }
+
+ bits = count switch
+ {
+ 0 => 0,
+ <= 64 => this.low & MaskFor(count),
+ _ => this.low
+ };
+ this.ShiftBuffer(count);
+ return true;
+ }
+
+ public bool TryGetBits(int count, out uint bits)
+ {
+ if (count > this.dataSize)
+ {
+ bits = 0;
+ return false;
+ }
+
+ bits = (uint)(count switch
+ {
+ 0 => 0UL,
+ <= 64 => this.low & MaskFor(count),
+ _ => this.low
+ });
+ this.ShiftBuffer(count);
+ return true;
+ }
+
+ private static ulong MaskFor(int bits)
+ => bits == 64
+ ? ~0UL
+ : ((1UL << bits) - 1UL);
+
+ private UInt128? GetBitsUInt128(int count)
+ {
+ if (count > this.dataSize)
+ {
+ return null;
+ }
+
+ UInt128 result = count switch
+ {
+ 0 => UInt128.Zero,
+ <= 64 => (UInt128)(this.low & MaskFor(count)),
+ 128 => new UInt128(this.high, this.low),
+ _ => new UInt128(
+ (count - 64 == 64) ? this.high : (this.high & MaskFor(count - 64)),
+ this.low)
+ };
+
+ this.ShiftBuffer(count);
+
+ return result;
+ }
+
+ private void ShiftBuffer(int count)
+ {
+ if (count < 64)
+ {
+ this.low = (this.low >> count) | (this.high << (64 - count));
+ this.high >>= count;
+ }
+ else if (count == 64)
+ {
+ this.low = this.high;
+ this.high = 0;
+ }
+ else
+ {
+ this.low = this.high >> (count - 64);
+ this.high = 0;
+ }
+
+ this.dataSize -= (uint)count;
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/ImageSharp.Textures.Astc.csproj b/src/ImageSharp.Textures.Astc/ImageSharp.Textures.Astc.csproj
new file mode 100644
index 00000000..a4d3f596
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/ImageSharp.Textures.Astc.csproj
@@ -0,0 +1,33 @@
+
+
+
+
+ SixLabors.ImageSharp.Textures.Astc
+ SixLabors.ImageSharp.Textures.Astc
+
+
+
+
+ enable
+ Nullable
+
+
+
+
+
+ net8.0;net9.0
+
+
+
+
+ net8.0
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ImageSharp.Textures.Astc/TexelBlock/BlockInfo.cs b/src/ImageSharp.Textures.Astc/TexelBlock/BlockInfo.cs
new file mode 100644
index 00000000..61e847f5
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/TexelBlock/BlockInfo.cs
@@ -0,0 +1,363 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Textures.Astc.BiseEncoding;
+using SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.TexelBlock;
+
+///
+/// Fused block info computed in a single pass from raw ASTC block bits
+///
+internal struct BlockInfo
+{
+ private static readonly int[] WeightRanges =
+ [-1, -1, 1, 2, 3, 4, 5, 7, -1, -1, 9, 11, 15, 19, 23, 31];
+
+ private static readonly int[] ExtraCemBitsForPartition = [0, 2, 5, 8];
+
+ // Valid BISE endpoint ranges in descending order (only these produce valid encodings)
+ private static readonly int[] ValidEndpointRanges =
+ [255, 191, 159, 127, 95, 79, 63, 47, 39, 31, 23, 19, 15, 11, 9, 7, 5];
+
+ public bool IsValid;
+ public bool IsVoidExtent;
+
+ // Weight grid
+ public int GridWidth;
+ public int GridHeight;
+ public int WeightRange;
+ public int WeightBitCount;
+
+ // Partitions
+ public int PartitionCount;
+
+ // Dual plane
+ public bool IsDualPlane;
+ public int DualPlaneChannel; // only valid if IsDualPlane
+
+ // Color endpoints
+ public int ColorStartBit;
+ public int ColorBitCount;
+ public int ColorValuesRange;
+ public int ColorValuesCount;
+
+ // Endpoint modes (up to 4 partitions)
+ public ColorEndpointMode EndpointMode0;
+ public ColorEndpointMode EndpointMode1;
+ public ColorEndpointMode EndpointMode2;
+ public ColorEndpointMode EndpointMode3;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly ColorEndpointMode GetEndpointMode(int partition) => partition switch
+ {
+ 0 => this.EndpointMode0,
+ 1 => this.EndpointMode1,
+ 2 => this.EndpointMode2,
+ 3 => this.EndpointMode3,
+ _ => this.EndpointMode0
+ };
+
+ ///
+ /// Decode all block info from raw 128-bit ASTC block data in a single pass.
+ /// Returns a BlockInfo with IsValid=false if the block is illegal or reserved.
+ /// Returns a BlockInfo with IsVoidExtent=true for void extent blocks.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ public static BlockInfo Decode(UInt128 bits)
+ {
+ ulong lowBits = bits.Low();
+
+ // ---- Step 1: Check void extent ----
+ // Void extent: bits[0:9] == 0x1FC (9 bits)
+ if ((lowBits & 0x1FF) == 0x1FC)
+ {
+ return new BlockInfo
+ {
+ IsVoidExtent = true,
+ IsValid = !CheckVoidExtentIsIllegal(bits, lowBits)
+ };
+ }
+
+ // ---- Step 2: Decode block mode, grid dims, weight range in ONE pass ----
+ // This inlines DecodeBlockMode + DecodeWeightProperties
+ int gridWidth, gridHeight;
+ bool isWidthA6HeightB6 = false;
+ uint rBits; // 3-bit range index component
+
+ // bits[0:2] != 0
+ if ((lowBits & 0x3) != 0)
+ {
+ ulong modeBits = (lowBits >> 2) & 0x3; // bits[2:4]
+ int a = (int)((lowBits >> 5) & 0x3); // bits[5:7]
+
+ (gridWidth, gridHeight) = modeBits switch
+ {
+ 0 => ((int)((lowBits >> 7) & 0x3) + 4, a + 2),
+ 1 => ((int)((lowBits >> 7) & 0x3) + 8, a + 2),
+ 2 => (a + 2, (int)((lowBits >> 7) & 0x3) + 8),
+ 3 when ((lowBits >> 8) & 1) != 0 => ((int)((lowBits >> 7) & 0x1) + 2, a + 2),
+ 3 => (a + 2, (int)((lowBits >> 7) & 0x1) + 6),
+ _ => default // unreachable
+ };
+
+ // Range r[2:0] = {bit4, bit1, bit0} for these modes
+ rBits = (uint)(((lowBits >> 4) & 1) | (((lowBits >> 0) & 0x3) << 1));
+ }
+ else
+ {
+ // bits[0:2] == 0
+ ulong modeBits = (lowBits >> 5) & 0xF; // bits[5:9]
+ int a = (int)((lowBits >> 5) & 0x3); // bits[5:7]
+
+ switch (modeBits)
+ {
+ case var _ when (modeBits & 0xC) == 0x0:
+ if ((lowBits & 0xF) == 0)
+ {
+ return default; // reserved block mode
+ }
+
+ gridWidth = 12;
+ gridHeight = a + 2;
+ break;
+ case var _ when (modeBits & 0xC) == 0x4:
+ gridWidth = a + 2;
+ gridHeight = 12;
+ break;
+ case 0xC:
+ gridWidth = 6;
+ gridHeight = 10;
+ break;
+ case 0xD:
+ gridWidth = 10;
+ gridHeight = 6;
+ break;
+ case var _ when (modeBits & 0xC) == 0x8:
+ gridWidth = a + 6;
+ gridHeight = (int)((lowBits >> 9) & 0x3) + 6;
+ isWidthA6HeightB6 = true;
+ break;
+ default:
+ return default; // reserved
+ }
+
+ // Range r[2:0] = {bit4, bit3, bit2} for these modes
+ rBits = (uint)(((lowBits >> 4) & 1) | (((lowBits >> 2) & 0x3) << 1));
+ }
+
+ // ---- Step 3: Compute weight range from r and h bits ----
+ uint hBit = isWidthA6HeightB6
+ ? 0u
+ : (uint)((lowBits >> 9) & 1);
+ int rangeIdx = (int)((hBit << 3) | rBits);
+ if ((uint)rangeIdx >= (uint)WeightRanges.Length)
+ {
+ return default;
+ }
+
+ int weightRange = WeightRanges[rangeIdx];
+ if (weightRange < 0)
+ {
+ return default;
+ }
+
+ // ---- Step 4: Dual plane ----
+ // WidthA6HeightB6 mode never has dual plane; otherwise check bit 10
+ bool isDualPlane = !isWidthA6HeightB6 && ((lowBits >> 10) & 1) != 0;
+
+ // ---- Step 5: Partition count ----
+ int partitionCount = 1 + (int)((lowBits >> 11) & 0x3);
+
+ // ---- Step 6: Validate weight count ----
+ int numWeights = gridWidth * gridHeight;
+ if (isDualPlane)
+ {
+ numWeights *= 2;
+ }
+
+ if (numWeights > 64)
+ {
+ return default;
+ }
+
+ // 4 partitions + dual plane is illegal
+ if (partitionCount == 4 && isDualPlane)
+ {
+ return default;
+ }
+
+ // ---- Step 7: Weight bit count ----
+ int weightBitCount = BoundedIntegerSequenceCodec.GetBitCountForRange(numWeights, weightRange);
+ if (weightBitCount is < 24 or > 96)
+ {
+ return default;
+ }
+
+ // ---- Step 8: Endpoint modes + extra CEM bits ----
+ ColorEndpointMode cem0 = default, cem1 = default, cem2 = default, cem3 = default;
+ int colorValuesCount = 0;
+ int numExtraCEMBits = 0;
+
+ if (partitionCount == 1)
+ {
+ cem0 = (ColorEndpointMode)((lowBits >> 13) & 0xF);
+ colorValuesCount = (((int)cem0 / 4) + 1) * 2;
+ }
+ else
+ {
+ // Multi-partition CEM decode
+ ulong sharedCemMarker = (lowBits >> 23) & 0x3;
+
+ if (sharedCemMarker == 0)
+ {
+ // Shared CEM: all partitions use the same mode
+ ColorEndpointMode sharedCem = (ColorEndpointMode)((lowBits >> 25) & 0xF);
+ cem0 = cem1 = cem2 = cem3 = sharedCem;
+ for (int i = 0; i < partitionCount; i++)
+ {
+ colorValuesCount += sharedCem.GetColorValuesCount();
+ }
+ }
+ else
+ {
+ // Non-shared CEM: per-partition modes
+ numExtraCEMBits = ExtraCemBitsForPartition[partitionCount - 1];
+
+ int extraCemStartPos = 128 - numExtraCEMBits - weightBitCount;
+ UInt128 extraCem = BitOperations.GetBits(bits, extraCemStartPos, numExtraCEMBits);
+
+ ulong cemval = (lowBits >> 23) & 0x3F; // 6 bits starting at bit 23
+ int baseCem = (int)(((cemval & 0x3) - 1) * 4);
+ cemval >>= 2;
+
+ ulong combined = cemval | (extraCem.Low() << 4);
+ ulong cembits = combined;
+
+ // Extract c bits (1 bit per partition)
+ Span c = stackalloc int[4];
+ for (int i = 0; i < partitionCount; i++)
+ {
+ c[i] = (int)(cembits & 0x1);
+ cembits >>= 1;
+ }
+
+ // Extract m bits (2 bits per partition)
+ for (int i = 0; i < partitionCount; i++)
+ {
+ int m = (int)(cembits & 0x3);
+ cembits >>= 2;
+ ColorEndpointMode mode = (ColorEndpointMode)(baseCem + (4 * c[i]) + m);
+ switch (i)
+ {
+ case 0:
+ cem0 = mode;
+ break;
+ case 1:
+ cem1 = mode;
+ break;
+ case 2:
+ cem2 = mode;
+ break;
+ case 3:
+ cem3 = mode;
+ break;
+ }
+
+ colorValuesCount += mode.GetColorValuesCount();
+ }
+ }
+ }
+
+ if (colorValuesCount > 18)
+ {
+ return default;
+ }
+
+ // ---- Step 9: Dual plane start position and channel ----
+ int dualPlaneBitStartPos = 128 - weightBitCount - numExtraCEMBits;
+ if (isDualPlane)
+ {
+ dualPlaneBitStartPos -= 2;
+ }
+
+ int dualPlaneChannel = isDualPlane
+ ? (int)BitOperations.GetBits(bits, dualPlaneBitStartPos, 2).Low()
+ : -1;
+
+ // ---- Step 10: Color values info ----
+ int colorStartBit = (partitionCount == 1) ? 17 : 29;
+ int maxColorBits = dualPlaneBitStartPos - colorStartBit;
+
+ // Minimum bits needed check
+ int requiredColorBits = ((13 * colorValuesCount) + 4) / 5;
+ if (maxColorBits < requiredColorBits)
+ {
+ return default;
+ }
+
+ // Find max color range that fits (only check valid BISE ranges: 17 vs up to 255)
+ int colorValuesRange = 0, colorBitCount = 0;
+ foreach (int rv in ValidEndpointRanges)
+ {
+ int bitCount = BoundedIntegerSequenceCodec.GetBitCountForRange(colorValuesCount, rv);
+ if (bitCount <= maxColorBits)
+ {
+ colorValuesRange = rv;
+ colorBitCount = bitCount;
+ break;
+ }
+ }
+
+ if (colorValuesRange == 0)
+ {
+ return default;
+ }
+
+ // ---- Step 11: Validate endpoint modes are not HDR for batchable checks ----
+ // (HDR blocks are still valid, just flagged for downstream use)
+ return new BlockInfo
+ {
+ IsValid = true,
+ IsVoidExtent = false,
+ GridWidth = gridWidth,
+ GridHeight = gridHeight,
+ WeightRange = weightRange,
+ WeightBitCount = weightBitCount,
+ PartitionCount = partitionCount,
+ IsDualPlane = isDualPlane,
+ DualPlaneChannel = dualPlaneChannel,
+ ColorStartBit = colorStartBit,
+ ColorBitCount = colorBitCount,
+ ColorValuesRange = colorValuesRange,
+ ColorValuesCount = colorValuesCount,
+ EndpointMode0 = cem0,
+ EndpointMode1 = cem1,
+ EndpointMode2 = cem2,
+ EndpointMode3 = cem3,
+ };
+ }
+
+ ///
+ /// Inline void extent validation (replaces PhysicalBlock.CheckVoidExtentIsIllegal).
+ ///
+ private static bool CheckVoidExtentIsIllegal(UInt128 bits, ulong lowBits)
+ {
+ if (BitOperations.GetBits(bits, 10, 2).Low() != 0x3UL)
+ {
+ return true;
+ }
+
+ int c0 = (int)BitOperations.GetBits(lowBits, 12, 13);
+ int c1 = (int)BitOperations.GetBits(lowBits, 25, 13);
+ int c2 = (int)BitOperations.GetBits(lowBits, 38, 13);
+ int c3 = (int)BitOperations.GetBits(lowBits, 51, 13);
+
+ const int all1s = (1 << 13) - 1;
+ bool coordsAll1s = c0 == all1s && c1 == all1s && c2 == all1s && c3 == all1s;
+
+ return !coordsAll1s && (c0 >= c1 || c2 >= c3);
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/TexelBlock/IntermediateBlock.cs b/src/ImageSharp.Textures.Astc/TexelBlock/IntermediateBlock.cs
new file mode 100644
index 00000000..086aa7ed
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/TexelBlock/IntermediateBlock.cs
@@ -0,0 +1,284 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.BiseEncoding;
+using SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+using SixLabors.ImageSharp.Textures.Astc.IO;
+
+namespace SixLabors.ImageSharp.Textures.Astc.TexelBlock;
+
+internal static class IntermediateBlock
+{
+ // From Table C.2.7 -- valid weight ranges
+ public static readonly int[] ValidWeightRanges = [1, 2, 3, 4, 5, 7, 9, 11, 15, 19, 23, 31];
+
+ // Returns the maximum endpoint value range or negative on error
+ private const int EndpointRangeInvalidWeightDimensions = -1;
+ private const int EndpointRangeNotEnoughColorBits = -2;
+
+ public static IntermediateBlockData? UnpackIntermediateBlock(PhysicalBlock physicalBlock)
+ {
+ if (physicalBlock.IsIllegalEncoding || physicalBlock.IsVoidExtent)
+ {
+ return null;
+ }
+
+ BlockInfo info = BlockInfo.Decode(physicalBlock.BlockBits);
+ if (!info.IsValid || info.IsVoidExtent)
+ {
+ return null;
+ }
+
+ return UnpackIntermediateBlock(physicalBlock.BlockBits, in info);
+ }
+
+ ///
+ /// Fast overload that uses pre-computed BlockInfo instead of calling PhysicalBlock getters.
+ ///
+ public static IntermediateBlockData? UnpackIntermediateBlock(UInt128 bits, in BlockInfo info)
+ {
+ if (!info.IsValid || info.IsVoidExtent)
+ {
+ return null;
+ }
+
+ IntermediateBlockData data = default;
+
+ // Use cached values from BlockInfo instead of PhysicalBlock getters
+ UInt128 colorBitMask = UInt128Extensions.OnesMask(info.ColorBitCount);
+ UInt128 colorBits = (bits >> info.ColorStartBit) & colorBitMask;
+ BitStream colorBitStream = new(colorBits, 128);
+
+ BoundedIntegerSequenceDecoder colorDecoder = BoundedIntegerSequenceDecoder.GetCached(info.ColorValuesRange);
+ Span colors = stackalloc int[info.ColorValuesCount];
+ colorDecoder.Decode(info.ColorValuesCount, ref colorBitStream, colors);
+
+ data.WeightGridX = info.GridWidth;
+ data.WeightGridY = info.GridHeight;
+ data.WeightRange = info.WeightRange;
+
+ data.PartitionId = info.PartitionCount > 1
+ ? (int)BitOperations.GetBits(bits.Low(), 13, 10)
+ : null;
+
+ data.DualPlaneChannel = info.IsDualPlane
+ ? info.DualPlaneChannel
+ : null;
+
+ int colorIndex = 0;
+ data.EndpointCount = info.PartitionCount;
+ for (int i = 0; i < info.PartitionCount; ++i)
+ {
+ ColorEndpointMode mode = info.GetEndpointMode(i);
+ int colorCount = mode.GetColorValuesCount();
+ IntermediateEndpointData ep = new()
+ { Mode = mode, ColorCount = colorCount };
+ for (int j = 0; j < colorCount; ++j)
+ {
+ ep.Colors[j] = colors[colorIndex++];
+ }
+
+ data.Endpoints[i] = ep;
+ }
+
+ data.EndpointRange = info.ColorValuesRange;
+
+ UInt128 weightBits = UInt128Extensions.ReverseBits(bits) & UInt128Extensions.OnesMask(info.WeightBitCount);
+ BitStream weightBitStream = new(weightBits, 128);
+
+ BoundedIntegerSequenceDecoder weightDecoder = BoundedIntegerSequenceDecoder.GetCached(data.WeightRange);
+ int weightsCount = data.WeightGridX * data.WeightGridY;
+ if (info.IsDualPlane)
+ {
+ weightsCount *= 2;
+ }
+
+ data.Weights = new int[weightsCount];
+ data.WeightsCount = weightsCount;
+ weightDecoder.Decode(weightsCount, ref weightBitStream, data.Weights);
+
+ return data;
+ }
+
+ public static int EndpointRangeForBlock(in IntermediateBlockData data)
+ {
+ int dualPlaneMultiplier = data.DualPlaneChannel.HasValue
+ ? 2
+ : 1;
+ if (BoundedIntegerSequenceCodec.GetBitCountForRange(data.WeightGridX * data.WeightGridY * dualPlaneMultiplier, data.WeightRange) > 96)
+ {
+ return EndpointRangeInvalidWeightDimensions;
+ }
+
+ int partitionCount = data.EndpointCount;
+ int bitsWrittenCount = 11 + 2
+ + ((partitionCount > 1) ? 10 : 0)
+ + ((partitionCount == 1) ? 4 : 6);
+ int availableColorBitsCount = ExtraConfigBitPosition(data) - bitsWrittenCount;
+
+ int colorValuesCount = 0;
+ for (int i = 0; i < data.EndpointCount; i++)
+ {
+ colorValuesCount += data.Endpoints[i].Mode.GetColorValuesCount();
+ }
+
+ int bitsNeededCount = ((13 * colorValuesCount) + 4) / 5;
+ if (availableColorBitsCount < bitsNeededCount)
+ {
+ return EndpointRangeNotEnoughColorBits;
+ }
+
+ int colorValueRange = byte.MaxValue;
+ for (; colorValueRange > 1; --colorValueRange)
+ {
+ int bitCountForRange = BoundedIntegerSequenceCodec.GetBitCountForRange(colorValuesCount, colorValueRange);
+ if (bitCountForRange <= availableColorBitsCount)
+ {
+ break;
+ }
+ }
+
+ return colorValueRange;
+ }
+
+ public static VoidExtentData? UnpackVoidExtent(PhysicalBlock physicalBlock)
+ {
+ int? colorStartBit = physicalBlock.GetColorStartBit();
+ int? colorBitCount = physicalBlock.GetColorBitCount();
+ if (physicalBlock.IsIllegalEncoding || !physicalBlock.IsVoidExtent || colorStartBit is null || colorBitCount is null)
+ {
+ return null;
+ }
+
+ UInt128 colorBits = (physicalBlock.BlockBits >> colorStartBit.Value) & UInt128Extensions.OnesMask(colorBitCount.Value);
+
+ // We expect low 64 bits contain the 4x16-bit channels
+ ulong low = colorBits.Low();
+
+ VoidExtentData data = default;
+
+ // Bit 9 of the block mode indicates HDR (1) vs LDR (0) void extent
+ data.IsHdr = (physicalBlock.BlockBits.Low() & (1UL << 9)) != 0;
+ data.R = (ushort)((low >> 0) & 0xFFFF);
+ data.G = (ushort)((low >> 16) & 0xFFFF);
+ data.B = (ushort)((low >> 32) & 0xFFFF);
+ data.A = (ushort)((low >> 48) & 0xFFFF);
+
+ int[]? coords = physicalBlock.GetVoidExtentCoordinates();
+ data.Coords = new ushort[4];
+ if (coords != null)
+ {
+ data.Coords[0] = (ushort)coords[0];
+ data.Coords[1] = (ushort)coords[1];
+ data.Coords[2] = (ushort)coords[2];
+ data.Coords[3] = (ushort)coords[3];
+ }
+ else
+ {
+ ushort allOnes = (1 << 13) - 1;
+ for (int i = 0; i < 4; ++i)
+ {
+ data.Coords[i] = allOnes;
+ }
+ }
+
+ return data;
+ }
+
+ ///
+ /// Determines if all endpoint modes in the intermediate block data are the same
+ ///
+ internal static bool SharedEndpointModes(in IntermediateBlockData data)
+ {
+ if (data.EndpointCount == 0)
+ {
+ return true;
+ }
+
+ ColorEndpointMode first = data.Endpoints[0].Mode;
+ for (int i = 1; i < data.EndpointCount; i++)
+ {
+ if (data.Endpoints[i].Mode != first)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ internal static int ExtraConfigBitPosition(in IntermediateBlockData data)
+ {
+ bool hasDualChannel = data.DualPlaneChannel.HasValue;
+ int weightCount = data.WeightGridX * data.WeightGridY * (hasDualChannel ? 2 : 1);
+ int weightBitCount = BoundedIntegerSequenceCodec.GetBitCountForRange(weightCount, data.WeightRange);
+
+ int extraConfigBitCount = 0;
+ if (!SharedEndpointModes(data))
+ {
+ int encodedCemBitCount = 2 + (data.EndpointCount * 3);
+ extraConfigBitCount = encodedCemBitCount - 6;
+ }
+
+ if (hasDualChannel)
+ {
+ extraConfigBitCount += 2;
+ }
+
+ return 128 - weightBitCount - extraConfigBitCount;
+ }
+
+ internal struct VoidExtentData
+ {
+ public bool IsHdr;
+ public ushort R;
+ public ushort G;
+ public ushort B;
+ public ushort A;
+ public ushort[] Coords; // length 4
+ }
+
+ [System.Runtime.CompilerServices.InlineArray(MaxColorValues)]
+ internal struct EndpointColorValues
+ {
+ public const int MaxColorValues = 8;
+#pragma warning disable CS0169, S1144 // Accessed by runtime via [InlineArray]
+ private int element0;
+#pragma warning restore CS0169, S1144
+ }
+
+ internal struct IntermediateBlockData
+ {
+ public int WeightGridX;
+ public int WeightGridY;
+ public int WeightRange;
+
+ public int[] Weights;
+ public int WeightsCount;
+
+ public int? PartitionId;
+ public int? DualPlaneChannel;
+
+ public IntermediateEndpointBuffer Endpoints;
+ public int EndpointCount;
+
+ public int? EndpointRange;
+ }
+
+ internal struct IntermediateEndpointData
+ {
+ public ColorEndpointMode Mode;
+ public EndpointColorValues Colors;
+ public int ColorCount;
+ }
+
+ [System.Runtime.CompilerServices.InlineArray(MaxPartitions)]
+ internal struct IntermediateEndpointBuffer
+ {
+ public const int MaxPartitions = 4;
+#pragma warning disable CS0169, S1144 // Accessed by runtime via [InlineArray]
+ private IntermediateEndpointData element0;
+#pragma warning restore CS0169, S1144
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/TexelBlock/IntermediateBlockPacker.cs b/src/ImageSharp.Textures.Astc/TexelBlock/IntermediateBlockPacker.cs
new file mode 100644
index 00000000..df157e29
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/TexelBlock/IntermediateBlockPacker.cs
@@ -0,0 +1,403 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.BiseEncoding;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+using SixLabors.ImageSharp.Textures.Astc.IO;
+
+namespace SixLabors.ImageSharp.Textures.Astc.TexelBlock;
+
+internal static class IntermediateBlockPacker
+{
+ private static readonly BlockModeInfo[] BlockModeInfoTable = [
+ new BlockModeInfo { MinWeightGridDimX = 4, MaxWeightGridDimX = 7, MinWeightGridDimY = 2, MaxWeightGridDimY = 5, R0BitPos = 4, R1BitPos = 0, R2BitPos = 1, WeightGridXOffsetBitPos = 7, WeightGridYOffsetBitPos = 5, RequireSinglePlaneLowPrec = false },
+ new BlockModeInfo { MinWeightGridDimX = 8, MaxWeightGridDimX = 11, MinWeightGridDimY = 2, MaxWeightGridDimY = 5, R0BitPos = 4, R1BitPos = 0, R2BitPos = 1, WeightGridXOffsetBitPos = 7, WeightGridYOffsetBitPos = 5, RequireSinglePlaneLowPrec = false },
+ new BlockModeInfo { MinWeightGridDimX = 2, MaxWeightGridDimX = 5, MinWeightGridDimY = 8, MaxWeightGridDimY = 11, R0BitPos = 4, R1BitPos = 0, R2BitPos = 1, WeightGridXOffsetBitPos = 5, WeightGridYOffsetBitPos = 7, RequireSinglePlaneLowPrec = false },
+ new BlockModeInfo { MinWeightGridDimX = 2, MaxWeightGridDimX = 5, MinWeightGridDimY = 6, MaxWeightGridDimY = 7, R0BitPos = 4, R1BitPos = 0, R2BitPos = 1, WeightGridXOffsetBitPos = 5, WeightGridYOffsetBitPos = 7, RequireSinglePlaneLowPrec = false },
+ new BlockModeInfo { MinWeightGridDimX = 2, MaxWeightGridDimX = 3, MinWeightGridDimY = 2, MaxWeightGridDimY = 5, R0BitPos = 4, R1BitPos = 0, R2BitPos = 1, WeightGridXOffsetBitPos = 7, WeightGridYOffsetBitPos = 5, RequireSinglePlaneLowPrec = false },
+ new BlockModeInfo { MinWeightGridDimX = 12, MaxWeightGridDimX = 12, MinWeightGridDimY = 2, MaxWeightGridDimY = 5, R0BitPos = 4, R1BitPos = 2, R2BitPos = 3, WeightGridXOffsetBitPos = -1, WeightGridYOffsetBitPos = 5, RequireSinglePlaneLowPrec = false },
+ new BlockModeInfo { MinWeightGridDimX = 2, MaxWeightGridDimX = 5, MinWeightGridDimY = 12, MaxWeightGridDimY = 12, R0BitPos = 4, R1BitPos = 2, R2BitPos = 3, WeightGridXOffsetBitPos = 5, WeightGridYOffsetBitPos = -1, RequireSinglePlaneLowPrec = false },
+ new BlockModeInfo { MinWeightGridDimX = 6, MaxWeightGridDimX = 6, MinWeightGridDimY = 10, MaxWeightGridDimY = 10, R0BitPos = 4, R1BitPos = 2, R2BitPos = 3, WeightGridXOffsetBitPos = -1, WeightGridYOffsetBitPos = -1, RequireSinglePlaneLowPrec = false },
+ new BlockModeInfo { MinWeightGridDimX = 10, MaxWeightGridDimX = 10, MinWeightGridDimY = 6, MaxWeightGridDimY = 6, R0BitPos = 4, R1BitPos = 2, R2BitPos = 3, WeightGridXOffsetBitPos = -1, WeightGridYOffsetBitPos = -1, RequireSinglePlaneLowPrec = false },
+ new BlockModeInfo { MinWeightGridDimX = 6, MaxWeightGridDimX = 9, MinWeightGridDimY = 6, MaxWeightGridDimY = 9, R0BitPos = 4, R1BitPos = 2, R2BitPos = 3, WeightGridXOffsetBitPos = 5, WeightGridYOffsetBitPos = 9, RequireSinglePlaneLowPrec = true }
+ ];
+
+ private static readonly uint[] BlockModeMasks = [0x0u, 0x4u, 0x8u, 0xCu, 0x10Cu, 0x0u, 0x80u, 0x180u, 0x1A0u, 0x100u];
+
+ public static (string? Error, UInt128 PhysicalBlockBits) Pack(in IntermediateBlock.IntermediateBlockData data)
+ {
+ UInt128 physicalBlockBits = 0;
+ int expectedWeightsCount = data.WeightGridX * data.WeightGridY
+ * (data.DualPlaneChannel.HasValue ? 2 : 1);
+ int actualWeightsCount = data.WeightsCount > 0
+ ? data.WeightsCount
+ : (data.Weights?.Length ?? 0);
+ if (actualWeightsCount != expectedWeightsCount)
+ {
+ return ("Incorrect number of weights!", 0);
+ }
+
+ BitStream bitSink = new(0UL, 0);
+
+ // First we need to encode the block mode.
+ string? errorMessage = PackBlockMode(data.WeightGridX, data.WeightGridY, data.WeightRange, data.DualPlaneChannel.HasValue, ref bitSink);
+ if (errorMessage != null)
+ {
+ return (errorMessage, 0);
+ }
+
+ // number of partitions minus one
+ int partitionCount = data.EndpointCount;
+ bitSink.PutBits((uint)(partitionCount - 1), 2);
+
+ if (partitionCount > 1)
+ {
+ int id = data.PartitionId ?? 0;
+ ArgumentOutOfRangeException.ThrowIfLessThan(id, 0);
+ bitSink.PutBits((uint)id, 10);
+ }
+
+ (BitStream weightSink, int weightBitsCount) = EncodeWeights(data);
+
+ (string? error, int extraConfig) = EncodeColorEndpointModes(data, partitionCount, ref bitSink);
+ if (error != null)
+ {
+ return (error, 0);
+ }
+
+ int colorValueRange = data.EndpointRange ?? IntermediateBlock.EndpointRangeForBlock(data);
+ if (colorValueRange == -1)
+ {
+ throw new InvalidOperationException($"{nameof(colorValueRange)} must not be EndpointRangeInvalidWeightDimensions");
+ }
+
+ if (colorValueRange == -2)
+ {
+ return ("Intermediate block emits illegal color range", 0);
+ }
+
+ BoundedIntegerSequenceEncoder colorEncoder = new(colorValueRange);
+ for (int i = 0; i < data.EndpointCount; i++)
+ {
+ IntermediateBlock.IntermediateEndpointData ep = data.Endpoints[i];
+ for (int j = 0; j < ep.ColorCount; j++)
+ {
+ int color = ep.Colors[j];
+ if (color > colorValueRange)
+ {
+ return ("Color outside available color range!", 0);
+ }
+
+ colorEncoder.AddValue(color);
+ }
+ }
+
+ colorEncoder.Encode(ref bitSink);
+
+ int extraConfigBitPosition = IntermediateBlock.ExtraConfigBitPosition(data);
+ int extraConfigBits = 128 - weightBitsCount - extraConfigBitPosition;
+
+ ArgumentOutOfRangeException.ThrowIfNegative(extraConfigBits);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(extraConfig, 1 << extraConfigBits);
+
+ int bitsToSkip = extraConfigBitPosition - (int)bitSink.Bits;
+ ArgumentOutOfRangeException.ThrowIfNegative(bitsToSkip);
+ while (bitsToSkip > 0)
+ {
+ int skipping = Math.Min(32, bitsToSkip);
+ bitSink.PutBits(0u, skipping);
+ bitsToSkip -= skipping;
+ }
+
+ if (extraConfigBits > 0)
+ {
+ bitSink.PutBits((uint)extraConfig, extraConfigBits);
+ }
+
+ ArgumentOutOfRangeException.ThrowIfNotEqual(bitSink.Bits, 128u - weightBitsCount);
+
+ // Flush out the bit writer
+ if (!bitSink.TryGetBits(128 - weightBitsCount, out UInt128 astcBits))
+ {
+ throw new InvalidOperationException();
+ }
+
+ if (!weightSink.TryGetBits(weightBitsCount, out UInt128 revWeightBits))
+ {
+ throw new InvalidOperationException();
+ }
+
+ UInt128 combined = astcBits | UInt128Extensions.ReverseBits(revWeightBits);
+ physicalBlockBits = combined;
+
+ PhysicalBlock block = PhysicalBlock.Create(physicalBlockBits);
+ string? illegal = block.IdentifyInvalidEncodingIssues();
+
+ return (illegal, physicalBlockBits);
+ }
+
+ public static (string? Error, UInt128 PhysicalBlockBits) Pack(IntermediateBlock.VoidExtentData data)
+ {
+ // Pack void extent
+ // Assemble the 128-bit value explicitly: low 64 bits = RGBA (4x16)
+ // high 64 bits = 12-bit header (0xDFC) followed by four 13-bit coords.
+ ulong high64 = ((ulong)data.A << 48) | ((ulong)data.B << 32) | ((ulong)data.G << 16) | data.R;
+ ulong low64 = 0UL;
+
+ // Header occupies lowest 12 bits of the high word
+ low64 |= 0xDFCu;
+ for (int i = 0; i < 4; ++i)
+ {
+ low64 |= ((ulong)(data.Coords[i] & 0x1FFF)) << (12 + (13 * i));
+ }
+
+ UInt128 physicalBlockBits;
+
+ // Decide representation: if the RGBA low word is zero we emit the
+ // compact single-ulong representation (low word = header+coords,
+ // high word = 0) to match the reference tests. Otherwise the
+ // low word holds RGBA and the high word holds header+coords.
+ if (high64 == 0UL)
+ {
+ physicalBlockBits = (UInt128)low64;
+
+ // using compact void extent representation
+ }
+ else
+ {
+ physicalBlockBits = new UInt128(high64, low64);
+
+ // using full void extent representation
+ }
+
+ PhysicalBlock block = PhysicalBlock.Create(physicalBlockBits);
+ string? illegal = block.IdentifyInvalidEncodingIssues();
+ if (illegal is not null)
+ {
+ throw new InvalidOperationException($"{nameof(Pack)}(void extent) produced illegal encoding");
+ }
+
+ return (illegal, physicalBlockBits);
+ }
+
+ private static (string? Error, int[] Range) GetEncodedWeightRange(int range)
+ {
+ int[][] validRangeEncodings = [
+ [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1],
+ [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]
+ ];
+
+ int smallestRange = IntermediateBlock.ValidWeightRanges.First();
+ int largestRange = IntermediateBlock.ValidWeightRanges.Last();
+ if (range < smallestRange || largestRange < range)
+ {
+ return ($"Could not find block mode. Invalid weight range: {range} not in [{smallestRange}, {largestRange}]", new int[3]);
+ }
+
+ int index = Array.FindIndex(IntermediateBlock.ValidWeightRanges, v => v >= range);
+ if (index < 0)
+ {
+ index = IntermediateBlock.ValidWeightRanges.Length - 1;
+ }
+
+ int[] encoding = validRangeEncodings[index];
+ return (null, [encoding[0], encoding[1], encoding[2]]);
+ }
+
+ private static string? PackBlockMode(int dimX, int dimY, int range, bool dualPlane, ref BitStream bitSink)
+ {
+ bool highPrec = range > 7;
+ (string? maybeErr, int[]? rangeValues) = GetEncodedWeightRange(range);
+ if (maybeErr != null)
+ {
+ return maybeErr;
+ }
+
+ // Ensure top two bits of r1 and r2 not both zero per reference
+ if ((rangeValues[1] | rangeValues[2]) <= 0)
+ {
+ throw new InvalidOperationException($"{nameof(rangeValues)}[1] | {nameof(rangeValues)}[2] must be > 0");
+ }
+
+ for (int mode = 0; mode < BlockModeInfoTable.Length; ++mode)
+ {
+ BlockModeInfo blockMode = BlockModeInfoTable[mode];
+ bool isValidMode = true;
+ isValidMode &= blockMode.MinWeightGridDimX <= dimX;
+ isValidMode &= dimX <= blockMode.MaxWeightGridDimX;
+ isValidMode &= blockMode.MinWeightGridDimY <= dimY;
+ isValidMode &= dimY <= blockMode.MaxWeightGridDimY;
+ isValidMode &= !(blockMode.RequireSinglePlaneLowPrec && dualPlane);
+ isValidMode &= !(blockMode.RequireSinglePlaneLowPrec && highPrec);
+
+ if (!isValidMode)
+ {
+ continue;
+ }
+
+ uint encodedMode = BlockModeMasks[mode];
+ void SetBit(uint value, int offset)
+ {
+ if (offset < 0)
+ {
+ return;
+ }
+
+ encodedMode = (encodedMode & ~(1u << offset)) | ((value & 1u) << offset);
+ }
+
+ SetBit((uint)rangeValues[0], blockMode.R0BitPos);
+ SetBit((uint)rangeValues[1], blockMode.R1BitPos);
+ SetBit((uint)rangeValues[2], blockMode.R2BitPos);
+
+ int offsetX = dimX - blockMode.MinWeightGridDimX;
+ int offsetY = dimY - blockMode.MinWeightGridDimY;
+
+ if (blockMode.WeightGridXOffsetBitPos >= 0)
+ {
+ encodedMode |= (uint)(offsetX << blockMode.WeightGridXOffsetBitPos);
+ }
+ else
+ {
+ ArgumentOutOfRangeException.ThrowIfNotEqual(offsetX, 0);
+ }
+
+ if (blockMode.WeightGridYOffsetBitPos >= 0)
+ {
+ encodedMode |= (uint)(offsetY << blockMode.WeightGridYOffsetBitPos);
+ }
+ else
+ {
+ ArgumentOutOfRangeException.ThrowIfNotEqual(offsetY, 0);
+ }
+
+ if (!blockMode.RequireSinglePlaneLowPrec)
+ {
+ SetBit(highPrec ? 1u : 0u, 9);
+ SetBit(dualPlane ? 1u : 0u, 10);
+ }
+
+ if (bitSink.Bits != 0)
+ {
+ throw new InvalidOperationException($"{nameof(bitSink)}.{nameof(bitSink.Bits)} must be 0");
+ }
+
+ bitSink.PutBits(encodedMode, 11);
+ return null;
+ }
+
+ return "Could not find viable block mode";
+ }
+
+ private static (BitStream WeightSink, int WeightBitsCount) EncodeWeights(in IntermediateBlock.IntermediateBlockData data)
+ {
+ BitStream weightSink = new(0UL, 0);
+ BoundedIntegerSequenceEncoder weightsEncoder = new(data.WeightRange);
+ int weightCount = data.WeightsCount > 0
+ ? data.WeightsCount
+ : (data.Weights?.Length ?? 0);
+ if (data.Weights is null)
+ {
+ throw new InvalidOperationException($"{nameof(data.Weights)} is null in {nameof(EncodeWeights)}");
+ }
+
+ for (int i = 0; i < weightCount; i++)
+ {
+ weightsEncoder.AddValue(data.Weights[i]);
+ }
+
+ weightsEncoder.Encode(ref weightSink);
+
+ int weightBitsCount = (int)weightSink.Bits;
+ if ((int)weightSink.Bits != BoundedIntegerSequenceCodec.GetBitCountForRange(weightCount, data.WeightRange))
+ {
+ throw new InvalidOperationException($"{nameof(weightSink)}.{nameof(weightSink.Bits)} does not match expected bit count");
+ }
+
+ return (weightSink, weightBitsCount);
+ }
+
+ private static (string? Error, int ExtraConfig) EncodeColorEndpointModes(in IntermediateBlock.IntermediateBlockData data, int partitionCount, ref BitStream bitSink)
+ {
+ int extraConfig = 0;
+ bool sharedEndpointMode = IntermediateBlock.SharedEndpointModes(data);
+
+ if (sharedEndpointMode)
+ {
+ if (partitionCount > 1)
+ {
+ bitSink.PutBits(0u, 2);
+ }
+
+ bitSink.PutBits((uint)data.Endpoints[0].Mode, 4);
+ }
+ else
+ {
+ // compute min_class, max_class
+ int minClass = 2;
+ int maxClass = 0;
+ for (int i = 0; i < data.EndpointCount; i++)
+ {
+ int endpointModeClass = ((int)data.Endpoints[i].Mode) >> 2;
+ minClass = Math.Min(minClass, endpointModeClass);
+ maxClass = Math.Max(maxClass, endpointModeClass);
+ }
+
+ if (maxClass - minClass > 1)
+ {
+ return ("Endpoint modes are invalid", 0);
+ }
+
+ BitStream cemEncoder = new(0UL, 0);
+ cemEncoder.PutBits((uint)(minClass + 1), 2);
+
+ for (int i = 0; i < data.EndpointCount; i++)
+ {
+ int endpointModeClass = ((int)data.Endpoints[i].Mode) >> 2;
+ int classSelectorBit = endpointModeClass - minClass;
+ cemEncoder.PutBits(classSelectorBit, 1);
+ }
+
+ for (int i = 0; i < data.EndpointCount; i++)
+ {
+ int epMode = ((int)data.Endpoints[i].Mode) & 3;
+ cemEncoder.PutBits(epMode, 2);
+ }
+
+ int cemBits = 2 + (partitionCount * 3);
+ if (!cemEncoder.TryGetBits(cemBits, out uint encodedCem))
+ {
+ throw new InvalidOperationException();
+ }
+
+ extraConfig = (int)(encodedCem >> 6);
+
+ bitSink.PutBits(encodedCem, Math.Min(6, cemBits));
+ }
+
+ // dual plane channel
+ if (data.DualPlaneChannel.HasValue)
+ {
+ int channel = data.DualPlaneChannel.Value;
+ ArgumentOutOfRangeException.ThrowIfLessThan(channel, 0);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(channel, 3);
+ extraConfig = (extraConfig << 2) | channel;
+ }
+
+ return (null, extraConfig);
+ }
+
+ private struct BlockModeInfo
+ {
+ public int MinWeightGridDimX;
+ public int MaxWeightGridDimX;
+ public int MinWeightGridDimY;
+ public int MaxWeightGridDimY;
+ public int R0BitPos;
+ public int R1BitPos;
+ public int R2BitPos;
+ public int WeightGridXOffsetBitPos;
+ public int WeightGridYOffsetBitPos;
+ public bool RequireSinglePlaneLowPrec;
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/TexelBlock/LogicalBlock.cs b/src/ImageSharp.Textures.Astc/TexelBlock/LogicalBlock.cs
new file mode 100644
index 00000000..e9cc92e7
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/TexelBlock/LogicalBlock.cs
@@ -0,0 +1,750 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.BiseEncoding.Quantize;
+using SixLabors.ImageSharp.Textures.Astc.BlockDecoder;
+using SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.TexelBlock;
+
+internal sealed class LogicalBlock
+{
+ private ColorEndpointPair[] endpoints;
+ private int endpointCount;
+ private readonly int[] weights;
+ private Partition partition;
+ private DualPlaneData? dualPlane;
+
+ public LogicalBlock(Footprint footprint)
+ {
+ this.endpoints = [ColorEndpointPair.Ldr(RgbaColor.Empty, RgbaColor.Empty)];
+ this.endpointCount = 1;
+ this.weights = new int[footprint.PixelCount];
+ this.partition = new Partition(footprint, 1, 0)
+ {
+ Assignment = new int[footprint.PixelCount]
+ };
+ }
+
+ public LogicalBlock(Footprint footprint, in IntermediateBlock.IntermediateBlockData block)
+ {
+ this.endpoints = new ColorEndpointPair[block.EndpointCount];
+ this.endpointCount = DecodeEndpoints(in block, this.endpoints);
+ this.partition = ComputePartition(footprint, in block);
+ this.weights = new int[footprint.PixelCount];
+ this.CalculateWeights(footprint, in block);
+ }
+
+ public LogicalBlock(Footprint footprint, IntermediateBlock.VoidExtentData block)
+ {
+ this.endpoints = new ColorEndpointPair[1];
+ this.endpointCount = DecodeEndpoints(block, this.endpoints);
+ this.partition = ComputePartition(footprint);
+ this.weights = new int[footprint.PixelCount];
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Decodes directly from raw bits + BlockInfo,
+ /// bypassing IntermediateBlock and using batch unquantize operations.
+ ///
+ private LogicalBlock(Footprint footprint, UInt128 bits, in BlockInfo info)
+ {
+ // --- BISE decode + batch unquantize color endpoint values ---
+ Span colors = stackalloc int[info.ColorValuesCount];
+ FusedBlockDecoder.DecodeBiseValues(
+ bits,
+ info.ColorStartBit,
+ info.ColorBitCount,
+ info.ColorValuesRange,
+ info.ColorValuesCount,
+ colors);
+ Quantization.UnquantizeCEValuesBatch(colors, info.ColorValuesCount, info.ColorValuesRange);
+
+ // --- Decode endpoints per partition ---
+ this.endpointCount = info.PartitionCount;
+ this.endpoints = new ColorEndpointPair[this.endpointCount];
+ int colorIndex = 0;
+ for (int i = 0; i < this.endpointCount; i++)
+ {
+ ColorEndpointMode mode = info.GetEndpointMode(i);
+ int colorCount = mode.GetColorValuesCount();
+ ReadOnlySpan slice = colors.Slice(colorIndex, colorCount);
+ this.endpoints[i] = EndpointCodec.DecodeColorsForModePolymorphicUnquantized(slice, mode);
+ colorIndex += colorCount;
+ }
+
+ // --- Set up partition ---
+ this.partition = info.PartitionCount > 1
+ ? Partition.GetASTCPartition(
+ footprint,
+ info.PartitionCount,
+ (int)BitOperations.GetBits(bits.Low(), 13, 10))
+ : GenerateSinglePartition(footprint);
+
+ // --- BISE decode + unquantize + infill weights ---
+ int gridSize = info.GridWidth * info.GridHeight;
+ bool isDualPlane = info.IsDualPlane;
+ int totalWeights = isDualPlane ? gridSize * 2 : gridSize;
+
+ Span rawWeights = stackalloc int[totalWeights];
+ FusedBlockDecoder.DecodeBiseWeights(
+ bits,
+ info.WeightBitCount,
+ info.WeightRange,
+ totalWeights,
+ rawWeights);
+
+ DecimationInfo decimationInfo = DecimationTable.Get(footprint, info.GridWidth, info.GridHeight);
+ this.weights = new int[footprint.PixelCount];
+
+ if (!isDualPlane)
+ {
+ Quantization.UnquantizeWeightsBatch(rawWeights, gridSize, info.WeightRange);
+ DecimationTable.InfillWeights(rawWeights[..gridSize], decimationInfo, this.weights);
+ }
+ else
+ {
+ // De-interleave: even indices -> plane0, odd indices -> plane1
+ Span plane0 = stackalloc int[gridSize];
+ Span plane1 = stackalloc int[gridSize];
+ for (int i = 0; i < gridSize; i++)
+ {
+ plane0[i] = rawWeights[i * 2];
+ plane1[i] = rawWeights[(i * 2) + 1];
+ }
+
+ Quantization.UnquantizeWeightsBatch(plane0, gridSize, info.WeightRange);
+ Quantization.UnquantizeWeightsBatch(plane1, gridSize, info.WeightRange);
+
+ DecimationTable.InfillWeights(plane0, decimationInfo, this.weights);
+
+ this.dualPlane = new DualPlaneData
+ {
+ Channel = info.DualPlaneChannel,
+ Weights = new int[footprint.PixelCount]
+ };
+ DecimationTable.InfillWeights(plane1, decimationInfo, this.dualPlane.Weights);
+ }
+ }
+
+ public Footprint GetFootprint() => this.partition.Footprint;
+
+ public void SetWeightAt(int x, int y, int weight)
+ {
+ if (weight is < 0 or > 64)
+ {
+ throw new ArgumentOutOfRangeException(nameof(weight));
+ }
+
+ this.weights[(y * this.GetFootprint().Width) + x] = weight;
+ }
+
+ public int WeightAt(int x, int y) => this.weights[(y * this.GetFootprint().Width) + x];
+
+ public void SetDualPlaneWeightAt(int channel, int x, int y, int weight)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(channel);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(weight, 64);
+
+ if (!this.IsDualPlane())
+ {
+ throw new InvalidOperationException("Not a dual plane block");
+ }
+
+ if (this.dualPlane is not null && this.dualPlane.Channel == channel)
+ {
+ this.dualPlane.Weights[(y * this.GetFootprint().Width) + x] = weight;
+ }
+ else
+ {
+ this.SetWeightAt(x, y, weight);
+ }
+ }
+
+ public int DualPlaneWeightAt(int channel, int x, int y)
+ {
+ if (!this.IsDualPlane())
+ {
+ return this.WeightAt(x, y);
+ }
+
+ return this.dualPlane is not null && this.dualPlane.Channel == channel
+ ? this.dualPlane.Weights[(y * this.GetFootprint().Width) + x]
+ : this.WeightAt(x, y);
+ }
+
+ public RgbaColor ColorAt(int x, int y)
+ {
+ Footprint footprint = this.GetFootprint();
+
+ ArgumentOutOfRangeException.ThrowIfNegative(x);
+ ArgumentOutOfRangeException.ThrowIfNegative(y);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(x, footprint.Width);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(y, footprint.Height);
+
+ int index = (y * footprint.Width) + x;
+ int part = this.partition.Assignment[index];
+ ref ColorEndpointPair endpoint = ref this.endpoints[part];
+
+ int weight = this.weights[index];
+ if (!endpoint.IsHdr)
+ {
+ if (this.dualPlane is not null)
+ {
+ return SimdHelpers.InterpolateColorLdrDualPlane(
+ endpoint.LdrLow, endpoint.LdrHigh, weight, this.dualPlane.Channel, this.dualPlane.Weights[index]);
+ }
+
+ return SimdHelpers.InterpolateColorLdr(endpoint.LdrLow, endpoint.LdrHigh, weight);
+ }
+ else
+ {
+ if (this.dualPlane is not null)
+ {
+ int dualPlaneChannel = this.dualPlane.Channel;
+ int dualPlaneWeight = this.dualPlane.Weights[index];
+ int rWeight = dualPlaneChannel == 0 ? dualPlaneWeight : weight;
+ int gWeight = dualPlaneChannel == 1 ? dualPlaneWeight : weight;
+ int bWeight = dualPlaneChannel == 2 ? dualPlaneWeight : weight;
+ int aWeight = dualPlaneChannel == 3 ? dualPlaneWeight : weight;
+ return new RgbaColor(
+ r: InterpolateChannelHdr(endpoint.HdrLow[0], endpoint.HdrHigh[0], rWeight) >> 8,
+ g: InterpolateChannelHdr(endpoint.HdrLow[1], endpoint.HdrHigh[1], gWeight) >> 8,
+ b: InterpolateChannelHdr(endpoint.HdrLow[2], endpoint.HdrHigh[2], bWeight) >> 8,
+ a: InterpolateChannelHdr(endpoint.HdrLow[3], endpoint.HdrHigh[3], aWeight) >> 8);
+ }
+
+ return new RgbaColor(
+ r: InterpolateChannelHdr(endpoint.HdrLow[0], endpoint.HdrHigh[0], weight) >> 8,
+ g: InterpolateChannelHdr(endpoint.HdrLow[1], endpoint.HdrHigh[1], weight) >> 8,
+ b: InterpolateChannelHdr(endpoint.HdrLow[2], endpoint.HdrHigh[2], weight) >> 8,
+ a: InterpolateChannelHdr(endpoint.HdrLow[3], endpoint.HdrHigh[3], weight) >> 8);
+ }
+ }
+
+ ///
+ /// Returns the HDR color at the specified pixel position.
+ ///
+ ///
+ /// For HDR endpoints, returns full 16-bit precision (0-65535) per channel.
+ /// For LDR endpoints, upscales to HDR range.
+ ///
+ public RgbaHdrColor ColorAtHdr(int x, int y)
+ {
+ Footprint footprint = this.GetFootprint();
+
+ ArgumentOutOfRangeException.ThrowIfNegative(x);
+ ArgumentOutOfRangeException.ThrowIfNegative(y);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(x, footprint.Width);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(y, footprint.Height);
+
+ int index = (y * footprint.Width) + x;
+ int part = this.partition.Assignment[index];
+ ref ColorEndpointPair endpoint = ref this.endpoints[part];
+
+ int weight = this.weights[index];
+ if (endpoint.IsHdr)
+ {
+ if (this.dualPlane != null)
+ {
+ int dualPlaneChannel = this.dualPlane.Channel;
+ int dualPlaneWeight = this.dualPlane.Weights[index];
+ int rWeight = dualPlaneChannel == 0 ? dualPlaneWeight : weight;
+ int gWeight = dualPlaneChannel == 1 ? dualPlaneWeight : weight;
+ int bWeight = dualPlaneChannel == 2 ? dualPlaneWeight : weight;
+ int aWeight = dualPlaneChannel == 3 ? dualPlaneWeight : weight;
+ return new RgbaHdrColor(
+ InterpolateChannelHdr(endpoint.HdrLow[0], endpoint.HdrHigh[0], rWeight),
+ InterpolateChannelHdr(endpoint.HdrLow[1], endpoint.HdrHigh[1], gWeight),
+ InterpolateChannelHdr(endpoint.HdrLow[2], endpoint.HdrHigh[2], bWeight),
+ InterpolateChannelHdr(endpoint.HdrLow[3], endpoint.HdrHigh[3], aWeight));
+ }
+
+ return new RgbaHdrColor(
+ InterpolateChannelHdr(endpoint.HdrLow[0], endpoint.HdrHigh[0], weight),
+ InterpolateChannelHdr(endpoint.HdrLow[1], endpoint.HdrHigh[1], weight),
+ InterpolateChannelHdr(endpoint.HdrLow[2], endpoint.HdrHigh[2], weight),
+ InterpolateChannelHdr(endpoint.HdrLow[3], endpoint.HdrHigh[3], weight));
+ }
+ else
+ {
+ if (this.dualPlane != null)
+ {
+ int dualPlaneChannel = this.dualPlane.Channel;
+ int dualPlaneWeight = this.dualPlane.Weights[index];
+ int rWeight = dualPlaneChannel == 0 ? dualPlaneWeight : weight;
+ int gWeight = dualPlaneChannel == 1 ? dualPlaneWeight : weight;
+ int bWeight = dualPlaneChannel == 2 ? dualPlaneWeight : weight;
+ int aWeight = dualPlaneChannel == 3 ? dualPlaneWeight : weight;
+ return new RgbaHdrColor(
+ (ushort)(InterpolateChannel(endpoint.LdrLow.R, endpoint.LdrHigh.R, rWeight) * 257),
+ (ushort)(InterpolateChannel(endpoint.LdrLow.G, endpoint.LdrHigh.G, gWeight) * 257),
+ (ushort)(InterpolateChannel(endpoint.LdrLow.B, endpoint.LdrHigh.B, bWeight) * 257),
+ (ushort)(InterpolateChannel(endpoint.LdrLow.A, endpoint.LdrHigh.A, aWeight) * 257));
+ }
+
+ return new RgbaHdrColor(
+ (ushort)(InterpolateChannel(endpoint.LdrLow.R, endpoint.LdrHigh.R, weight) * 257),
+ (ushort)(InterpolateChannel(endpoint.LdrLow.G, endpoint.LdrHigh.G, weight) * 257),
+ (ushort)(InterpolateChannel(endpoint.LdrLow.B, endpoint.LdrHigh.B, weight) * 257),
+ (ushort)(InterpolateChannel(endpoint.LdrLow.A, endpoint.LdrHigh.A, weight) * 257));
+ }
+ }
+
+ ///
+ /// Writes the HDR float values for the pixel at (x, y) into the output span.
+ ///
+ ///
+ /// For HDR endpoints, values are in LNS (Log-Normalized Space). After interpolation
+ /// in LNS, the result is converted to FP16 via then widened to float.
+ /// For Mode 14 (HDR RGB + LDR Alpha), the alpha channel is UNORM16 instead of LNS.
+ /// For LDR endpoints, the interpolated UNORM16 value is normalized to 0.0-1.0.
+ ///
+ public void WriteHdrPixel(int x, int y, Span output)
+ {
+ Footprint footprint = this.GetFootprint();
+
+ ArgumentOutOfRangeException.ThrowIfNegative(x);
+ ArgumentOutOfRangeException.ThrowIfNegative(y);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(x, footprint.Width);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(y, footprint.Height);
+
+ int index = (y * footprint.Width) + x;
+ int part = this.partition.Assignment[index];
+ ref ColorEndpointPair endpoint = ref this.endpoints[part];
+
+ int weight = this.weights[index];
+ int dualPlaneChannel = this.dualPlane?.Channel ?? -1;
+ int dualPlaneWeight = this.dualPlane?.Weights[index] ?? weight;
+
+ if (endpoint.IsHdr)
+ {
+ for (int channel = 0; channel < RgbaColor.BytesPerPixel; ++channel)
+ {
+ int channelWeight = (channel == dualPlaneChannel)
+ ? dualPlaneWeight
+ : weight;
+ ushort interpolated = InterpolateChannelHdr(endpoint.HdrLow[channel], endpoint.HdrHigh[channel], channelWeight);
+
+ if (channel == 3 && endpoint.AlphaIsLdr)
+ {
+ // Mode 14: alpha is UNORM16, normalize directly
+ output[channel] = interpolated / 65535.0f;
+ }
+ else if (endpoint.ValuesAreLns)
+ {
+ // Normal HDR block: convert from LNS to FP16, then to float
+ ushort halfFloatBits = LnsToSf16(interpolated);
+ output[channel] = (float)BitConverter.UInt16BitsToHalf(halfFloatBits);
+ }
+ else
+ {
+ // Void extent HDR: values are already FP16 bit patterns
+ output[channel] = (float)BitConverter.UInt16BitsToHalf(interpolated);
+ }
+ }
+ }
+ else
+ {
+ for (int channel = 0; channel < RgbaColor.BytesPerPixel; ++channel)
+ {
+ int channelWeight = (channel == dualPlaneChannel)
+ ? dualPlaneWeight
+ : weight;
+ int p0 = channel switch { 0 => endpoint.LdrLow.R, 1 => endpoint.LdrLow.G, 2 => endpoint.LdrLow.B, _ => endpoint.LdrLow.A };
+ int p1 = channel switch { 0 => endpoint.LdrHigh.R, 1 => endpoint.LdrHigh.G, 2 => endpoint.LdrHigh.B, _ => endpoint.LdrHigh.A };
+ ushort unorm16 = InterpolateLdrAsUnorm16(p0, p1, channelWeight);
+ output[channel] = unorm16 / 65535.0f;
+ }
+ }
+ }
+
+ ///
+ /// Writes all pixels in the block directly to the output buffer in RGBA byte format.
+ /// Avoids per-pixel method call overhead, type dispatch, and RgbaColor allocation.
+ ///
+ public void WriteAllPixelsLdr(Footprint footprint, Span buffer)
+ {
+ ref ColorEndpointPair endpoint0 = ref this.endpoints[0];
+
+ if (!endpoint0.IsHdr && this.partition.PartitionCount == 1)
+ {
+ // Fast path: single-partition LDR block (most common case)
+ int lowR = endpoint0.LdrLow.R, lowG = endpoint0.LdrLow.G, lowB = endpoint0.LdrLow.B, lowA = endpoint0.LdrLow.A;
+ int highR = endpoint0.LdrHigh.R, highG = endpoint0.LdrHigh.G, highB = endpoint0.LdrHigh.B, highA = endpoint0.LdrHigh.A;
+
+ if (this.dualPlane == null)
+ {
+ this.WriteLdrSinglePartition(buffer, footprint, lowR, lowG, lowB, lowA, highR, highG, highB, highA);
+ }
+ else
+ {
+ int dualPlaneChannel = this.dualPlane.Channel;
+ int[] dpWeights = this.dualPlane.Weights;
+ int pixelCount = footprint.PixelCount;
+ for (int i = 0; i < pixelCount; i++)
+ {
+ SimdHelpers.WriteSinglePixelLdrDualPlane(
+ buffer,
+ i * 4,
+ lowR,
+ lowG,
+ lowB,
+ lowA,
+ highR,
+ highG,
+ highB,
+ highA,
+ this.weights[i],
+ dualPlaneChannel,
+ dpWeights[i]);
+ }
+ }
+ }
+ else
+ {
+ // General path: multi-partition or HDR blocks
+ this.WriteAllPixelsGeneral(footprint, buffer);
+ }
+ }
+
+ public void SetPartition(Partition p)
+ {
+ if (!p.Footprint.Equals(this.partition.Footprint))
+ {
+ throw new InvalidOperationException("New partitions may not be for a different footprint");
+ }
+
+ this.partition = p;
+ if (this.endpointCount < p.PartitionCount)
+ {
+ ColorEndpointPair[] newEndpoints = new ColorEndpointPair[p.PartitionCount];
+ Array.Copy(this.endpoints, newEndpoints, this.endpointCount);
+ for (int i = this.endpointCount; i < p.PartitionCount; i++)
+ {
+ newEndpoints[i] = ColorEndpointPair.Ldr(RgbaColor.Empty, RgbaColor.Empty);
+ }
+
+ this.endpoints = newEndpoints;
+ }
+
+ this.endpointCount = p.PartitionCount;
+ }
+
+ public void SetEndpoints(RgbaColor firstEndpoint, RgbaColor secondEndpoint, int subset)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(subset);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(subset, this.partition.PartitionCount);
+
+ this.endpoints[subset] = ColorEndpointPair.Ldr(firstEndpoint, secondEndpoint);
+ }
+
+ public void SetDualPlaneChannel(int channel)
+ {
+ if (channel < 0)
+ {
+ this.dualPlane = null;
+ }
+ else if (this.dualPlane != null)
+ {
+ this.dualPlane.Channel = channel;
+ }
+ else
+ {
+ this.dualPlane = new DualPlaneData { Channel = channel, Weights = (int[])this.weights.Clone() };
+ }
+ }
+
+ public bool IsDualPlane() => this.dualPlane is not null;
+
+ public static LogicalBlock? UnpackLogicalBlock(Footprint footprint, UInt128 bits, in BlockInfo info)
+ {
+ if (!info.IsValid)
+ {
+ return null;
+ }
+
+ if (info.IsVoidExtent)
+ {
+ // Void extent blocks are rare; fall back to existing PhysicalBlock path
+ PhysicalBlock pb = PhysicalBlock.Create(bits);
+ IntermediateBlock.VoidExtentData? voidExtentData = IntermediateBlock.UnpackVoidExtent(pb);
+ if (voidExtentData is null)
+ {
+ return null;
+ }
+
+ return new LogicalBlock(footprint, voidExtentData.Value);
+ }
+ else
+ {
+ return new LogicalBlock(footprint, bits, in info);
+ }
+ }
+
+ ///
+ /// Converts a 16-bit LNS (Log-Normalized Space) value to a 16-bit SF16 (FP16) bit pattern.
+ ///
+ ///
+ /// The LNS value encodes a 5-bit exponent in the upper bits and an 11-bit mantissa
+ /// in the lower bits. The mantissa is transformed using a piecewise linear function
+ /// before being combined with the exponent to form the FP16 result.
+ ///
+ internal static ushort LnsToSf16(int lns)
+ {
+ int mantissaComponent = lns & 0x7FF; // Lower 11 bits: mantissa component
+ int exponentComponent = (lns >> 11) & 0x1F; // Upper 5 bits: exponent component
+
+ int mantissaTransformed;
+ if (mantissaComponent < 512)
+ {
+ mantissaTransformed = mantissaComponent * 3;
+ }
+ else if (mantissaComponent < 1536)
+ {
+ mantissaTransformed = (mantissaComponent * 4) - 512;
+ }
+ else
+ {
+ mantissaTransformed = (mantissaComponent * 5) - 2048;
+ }
+
+ int result = (exponentComponent << 10) | (mantissaTransformed >> 3);
+ return (ushort)Math.Min(result, 0x7BFF); // Clamp to max finite FP16
+ }
+
+ private static int DecodeEndpoints(in IntermediateBlock.IntermediateBlockData block, ColorEndpointPair[] endpointPair)
+ {
+ int endpointRange = block.EndpointRange ?? IntermediateBlock.EndpointRangeForBlock(block);
+ if (endpointRange <= 0)
+ {
+ throw new InvalidOperationException("Invalid endpoint range");
+ }
+
+ for (int i = 0; i < block.EndpointCount; i++)
+ {
+ IntermediateBlock.IntermediateEndpointData ed = block.Endpoints[i];
+ ReadOnlySpan colorSpan = ((ReadOnlySpan)ed.Colors)[..ed.ColorCount];
+ endpointPair[i] = EndpointCodec.DecodeColorsForModePolymorphic(colorSpan, endpointRange, ed.Mode);
+ }
+
+ return block.EndpointCount;
+ }
+
+ private static int DecodeEndpoints(IntermediateBlock.VoidExtentData block, ColorEndpointPair[] endpointPair)
+ {
+ if (block.IsHdr)
+ {
+ // HDR void extent: ushort values are FP16 bit patterns (not LNS)
+ RgbaHdrColor hdrColor = new(block.R, block.G, block.B, block.A);
+ endpointPair[0] = ColorEndpointPair.Hdr(hdrColor, hdrColor, valuesAreLns: false);
+ }
+ else
+ {
+ // LDR void extent: ushort values are UNORM16, convert to byte range
+ RgbaColor ldrColor = new(
+ (byte)(block.R >> 8),
+ (byte)(block.G >> 8),
+ (byte)(block.B >> 8),
+ (byte)(block.A >> 8));
+ endpointPair[0] = ColorEndpointPair.Ldr(ldrColor, ldrColor);
+ }
+
+ return 1;
+ }
+
+ private static Partition GenerateSinglePartition(Footprint footprint) => new(footprint, 1, 0)
+ {
+ Assignment = new int[footprint.PixelCount]
+ };
+
+ private static Partition ComputePartition(Footprint footprint, in IntermediateBlock.IntermediateBlockData block)
+ => block.PartitionId.HasValue
+ ? Partition.GetASTCPartition(footprint, block.EndpointCount, block.PartitionId.Value)
+ : GenerateSinglePartition(footprint);
+
+ private static Partition ComputePartition(Footprint footprint)
+ => GenerateSinglePartition(footprint);
+
+ private void CalculateWeights(Footprint footprint, in IntermediateBlock.IntermediateBlockData block)
+ {
+ int gridSize = block.WeightGridX * block.WeightGridY;
+ int weightFrequency = block.DualPlaneChannel.HasValue ? 2 : 1;
+
+ // Get decimation info once for both planes
+ DecimationInfo decimationInfo = DecimationTable.Get(footprint, block.WeightGridX, block.WeightGridY);
+
+ // stackalloc avoids per-block heap allocation (max 12×12 = 144 ints = 576 bytes)
+ Span unquantized = stackalloc int[gridSize];
+ for (int i = 0; i < gridSize; ++i)
+ {
+ unquantized[i] = Quantization.UnquantizeWeightFromRange(
+ block.Weights[i * weightFrequency], block.WeightRange);
+ }
+
+ DecimationTable.InfillWeights(unquantized, decimationInfo, this.weights);
+
+ if (block.DualPlaneChannel.HasValue)
+ {
+ DualPlaneData dualPlane = new()
+ {
+ Channel = block.DualPlaneChannel.Value,
+ Weights = new int[footprint.PixelCount]
+ };
+ this.dualPlane = dualPlane;
+ for (int i = 0; i < gridSize; ++i)
+ {
+ unquantized[i] = Quantization.UnquantizeWeightFromRange(
+ block.Weights[(i * weightFrequency) + 1], block.WeightRange);
+ }
+
+ DecimationTable.InfillWeights(unquantized, decimationInfo, this.dualPlane.Weights);
+ }
+ }
+
+ private static int InterpolateChannel(int p0, int p1, int weight)
+ {
+ int c0 = (p0 << 8) | p0;
+ int c1 = (p1 << 8) | p1;
+ int c = ((c0 * (64 - weight)) + (c1 * weight) + 32) / 64;
+ int quantized = ((c * byte.MaxValue) + short.MaxValue) / (ushort.MaxValue + 1);
+ return Math.Clamp(quantized, 0, byte.MaxValue);
+ }
+
+ ///
+ /// Interpolates an LDR channel value and returns the full 16-bit UNORM result
+ /// (before reduction to byte). Used by the HDR output path for LDR endpoints.
+ ///
+ private static ushort InterpolateLdrAsUnorm16(int p0, int p1, int weight)
+ {
+ int c0 = (p0 << 8) | p0;
+ int c1 = (p1 << 8) | p1;
+ int c = ((c0 * (64 - weight)) + (c1 * weight) + 32) / 64;
+ return (ushort)Math.Clamp(c, 0, 0xFFFF);
+ }
+
+ ///
+ /// Interpolates an HDR channel value between two endpoints using the specified weight.
+ ///
+ ///
+ /// HDR endpoints are already 16-bit values (FP16 bit patterns). Unlike LDR interpolation
+ /// which expands 8-bit to 16-bit before interpolating, HDR interpolation operates directly
+ /// on the 16-bit values
+ ///
+ private static ushort InterpolateChannelHdr(int p0, int p1, int weight)
+ {
+ int c = ((p0 * (64 - weight)) + (p1 * weight) + 32) / 64;
+ return (ushort)Math.Clamp(c, 0, 0xFFFF);
+ }
+
+ private void WriteLdrSinglePartition(
+ Span buffer,
+ Footprint footprint,
+ int lowR,
+ int lowG,
+ int lowB,
+ int lowA,
+ int highR,
+ int highG,
+ int highB,
+ int highA)
+ {
+ int pixelCount = footprint.PixelCount;
+ for (int i = 0; i < pixelCount; i++)
+ {
+ SimdHelpers.WriteSinglePixelLdr(
+ buffer,
+ i * 4,
+ lowR,
+ lowG,
+ lowB,
+ lowA,
+ highR,
+ highG,
+ highB,
+ highA,
+ this.weights[i]);
+ }
+ }
+
+ private void WriteAllPixelsGeneral(Footprint footprint, Span buffer)
+ {
+ int pixelCount = footprint.PixelCount;
+ for (int i = 0; i < pixelCount; i++)
+ {
+ int part = this.partition.Assignment[i];
+ ref ColorEndpointPair endpoint = ref this.endpoints[part];
+
+ int weight = this.weights[i];
+ if (!endpoint.IsHdr)
+ {
+ if (this.dualPlane is not null)
+ {
+ SimdHelpers.WriteSinglePixelLdrDualPlane(
+ buffer,
+ i * 4,
+ endpoint.LdrLow.R,
+ endpoint.LdrLow.G,
+ endpoint.LdrLow.B,
+ endpoint.LdrLow.A,
+ endpoint.LdrHigh.R,
+ endpoint.LdrHigh.G,
+ endpoint.LdrHigh.B,
+ endpoint.LdrHigh.A,
+ weight,
+ this.dualPlane.Channel,
+ this.dualPlane.Weights[i]);
+ }
+ else
+ {
+ SimdHelpers.WriteSinglePixelLdr(
+ buffer,
+ i * 4,
+ endpoint.LdrLow.R,
+ endpoint.LdrLow.G,
+ endpoint.LdrLow.B,
+ endpoint.LdrLow.A,
+ endpoint.LdrHigh.R,
+ endpoint.LdrHigh.G,
+ endpoint.LdrHigh.B,
+ endpoint.LdrHigh.A,
+ weight);
+ }
+ }
+ else
+ {
+ int dualPlaneChannel = this.dualPlane?.Channel ?? -1;
+ int dualPlaneWeight = this.dualPlane?.Weights[i] ?? weight;
+ int rWeight = dualPlaneChannel == 0 ? dualPlaneWeight : weight;
+ int gWeight = dualPlaneChannel == 1 ? dualPlaneWeight : weight;
+ int bWeight = dualPlaneChannel == 2 ? dualPlaneWeight : weight;
+ int aWeight = dualPlaneChannel == 3 ? dualPlaneWeight : weight;
+ buffer[(i * 4) + 0] = (byte)(InterpolateChannelHdr(
+ endpoint.HdrLow[0],
+ endpoint.HdrHigh[0],
+ rWeight) >> 8);
+ buffer[(i * 4) + 1] = (byte)(InterpolateChannelHdr(
+ endpoint.HdrLow[1],
+ endpoint.HdrHigh[1],
+ gWeight) >> 8);
+ buffer[(i * 4) + 2] = (byte)(InterpolateChannelHdr(
+ endpoint.HdrLow[2],
+ endpoint.HdrHigh[2],
+ bWeight) >> 8);
+ buffer[(i * 4) + 3] = (byte)(InterpolateChannelHdr(
+ endpoint.HdrLow[3],
+ endpoint.HdrHigh[3],
+ aWeight) >> 8);
+ }
+ }
+ }
+
+ private class DualPlaneData
+ {
+ public int Channel { get; set; }
+
+ public int[] Weights { get; set; } = [];
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/TexelBlock/PhysicalBlock.cs b/src/ImageSharp.Textures.Astc/TexelBlock/PhysicalBlock.cs
new file mode 100644
index 00000000..d7b6e2c6
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/TexelBlock/PhysicalBlock.cs
@@ -0,0 +1,216 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Astc.TexelBlock;
+
+///
+/// A physical ASTC texel block (128 bits).
+/// Delegates all block mode decoding to .
+///
+internal readonly struct PhysicalBlock
+{
+ public const int SizeInBytes = 16;
+ private readonly BlockInfo info;
+
+ private PhysicalBlock(UInt128 bits, BlockInfo info)
+ {
+ this.BlockBits = bits;
+ this.info = info;
+ }
+
+ public UInt128 BlockBits { get; }
+
+ public bool IsVoidExtent => this.info.IsVoidExtent;
+
+ public bool IsIllegalEncoding => !this.info.IsValid;
+
+ public bool IsDualPlane
+ => this.info.IsValid && !this.info.IsVoidExtent && this.info.IsDualPlane;
+
+ ///
+ /// Factory method to create a PhysicalBlock from raw bits
+ ///
+ public static PhysicalBlock Create(UInt128 bits)
+ => new(bits, BlockInfo.Decode(bits));
+
+ public static PhysicalBlock Create(ulong low) => Create((UInt128)low);
+
+ public static PhysicalBlock Create(ulong low, ulong high) => Create(new UInt128(high, low));
+
+ internal (int Width, int Height)? GetWeightGridDimensions()
+ => this.info.IsValid && !this.info.IsVoidExtent
+ ? (this.info.GridWidth, this.info.GridHeight)
+ : null;
+
+ internal int? GetWeightRange()
+ => this.info.IsValid && !this.info.IsVoidExtent
+ ? this.info.WeightRange
+ : null;
+
+ internal int[]? GetVoidExtentCoordinates()
+ {
+ if (!this.info.IsVoidExtent)
+ {
+ return null;
+ }
+
+ // If void extent coords are all 1's then these are not valid void extent coords
+ ulong voidExtentMask = 0xFFFFFFFFFFFFFDFFUL;
+ ulong constBlockMode = 0xFFFFFFFFFFFFFDFCUL;
+
+ return this.info.IsValid && (voidExtentMask & this.BlockBits.Low()) != constBlockMode
+ ? DecodeVoidExtentCoordinates(this.BlockBits)
+ : null;
+ }
+
+ ///
+ /// Get the dual plane channel if dual plane is enabled
+ ///
+ /// The dual plane channel if enabled, otherwise null.
+ internal int? GetDualPlaneChannel()
+ => this.info.IsValid && this.info.IsDualPlane
+ ? this.info.DualPlaneChannel
+ : null;
+
+ internal string? IdentifyInvalidEncodingIssues()
+ {
+ if (this.info.IsValid)
+ {
+ return null;
+ }
+
+ return this.info.IsVoidExtent
+ ? IdentifyVoidExtentIssues(this.BlockBits)
+ : "Invalid block encoding";
+ }
+
+ internal int? GetWeightBitCount()
+ => this.info.IsValid && !this.info.IsVoidExtent
+ ? this.info.WeightBitCount
+ : null;
+
+ internal int? GetWeightStartBit()
+ => this.info.IsValid && !this.info.IsVoidExtent
+ ? 128 - this.info.WeightBitCount
+ : null;
+
+ internal int? GetPartitionsCount()
+ => this.info.IsValid && !this.info.IsVoidExtent
+ ? this.info.PartitionCount
+ : null;
+
+ internal int? GetPartitionId()
+ {
+ if (!this.info.IsValid || this.info.IsVoidExtent || this.info.PartitionCount == 1)
+ {
+ return null;
+ }
+
+ return (int)BitOperations.GetBits(this.BlockBits.Low(), 13, 10);
+ }
+
+ internal ColorEndpointMode? GetEndpointMode(int partition)
+ {
+ if (!this.info.IsValid || this.info.IsVoidExtent)
+ {
+ return null;
+ }
+
+ if (partition < 0 || partition >= this.info.PartitionCount)
+ {
+ return null;
+ }
+
+ return this.info.GetEndpointMode(partition);
+ }
+
+ internal int? GetColorStartBit()
+ {
+ if (this.info.IsVoidExtent)
+ {
+ return 64;
+ }
+
+ return this.info.IsValid
+ ? this.info.ColorStartBit
+ : null;
+ }
+
+ internal int? GetColorValuesCount()
+ {
+ if (this.info.IsVoidExtent)
+ {
+ return 4;
+ }
+
+ return this.info.IsValid
+ ? this.info.ColorValuesCount
+ : null;
+ }
+
+ internal int? GetColorBitCount()
+ {
+ if (this.info.IsVoidExtent)
+ {
+ return 64;
+ }
+
+ return this.info.IsValid
+ ? this.info.ColorBitCount
+ : null;
+ }
+
+ internal int? GetColorValuesRange()
+ {
+ if (this.info.IsVoidExtent)
+ {
+ return (1 << 16) - 1;
+ }
+
+ return this.info.IsValid
+ ? this.info.ColorValuesRange
+ : null;
+ }
+
+ internal static int[] DecodeVoidExtentCoordinates(UInt128 astcBits)
+ {
+ ulong lowBits = astcBits.Low();
+ int[] coords = new int[4];
+ for (int i = 0; i < 4; ++i)
+ {
+ coords[i] = (int)BitOperations.GetBits(lowBits, 12 + (13 * i), 13);
+ }
+
+ return coords;
+ }
+
+ ///
+ /// Full error-string version for void extent issues (used for error reporting)
+ ///
+ private static string? IdentifyVoidExtentIssues(UInt128 bits)
+ {
+ if (BitOperations.GetBits(bits, 10, 2).Low() != 0x3UL)
+ {
+ return "Reserved bits set for void extent block";
+ }
+
+ ulong lowBits = bits.Low();
+ int c0 = (int)BitOperations.GetBits(lowBits, 12, 13);
+ int c1 = (int)BitOperations.GetBits(lowBits, 25, 13);
+ int c2 = (int)BitOperations.GetBits(lowBits, 38, 13);
+ int c3 = (int)BitOperations.GetBits(lowBits, 51, 13);
+
+ const int all1s = (1 << 13) - 1;
+ bool coordsAll1s = c0 == all1s && c1 == all1s && c2 == all1s && c3 == all1s;
+
+ if (!coordsAll1s && (c0 >= c1 || c2 >= c3))
+ {
+ return "Void extent texture coordinates are invalid";
+ }
+
+ return null;
+ }
+}
diff --git a/src/ImageSharp.Textures.Astc/TexelBlock/PhysicalBlockMode.cs b/src/ImageSharp.Textures.Astc/TexelBlock/PhysicalBlockMode.cs
new file mode 100644
index 00000000..dc414388
--- /dev/null
+++ b/src/ImageSharp.Textures.Astc/TexelBlock/PhysicalBlockMode.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Astc.TexelBlock;
+
+///
+/// The overall block modes defined in table C.2.8. There are 10
+/// weight grid encoding schemes + void extent.
+///
+internal enum PhysicalBlockMode
+{
+ WidthB4HeightA2,
+ WidthB8HeightA2,
+ WidthA2HeightB8,
+ WidthA2HeightB6,
+ WidthB2HeightA2,
+ Width12HeightA2,
+ WidthA2Height12,
+ Width6Height10,
+ Width10Height6,
+ WidthA6HeightB6,
+ VoidExtent,
+}
diff --git a/src/ImageSharp.Textures/Common/Exceptions/TextureFormatException.cs b/src/ImageSharp.Textures/Common/Exceptions/TextureFormatException.cs
index 8fd3c014..df73308e 100644
--- a/src/ImageSharp.Textures/Common/Exceptions/TextureFormatException.cs
+++ b/src/ImageSharp.Textures/Common/Exceptions/TextureFormatException.cs
@@ -1,36 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
+namespace SixLabors.ImageSharp.Textures.Common.Exceptions;
-namespace SixLabors.ImageSharp.Textures.Common.Exceptions
+///
+/// The exception that is thrown when the library tries to load
+/// an image, which has an invalid format.
+///
+public class TextureFormatException : Exception
{
///
- /// The exception that is thrown when the library tries to load
- /// an image, which has an invalid format.
+ /// Initializes a new instance of the class with the name of the
+ /// parameter that causes this exception.
///
- public class TextureFormatException : Exception
+ /// The error message that explains the reason for this exception.
+ internal TextureFormatException(string errorMessage)
+ : base(errorMessage)
{
- ///
- /// Initializes a new instance of the class with the name of the
- /// parameter that causes this exception.
- ///
- /// The error message that explains the reason for this exception.
- internal TextureFormatException(string errorMessage)
- : base(errorMessage)
- {
- }
+ }
- ///
- /// Initializes a new instance of the class with a specified
- /// error message and the exception that is the cause of this exception.
- ///
- /// The error message that explains the reason for this exception.
- /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic)
- /// if no inner exception is specified.
- internal TextureFormatException(string errorMessage, Exception innerException)
- : base(errorMessage, innerException)
- {
- }
+ ///
+ /// Initializes a new instance of the class with a specified
+ /// error message and the exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for this exception.
+ /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic)
+ /// if no inner exception is specified.
+ internal TextureFormatException(string errorMessage, Exception innerException)
+ : base(errorMessage, innerException)
+ {
}
}
diff --git a/src/ImageSharp.Textures/Common/Exceptions/TextureProcessingException.cs b/src/ImageSharp.Textures/Common/Exceptions/TextureProcessingException.cs
index ba360be0..e714fb18 100644
--- a/src/ImageSharp.Textures/Common/Exceptions/TextureProcessingException.cs
+++ b/src/ImageSharp.Textures/Common/Exceptions/TextureProcessingException.cs
@@ -1,35 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
+namespace SixLabors.ImageSharp.Textures.Common.Exceptions;
-namespace SixLabors.ImageSharp.Textures.Common.Exceptions
+///
+/// The exception that is thrown when an error occurs when applying a process to an image.
+///
+public sealed class TextureProcessingException : Exception
{
///
- /// The exception that is thrown when an error occurs when applying a process to an image.
+ /// Initializes a new instance of the class with the name of the
+ /// parameter that causes this exception.
///
- public sealed class TextureProcessingException : Exception
+ /// The error message that explains the reason for this exception.
+ public TextureProcessingException(string errorMessage)
+ : base(errorMessage)
{
- ///
- /// Initializes a new instance of the class with the name of the
- /// parameter that causes this exception.
- ///
- /// The error message that explains the reason for this exception.
- public TextureProcessingException(string errorMessage)
- : base(errorMessage)
- {
- }
+ }
- ///
- /// Initializes a new instance of the class with a specified
- /// error message and the exception that is the cause of this exception.
- ///
- /// The error message that explains the reason for this exception.
- /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic)
- /// if no inner exception is specified.
- public TextureProcessingException(string errorMessage, Exception innerException)
- : base(errorMessage, innerException)
- {
- }
+ ///
+ /// Initializes a new instance of the class with a specified
+ /// error message and the exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for this exception.
+ /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic)
+ /// if no inner exception is specified.
+ public TextureProcessingException(string errorMessage, Exception innerException)
+ : base(errorMessage, innerException)
+ {
}
}
diff --git a/src/ImageSharp.Textures/Common/Exceptions/UnknownTextureFormatException.cs b/src/ImageSharp.Textures/Common/Exceptions/UnknownTextureFormatException.cs
index 05bce3f2..80b093f9 100644
--- a/src/ImageSharp.Textures/Common/Exceptions/UnknownTextureFormatException.cs
+++ b/src/ImageSharp.Textures/Common/Exceptions/UnknownTextureFormatException.cs
@@ -1,22 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Textures.Common.Exceptions
+namespace SixLabors.ImageSharp.Textures.Common.Exceptions;
+
+///
+/// The exception that is thrown when the library tries to load
+/// an image which has an unknown format.
+///
+public sealed class UnknownTextureFormatException : TextureFormatException
{
///
- /// The exception that is thrown when the library tries to load
- /// an image which has an unknown format.
+ /// Initializes a new instance of the class with the name of the
+ /// parameter that causes this exception.
///
- public sealed class UnknownTextureFormatException : TextureFormatException
+ /// The error message that explains the reason for this exception.
+ public UnknownTextureFormatException(string errorMessage)
+ : base(errorMessage)
{
- ///
- /// Initializes a new instance of the class with the name of the
- /// parameter that causes this exception.
- ///
- /// The error message that explains the reason for this exception.
- public UnknownTextureFormatException(string errorMessage)
- : base(errorMessage)
- {
- }
}
}
diff --git a/src/ImageSharp.Textures/Common/Extensions/StreamExtensions.cs b/src/ImageSharp.Textures/Common/Extensions/StreamExtensions.cs
index 274a0227..723927e9 100644
--- a/src/ImageSharp.Textures/Common/Extensions/StreamExtensions.cs
+++ b/src/ImageSharp.Textures/Common/Extensions/StreamExtensions.cs
@@ -1,105 +1,101 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
using System.Buffers;
-using System.IO;
-using SixLabors.ImageSharp.Memory;
-namespace SixLabors.ImageSharp.Textures.Common.Extensions
+namespace SixLabors.ImageSharp.Textures.Common.Extensions;
+
+///
+/// Extension methods for the type.
+///
+internal static class StreamExtensions
{
///
- /// Extension methods for the type.
+ /// Reads data from a stream into the provided buffer.
///
- internal static class StreamExtensions
- {
- ///
- /// Reads data from a stream into the provided buffer.
- ///
- /// The stream.
- /// The buffer..
- /// The offset within the buffer where the bytes are read into.
- /// The number of bytes, if available, to read.
- /// The actual number of bytes read.
- public static int Read(this Stream stream, Span buffer, int offset, int count) => stream.Read(buffer.Slice(offset, count));
+ /// The stream.
+ /// The buffer..
+ /// The offset within the buffer where the bytes are read into.
+ /// The number of bytes, if available, to read.
+ /// The actual number of bytes read.
+ public static int Read(this Stream stream, Span buffer, int offset, int count) => stream.Read(buffer.Slice(offset, count));
- ///
- /// Skips the number of bytes in the given stream.
- ///
- /// The stream.
- /// The count.
- public static void Skip(this Stream stream, int count)
+ ///
+ /// Skips the number of bytes in the given stream.
+ ///
+ /// The stream.
+ /// The count.
+ public static void Skip(this Stream stream, int count)
+ {
+ if (count < 1)
{
- if (count < 1)
- {
- return;
- }
+ return;
+ }
- if (stream.CanSeek)
- {
- stream.Seek(count, SeekOrigin.Current); // Position += count;
- }
- else
+ if (stream.CanSeek)
+ {
+ stream.Seek(count, SeekOrigin.Current); // Position += count;
+ }
+ else
+ {
+ byte[] foo = new byte[count];
+ while (count > 0)
{
- byte[] foo = new byte[count];
- while (count > 0)
+ int bytesRead = stream.Read(foo, 0, count);
+ if (bytesRead == 0)
{
- int bytesRead = stream.Read(foo, 0, count);
- if (bytesRead == 0)
- {
- break;
- }
-
- count -= bytesRead;
+ break;
}
+
+ count -= bytesRead;
}
}
+ }
#if NET472 || NETSTANDARD1_3 || NETSTANDARD2_0
- // This is a port of the CoreFX implementation and is MIT Licensed: https://github.com/dotnet/coreclr/blob/c4dca1072d15bdda64c754ad1ea474b1580fa554/src/System.Private.CoreLib/shared/System/IO/Stream.cs#L770
- public static void Write(this Stream stream, ReadOnlySpan buffer)
+ // This is a port of the CoreFX implementation and is MIT Licensed: https://github.com/dotnet/coreclr/blob/c4dca1072d15bdda64c754ad1ea474b1580fa554/src/System.Private.CoreLib/shared/System/IO/Stream.cs#L770
+ public static void Write(this Stream stream, ReadOnlySpan buffer)
+ {
+ // This uses ArrayPool.Shared, rather than taking a MemoryAllocator,
+ // in order to match the signature of the framework method that exists in
+ // .NET Core.
+ byte[] sharedBuffer = ArrayPool.Shared.Rent(buffer.Length);
+ try
{
- // This uses ArrayPool.Shared, rather than taking a MemoryAllocator,
- // in order to match the signature of the framework method that exists in
- // .NET Core.
- byte[] sharedBuffer = ArrayPool.Shared.Rent(buffer.Length);
- try
- {
- buffer.CopyTo(sharedBuffer);
- stream.Write(sharedBuffer, 0, buffer.Length);
- }
- finally
- {
- ArrayPool.Shared.Return(sharedBuffer);
- }
+ buffer.CopyTo(sharedBuffer);
+ stream.Write(sharedBuffer, 0, buffer.Length);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(sharedBuffer);
}
+ }
#endif
#if !SUPPORTS_SPAN_STREAM
- // This is a port of the CoreFX implementation and is MIT Licensed:
- // https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/src/Common/src/CoreLib/System/IO/Stream.cs#L742
- public static int Read(this Stream stream, Span buffer)
+ // This is a port of the CoreFX implementation and is MIT Licensed:
+ // https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/src/Common/src/CoreLib/System/IO/Stream.cs#L742
+ public static int Read(this Stream stream, Span buffer)
+ {
+ // This uses ArrayPool.Shared, rather than taking a MemoryAllocator,
+ // in order to match the signature of the framework method that exists in
+ // .NET Core.
+ byte[] sharedBuffer = ArrayPool.Shared.Rent(buffer.Length);
+ try
{
- // This uses ArrayPool.Shared, rather than taking a MemoryAllocator,
- // in order to match the signature of the framework method that exists in
- // .NET Core.
- byte[] sharedBuffer = ArrayPool.Shared.Rent(buffer.Length);
- try
+ int numRead = stream.Read(sharedBuffer, 0, buffer.Length);
+ if ((uint)numRead > (uint)buffer.Length)
{
- int numRead = stream.Read(sharedBuffer, 0, buffer.Length);
- if ((uint)numRead > (uint)buffer.Length)
- {
- throw new IOException("Stream was too long.");
- }
-
- new Span(sharedBuffer, 0, numRead).CopyTo(buffer);
- return numRead;
- }
- finally
- {
- ArrayPool.Shared.Return(sharedBuffer);
+ throw new IOException("Stream was too long.");
}
+
+ new Span(sharedBuffer, 0, numRead).CopyTo(buffer);
+ return numRead;
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(sharedBuffer);
}
-#endif
}
+#endif
}
diff --git a/src/ImageSharp.Textures/Common/Extensions/ToStringExtension.cs b/src/ImageSharp.Textures/Common/Extensions/ToStringExtension.cs
index da8ec122..07e0d106 100644
--- a/src/ImageSharp.Textures/Common/Extensions/ToStringExtension.cs
+++ b/src/ImageSharp.Textures/Common/Extensions/ToStringExtension.cs
@@ -1,24 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
using System.Text;
-namespace SixLabors.ImageSharp.Textures.Common.Extensions
+namespace SixLabors.ImageSharp.Textures.Common.Extensions;
+
+///
+/// To string extension methods.
+///
+public static class ToStringExtension
{
///
- /// To string extension methods.
+ /// Converts a FourCC value to a string.
///
- public static class ToStringExtension
- {
- ///
- /// Converts a FourCC value to a string.
- ///
- /// The FourCC.
- /// A string for the FourCC.
- public static string FourCcToString(this uint value)
- {
- return Encoding.UTF8.GetString(BitConverter.GetBytes(value));
- }
- }
+ /// The FourCC.
+ /// A string for the FourCC.
+ public static string FourCcToString(this uint value) => Encoding.UTF8.GetString(BitConverter.GetBytes(value));
}
diff --git a/src/ImageSharp.Textures/Common/Helpers/FloatHelper.cs b/src/ImageSharp.Textures/Common/Helpers/FloatHelper.cs
index 98b8de50..63e86b8d 100644
--- a/src/ImageSharp.Textures/Common/Helpers/FloatHelper.cs
+++ b/src/ImageSharp.Textures/Common/Helpers/FloatHelper.cs
@@ -3,70 +3,69 @@
using System.Runtime.CompilerServices;
-namespace SixLabors.ImageSharp.Textures.Common.Helpers
+namespace SixLabors.ImageSharp.Textures.Common.Helpers;
+
+internal static class FloatHelper
{
- internal static class FloatHelper
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float UnpackFloat32ToFloat(uint value) => Unsafe.As(ref value);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float UnpackFloat32ToFloat(uint value) => Unsafe.As(ref value);
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static uint PackFloatToFloat32(float value) => Unsafe.As(ref value);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint PackFloatToFloat32(float value) => Unsafe.As(ref value);
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float UnpackFloat16ToFloat(uint value)
- {
- uint result =
- ((value >> 15) << 31) |
- ((((value >> 10) & 0x1f) - 15 + 127) << 23) |
- ((value & 0x3ff) << 13);
- return Unsafe.As(ref result);
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float UnpackFloat16ToFloat(uint value)
+ {
+ uint result =
+ ((value >> 15) << 31) |
+ ((((value >> 10) & 0x1f) - 15 + 127) << 23) |
+ ((value & 0x3ff) << 13);
+ return Unsafe.As(ref result);
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static uint PackFloatToFloat16(float value)
- {
- uint temp = Unsafe.As(ref value);
- return
- ((temp >> 31) << 15) |
- ((((temp >> 23) & 0xff) - 127 + 15) << 10) |
- ((temp & 0x7fffff) >> 13);
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint PackFloatToFloat16(float value)
+ {
+ uint temp = Unsafe.As(ref value);
+ return
+ ((temp >> 31) << 15) |
+ ((((temp >> 23) & 0xff) - 127 + 15) << 10) |
+ ((temp & 0x7fffff) >> 13);
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float UnpackFloat10ToFloat(uint value)
- {
- uint result =
- ((((value >> 5) & 0x1f) - 10 + 127) << 23) |
- ((value & 0x1f) << 18);
- return Unsafe.As(ref result);
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float UnpackFloat10ToFloat(uint value)
+ {
+ uint result =
+ ((((value >> 5) & 0x1f) - 10 + 127) << 23) |
+ ((value & 0x1f) << 18);
+ return Unsafe.As(ref result);
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static uint PackFloatToFloat10(float value)
- {
- uint temp = Unsafe.As(ref value);
- return
- ((((temp >> 23) & 0xff) - 127 + 10) << 5) |
- ((temp & 0x7fffff) >> 18);
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint PackFloatToFloat10(float value)
+ {
+ uint temp = Unsafe.As(ref value);
+ return
+ ((((temp >> 23) & 0xff) - 127 + 10) << 5) |
+ ((temp & 0x7fffff) >> 18);
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float UnpackFloat11ToFloat(uint value)
- {
- uint result =
- ((((value >> 6) & 0x1f) - 11 + 127) << 23) |
- ((value & 0x3f) << 17);
- return Unsafe.As(ref result);
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float UnpackFloat11ToFloat(uint value)
+ {
+ uint result =
+ ((((value >> 6) & 0x1f) - 11 + 127) << 23) |
+ ((value & 0x3f) << 17);
+ return Unsafe.As(ref result);
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static uint PackFloatToFloat11(float value)
- {
- uint temp = Unsafe.As(ref value);
- return
- ((((temp >> 23) & 0xff) - 127 + 11) << 6) |
- ((temp & 0x7fffff) >> 17);
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint PackFloatToFloat11(float value)
+ {
+ uint temp = Unsafe.As(ref value);
+ return
+ ((((temp >> 23) & 0xff) - 127 + 11) << 6) |
+ ((temp & 0x7fffff) >> 17);
}
}
diff --git a/src/ImageSharp.Textures/Common/Helpers/PixelUtils.cs b/src/ImageSharp.Textures/Common/Helpers/PixelUtils.cs
index ed7907b2..d9c789bb 100644
--- a/src/ImageSharp.Textures/Common/Helpers/PixelUtils.cs
+++ b/src/ImageSharp.Textures/Common/Helpers/PixelUtils.cs
@@ -4,43 +4,42 @@
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
-namespace SixLabors.ImageSharp.Textures.Common.Helpers
+namespace SixLabors.ImageSharp.Textures.Common.Helpers;
+
+///
+/// Provides methods for calculating pixel values.
+///
+internal static class PixelUtils
{
///
- /// Provides methods for calculating pixel values.
+ /// Performs final shifting from a 5bit value to an 8bit one.
///
- internal static class PixelUtils
- {
- ///
- /// Performs final shifting from a 5bit value to an 8bit one.
- ///
- /// The masked and shifted value.
- /// The
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2));
+ /// The masked and shifted value.
+ /// The
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2));
- ///
- /// Performs final shifting from a 6bit value to an 8bit one.
- ///
- /// The masked and shifted value.
- /// The
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static byte GetBytesFrom6BitValue(int value) => (byte)((value << 2) | (value >> 4));
+ ///
+ /// Performs final shifting from a 6bit value to an 8bit one.
+ ///
+ /// The masked and shifted value.
+ /// The
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static byte GetBytesFrom6BitValue(int value) => (byte)((value << 2) | (value >> 4));
- ///
- /// Extracts the R5G6B5 values from a packed ushort pixel in that order.
- ///
- /// The packed color.
- /// The extracted color.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void ExtractR5G6B5(ushort color, ref Rgb24 dest)
- {
- var r = (color & 0xF800) >> 11;
- var g = (color & 0x7E0) >> 5;
- var b = color & 0x1f;
- dest.R = PixelUtils.GetBytesFrom5BitValue(r);
- dest.G = PixelUtils.GetBytesFrom6BitValue(g);
- dest.B = PixelUtils.GetBytesFrom5BitValue(b);
- }
+ ///
+ /// Extracts the R5G6B5 values from a packed ushort pixel in that order.
+ ///
+ /// The packed color.
+ /// The extracted color.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ExtractR5G6B5(ushort color, ref Rgb24 dest)
+ {
+ int r = (color & 0xF800) >> 11;
+ int g = (color & 0x7E0) >> 5;
+ int b = color & 0x1f;
+ dest.R = PixelUtils.GetBytesFrom5BitValue(r);
+ dest.G = PixelUtils.GetBytesFrom6BitValue(g);
+ dest.B = PixelUtils.GetBytesFrom5BitValue(b);
}
}
diff --git a/src/ImageSharp.Textures/Configuration.cs b/src/ImageSharp.Textures/Configuration.cs
index d68b4726..9e4e9be8 100644
--- a/src/ImageSharp.Textures/Configuration.cs
+++ b/src/ImageSharp.Textures/Configuration.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
-using System.Collections.Generic;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Textures.Formats;
using SixLabors.ImageSharp.Textures.Formats.Dds;
@@ -10,147 +8,146 @@
using SixLabors.ImageSharp.Textures.Formats.Ktx2;
using SixLabors.ImageSharp.Textures.IO;
-namespace SixLabors.ImageSharp.Textures
+namespace SixLabors.ImageSharp.Textures;
+
+///
+/// Provides configuration code which allows altering default behaviour or extending the library.
+///
+public sealed class Configuration
{
///
- /// Provides configuration code which allows altering default behaviour or extending the library.
+ /// A lazily initialized configuration default instance.
///
- public sealed class Configuration
- {
- ///
- /// A lazily initialized configuration default instance.
- ///
- private static readonly Lazy Lazy = new(CreateDefaultInstance);
+ private static readonly Lazy Lazy = new(CreateDefaultInstance);
- private int maxDegreeOfParallelism = Environment.ProcessorCount;
+ private int maxDegreeOfParallelism = Environment.ProcessorCount;
- private MemoryAllocator memoryAllocator = MemoryAllocator.Default;
+ private MemoryAllocator memoryAllocator = MemoryAllocator.Default;
- ///
- /// Initializes a new instance of the class.
- ///
- public Configuration()
- {
- }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Configuration()
+ {
+ }
- ///
- /// Initializes a new instance of the class.
- ///
- /// A collection of configuration modules to register
- public Configuration(params IConfigurationModule[] configurationModules)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A collection of configuration modules to register
+ public Configuration(params IConfigurationModule[] configurationModules)
+ {
+ if (configurationModules != null)
{
- if (configurationModules != null)
+ foreach (IConfigurationModule p in configurationModules)
{
- foreach (IConfigurationModule p in configurationModules)
- {
- p.Configure(this);
- }
+ p.Configure(this);
}
}
+ }
- ///
- /// Gets the default instance.
- ///
- public static Configuration Default { get; } = Lazy.Value;
-
- ///
- /// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms
- /// configured with this instance.
- /// Initialized with by default.
- ///
- public int MaxDegreeOfParallelism
- {
- get => this.maxDegreeOfParallelism;
- set
- {
- if (value == 0 || value < -1)
- {
- throw new ArgumentOutOfRangeException(nameof(this.MaxDegreeOfParallelism));
- }
-
- this.maxDegreeOfParallelism = value;
- }
- }
+ ///
+ /// Gets the default instance.
+ ///
+ public static Configuration Default { get; } = Lazy.Value;
- ///
- /// Gets the currently registered s.
- ///
- public IEnumerable ImageFormats => this.ImageFormatsManager.ImageFormats;
-
- ///
- /// Gets or sets the position in a stream to use for reading when using a seekable stream as an image data source.
- ///
- public ReadOrigin ReadOrigin { get; set; } = ReadOrigin.Current;
-
- ///
- /// Gets or sets the that is currently in use.
- ///
- public TextureFormatManager ImageFormatsManager { get; set; } = new();
-
- ///
- /// Gets or sets the that is currently in use.
- ///
- public MemoryAllocator MemoryAllocator
+ ///
+ /// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms
+ /// configured with this instance.
+ /// Initialized with by default.
+ ///
+ public int MaxDegreeOfParallelism
+ {
+ get => this.maxDegreeOfParallelism;
+ set
{
- get => this.memoryAllocator;
- set
+ if (value is 0 or < (-1))
{
- Guard.NotNull(value, nameof(this.MemoryAllocator));
- this.memoryAllocator = value;
+ throw new ArgumentOutOfRangeException(nameof(this.MaxDegreeOfParallelism));
}
+
+ this.maxDegreeOfParallelism = value;
}
+ }
- ///
- /// Gets the maximum header size of all the formats.
- ///
- internal int MaxHeaderSize => this.ImageFormatsManager.MaxHeaderSize;
-
- ///
- /// Gets or sets the filesystem helper for accessing the local file system.
- ///
- internal IFileSystem FileSystem { get; set; } = new LocalFileSystem();
-
- ///
- /// Gets or sets the working buffer size hint for image processors.
- /// The default value is 1MB.
- ///
- ///
- /// Currently only used by Resize.
- ///
- internal int WorkingBufferSizeHintInBytes { get; set; } = 1 * 1024 * 1024;
-
- ///
- /// Registers a new format provider.
- ///
- /// The configuration provider to call configure on.
- public void Configure(IConfigurationModule configuration)
+ ///
+ /// Gets the currently registered s.
+ ///
+ public IEnumerable ImageFormats => this.ImageFormatsManager.ImageFormats;
+
+ ///
+ /// Gets or sets the position in a stream to use for reading when using a seekable stream as an image data source.
+ ///
+ public ReadOrigin ReadOrigin { get; set; } = ReadOrigin.Current;
+
+ ///
+ /// Gets or sets the that is currently in use.
+ ///
+ public TextureFormatManager ImageFormatsManager { get; set; } = new();
+
+ ///
+ /// Gets or sets the that is currently in use.
+ ///
+ public MemoryAllocator MemoryAllocator
+ {
+ get => this.memoryAllocator;
+ set
{
- Guard.NotNull(configuration, nameof(configuration));
- configuration.Configure(this);
+ Guard.NotNull(value, nameof(this.MemoryAllocator));
+ this.memoryAllocator = value;
}
+ }
- ///
- /// Creates a shallow copy of the .
- ///
- /// A new configuration instance.
- public Configuration Clone() => new()
- {
- MaxDegreeOfParallelism = this.MaxDegreeOfParallelism,
- ImageFormatsManager = this.ImageFormatsManager,
- MemoryAllocator = this.MemoryAllocator,
- ReadOrigin = this.ReadOrigin,
- FileSystem = this.FileSystem,
- WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInBytes,
- };
-
- ///
- /// Creates the default instance with the following s preregistered:
- ///
- ///
- /// The default configuration of .
- internal static Configuration CreateDefaultInstance() => new(
- new DdsConfigurationModule(),
- new KtxConfigurationModule(),
- new Ktx2ConfigurationModule());
+ ///
+ /// Gets the maximum header size of all the formats.
+ ///
+ internal int MaxHeaderSize => this.ImageFormatsManager.MaxHeaderSize;
+
+ ///
+ /// Gets or sets the filesystem helper for accessing the local file system.
+ ///
+ internal IFileSystem FileSystem { get; set; } = new LocalFileSystem();
+
+ ///
+ /// Gets or sets the working buffer size hint for image processors.
+ /// The default value is 1MB.
+ ///
+ ///
+ /// Currently only used by Resize.
+ ///
+ internal int WorkingBufferSizeHintInBytes { get; set; } = 1 * 1024 * 1024;
+
+ ///
+ /// Registers a new format provider.
+ ///
+ /// The configuration provider to call configure on.
+ public void Configure(IConfigurationModule configuration)
+ {
+ Guard.NotNull(configuration, nameof(configuration));
+ configuration.Configure(this);
}
+
+ ///
+ /// Creates a shallow copy of the .
+ ///
+ /// A new configuration instance.
+ public Configuration Clone() => new()
+ {
+ MaxDegreeOfParallelism = this.MaxDegreeOfParallelism,
+ ImageFormatsManager = this.ImageFormatsManager,
+ MemoryAllocator = this.MemoryAllocator,
+ ReadOrigin = this.ReadOrigin,
+ FileSystem = this.FileSystem,
+ WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInBytes,
+ };
+
+ ///
+ /// Creates the default instance with the following s preregistered:
+ ///
+ ///
+ /// The default configuration of .
+ internal static Configuration CreateDefaultInstance() => new(
+ new DdsConfigurationModule(),
+ new KtxConfigurationModule(),
+ new Ktx2ConfigurationModule());
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsConfigurationModule.cs b/src/ImageSharp.Textures/Formats/Dds/DdsConfigurationModule.cs
index c81d20b7..88085655 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsConfigurationModule.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsConfigurationModule.cs
@@ -1,18 +1,17 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
+
+///
+/// Registers the image encoders, decoders and mime type detectors for texture formats.
+///
+public sealed class DdsConfigurationModule : IConfigurationModule
{
- ///
- /// Registers the image encoders, decoders and mime type detectors for texture formats.
- ///
- public sealed class DdsConfigurationModule : IConfigurationModule
+ ///
+ public void Configure(Configuration configuration)
{
- ///
- public void Configure(Configuration configuration)
- {
- configuration.ImageFormatsManager.SetDecoder(DdsFormat.Instance, new DdsDecoder());
- configuration.ImageFormatsManager.AddImageFormatDetector(new DdsImageFormatDetector());
- }
+ configuration.ImageFormatsManager.SetDecoder(DdsFormat.Instance, new DdsDecoder());
+ configuration.ImageFormatsManager.AddImageFormatDetector(new DdsImageFormatDetector());
}
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsConstants.cs b/src/ImageSharp.Textures/Formats/Dds/DdsConstants.cs
index 0caac846..c7ec94dd 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsConstants.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsConstants.cs
@@ -1,35 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Collections.Generic;
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+internal static class DdsConstants
{
- internal static class DdsConstants
- {
- ///
- /// The list of mimetypes that equate to a dds file.
- ///
- public static readonly IEnumerable MimeTypes = new[] { "image/vnd.ms-dds" };
+ ///
+ /// The list of mimetypes that equate to a dds file.
+ ///
+ public static readonly IEnumerable MimeTypes = new[] { "image/vnd.ms-dds" };
- ///
- /// The list of file extensions that equate to a dds file.
- ///
- public static readonly IEnumerable FileExtensions = new[] { "dds" };
+ ///
+ /// The list of file extensions that equate to a dds file.
+ ///
+ public static readonly IEnumerable FileExtensions = new[] { "dds" };
- ///
- /// The dds header size in bytes.
- ///
- public const int DdsHeaderSize = 124;
+ ///
+ /// The dds header size in bytes.
+ ///
+ public const int DdsHeaderSize = 124;
- ///
- /// The dds pixel format size in bytes.
- ///
- public const int DdsPixelFormatSize = 32;
+ ///
+ /// The dds pixel format size in bytes.
+ ///
+ public const int DdsPixelFormatSize = 32;
- ///
- /// The dds dxt10 header size in bytes.
- ///
- public const int DdsDxt10HeaderSize = 20;
- }
+ ///
+ /// The dds dxt10 header size in bytes.
+ ///
+ public const int DdsDxt10HeaderSize = 20;
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsDecoder.cs b/src/ImageSharp.Textures/Formats/Dds/DdsDecoder.cs
index 6291bf5d..c4983089 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsDecoder.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsDecoder.cs
@@ -1,29 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.IO;
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+///
+/// Image decoder for DDS textures.
+///
+public sealed class DdsDecoder : ITextureDecoder, IDdsDecoderOptions, ITextureInfoDetector
{
- ///
- /// Image decoder for DDS textures.
- ///
- public sealed class DdsDecoder : ITextureDecoder, IDdsDecoderOptions, ITextureInfoDetector
+ ///
+ public Texture DecodeTexture(Configuration configuration, Stream stream)
{
- ///
- public Texture DecodeTexture(Configuration configuration, Stream stream)
- {
- Guard.NotNull(stream, nameof(stream));
+ Guard.NotNull(stream, nameof(stream));
- return new DdsDecoderCore(configuration, this).DecodeTexture(stream);
- }
+ return new DdsDecoderCore(configuration, this).DecodeTexture(stream);
+ }
- ///
- public ITextureInfo Identify(Configuration configuration, Stream stream)
- {
- Guard.NotNull(stream, nameof(stream));
+ ///
+ public ITextureInfo Identify(Configuration configuration, Stream stream)
+ {
+ Guard.NotNull(stream, nameof(stream));
- return new DdsDecoderCore(configuration, this).Identify(stream);
- }
+ return new DdsDecoderCore(configuration, this).Identify(stream);
}
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsDecoderCore.cs b/src/ImageSharp.Textures/Formats/Dds/DdsDecoderCore.cs
index 1bb1d0b5..f99bde8e 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsDecoderCore.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsDecoderCore.cs
@@ -1,9 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
using System.Buffers.Binary;
-using System.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Textures.Common.Exceptions;
using SixLabors.ImageSharp.Textures.Common.Extensions;
@@ -12,199 +10,198 @@
using SixLabors.ImageSharp.Textures.Formats.Dds.Processing;
using SixLabors.ImageSharp.Textures.TextureFormats;
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
+
+///
+/// Performs the dds decoding operation.
+///
+internal sealed class DdsDecoderCore
{
///
- /// Performs the dds decoding operation.
+ /// The file header containing general information about the texture.
+ ///
+ private DdsHeader ddsHeader;
+
+ ///
+ /// The dxt10 header if available
///
- internal sealed class DdsDecoderCore
+ private DdsHeaderDxt10 ddsDxt10header;
+
+ ///
+ /// The global configuration.
+ ///
+ private readonly Configuration configuration;
+
+ ///
+ /// Used for allocating memory during processing operations.
+ ///
+ private readonly MemoryAllocator memoryAllocator;
+
+ ///
+ /// The texture decoder options.
+ ///
+ private readonly IDdsDecoderOptions options;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration.
+ /// The options.
+ public DdsDecoderCore(Configuration configuration, IDdsDecoderOptions options)
{
- ///
- /// The file header containing general information about the texture.
- ///
- private DdsHeader ddsHeader;
-
- ///
- /// The dxt10 header if available
- ///
- private DdsHeaderDxt10 ddsDxt10header;
-
- ///
- /// The global configuration.
- ///
- private readonly Configuration configuration;
-
- ///
- /// Used for allocating memory during processing operations.
- ///
- private readonly MemoryAllocator memoryAllocator;
-
- ///
- /// The texture decoder options.
- ///
- private readonly IDdsDecoderOptions options;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The configuration.
- /// The options.
- public DdsDecoderCore(Configuration configuration, IDdsDecoderOptions options)
- {
- this.configuration = configuration;
- this.memoryAllocator = configuration.MemoryAllocator;
- this.options = options;
- }
+ this.configuration = configuration;
+ this.memoryAllocator = configuration.MemoryAllocator;
+ this.options = options;
+ }
- ///
- /// Decodes the texture from the specified stream.
- ///
- /// The stream, where the texture should be decoded from. Cannot be null.
- /// The decoded image.
- public Texture DecodeTexture(Stream stream)
+ ///
+ /// Decodes the texture from the specified stream.
+ ///
+ /// The stream, where the texture should be decoded from. Cannot be null.
+ /// The decoded image.
+ public Texture DecodeTexture(Stream stream)
+ {
+ try
{
- try
- {
- this.ReadFileHeader(stream);
+ this.ReadFileHeader(stream);
- if (this.ddsHeader.Width == 0 || this.ddsHeader.Height == 0)
- {
- throw new UnknownTextureFormatException("Width or height cannot be 0");
- }
+ if (this.ddsHeader.Width == 0 || this.ddsHeader.Height == 0)
+ {
+ throw new UnknownTextureFormatException("Width or height cannot be 0");
+ }
- var ddsProcessor = new DdsProcessor(this.ddsHeader, this.ddsDxt10header);
+ DdsProcessor ddsProcessor = new DdsProcessor(this.ddsHeader, this.ddsDxt10header);
- int width = (int)this.ddsHeader.Width;
- int height = (int)this.ddsHeader.Height;
- int count = this.ddsHeader.TextureCount();
+ int width = (int)this.ddsHeader.Width;
+ int height = (int)this.ddsHeader.Height;
+ int count = this.ddsHeader.TextureCount();
- if (this.ddsHeader.IsVolumeTexture())
- {
- int depths = this.ddsHeader.ComputeDepth();
+ if (this.ddsHeader.IsVolumeTexture())
+ {
+ int depths = this.ddsHeader.ComputeDepth();
- var texture = new VolumeTexture();
- var surfaces = new FlatTexture[depths];
+ VolumeTexture texture = new VolumeTexture();
+ FlatTexture[] surfaces = new FlatTexture[depths];
- for (int i = 0; i < count; i++)
+ for (int i = 0; i < count; i++)
+ {
+ for (int depth = 0; depth < depths; depth++)
{
- for (int depth = 0; depth < depths; depth++)
+ if (i == 0)
{
- if (i == 0)
- {
- surfaces[depth] = new FlatTexture();
- }
-
- MipMap[] mipMaps = ddsProcessor.DecodeDds(stream, width, height, 1);
- surfaces[depth].MipMaps.AddRange(mipMaps);
+ surfaces[depth] = new FlatTexture();
}
- depths >>= 1;
- width >>= 1;
- height >>= 1;
+ MipMap[] mipMaps = ddsProcessor.DecodeDds(stream, width, height, 1);
+ surfaces[depth].MipMaps.AddRange(mipMaps);
}
- texture.Slices.AddRange(surfaces);
- return texture;
+ depths >>= 1;
+ width >>= 1;
+ height >>= 1;
}
- else if (this.ddsHeader.IsCubemap())
- {
- DdsSurfaceType[] faces = this.ddsHeader.GetExistingCubemapFaces() ?? Array.Empty();
- var texture = new CubemapTexture();
- for (int face = 0; face < faces.Length; face++)
- {
- MipMap[] mipMaps = ddsProcessor.DecodeDds(stream, width, height, count);
- if (faces[face] == DdsSurfaceType.CubemapPositiveX)
- {
- texture.PositiveX.MipMaps.AddRange(mipMaps);
- }
+ texture.Slices.AddRange(surfaces);
+ return texture;
+ }
+ else if (this.ddsHeader.IsCubemap())
+ {
+ DdsSurfaceType[] faces = this.ddsHeader.GetExistingCubemapFaces() ?? Array.Empty();
- if (faces[face] == DdsSurfaceType.CubemapNegativeX)
- {
- texture.NegativeX.MipMaps.AddRange(mipMaps);
- }
+ CubemapTexture texture = new CubemapTexture();
+ for (int face = 0; face < faces.Length; face++)
+ {
+ MipMap[] mipMaps = ddsProcessor.DecodeDds(stream, width, height, count);
+ if (faces[face] == DdsSurfaceType.CubemapPositiveX)
+ {
+ texture.PositiveX.MipMaps.AddRange(mipMaps);
+ }
- if (faces[face] == DdsSurfaceType.CubemapPositiveY)
- {
- texture.PositiveY.MipMaps.AddRange(mipMaps);
- }
+ if (faces[face] == DdsSurfaceType.CubemapNegativeX)
+ {
+ texture.NegativeX.MipMaps.AddRange(mipMaps);
+ }
- if (faces[face] == DdsSurfaceType.CubemapNegativeY)
- {
- texture.NegativeY.MipMaps.AddRange(mipMaps);
- }
+ if (faces[face] == DdsSurfaceType.CubemapPositiveY)
+ {
+ texture.PositiveY.MipMaps.AddRange(mipMaps);
+ }
- if (faces[face] == DdsSurfaceType.CubemapPositiveZ)
- {
- texture.PositiveZ.MipMaps.AddRange(mipMaps);
- }
+ if (faces[face] == DdsSurfaceType.CubemapNegativeY)
+ {
+ texture.NegativeY.MipMaps.AddRange(mipMaps);
+ }
- if (faces[face] == DdsSurfaceType.CubemapNegativeZ)
- {
- texture.NegativeZ.MipMaps.AddRange(mipMaps);
- }
+ if (faces[face] == DdsSurfaceType.CubemapPositiveZ)
+ {
+ texture.PositiveZ.MipMaps.AddRange(mipMaps);
}
- return texture;
- }
- else
- {
- var texture = new FlatTexture();
- MipMap[] mipMaps = ddsProcessor.DecodeDds(stream, width, height, count);
- texture.MipMaps.AddRange(mipMaps);
- return texture;
+ if (faces[face] == DdsSurfaceType.CubemapNegativeZ)
+ {
+ texture.NegativeZ.MipMaps.AddRange(mipMaps);
+ }
}
+
+ return texture;
}
- catch (IndexOutOfRangeException e)
+ else
{
- throw new TextureFormatException("Dds image does not have a valid format.", e);
+ FlatTexture texture = new FlatTexture();
+ MipMap[] mipMaps = ddsProcessor.DecodeDds(stream, width, height, count);
+ texture.MipMaps.AddRange(mipMaps);
+ return texture;
}
}
-
- ///
- /// Reads the raw image information from the specified stream.
- ///
- /// The containing image data.
- public ITextureInfo Identify(Stream stream)
+ catch (IndexOutOfRangeException e)
{
- this.ReadFileHeader(stream);
+ throw new TextureFormatException("Dds image does not have a valid format.", e);
+ }
+ }
- D3dFormat d3dFormat = this.ddsHeader.PixelFormat.GetD3DFormat();
- DxgiFormat dxgiFormat = this.ddsHeader.ShouldHaveDxt10Header() ? this.ddsDxt10header.DxgiFormat : DxgiFormat.Unknown;
- int bitsPerPixel = DdsTools.GetBitsPerPixel(d3dFormat, dxgiFormat);
+ ///
+ /// Reads the raw image information from the specified stream.
+ ///
+ /// The containing image data.
+ public ITextureInfo Identify(Stream stream)
+ {
+ this.ReadFileHeader(stream);
- return new TextureInfo(
- new TextureTypeInfo(bitsPerPixel),
- (int)this.ddsHeader.Width,
- (int)this.ddsHeader.Height);
- }
+ D3dFormat d3dFormat = this.ddsHeader.PixelFormat.GetD3DFormat();
+ DxgiFormat dxgiFormat = this.ddsHeader.ShouldHaveDxt10Header() ? this.ddsDxt10header.DxgiFormat : DxgiFormat.Unknown;
+ int bitsPerPixel = DdsTools.GetBitsPerPixel(d3dFormat, dxgiFormat);
- ///
- /// Reads the dds file header from the stream.
- ///
- /// The containing texture data.
- private void ReadFileHeader(Stream stream)
+ return new TextureInfo(
+ new TextureTypeInfo(bitsPerPixel),
+ (int)this.ddsHeader.Width,
+ (int)this.ddsHeader.Height);
+ }
+
+ ///
+ /// Reads the dds file header from the stream.
+ ///
+ /// The containing texture data.
+ private void ReadFileHeader(Stream stream)
+ {
+ Span magicBuffer = stackalloc byte[4];
+ stream.Read(magicBuffer, 0, 4);
+ uint magicValue = BinaryPrimitives.ReadUInt32LittleEndian(magicBuffer);
+ if (magicValue != DdsFourCc.DdsMagicWord)
{
- Span magicBuffer = stackalloc byte[4];
- stream.Read(magicBuffer, 0, 4);
- uint magicValue = BinaryPrimitives.ReadUInt32LittleEndian(magicBuffer);
- if (magicValue != DdsFourCc.DdsMagicWord)
- {
- throw new NotSupportedException("Invalid DDS magic value.");
- }
+ throw new NotSupportedException("Invalid DDS magic value.");
+ }
- byte[] ddsHeaderBuffer = new byte[DdsConstants.DdsHeaderSize];
+ byte[] ddsHeaderBuffer = new byte[DdsConstants.DdsHeaderSize];
- stream.Read(ddsHeaderBuffer, 0, DdsConstants.DdsHeaderSize);
- this.ddsHeader = DdsHeader.Parse(ddsHeaderBuffer);
- this.ddsHeader.Validate();
+ stream.Read(ddsHeaderBuffer, 0, DdsConstants.DdsHeaderSize);
+ this.ddsHeader = DdsHeader.Parse(ddsHeaderBuffer);
+ this.ddsHeader.Validate();
- if (this.ddsHeader.ShouldHaveDxt10Header())
- {
- byte[] ddsDxt10headerBuffer = new byte[DdsConstants.DdsDxt10HeaderSize];
- stream.Read(ddsDxt10headerBuffer, 0, DdsConstants.DdsDxt10HeaderSize);
- this.ddsDxt10header = DdsHeaderDxt10.Parse(ddsDxt10headerBuffer);
- }
+ if (this.ddsHeader.ShouldHaveDxt10Header())
+ {
+ byte[] ddsDxt10headerBuffer = new byte[DdsConstants.DdsDxt10HeaderSize];
+ stream.Read(ddsDxt10headerBuffer, 0, DdsConstants.DdsDxt10HeaderSize);
+ this.ddsDxt10header = DdsHeaderDxt10.Parse(ddsDxt10headerBuffer);
}
}
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsFormat.cs b/src/ImageSharp.Textures/Formats/Dds/DdsFormat.cs
index 7028ce09..b19c0ab0 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsFormat.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsFormat.cs
@@ -1,37 +1,34 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Collections.Generic;
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+///
+/// Registers the texture decoders and mime type detectors for the dds format.
+///
+public sealed class DdsFormat : ITextureFormat
{
///
- /// Registers the texture decoders and mime type detectors for the dds format.
+ /// Prevents a default instance of the class from being created.
///
- public sealed class DdsFormat : ITextureFormat
+ private DdsFormat()
{
- ///
- /// Prevents a default instance of the class from being created.
- ///
- private DdsFormat()
- {
- }
+ }
- ///
- /// Gets the current instance.
- ///
- public static DdsFormat Instance { get; } = new DdsFormat();
+ ///
+ /// Gets the current instance.
+ ///
+ public static DdsFormat Instance { get; } = new DdsFormat();
- ///
- public string Name => "DDS";
+ ///
+ public string Name => "DDS";
- ///
- public string DefaultMimeType => "image/vnd.ms-dds";
+ ///
+ public string DefaultMimeType => "image/vnd.ms-dds";
- ///
- public IEnumerable MimeTypes => DdsConstants.MimeTypes;
+ ///
+ public IEnumerable MimeTypes => DdsConstants.MimeTypes;
- ///
- public IEnumerable FileExtensions => DdsConstants.FileExtensions;
- }
+ ///
+ public IEnumerable FileExtensions => DdsConstants.FileExtensions;
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsFourCC.cs b/src/ImageSharp.Textures/Formats/Dds/DdsFourCC.cs
index 2f922330..e549205b 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsFourCC.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsFourCC.cs
@@ -2,73 +2,72 @@
// Licensed under the Six Labors Split License.
// ReSharper disable InconsistentNaming
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
+
+///
+/// Four character codes constants used in DDS files.
+///
+internal static class DdsFourCc
{
- ///
- /// Four character codes constants used in DDS files.
- ///
- internal static class DdsFourCc
- {
- public const uint DdsMagicWord = 'D' | ('D' << 8) | ('S' << 16) | (' ' << 24);
+ public const uint DdsMagicWord = 'D' | ('D' << 8) | ('S' << 16) | (' ' << 24);
- public const uint None = 0;
+ public const uint None = 0;
- public const uint DX10 = 'D' | ('X' << 8) | ('1' << 16) | ('0' << 24);
+ public const uint DX10 = 'D' | ('X' << 8) | ('1' << 16) | ('0' << 24);
- public const uint UYVY = 'U' | ('Y' << 8) | ('V' << 16) | ('Y' << 24);
+ public const uint UYVY = 'U' | ('Y' << 8) | ('V' << 16) | ('Y' << 24);
- public const uint RGBG = 'R' | ('G' << 8) | ('B' << 16) | ('G' << 24);
+ public const uint RGBG = 'R' | ('G' << 8) | ('B' << 16) | ('G' << 24);
- public const uint YUY2 = 'Y' | ('U' << 8) | ('Y' << 16) | ('2' << 24);
+ public const uint YUY2 = 'Y' | ('U' << 8) | ('Y' << 16) | ('2' << 24);
- public const uint GRGB = 'G' | ('R' << 8) | ('G' << 16) | ('B' << 24);
+ public const uint GRGB = 'G' | ('R' << 8) | ('G' << 16) | ('B' << 24);
- public const uint DXT1 = 'D' | ('X' << 8) | ('T' << 16) | ('1' << 24);
+ public const uint DXT1 = 'D' | ('X' << 8) | ('T' << 16) | ('1' << 24);
- public const uint DXT2 = 'D' | ('X' << 8) | ('T' << 16) | ('2' << 24);
+ public const uint DXT2 = 'D' | ('X' << 8) | ('T' << 16) | ('2' << 24);
- public const uint DXT3 = 'D' | ('X' << 8) | ('T' << 16) | ('3' << 24);
+ public const uint DXT3 = 'D' | ('X' << 8) | ('T' << 16) | ('3' << 24);
- public const uint DXT4 = 'D' | ('X' << 8) | ('T' << 16) | ('4' << 24);
+ public const uint DXT4 = 'D' | ('X' << 8) | ('T' << 16) | ('4' << 24);
- public const uint DXT5 = 'D' | ('X' << 8) | ('T' << 16) | ('5' << 24);
+ public const uint DXT5 = 'D' | ('X' << 8) | ('T' << 16) | ('5' << 24);
- public const uint MET1 = 'M' | ('E' << 8) | ('T' << 16) | ('1' << 24);
+ public const uint MET1 = 'M' | ('E' << 8) | ('T' << 16) | ('1' << 24);
- public const uint BC4U = 'B' | ('C' << 8) | ('4' << 16) | ('U' << 24);
+ public const uint BC4U = 'B' | ('C' << 8) | ('4' << 16) | ('U' << 24);
- public const uint BC4S = 'B' | ('C' << 8) | ('4' << 16) | ('S' << 24);
+ public const uint BC4S = 'B' | ('C' << 8) | ('4' << 16) | ('S' << 24);
- public const uint BC5U = 'B' | ('C' << 8) | ('5' << 16) | ('U' << 24);
+ public const uint BC5U = 'B' | ('C' << 8) | ('5' << 16) | ('U' << 24);
- public const uint BC5S = 'B' | ('C' << 8) | ('5' << 16) | ('S' << 24);
+ public const uint BC5S = 'B' | ('C' << 8) | ('5' << 16) | ('S' << 24);
- public const uint ATI1 = 'A' | ('T' << 8) | ('I' << 16) | ('1' << 24);
+ public const uint ATI1 = 'A' | ('T' << 8) | ('I' << 16) | ('1' << 24);
- public const uint ATI2 = 'A' | ('T' << 8) | ('I' << 16) | ('2' << 24);
+ public const uint ATI2 = 'A' | ('T' << 8) | ('I' << 16) | ('2' << 24);
- // DXGI_FORMAT_R16G16B16A16_UNORM
- public const uint R16G16B16A16UNORM = 36;
+ // DXGI_FORMAT_R16G16B16A16_UNORM
+ public const uint R16G16B16A16UNORM = 36;
- // DXGI_FORMAT_R16G16B16A16_SNORM
- public const uint R16G16B16A16SNORM = 110;
+ // DXGI_FORMAT_R16G16B16A16_SNORM
+ public const uint R16G16B16A16SNORM = 110;
- // DXGI_FORMAT_R16_FLOAT
- public const uint R16FLOAT = 111;
+ // DXGI_FORMAT_R16_FLOAT
+ public const uint R16FLOAT = 111;
- // DXGI_FORMAT_R16G16_FLOAT
- public const uint R16G16FLOAT = 112;
+ // DXGI_FORMAT_R16G16_FLOAT
+ public const uint R16G16FLOAT = 112;
- // D3DFMT_A16B16G16R16F
- public const uint R16G16B16A16FLOAT = 113;
+ // D3DFMT_A16B16G16R16F
+ public const uint R16G16B16A16FLOAT = 113;
- // DXGI_FORMAT_R32_FLOAT
- public const uint R32FLOAT = 114;
+ // DXGI_FORMAT_R32_FLOAT
+ public const uint R32FLOAT = 114;
- // DXGI_FORMAT_R32G32_FLOAT
- public const uint R32G32FLOAT = 115;
+ // DXGI_FORMAT_R32G32_FLOAT
+ public const uint R32G32FLOAT = 115;
- // DXGI_FORMAT_R32G32B32A32_FLOAT
- public const uint R32G32B32A32FLOAT = 116;
- }
+ // DXGI_FORMAT_R32G32B32A32_FLOAT
+ public const uint R32G32B32A32FLOAT = 116;
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsHeader.cs b/src/ImageSharp.Textures/Formats/Dds/DdsHeader.cs
index 27e1271e..4a4280ba 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsHeader.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsHeader.cs
@@ -1,236 +1,234 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Textures.Formats.Dds.Emums;
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
+
+///
+/// Describes a DDS file header.
+///
+///
+/// Note When you write .dds files, you should set the and
+/// flags, and for mipmapped textures you should also set the
+/// flag.
+/// However, when you read a .dds file, you should not rely on the ,
+/// , and
+/// flags being set because some writers of such a file might not set these flags.
+/// Include flags in for the members of the structure that contain valid data. Use this
+/// structure in combination with a to store a resource array in a DDS file.
+/// For more information, see texture arrays.
+/// is identical to the DirectDraw DDSURFACEDESC2 structure without DirectDraw
+/// dependencies.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+internal struct DdsHeader
{
+ public DdsHeader(
+ uint size,
+ DdsFlags flags,
+ uint height,
+ uint width,
+ uint pitchOrLinearSize,
+ uint depth,
+ uint mipMapCount,
+ uint[] reserved1,
+ DdsPixelFormat pixelFormat,
+ DdsCaps1 caps1,
+ DdsCaps2 caps2,
+ uint caps3,
+ uint caps4,
+ uint reserved2)
+ {
+ this.Size = size;
+ this.Flags = flags;
+ this.Height = height;
+ this.Width = width;
+ this.PitchOrLinearSize = pitchOrLinearSize;
+ this.Depth = depth;
+ this.MipMapCount = mipMapCount;
+ this.Reserved1 = reserved1;
+ this.PixelFormat = pixelFormat;
+ this.Caps1 = caps1;
+ this.Caps2 = caps2;
+ this.Caps3 = caps3;
+ this.Caps4 = caps4;
+ this.Reserved2 = reserved2;
+ }
+
///
- /// Describes a DDS file header.
+ /// Gets size of structure. This member must be set to 124.
+ ///
+ public uint Size { get; }
+
+ ///
+ /// Gets flags to indicate which members contain valid data.
///
///
- /// Note When you write .dds files, you should set the and
- /// flags, and for mipmapped textures you should also set the
- /// flag.
+ /// When you write .dds files, you should set the and
+ /// flags,
+ /// and for mipmapped textures you should also set the flag.
/// However, when you read a .dds file, you should not rely on the ,
/// , and
/// flags being set because some writers of such a file might not set these flags.
- /// Include flags in for the members of the structure that contain valid data. Use this
- /// structure in combination with a to store a resource array in a DDS file.
- /// For more information, see texture arrays.
- /// is identical to the DirectDraw DDSURFACEDESC2 structure without DirectDraw
- /// dependencies.
///
- [StructLayout(LayoutKind.Sequential, Pack = 1)]
- internal struct DdsHeader
+ public DdsFlags Flags { get; }
+
+ ///
+ /// Gets surface height (in pixels).
+ ///
+ public uint Height { get; }
+
+ ///
+ /// Gets surface width (in pixels).
+ ///
+ public uint Width { get; }
+
+ ///
+ /// Gets the pitch or number of bytes per scan line in an uncompressed texture;
+ /// the total number of bytes in the top level texture for a compressed texture.
+ ///
+ public uint PitchOrLinearSize { get; }
+
+ ///
+ /// Gets depth of a volume texture (in pixels), otherwise unused.
+ ///
+ public uint Depth { get; }
+
+ ///
+ /// Gets number of mipmap levels, otherwise unused.
+ ///
+ public uint MipMapCount { get; }
+
+ ///
+ /// Gets unused.
+ ///
+ public uint[] Reserved1 { get; }
+
+ ///
+ /// Gets the pixel format.
+ ///
+ public DdsPixelFormat PixelFormat { get; }
+
+ ///
+ /// Gets specifies the complexity of the surfaces stored.
+ ///
+ ///
+ /// When you write .dds files, you should set the flag,
+ /// and for multiple surfaces you should also set the flag.
+ /// However, when you read a .dds file, you should not rely on the and
+ /// flags being set because some file writers might not set these flags.
+ ///
+ public DdsCaps1 Caps1 { get; }
+
+ ///
+ /// Gets defines additional capabilities of the surface.
+ ///
+ public DdsCaps2 Caps2 { get; }
+
+ ///
+ /// Gets unused.
+ ///
+ public uint Caps3 { get; }
+
+ ///
+ /// Gets unused.
+ ///
+ public uint Caps4 { get; }
+
+ ///
+ /// Gets unused.
+ ///
+ public uint Reserved2 { get; }
+
+ ///
+ /// Validates the dds header.
+ ///
+ ///
+ /// Thrown if the image does not pass validation.
+ ///
+ public readonly void Validate()
{
- public DdsHeader(
- uint size,
- DdsFlags flags,
- uint height,
- uint width,
- uint pitchOrLinearSize,
- uint depth,
- uint mipMapCount,
- uint[] reserved1,
- DdsPixelFormat pixelFormat,
- DdsCaps1 caps1,
- DdsCaps2 caps2,
- uint caps3,
- uint caps4,
- uint reserved2)
+ bool incorrectSize = (this.Size != DdsConstants.DdsHeaderSize) || (this.PixelFormat.Size != DdsConstants.DdsPixelFormatSize);
+ if (incorrectSize)
{
- this.Size = size;
- this.Flags = flags;
- this.Height = height;
- this.Width = width;
- this.PitchOrLinearSize = pitchOrLinearSize;
- this.Depth = depth;
- this.MipMapCount = mipMapCount;
- this.Reserved1 = reserved1;
- this.PixelFormat = pixelFormat;
- this.Caps1 = caps1;
- this.Caps2 = caps2;
- this.Caps3 = caps3;
- this.Caps4 = caps4;
- this.Reserved2 = reserved2;
+ throw new NotSupportedException("Invalid structure size.");
}
- ///
- /// Gets size of structure. This member must be set to 124.
- ///
- public uint Size { get; }
-
- ///
- /// Gets flags to indicate which members contain valid data.
- ///
- ///
- /// When you write .dds files, you should set the and
- /// flags,
- /// and for mipmapped textures you should also set the flag.
- /// However, when you read a .dds file, you should not rely on the ,
- /// , and
- /// flags being set because some writers of such a file might not set these flags.
- ///
- public DdsFlags Flags { get; }
-
- ///
- /// Gets surface height (in pixels).
- ///
- public uint Height { get; }
-
- ///
- /// Gets surface width (in pixels).
- ///
- public uint Width { get; }
-
- ///
- /// Gets the pitch or number of bytes per scan line in an uncompressed texture;
- /// the total number of bytes in the top level texture for a compressed texture.
- ///
- public uint PitchOrLinearSize { get; }
-
- ///
- /// Gets depth of a volume texture (in pixels), otherwise unused.
- ///
- public uint Depth { get; }
-
- ///
- /// Gets number of mipmap levels, otherwise unused.
- ///
- public uint MipMapCount { get; }
-
- ///
- /// Gets unused.
- ///
- public uint[] Reserved1 { get; }
-
- ///
- /// Gets the pixel format.
- ///
- public DdsPixelFormat PixelFormat { get; }
-
- ///
- /// Gets specifies the complexity of the surfaces stored.
- ///
- ///
- /// When you write .dds files, you should set the flag,
- /// and for multiple surfaces you should also set the flag.
- /// However, when you read a .dds file, you should not rely on the and
- /// flags being set because some file writers might not set these flags.
- ///
- public DdsCaps1 Caps1 { get; }
-
- ///
- /// Gets defines additional capabilities of the surface.
- ///
- public DdsCaps2 Caps2 { get; }
-
- ///
- /// Gets unused.
- ///
- public uint Caps3 { get; }
-
- ///
- /// Gets unused.
- ///
- public uint Caps4 { get; }
-
- ///
- /// Gets unused.
- ///
- public uint Reserved2 { get; }
-
- ///
- /// Validates the dds header.
- ///
- ///
- /// Thrown if the image does not pass validation.
- ///
- public void Validate()
+ bool requiredFlagsMissing = (this.Flags & DdsFlags.Caps) == 0 || (this.Flags & DdsFlags.PixelFormat) == 0 || (this.Caps1 & DdsCaps1.Texture) == 0;
+ if (requiredFlagsMissing)
{
- bool incorrectSize = (this.Size != DdsConstants.DdsHeaderSize) || (this.PixelFormat.Size != DdsConstants.DdsPixelFormatSize);
- if (incorrectSize)
- {
- throw new NotSupportedException("Invalid structure size.");
- }
-
- bool requiredFlagsMissing = (this.Flags & DdsFlags.Caps) == 0 || (this.Flags & DdsFlags.PixelFormat) == 0 || (this.Caps1 & DdsCaps1.Texture) == 0;
- if (requiredFlagsMissing)
- {
- throw new NotSupportedException("Required flags missing.");
- }
-
- bool hasInvalidCompression = (this.Flags & DdsFlags.Pitch) != 0 && (this.Flags & DdsFlags.LinearSize) != 0;
- if (hasInvalidCompression)
- {
- throw new NotSupportedException("Invalid compression.");
- }
+ throw new NotSupportedException("Required flags missing.");
}
- public void WriteTo(Span buffer)
+ bool hasInvalidCompression = (this.Flags & DdsFlags.Pitch) != 0 && (this.Flags & DdsFlags.LinearSize) != 0;
+ if (hasInvalidCompression)
{
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(0, 4), this.Size);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(4, 4), (uint)this.Flags);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(8, 4), this.Height);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(12, 4), this.Width);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(16, 4), this.PitchOrLinearSize);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(20, 4), this.Depth);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(24, 4), this.MipMapCount);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(28, 4), this.Reserved1[0]);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(32, 4), this.Reserved1[1]);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(36, 4), this.Reserved1[2]);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(40, 4), this.Reserved1[3]);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(44, 4), this.Reserved1[4]);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(48, 4), this.Reserved1[5]);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(52, 4), this.Reserved1[6]);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(56, 4), this.Reserved1[7]);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(60, 4), this.Reserved1[8]);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(64, 4), this.Reserved1[9]);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(68, 4), this.Reserved1[10]);
- this.PixelFormat.WriteTo(buffer, 72);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(104, 4), (uint)this.Caps1);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(108, 4), (uint)this.Caps2);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(112, 4), this.Caps3);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(116, 4), this.Caps4);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(120, 4), this.Reserved2);
+ throw new NotSupportedException("Invalid compression.");
}
+ }
- public static DdsHeader Parse(Span data)
- {
- uint[] reserved1 =
- {
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(28, 4)),
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(32, 4)),
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(36, 4)),
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(40, 4)),
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(44, 4)),
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(48, 4)),
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(52, 4)),
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(56, 4)),
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(60, 4)),
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(64, 4)),
- BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(68, 4))
- };
-
- return new DdsHeader(
- size: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(0, 4)),
- flags: (DdsFlags)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(4, 4)),
- height: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(8, 4)),
- width: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(12, 4)),
- pitchOrLinearSize: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(16, 4)),
- depth: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(20, 4)),
- mipMapCount: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(24, 4)),
- reserved1: reserved1,
- pixelFormat: DdsPixelFormat.Parse(data, 72),
- caps1: (DdsCaps1)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(104, 4)),
- caps2: (DdsCaps2)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(108, 4)),
- caps3: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(112, 4)),
- caps4: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(116, 4)),
- reserved2: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(120, 4)));
- }
+ public readonly void WriteTo(Span buffer)
+ {
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer[..4], this.Size);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(4, 4), (uint)this.Flags);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(8, 4), this.Height);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(12, 4), this.Width);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(16, 4), this.PitchOrLinearSize);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(20, 4), this.Depth);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(24, 4), this.MipMapCount);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(28, 4), this.Reserved1[0]);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(32, 4), this.Reserved1[1]);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(36, 4), this.Reserved1[2]);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(40, 4), this.Reserved1[3]);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(44, 4), this.Reserved1[4]);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(48, 4), this.Reserved1[5]);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(52, 4), this.Reserved1[6]);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(56, 4), this.Reserved1[7]);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(60, 4), this.Reserved1[8]);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(64, 4), this.Reserved1[9]);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(68, 4), this.Reserved1[10]);
+ this.PixelFormat.WriteTo(buffer, 72);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(104, 4), (uint)this.Caps1);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(108, 4), (uint)this.Caps2);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(112, 4), this.Caps3);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(116, 4), this.Caps4);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(120, 4), this.Reserved2);
+ }
+
+ public static DdsHeader Parse(Span data)
+ {
+ uint[] reserved1 =
+ [
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(28, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(32, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(36, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(40, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(44, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(48, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(52, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(56, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(60, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(64, 4)),
+ BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(68, 4))
+ ];
+
+ return new DdsHeader(
+ size: BinaryPrimitives.ReadUInt32LittleEndian(data[..4]),
+ flags: (DdsFlags)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(4, 4)),
+ height: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(8, 4)),
+ width: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(12, 4)),
+ pitchOrLinearSize: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(16, 4)),
+ depth: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(20, 4)),
+ mipMapCount: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(24, 4)),
+ reserved1: reserved1,
+ pixelFormat: DdsPixelFormat.Parse(data, 72),
+ caps1: (DdsCaps1)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(104, 4)),
+ caps2: (DdsCaps2)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(108, 4)),
+ caps3: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(112, 4)),
+ caps4: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(116, 4)),
+ reserved2: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(120, 4)));
}
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsHeaderDxt10.cs b/src/ImageSharp.Textures/Formats/Dds/DdsHeaderDxt10.cs
index 25494921..c52fa550 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsHeaderDxt10.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsHeaderDxt10.cs
@@ -1,111 +1,109 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Textures.Formats.Dds.Emums;
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
+
+///
+/// DDS header extension to handle resource arrays.
+///
+///
+/// Use this structure together with a to store a resource array in a DDS file.
+/// For more information, see texture arrays.
+/// This header is present if the member of the
+/// structure is set to 'DX10'.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+internal struct DdsHeaderDxt10
{
- ///
- /// DDS header extension to handle resource arrays.
- ///
- ///
- /// Use this structure together with a to store a resource array in a DDS file.
- /// For more information, see texture arrays.
- /// This header is present if the member of the
- /// structure is set to 'DX10'.
- ///
- [StructLayout(LayoutKind.Sequential, Pack = 1)]
- internal struct DdsHeaderDxt10
+ public DdsHeaderDxt10(
+ DxgiFormat dxgiFormat,
+ D3d10ResourceDimension resourceDimension,
+ D3d10ResourceMiscFlags miscFlag,
+ uint arraySize,
+ uint reserved)
{
- public DdsHeaderDxt10(
- DxgiFormat dxgiFormat,
- D3d10ResourceDimension resourceDimension,
- D3d10ResourceMiscFlags miscFlag,
- uint arraySize,
- uint reserved)
- {
- this.DxgiFormat = dxgiFormat;
- this.ResourceDimension = resourceDimension;
- this.MiscFlag = miscFlag;
- this.ArraySize = arraySize;
- this.Reserved = reserved;
- }
-
- ///
- /// Gets the surface pixel format.
- ///
- public DxgiFormat DxgiFormat { get; }
+ this.DxgiFormat = dxgiFormat;
+ this.ResourceDimension = resourceDimension;
+ this.MiscFlag = miscFlag;
+ this.ArraySize = arraySize;
+ this.Reserved = reserved;
+ }
- ///
- /// Gets identifies the type of resource.
- /// The following values for this member are a subset of the values in the
- /// enumeration:
- /// :
- /// Resource is a 1D texture. The member of
- /// specifies the size of the texture. Typically, you set the member of
- /// to 1; you also must set the flag in
- /// the member of .
- /// :
- /// Resource is a 2D texture with an area specified by the and
- /// members of .
- /// You can also use this type to identify a cube-map texture. For more information about how to identify a
- /// cube-map texture, see and members.
- /// :
- /// Resource is a 3D texture with a volume specified by the ,
- /// , and members of
- /// . You also must set the flag
- /// in the member of .
- ///
- public D3d10ResourceDimension ResourceDimension { get; }
+ ///
+ /// Gets the surface pixel format.
+ ///
+ public DxgiFormat DxgiFormat { get; }
- ///
- /// Gets identifies other, less common options for resources. The following value for this member is a
- /// subset of the values in the enumeration:
- /// DDS_RESOURCE_MISC_TEXTURECUBE Indicates a 2D texture is a cube-map texture.
- ///
- public D3d10ResourceMiscFlags MiscFlag { get; }
+ ///
+ /// Gets identifies the type of resource.
+ /// The following values for this member are a subset of the values in the
+ /// enumeration:
+ /// :
+ /// Resource is a 1D texture. The member of
+ /// specifies the size of the texture. Typically, you set the member of
+ /// to 1; you also must set the flag in
+ /// the member of .
+ /// :
+ /// Resource is a 2D texture with an area specified by the and
+ /// members of .
+ /// You can also use this type to identify a cube-map texture. For more information about how to identify a
+ /// cube-map texture, see and members.
+ /// :
+ /// Resource is a 3D texture with a volume specified by the ,
+ /// , and members of
+ /// . You also must set the flag
+ /// in the member of .
+ ///
+ public D3d10ResourceDimension ResourceDimension { get; }
- ///
- /// Gets the number of elements in the array.
- /// For a 2D texture that is also a cube-map texture, this number represents the number of cubes.
- /// This number is the same as the number in the NumCubes member of D3D10_TEXCUBE_ARRAY_SRV1 or
- /// D3D11_TEXCUBE_ARRAY_SRV). In this case, the DDS file contains arraySize*6 2D textures.
- /// For more information about this case, see the description.
- /// For a 3D texture, you must set this number to 1.
- ///
- public uint ArraySize { get; }
+ ///
+ /// Gets identifies other, less common options for resources. The following value for this member is a
+ /// subset of the values in the enumeration:
+ /// DDS_RESOURCE_MISC_TEXTURECUBE Indicates a 2D texture is a cube-map texture.
+ ///
+ public D3d10ResourceMiscFlags MiscFlag { get; }
- ///
- /// Gets reserved for future use.
- ///
- public uint Reserved { get; }
+ ///
+ /// Gets the number of elements in the array.
+ /// For a 2D texture that is also a cube-map texture, this number represents the number of cubes.
+ /// This number is the same as the number in the NumCubes member of D3D10_TEXCUBE_ARRAY_SRV1 or
+ /// D3D11_TEXCUBE_ARRAY_SRV). In this case, the DDS file contains arraySize*6 2D textures.
+ /// For more information about this case, see the description.
+ /// For a 3D texture, you must set this number to 1.
+ ///
+ public uint ArraySize { get; }
- ///
- /// Writes the header to the given buffer.
- ///
- /// The buffer to write to.
- public void WriteTo(Span buffer)
- {
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(0, 4), (uint)this.DxgiFormat);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(4, 4), (uint)this.ResourceDimension);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(8, 4), (uint)this.MiscFlag);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(12, 4), this.ArraySize);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(16, 4), this.Reserved);
- }
+ ///
+ /// Gets reserved for future use.
+ ///
+ public uint Reserved { get; }
- ///
- /// Parses the DDS_HEADER_DXT10 from the given data buffer.
- ///
- /// The data to parse.
- /// The parsed DDS_HEADER_DXT10.
- public static DdsHeaderDxt10 Parse(Span data) => new DdsHeaderDxt10(
- dxgiFormat: (DxgiFormat)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(0, 4)),
- resourceDimension: (D3d10ResourceDimension)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(4, 4)),
- miscFlag: (D3d10ResourceMiscFlags)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(8, 4)),
- arraySize: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(12, 4)),
- reserved: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(16, 4)));
+ ///
+ /// Writes the header to the given buffer.
+ ///
+ /// The buffer to write to.
+ public readonly void WriteTo(Span buffer)
+ {
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer[..4], (uint)this.DxgiFormat);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(4, 4), (uint)this.ResourceDimension);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(8, 4), (uint)this.MiscFlag);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(12, 4), this.ArraySize);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(16, 4), this.Reserved);
}
+
+ ///
+ /// Parses the DDS_HEADER_DXT10 from the given data buffer.
+ ///
+ /// The data to parse.
+ /// The parsed DDS_HEADER_DXT10.
+ public static DdsHeaderDxt10 Parse(Span data) => new(
+ dxgiFormat: (DxgiFormat)BinaryPrimitives.ReadUInt32LittleEndian(data[..4]),
+ resourceDimension: (D3d10ResourceDimension)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(4, 4)),
+ miscFlag: (D3d10ResourceMiscFlags)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(8, 4)),
+ arraySize: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(12, 4)),
+ reserved: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(16, 4)));
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsImageFormatDetector.cs b/src/ImageSharp.Textures/Formats/Dds/DdsImageFormatDetector.cs
index 0a352438..b4248348 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsImageFormatDetector.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsImageFormatDetector.cs
@@ -1,31 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
using System.Buffers.Binary;
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
+
+///
+/// Detects dds file headers.
+///
+public sealed class DdsImageFormatDetector : ITextureFormatDetector
{
- ///
- /// Detects dds file headers.
- ///
- public sealed class DdsImageFormatDetector : ITextureFormatDetector
- {
- ///
- public int HeaderSize => 8;
+ ///
+ public int HeaderSize => 8;
- ///
- public ITextureFormat? DetectFormat(ReadOnlySpan header) => this.IsSupportedFileFormat(header) ? DdsFormat.Instance : null;
+ ///
+ public ITextureFormat? DetectFormat(ReadOnlySpan header) => this.IsSupportedFileFormat(header) ? DdsFormat.Instance : null;
- private bool IsSupportedFileFormat(ReadOnlySpan header)
+ private bool IsSupportedFileFormat(ReadOnlySpan header)
+ {
+ if (header.Length >= this.HeaderSize)
{
- if (header.Length >= this.HeaderSize)
- {
- uint magicValue = BinaryPrimitives.ReadUInt32LittleEndian(header);
- return magicValue == DdsFourCc.DdsMagicWord;
- }
-
- return false;
+ uint magicValue = BinaryPrimitives.ReadUInt32LittleEndian(header);
+ return magicValue == DdsFourCc.DdsMagicWord;
}
+
+ return false;
}
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsPixelFormat.cs b/src/ImageSharp.Textures/Formats/Dds/DdsPixelFormat.cs
index 76c4c46c..4d969a89 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsPixelFormat.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsPixelFormat.cs
@@ -1,320 +1,318 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Textures.Formats.Dds.Emums;
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
+
+/// Surface pixel format.
+///
+/// To store DXGI formats such as floating-point data, use a of
+/// and set to
+/// 'D','X','1','0'. Use the extension header to store the DXGI format in the
+/// member.
+/// Note that there are non-standard variants of DDS files where has
+/// and the value
+/// is set directly to a D3DFORMAT or enumeration value. It is not possible to
+/// disambiguate the D3DFORMAT versus values using this non-standard scheme, so the
+/// DX10 extension header is recommended instead.
+///
+[StructLayout(LayoutKind.Sequential, Size = 32, Pack = 1)]
+internal struct DdsPixelFormat
{
- /// Surface pixel format.
- ///
- /// To store DXGI formats such as floating-point data, use a of
- /// and set to
- /// 'D','X','1','0'. Use the extension header to store the DXGI format in the
- /// member.
- /// Note that there are non-standard variants of DDS files where has
- /// and the value
- /// is set directly to a D3DFORMAT or enumeration value. It is not possible to
- /// disambiguate the D3DFORMAT versus values using this non-standard scheme, so the
- /// DX10 extension header is recommended instead.
- ///
- [StructLayout(LayoutKind.Sequential, Size = 32, Pack = 1)]
- internal struct DdsPixelFormat
+ public DdsPixelFormat(
+ uint size,
+ DdsPixelFormatFlags flags,
+ uint fourCC,
+ uint rgbBitCount,
+ uint rBitMask,
+ uint gBitMask,
+ uint bBitMask,
+ uint aBitMask)
{
- public DdsPixelFormat(
- uint size,
- DdsPixelFormatFlags flags,
- uint fourCC,
- uint rgbBitCount,
- uint rBitMask,
- uint gBitMask,
- uint bBitMask,
- uint aBitMask)
- {
- this.Size = size;
- this.Flags = flags;
- this.FourCC = fourCC;
- this.RGBBitCount = rgbBitCount;
- this.RBitMask = rBitMask;
- this.GBitMask = gBitMask;
- this.BBitMask = bBitMask;
- this.ABitMask = aBitMask;
- }
+ this.Size = size;
+ this.Flags = flags;
+ this.FourCC = fourCC;
+ this.RGBBitCount = rgbBitCount;
+ this.RBitMask = rBitMask;
+ this.GBitMask = gBitMask;
+ this.BBitMask = bBitMask;
+ this.ABitMask = aBitMask;
+ }
- ///
- /// Gets Structure size; set to 32 (bytes).
- ///
- public uint Size { get; }
-
- ///
- /// Gets Values which indicate what type of data is in the surface.
- ///
- public DdsPixelFormatFlags Flags { get; }
-
- ///
- /// Gets Four-character codes for specifying compressed or custom formats.
- /// Possible values include: DXT1, DXT2, DXT3, DXT4, or DXT5. A FOURCC of DX10 indicates the prescense of
- /// the extended header, and the
- /// member of that structure indicates the
- /// true format. When using a four-character code, must include
- /// .
- ///
- public uint FourCC { get; }
-
- ///
- /// Gets Number of bits in an RGB (possibly including alpha) format. Valid when
- /// includes , or ,
- /// or .
- ///
- public uint RGBBitCount { get; }
-
- ///
- /// Gets Red (or lumiannce or Y) mask for reading color data. For instance, given the A8R8G8B8 format,
- /// the red mask would be 0x00ff0000.
- ///
- public uint RBitMask { get; }
-
- ///
- /// Gets Green (or U) mask for reading color data. For instance, given the A8R8G8B8 format,
- /// the green mask would be 0x0000ff00.
- ///
- public uint GBitMask { get; }
-
- ///
- /// Gets Blue (or V) mask for reading color data. For instance, given the A8R8G8B8 format,
- /// the blue mask would be 0x000000ff.
- ///
- public uint BBitMask { get; }
-
- ///
- /// Gets Alpha mask for reading alpha data. dwFlags must include
- /// or .
- /// For instance, given the A8R8G8B8 format, the alpha mask would be 0xff000000.
- ///
- public uint ABitMask { get; }
-
- ///
- /// Writes the header to the given buffer.
- ///
- /// The buffer to write to.
- /// Offset in the buffer.
- public void WriteTo(Span buffer, int offset)
- {
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset, 4), this.Size);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 4, 4), (uint)this.Flags);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 8, 4), this.FourCC);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 12, 4), this.RGBBitCount);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 16, 4), this.RBitMask);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 20, 4), this.GBitMask);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 24, 4), this.BBitMask);
- BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 28, 4), this.ABitMask);
- }
+ ///
+ /// Gets Structure size; set to 32 (bytes).
+ ///
+ public uint Size { get; }
+
+ ///
+ /// Gets Values which indicate what type of data is in the surface.
+ ///
+ public DdsPixelFormatFlags Flags { get; }
+
+ ///
+ /// Gets Four-character codes for specifying compressed or custom formats.
+ /// Possible values include: DXT1, DXT2, DXT3, DXT4, or DXT5. A FOURCC of DX10 indicates the prescense of
+ /// the extended header, and the
+ /// member of that structure indicates the
+ /// true format. When using a four-character code, must include
+ /// .
+ ///
+ public uint FourCC { get; }
+
+ ///
+ /// Gets Number of bits in an RGB (possibly including alpha) format. Valid when
+ /// includes , or ,
+ /// or .
+ ///
+ public uint RGBBitCount { get; }
+
+ ///
+ /// Gets Red (or lumiannce or Y) mask for reading color data. For instance, given the A8R8G8B8 format,
+ /// the red mask would be 0x00ff0000.
+ ///
+ public uint RBitMask { get; }
+
+ ///
+ /// Gets Green (or U) mask for reading color data. For instance, given the A8R8G8B8 format,
+ /// the green mask would be 0x0000ff00.
+ ///
+ public uint GBitMask { get; }
+
+ ///
+ /// Gets Blue (or V) mask for reading color data. For instance, given the A8R8G8B8 format,
+ /// the blue mask would be 0x000000ff.
+ ///
+ public uint BBitMask { get; }
+
+ ///
+ /// Gets Alpha mask for reading alpha data. dwFlags must include
+ /// or .
+ /// For instance, given the A8R8G8B8 format, the alpha mask would be 0xff000000.
+ ///
+ public uint ABitMask { get; }
+
+ ///
+ /// Writes the header to the given buffer.
+ ///
+ /// The buffer to write to.
+ /// Offset in the buffer.
+ public readonly void WriteTo(Span buffer, int offset)
+ {
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset, 4), this.Size);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 4, 4), (uint)this.Flags);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 8, 4), this.FourCC);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 12, 4), this.RGBBitCount);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 16, 4), this.RBitMask);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 20, 4), this.GBitMask);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 24, 4), this.BBitMask);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset + 28, 4), this.ABitMask);
+ }
- ///
- /// Parses the DdsPixelFormat from the given data buffer.
- ///
- /// The data to parse.
- /// Offset in the buffer.
- /// The parsed DdsPixelFormat.
- public static DdsPixelFormat Parse(Span data, int offset) =>
- new DdsPixelFormat(
- size: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset, 4)),
- flags: (DdsPixelFormatFlags)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 4, 4)),
- fourCC: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 8, 4)),
- rgbBitCount: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 12, 4)),
- rBitMask: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 16, 4)),
- gBitMask: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 20, 4)),
- bBitMask: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 24, 4)),
- aBitMask: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 28, 4)));
-
- ///
- /// Gets the represented by this structure.
- ///
- ///
- /// The represented by this structure.
- ///
- public D3dFormat GetD3DFormat()
+ ///
+ /// Parses the DdsPixelFormat from the given data buffer.
+ ///
+ /// The data to parse.
+ /// Offset in the buffer.
+ /// The parsed DdsPixelFormat.
+ public static DdsPixelFormat Parse(Span data, int offset) =>
+ new(
+ size: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset, 4)),
+ flags: (DdsPixelFormatFlags)BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 4, 4)),
+ fourCC: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 8, 4)),
+ rgbBitCount: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 12, 4)),
+ rBitMask: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 16, 4)),
+ gBitMask: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 20, 4)),
+ bBitMask: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 24, 4)),
+ aBitMask: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset + 28, 4)));
+
+ ///
+ /// Gets the represented by this structure.
+ ///
+ ///
+ /// The represented by this structure.
+ ///
+ public D3dFormat GetD3DFormat()
+ {
+ if ((this.Flags & DdsPixelFormatFlags.RGBA) == DdsPixelFormatFlags.RGBA)
{
- if ((this.Flags & DdsPixelFormatFlags.RGBA) == DdsPixelFormatFlags.RGBA)
+ switch (this.RGBBitCount)
{
- switch (this.RGBBitCount)
- {
- case 32:
- return this.GetRgba32();
- case 16:
- return this.GetRgba16();
- }
- }
- else if ((this.Flags & DdsPixelFormatFlags.RGB) == DdsPixelFormatFlags.RGB)
- {
- switch (this.RGBBitCount)
- {
- case 32:
- return this.GetRgb32();
- case 24:
- return this.GetRgb24();
- case 16:
- return this.GetRgb16();
- }
+ case 32:
+ return this.GetRgba32();
+ case 16:
+ return this.GetRgba16();
}
- else if ((this.Flags & DdsPixelFormatFlags.Alpha) == DdsPixelFormatFlags.Alpha)
+ }
+ else if ((this.Flags & DdsPixelFormatFlags.RGB) == DdsPixelFormatFlags.RGB)
+ {
+ switch (this.RGBBitCount)
{
- if (this.RGBBitCount == 8)
- {
- if (this.ABitMask == 0xff)
- {
- return D3dFormat.A8;
- }
- }
+ case 32:
+ return this.GetRgb32();
+ case 24:
+ return this.GetRgb24();
+ case 16:
+ return this.GetRgb16();
}
- else if ((this.Flags & DdsPixelFormatFlags.Luminance) == DdsPixelFormatFlags.Luminance)
+ }
+ else if ((this.Flags & DdsPixelFormatFlags.Alpha) == DdsPixelFormatFlags.Alpha)
+ {
+ if (this.RGBBitCount == 8)
{
- switch (this.RGBBitCount)
+ if (this.ABitMask == 0xff)
{
- case 16:
- return this.GetLumi16();
- case 8:
- return this.GetLumi8();
+ return D3dFormat.A8;
}
}
- else if ((this.Flags & DdsPixelFormatFlags.FourCC) == DdsPixelFormatFlags.FourCC)
- {
- return (D3dFormat)this.FourCC;
- }
-
- return D3dFormat.Unknown;
}
-
- private D3dFormat GetRgba32()
+ else if ((this.Flags & DdsPixelFormatFlags.Luminance) == DdsPixelFormatFlags.Luminance)
{
- if (this.RBitMask == 0xff && this.GBitMask == 0xff00 && this.BBitMask == 0xff0000 && this.ABitMask == 0xff000000)
- {
- return D3dFormat.A8B8G8R8;
- }
-
- if (this.RBitMask == 0xffff && this.GBitMask == 0xffff0000)
+ switch (this.RGBBitCount)
{
- return D3dFormat.G16R16;
+ case 16:
+ return this.GetLumi16();
+ case 8:
+ return this.GetLumi8();
}
+ }
+ else if ((this.Flags & DdsPixelFormatFlags.FourCC) == DdsPixelFormatFlags.FourCC)
+ {
+ return (D3dFormat)this.FourCC;
+ }
- if (this.RBitMask == 0x3ff && this.GBitMask == 0xffc00 && this.BBitMask == 0x3ff00000)
- {
- return D3dFormat.A2B10G10R10;
- }
+ return D3dFormat.Unknown;
+ }
- if (this.RBitMask == 0xff0000 && this.GBitMask == 0xff00 && this.BBitMask == 0xff && this.ABitMask == 0xff000000)
- {
- return D3dFormat.A8R8G8B8;
- }
+ private readonly D3dFormat GetRgba32()
+ {
+ if (this.RBitMask == 0xff && this.GBitMask == 0xff00 && this.BBitMask == 0xff0000 && this.ABitMask == 0xff000000)
+ {
+ return D3dFormat.A8B8G8R8;
+ }
- if (this.RBitMask == 0x3ff00000 && this.GBitMask == 0xffc00 && this.BBitMask == 0x3ff && this.ABitMask == 0xc0000000)
- {
- return D3dFormat.A2R10G10B10;
- }
+ if (this.RBitMask == 0xffff && this.GBitMask == 0xffff0000)
+ {
+ return D3dFormat.G16R16;
+ }
- return D3dFormat.Unknown;
+ if (this.RBitMask == 0x3ff && this.GBitMask == 0xffc00 && this.BBitMask == 0x3ff00000)
+ {
+ return D3dFormat.A2B10G10R10;
}
- private D3dFormat GetRgba16()
+ if (this.RBitMask == 0xff0000 && this.GBitMask == 0xff00 && this.BBitMask == 0xff && this.ABitMask == 0xff000000)
{
- if (this.RBitMask == 0x7c00 && this.GBitMask == 0x3e0 && this.BBitMask == 0x1f && this.ABitMask == 0x8000)
- {
- return D3dFormat.A1R5G5B5;
- }
+ return D3dFormat.A8R8G8B8;
+ }
- if (this.RBitMask == 0xf00 && this.GBitMask == 0xf0 && this.BBitMask == 0xf && this.ABitMask == 0xf000)
- {
- return D3dFormat.A4R4G4B4;
- }
+ if (this.RBitMask == 0x3ff00000 && this.GBitMask == 0xffc00 && this.BBitMask == 0x3ff && this.ABitMask == 0xc0000000)
+ {
+ return D3dFormat.A2R10G10B10;
+ }
- if (this.RBitMask == 0xe0 && this.GBitMask == 0x1c && this.BBitMask == 0x3 && this.ABitMask == 0xff00)
- {
- return D3dFormat.A8R3G3B2;
- }
+ return D3dFormat.Unknown;
+ }
- return D3dFormat.Unknown;
+ private readonly D3dFormat GetRgba16()
+ {
+ if (this.RBitMask == 0x7c00 && this.GBitMask == 0x3e0 && this.BBitMask == 0x1f && this.ABitMask == 0x8000)
+ {
+ return D3dFormat.A1R5G5B5;
}
- private D3dFormat GetRgb32()
+ if (this.RBitMask == 0xf00 && this.GBitMask == 0xf0 && this.BBitMask == 0xf && this.ABitMask == 0xf000)
{
- if (this.RBitMask == 0xffff && this.GBitMask == 0xffff0000)
- {
- return D3dFormat.G16R16;
- }
+ return D3dFormat.A4R4G4B4;
+ }
- if (this.RBitMask == 0xff0000 && this.GBitMask == 0xff00 && this.BBitMask == 0xff)
- {
- return D3dFormat.X8R8G8B8;
- }
+ if (this.RBitMask == 0xe0 && this.GBitMask == 0x1c && this.BBitMask == 0x3 && this.ABitMask == 0xff00)
+ {
+ return D3dFormat.A8R3G3B2;
+ }
- if (this.RBitMask == 0xff && this.GBitMask == 0xff00 && this.BBitMask == 0xff0000)
- {
- return D3dFormat.X8B8G8R8;
- }
+ return D3dFormat.Unknown;
+ }
- return D3dFormat.Unknown;
+ private readonly D3dFormat GetRgb32()
+ {
+ if (this.RBitMask == 0xffff && this.GBitMask == 0xffff0000)
+ {
+ return D3dFormat.G16R16;
}
- private D3dFormat GetRgb24()
+ if (this.RBitMask == 0xff0000 && this.GBitMask == 0xff00 && this.BBitMask == 0xff)
{
- if (this.RBitMask == 0xff0000 && this.GBitMask == 0xff00 && this.BBitMask == 0xff)
- {
- return D3dFormat.R8G8B8;
- }
+ return D3dFormat.X8R8G8B8;
+ }
- return D3dFormat.Unknown;
+ if (this.RBitMask == 0xff && this.GBitMask == 0xff00 && this.BBitMask == 0xff0000)
+ {
+ return D3dFormat.X8B8G8R8;
}
- private D3dFormat GetRgb16()
+ return D3dFormat.Unknown;
+ }
+
+ private readonly D3dFormat GetRgb24()
+ {
+ if (this.RBitMask == 0xff0000 && this.GBitMask == 0xff00 && this.BBitMask == 0xff)
{
- if (this.RBitMask == 0xf800 && this.GBitMask == 0x7e0 && this.BBitMask == 0x1f)
- {
- return D3dFormat.R5G6B5;
- }
+ return D3dFormat.R8G8B8;
+ }
- if (this.RBitMask == 0x7c00 && this.GBitMask == 0x3e0 && this.BBitMask == 0x1f)
- {
- return D3dFormat.X1R5G5B5;
- }
+ return D3dFormat.Unknown;
+ }
- if (this.RBitMask == 0xf00 && this.GBitMask == 0xf0 && this.BBitMask == 0xf)
- {
- return D3dFormat.X4R4G4B4;
- }
+ private readonly D3dFormat GetRgb16()
+ {
+ if (this.RBitMask == 0xf800 && this.GBitMask == 0x7e0 && this.BBitMask == 0x1f)
+ {
+ return D3dFormat.R5G6B5;
+ }
- return D3dFormat.Unknown;
+ if (this.RBitMask == 0x7c00 && this.GBitMask == 0x3e0 && this.BBitMask == 0x1f)
+ {
+ return D3dFormat.X1R5G5B5;
}
- private D3dFormat GetLumi16()
+ if (this.RBitMask == 0xf00 && this.GBitMask == 0xf0 && this.BBitMask == 0xf)
{
- if (this.RBitMask == 0xff && this.ABitMask == 0xff00)
- {
- return D3dFormat.A8L8;
- }
+ return D3dFormat.X4R4G4B4;
+ }
- if (this.RBitMask == 0xffff)
- {
- return D3dFormat.L16;
- }
+ return D3dFormat.Unknown;
+ }
- return D3dFormat.Unknown;
+ private readonly D3dFormat GetLumi16()
+ {
+ if (this.RBitMask == 0xff && this.ABitMask == 0xff00)
+ {
+ return D3dFormat.A8L8;
}
- private D3dFormat GetLumi8()
+ if (this.RBitMask == 0xffff)
{
- if (this.RBitMask == 0xf && this.ABitMask == 0xf0)
- {
- return D3dFormat.A4L4;
- }
+ return D3dFormat.L16;
+ }
- if (this.RBitMask == 0xff)
- {
- return D3dFormat.L8;
- }
+ return D3dFormat.Unknown;
+ }
- return D3dFormat.Unknown;
+ private readonly D3dFormat GetLumi8()
+ {
+ if (this.RBitMask == 0xf && this.ABitMask == 0xf0)
+ {
+ return D3dFormat.A4L4;
+ }
+
+ if (this.RBitMask == 0xff)
+ {
+ return D3dFormat.L8;
}
+
+ return D3dFormat.Unknown;
}
}
diff --git a/src/ImageSharp.Textures/Formats/Dds/DdsProcessor.cs b/src/ImageSharp.Textures/Formats/Dds/DdsProcessor.cs
index 2c976ff0..15d6a2c4 100644
--- a/src/ImageSharp.Textures/Formats/Dds/DdsProcessor.cs
+++ b/src/ImageSharp.Textures/Formats/Dds/DdsProcessor.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System;
-using System.IO;
using SixLabors.ImageSharp.Textures.Common.Exceptions;
using SixLabors.ImageSharp.Textures.Common.Extensions;
using SixLabors.ImageSharp.Textures.Formats.Dds.Emums;
@@ -10,489 +8,488 @@
using Fp32 = SixLabors.ImageSharp.Textures.TextureFormats.Decoding.Fp32;
using L32 = SixLabors.ImageSharp.Textures.TextureFormats.Decoding.L32;
-namespace SixLabors.ImageSharp.Textures.Formats.Dds
+namespace SixLabors.ImageSharp.Textures.Formats.Dds;
+
+///
+/// Decodes direct draw surfaces.
+///
+internal class DdsProcessor
{
///
- /// Decodes direct draw surfaces.
+ /// Initializes a new instance of the class.
///
- internal class DdsProcessor
+ /// The DDS header.
+ /// The DDS header DXT10.
+ public DdsProcessor(DdsHeader ddsHeader, DdsHeaderDxt10 ddsHeaderDxt10)
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The DDS header.
- /// The DDS header DXT10.
- public DdsProcessor(DdsHeader ddsHeader, DdsHeaderDxt10 ddsHeaderDxt10)
- {
- this.DdsHeader = ddsHeader;
- this.DdsHeaderDxt10 = ddsHeaderDxt10;
- }
+ this.DdsHeader = ddsHeader;
+ this.DdsHeaderDxt10 = ddsHeaderDxt10;
+ }
+
+ ///
+ /// Gets the dds header.
+ ///
+ public DdsHeader DdsHeader { get; }
+
+ ///
+ /// Gets the dxt 10 header.
+ ///
+ public DdsHeaderDxt10 DdsHeaderDxt10 { get; }
+
+ ///
+ /// Decodes the mipmaps of a DDS textures.
+ ///
+ /// The stream to read the texture data from.
+ /// The width of the texture at level 0.
+ /// The height of the texture at level 0.
+ /// The mipmap count.
+ /// The decoded mipmaps.
+ public MipMap[] DecodeDds(Stream stream, int width, int height, int count)
+ {
+ Guard.MustBeGreaterThan(count, 0, nameof(count));
- ///
- /// Gets the dds header.
- ///
- public DdsHeader DdsHeader { get; }
-
- ///
- /// Gets the dxt 10 header.
- ///
- public DdsHeaderDxt10 DdsHeaderDxt10 { get; }
-
- ///
- /// Decodes the mipmaps of a DDS textures.
- ///
- /// The stream to read the texture data from.
- /// The width of the texture at level 0.
- /// The height of the texture at level 0.
- /// The mipmap count.
- /// The decoded mipmaps.
- public MipMap[] DecodeDds(Stream stream, int width, int height, int count)
+ return this.DdsHeader.PixelFormat.FourCC switch
{
- Guard.MustBeGreaterThan(count, 0, nameof(count));
+ DdsFourCc.None
+ or DdsFourCc.R16FLOAT
+ or DdsFourCc.R16G16FLOAT
+ or DdsFourCc.R16G16B16A16SNORM
+ or DdsFourCc.R16G16B16A16UNORM
+ or DdsFourCc.R16G16B16A16FLOAT
+ or DdsFourCc.R32FLOAT
+ or DdsFourCc.R32G32FLOAT
+ or DdsFourCc.R32G32B32A32FLOAT
+ or DdsFourCc.YUY2
+ or DdsFourCc.RGBG
+ or DdsFourCc.GRGB => this.ProcessUncompressed(stream, width, height, count),
+ DdsFourCc.DXT1 => AllocateMipMaps(stream, width, height, count),
+ DdsFourCc.DXT2 or DdsFourCc.DXT4 => throw new NotSupportedException("Due to patents Can, DXT2 or DXT4 cannot be supported."),
+ DdsFourCc.DXT3 => AllocateMipMaps(stream, width, height, count),
+ DdsFourCc.DXT5 => AllocateMipMaps(stream, width, height, count),
+ DdsFourCc.DX10 => this.GetDx10Dds(stream, width, height, count),
+ DdsFourCc.ATI1 or DdsFourCc.BC4U => AllocateMipMaps(stream, width, height, count),
+ DdsFourCc.BC4S => AllocateMipMaps(stream, width, height, count),
+ DdsFourCc.ATI2 or DdsFourCc.BC5U => AllocateMipMaps(stream, width, height, count),
+ DdsFourCc.BC5S => AllocateMipMaps(stream, width, height, count),
+ _ => throw new NotSupportedException($"FourCC: {this.DdsHeader.PixelFormat.FourCC.FourCcToString()} not supported."),
+ };
+ }
- return this.DdsHeader.PixelFormat.FourCC switch
+ public MipMap[] ProcessUncompressed(Stream stream, int width, int height, int count)
+ {
+ uint bitsPerPixel = this.DdsHeader.PixelFormat.RGBBitCount;
+ return bitsPerPixel switch
+ {
+ 8 => this.EightBitImageFormat(stream, width, height, count),
+ 16 => this.SixteenBitImageFormat(stream, width, height, count),
+ 24 => this.TwentyFourBitImageFormat(stream, width, height, count),
+ 32 => this.ThirtyTwoBitImageFormat(stream, width, height, count),
+ _ => this.DdsHeader.PixelFormat.FourCC switch
{
- DdsFourCc.None
- or DdsFourCc.R16FLOAT
+ DdsFourCc.R16FLOAT => this.SixteenBitImageFormat(stream, width, height, count),
+ DdsFourCc.R32FLOAT
or DdsFourCc.R16G16FLOAT
- or DdsFourCc.R16G16B16A16SNORM
- or DdsFourCc.R16G16B16A16UNORM
- or DdsFourCc.R16G16B16A16FLOAT
- or DdsFourCc.R32FLOAT
- or DdsFourCc.R32G32FLOAT
- or DdsFourCc.R32G32B32A32FLOAT
or DdsFourCc.YUY2
or DdsFourCc.RGBG
- or DdsFourCc.GRGB => this.ProcessUncompressed(stream, width, height, count),
- DdsFourCc.DXT1 => AllocateMipMaps(stream, width, height, count),
- DdsFourCc.DXT2 or DdsFourCc.DXT4 => throw new NotSupportedException("Due to patents Can, DXT2 or DXT4 cannot be supported."),
- DdsFourCc.DXT3 => AllocateMipMaps(stream, width, height, count),
- DdsFourCc.DXT5 => AllocateMipMaps(stream, width, height, count),
- DdsFourCc.DX10 => this.GetDx10Dds(stream, width, height, count),
- DdsFourCc.ATI1 or DdsFourCc.BC4U => AllocateMipMaps(stream, width, height, count),
- DdsFourCc.BC4S => AllocateMipMaps(stream, width, height, count),
- DdsFourCc.ATI2 or DdsFourCc.BC5U => AllocateMipMaps(stream, width, height, count),
- DdsFourCc.BC5S => AllocateMipMaps(stream, width, height, count),
- _ => throw new NotSupportedException($"FourCC: {this.DdsHeader.PixelFormat.FourCC.FourCcToString()} not supported."),
- };
- }
+ or DdsFourCc.GRGB => this.ThirtyTwoBitImageFormat(stream, width, height, count),
+ DdsFourCc.R16G16B16A16SNORM
+ or DdsFourCc.R16G16B16A16UNORM
+ or DdsFourCc.R16G16B16A16FLOAT
+ or DdsFourCc.R32G32FLOAT => this.SixtyFourBitImageFormat(stream, width, height, count),
+ DdsFourCc.R32G32B32A32FLOAT => this.HundredTwentyEightBitImageFormat(stream, width, height, count),
+ _ => throw new ArgumentOutOfRangeException($"Unrecognized rgb bit count: {this.DdsHeader.PixelFormat.RGBBitCount}"),
+ }, // For unknown reason some formats do not have the bitsPerPixel set in the header (its zero).
+ };
+ }
- public MipMap[] ProcessUncompressed(Stream stream, int width, int height, int count)
- {
- uint bitsPerPixel = this.DdsHeader.PixelFormat.RGBBitCount;
- return bitsPerPixel switch
- {
- 8 => this.EightBitImageFormat(stream, width, height, count),
- 16 => this.SixteenBitImageFormat(stream, width, height, count),
- 24 => this.TwentyFourBitImageFormat(stream, width, height, count),
- 32 => this.ThirtyTwoBitImageFormat(stream, width, height, count),
- _ => this.DdsHeader.PixelFormat.FourCC switch
- {
- DdsFourCc.R16FLOAT => this.SixteenBitImageFormat(stream, width, height, count),
- DdsFourCc.R32FLOAT
- or DdsFourCc.R16G16FLOAT
- or DdsFourCc.YUY2
- or DdsFourCc.RGBG
- or DdsFourCc.GRGB => this.ThirtyTwoBitImageFormat(stream, width, height, count),
- DdsFourCc.R16G16B16A16SNORM
- or DdsFourCc.R16G16B16A16UNORM
- or DdsFourCc.R16G16B16A16FLOAT
- or DdsFourCc.R32G32FLOAT => this.SixtyFourBitImageFormat(stream, width, height, count),
- DdsFourCc.R32G32B32A32FLOAT => this.HundredTwentyEightBitImageFormat(stream, width, height, count),
- _ => throw new ArgumentOutOfRangeException($"Unrecognized rgb bit count: {this.DdsHeader.PixelFormat.RGBBitCount}"),
- }, // For unknown reason some formats do not have the bitsPerPixel set in the header (its zero).
- };
- }
+ ///
+ /// Allocates and decodes all mipmap levels of a DDS texture.
+ ///
+ /// The stream to read the texture data from.
+ /// The width of the texture at level 0.
+ /// The height of the texture at level 0.
+ /// The mipmap count.
+ /// The decoded mipmaps.
+ private static MipMap[] AllocateMipMaps(Stream stream, int width, int height, int count)
+ where TBlock : struct, IBlock
+ {
+ TBlock blockFormat = default(TBlock);
+
+ MipMap[] mipMaps = new MipMap[count];
- ///
- /// Allocates and decodes all mipmap levels of a DDS texture.
- ///
- /// The stream to read the texture data from.
- /// The width of the texture at level 0.
- /// The height of the texture at level 0.
- /// The mipmap count.
- /// The decoded mipmaps.
- private static MipMap[] AllocateMipMaps(Stream stream, int width, int height, int count)
- where TBlock : struct, IBlock
+ for (int i = 0; i < count; i++)
{
- var blockFormat = default(TBlock);
+ int widthBlocks = blockFormat.Compressed ? Helper.CalcBlocks(width) : width;
+ int heightBlocks = blockFormat.Compressed ? Helper.CalcBlocks(height) : height;
+ int bytesToRead = heightBlocks * widthBlocks * blockFormat.CompressedBytesPerBlock;
- var mipMaps = new MipMap[count];
+ // Special case for yuv formats with a single pixel.
+ if (bytesToRead < blockFormat.BitsPerPixel / 8)
+ {
+ bytesToRead = blockFormat.BitsPerPixel / 8;
+ }
- for (int i = 0; i < count; i++)
+ byte[] mipData = new byte[bytesToRead];
+ int read = stream.Read(mipData, 0, bytesToRead);
+ if (read != bytesToRead)
{
- int widthBlocks = blockFormat.Compressed ? Helper.CalcBlocks(width) : width;
- int heightBlocks = blockFormat.Compressed ? Helper.CalcBlocks(height) : height;
- int bytesToRead = heightBlocks * widthBlocks * blockFormat.CompressedBytesPerBlock;
-
- // Special case for yuv formats with a single pixel.
- if (bytesToRead < blockFormat.BitsPerPixel / 8)
- {
- bytesToRead = blockFormat.BitsPerPixel / 8;
- }
-
- byte[] mipData = new byte[bytesToRead];
- int read = stream.Read(mipData, 0, bytesToRead);
- if (read != bytesToRead)
- {
- throw new TextureFormatException("could not read enough texture data from the stream");
- }
-
- mipMaps[i] = new MipMap(blockFormat, mipData, width, height);
-
- width >>= 1;
- height >>= 1;
+ throw new TextureFormatException("could not read enough texture data from the stream");
}
- return mipMaps;
- }
+ mipMaps[i] = new MipMap(blockFormat, mipData, width, height);
- private MipMap[] EightBitImageFormat(Stream stream, int width, int height, int count)
- {
- DdsPixelFormat pixelFormat = this.DdsHeader.PixelFormat;
+ width >>= 1;
+ height >>= 1;
+ }
- bool hasAlpha = pixelFormat.Flags.HasFlag(DdsPixelFormatFlags.AlphaPixels);
+ return mipMaps;
+ }
- if (pixelFormat.RBitMask == 0x0 && pixelFormat.GBitMask == 0x0 && pixelFormat.BBitMask == 0x0)
- {
- return AllocateMipMaps(stream, width, height, count);
- }
+ private MipMap[] EightBitImageFormat(Stream stream, int width, int height, int count)
+ {
+ DdsPixelFormat pixelFormat = this.DdsHeader.PixelFormat;
- if (!hasAlpha && pixelFormat.RBitMask == 0xFF && pixelFormat.GBitMask == 0x0 && pixelFormat.BBitMask == 0x0)
- {
- return AllocateMipMaps(stream, width, height, count);
- }
+ bool hasAlpha = pixelFormat.Flags.HasFlag(DdsPixelFormatFlags.AlphaPixels);
- throw new NotSupportedException("Unsupported 8 bit format");
+ if (pixelFormat.RBitMask == 0x0 && pixelFormat.GBitMask == 0x0 && pixelFormat.BBitMask == 0x0)
+ {
+ return AllocateMipMaps(stream, width, height, count);
}
- private MipMap[] SixteenBitImageFormat(Stream stream, int width, int height, int count)
+ if (!hasAlpha && pixelFormat.RBitMask == 0xFF && pixelFormat.GBitMask == 0x0 && pixelFormat.BBitMask == 0x0)
{
- DdsPixelFormat pixelFormat = this.DdsHeader.PixelFormat;
+ return AllocateMipMaps(stream, width, height, count);
+ }
- bool hasAlpha = pixelFormat.Flags.HasFlag(DdsPixelFormatFlags.AlphaPixels);
+ throw new NotSupportedException("Unsupported 8 bit format");
+ }
- if (hasAlpha && pixelFormat.RBitMask == 0xF00 && pixelFormat.GBitMask == 0xF0 && pixelFormat.BBitMask == 0xF)
- {
- return AllocateMipMaps(stream, width, height, count);
- }
+ private MipMap[] SixteenBitImageFormat(Stream stream, int width, int height, int count)
+ {
+ DdsPixelFormat pixelFormat = this.DdsHeader.PixelFormat;
- if (!hasAlpha && pixelFormat.RBitMask == 0x7C00 && pixelFormat.GBitMask == 0x3E0 && pixelFormat.BBitMask == 0x1F)
- {
- return AllocateMipMaps(stream, width, height, count);
- }
+ bool hasAlpha = pixelFormat.Flags.HasFlag(DdsPixelFormatFlags.AlphaPixels);
- if (hasAlpha && pixelFormat.RBitMask == 0x7C00 && pixelFormat.GBitMask == 0x3E0 && pixelFormat.BBitMask == 0x1F)
- {
- return AllocateMipMaps(stream, width, height, count);
- }
+ if (hasAlpha && pixelFormat.RBitMask == 0xF00 && pixelFormat.GBitMask == 0xF0 && pixelFormat.BBitMask == 0xF)
+ {
+ return AllocateMipMaps(stream, width, height, count);
+ }
- if (!hasAlpha && pixelFormat.RBitMask == 0xF800 && pixelFormat.GBitMask == 0x7E0 && pixelFormat.BBitMask == 0x1F)
- {
- return AllocateMipMaps(stream, width, height, count);
- }
+ if (!hasAlpha && pixelFormat.RBitMask == 0x7C00 && pixelFormat.GBitMask == 0x3E0 && pixelFormat.BBitMask == 0x1F)
+ {
+ return AllocateMipMaps(stream, width, height, count);
+ }
- if (hasAlpha && pixelFormat.RBitMask == 0xFF && pixelFormat.GBitMask == 0x0 && pixelFormat.BBitMask == 0x0)
- {
- return AllocateMipMaps(stream, width, height, count);
- }
+ if (hasAlpha && pixelFormat.RBitMask == 0x7C00 && pixelFormat.GBitMask == 0x3E0 && pixelFormat.BBitMask == 0x1F)
+ {
+ return AllocateMipMaps(stream, width, height, count);
+ }
- if (!hasAlpha && pixelFormat.RBitMask == 0xFFFF && pixelFormat.GBitMask == 0x0 && pixelFormat.BBitMask == 0x0)
- {
- return AllocateMipMaps(stream, width, height, count);
- }
+ if (!hasAlpha && pixelFormat.RBitMask == 0xF800 && pixelFormat.GBitMask == 0x7E0 && pixelFormat.BBitMask == 0x1F)
+ {
+ return AllocateMipMaps(stream, width, height, count);
+ }
- if (!hasAlpha && pixelFormat.RBitMask == 0xFF && pixelFormat.GBitMask == 0xFF00 && pixelFormat.BBitMask == 0x0)
- {
- return AllocateMipMaps(stream, width, height, count);
- }
+ if (hasAlpha && pixelFormat.RBitMask == 0xFF && pixelFormat.GBitMask == 0x0 && pixelFormat.BBitMask == 0x0)
+ {
+ return AllocateMipMaps(stream, width, height, count);
+ }
- if (pixelFormat.FourCC == DdsFourCc.R16FLOAT)
- {
- return AllocateMipMaps(stream, width, height, count);
- }
+ if (!hasAlpha && pixelFormat.RBitMask == 0xFFFF && pixelFormat.GBitMask == 0x0 && pixelFormat.BBitMask == 0x0)
+ {
+ return AllocateMipMaps(stream, width, height, count);
+ }
- throw new NotSupportedException("Unsupported 16 bit format");
+ if (!hasAlpha && pixelFormat.RBitMask == 0xFF && pixelFormat.GBitMask == 0xFF00 && pixelFormat.BBitMask == 0x0)
+ {
+ return AllocateMipMaps