From 6577b4c0c05c80692f1432a447681d9d0a5ee29b Mon Sep 17 00:00:00 2001
From: Erik White <26148654+Erik-White@users.noreply.github.com>
Date: Fri, 27 Feb 2026 13:47:49 +0100
Subject: [PATCH 01/10] Add ASTC decoding support
---
README.md | 3 +-
src/Directory.Build.props | 2 +-
.../Formats/Ktx/KtxProcessor.cs | 62 +++-
.../Formats/Ktx2/Ktx2DecoderCore.cs | 35 +-
.../Formats/Ktx2/Ktx2Processor.cs | 84 +++++
.../ImageSharp.Textures.csproj | 1 +
.../TextureFormats/Decoding/AstcDecoder.cs | 146 ++++++++
.../TextureFormats/Decoding/RgbaAstc10x10.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc10x5.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc10x6.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc10x8.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc12x10.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc12x12.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc4x4.cs | 42 +++
.../TextureFormats/Decoding/RgbaAstc5x4.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc5x5.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc6x5.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc6x6.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc8x5.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc8x6.cs | 38 ++
.../TextureFormats/Decoding/RgbaAstc8x8.cs | 38 ++
tests/Directory.Build.props | 2 +-
.../Formats/Ktx/KtxAstcDecoderTests.cs | 40 +++
.../Ktx2/Ktx2AstcDecoderCubemapTests.cs | 65 ++++
.../Formats/Ktx2/Ktx2AstcDecoderFlatTests.cs | 194 ++++++++++
tests/ImageSharp.Textures.Tests/TestImages.cs | 86 ++++-
.../Decoding/AstcDecoderTests.cs | 331 ++++++++++++++++++
tests/Images/Input/Ktx/astc-rgba32-8x8.ktx | 3 +
.../astc_ldr_10x5_FlightHelmet_baseColor.ktx2 | 3 +
.../Ktx2/astc_ldr_6x6_arraytex_7_mipmap.ktx2 | 3 +
.../Input/Ktx2/astc_ldr_cubemap_6x6.ktx2 | 3 +
.../Images/Input/Ktx2/astc_rgba32_10x10.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_10x5.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_10x6.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_10x8.ktx2 | 3 +
.../Images/Input/Ktx2/astc_rgba32_12x10.ktx2 | 3 +
.../Images/Input/Ktx2/astc_rgba32_12x12.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_4x4.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_5x4.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_5x5.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_6x5.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_6x6.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_8x5.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_8x6.ktx2 | 3 +
tests/Images/Input/Ktx2/astc_rgba32_8x8.ktx2 | 3 +
.../Ktx2/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2 | 3 +
.../valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2 | 3 +
.../valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2 | 3 +
.../valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2 | 3 +
.../valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2 | 3 +
.../Ktx2/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2 | 3 +
.../Ktx2/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2 | 3 +
...x2AstcDecoder_CanDecode_All_Faces_negX.png | 3 +
...x2AstcDecoder_CanDecode_All_Faces_negY.png | 3 +
...x2AstcDecoder_CanDecode_All_Faces_negZ.png | 3 +
...x2AstcDecoder_CanDecode_All_Faces_posX.png | 3 +
...x2AstcDecoder_CanDecode_All_Faces_posY.png | 3 +
...x2AstcDecoder_CanDecode_All_Faces_posZ.png | 3 +
.../Ktx2AstcDecoder_CanDecode_MipMaps_0.png | 3 +
.../Ktx2AstcDecoder_CanDecode_MipMaps_1.png | 3 +
.../Ktx2AstcDecoder_CanDecode_MipMaps_2.png | 3 +
.../Ktx2AstcDecoder_CanDecode_MipMaps_3.png | 3 +
.../Ktx2AstcDecoder_CanDecode_MipMaps_4.png | 3 +
...stcDecoder_CanDecode_Rgba32_Blocksizes.png | 3 +
...stcDecoder_CanDecode_Rgba32_Srgb_10x10.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Srgb_10x5.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Srgb_10x6.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Srgb_10x8.png | 3 +
...stcDecoder_CanDecode_Rgba32_Srgb_12x10.png | 3 +
...stcDecoder_CanDecode_Rgba32_Srgb_12x12.png | 3 +
...2AstcDecoder_CanDecode_Rgba32_Srgb_4x4.png | 3 +
...2AstcDecoder_CanDecode_Rgba32_Srgb_5x4.png | 3 +
...2AstcDecoder_CanDecode_Rgba32_Srgb_5x5.png | 3 +
...2AstcDecoder_CanDecode_Rgba32_Srgb_6x5.png | 3 +
...2AstcDecoder_CanDecode_Rgba32_Srgb_6x6.png | 3 +
...2AstcDecoder_CanDecode_Rgba32_Srgb_8x5.png | 3 +
...2AstcDecoder_CanDecode_Rgba32_Srgb_8x6.png | 3 +
...2AstcDecoder_CanDecode_Rgba32_Srgb_8x8.png | 3 +
...stcDecoder_CanDecode_Rgba32_Srgb_Large.png | 3 +
...anDecode_Rgba32_Supercompressed_ZLIB_1.png | 3 +
...anDecode_Rgba32_Supercompressed_ZLIB_9.png | 3 +
...anDecode_Rgba32_Supercompressed_ZSTD_1.png | 3 +
...anDecode_Rgba32_Supercompressed_ZSTD_9.png | 3 +
...tcDecoder_CanDecode_Rgba32_Unorm_10x10.png | 3 +
...stcDecoder_CanDecode_Rgba32_Unorm_10x5.png | 3 +
...stcDecoder_CanDecode_Rgba32_Unorm_10x6.png | 3 +
...stcDecoder_CanDecode_Rgba32_Unorm_10x8.png | 3 +
...tcDecoder_CanDecode_Rgba32_Unorm_12x10.png | 3 +
...tcDecoder_CanDecode_Rgba32_Unorm_12x12.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Unorm_4x4.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Unorm_5x4.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Unorm_5x5.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Unorm_6x5.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Unorm_6x6.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Unorm_8x5.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Unorm_8x6.png | 3 +
...AstcDecoder_CanDecode_Rgba32_Unorm_8x8.png | 3 +
...stcDecoder_CanDecode_Rgba32_Blocksizes.png | 3 +
123 files changed, 1857 insertions(+), 18 deletions(-)
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x10.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x6.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x8.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x10.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x12.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc4x4.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc5x4.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc5x5.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc6x5.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc6x6.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x5.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x6.cs
create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x8.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderFlatTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/TextureFormats/Decoding/AstcDecoderTests.cs
create mode 100644 tests/Images/Input/Ktx/astc-rgba32-8x8.ktx
create mode 100644 tests/Images/Input/Ktx2/astc_ldr_10x5_FlightHelmet_baseColor.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_ldr_6x6_arraytex_7_mipmap.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_ldr_cubemap_6x6.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_10x10.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_10x5.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_10x6.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_10x8.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_12x10.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_12x12.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_4x4.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_5x4.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_5x5.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_6x5.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_6x6.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_8x5.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_8x6.ktx2
create mode 100644 tests/Images/Input/Ktx2/astc_rgba32_8x8.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2
create mode 100644 tests/Images/Input/Ktx2/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negX.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negY.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negZ.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posX.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posY.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posZ.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_0.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_1.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_2.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_3.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_4.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x10.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x6.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x8.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x10.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x12.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_4x4.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x4.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x6.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x6.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x8.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_Large.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_1.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_9.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_1.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_9.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x10.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x6.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x8.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x10.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x12.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_4x4.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x4.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x6.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x6.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x8.png
create mode 100644 tests/Images/ReferenceOutput/KtxAstcDecoder_CanDecode_Rgba32_Blocksizes.png
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..e40e9b78 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -11,7 +11,7 @@
-->
-
+
diff --git a/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs b/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs
index 88ac7f20..8dbafc62 100644
--- a/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs
+++ b/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs
@@ -41,7 +41,7 @@ internal class KtxProcessor
/// The decoded mipmaps.
public MipMap[] DecodeMipMaps(Stream stream, int width, int height, uint count)
{
- if (this.KtxHeader.GlTypeSize == 1)
+ if (this.KtxHeader.GlTypeSize is 0 or 1)
{
switch (this.KtxHeader.GlFormat)
{
@@ -86,13 +86,41 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, uint count)
return this.AllocateMipMaps(stream, width, height, count);
case GlInternalPixelFormat.CompressedRgb8Etc2:
return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc4x4Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc5x4Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc5x5Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc6x5Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc6x6Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc8x5Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc8x6Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc8x8Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc10x5Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc10x6Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc10x8Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc10x10Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc12x10Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
+ case GlInternalPixelFormat.CompressedRgbaAstc12x12Khr:
+ return this.AllocateMipMaps(stream, width, height, count);
}
break;
}
}
- if (this.KtxHeader.GlTypeSize == 2 || this.KtxHeader.GlTypeSize == 4)
+ if (this.KtxHeader.GlTypeSize is 2 or 4)
{
// TODO: endianess is not respected here. Use stream reader which respects endianess.
switch (this.KtxHeader.GlInternalFormat)
@@ -166,12 +194,40 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height)
return this.AllocateCubeMap(stream, width, height);
case GlInternalPixelFormat.CompressedRgb8Etc2:
return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc4x4Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc5x4Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc5x5Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc6x5Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc6x6Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc8x5Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc8x6Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc8x8Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc10x5Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc10x6Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc10x8Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc10x10Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc12x10Khr:
+ return this.AllocateCubeMap(stream, width, height);
+ case GlInternalPixelFormat.CompressedRgbaAstc12x12Khr:
+ return this.AllocateCubeMap(stream, width, height);
}
break;
}
- if (this.KtxHeader.GlTypeSize == 2 || this.KtxHeader.GlTypeSize == 4)
+ if (this.KtxHeader.GlTypeSize is 2 or 4)
{
// TODO: endianess is not respected here. Use stream reader which respects endianess.
switch (this.KtxHeader.GlInternalFormat)
diff --git a/src/ImageSharp.Textures/Formats/Ktx2/Ktx2DecoderCore.cs b/src/ImageSharp.Textures/Formats/Ktx2/Ktx2DecoderCore.cs
index 6decbef3..b5367090 100644
--- a/src/ImageSharp.Textures/Formats/Ktx2/Ktx2DecoderCore.cs
+++ b/src/ImageSharp.Textures/Formats/Ktx2/Ktx2DecoderCore.cs
@@ -69,6 +69,7 @@ public Texture DecodeTexture(Stream stream)
int width = (int)this.ktxHeader.PixelWidth;
int height = (int)this.ktxHeader.PixelHeight;
+ // Level indices start immediately after the header
var levelIndices = new LevelIndex[this.ktxHeader.LevelCount];
for (int i = 0; i < levelIndices.Length; i++)
{
@@ -84,15 +85,39 @@ public Texture DecodeTexture(Stream stream)
var ktxProcessor = new Ktx2Processor(this.ktxHeader);
+ Texture texture;
if (this.ktxHeader.FaceCount == 6)
{
- CubemapTexture cubeMapTexture = ktxProcessor.DecodeCubeMap(stream, width, height, levelIndices);
- return cubeMapTexture;
+ texture = ktxProcessor.DecodeCubeMap(stream, width, height, levelIndices);
+ }
+ else
+ {
+ var flatTexture = new FlatTexture();
+ MipMap[] mipMaps = ktxProcessor.DecodeMipMaps(stream, width, height, levelIndices);
+ flatTexture.MipMaps.AddRange(mipMaps);
+ texture = flatTexture;
}
- var texture = new FlatTexture();
- MipMap[] mipMaps = ktxProcessor.DecodeMipMaps(stream, width, height, levelIndices);
- texture.MipMaps.AddRange(mipMaps);
+ // Seek to the end of the file to ensure the entire stream is consumed.
+ // KTX2 files use byte offsets for mipmap data, so the stream position may not
+ // be at the end after reading. We need to find the furthest point read.
+ if (levelIndices.Length > 0)
+ {
+ long maxEndPosition = 0;
+ for (int i = 0; i < levelIndices.Length; i++)
+ {
+ long endPosition = (long)(levelIndices[i].ByteOffset + levelIndices[i].UncompressedByteLength);
+ if (endPosition > maxEndPosition)
+ {
+ maxEndPosition = endPosition;
+ }
+ }
+
+ if (stream.Position < maxEndPosition && stream.CanSeek)
+ {
+ stream.Position = maxEndPosition;
+ }
+ }
return texture;
}
diff --git a/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs b/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs
index 4dbd30d2..ce516cda 100644
--- a/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs
+++ b/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs
@@ -157,6 +157,48 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, LevelIndex[]
return AllocateMipMaps(memoryStream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK:
return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_4x4_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_5x4_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_5x4_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_5x5_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_5x5_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_6x5_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_6x5_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_6x6_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_6x6_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_8x5_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_8x5_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_8x6_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_8x6_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_8x8_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_8x8_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_10x5_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_10x5_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_10x6_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_10x6_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_10x8_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_10x8_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_10x10_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_10x10_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_12x10_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_12x10_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_12x12_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
+ return AllocateMipMaps(memoryStream, width, height, levelIndices);
}
throw new NotSupportedException("The pixel format is not supported");
@@ -286,6 +328,48 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height, LevelI
return AllocateCubeMap(stream, width, height, levelIndices);
case VkFormat.VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK:
return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_4x4_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_5x4_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_5x4_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_5x5_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_5x5_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_6x5_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_6x5_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_6x6_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_6x6_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_8x5_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_8x5_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_8x6_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_8x6_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_8x8_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_8x8_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_10x5_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_10x5_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_10x6_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_10x6_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_10x8_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_10x8_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_10x10_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_10x10_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_12x10_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_12x10_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
+ case VkFormat.VK_FORMAT_ASTC_12x12_UNORM_BLOCK:
+ case VkFormat.VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
+ return AllocateCubeMap(stream, width, height, levelIndices);
}
throw new NotSupportedException("The pixel format is not supported");
diff --git a/src/ImageSharp.Textures/ImageSharp.Textures.csproj b/src/ImageSharp.Textures/ImageSharp.Textures.csproj
index 38c21b95..bad97213 100644
--- a/src/ImageSharp.Textures/ImageSharp.Textures.csproj
+++ b/src/ImageSharp.Textures/ImageSharp.Textures.csproj
@@ -39,6 +39,7 @@
+
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs
new file mode 100644
index 00000000..99259857
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using AstcSharp.Core;
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// ASTC (Adaptive scalable texture compression) decoder for all valid block footprints.
+///
+internal static class AstcDecoder
+{
+ internal const int AstcBlockSize = 16;
+
+ ///
+ /// Decodes an ASTC block into RGBA pixels.
+ ///
+ /// The 16-byte ASTC block data.
+ /// The width of the block footprint (4-12).
+ /// The height of the block footprint (4-12).
+ /// The output span for decoded RGBA pixels.
+ /// Thrown if blockData is not 16 bytes or decodedPixels is the wrong size.
+ /// Thrown if the block dimensions are invalid.
+ public static void DecodeBlock(ReadOnlySpan blockData, int blockWidth, int blockHeight, Span decodedPixels)
+ {
+ if (blockData.Length != AstcBlockSize)
+ {
+ throw new ArgumentException($"ASTC block data must be exactly {AstcBlockSize} bytes. Received {blockData.Length} bytes.", nameof(blockData));
+ }
+
+ int expectedDecodedSize = blockWidth * blockHeight * 4;
+ if (decodedPixels.Length < expectedDecodedSize)
+ {
+ throw new ArgumentException($"Output buffer must be at least {expectedDecodedSize} bytes for {blockWidth}x{blockHeight} block. Received {decodedPixels.Length} bytes.", nameof(decodedPixels));
+ }
+
+ Footprint footprint = Footprint.FromFootprintType(FootprintFromDimensions(blockWidth, blockHeight));
+
+ AstcSharp.AstcDecoder.DecompressBlock(blockData, footprint, decodedPixels);
+ }
+
+ ///
+ /// Decompresses ASTC-compressed image data to RGBA pixels.
+ ///
+ /// The compressed block data.
+ /// The width of the texture.
+ /// The height of the texture.
+ /// The width of the block footprint.
+ /// The height of the block footprint.
+ /// The number of compressed bytes per block.
+ /// The decompressed RGBA pixel data.
+ /// Thrown if blockData is null.
+ /// Thrown if dimensions or block parameters are invalid.
+ /// Thrown if blockData length is invalid.
+ public static byte[] DecompressImage(
+ byte[] blockData,
+ int width,
+ int height,
+ int blockWidth,
+ int blockHeight,
+ byte compressedBytesPerBlock)
+ {
+ ArgumentNullException.ThrowIfNull(blockData);
+ ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(width, 0);
+ ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(height, 0);
+
+ if (compressedBytesPerBlock != AstcBlockSize)
+ {
+ throw new ArgumentOutOfRangeException(nameof(compressedBytesPerBlock), compressedBytesPerBlock, $"ASTC blocks must be {AstcBlockSize} bytes.");
+ }
+
+ // Validate block dimensions (will throw if invalid)
+ _ = FootprintFromDimensions(blockWidth, blockHeight);
+
+ int blocksWide = (width + blockWidth - 1) / blockWidth;
+ int blocksHigh = (height + blockHeight - 1) / blockHeight;
+ int totalBlocks = blocksWide * blocksHigh;
+ int expectedDataLength = totalBlocks * compressedBytesPerBlock;
+
+ if (blockData.Length < expectedDataLength)
+ {
+ throw new ArgumentException($"Block data is too small. Expected at least {expectedDataLength} bytes for {width}x{height} texture with {blockWidth}x{blockHeight} blocks, but received {blockData.Length} bytes.", nameof(blockData));
+ }
+
+ byte[] decompressedData = new byte[width * height * 4];
+ byte[] decodedBlock = new byte[blockWidth * blockHeight * 4];
+
+ int blockIndex = 0;
+
+ for (int by = 0; by < blocksHigh; by++)
+ {
+ for (int bx = 0; bx < blocksWide; bx++)
+ {
+ int blockDataOffset = blockIndex * compressedBytesPerBlock;
+ if (blockDataOffset + compressedBytesPerBlock <= blockData.Length)
+ {
+ DecodeBlock(
+ blockData.AsSpan(blockDataOffset, compressedBytesPerBlock),
+ blockWidth,
+ blockHeight,
+ decodedBlock);
+
+ for (int py = 0; py < blockHeight && ((by * blockHeight) + py) < height; py++)
+ {
+ for (int px = 0; px < blockWidth && ((bx * blockWidth) + px) < width; px++)
+ {
+ int srcIndex = ((py * blockWidth) + px) * 4;
+ int dstX = (bx * blockWidth) + px;
+ int dstY = (by * blockHeight) + py;
+ int dstIndex = ((dstY * width) + dstX) * 4;
+
+ decompressedData[dstIndex] = decodedBlock[srcIndex];
+ decompressedData[dstIndex + 1] = decodedBlock[srcIndex + 1];
+ decompressedData[dstIndex + 2] = decodedBlock[srcIndex + 2];
+ decompressedData[dstIndex + 3] = decodedBlock[srcIndex + 3];
+ }
+ }
+ }
+
+ blockIndex++;
+ }
+ }
+
+ return decompressedData;
+ }
+
+ private static FootprintType FootprintFromDimensions(int width, int height)
+ => (width, height) switch
+ {
+ (4, 4) => FootprintType.Footprint4x4,
+ (5, 4) => FootprintType.Footprint5x4,
+ (5, 5) => FootprintType.Footprint5x5,
+ (6, 5) => FootprintType.Footprint6x5,
+ (6, 6) => FootprintType.Footprint6x6,
+ (8, 5) => FootprintType.Footprint8x5,
+ (8, 6) => FootprintType.Footprint8x6,
+ (8, 8) => FootprintType.Footprint8x8,
+ (10, 5) => FootprintType.Footprint10x5,
+ (10, 6) => FootprintType.Footprint10x6,
+ (10, 8) => FootprintType.Footprint10x8,
+ (10, 10) => FootprintType.Footprint10x10,
+ (12, 10) => FootprintType.Footprint12x10,
+ (12, 12) => FootprintType.Footprint12x12,
+ _ => throw new ArgumentOutOfRangeException(nameof(width), $"Invalid ASTC block dimensions: {width}x{height}. Valid sizes are 4x4, 5x4, 5x5, 6x5, 6x6, 8x5, 8x6, 8x8, 10x5, 10x6, 10x8, 10x10, 12x10, 12x12."),
+ };
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x10.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x10.cs
new file mode 100644
index 00000000..7f13e992
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x10.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc10x10.
+///
+internal readonly struct RgbaAstc10X10 : IBlock
+{
+ public static Size BlockSize => new(10, 10);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 10;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs
new file mode 100644
index 00000000..d75b58a9
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc10x5.
+///
+internal readonly struct RgbaAstc10X5 : IBlock
+{
+ public static Size BlockSize => new(10, 5);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 10;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x6.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x6.cs
new file mode 100644
index 00000000..1f041af9
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x6.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc10x6.
+///
+internal readonly struct RgbaAstc10X6 : IBlock
+{
+ public static Size BlockSize => new(10, 6);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 10;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x8.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x8.cs
new file mode 100644
index 00000000..d2883ded
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x8.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc10x8.
+///
+internal readonly struct RgbaAstc10X8 : IBlock
+{
+ public static Size BlockSize => new(10, 8);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 10;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x10.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x10.cs
new file mode 100644
index 00000000..38a4e1aa
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x10.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc12x10.
+///
+internal readonly struct RgbaAstc12X10 : IBlock
+{
+ public static Size BlockSize => new(12, 10);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 12;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x12.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x12.cs
new file mode 100644
index 00000000..0c0d9347
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x12.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc12x12.
+///
+internal readonly struct RgbaAstc12X12 : IBlock
+{
+ public static Size BlockSize => new(12, 12);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 12;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc4x4.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc4x4.cs
new file mode 100644
index 00000000..c1068b05
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc4x4.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc4x4.
+///
+internal readonly struct RgbaAstc4X4 : IBlock
+{
+ // See https://developer.nvidia.com/astc-texture-compression-for-game-assets
+ // https://chromium.googlesource.com/external/github.com/ARM-software/astc-encoder/+/HEAD/Docs/FormatOverview.md
+ public static Size BlockSize => new(4, 4);
+
+ ///
+ // The 2D block footprints in ASTC range from 4x4 texels up to 12x12 texels, which all compress into 128-bit output blocks.
+ // By dividing 128 bits by the number of texels in the footprint, we derive the format bit rates which range from 8 bpt(128/(4*4)) down to 0.89 bpt(128/(12*12)).
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 4;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc5x4.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc5x4.cs
new file mode 100644
index 00000000..f08b8494
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc5x4.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc5x4.
+///
+internal readonly struct RgbaAstc5X4 : IBlock
+{
+ public static Size BlockSize => new(5, 4);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 5;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc5x5.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc5x5.cs
new file mode 100644
index 00000000..dd7efeb7
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc5x5.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc5x5.
+///
+internal readonly struct RgbaAstc5X5 : IBlock
+{
+ public static Size BlockSize => new(5, 5);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 5;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc6x5.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc6x5.cs
new file mode 100644
index 00000000..f28368a6
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc6x5.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc6x5.
+///
+internal readonly struct RgbaAstc6X5 : IBlock
+{
+ public static Size BlockSize => new(6, 5);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 6;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc6x6.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc6x6.cs
new file mode 100644
index 00000000..ce71e216
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc6x6.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc6x6.
+///
+internal readonly struct RgbaAstc6X6 : IBlock
+{
+ public static Size BlockSize => new(6, 6);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 6;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x5.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x5.cs
new file mode 100644
index 00000000..5f9b80b7
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x5.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc8x5.
+///
+internal readonly struct RgbaAstc8X5 : IBlock
+{
+ public static Size BlockSize => new(8, 5);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 8;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x6.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x6.cs
new file mode 100644
index 00000000..984859ea
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x6.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc8x6.
+///
+internal readonly struct RgbaAstc8X6 : IBlock
+{
+ public static Size BlockSize => new(8, 6);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 8;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x8.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x8.cs
new file mode 100644
index 00000000..da93a202
--- /dev/null
+++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc8x8.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+///
+/// Texture compressed with RgbaAstc8x8.
+///
+internal readonly struct RgbaAstc8X8 : IBlock
+{
+ public static Size BlockSize => new(8, 8);
+
+ ///
+ public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height);
+
+ ///
+ public byte PixelDepthBytes => 4;
+
+ ///
+ public byte DivSize => 8;
+
+ ///
+ public byte CompressedBytesPerBlock => 16;
+
+ ///
+ public bool Compressed => true;
+
+ ///
+ public Image GetImage(byte[] blockData, int width, int height)
+ {
+ byte[] decompressedData = this.Decompress(blockData, width, height);
+ return Image.LoadPixelData(decompressedData, width, height);
+ }
+
+ ///
+ public byte[] Decompress(byte[] blockData, int width, int height) =>
+ AstcDecoder.DecompressImage(blockData, width, height, BlockSize.Width, BlockSize.Height, this.CompressedBytesPerBlock);
+}
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
index 775062ad..527225fe 100644
--- a/tests/Directory.Build.props
+++ b/tests/Directory.Build.props
@@ -11,7 +11,7 @@
-->
-
+
diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs
new file mode 100644
index 00000000..71482d92
--- /dev/null
+++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Textures.Formats.Ktx;
+using SixLabors.ImageSharp.Textures.Tests.Enums;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders;
+using SixLabors.ImageSharp.Textures.TextureFormats;
+
+namespace SixLabors.ImageSharp.Textures.Tests.Formats.Ktx;
+
+[Trait("Format", "Ktx")]
+[Trait("Format", "Astc")]
+public class KtxAstcDecoderTests
+{
+ private static readonly KtxDecoder KtxDecoder = new();
+
+ [Theory]
+ [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Astc.Rgb32_8x8)]
+ public void KtxAstcDecoder_CanDecode_Rgba32_Blocksizes(TestTextureProvider provider)
+ {
+ using Texture texture = provider.GetTexture(KtxDecoder);
+ provider.SaveTextures(texture);
+ FlatTexture flatTexture = texture as FlatTexture;
+
+ Assert.NotNull(flatTexture?.MipMaps);
+ Assert.Single(flatTexture.MipMaps);
+
+ Image firstMipMap = flatTexture.MipMaps[0].GetImage();
+ Assert.Equal(256, firstMipMap.Width);
+ Assert.Equal(256, firstMipMap.Height);
+ Assert.Equal(32, firstMipMap.PixelType.BitsPerPixel);
+
+ Image firstMipMapImage = firstMipMap as Image;
+
+ firstMipMapImage.CompareToReferenceOutput(provider, appendPixelTypeToFileName: false);
+ }
+}
diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs
new file mode 100644
index 00000000..63e8d375
--- /dev/null
+++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Textures.Formats.Ktx2;
+using SixLabors.ImageSharp.Textures.Tests.Enums;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities.ImageComparison;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders;
+using SixLabors.ImageSharp.Textures.TextureFormats;
+
+namespace SixLabors.ImageSharp.Textures.Tests.Formats.Ktx2;
+
+[Trait("Format", "Ktx2")]
+[Trait("Format", "Astc")]
+public class Ktx2AstcDecoderCubemapTests
+{
+ private static readonly Ktx2Decoder KtxDecoder = new();
+
+ [Theory]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Cubemap, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Srgb_6x6_Cube)]
+ public void Ktx2AstcDecoder_CanDecode_All_Faces(TestTextureProvider provider)
+ {
+ using Texture texture = provider.GetTexture(KtxDecoder);
+ provider.SaveTextures(texture);
+ CubemapTexture cubemapTexture = texture as CubemapTexture;
+
+ using Image posXImage = cubemapTexture.PositiveX.MipMaps[0].GetImage();
+ (posXImage as Image).CompareToReferenceOutput(
+ ImageComparer.Exact,
+ provider,
+ testOutputDetails: "posX");
+
+ using Image negXImage = cubemapTexture.NegativeX.MipMaps[0].GetImage();
+ (negXImage as Image).CompareToReferenceOutput(
+ ImageComparer.Exact,
+ provider,
+ testOutputDetails: "negX");
+
+ using Image posYImage = cubemapTexture.PositiveY.MipMaps[0].GetImage();
+ (posYImage as Image).CompareToReferenceOutput(
+ ImageComparer.Exact,
+ provider,
+ testOutputDetails: "posY");
+
+ using Image negYImage = cubemapTexture.NegativeY.MipMaps[0].GetImage();
+ (negYImage as Image).CompareToReferenceOutput(
+ ImageComparer.TolerantPercentage(3.0f),
+ provider,
+ testOutputDetails: "negY");
+
+ using Image posZImage = cubemapTexture.PositiveZ.MipMaps[0].GetImage();
+ (posZImage as Image).CompareToReferenceOutput(
+ ImageComparer.Exact,
+ provider,
+ testOutputDetails: "posZ");
+
+ using Image negZImage = cubemapTexture.NegativeZ.MipMaps[0].GetImage();
+ (negZImage as Image).CompareToReferenceOutput(
+ ImageComparer.Exact,
+ provider,
+ testOutputDetails: "negZ");
+ }
+}
diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderFlatTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderFlatTests.cs
new file mode 100644
index 00000000..b8efc920
--- /dev/null
+++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderFlatTests.cs
@@ -0,0 +1,194 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Text.RegularExpressions;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Textures.Formats.Ktx2;
+using SixLabors.ImageSharp.Textures.Tests.Enums;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities.ImageComparison;
+using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders;
+using SixLabors.ImageSharp.Textures.TextureFormats;
+
+namespace SixLabors.ImageSharp.Textures.Tests.Formats.Ktx2;
+
+[Trait("Format", "Ktx2")]
+[Trait("Format", "Astc")]
+public partial class Ktx2AstcDecoderFlatTests
+{
+ private static readonly Ktx2Decoder KtxDecoder = new();
+
+ [Theory]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_4x4)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_5x4)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_5x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_6x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_6x6)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_8x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_8x6)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_8x8)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_10x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_10x6)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_10x8)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_10x10)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_12x10)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgba32_12x12)]
+ public void Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes(TestTextureProvider provider)
+ {
+ using Texture texture = provider.GetTexture(KtxDecoder);
+ provider.SaveTextures(texture);
+ FlatTexture flatTexture = texture as FlatTexture;
+
+ Assert.NotNull(flatTexture?.MipMaps);
+ Assert.Single(flatTexture.MipMaps);
+
+ using Image firstMipMap = flatTexture.MipMaps[0].GetImage();
+ Assert.Equal(256, firstMipMap.Width);
+ Assert.Equal(256, firstMipMap.Height);
+ Assert.Equal(32, firstMipMap.PixelType.BitsPerPixel);
+
+ Image firstMipMapImage = firstMipMap as Image;
+
+ // Note that the comparer is given a higher threshold to allow for the lossy compression of ASTC,
+ // especially at larger block sizes, but the output is still expected to be very similar to the reference image.
+ // A single reference image is used to save on the amount of test data otherwise required for each block size.
+ firstMipMapImage.CompareToReferenceOutput(ImageComparer.TolerantPercentage(4.0f), provider, appendPixelTypeToFileName: false);
+ }
+
+ [Theory]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_4x4)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_5x4)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_5x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_6x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_6x6)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_8x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_8x6)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_8x8)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_10x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_10x6)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_10x8)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_10x10)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_12x10)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_12x12)]
+ public void Ktx2AstcDecoder_CanDecode_Rgba32_Unorm(TestTextureProvider provider)
+ {
+ string blockSize = GetBlockSizeFromFileName(provider.InputFile);
+ using Texture texture = provider.GetTexture(KtxDecoder);
+ provider.SaveTextures(texture);
+ FlatTexture flatTexture = texture as FlatTexture;
+
+ Assert.NotNull(flatTexture?.MipMaps);
+ Assert.Single(flatTexture.MipMaps);
+
+ using Image firstMipMap = flatTexture.MipMaps[0].GetImage();
+ Assert.Equal(16, firstMipMap.Width);
+ Assert.Equal(16, firstMipMap.Height);
+ Assert.Equal(32, firstMipMap.PixelType.BitsPerPixel);
+
+ (firstMipMap as Image).CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.05f), provider, testOutputDetails: $"{blockSize}");
+ }
+
+ [Theory]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_4x4)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_5x4)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_5x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_6x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_6x6)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_8x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_8x6)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_8x8)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_10x5)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_10x6)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_10x8)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_10x10)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_12x10)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_sRgb_12x12)]
+ public void Ktx2AstcDecoder_CanDecode_Rgba32_Srgb(TestTextureProvider provider)
+ {
+ string blockSize = GetBlockSizeFromFileName(provider.InputFile);
+ using Texture texture = provider.GetTexture(KtxDecoder);
+ provider.SaveTextures(texture);
+ FlatTexture flatTexture = texture as FlatTexture;
+
+ Assert.NotNull(flatTexture?.MipMaps);
+ Assert.Single(flatTexture.MipMaps);
+
+ using Image firstMipMap = flatTexture.MipMaps[0].GetImage();
+ Assert.Equal(16, firstMipMap.Width);
+ Assert.Equal(16, firstMipMap.Height);
+ Assert.Equal(32, firstMipMap.PixelType.BitsPerPixel);
+
+ (firstMipMap as Image).CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.05f), provider, testOutputDetails: $"{blockSize}");
+ }
+
+ [Theory]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Srgb_Large)]
+ public void Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_Large(TestTextureProvider provider)
+ {
+ using Texture texture = provider.GetTexture(KtxDecoder);
+ provider.SaveTextures(texture);
+ FlatTexture flatTexture = texture as FlatTexture;
+
+ using Image firstMipMap = flatTexture.MipMaps[0].GetImage();
+ Assert.Equal(2048, firstMipMap.Width);
+ Assert.Equal(2048, firstMipMap.Height);
+ Assert.Equal(32, firstMipMap.PixelType.BitsPerPixel);
+
+ (firstMipMap as Image).CompareToReferenceOutput(ImageComparer.Exact, provider);
+ }
+
+ [Theory]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Srgb_6x6_MipMap)]
+ public void Ktx2AstcDecoder_CanDecode_MipMaps(TestTextureProvider provider)
+ {
+ int mimMapLevel = 0;
+
+ using Texture texture = provider.GetTexture(KtxDecoder);
+ provider.SaveTextures(texture);
+ FlatTexture flatTexture = texture as FlatTexture;
+
+ foreach (MipMap mipMap in flatTexture.MipMaps)
+ {
+ using Image image = mipMap.GetImage();
+ (image as Image).CompareToReferenceOutput(ImageComparer.Exact, provider, testOutputDetails: $"{mimMapLevel++}");
+ }
+ }
+
+ [Theory(Skip = "Supercompression support not yet implemented")]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_4x4_Zlib1)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_4x4_Zlib9)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_4x4_Zstd1)]
+ [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Unorm_4x4_Zstd9)]
+ public void Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed(TestTextureProvider provider)
+ {
+ string fileName = Path.GetFileNameWithoutExtension(provider.InputFile);
+ string compressionDetails = fileName.Contains("ZLIB", StringComparison.Ordinal)
+ ? fileName[fileName.IndexOf("ZLIB", StringComparison.Ordinal)..]
+ : fileName[fileName.IndexOf("ZSTD", StringComparison.Ordinal)..];
+
+ using Texture texture = provider.GetTexture(KtxDecoder);
+ provider.SaveTextures(texture);
+ FlatTexture flatTexture = texture as FlatTexture;
+
+ Assert.NotNull(flatTexture?.MipMaps);
+ Assert.Single(flatTexture.MipMaps);
+
+ using Image firstMipMap = flatTexture.MipMaps[0].GetImage();
+ Assert.Equal(16, firstMipMap.Width);
+ Assert.Equal(16, firstMipMap.Height);
+ Assert.Equal(32, firstMipMap.PixelType.BitsPerPixel);
+
+ (firstMipMap as Image).CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.05f), provider, testOutputDetails: $"{compressionDetails}");
+ }
+
+ private static string GetBlockSizeFromFileName(string fileName)
+ {
+ Match match = GetBlockSizeFromFileName().Match(fileName);
+
+ return match.Success ? match.Value : string.Empty;
+ }
+
+ [GeneratedRegex(@"(\d+x\d+)")]
+ private static partial Regex GetBlockSizeFromFileName();
+}
diff --git a/tests/ImageSharp.Textures.Tests/TestImages.cs b/tests/ImageSharp.Textures.Tests/TestImages.cs
index d3df36c1..c405b47a 100644
--- a/tests/ImageSharp.Textures.Tests/TestImages.cs
+++ b/tests/ImageSharp.Textures.Tests/TestImages.cs
@@ -1,16 +1,88 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Textures.Tests
+namespace SixLabors.ImageSharp.Textures.Tests;
+
+///
+/// Class that contains all the relative test image paths in the Images/Input/Formats directory.
+///
+public static class TestImages
{
- ///
- /// Class that contains all the relative test image paths in the Images/Input/Formats directory.
- ///
- public static class TestImages
+ public static class Ktx
+ {
+ public const string Rgba = "rgba8888.ktx";
+
+ public static class Astc
+ {
+ public const string Rgb32_8x8 = "astc-rgba32-8x8.ktx";
+ }
+ }
+
+ public static class Ktx2
{
- public static class Ktx
+ public static class Astc
{
- public const string Rgba = "rgba8888.ktx";
+ // Flat textures with various block sizes
+ public const string Rgba32_4x4 = "astc_rgba32_4x4.ktx2";
+ public const string Rgba32_5x4 = "astc_rgba32_5x4.ktx2";
+ public const string Rgba32_5x5 = "astc_rgba32_5x5.ktx2";
+ public const string Rgba32_6x5 = "astc_rgba32_6x5.ktx2";
+ public const string Rgba32_6x6 = "astc_rgba32_6x6.ktx2";
+ public const string Rgba32_8x5 = "astc_rgba32_8x5.ktx2";
+ public const string Rgba32_8x6 = "astc_rgba32_8x6.ktx2";
+ public const string Rgba32_8x8 = "astc_rgba32_8x8.ktx2";
+ public const string Rgba32_10x5 = "astc_rgba32_10x5.ktx2";
+ public const string Rgba32_10x6 = "astc_rgba32_10x6.ktx2";
+ public const string Rgba32_10x8 = "astc_rgba32_10x8.ktx2";
+ public const string Rgba32_10x10 = "astc_rgba32_10x10.ktx2";
+ public const string Rgba32_12x10 = "astc_rgba32_12x10.ktx2";
+ public const string Rgba32_12x12 = "astc_rgba32_12x12.ktx2";
+
+ public const string Rgb32_sRgb_4x4 = "valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_5x4 = "valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_5x5 = "valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_6x5 = "valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_6x6 = "valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_8x5 = "valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_8x6 = "valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_8x8 = "valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_10x5 = "valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_10x6 = "valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_10x8 = "valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_10x10 = "valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_12x10 = "valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2";
+ public const string Rgb32_sRgb_12x12 = "valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2";
+
+ public const string Rgb32_Unorm_4x4 = "valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_5x4 = "valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_5x5 = "valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_6x5 = "valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_6x6 = "valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_8x5 = "valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_8x6 = "valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_8x8 = "valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_10x5 = "valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_10x6 = "valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_10x8 = "valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_10x10 = "valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_12x10 = "valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2";
+ public const string Rgb32_Unorm_12x12 = "valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2";
+
+ public const string Rgb32_Srgb_Large = "astc_ldr_10x5_FlightHelmet_baseColor.ktx2";
+
+ // Textures with several levels of MipMaps
+ public const string Rgb32_Srgb_6x6_MipMap = "astc_ldr_6x6_arraytex_7_mipmap.ktx2";
+
+ // Supercompressed textures (ZLIB)
+ public const string Rgb32_Unorm_4x4_Zlib1 = "valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2";
+ public const string Rgb32_Unorm_4x4_Zlib9 = "valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2";
+
+ // Supercompressed textures (ZSTD)
+ public const string Rgb32_Unorm_4x4_Zstd1 = "valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2";
+ public const string Rgb32_Unorm_4x4_Zstd9 = "valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2";
+
+ // Cubemap textures
+ public const string Rgb32_Srgb_6x6_Cube = "astc_ldr_cubemap_6x6.ktx2";
}
}
}
diff --git a/tests/ImageSharp.Textures.Tests/TextureFormats/Decoding/AstcDecoderTests.cs b/tests/ImageSharp.Textures.Tests/TextureFormats/Decoding/AstcDecoderTests.cs
new file mode 100644
index 00000000..f98efec7
--- /dev/null
+++ b/tests/ImageSharp.Textures.Tests/TextureFormats/Decoding/AstcDecoderTests.cs
@@ -0,0 +1,331 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.TextureFormats.Decoding;
+
+namespace SixLabors.ImageSharp.Textures.Tests.TextureFormats.Decoding;
+
+[Trait("Format", "Astc")]
+public class AstcDecoderTests
+{
+ [Theory]
+ [InlineData(4, 4)]
+ [InlineData(5, 4)]
+ [InlineData(5, 5)]
+ [InlineData(6, 5)]
+ [InlineData(6, 6)]
+ [InlineData(8, 5)]
+ [InlineData(8, 6)]
+ [InlineData(8, 8)]
+ [InlineData(10, 5)]
+ [InlineData(10, 6)]
+ [InlineData(10, 8)]
+ [InlineData(10, 10)]
+ [InlineData(12, 10)]
+ [InlineData(12, 12)]
+ public void DecodeBlock_WithValidBlockData_DoesNotThrow(int blockWidth, int blockHeight)
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize]; // ASTC blocks are always 16 bytes
+ byte[] decodedPixels = new byte[blockWidth * blockHeight * 4];
+
+ AstcDecoder.DecodeBlock(blockData, blockWidth, blockHeight, decodedPixels);
+
+ Assert.Equal(blockWidth * blockHeight * 4, decodedPixels.Length);
+ }
+
+ [Fact]
+ public void DecodeBlock_WithTooSmallBlockData_ThrowsArgumentException()
+ {
+ byte[] blockData = new byte[15]; // Too small
+ byte[] decodedPixels = new byte[4 * 4 * 4];
+
+ ArgumentException ex = Assert.Throws(() =>
+ AstcDecoder.DecodeBlock(blockData, 4, 4, decodedPixels));
+
+ Assert.Contains("16 bytes", ex.Message);
+ Assert.Contains("blockData", ex.ParamName);
+ }
+
+ [Fact]
+ public void DecodeBlock_WithTooLargeBlockData_ThrowsArgumentException()
+ {
+ byte[] blockData = new byte[17]; // Too large
+ byte[] decodedPixels = new byte[4 * 4 * 4];
+
+ ArgumentException ex = Assert.Throws(() =>
+ AstcDecoder.DecodeBlock(blockData, 4, 4, decodedPixels));
+
+ Assert.Contains("16 bytes", ex.Message);
+ Assert.Contains("blockData", ex.ParamName);
+ }
+
+ [Fact]
+ public void DecodeBlock_WithEmptyBlockData_ThrowsArgumentException()
+ {
+ byte[] blockData = [];
+ byte[] decodedPixels = new byte[4 * 4 * 4];
+
+ ArgumentException ex = Assert.Throws(() =>
+ AstcDecoder.DecodeBlock(blockData, 4, 4, decodedPixels));
+
+ Assert.Contains("blockData", ex.ParamName);
+ }
+
+ [Fact]
+ public void DecodeBlock_WithTooSmallOutputBuffer_ThrowsArgumentException()
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+ byte[] decodedPixels = new byte[10]; // Too small for 4x4 block (needs 64 bytes)
+
+ ArgumentException ex = Assert.Throws(() =>
+ AstcDecoder.DecodeBlock(blockData, 4, 4, decodedPixels));
+
+ Assert.Contains("Output buffer", ex.Message);
+ Assert.Contains("decodedPixels", ex.ParamName);
+ }
+
+ [Theory]
+ [InlineData(3, 3)]
+ [InlineData(4, 3)]
+ [InlineData(3, 4)]
+ [InlineData(7, 7)]
+ [InlineData(11, 11)]
+ [InlineData(13, 13)]
+ [InlineData(16, 16)]
+ public void DecodeBlock_WithInvalidBlockDimensions_ThrowsArgumentOutOfRangeException(int blockWidth, int blockHeight)
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+ byte[] decodedPixels = new byte[blockWidth * blockHeight * 4];
+
+ ArgumentOutOfRangeException ex = Assert.Throws(() =>
+ AstcDecoder.DecodeBlock(blockData, blockWidth, blockHeight, decodedPixels));
+
+ Assert.Contains("Invalid ASTC block dimensions", ex.Message);
+ }
+
+ [Fact]
+ public void DecodeBlock_WithZeroBlockWidth_ThrowsArgumentOutOfRangeException()
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+ byte[] decodedPixels = new byte[64];
+
+ Assert.Throws(() =>
+ AstcDecoder.DecodeBlock(blockData, 0, 4, decodedPixels));
+ }
+
+ [Fact]
+ public void DecodeBlock_WithNegativeBlockWidth_ThrowsArgumentOutOfRangeException()
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+ byte[] decodedPixels = new byte[64];
+
+ Assert.Throws(() =>
+ AstcDecoder.DecodeBlock(blockData, -1, 4, decodedPixels));
+ }
+
+ [Fact]
+ public void DecodeBlock_WithNegativeBlockHeight_ThrowsArgumentOutOfRangeException()
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+ byte[] decodedPixels = new byte[64];
+
+ Assert.Throws(() =>
+ AstcDecoder.DecodeBlock(blockData, 4, -1, decodedPixels));
+ }
+
+ [Theory]
+ [InlineData(256, 256, 4, 4)]
+ [InlineData(512, 512, 8, 8)]
+ [InlineData(128, 128, 6, 6)]
+ [InlineData(200, 200, 10, 10)]
+ public void DecompressImage_WithValidParameters_ReturnsCorrectSizedArray(int width, int height, int blockWidth, int blockHeight)
+ {
+ int blocksWide = (width + blockWidth - 1) / blockWidth;
+ int blocksHigh = (height + blockHeight - 1) / blockHeight;
+ int totalBlocks = blocksWide * blocksHigh;
+ byte[] blockData = new byte[totalBlocks * AstcDecoder.AstcBlockSize];
+
+ byte[] result = AstcDecoder.DecompressImage(blockData, width, height, blockWidth, blockHeight, AstcDecoder.AstcBlockSize);
+
+ Assert.Equal(width * height * 4, result.Length);
+ }
+
+ [Fact]
+ public void DecompressImage_WithNullBlockData_ThrowsArgumentNullException()
+ {
+ ArgumentNullException ex = Assert.Throws(() =>
+ AstcDecoder.DecompressImage(null, 256, 256, 4, 4, 16));
+
+ Assert.Equal("blockData", ex.ParamName);
+ }
+
+ [Fact]
+ public void DecompressImage_WithZeroWidth_ThrowsArgumentOutOfRangeException()
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+
+ ArgumentOutOfRangeException ex = Assert.Throws(() =>
+ AstcDecoder.DecompressImage(blockData, 0, 256, 4, 4, 16));
+
+ Assert.Equal("width", ex.ParamName);
+ }
+
+ [Fact]
+ public void DecompressImage_WithNegativeWidth_ThrowsArgumentOutOfRangeException()
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+
+ ArgumentOutOfRangeException ex = Assert.Throws(() =>
+ AstcDecoder.DecompressImage(blockData, -256, 256, 4, 4, 16));
+
+ Assert.Equal("width", ex.ParamName);
+ }
+
+ [Fact]
+ public void DecompressImage_WithZeroHeight_ThrowsArgumentOutOfRangeException()
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+
+ ArgumentOutOfRangeException ex = Assert.Throws(() =>
+ AstcDecoder.DecompressImage(blockData, 256, 0, 4, 4, 16));
+
+ Assert.Equal("height", ex.ParamName);
+ }
+
+ [Fact]
+ public void DecompressImage_WithNegativeHeight_ThrowsArgumentOutOfRangeException()
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+
+ ArgumentOutOfRangeException ex = Assert.Throws(() =>
+ AstcDecoder.DecompressImage(blockData, 256, -256, 4, 4, 16));
+
+ Assert.Equal("height", ex.ParamName);
+ }
+
+ [Theory]
+ [InlineData(15)]
+ [InlineData(17)]
+ [InlineData(32)]
+ [InlineData(8)]
+ public void DecompressImage_WithInvalidCompressedBytesPerBlock_ThrowsArgumentOutOfRangeException(byte invalidBytes)
+ {
+ byte[] blockData = new byte[invalidBytes * 64]; // 8x8 blocks for 256x256
+
+ ArgumentOutOfRangeException ex = Assert.Throws(() =>
+ AstcDecoder.DecompressImage(blockData, 256, 256, 4, 4, invalidBytes));
+
+ Assert.Equal("compressedBytesPerBlock", ex.ParamName);
+ Assert.Contains("16 bytes", ex.Message);
+ }
+
+ [Theory]
+ [InlineData(3, 3)]
+ [InlineData(4, 3)]
+ [InlineData(7, 7)]
+ [InlineData(16, 16)]
+ public void DecompressImage_WithInvalidBlockDimensions_ThrowsArgumentOutOfRangeException(int blockWidth, int blockHeight)
+ {
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+
+ ArgumentOutOfRangeException ex = Assert.Throws(() =>
+ AstcDecoder.DecompressImage(blockData, 256, 256, blockWidth, blockHeight, 16));
+
+ Assert.Contains("Invalid ASTC block dimensions", ex.Message);
+ }
+
+ [Fact]
+ public void DecompressImage_WithTooSmallBlockData_ThrowsArgumentException()
+ {
+ // For 256x256 with 4x4 blocks, we need 64x64 = 4096 blocks * 16 bytes = 65536 bytes
+ byte[] blockData = new byte[1000]; // Too small
+
+ ArgumentException ex = Assert.Throws(() =>
+ AstcDecoder.DecompressImage(blockData, 256, 256, 4, 4, 16));
+
+ Assert.Equal("blockData", ex.ParamName);
+ Assert.Contains("too small", ex.Message);
+ }
+
+ [Fact]
+ public void DecompressImage_WithEmptyBlockData_ThrowsArgumentException()
+ {
+ byte[] blockData = [];
+
+ ArgumentException ex = Assert.Throws(() =>
+ AstcDecoder.DecompressImage(blockData, 256, 256, 4, 4, 16));
+
+ Assert.Equal("blockData", ex.ParamName);
+ }
+
+ [Theory]
+ [InlineData(257, 256)] // Width not multiple of block size
+ [InlineData(256, 257)] // Height not multiple of block size
+ [InlineData(255, 255)] // Both not multiples
+ [InlineData(100, 100)] // Different non-multiples
+ public void DecompressImage_WithNonMultipleImageSizes_ReturnExpectedSize(int width, int height)
+ {
+ int blockWidth = 4;
+ int blockHeight = 4;
+ int blocksWide = (width + blockWidth - 1) / blockWidth;
+ int blocksHigh = (height + blockHeight - 1) / blockHeight;
+ int totalBlocks = blocksWide * blocksHigh;
+ byte[] blockData = new byte[totalBlocks * 16];
+
+ byte[] result = AstcDecoder.DecompressImage(blockData, width, height, blockWidth, blockHeight, 16);
+
+ Assert.Equal(width * height * 4, result.Length);
+ }
+
+ [Theory]
+ [InlineData(1, 1)]
+ [InlineData(2, 2)]
+ [InlineData(3, 3)]
+ public void DecompressImage_WithVerySmallImages_ReturnExpectedSize(int width, int height)
+ {
+ // Even tiny images need at least one block
+ byte[] blockData = new byte[AstcDecoder.AstcBlockSize];
+
+ byte[] result = AstcDecoder.DecompressImage(blockData, width, height, 4, 4, 16);
+
+ Assert.Equal(width * height * 4, result.Length);
+ }
+
+ [Theory]
+ [InlineData(4096, 4096, 4, 4)]
+ [InlineData(2048, 2048, 8, 8)]
+ public void DecompressImage_WithLargeImages_ReturnExpectedSize(int width, int height, int blockWidth, int blockHeight)
+ {
+ int blocksWide = (width + blockWidth - 1) / blockWidth;
+ int blocksHigh = (height + blockHeight - 1) / blockHeight;
+ int totalBlocks = blocksWide * blocksHigh;
+ byte[] blockData = new byte[totalBlocks * 16];
+
+ byte[] result = AstcDecoder.DecompressImage(blockData, width, height, blockWidth, blockHeight, 16);
+
+ Assert.Equal(width * height * 4, result.Length);
+ }
+
+ [Fact]
+ public void DecompressImage_WithExactBlockDataSize_ReturnExpectedSize()
+ {
+ // 256x256 with 4x4 blocks = 64x64 blocks = 4096 blocks * 16 bytes = 65536 bytes
+ byte[] blockData = new byte[65536];
+
+ byte[] result = AstcDecoder.DecompressImage(blockData, 256, 256, 4, 4, 16);
+
+ Assert.Equal(256 * 256 * 4, result.Length);
+ }
+
+ [Fact]
+ public void DecompressImage_WithExtraBlockData_ReturnExpectedSize()
+ {
+ // More data than needed should work (extra data ignored)
+ byte[] blockData = new byte[100000];
+
+ byte[] result = AstcDecoder.DecompressImage(blockData, 256, 256, 4, 4, 16);
+
+ Assert.Equal(256 * 256 * 4, result.Length);
+ }
+
+}
diff --git a/tests/Images/Input/Ktx/astc-rgba32-8x8.ktx b/tests/Images/Input/Ktx/astc-rgba32-8x8.ktx
new file mode 100644
index 00000000..0e2893a4
--- /dev/null
+++ b/tests/Images/Input/Ktx/astc-rgba32-8x8.ktx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:046f6f04070340bfaa465129a49e4ea082923a5de83fc9d819d6abea4bf779cf
+size 114788
diff --git a/tests/Images/Input/Ktx2/astc_ldr_10x5_FlightHelmet_baseColor.ktx2 b/tests/Images/Input/Ktx2/astc_ldr_10x5_FlightHelmet_baseColor.ktx2
new file mode 100644
index 00000000..0c9ffd3e
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_ldr_10x5_FlightHelmet_baseColor.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:42deecaeef010d228ac8662fe10b2dcf6e3e30bd6955f76f73b89296afd51e84
+size 1345104
diff --git a/tests/Images/Input/Ktx2/astc_ldr_6x6_arraytex_7_mipmap.ktx2 b/tests/Images/Input/Ktx2/astc_ldr_6x6_arraytex_7_mipmap.ktx2
new file mode 100644
index 00000000..d1a3374a
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_ldr_6x6_arraytex_7_mipmap.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3dfb9321bbfe55f1f12c85ec298f595c183212c7082a6ea298b8c4633483537a
+size 2192
diff --git a/tests/Images/Input/Ktx2/astc_ldr_cubemap_6x6.ktx2 b/tests/Images/Input/Ktx2/astc_ldr_cubemap_6x6.ktx2
new file mode 100644
index 00000000..9a10d9fe
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_ldr_cubemap_6x6.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:70cdc0097301bb634c86acea3484b4c29f0ce1ebf0f23895fd86917b4ffb54ca
+size 11228848
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_10x10.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_10x10.ktx2
new file mode 100644
index 00000000..2334aeda
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_10x10.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:512c8f4d0352492ac70fb8d72c69802980cefd3de0ccfe179653f74028e1a548
+size 11088
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_10x5.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_10x5.ktx2
new file mode 100644
index 00000000..76bd7adb
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_10x5.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:80c0ea7f71b120a09e8c3a1ac39e6677e0cc2a663f30d0c7b349d93386741314
+size 21904
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_10x6.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_10x6.ktx2
new file mode 100644
index 00000000..26940296
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_10x6.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7fababed3062e1a82b51ad4ca443423fa2cab21ff74166ff24cd5a7583f82da6
+size 18160
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_10x8.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_10x8.ktx2
new file mode 100644
index 00000000..084e8eaf
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_10x8.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e2c61a06ee3f2301c7acc1518808b934373349ba4938cdc3c40057b651143b40
+size 13584
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_12x10.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_12x10.ktx2
new file mode 100644
index 00000000..3ee93205
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_12x10.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:78353f3d9f0fdf2ecdff1bcae1c51832c00465beb3f2922ac2e6753693329686
+size 9424
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_12x12.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_12x12.ktx2
new file mode 100644
index 00000000..5b7d251f
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_12x12.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:944495207e2b869e0cc47f3b4537b5df284b01ee36066fcaac42fb7ce867c269
+size 8016
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_4x4.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_4x4.ktx2
new file mode 100644
index 00000000..ae15d16b
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_4x4.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e21c1242774ef15aacc2ca950a849c9dc3a3e296515faa1ee876bd51c3306136
+size 65808
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_5x4.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_5x4.ktx2
new file mode 100644
index 00000000..fdcb8eea
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_5x4.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6d8d410a26c834736b596ae08a3a423c9d4bd899cd9729a09b4abc9cbba84b02
+size 53520
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_5x5.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_5x5.ktx2
new file mode 100644
index 00000000..ec54874c
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_5x5.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ed1330f50aa9061a760daefe7285cf4ca71e1105f6e2c867b3f5373735b238b8
+size 43536
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_6x5.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_6x5.ktx2
new file mode 100644
index 00000000..8f3ab4d6
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_6x5.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2c95a1ba7ced72135f72fa3b1d94a51e2b4c2b42cbd086b6e84774fb5708d4aa
+size 36048
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_6x6.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_6x6.ktx2
new file mode 100644
index 00000000..c5c8b7e2
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_6x6.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:efb357573e877eabc378059496e7572e1de35529db3898dd3100c7a55ba42af3
+size 29856
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_8x5.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_8x5.ktx2
new file mode 100644
index 00000000..115ef10a
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_8x5.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7bf0481dea0eabbe6f1d7dcae23531fb25f3c7bb6c8f2e8c64018cd843de2bd4
+size 26896
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_8x6.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_8x6.ktx2
new file mode 100644
index 00000000..6f0c684f
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_8x6.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:419038f24a8e6516a376dcbcb4c733969e5649acf478b598125acaba7b84a272
+size 22288
diff --git a/tests/Images/Input/Ktx2/astc_rgba32_8x8.ktx2 b/tests/Images/Input/Ktx2/astc_rgba32_8x8.ktx2
new file mode 100644
index 00000000..305fbf1b
--- /dev/null
+++ b/tests/Images/Input/Ktx2/astc_rgba32_8x8.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bf90e732eb27f5ab902fc04cd149d04af59e00667932dbe898391607457da83b
+size 16656
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..44fa4a48
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:179183db8cb2f1d66bf064b5555790906c0e83e6034db53a322b383acfafcd33
+size 288
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..b5bb9290
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:533bab3b7990c5f283595f35e7340ef77975372661c8541b369f0bc51754ecc3
+size 288
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..b8f3744e
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b8e9ac64c222117a5ee9dc65c0b5488e0bca98d16ad714d2c91f55f93e3a31be
+size 352
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..40e13fb5
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:516a7a67c584836c2f01bce00bea27fef5f5a3d2d0c7afe93dafeb5c169fc355
+size 352
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..65285e5c
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0314740d4e2d869d4935aa5eb16f0e6c195fb01ab4ef17c34069946a04850af1
+size 320
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..74adaf23
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f87cdb572e3dfd17b66f1beb7821b56b1e7838effc4dbdc8c20c450d58b1b8c6
+size 320
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..119a0f0b
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fe27c258349ba760e2bcf20eb1fee6a1b332be8d4e5364c2eadbf6381d6a640f
+size 288
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..95829e40
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:42b7708149d39b9eecc76b60c10b6d79ae9b2e5904cbe77799d299b201123df2
+size 288
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..e265b30a
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5b9638a8adac865a9955b9c13af3f80dd30f77711c1b8931c5b9bef95ffab4a8
+size 288
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..7a88ecdd
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bb0bb30011574a796786d1d93ecbccd1cc91d585f4a685ecc6627dea61d98cc
+size 288
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..46e0c53f
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ad07ce4c69e459141ef864f1d8eaf86f851ddd2b283073890c6ffb6b501dfee6
+size 288
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..372866cf
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:52c52e9581bf36e9dc0e1c178e21e2c4c25eb520a3a4ded3b7ff534abda83667
+size 288
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..77635ccf
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e4a8bb46326439994e152e620700f12b0d090d7069702ceb2f15320693aea9f9
+size 480
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..a268409f
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:89871b4df0314bd68b3fb30b8fa7de9702029bed255bc0f22d09864ff8242362
+size 480
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2
new file mode 100644
index 00000000..ac28ec16
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ce6c7aff34040079b4067b6ee19b7fc338621f9e453b0e6d379bcac4e0d7c4c8
+size 299
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2
new file mode 100644
index 00000000..e15efa89
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:68cce8fa95fd5484817c4a70f34c97ebb25a09a1f0a7db731e13e92bff1908e4
+size 299
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2
new file mode 100644
index 00000000..0ed01195
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7b9f8cba0659da234461cb621d10b5f02a01592e352385e429e8d7d3d8bed929
+size 297
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2
new file mode 100644
index 00000000..112fcf56
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1f3a8c9b7b5c5e426f9a6986274c266230671d27cd7b10b6d1c0650b9833ac56
+size 297
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..46869ef7
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:79ffea95e9cd8c4d7524bfe9b6862c1795c724b19392208fb7b4ecd33a667cdf
+size 480
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..26e917cf
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ffcb506ca972e68c470af50aaf26e9c687c6a63cd68a1ede00dcfe5ecb786e27
+size 480
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..245b0336
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d68cb3cf797346691d7de4313870b58f04b290f562e7dc797527f8594cd53f08
+size 480
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..7a3be45b
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a43bf736c6e7a88453284ad3820e545859582cc50a969940549bdd59a113395e
+size 480
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..8465bf20
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:95c42b019169a33646886a28246d54a5435a75350ca514f4fe1eb4be9522d48b
+size 416
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..cc236894
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:321c7133a313b87da891b56571ed26e96f09a993daead2ef16d65fa660b9c9a3
+size 416
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..b34b1fa4
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:38fc0dbc5920b4c90c6982b8b4459447dd86a76ef496349a33230fab9d57c8d8
+size 368
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..248b9a43
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:536e580ad30969214a7c8510fe89a61e9c632be535225dd61f31e606822b1086
+size 368
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..c7f321af
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eab82cb6499bff8c402361880603ec4db484e5fe7bf6f3d6d15d9b78c9da5ebc
+size 352
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..b0ec3d69
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b1c0aaf84c461da681571ce1335146acc0e88d6669853a29577b10dd8476a6a9
+size 352
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..7e612cae
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b4715a22d329b107b8e84224489f9743b42cbaf740d1213df9a35112b8008291
+size 320
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..3e08c23a
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:425fef4e127b8b505482cffbcbdc3279c87d2860062377b6fdb9219301eb51f2
+size 320
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2
new file mode 100644
index 00000000..ad606fdd
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ef826457d9c0c6797aabad6530398feccead690213a556fe0960769b6aaf98a8
+size 288
diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2
new file mode 100644
index 00000000..60da1caf
--- /dev/null
+++ b/tests/Images/Input/Ktx2/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee6be1f8eebb06f6d312d20848d3bed7f7ffb70b12163a4b822826f523a906a9
+size 288
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negX.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negX.png
new file mode 100644
index 00000000..2f898e90
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negX.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:04535fdbc632fc946f1bdce0229bbe17f15ef9ede7b089d893a7539d3428093b
+size 4456232
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negY.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negY.png
new file mode 100644
index 00000000..653b279e
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negY.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8615a15cfbac32d0c440520e70af99bd1af3c64b6651f4dffe990007afe470d5
+size 4520170
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negZ.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negZ.png
new file mode 100644
index 00000000..464ad67a
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negZ.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:76e41968dc617f55c19913a844790f432d345fd7de069b8ae91ce8b258536961
+size 4188624
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posX.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posX.png
new file mode 100644
index 00000000..a0afbf06
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posX.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4a244bc0d7e9ff1b82dae09678f18f0dc7ed7c082757c402bcfe2f62afbf8a4f
+size 4433772
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posY.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posY.png
new file mode 100644
index 00000000..c8554e46
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posY.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ddc6b41b993735daf6bc947cb90347ff5d387aa236ed3a2d614a296c0d9f2797
+size 2820406
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posZ.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posZ.png
new file mode 100644
index 00000000..3820a0e4
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posZ.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3cc4b3ecdb34d3ba6f02c6ea5f78a7fa99904ab81ec0538863694ad1fdfb8b33
+size 3910835
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_0.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_0.png
new file mode 100644
index 00000000..9211d075
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_0.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cfd253f555e9d6b5a3493c93f6bfdfcfa7d9cab759d7994e544dd5b72f0753a9
+size 107
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_1.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_1.png
new file mode 100644
index 00000000..00b6da1a
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_1.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c0313d2900eae51bd5f56b609839d130f6c6ec17320f7595973a36bbcdee84df
+size 100
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_2.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_2.png
new file mode 100644
index 00000000..47c4dead
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b708d4e34181c551722491a8d68b6b2d79de3a041f3b79cb49728bed60c015e1
+size 99
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_3.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_3.png
new file mode 100644
index 00000000..134de01e
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_3.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cd189117b29ddb0fbcaf8def1a5d90c0426be7241008cbea0aa470d184bb619b
+size 97
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_4.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_4.png
new file mode 100644
index 00000000..8af3b126
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_4.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aef68645bf32626a51b9420b91a3ad2f1af01cfa8aebb929d2144c1f3944b624
+size 91
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes.png
new file mode 100644
index 00000000..598f93a5
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:72bf89c439781bb8e9bf582589fec9e2dcfd3581472d1d8182b445a1099b5879
+size 164227
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x10.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x10.png
new file mode 100644
index 00000000..b7859c46
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x10.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b77dd1564ec9452df5e4b8c71be4b65809ee513b84e83a385b27d1c53b6d2f07
+size 475
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x5.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x5.png
new file mode 100644
index 00000000..637f711f
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:40f0e6160fda4625f56a9c2a4da0154781aa61b5aeb03c426c92cfe565b3dab8
+size 396
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x6.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x6.png
new file mode 100644
index 00000000..052f1ac6
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x6.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2baccdb9e775a730fa1ad38beedb2f3674602c3c583f6fdf6d35baf2d76d2da1
+size 438
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x8.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x8.png
new file mode 100644
index 00000000..593a0365
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x8.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ed3bd9fcd8b1bf5e9214204deee1adbd30af5649f3549fcdf3248e99b226b9b2
+size 384
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x10.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x10.png
new file mode 100644
index 00000000..c5d7b0bb
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x10.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b4de083f86ecedca4298d8db172000fe56edf7993deeebad718b20679b93fa11
+size 443
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x12.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x12.png
new file mode 100644
index 00000000..3d2787f9
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x12.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b80ef91c2e480f370b49a110e36385c437ab83f2ae2bc84034240d896a392a8b
+size 457
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_4x4.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_4x4.png
new file mode 100644
index 00000000..a123f4f4
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_4x4.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b9cff4006b0483486df0d4d32bd55328b615987dfe65c76a2676bea86424a234
+size 384
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x4.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x4.png
new file mode 100644
index 00000000..a12a7a69
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x4.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6179453f57d3711594c64f4e4ee913d460e81c537b85b1ed240d36b168d4f3ce
+size 460
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x5.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x5.png
new file mode 100644
index 00000000..29cd2256
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fd375c9b74a64145d3093a4020efaae0c1ab443fbb885d103aeb7a3d57970c9c
+size 589
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x5.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x5.png
new file mode 100644
index 00000000..34e8d5cb
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e1d235bf6d5a4685ddd3fa31fc912994c7acb098e5445174b8c5516c06fffc92
+size 544
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x6.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x6.png
new file mode 100644
index 00000000..f0368e89
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x6.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5583508ef1dbb46d35e002ba0841c3c45523df512d4183a4a811dbc59d1fde05
+size 504
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x5.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x5.png
new file mode 100644
index 00000000..c6c09b56
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:71e3d9dc1307a2afac70b4eaf1b3c4f2be8e013d79533984613ca12a0e60a7b8
+size 435
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x6.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x6.png
new file mode 100644
index 00000000..a52503c9
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x6.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bceeb7a936cd629ce909d2b7a99ab4d1686b7baeab41179e12bd3354fcb6f1c7
+size 520
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x8.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x8.png
new file mode 100644
index 00000000..98c95207
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x8.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:56d817a096705e39cb76f5505ddf34c1d012d16bfc40fc43939317bb0af632c8
+size 385
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_Large.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_Large.png
new file mode 100644
index 00000000..04cebc1a
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_Large.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6274ba573a2f6dae924e1aa40b586ca89329d5846cd5cf31b29188586e68c285
+size 1450103
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_1.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_1.png
new file mode 100644
index 00000000..6bb0f52c
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_1.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8a908971e007eb33f0990f0fa63c0ba6a8faaa8034bd0ed161b2aa092a59b78a
+size 381
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_9.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_9.png
new file mode 100644
index 00000000..6bb0f52c
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_9.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8a908971e007eb33f0990f0fa63c0ba6a8faaa8034bd0ed161b2aa092a59b78a
+size 381
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_1.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_1.png
new file mode 100644
index 00000000..6bb0f52c
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_1.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8a908971e007eb33f0990f0fa63c0ba6a8faaa8034bd0ed161b2aa092a59b78a
+size 381
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_9.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_9.png
new file mode 100644
index 00000000..6bb0f52c
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_9.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8a908971e007eb33f0990f0fa63c0ba6a8faaa8034bd0ed161b2aa092a59b78a
+size 381
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x10.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x10.png
new file mode 100644
index 00000000..d0f0d932
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x10.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b8378b4b6a70c4414f9f549b009e01ddc934c28623e985e0818b357a68a96b5c
+size 373
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x5.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x5.png
new file mode 100644
index 00000000..c9943a80
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:57140275641276969e3e6d16f4d9ef36975026267d81072fc270ef1fffd75ea9
+size 388
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x6.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x6.png
new file mode 100644
index 00000000..a8421169
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x6.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6beb53de172583ed13a37499f7c7df85283911c572bea8149eeac1ee7ebdeee5
+size 350
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x8.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x8.png
new file mode 100644
index 00000000..e0645247
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x8.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:271e3e222374be3e48597759d72566a908afdb422c77175b7c2149c5836ce0b0
+size 352
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x10.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x10.png
new file mode 100644
index 00000000..da1775e8
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x10.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:70c48f806cb55b20ccc44e57714506453c34789b20bde10feea51b2c56d742f1
+size 485
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x12.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x12.png
new file mode 100644
index 00000000..67d37ce3
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x12.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6dd4e0fa30523f6b4ba6d0a8142d09440b71e6a42e6c3bc272539444175c316e
+size 558
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_4x4.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_4x4.png
new file mode 100644
index 00000000..6bb0f52c
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_4x4.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8a908971e007eb33f0990f0fa63c0ba6a8faaa8034bd0ed161b2aa092a59b78a
+size 381
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x4.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x4.png
new file mode 100644
index 00000000..2f548c43
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x4.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bcdeaf19046832ea27917c042f1b801820980ddaa86fdf58ab713f130dccd0d7
+size 421
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x5.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x5.png
new file mode 100644
index 00000000..590b3da9
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:be4c2d4c8658ac6cb7897bafd184d17bcc80007b90267703b2c36a4a625b34a1
+size 562
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x5.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x5.png
new file mode 100644
index 00000000..eb7083aa
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:14f87a9677902b09218e7ac844e392304fd14b90ac9bb6a41015477bfdadf4b1
+size 481
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x6.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x6.png
new file mode 100644
index 00000000..b40edcdc
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x6.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b895db282773205885753c650a89eaea773cce21d47c8558d4c33f5f64ab0ab3
+size 465
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x5.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x5.png
new file mode 100644
index 00000000..404ca851
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:601b2f20a5d891c947f40940def171bbc36741cbd2708dadd335130376f0b227
+size 389
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x6.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x6.png
new file mode 100644
index 00000000..1ff3182c
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x6.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8074f9f74efc1f1775e14ff9514762ec9764b2ed16ee45cf6dd5330aa1da9aeb
+size 385
diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x8.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x8.png
new file mode 100644
index 00000000..cb6f1d82
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x8.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0bbae80913ee6f5c24f04da0612364bda8e092f5349fe2acf494c9ac9bed005e
+size 352
diff --git a/tests/Images/ReferenceOutput/KtxAstcDecoder_CanDecode_Rgba32_Blocksizes.png b/tests/Images/ReferenceOutput/KtxAstcDecoder_CanDecode_Rgba32_Blocksizes.png
new file mode 100644
index 00000000..6a18547c
--- /dev/null
+++ b/tests/Images/ReferenceOutput/KtxAstcDecoder_CanDecode_Rgba32_Blocksizes.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ebd9984f80e75676d74bdb22d32c923a6c248df13b8817dcecb8e0627707cc2e
+size 82338
From 6e58d6f2eddd486b14d6ffd6f87e1bfd52f80cfa Mon Sep 17 00:00:00 2001
From: Erik White <26148654+Erik-White@users.noreply.github.com>
Date: Fri, 27 Feb 2026 13:40:48 +0100
Subject: [PATCH 02/10] Integrate AstcSharp ASTC decoder
---
.gitattributes | 1 +
ImageSharp.Textures.sln | 60 +-
src/Directory.Build.props | 3 +-
.../Compression/Astc/AstcDecoder.cs | 447 +++++++++++
.../Astc/BiseEncoding/BiseEncodingMode.cs | 18 +
.../BoundedIntegerSequenceCodec.cs | 264 ++++++
.../BoundedIntegerSequenceDecoder.cs | 159 ++++
.../BoundedIntegerSequenceEncoder.cs | 168 ++++
.../Quantize/BitQuantizationMap.cs | 65 ++
.../BiseEncoding/Quantize/Quantization.cs | 245 ++++++
.../BiseEncoding/Quantize/QuantizationMap.cs | 74 ++
.../Quantize/QuintQuantizationMap.cs | 65 ++
.../Quantize/TritQuantizationMap.cs | 74 ++
.../Astc/BlockDecoder/FusedBlockDecoder.cs | 183 +++++
.../Astc/BlockDecoder/FusedHdrBlockDecoder.cs | 223 ++++++
.../Astc/BlockDecoder/FusedLdrBlockDecoder.cs | 172 ++++
.../Astc/ColorEncoding/ColorEndpointMode.cs | 38 +
.../ColorEndpointModeExtensions.cs | 31 +
.../Astc/ColorEncoding/ColorEndpointPair.cs | 32 +
.../Astc/ColorEncoding/EndpointCodec.cs | 251 ++++++
.../Astc/ColorEncoding/EndpointEncoder.cs | 551 +++++++++++++
.../ColorEncoding/EndpointEncodingMode.cs | 14 +
.../EndpointEncodingModeExtensions.cs | 15 +
.../Astc/ColorEncoding/HdrEndpointDecoder.cs | 478 +++++++++++
.../Astc/ColorEncoding/Partition.cs | 248 ++++++
.../Astc/ColorEncoding/RgbaColorExtensions.cs | 56 ++
.../Compression/Astc/Core/BitOperations.cs | 107 +++
.../Compression/Astc/Core/DecimationInfo.cs | 27 +
.../Compression/Astc/Core/DecimationTable.cs | 140 ++++
.../Compression/Astc/Core/Footprint.cs | 85 ++
.../Compression/Astc/Core/FootprintType.cs | 52 ++
.../Compression/Astc/Core/Rgba32Extensions.cs | 80 ++
.../Compression/Astc/Core/Rgba64Extensions.cs | 36 +
.../Compression/Astc/Core/SimdHelpers.cs | 177 +++++
.../Astc/Core/UInt128Extensions.cs | 77 ++
.../Compression/Astc/IO/AstcFile.cs | 69 ++
.../Compression/Astc/IO/AstcFileHeader.cs | 41 +
.../Compression/Astc/IO/BitStream.cs | 188 +++++
.../Compression/Astc/README.md | 44 +
.../Compression/Astc/TexelBlock/BlockInfo.cs | 363 +++++++++
.../Astc/TexelBlock/IntermediateBlock.cs | 284 +++++++
.../TexelBlock/IntermediateBlockPacker.cs | 403 ++++++++++
.../Astc/TexelBlock/LogicalBlock.cs | 752 ++++++++++++++++++
.../Astc/TexelBlock/PhysicalBlock.cs | 216 +++++
.../Astc/TexelBlock/PhysicalBlockMode.cs | 23 +
.../ImageSharp.Textures.csproj | 1 -
.../TextureFormats/Decoding/AstcDecoder.cs | 4 +-
tests/Directory.Build.targets | 4 +-
...Sharp.Textures.Astc.Reference.Tests.csproj | 34 +
.../ReferenceDecoderHdrTests.cs | 252 ++++++
.../ReferenceDecoderTests.cs | 272 +++++++
.../Utils/ReferenceDecoder.cs | 238 ++++++
.../AstcDecodingBenchmark.cs | 67 ++
.../AstcImageDecodeBenchmark.cs | 38 +
.../ImageSharp.Textures.Benchmarks/Config.cs | 25 +-
.../ImageSharp.Textures.Benchmarks.csproj | 10 +-
.../ImageSharp.Textures.Benchmarks/Program.cs | 9 +-
...ImageSharp.Textures.InteractiveTest.csproj | 2 +
.../Formats/Astc/AstcDecoderTests.cs | 174 ++++
.../Formats/Astc/BitOperationsTests.cs | 121 +++
.../Formats/Astc/BitStreamTests.cs | 192 +++++
.../Formats/Astc/EndpointCodecTests.cs | 384 +++++++++
.../Formats/Astc/FootprintTests.cs | 82 ++
.../Formats/Astc/HDR/HdrComparisonTests.cs | 211 +++++
.../Formats/Astc/HDR/HdrDecoderTests.cs | 96 +++
.../Formats/Astc/HDR/HdrImageTests.cs | 153 ++++
.../Formats/Astc/HDR/Rgba64ExtensionsTests.cs | 66 ++
.../Formats/Astc/IntegerSequenceCodecTests.cs | 333 ++++++++
.../Formats/Astc/IntermediateBlockTests.cs | 462 +++++++++++
.../Formats/Astc/LogicalAstcBlockTests.cs | 441 ++++++++++
.../Formats/Astc/PartitionTests.cs | 224 ++++++
.../Formats/Astc/PhysicalAstcBlockTests.cs | 615 ++++++++++++++
.../Formats/Astc/QuantizationTests.cs | 399 ++++++++++
.../Formats/Astc/WeightInfillTests.cs | 47 ++
.../Formats/Ktx/KtxAstcDecoderTests.cs | 3 +-
.../Formats/Ktx/KtxDecoderTests.cs | 3 +-
.../Ktx2/Ktx2AstcDecoderCubemapTests.cs | 3 +-
.../Formats/Ktx2/Ktx2AstcDecoderFlatTests.cs | 19 +-
tests/ImageSharp.Textures.Tests/TestImages.cs | 182 +++--
.../Attributes/WithFileAttribute.cs | 37 +-
.../TextureProviders/TestTextureProvider.cs | 8 +-
.../Decoding/AstcDecoderTests.cs | 1 -
tests/Images/Input/Astc/HDR/HDR-A-1x1.astc | 3 +
tests/Images/Input/Astc/HDR/HDR-A-1x1.exr | 3 +
tests/Images/Input/Astc/HDR/LDR-A-1x1.astc | 3 +
tests/Images/Input/Astc/HDR/LDR-A-1x1.png | 3 +
tests/Images/Input/Astc/HDR/hdr-complex.exr | 3 +
tests/Images/Input/Astc/HDR/hdr-tile.astc | 3 +
tests/Images/Input/Astc/HDR/hdr.exr | 3 +
tests/Images/Input/Astc/HDR/hdr.hdr | Bin 0 -> 217 bytes
tests/Images/Input/Astc/HDR/ldr-tile.astc | 3 +
tests/Images/Input/Astc/atlas_small_4x4.astc | 3 +
tests/Images/Input/Astc/atlas_small_5x5.astc | 3 +
tests/Images/Input/Astc/atlas_small_6x6.astc | 3 +
tests/Images/Input/Astc/atlas_small_8x8.astc | 3 +
tests/Images/Input/Astc/checkerboard.astc | 3 +
tests/Images/Input/Astc/checkered_10.astc | 3 +
tests/Images/Input/Astc/checkered_11.astc | 3 +
tests/Images/Input/Astc/checkered_12.astc | 3 +
tests/Images/Input/Astc/checkered_4.astc | 3 +
tests/Images/Input/Astc/checkered_5.astc | 3 +
tests/Images/Input/Astc/checkered_6.astc | 3 +
tests/Images/Input/Astc/checkered_7.astc | 3 +
tests/Images/Input/Astc/checkered_8.astc | 3 +
tests/Images/Input/Astc/checkered_9.astc | 3 +
tests/Images/Input/Astc/footprint_10x10.astc | 3 +
tests/Images/Input/Astc/footprint_10x5.astc | 3 +
tests/Images/Input/Astc/footprint_10x6.astc | 3 +
tests/Images/Input/Astc/footprint_10x8.astc | 3 +
tests/Images/Input/Astc/footprint_12x10.astc | 3 +
tests/Images/Input/Astc/footprint_12x12.astc | 3 +
tests/Images/Input/Astc/footprint_4x4.astc | 3 +
tests/Images/Input/Astc/footprint_5x4.astc | 3 +
tests/Images/Input/Astc/footprint_5x5.astc | 3 +
tests/Images/Input/Astc/footprint_6x5.astc | 3 +
tests/Images/Input/Astc/footprint_6x6.astc | 3 +
tests/Images/Input/Astc/footprint_8x5.astc | 3 +
tests/Images/Input/Astc/footprint_8x6.astc | 3 +
tests/Images/Input/Astc/footprint_8x8.astc | 3 +
tests/Images/Input/Astc/rgb_12x12.astc | 3 +
tests/Images/Input/Astc/rgb_4x4.astc | 3 +
tests/Images/Input/Astc/rgb_5x4.astc | 3 +
tests/Images/Input/Astc/rgb_6x6.astc | 3 +
tests/Images/Input/Astc/rgb_8x8.astc | 3 +
.../Astc}/astc_ldr_cubemap_6x6.ktx2 | 0
.../valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2 | 0
.../valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2 | 0
.../valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2 | 0
.../valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2 | 0
.../astc_ldr_10x5_FlightHelmet_baseColor.ktx2 | 0
.../Astc}/astc_ldr_6x6_arraytex_7_mipmap.ktx2 | 0
.../{ => Flat/Astc}/astc_rgba32_10x10.ktx2 | 0
.../{ => Flat/Astc}/astc_rgba32_10x5.ktx2 | 0
.../{ => Flat/Astc}/astc_rgba32_10x6.ktx2 | 0
.../{ => Flat/Astc}/astc_rgba32_10x8.ktx2 | 0
.../{ => Flat/Astc}/astc_rgba32_12x10.ktx2 | 0
.../{ => Flat/Astc}/astc_rgba32_12x12.ktx2 | 0
.../Ktx2/{ => Flat/Astc}/astc_rgba32_4x4.ktx2 | 0
.../Ktx2/{ => Flat/Astc}/astc_rgba32_5x4.ktx2 | 0
.../Ktx2/{ => Flat/Astc}/astc_rgba32_5x5.ktx2 | 0
.../Ktx2/{ => Flat/Astc}/astc_rgba32_6x5.ktx2 | 0
.../Ktx2/{ => Flat/Astc}/astc_rgba32_6x6.ktx2 | 0
.../Ktx2/{ => Flat/Astc}/astc_rgba32_8x5.ktx2 | 0
.../Ktx2/{ => Flat/Astc}/astc_rgba32_8x6.ktx2 | 0
.../Ktx2/{ => Flat/Astc}/astc_rgba32_8x8.ktx2 | 0
.../Astc}/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2 | 0
.../valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2 | 0
.../valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2 | 0
.../valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2 | 0
.../Astc}/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2 | 0
.../CanDecode_Rgba32_Blocksizes.png} | 0
.../KtxDecoderTests/CanDecode_Rgba8888.png} | 0
.../CanDecode_All_Faces_negX.png} | 0
.../CanDecode_All_Faces_negY.png} | 0
.../CanDecode_All_Faces_negZ.png} | 0
.../CanDecode_All_Faces_posX.png} | 0
.../CanDecode_All_Faces_posY.png} | 0
.../CanDecode_All_Faces_posZ.png} | 0
.../CanDecode_MipMaps_0.png} | 0
.../CanDecode_MipMaps_1.png} | 0
.../CanDecode_MipMaps_2.png} | 0
.../CanDecode_MipMaps_3.png} | 0
.../CanDecode_MipMaps_4.png} | 0
.../CanDecode_Rgba32_Blocksizes_10x10.png | 3 +
.../CanDecode_Rgba32_Blocksizes_10x5.png | 3 +
.../CanDecode_Rgba32_Blocksizes_10x6.png | 3 +
.../CanDecode_Rgba32_Blocksizes_10x8.png | 3 +
.../CanDecode_Rgba32_Blocksizes_12x10.png | 3 +
.../CanDecode_Rgba32_Blocksizes_12x12.png | 3 +
.../CanDecode_Rgba32_Blocksizes_4x4.png | 3 +
.../CanDecode_Rgba32_Blocksizes_5x4.png | 3 +
.../CanDecode_Rgba32_Blocksizes_5x5.png | 3 +
.../CanDecode_Rgba32_Blocksizes_6x5.png | 3 +
.../CanDecode_Rgba32_Blocksizes_6x6.png | 3 +
.../CanDecode_Rgba32_Blocksizes_8x5.png | 3 +
.../CanDecode_Rgba32_Blocksizes_8x6.png | 3 +
.../CanDecode_Rgba32_Blocksizes_8x8.png | 3 +
.../CanDecode_Rgba32_Srgb_10x10.png} | 0
.../CanDecode_Rgba32_Srgb_10x5.png} | 0
.../CanDecode_Rgba32_Srgb_10x6.png} | 0
.../CanDecode_Rgba32_Srgb_10x8.png} | 0
.../CanDecode_Rgba32_Srgb_12x10.png} | 0
.../CanDecode_Rgba32_Srgb_12x12.png} | 0
.../CanDecode_Rgba32_Srgb_4x4.png} | 0
.../CanDecode_Rgba32_Srgb_5x4.png} | 0
.../CanDecode_Rgba32_Srgb_5x5.png} | 0
.../CanDecode_Rgba32_Srgb_6x5.png} | 0
.../CanDecode_Rgba32_Srgb_6x6.png} | 0
.../CanDecode_Rgba32_Srgb_8x5.png} | 0
.../CanDecode_Rgba32_Srgb_8x6.png} | 0
.../CanDecode_Rgba32_Srgb_8x8.png} | 0
.../CanDecode_Rgba32_Srgb_Large.png} | 0
...nDecode_Rgba32_Supercompressed_ZLIB_1.png} | 0
...nDecode_Rgba32_Supercompressed_ZLIB_9.png} | 0
...nDecode_Rgba32_Supercompressed_ZSTD_1.png} | 0
...nDecode_Rgba32_Supercompressed_ZSTD_9.png} | 0
.../CanDecode_Rgba32_Unorm_10x10.png} | 0
.../CanDecode_Rgba32_Unorm_10x5.png} | 0
.../CanDecode_Rgba32_Unorm_10x6.png} | 0
.../CanDecode_Rgba32_Unorm_10x8.png} | 0
.../CanDecode_Rgba32_Unorm_12x10.png} | 0
.../CanDecode_Rgba32_Unorm_12x12.png} | 0
.../CanDecode_Rgba32_Unorm_4x4.png} | 0
.../CanDecode_Rgba32_Unorm_5x4.png} | 0
.../CanDecode_Rgba32_Unorm_5x5.png} | 0
.../CanDecode_Rgba32_Unorm_6x5.png} | 0
.../CanDecode_Rgba32_Unorm_6x6.png} | 0
.../CanDecode_Rgba32_Unorm_8x5.png} | 0
.../CanDecode_Rgba32_Unorm_8x6.png} | 0
.../CanDecode_Rgba32_Unorm_8x8.png} | 0
...stcDecoder_CanDecode_Rgba32_Blocksizes.png | 3 -
234 files changed, 12359 insertions(+), 120 deletions(-)
create mode 100644 src/ImageSharp.Textures/Compression/Astc/AstcDecoder.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BiseEncodingMode.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BoundedIntegerSequenceCodec.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BoundedIntegerSequenceDecoder.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BoundedIntegerSequenceEncoder.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/BitQuantizationMap.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/Quantization.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/QuantizationMap.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/QuintQuantizationMap.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/TritQuantizationMap.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BlockDecoder/FusedBlockDecoder.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BlockDecoder/FusedHdrBlockDecoder.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/BlockDecoder/FusedLdrBlockDecoder.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/ColorEncoding/ColorEndpointMode.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/ColorEncoding/ColorEndpointModeExtensions.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/ColorEncoding/ColorEndpointPair.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointCodec.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncoder.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingMode.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingModeExtensions.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/ColorEncoding/HdrEndpointDecoder.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/ColorEncoding/Partition.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/ColorEncoding/RgbaColorExtensions.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/Core/BitOperations.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/Core/DecimationInfo.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/Core/DecimationTable.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/Core/Footprint.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/Core/FootprintType.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/Core/Rgba32Extensions.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/Core/Rgba64Extensions.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/Core/SimdHelpers.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/Core/UInt128Extensions.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/IO/AstcFile.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/IO/AstcFileHeader.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/IO/BitStream.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/README.md
create mode 100644 src/ImageSharp.Textures/Compression/Astc/TexelBlock/BlockInfo.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/TexelBlock/IntermediateBlock.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/TexelBlock/IntermediateBlockPacker.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/TexelBlock/LogicalBlock.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/TexelBlock/PhysicalBlock.cs
create mode 100644 src/ImageSharp.Textures/Compression/Astc/TexelBlock/PhysicalBlockMode.cs
create mode 100644 tests/ImageSharp.Textures.Astc.Reference.Tests/ImageSharp.Textures.Astc.Reference.Tests.csproj
create mode 100644 tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderHdrTests.cs
create mode 100644 tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderTests.cs
create mode 100644 tests/ImageSharp.Textures.Astc.Reference.Tests/Utils/ReferenceDecoder.cs
create mode 100644 tests/ImageSharp.Textures.Benchmarks/AstcDecodingBenchmark.cs
create mode 100644 tests/ImageSharp.Textures.Benchmarks/AstcImageDecodeBenchmark.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/AstcDecoderTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/BitOperationsTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/BitStreamTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/FootprintTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrComparisonTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrDecoderTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrImageTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/Rgba64ExtensionsTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/IntegerSequenceCodecTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/IntermediateBlockTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/LogicalAstcBlockTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/PartitionTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/PhysicalAstcBlockTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/QuantizationTests.cs
create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Astc/WeightInfillTests.cs
create mode 100644 tests/Images/Input/Astc/HDR/HDR-A-1x1.astc
create mode 100644 tests/Images/Input/Astc/HDR/HDR-A-1x1.exr
create mode 100644 tests/Images/Input/Astc/HDR/LDR-A-1x1.astc
create mode 100644 tests/Images/Input/Astc/HDR/LDR-A-1x1.png
create mode 100644 tests/Images/Input/Astc/HDR/hdr-complex.exr
create mode 100644 tests/Images/Input/Astc/HDR/hdr-tile.astc
create mode 100644 tests/Images/Input/Astc/HDR/hdr.exr
create mode 100644 tests/Images/Input/Astc/HDR/hdr.hdr
create mode 100644 tests/Images/Input/Astc/HDR/ldr-tile.astc
create mode 100644 tests/Images/Input/Astc/atlas_small_4x4.astc
create mode 100644 tests/Images/Input/Astc/atlas_small_5x5.astc
create mode 100644 tests/Images/Input/Astc/atlas_small_6x6.astc
create mode 100644 tests/Images/Input/Astc/atlas_small_8x8.astc
create mode 100644 tests/Images/Input/Astc/checkerboard.astc
create mode 100644 tests/Images/Input/Astc/checkered_10.astc
create mode 100644 tests/Images/Input/Astc/checkered_11.astc
create mode 100644 tests/Images/Input/Astc/checkered_12.astc
create mode 100644 tests/Images/Input/Astc/checkered_4.astc
create mode 100644 tests/Images/Input/Astc/checkered_5.astc
create mode 100644 tests/Images/Input/Astc/checkered_6.astc
create mode 100644 tests/Images/Input/Astc/checkered_7.astc
create mode 100644 tests/Images/Input/Astc/checkered_8.astc
create mode 100644 tests/Images/Input/Astc/checkered_9.astc
create mode 100644 tests/Images/Input/Astc/footprint_10x10.astc
create mode 100644 tests/Images/Input/Astc/footprint_10x5.astc
create mode 100644 tests/Images/Input/Astc/footprint_10x6.astc
create mode 100644 tests/Images/Input/Astc/footprint_10x8.astc
create mode 100644 tests/Images/Input/Astc/footprint_12x10.astc
create mode 100644 tests/Images/Input/Astc/footprint_12x12.astc
create mode 100644 tests/Images/Input/Astc/footprint_4x4.astc
create mode 100644 tests/Images/Input/Astc/footprint_5x4.astc
create mode 100644 tests/Images/Input/Astc/footprint_5x5.astc
create mode 100644 tests/Images/Input/Astc/footprint_6x5.astc
create mode 100644 tests/Images/Input/Astc/footprint_6x6.astc
create mode 100644 tests/Images/Input/Astc/footprint_8x5.astc
create mode 100644 tests/Images/Input/Astc/footprint_8x6.astc
create mode 100644 tests/Images/Input/Astc/footprint_8x8.astc
create mode 100644 tests/Images/Input/Astc/rgb_12x12.astc
create mode 100644 tests/Images/Input/Astc/rgb_4x4.astc
create mode 100644 tests/Images/Input/Astc/rgb_5x4.astc
create mode 100644 tests/Images/Input/Astc/rgb_6x6.astc
create mode 100644 tests/Images/Input/Astc/rgb_8x8.astc
rename tests/Images/Input/Ktx2/{ => Cubemap/Astc}/astc_ldr_cubemap_6x6.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc/Supercompressed}/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc/Supercompressed}/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc/Supercompressed}/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc/Supercompressed}/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_ldr_10x5_FlightHelmet_baseColor.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_ldr_6x6_arraytex_7_mipmap.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_10x10.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_10x5.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_10x6.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_10x8.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_12x10.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_12x12.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_4x4.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_5x4.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_5x5.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_6x5.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_6x6.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_8x5.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_8x6.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/astc_rgba32_8x8.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2 (100%)
rename tests/Images/Input/Ktx2/{ => Flat/Astc}/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2 (100%)
rename tests/Images/ReferenceOutput/{KtxAstcDecoder_CanDecode_Rgba32_Blocksizes.png => Ktx/KtxAstcDecoderTests/CanDecode_Rgba32_Blocksizes.png} (100%)
rename tests/Images/ReferenceOutput/{KtxDecoder_CanDecode_Rgba8888.png => Ktx/KtxDecoderTests/CanDecode_Rgba8888.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_All_Faces_negX.png => Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_negX.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_All_Faces_negY.png => Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_negY.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_All_Faces_negZ.png => Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_negZ.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_All_Faces_posX.png => Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_posX.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_All_Faces_posY.png => Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_posY.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_All_Faces_posZ.png => Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_posZ.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_MipMaps_0.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_0.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_MipMaps_1.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_1.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_MipMaps_2.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_2.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_MipMaps_3.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_3.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_MipMaps_4.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_4.png} (100%)
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x10.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x6.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x8.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_12x10.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_12x12.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_4x4.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_5x4.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_5x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_6x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_6x6.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x5.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x6.png
create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x8.png
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x10.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x10.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x5.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x5.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x6.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x6.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x8.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x8.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x10.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_12x10.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x12.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_12x12.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_4x4.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_4x4.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x4.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_5x4.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x5.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_5x5.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x5.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_6x5.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x6.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_6x6.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x5.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_8x5.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x6.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_8x6.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x8.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_8x8.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_Large.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_Large.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_1.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZLIB_1.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_9.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZLIB_9.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_1.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZSTD_1.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_9.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZSTD_9.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x10.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x10.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x5.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x5.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x6.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x6.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x8.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x8.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x10.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_12x10.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x12.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_12x12.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_4x4.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_4x4.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x4.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_5x4.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x5.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_5x5.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x5.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_6x5.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x6.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_6x6.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x5.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_8x5.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x6.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_8x6.png} (100%)
rename tests/Images/ReferenceOutput/{Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x8.png => Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_8x8.png} (100%)
delete mode 100644 tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes.png
diff --git a/.gitattributes b/.gitattributes
index f7bd4d06..80c19213 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -126,6 +126,7 @@
*.dds filter=lfs diff=lfs merge=lfs -text
*.ktx filter=lfs diff=lfs merge=lfs -text
*.ktx2 filter=lfs diff=lfs merge=lfs -text
+*.astc filter=lfs diff=lfs merge=lfs -text
*.pam filter=lfs diff=lfs merge=lfs -text
*.pbm filter=lfs diff=lfs merge=lfs -text
*.pgm filter=lfs diff=lfs merge=lfs -text
diff --git a/ImageSharp.Textures.sln b/ImageSharp.Textures.sln
index 636514f8..7b07d55a 100644
--- a/ImageSharp.Textures.sln
+++ b/ImageSharp.Textures.sln
@@ -49,35 +49,72 @@ 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.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
+{E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Release|x64.ActiveCfg = Release|Any CPU
+ {E15C2E1A-FCD6-42B1-82D2-2C72CC4DC720}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -90,8 +127,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}
+ {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
+ tests\Images\Images.projitems*{e15c2e1a-fcd6-42b1-82d2-2c72cc4dc720}*SharedItemsImports = 5
+ EndGlobalSection
EndGlobal
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index e40e9b78..2e8451ba 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -22,9 +22,10 @@
-
+
+
diff --git a/src/ImageSharp.Textures/Compression/Astc/AstcDecoder.cs b/src/ImageSharp.Textures/Compression/Astc/AstcDecoder.cs
new file mode 100644
index 00000000..991d8d20
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/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.Compression.Astc.BlockDecoder;
+using SixLabors.ImageSharp.Textures.Compression.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+using SixLabors.ImageSharp.Textures.Compression.Astc.IO;
+using SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BiseEncoding/BiseEncodingMode.cs b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BiseEncodingMode.cs
new file mode 100644
index 00000000..028efe0c
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BiseEncodingMode.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BiseEncoding/BoundedIntegerSequenceCodec.cs b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BoundedIntegerSequenceCodec.cs
new file mode 100644
index 00000000..64331f95
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BoundedIntegerSequenceCodec.cs
@@ -0,0 +1,264 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BiseEncoding/BoundedIntegerSequenceDecoder.cs b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BoundedIntegerSequenceDecoder.cs
new file mode 100644
index 00000000..1b319c97
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BoundedIntegerSequenceDecoder.cs
@@ -0,0 +1,159 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Compression.Astc.IO;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BiseEncoding/BoundedIntegerSequenceEncoder.cs b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BoundedIntegerSequenceEncoder.cs
new file mode 100644
index 00000000..fe10d796
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/BoundedIntegerSequenceEncoder.cs
@@ -0,0 +1,168 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Compression.Astc.IO;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BiseEncoding/Quantize/BitQuantizationMap.cs b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/BitQuantizationMap.cs
new file mode 100644
index 00000000..5b5b3b59
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/BitQuantizationMap.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BiseEncoding/Quantize/Quantization.cs b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/Quantization.cs
new file mode 100644
index 00000000..1476cff8
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/Quantization.cs
@@ -0,0 +1,245 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BiseEncoding/Quantize/QuantizationMap.cs b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/QuantizationMap.cs
new file mode 100644
index 00000000..37db8d31
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/QuantizationMap.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BiseEncoding/Quantize/QuintQuantizationMap.cs b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/QuintQuantizationMap.cs
new file mode 100644
index 00000000..5d4a49da
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/QuintQuantizationMap.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BiseEncoding/Quantize/TritQuantizationMap.cs b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/TritQuantizationMap.cs
new file mode 100644
index 00000000..27bb6ac4
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/BiseEncoding/Quantize/TritQuantizationMap.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BlockDecoder/FusedBlockDecoder.cs b/src/ImageSharp.Textures/Compression/Astc/BlockDecoder/FusedBlockDecoder.cs
new file mode 100644
index 00000000..3e24f058
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/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.Compression.Astc.BiseEncoding;
+using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding.Quantize;
+using SixLabors.ImageSharp.Textures.Compression.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+using SixLabors.ImageSharp.Textures.Compression.Astc.IO;
+using SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BlockDecoder/FusedHdrBlockDecoder.cs b/src/ImageSharp.Textures/Compression/Astc/BlockDecoder/FusedHdrBlockDecoder.cs
new file mode 100644
index 00000000..f8dc7cf5
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/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.Compression.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+using SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/BlockDecoder/FusedLdrBlockDecoder.cs b/src/ImageSharp.Textures/Compression/Astc/BlockDecoder/FusedLdrBlockDecoder.cs
new file mode 100644
index 00000000..5082f152
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/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.Compression.Astc.ColorEncoding;
+using SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+using SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/ColorEncoding/ColorEndpointMode.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/ColorEndpointMode.cs
new file mode 100644
index 00000000..d14e50dd
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/ColorEndpointMode.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/ColorEncoding/ColorEndpointModeExtensions.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/ColorEndpointModeExtensions.cs
new file mode 100644
index 00000000..7ceccf86
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/ColorEndpointModeExtensions.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/ColorEncoding/ColorEndpointPair.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/ColorEndpointPair.cs
new file mode 100644
index 00000000..b4c11fd5
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/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.PixelFormats;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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 Rgba32 LdrLow;
+ public Rgba32 LdrHigh;
+
+ // HDR fields (used when IsHdr == true)
+ public Rgba64 HdrLow;
+ public Rgba64 HdrHigh;
+ public bool AlphaIsLdr;
+ public bool ValuesAreLns;
+
+ public static ColorEndpointPair Ldr(Rgba32 low, Rgba32 high)
+ => new() { IsHdr = false, LdrLow = low, LdrHigh = high };
+
+ public static ColorEndpointPair Hdr(Rgba64 low, Rgba64 high, bool alphaIsLdr = false, bool valuesAreLns = true)
+ => new() { IsHdr = true, HdrLow = low, HdrHigh = high, AlphaIsLdr = alphaIsLdr, ValuesAreLns = valuesAreLns };
+}
diff --git a/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointCodec.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointCodec.cs
new file mode 100644
index 00000000..8bfa15b8
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointCodec.cs
@@ -0,0 +1,251 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding.Quantize;
+using SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+using static SixLabors.ImageSharp.Textures.Compression.Astc.Core.Rgba32Extensions;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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())
+ {
+ (Rgba64 low, Rgba64 high) = HdrEndpointDecoder.DecodeHdrMode(values, maxValue, mode);
+ bool alphaIsLdr = mode == ColorEndpointMode.HdrRgbDirectLdrAlpha;
+ return ColorEndpointPair.Hdr(low, high, alphaIsLdr);
+ }
+ else
+ {
+ (Rgba32 low, Rgba32 high) = DecodeColorsForMode(values, maxValue, mode);
+ return ColorEndpointPair.Ldr(low, high);
+ }
+ }
+
+ public static (Rgba32 EndpointLowRgba, Rgba32 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())
+ {
+ (Rgba64 low, Rgba64 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)
+ {
+ Rgba32 endpointLowRgba, endpointHighRgba;
+
+ switch (mode)
+ {
+ case ColorEndpointMode.LdrLumaDirect:
+ endpointLowRgba = ClampedRgba32(unquantizedValues[0], unquantizedValues[0], unquantizedValues[0]);
+ endpointHighRgba = ClampedRgba32(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 = ClampedRgba32(l0, l0, l0);
+ endpointHighRgba = ClampedRgba32(l1, l1, l1);
+ break;
+ }
+
+ case ColorEndpointMode.LdrLumaAlphaDirect:
+ endpointLowRgba = ClampedRgba32(unquantizedValues[0], unquantizedValues[0], unquantizedValues[0], unquantizedValues[2]);
+ endpointHighRgba = ClampedRgba32(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 = ClampedRgba32(a0, a0, a0, a2);
+ int highLuma = a0 + b0;
+ endpointHighRgba = ClampedRgba32(highLuma, highLuma, highLuma, a2 + b2);
+ break;
+ }
+
+ case ColorEndpointMode.LdrRgbBaseScale:
+ endpointLowRgba = ClampedRgba32(
+ (unquantizedValues[0] * unquantizedValues[3]) >> 8,
+ (unquantizedValues[1] * unquantizedValues[3]) >> 8,
+ (unquantizedValues[2] * unquantizedValues[3]) >> 8);
+ endpointHighRgba = ClampedRgba32(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 = ClampedRgba32(
+ r: (unquantizedValues[1] + unquantizedValues[5]) >> 1,
+ g: (unquantizedValues[3] + unquantizedValues[5]) >> 1,
+ b: unquantizedValues[5]);
+ endpointHighRgba = ClampedRgba32(
+ r: (unquantizedValues[0] + unquantizedValues[4]) >> 1,
+ g: (unquantizedValues[2] + unquantizedValues[4]) >> 1,
+ b: unquantizedValues[4]);
+ }
+ else
+ {
+ endpointLowRgba = ClampedRgba32(unquantizedValues[0], unquantizedValues[2], unquantizedValues[4]);
+ endpointHighRgba = ClampedRgba32(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 = ClampedRgba32(
+ r: (a0 + b0 + a2 + b2) >> 1,
+ g: (a1 + b1 + a2 + b2) >> 1,
+ b: a2 + b2);
+ endpointHighRgba = ClampedRgba32(
+ r: (a0 + a2) >> 1,
+ g: (a1 + a2) >> 1,
+ b: a2);
+ }
+ else
+ {
+ endpointLowRgba = ClampedRgba32(a0, a1, a2);
+ endpointHighRgba = ClampedRgba32(a0 + b0, a1 + b1, a2 + b2);
+ }
+
+ break;
+ }
+
+ case ColorEndpointMode.LdrRgbBaseScaleTwoA:
+ endpointLowRgba = ClampedRgba32(
+ r: (unquantizedValues[0] * unquantizedValues[3]) >> 8,
+ g: (unquantizedValues[1] * unquantizedValues[3]) >> 8,
+ b: (unquantizedValues[2] * unquantizedValues[3]) >> 8,
+ a: unquantizedValues[4]);
+ endpointHighRgba = ClampedRgba32(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 = ClampedRgba32(unquantizedValues[0], unquantizedValues[2], unquantizedValues[4], unquantizedValues[6]);
+ endpointHighRgba = ClampedRgba32(unquantizedValues[1], unquantizedValues[3], unquantizedValues[5], unquantizedValues[7]);
+ }
+ else
+ {
+ endpointLowRgba = ClampedRgba32(
+ r: (unquantizedValues[1] + unquantizedValues[5]) >> 1,
+ g: (unquantizedValues[3] + unquantizedValues[5]) >> 1,
+ b: unquantizedValues[5],
+ a: unquantizedValues[7]);
+ endpointHighRgba = ClampedRgba32(
+ 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 = ClampedRgba32(
+ r: (a0 + b0 + a2 + b2) >> 1,
+ g: (a1 + b1 + a2 + b2) >> 1,
+ b: a2 + b2,
+ a: a3 + b3);
+ endpointHighRgba = ClampedRgba32(
+ r: (a0 + a2) >> 1,
+ g: (a1 + a2) >> 1,
+ b: a2,
+ a: a3);
+ }
+ else
+ {
+ endpointLowRgba = ClampedRgba32(a0, a1, a2, a3);
+ endpointHighRgba = ClampedRgba32(a0 + b0, a1 + b1, a2 + b2, a3 + b3);
+ }
+
+ break;
+ }
+
+ default:
+ endpointLowRgba = default;
+ endpointHighRgba = default;
+ 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/Compression/Astc/ColorEncoding/EndpointEncoder.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncoder.cs
new file mode 100644
index 00000000..b55a02a7
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncoder.cs
@@ -0,0 +1,551 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding.Quantize;
+using SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+using static SixLabors.ImageSharp.Textures.Compression.Astc.Core.Rgba32Extensions;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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(Rgba32 endpointLowRgba, Rgba32 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.GetAverage();
+ int avg2 = endpointHighRgba.GetAverage();
+ values[0] = Quantization.QuantizeCEValueToRange(avg1, maxValue);
+ values[1] = Quantization.QuantizeCEValueToRange(avg2, maxValue);
+ values[2] = Quantization.QuantizeCEValueToRange(endpointLowRgba.GetChannel(3), maxValue);
+ values[3] = Quantization.QuantizeCEValueToRange(endpointHighRgba.GetChannel(3), maxValue);
+ astcMode = ColorEndpointMode.LdrLumaAlphaDirect;
+ break;
+ }
+
+ case EndpointEncodingMode.BaseScaleRgb:
+ case EndpointEncodingMode.BaseScaleRgba:
+ {
+ Rgba32 baseColor = endpointHighRgba;
+ Rgba32 scaled = endpointLowRgba;
+
+ int numChannelsGe = 0;
+ for (int i = 0; i < 3; ++i)
+ {
+ numChannelsGe += endpointHighRgba.GetChannel(i) >= endpointLowRgba.GetChannel(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.GetChannel(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.GetChannel(3), maxValue);
+ values[5] = Quantization.QuantizeCEValueToRange(baseColor.GetChannel(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(Rgba32 c, int maxValue)
+ {
+ int[] array = new int[4];
+ for (int i = 0; i < 4; ++i)
+ {
+ array[i] = Quantization.QuantizeCEValueToRange(c.GetChannel(i), maxValue);
+ }
+
+ return array;
+ }
+
+ private static bool EncodeColorsLuma(Rgba32 endpointLow, Rgba32 endpointHigh, int maxValue, out ColorEndpointMode astcMode, List values)
+ {
+ astcMode = ColorEndpointMode.LdrLumaDirect;
+ ArgumentOutOfRangeException.ThrowIfLessThan(values.Count, 2);
+
+ int avg1 = endpointLow.GetAverage();
+ int avg2 = endpointHigh.GetAverage();
+
+ 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;
+ (Rgba32 decLowOff, Rgba32 decHighOff) = EndpointCodec.DecodeColorsForMode(values.ToArray(), maxValue, ColorEndpointMode.LdrLumaBaseOffset);
+
+ values[0] = quantLow;
+ values[1] = quantHigh;
+ (Rgba32 decLowDir, Rgba32 decHighDir) = EndpointCodec.DecodeColorsForMode(values.ToArray(), maxValue, ColorEndpointMode.LdrLumaDirect);
+
+ int calculateErrorOff = 0;
+ int calculateErrorDir = 0;
+ if (needsWeightSwap)
+ {
+ calculateErrorDir = SquaredError(decLowDir, endpointHigh) + SquaredError(decHighDir, endpointLow);
+ calculateErrorOff = SquaredError(decLowOff, endpointHigh) + SquaredError(decHighOff, endpointLow);
+ }
+ else
+ {
+ calculateErrorDir = SquaredError(decLowDir, endpointLow) + SquaredError(decHighDir, endpointHigh);
+ calculateErrorOff = SquaredError(decLowOff, endpointLow) + 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(Rgba32 endpointLowRgba, Rgba32 endpointHighRgba, int maxValue, bool withAlpha, out ColorEndpointMode astcMode, List values)
+ {
+ astcMode = ColorEndpointMode.LdrRgbDirect;
+ int numChannels = withAlpha ? 4 : 3;
+
+ Rgba32 invertedBlueContractLow = endpointLowRgba.WithInvertedBlueContract();
+ Rgba32 invertedBlueContractHigh = endpointHighRgba.WithInvertedBlueContract();
+
+ int[] directBase = new int[4];
+ int[] directOffset = new int[4];
+ for (int i = 0; i < 4; ++i)
+ {
+ directBase[i] = endpointLowRgba.GetChannel(i);
+ directOffset[i] = Math.Clamp(endpointHighRgba.GetChannel(i) - endpointLowRgba.GetChannel(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.GetChannel(i);
+ invertedBlueContractOffset[i] = Math.Clamp(invertedBlueContractLow.GetChannel(i) - invertedBlueContractHigh.GetChannel(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.GetChannel(i);
+ directOffsetSwapped[i] = Math.Clamp(endpointLowRgba.GetChannel(i) - endpointHighRgba.GetChannel(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.GetChannel(i);
+ invertedBlueContractOffsetSwapped[i] = Math.Clamp(invertedBlueContractHigh.GetChannel(i) - invertedBlueContractLow.GetChannel(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(ClampedRgba32(directBase[0], directBase[1], directBase[2], directBase[3]), ClampedRgba32(directOffset[0], directOffset[1], directOffset[2], directOffset[3]), maxValue);
+ QuantizedEndpointPair bcOffsetQuantized = new(ClampedRgba32(invertedBlueContractBase[0], invertedBlueContractBase[1], invertedBlueContractBase[2], invertedBlueContractBase[3]), ClampedRgba32(invertedBlueContractOffset[0], invertedBlueContractOffset[1], invertedBlueContractOffset[2], invertedBlueContractOffset[3]), maxValue);
+
+ QuantizedEndpointPair offsetSwappedQuantized = new(ClampedRgba32(directBaseSwapped[0], directBaseSwapped[1], directBaseSwapped[2], directBaseSwapped[3]), ClampedRgba32(directOffsetSwapped[0], directOffsetSwapped[1], directOffsetSwapped[2], directOffsetSwapped[3]), maxValue);
+ QuantizedEndpointPair bcOffsetSwappedQuantized = new(ClampedRgba32(invertedBlueContractBaseSwapped[0], invertedBlueContractBaseSwapped[1], invertedBlueContractBaseSwapped[2], invertedBlueContractBaseSwapped[3]), ClampedRgba32(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();
+ Rgba32 lowColor = ClampedRgba32(rgbaLow[0], rgbaLow[1], rgbaLow[2], rgbaLow[3]);
+ Rgba32 highColor = ClampedRgba32(rgbaHigh[0], rgbaHigh[1], rgbaHigh[2], rgbaHigh[3]);
+ int squaredRgbError = withAlpha
+ ? SquaredError(lowColor, endpointLowRgba) + SquaredError(highColor, endpointHighRgba)
+ : SquaredErrorRgb(lowColor, endpointLowRgba) + SquaredErrorRgb(highColor, endpointHighRgba);
+ errors.Add(new CEEncodingOption(squaredRgbError, directQuantized, false, false, false));
+ }
+
+ // 3.2 blue-contract
+ {
+ int[] blueContractUnquantizedLow = bcQuantized.UnquantizedLow();
+ int[] blueContractUnquantizedHigh = bcQuantized.UnquantizedHigh();
+ Rgba32 blueContractLow = RgbaColorExtensions.WithBlueContract(blueContractUnquantizedLow[0], blueContractUnquantizedLow[1], blueContractUnquantizedLow[2], blueContractUnquantizedLow[3]);
+ Rgba32 blueContractHigh = RgbaColorExtensions.WithBlueContract(blueContractUnquantizedHigh[0], blueContractUnquantizedHigh[1], blueContractUnquantizedHigh[2], blueContractUnquantizedHigh[3]);
+
+ // TODO: How to handle alpha for this entire functions??
+ int blueContractSquaredError = withAlpha
+ ? SquaredError(blueContractLow, endpointLowRgba) + SquaredError(blueContractHigh, endpointHighRgba)
+ : SquaredErrorRgb(blueContractLow, endpointLowRgba) + SquaredErrorRgb(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();
+
+ Rgba32 baseColor = ClampedRgba32(baseArr[0], baseArr[1], baseArr[2], baseArr[3]);
+ Rgba32 offsetColor = ClampedRgba32(offsetArr[0], offsetArr[1], offsetArr[2], offsetArr[3]).AsOffsetFrom(baseColor);
+
+ int baseOffsetError = 0;
+ if (swapped)
+ {
+ baseOffsetError = withAlpha
+ ? SquaredError(baseColor, endpointHighRgba) + SquaredError(offsetColor, endpointLowRgba)
+ : SquaredErrorRgb(baseColor, endpointHighRgba) + SquaredErrorRgb(offsetColor, endpointLowRgba);
+ }
+ else
+ {
+ baseOffsetError = withAlpha
+ ? SquaredError(baseColor, endpointLowRgba) + SquaredError(offsetColor, endpointHighRgba)
+ : SquaredErrorRgb(baseColor, endpointLowRgba) + SquaredErrorRgb(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();
+
+ Rgba32 baseColor = ClampedRgba32(baseArr[0], baseArr[1], baseArr[2], baseArr[3]);
+ Rgba32 offsetColor = ClampedRgba32(offsetArr[0], offsetArr[1], offsetArr[2], offsetArr[3]).AsOffsetFrom(baseColor);
+
+ baseColor = baseColor.WithBlueContract();
+ offsetColor = offsetColor.WithBlueContract();
+
+ int squaredBlueContractError = 0;
+ if (swapped)
+ {
+ squaredBlueContractError = withAlpha
+ ? SquaredError(baseColor, endpointLowRgba) + SquaredError(offsetColor, endpointHighRgba)
+ : SquaredErrorRgb(baseColor, endpointLowRgba) + SquaredErrorRgb(offsetColor, endpointHighRgba);
+ }
+ else
+ {
+ squaredBlueContractError = withAlpha
+ ? SquaredError(baseColor, endpointHighRgba) + SquaredError(offsetColor, endpointLowRgba)
+ : SquaredErrorRgb(baseColor, endpointHighRgba) + SquaredErrorRgb(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 Rgba32 originalLow;
+ private readonly Rgba32 originalHigh;
+ private readonly int[] quantizedLow;
+ private readonly int[] quantizedHigh;
+ private readonly int[] unquantizedLow;
+ private readonly int[] unquantizedHigh;
+
+ public QuantizedEndpointPair(Rgba32 low, Rgba32 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 Rgba32 OriginalLow() => this.originalLow;
+
+ public Rgba32 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/Compression/Astc/ColorEncoding/EndpointEncodingMode.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingMode.cs
new file mode 100644
index 00000000..b398fa33
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingMode.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.Astc.ColorEncoding;
+
+internal enum EndpointEncodingMode
+{
+ DirectLuma,
+ DirectLumaAlpha,
+ BaseScaleRgb,
+ BaseScaleRgba,
+ DirectRbg,
+ DirectRgba
+}
diff --git a/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingModeExtensions.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingModeExtensions.cs
new file mode 100644
index 00000000..76fb4a68
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingModeExtensions.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/ColorEncoding/HdrEndpointDecoder.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/HdrEndpointDecoder.cs
new file mode 100644
index 00000000..0077074f
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/HdrEndpointDecoder.cs
@@ -0,0 +1,478 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding.Quantize;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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 (Rgba64 Low, Rgba64 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 (Rgba64 Low, Rgba64 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 (Rgba64 Low, Rgba64 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;
+ }
+
+ Rgba64 low = new((ushort)(y0 << 4), (ushort)(y0 << 4), (ushort)(y0 << 4), 0x7800);
+ Rgba64 high = new((ushort)(y1 << 4), (ushort)(y1 << 4), (ushort)(y1 << 4), 0x7800);
+ return (low, high);
+ }
+
+ private static (Rgba64 Low, Rgba64 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;
+ }
+
+ Rgba64 low = new((ushort)(y0 << 4), (ushort)(y0 << 4), (ushort)(y0 << 4), 0x7800);
+ Rgba64 high = new((ushort)(y1 << 4), (ushort)(y1 << 4), (ushort)(y1 << 4), 0x7800);
+ return (low, high);
+ }
+
+ private static (Rgba64 Low, Rgba64 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);
+
+ Rgba64 low = new((ushort)(red0 << 4), (ushort)(green0 << 4), (ushort)(blue0 << 4), 0x7800);
+ Rgba64 high = new((ushort)(red << 4), (ushort)(green << 4), (ushort)(blue << 4), 0x7800);
+ return (low, high);
+ }
+
+ private static (Rgba64 Low, Rgba64 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)
+ {
+ Rgba64 low = new(
+ (ushort)(v0 << 8),
+ (ushort)(v2 << 8),
+ (ushort)((v4 & 0x7F) << 9),
+ 0x7800);
+ Rgba64 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)
+ };
+
+ Rgba64 lowResult = new((ushort)(red0 << 4), (ushort)(green0 << 4), (ushort)(blue0 << 4), 0x7800);
+ Rgba64 highResult = new((ushort)(red1 << 4), (ushort)(green1 << 4), (ushort)(blue1 << 4), 0x7800);
+ return (lowResult, highResult);
+ }
+
+ private static (Rgba64 Low, Rgba64 High) UnpackHdrRgbDirectLdrAlphaCore(ReadOnlySpan unquantizedValues)
+ {
+ (Rgba64 rgbLow, Rgba64 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);
+
+ Rgba64 low = new(rgbLow.R, rgbLow.G, rgbLow.B, alpha0);
+ Rgba64 high = new(rgbHigh.R, rgbHigh.G, rgbHigh.B, alpha1);
+ return (low, high);
+ }
+
+ private static (Rgba64 Low, Rgba64 High) UnpackHdrRgbDirectHdrAlphaCore(ReadOnlySpan unquantizedValues)
+ {
+ (Rgba64 rgbLow, Rgba64 rgbHigh) = UnpackHdrRgbDirectCore(unquantizedValues[0], unquantizedValues[1], unquantizedValues[2], unquantizedValues[3], unquantizedValues[4], unquantizedValues[5]);
+
+ (ushort alpha0, ushort alpha1) = UnpackHdrAlpha(unquantizedValues[6], unquantizedValues[7]);
+
+ Rgba64 low = new(rgbLow.R, rgbLow.G, rgbLow.B, alpha0);
+ Rgba64 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/Compression/Astc/ColorEncoding/Partition.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/Partition.cs
new file mode 100644
index 00000000..88cc5299
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/Partition.cs
@@ -0,0 +1,248 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/ColorEncoding/RgbaColorExtensions.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/RgbaColorExtensions.cs
new file mode 100644
index 00000000..5ba859da
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/RgbaColorExtensions.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+using static SixLabors.ImageSharp.Textures.Compression.Astc.Core.Rgba32Extensions;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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 Rgba32 WithBlueContract(int red, int green, int blue, int alpha)
+ => ClampedRgba32(
+ 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 Rgba32 WithBlueContract(this Rgba32 color)
+ => WithBlueContract(color.R, color.G, color.B, color.A);
+
+ ///
+ /// The inverse of
+ ///
+ public static Rgba32 WithInvertedBlueContract(this Rgba32 color)
+ => ClampedRgba32(
+ r: (2 * color.R) - color.B,
+ g: (2 * color.G) - color.B,
+ b: color.B,
+ a: color.A);
+
+ public static Rgba32 AsOffsetFrom(this Rgba32 color, Rgba32 baseColor)
+ {
+ int[] offset = [color.R, color.G, color.B, color.A];
+
+ for (int i = 0; i < 4; ++i)
+ {
+ (int a, int b) = BitOperations.TransferPrecision(offset[i], baseColor.GetChannel(i));
+ offset[i] = Math.Clamp(baseColor.GetChannel(i) + a, byte.MinValue, byte.MaxValue);
+ }
+
+ return ClampedRgba32(offset[0], offset[1], offset[2], offset[3]);
+ }
+}
diff --git a/src/ImageSharp.Textures/Compression/Astc/Core/BitOperations.cs b/src/ImageSharp.Textures/Compression/Astc/Core/BitOperations.cs
new file mode 100644
index 00000000..354f2117
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/Core/BitOperations.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/Core/DecimationInfo.cs b/src/ImageSharp.Textures/Compression/Astc/Core/DecimationInfo.cs
new file mode 100644
index 00000000..76da48fb
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/Core/DecimationInfo.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/Core/DecimationTable.cs b/src/ImageSharp.Textures/Compression/Astc/Core/DecimationTable.cs
new file mode 100644
index 00000000..4c375c37
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/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.Compression.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/Compression/Astc/Core/Footprint.cs b/src/ImageSharp.Textures/Compression/Astc/Core/Footprint.cs
new file mode 100644
index 00000000..028f82f7
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/Core/Footprint.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/Core/FootprintType.cs b/src/ImageSharp.Textures/Compression/Astc/Core/FootprintType.cs
new file mode 100644
index 00000000..381d3510
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/Core/FootprintType.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.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/Compression/Astc/Core/Rgba32Extensions.cs b/src/ImageSharp.Textures/Compression/Astc/Core/Rgba32Extensions.cs
new file mode 100644
index 00000000..c9adb5d5
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/Core/Rgba32Extensions.cs
@@ -0,0 +1,80 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+
+///
+/// ASTC-specific extension methods and helpers for .
+///
+internal static class Rgba32Extensions
+{
+ ///
+ /// Creates an from integer values, clamping each channel to [0, 255].
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rgba32 ClampedRgba32(int r, int g, int b, int a = byte.MaxValue)
+ => new(
+ (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));
+
+ ///
+ /// Gets the rounded arithmetic mean of the R, G, and B channels.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static byte GetAverage(this Rgba32 color)
+ {
+ int sum = color.R + color.G + color.B;
+ return (byte)(((sum * 256) + 384) / 768);
+ }
+
+ ///
+ /// Gets the channel value at the specified index: 0=R, 1=G, 2=B, 3=A.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetChannel(this Rgba32 color, int i)
+ => i switch
+ {
+ 0 => color.R,
+ 1 => color.G,
+ 2 => color.B,
+ 3 => color.A,
+ _ => throw new ArgumentOutOfRangeException(nameof(i), $"Index must be between 0 and 3. Actual value: {i}.")
+ };
+
+ ///
+ /// Computes the sum of squared per-channel differences across all four RGBA channels.
+ ///
+ public static int SquaredError(Rgba32 a, Rgba32 b)
+ {
+ int dr = a.R - b.R;
+ int dg = a.G - b.G;
+ int db = a.B - b.B;
+ int da = a.A - b.A;
+ return (dr * dr) + (dg * dg) + (db * db) + (da * da);
+ }
+
+ ///
+ /// Computes the sum of squared per-channel differences for the RGB channels only, ignoring alpha.
+ ///
+ public static int SquaredErrorRgb(Rgba32 a, Rgba32 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);
+ }
+
+ ///
+ /// Returns true if all four channels are within the specified tolerance of the other color.
+ ///
+ public static bool IsCloseTo(this Rgba32 color, Rgba32 other, int tolerance)
+ => Math.Abs(color.R - other.R) <= tolerance &&
+ Math.Abs(color.G - other.G) <= tolerance &&
+ Math.Abs(color.B - other.B) <= tolerance &&
+ Math.Abs(color.A - other.A) <= tolerance;
+}
diff --git a/src/ImageSharp.Textures/Compression/Astc/Core/Rgba64Extensions.cs b/src/ImageSharp.Textures/Compression/Astc/Core/Rgba64Extensions.cs
new file mode 100644
index 00000000..d792431c
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/Core/Rgba64Extensions.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+
+///
+/// ASTC-specific extension methods and helpers for .
+///
+internal static class Rgba64Extensions
+{
+ ///
+ /// Gets the channel value at the specified index: 0=R, 1=G, 2=B, 3=A.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ushort GetChannel(this Rgba64 color, int i)
+ => i switch
+ {
+ 0 => color.R,
+ 1 => color.G,
+ 2 => color.B,
+ 3 => color.A,
+ _ => throw new ArgumentOutOfRangeException(nameof(i), $"Index must be between 0 and 3. Actual value: {i}.")
+ };
+
+ ///
+ /// Returns true if all four channels are within the specified tolerance of the other color.
+ ///
+ public static bool IsCloseTo(this Rgba64 color, Rgba64 other, int tolerance)
+ => Math.Abs(color.R - other.R) <= tolerance &&
+ Math.Abs(color.G - other.G) <= tolerance &&
+ Math.Abs(color.B - other.B) <= tolerance &&
+ Math.Abs(color.A - other.A) <= tolerance;
+}
diff --git a/src/ImageSharp.Textures/Compression/Astc/Core/SimdHelpers.cs b/src/ImageSharp.Textures/Compression/Astc/Core/SimdHelpers.cs
new file mode 100644
index 00000000..a09f021c
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/Core/SimdHelpers.cs
@@ -0,0 +1,177 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
+using SixLabors.ImageSharp.PixelFormats;
+using static SixLabors.ImageSharp.Textures.Compression.Astc.Core.Rgba32Extensions;
+
+namespace SixLabors.ImageSharp.Textures.Compression.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 Rgba32 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 Rgba32 InterpolateColorLdr(Rgba32 low, Rgba32 high, int weight)
+ => ClampedRgba32(
+ 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 Rgba32 InterpolateColorLdrDualPlane(
+ Rgba32 low,
+ Rgba32 high,
+ int weight,
+ int dualPlaneChannel,
+ int dualPlaneWeight)
+ => ClampedRgba32(
+ 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/Compression/Astc/Core/UInt128Extensions.cs b/src/ImageSharp.Textures/Compression/Astc/Core/UInt128Extensions.cs
new file mode 100644
index 00000000..4f423c19
--- /dev/null
+++ b/src/ImageSharp.Textures/Compression/Astc/Core/UInt128Extensions.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Textures.Compression.Astc.Core;
+
+internal static class UInt128Extensions
+{
+ ///
+ /// The lower 64 bits of the