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 [![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp.Textures/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp.Textures/actions) [![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp.Textures/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp) -[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) [![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](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 value + /// + public static ulong Low(this UInt128 value) + => (ulong)(value & 0xFFFFFFFFFFFFFFFFUL); + + /// + /// The upper 64 bits of the value + /// + public static ulong High(this UInt128 value) + => (ulong)(value >> 64); + + /// + /// A mask with the lowest n bits set to 1 + /// + public static UInt128 OnesMask(int n) + { + if (n <= 0) + { + return UInt128.Zero; + } + + if (n >= 128) + { + return new UInt128(~0UL, ~0UL); + } + + if (n <= 64) + { + ulong low = (n == 64) + ? ~0UL + : ((1UL << n) - 1UL); + + return new UInt128(0UL, low); + } + else + { + int highBits = n - 64; + ulong low = ~0UL; + ulong high = (highBits == 64) + ? ~0UL + : ((1UL << highBits) - 1UL); + + return new UInt128(high, low); + } + } + + /// + /// Reverse bits across the full 128-bit value + /// + public static UInt128 ReverseBits(this UInt128 value) + { + ulong revLow = ReverseBits(value.Low()); + ulong revHigh = ReverseBits(value.High()); + + return new UInt128(revLow, revHigh); + } + + private static ulong ReverseBits(ulong x) + { + x = ((x >> 1) & 0x5555555555555555UL) | ((x & 0x5555555555555555UL) << 1); + x = ((x >> 2) & 0x3333333333333333UL) | ((x & 0x3333333333333333UL) << 2); + x = ((x >> 4) & 0x0F0F0F0F0F0F0F0FUL) | ((x & 0x0F0F0F0F0F0F0F0FUL) << 4); + x = ((x >> 8) & 0x00FF00FF00FF00FFUL) | ((x & 0x00FF00FF00FF00FFUL) << 8); + x = ((x >> 16) & 0x0000FFFF0000FFFFUL) | ((x & 0x0000FFFF0000FFFFUL) << 16); + x = (x >> 32) | (x << 32); + + return x; + } +} diff --git a/src/ImageSharp.Textures/Compression/Astc/IO/AstcFile.cs b/src/ImageSharp.Textures/Compression/Astc/IO/AstcFile.cs new file mode 100644 index 00000000..f0c64ee4 --- /dev/null +++ b/src/ImageSharp.Textures/Compression/Astc/IO/AstcFile.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +/// +/// A very simple format consisting of a small header followed immediately +/// by the binary payload for a single image surface. +/// +/// +/// See https://github.com/ARM-software/astc-encoder/blob/main/Docs/FileFormat.md +/// +internal record AstcFile +{ + private readonly AstcFileHeader header; + private readonly byte[] blocks; + + internal AstcFile(AstcFileHeader header, byte[] blocks) + { + this.header = header; + this.blocks = blocks; + this.Footprint = this.GetFootprint(); + } + + public ReadOnlySpan Blocks => this.blocks; + + public Footprint Footprint { get; } + + public int Width => this.header.ImageWidth; + + public int Height => this.header.ImageHeight; + + public int Depth => this.header.ImageDepth; + + public static AstcFile FromMemory(byte[] data) + { + AstcFileHeader header = AstcFileHeader.FromMemory(data.AsSpan(0, AstcFileHeader.SizeInBytes)); + + // Remaining bytes are blocks; C++ reference keeps them as string; here we keep as byte[] + byte[] blocks = new byte[data.Length - AstcFileHeader.SizeInBytes]; + Array.Copy(data, AstcFileHeader.SizeInBytes, blocks, 0, blocks.Length); + + return new AstcFile(header, blocks); + } + + /// + /// Map the block dimensions in the header to a Footprint, if possible. + /// + private Footprint GetFootprint() => (this.header.BlockWidth, this.header.BlockHeight) switch + { + (4, 4) => Footprint.FromFootprintType(FootprintType.Footprint4x4), + (5, 4) => Footprint.FromFootprintType(FootprintType.Footprint5x4), + (5, 5) => Footprint.FromFootprintType(FootprintType.Footprint5x5), + (6, 5) => Footprint.FromFootprintType(FootprintType.Footprint6x5), + (6, 6) => Footprint.FromFootprintType(FootprintType.Footprint6x6), + (8, 5) => Footprint.FromFootprintType(FootprintType.Footprint8x5), + (8, 6) => Footprint.FromFootprintType(FootprintType.Footprint8x6), + (8, 8) => Footprint.FromFootprintType(FootprintType.Footprint8x8), + (10, 5) => Footprint.FromFootprintType(FootprintType.Footprint10x5), + (10, 6) => Footprint.FromFootprintType(FootprintType.Footprint10x6), + (10, 8) => Footprint.FromFootprintType(FootprintType.Footprint10x8), + (10, 10) => Footprint.FromFootprintType(FootprintType.Footprint10x10), + (12, 10) => Footprint.FromFootprintType(FootprintType.Footprint12x10), + (12, 12) => Footprint.FromFootprintType(FootprintType.Footprint12x12), + _ => throw new ArgumentOutOfRangeException($"Unsupported block dimensions: {this.header.BlockWidth}x{this.header.BlockHeight}"), + }; +} diff --git a/src/ImageSharp.Textures/Compression/Astc/IO/AstcFileHeader.cs b/src/ImageSharp.Textures/Compression/Astc/IO/AstcFileHeader.cs new file mode 100644 index 00000000..20e92a52 --- /dev/null +++ b/src/ImageSharp.Textures/Compression/Astc/IO/AstcFileHeader.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +/// +/// The 16 byte ASTC file header +/// +/// +/// ASTC block and decoded image dimensions in texels. +/// +/// For 2D images the Z dimension must be set to 1. +/// +/// Note that the image is not required to be an exact multiple of the compressed block +/// size; the compressed data may include padding that is discarded during decompression. +/// +internal readonly record struct AstcFileHeader(byte BlockWidth, byte BlockHeight, byte BlockDepth, int ImageWidth, int ImageHeight, int ImageDepth) +{ + public const uint Magic = 0x5CA1AB13; + public const int SizeInBytes = 16; + + public static AstcFileHeader FromMemory(Span data) + { + ArgumentOutOfRangeException.ThrowIfLessThan(data.Length, SizeInBytes); + + // ASTC header is 16 bytes: + // - magic (4), + // - blockdim (3), + // - xsize,y,z (each 3 little-endian bytes) + uint magic = BitConverter.ToUInt32(data); + ArgumentOutOfRangeException.ThrowIfNotEqual(magic, Magic); + + return new AstcFileHeader( + BlockWidth: data[4], + BlockHeight: data[5], + BlockDepth: data[6], + ImageWidth: data[7] | (data[8] << 8) | (data[9] << 16), + ImageHeight: data[10] | (data[11] << 8) | (data[12] << 16), + ImageDepth: data[13] | (data[14] << 8) | (data[15] << 16)); + } +} diff --git a/src/ImageSharp.Textures/Compression/Astc/IO/BitStream.cs b/src/ImageSharp.Textures/Compression/Astc/IO/BitStream.cs new file mode 100644 index 00000000..a6e0791d --- /dev/null +++ b/src/ImageSharp.Textures/Compression/Astc/IO/BitStream.cs @@ -0,0 +1,188 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +/// +/// A simple bit stream used for reading/writing arbitrary-sized chunks. +/// +internal struct BitStream +{ + private ulong low; + private ulong high; + private uint dataSize; // number of valid bits in the 128-bit buffer + + public BitStream(ulong data = 0, uint dataSize = 0) + { + this.low = data; + this.high = 0; + this.dataSize = dataSize; + } + + public BitStream(UInt128 data, uint dataSize) + { + this.low = data.Low(); + this.high = data.High(); + this.dataSize = dataSize; + } + + public readonly uint Bits => this.dataSize; + + public void PutBits(T x, int size) + where T : unmanaged + { + ulong value = x switch + { + uint ui => ui, + ulong ul => ul, + ushort us => us, + byte b => b, + _ => Convert.ToUInt64(x, CultureInfo.InvariantCulture) + }; + + if (this.dataSize + (uint)size > 128) + { + throw new InvalidOperationException("Not enough space in BitStream"); + } + + if (this.dataSize < 64) + { + int lowFree = (int)(64 - this.dataSize); + if (size <= lowFree) + { + this.low |= (value & MaskFor(size)) << (int)this.dataSize; + } + else + { + this.low |= (value & MaskFor(lowFree)) << (int)this.dataSize; + this.high |= (value >> lowFree) & MaskFor(size - lowFree); + } + } + else + { + int shift = (int)(this.dataSize - 64); + this.high |= (value & MaskFor(size)) << shift; + } + + this.dataSize += (uint)size; + } + + /// + /// Attempt to retrieve the specified number of bits from the buffer. + /// The buffer is shifted accordingly if successful. + /// + public bool TryGetBits(int count, out T bits) + where T : unmanaged + { + T? result = null; + + if (typeof(T) == typeof(UInt128)) + { + result = (T?)(object?)this.GetBitsUInt128(count); + } + else if (count <= this.dataSize) + { + ulong value = count switch + { + 0 => 0, + <= 64 => this.low & MaskFor(count), + _ => this.low + }; + + this.ShiftBuffer(count); + object boxed = Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); + result = (T)boxed; + } + + bits = result ?? default; + + return result is not null; + } + + public bool TryGetBits(int count, out ulong bits) + { + if (count > this.dataSize) + { + bits = 0; + return false; + } + + bits = count switch + { + 0 => 0, + <= 64 => this.low & MaskFor(count), + _ => this.low + }; + this.ShiftBuffer(count); + return true; + } + + public bool TryGetBits(int count, out uint bits) + { + if (count > this.dataSize) + { + bits = 0; + return false; + } + + bits = (uint)(count switch + { + 0 => 0UL, + <= 64 => this.low & MaskFor(count), + _ => this.low + }); + this.ShiftBuffer(count); + return true; + } + + private static ulong MaskFor(int bits) + => bits == 64 + ? ~0UL + : ((1UL << bits) - 1UL); + + private UInt128? GetBitsUInt128(int count) + { + if (count > this.dataSize) + { + return null; + } + + UInt128 result = count switch + { + 0 => UInt128.Zero, + <= 64 => (UInt128)(this.low & MaskFor(count)), + 128 => new UInt128(this.high, this.low), + _ => new UInt128( + (count - 64 == 64) ? this.high : (this.high & MaskFor(count - 64)), + this.low) + }; + + this.ShiftBuffer(count); + + return result; + } + + private void ShiftBuffer(int count) + { + if (count < 64) + { + this.low = (this.low >> count) | (this.high << (64 - count)); + this.high >>= count; + } + else if (count == 64) + { + this.low = this.high; + this.high = 0; + } + else + { + this.low = this.high >> (count - 64); + this.high = 0; + } + + this.dataSize -= (uint)count; + } +} diff --git a/src/ImageSharp.Textures/Compression/Astc/README.md b/src/ImageSharp.Textures/Compression/Astc/README.md new file mode 100644 index 00000000..932867fd --- /dev/null +++ b/src/ImageSharp.Textures/Compression/Astc/README.md @@ -0,0 +1,44 @@ +Decoder for ASTC (Adaptive Scalable Texture Compression) textures, supporting both LDR and HDR content. + +Originally developed as the standalone [AstcSharp](https://github.com/Erik-White/AstcSharp) library. + +## Background + +ASTC is a lossy block-based texture compression format developed by ARM and standardized by Khronos. It was designed as a single format to replace the patchwork of earlier GPU compression schemes (ETC, S3TC/DXT, PVRTC) that were each tied to specific hardware vendors or pixel formats. + +### Key characteristics + +- **Fixed block size** — Every compressed block is 128 bits (16 bytes), regardless of the footprint. This gives a constant memory bandwidth cost per block fetch on the GPU. +- **Variable footprint** — The block footprint ranges from 4x4 to 12x12 texels, giving bit rates from 8 bpp down to 0.89 bpp. Smaller footprints preserve more detail; larger footprints achieve higher compression. +- **LDR and HDR support** — The same format handles both standard 8-bit and high dynamic range content. HDR blocks use a different endpoint encoding that stores values as UNORM16 instead of UNORM8. +- **Partitions** — A single block can contain up to four partitions, each with its own pair of color endpoints. The decoder selects between partition layouts using a seed-based hash function. +- **Dual plane** — Blocks can optionally use a second set of interpolation weights for one color channel, improving quality for textures where one channel varies independently (e.g. alpha or normal maps). +- **Bounded Integer Sequence Encoding (BISE)** — Weights and color endpoint values are packed using a mixed radix encoding that combines bits with trits (base-3) or quints (base-5) to fill the 128-bit budget more efficiently than pure binary encoding. + +### Block decoding overview + +Decoding a single ASTC block involves: + +1. Reading the block mode to determine the weight grid dimensions, quantization level, and whether the block is void-extent or standard +2. Unpacking the BISE-encoded interpolation weights and upsampling them to the texel grid +3. Decoding the color endpoints for each partition using the endpoint encoding mode (luminance, RGB, RGBA, or HDR variants) +4. For each texel, looking up its partition assignment, then interpolating between the two endpoints using the weight value + +## Features + +- Decode ASTC textures to RGBA32 (LDR) or RGBA float (HDR) +- All 2D block footprints from 4x4 to 12x12 + +## Decoding paths + +The decoder employs three block decoding strategies: + +1. **Direct decode** — Standard approach for normal blocks using batch unquantization without intermediate allocations +2. **Fused decode** — Accelerated path for single-partition, single-plane LDR blocks with combined decoding and interpolation +3. **Void extent** — Handles constant-color blocks + +## Useful links + +- [ASTC specification (Khronos Data Format Specification)](https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html#ASTC) +- [ARM ASTC Encoder](https://github.com/ARM-software/astc-encoder) +- [Google astc-codec](https://github.com/google/astc-codec) diff --git a/src/ImageSharp.Textures/Compression/Astc/TexelBlock/BlockInfo.cs b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/BlockInfo.cs new file mode 100644 index 00000000..2468b776 --- /dev/null +++ b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/BlockInfo.cs @@ -0,0 +1,363 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.ColorEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; + +/// +/// Fused block info computed in a single pass from raw ASTC block bits +/// +internal struct BlockInfo +{ + private static readonly int[] WeightRanges = + [-1, -1, 1, 2, 3, 4, 5, 7, -1, -1, 9, 11, 15, 19, 23, 31]; + + private static readonly int[] ExtraCemBitsForPartition = [0, 2, 5, 8]; + + // Valid BISE endpoint ranges in descending order (only these produce valid encodings) + private static readonly int[] ValidEndpointRanges = + [255, 191, 159, 127, 95, 79, 63, 47, 39, 31, 23, 19, 15, 11, 9, 7, 5]; + + public bool IsValid; + public bool IsVoidExtent; + + // Weight grid + public int GridWidth; + public int GridHeight; + public int WeightRange; + public int WeightBitCount; + + // Partitions + public int PartitionCount; + + // Dual plane + public bool IsDualPlane; + public int DualPlaneChannel; // only valid if IsDualPlane + + // Color endpoints + public int ColorStartBit; + public int ColorBitCount; + public int ColorValuesRange; + public int ColorValuesCount; + + // Endpoint modes (up to 4 partitions) + public ColorEndpointMode EndpointMode0; + public ColorEndpointMode EndpointMode1; + public ColorEndpointMode EndpointMode2; + public ColorEndpointMode EndpointMode3; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ColorEndpointMode GetEndpointMode(int partition) => partition switch + { + 0 => this.EndpointMode0, + 1 => this.EndpointMode1, + 2 => this.EndpointMode2, + 3 => this.EndpointMode3, + _ => this.EndpointMode0 + }; + + /// + /// Decode all block info from raw 128-bit ASTC block data in a single pass. + /// Returns a BlockInfo with IsValid=false if the block is illegal or reserved. + /// Returns a BlockInfo with IsVoidExtent=true for void extent blocks. + /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public static BlockInfo Decode(UInt128 bits) + { + ulong lowBits = bits.Low(); + + // ---- Step 1: Check void extent ---- + // Void extent: bits[0:9] == 0x1FC (9 bits) + if ((lowBits & 0x1FF) == 0x1FC) + { + return new BlockInfo + { + IsVoidExtent = true, + IsValid = !CheckVoidExtentIsIllegal(bits, lowBits) + }; + } + + // ---- Step 2: Decode block mode, grid dims, weight range in ONE pass ---- + // This inlines DecodeBlockMode + DecodeWeightProperties + int gridWidth, gridHeight; + bool isWidthA6HeightB6 = false; + uint rBits; // 3-bit range index component + + // bits[0:2] != 0 + if ((lowBits & 0x3) != 0) + { + ulong modeBits = (lowBits >> 2) & 0x3; // bits[2:4] + int a = (int)((lowBits >> 5) & 0x3); // bits[5:7] + + (gridWidth, gridHeight) = modeBits switch + { + 0 => ((int)((lowBits >> 7) & 0x3) + 4, a + 2), + 1 => ((int)((lowBits >> 7) & 0x3) + 8, a + 2), + 2 => (a + 2, (int)((lowBits >> 7) & 0x3) + 8), + 3 when ((lowBits >> 8) & 1) != 0 => ((int)((lowBits >> 7) & 0x1) + 2, a + 2), + 3 => (a + 2, (int)((lowBits >> 7) & 0x1) + 6), + _ => default // unreachable + }; + + // Range r[2:0] = {bit4, bit1, bit0} for these modes + rBits = (uint)(((lowBits >> 4) & 1) | (((lowBits >> 0) & 0x3) << 1)); + } + else + { + // bits[0:2] == 0 + ulong modeBits = (lowBits >> 5) & 0xF; // bits[5:9] + int a = (int)((lowBits >> 5) & 0x3); // bits[5:7] + + switch (modeBits) + { + case var _ when (modeBits & 0xC) == 0x0: + if ((lowBits & 0xF) == 0) + { + return default; // reserved block mode + } + + gridWidth = 12; + gridHeight = a + 2; + break; + case var _ when (modeBits & 0xC) == 0x4: + gridWidth = a + 2; + gridHeight = 12; + break; + case 0xC: + gridWidth = 6; + gridHeight = 10; + break; + case 0xD: + gridWidth = 10; + gridHeight = 6; + break; + case var _ when (modeBits & 0xC) == 0x8: + gridWidth = a + 6; + gridHeight = (int)((lowBits >> 9) & 0x3) + 6; + isWidthA6HeightB6 = true; + break; + default: + return default; // reserved + } + + // Range r[2:0] = {bit4, bit3, bit2} for these modes + rBits = (uint)(((lowBits >> 4) & 1) | (((lowBits >> 2) & 0x3) << 1)); + } + + // ---- Step 3: Compute weight range from r and h bits ---- + uint hBit = isWidthA6HeightB6 + ? 0u + : (uint)((lowBits >> 9) & 1); + int rangeIdx = (int)((hBit << 3) | rBits); + if ((uint)rangeIdx >= (uint)WeightRanges.Length) + { + return default; + } + + int weightRange = WeightRanges[rangeIdx]; + if (weightRange < 0) + { + return default; + } + + // ---- Step 4: Dual plane ---- + // WidthA6HeightB6 mode never has dual plane; otherwise check bit 10 + bool isDualPlane = !isWidthA6HeightB6 && ((lowBits >> 10) & 1) != 0; + + // ---- Step 5: Partition count ---- + int partitionCount = 1 + (int)((lowBits >> 11) & 0x3); + + // ---- Step 6: Validate weight count ---- + int numWeights = gridWidth * gridHeight; + if (isDualPlane) + { + numWeights *= 2; + } + + if (numWeights > 64) + { + return default; + } + + // 4 partitions + dual plane is illegal + if (partitionCount == 4 && isDualPlane) + { + return default; + } + + // ---- Step 7: Weight bit count ---- + int weightBitCount = BoundedIntegerSequenceCodec.GetBitCountForRange(numWeights, weightRange); + if (weightBitCount is < 24 or > 96) + { + return default; + } + + // ---- Step 8: Endpoint modes + extra CEM bits ---- + ColorEndpointMode cem0 = default, cem1 = default, cem2 = default, cem3 = default; + int colorValuesCount = 0; + int numExtraCEMBits = 0; + + if (partitionCount == 1) + { + cem0 = (ColorEndpointMode)((lowBits >> 13) & 0xF); + colorValuesCount = (((int)cem0 / 4) + 1) * 2; + } + else + { + // Multi-partition CEM decode + ulong sharedCemMarker = (lowBits >> 23) & 0x3; + + if (sharedCemMarker == 0) + { + // Shared CEM: all partitions use the same mode + ColorEndpointMode sharedCem = (ColorEndpointMode)((lowBits >> 25) & 0xF); + cem0 = cem1 = cem2 = cem3 = sharedCem; + for (int i = 0; i < partitionCount; i++) + { + colorValuesCount += sharedCem.GetColorValuesCount(); + } + } + else + { + // Non-shared CEM: per-partition modes + numExtraCEMBits = ExtraCemBitsForPartition[partitionCount - 1]; + + int extraCemStartPos = 128 - numExtraCEMBits - weightBitCount; + UInt128 extraCem = BitOperations.GetBits(bits, extraCemStartPos, numExtraCEMBits); + + ulong cemval = (lowBits >> 23) & 0x3F; // 6 bits starting at bit 23 + int baseCem = (int)(((cemval & 0x3) - 1) * 4); + cemval >>= 2; + + ulong combined = cemval | (extraCem.Low() << 4); + ulong cembits = combined; + + // Extract c bits (1 bit per partition) + Span c = stackalloc int[4]; + for (int i = 0; i < partitionCount; i++) + { + c[i] = (int)(cembits & 0x1); + cembits >>= 1; + } + + // Extract m bits (2 bits per partition) + for (int i = 0; i < partitionCount; i++) + { + int m = (int)(cembits & 0x3); + cembits >>= 2; + ColorEndpointMode mode = (ColorEndpointMode)(baseCem + (4 * c[i]) + m); + switch (i) + { + case 0: + cem0 = mode; + break; + case 1: + cem1 = mode; + break; + case 2: + cem2 = mode; + break; + case 3: + cem3 = mode; + break; + } + + colorValuesCount += mode.GetColorValuesCount(); + } + } + } + + if (colorValuesCount > 18) + { + return default; + } + + // ---- Step 9: Dual plane start position and channel ---- + int dualPlaneBitStartPos = 128 - weightBitCount - numExtraCEMBits; + if (isDualPlane) + { + dualPlaneBitStartPos -= 2; + } + + int dualPlaneChannel = isDualPlane + ? (int)BitOperations.GetBits(bits, dualPlaneBitStartPos, 2).Low() + : -1; + + // ---- Step 10: Color values info ---- + int colorStartBit = (partitionCount == 1) ? 17 : 29; + int maxColorBits = dualPlaneBitStartPos - colorStartBit; + + // Minimum bits needed check + int requiredColorBits = ((13 * colorValuesCount) + 4) / 5; + if (maxColorBits < requiredColorBits) + { + return default; + } + + // Find max color range that fits (only check valid BISE ranges: 17 vs up to 255) + int colorValuesRange = 0, colorBitCount = 0; + foreach (int rv in ValidEndpointRanges) + { + int bitCount = BoundedIntegerSequenceCodec.GetBitCountForRange(colorValuesCount, rv); + if (bitCount <= maxColorBits) + { + colorValuesRange = rv; + colorBitCount = bitCount; + break; + } + } + + if (colorValuesRange == 0) + { + return default; + } + + // ---- Step 11: Validate endpoint modes are not HDR for batchable checks ---- + // (HDR blocks are still valid, just flagged for downstream use) + return new BlockInfo + { + IsValid = true, + IsVoidExtent = false, + GridWidth = gridWidth, + GridHeight = gridHeight, + WeightRange = weightRange, + WeightBitCount = weightBitCount, + PartitionCount = partitionCount, + IsDualPlane = isDualPlane, + DualPlaneChannel = dualPlaneChannel, + ColorStartBit = colorStartBit, + ColorBitCount = colorBitCount, + ColorValuesRange = colorValuesRange, + ColorValuesCount = colorValuesCount, + EndpointMode0 = cem0, + EndpointMode1 = cem1, + EndpointMode2 = cem2, + EndpointMode3 = cem3, + }; + } + + /// + /// Inline void extent validation (replaces PhysicalBlock.CheckVoidExtentIsIllegal). + /// + private static bool CheckVoidExtentIsIllegal(UInt128 bits, ulong lowBits) + { + if (BitOperations.GetBits(bits, 10, 2).Low() != 0x3UL) + { + return true; + } + + int c0 = (int)BitOperations.GetBits(lowBits, 12, 13); + int c1 = (int)BitOperations.GetBits(lowBits, 25, 13); + int c2 = (int)BitOperations.GetBits(lowBits, 38, 13); + int c3 = (int)BitOperations.GetBits(lowBits, 51, 13); + + const int all1s = (1 << 13) - 1; + bool coordsAll1s = c0 == all1s && c1 == all1s && c2 == all1s && c3 == all1s; + + return !coordsAll1s && (c0 >= c1 || c2 >= c3); + } +} diff --git a/src/ImageSharp.Textures/Compression/Astc/TexelBlock/IntermediateBlock.cs b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/IntermediateBlock.cs new file mode 100644 index 00000000..2db99291 --- /dev/null +++ b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/IntermediateBlock.cs @@ -0,0 +1,284 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.ColorEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; +using SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +namespace SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; + +internal static class IntermediateBlock +{ + // From Table C.2.7 -- valid weight ranges + public static readonly int[] ValidWeightRanges = [1, 2, 3, 4, 5, 7, 9, 11, 15, 19, 23, 31]; + + // Returns the maximum endpoint value range or negative on error + private const int EndpointRangeInvalidWeightDimensions = -1; + private const int EndpointRangeNotEnoughColorBits = -2; + + public static IntermediateBlockData? UnpackIntermediateBlock(PhysicalBlock physicalBlock) + { + if (physicalBlock.IsIllegalEncoding || physicalBlock.IsVoidExtent) + { + return null; + } + + BlockInfo info = BlockInfo.Decode(physicalBlock.BlockBits); + if (!info.IsValid || info.IsVoidExtent) + { + return null; + } + + return UnpackIntermediateBlock(physicalBlock.BlockBits, in info); + } + + /// + /// Fast overload that uses pre-computed BlockInfo instead of calling PhysicalBlock getters. + /// + public static IntermediateBlockData? UnpackIntermediateBlock(UInt128 bits, in BlockInfo info) + { + if (!info.IsValid || info.IsVoidExtent) + { + return null; + } + + IntermediateBlockData data = default; + + // Use cached values from BlockInfo instead of PhysicalBlock getters + UInt128 colorBitMask = UInt128Extensions.OnesMask(info.ColorBitCount); + UInt128 colorBits = (bits >> info.ColorStartBit) & colorBitMask; + BitStream colorBitStream = new(colorBits, 128); + + BoundedIntegerSequenceDecoder colorDecoder = BoundedIntegerSequenceDecoder.GetCached(info.ColorValuesRange); + Span colors = stackalloc int[info.ColorValuesCount]; + colorDecoder.Decode(info.ColorValuesCount, ref colorBitStream, colors); + + data.WeightGridX = info.GridWidth; + data.WeightGridY = info.GridHeight; + data.WeightRange = info.WeightRange; + + data.PartitionId = info.PartitionCount > 1 + ? (int)BitOperations.GetBits(bits.Low(), 13, 10) + : null; + + data.DualPlaneChannel = info.IsDualPlane + ? info.DualPlaneChannel + : null; + + int colorIndex = 0; + data.EndpointCount = info.PartitionCount; + for (int i = 0; i < info.PartitionCount; ++i) + { + ColorEndpointMode mode = info.GetEndpointMode(i); + int colorCount = mode.GetColorValuesCount(); + IntermediateEndpointData ep = new() + { Mode = mode, ColorCount = colorCount }; + for (int j = 0; j < colorCount; ++j) + { + ep.Colors[j] = colors[colorIndex++]; + } + + data.Endpoints[i] = ep; + } + + data.EndpointRange = info.ColorValuesRange; + + UInt128 weightBits = UInt128Extensions.ReverseBits(bits) & UInt128Extensions.OnesMask(info.WeightBitCount); + BitStream weightBitStream = new(weightBits, 128); + + BoundedIntegerSequenceDecoder weightDecoder = BoundedIntegerSequenceDecoder.GetCached(data.WeightRange); + int weightsCount = data.WeightGridX * data.WeightGridY; + if (info.IsDualPlane) + { + weightsCount *= 2; + } + + data.Weights = new int[weightsCount]; + data.WeightsCount = weightsCount; + weightDecoder.Decode(weightsCount, ref weightBitStream, data.Weights); + + return data; + } + + public static int EndpointRangeForBlock(in IntermediateBlockData data) + { + int dualPlaneMultiplier = data.DualPlaneChannel.HasValue + ? 2 + : 1; + if (BoundedIntegerSequenceCodec.GetBitCountForRange(data.WeightGridX * data.WeightGridY * dualPlaneMultiplier, data.WeightRange) > 96) + { + return EndpointRangeInvalidWeightDimensions; + } + + int partitionCount = data.EndpointCount; + int bitsWrittenCount = 11 + 2 + + ((partitionCount > 1) ? 10 : 0) + + ((partitionCount == 1) ? 4 : 6); + int availableColorBitsCount = ExtraConfigBitPosition(data) - bitsWrittenCount; + + int colorValuesCount = 0; + for (int i = 0; i < data.EndpointCount; i++) + { + colorValuesCount += data.Endpoints[i].Mode.GetColorValuesCount(); + } + + int bitsNeededCount = ((13 * colorValuesCount) + 4) / 5; + if (availableColorBitsCount < bitsNeededCount) + { + return EndpointRangeNotEnoughColorBits; + } + + int colorValueRange = byte.MaxValue; + for (; colorValueRange > 1; --colorValueRange) + { + int bitCountForRange = BoundedIntegerSequenceCodec.GetBitCountForRange(colorValuesCount, colorValueRange); + if (bitCountForRange <= availableColorBitsCount) + { + break; + } + } + + return colorValueRange; + } + + public static VoidExtentData? UnpackVoidExtent(PhysicalBlock physicalBlock) + { + int? colorStartBit = physicalBlock.GetColorStartBit(); + int? colorBitCount = physicalBlock.GetColorBitCount(); + if (physicalBlock.IsIllegalEncoding || !physicalBlock.IsVoidExtent || colorStartBit is null || colorBitCount is null) + { + return null; + } + + UInt128 colorBits = (physicalBlock.BlockBits >> colorStartBit.Value) & UInt128Extensions.OnesMask(colorBitCount.Value); + + // We expect low 64 bits contain the 4x16-bit channels + ulong low = colorBits.Low(); + + VoidExtentData data = default; + + // Bit 9 of the block mode indicates HDR (1) vs LDR (0) void extent + data.IsHdr = (physicalBlock.BlockBits.Low() & (1UL << 9)) != 0; + data.R = (ushort)((low >> 0) & 0xFFFF); + data.G = (ushort)((low >> 16) & 0xFFFF); + data.B = (ushort)((low >> 32) & 0xFFFF); + data.A = (ushort)((low >> 48) & 0xFFFF); + + int[]? coords = physicalBlock.GetVoidExtentCoordinates(); + data.Coords = new ushort[4]; + if (coords != null) + { + data.Coords[0] = (ushort)coords[0]; + data.Coords[1] = (ushort)coords[1]; + data.Coords[2] = (ushort)coords[2]; + data.Coords[3] = (ushort)coords[3]; + } + else + { + ushort allOnes = (1 << 13) - 1; + for (int i = 0; i < 4; ++i) + { + data.Coords[i] = allOnes; + } + } + + return data; + } + + /// + /// Determines if all endpoint modes in the intermediate block data are the same + /// + internal static bool SharedEndpointModes(in IntermediateBlockData data) + { + if (data.EndpointCount == 0) + { + return true; + } + + ColorEndpointMode first = data.Endpoints[0].Mode; + for (int i = 1; i < data.EndpointCount; i++) + { + if (data.Endpoints[i].Mode != first) + { + return false; + } + } + + return true; + } + + internal static int ExtraConfigBitPosition(in IntermediateBlockData data) + { + bool hasDualChannel = data.DualPlaneChannel.HasValue; + int weightCount = data.WeightGridX * data.WeightGridY * (hasDualChannel ? 2 : 1); + int weightBitCount = BoundedIntegerSequenceCodec.GetBitCountForRange(weightCount, data.WeightRange); + + int extraConfigBitCount = 0; + if (!SharedEndpointModes(data)) + { + int encodedCemBitCount = 2 + (data.EndpointCount * 3); + extraConfigBitCount = encodedCemBitCount - 6; + } + + if (hasDualChannel) + { + extraConfigBitCount += 2; + } + + return 128 - weightBitCount - extraConfigBitCount; + } + + internal struct VoidExtentData + { + public bool IsHdr; + public ushort R; + public ushort G; + public ushort B; + public ushort A; + public ushort[] Coords; // length 4 + } + + [System.Runtime.CompilerServices.InlineArray(MaxColorValues)] + internal struct EndpointColorValues + { + public const int MaxColorValues = 8; +#pragma warning disable CS0169, S1144 // Accessed by runtime via [InlineArray] + private int element0; +#pragma warning restore CS0169, S1144 + } + + internal struct IntermediateBlockData + { + public int WeightGridX; + public int WeightGridY; + public int WeightRange; + + public int[] Weights; + public int WeightsCount; + + public int? PartitionId; + public int? DualPlaneChannel; + + public IntermediateEndpointBuffer Endpoints; + public int EndpointCount; + + public int? EndpointRange; + } + + internal struct IntermediateEndpointData + { + public ColorEndpointMode Mode; + public EndpointColorValues Colors; + public int ColorCount; + } + + [System.Runtime.CompilerServices.InlineArray(MaxPartitions)] + internal struct IntermediateEndpointBuffer + { + public const int MaxPartitions = 4; +#pragma warning disable CS0169, S1144 // Accessed by runtime via [InlineArray] + private IntermediateEndpointData element0; +#pragma warning restore CS0169, S1144 + } +} diff --git a/src/ImageSharp.Textures/Compression/Astc/TexelBlock/IntermediateBlockPacker.cs b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/IntermediateBlockPacker.cs new file mode 100644 index 00000000..aa0e37f6 --- /dev/null +++ b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/IntermediateBlockPacker.cs @@ -0,0 +1,403 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; +using SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +namespace SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; + +internal static class IntermediateBlockPacker +{ + private static readonly BlockModeInfo[] BlockModeInfoTable = [ + new BlockModeInfo { MinWeightGridDimX = 4, MaxWeightGridDimX = 7, MinWeightGridDimY = 2, MaxWeightGridDimY = 5, R0BitPos = 4, R1BitPos = 0, R2BitPos = 1, WeightGridXOffsetBitPos = 7, WeightGridYOffsetBitPos = 5, RequireSinglePlaneLowPrec = false }, + new BlockModeInfo { MinWeightGridDimX = 8, MaxWeightGridDimX = 11, MinWeightGridDimY = 2, MaxWeightGridDimY = 5, R0BitPos = 4, R1BitPos = 0, R2BitPos = 1, WeightGridXOffsetBitPos = 7, WeightGridYOffsetBitPos = 5, RequireSinglePlaneLowPrec = false }, + new BlockModeInfo { MinWeightGridDimX = 2, MaxWeightGridDimX = 5, MinWeightGridDimY = 8, MaxWeightGridDimY = 11, R0BitPos = 4, R1BitPos = 0, R2BitPos = 1, WeightGridXOffsetBitPos = 5, WeightGridYOffsetBitPos = 7, RequireSinglePlaneLowPrec = false }, + new BlockModeInfo { MinWeightGridDimX = 2, MaxWeightGridDimX = 5, MinWeightGridDimY = 6, MaxWeightGridDimY = 7, R0BitPos = 4, R1BitPos = 0, R2BitPos = 1, WeightGridXOffsetBitPos = 5, WeightGridYOffsetBitPos = 7, RequireSinglePlaneLowPrec = false }, + new BlockModeInfo { MinWeightGridDimX = 2, MaxWeightGridDimX = 3, MinWeightGridDimY = 2, MaxWeightGridDimY = 5, R0BitPos = 4, R1BitPos = 0, R2BitPos = 1, WeightGridXOffsetBitPos = 7, WeightGridYOffsetBitPos = 5, RequireSinglePlaneLowPrec = false }, + new BlockModeInfo { MinWeightGridDimX = 12, MaxWeightGridDimX = 12, MinWeightGridDimY = 2, MaxWeightGridDimY = 5, R0BitPos = 4, R1BitPos = 2, R2BitPos = 3, WeightGridXOffsetBitPos = -1, WeightGridYOffsetBitPos = 5, RequireSinglePlaneLowPrec = false }, + new BlockModeInfo { MinWeightGridDimX = 2, MaxWeightGridDimX = 5, MinWeightGridDimY = 12, MaxWeightGridDimY = 12, R0BitPos = 4, R1BitPos = 2, R2BitPos = 3, WeightGridXOffsetBitPos = 5, WeightGridYOffsetBitPos = -1, RequireSinglePlaneLowPrec = false }, + new BlockModeInfo { MinWeightGridDimX = 6, MaxWeightGridDimX = 6, MinWeightGridDimY = 10, MaxWeightGridDimY = 10, R0BitPos = 4, R1BitPos = 2, R2BitPos = 3, WeightGridXOffsetBitPos = -1, WeightGridYOffsetBitPos = -1, RequireSinglePlaneLowPrec = false }, + new BlockModeInfo { MinWeightGridDimX = 10, MaxWeightGridDimX = 10, MinWeightGridDimY = 6, MaxWeightGridDimY = 6, R0BitPos = 4, R1BitPos = 2, R2BitPos = 3, WeightGridXOffsetBitPos = -1, WeightGridYOffsetBitPos = -1, RequireSinglePlaneLowPrec = false }, + new BlockModeInfo { MinWeightGridDimX = 6, MaxWeightGridDimX = 9, MinWeightGridDimY = 6, MaxWeightGridDimY = 9, R0BitPos = 4, R1BitPos = 2, R2BitPos = 3, WeightGridXOffsetBitPos = 5, WeightGridYOffsetBitPos = 9, RequireSinglePlaneLowPrec = true } + ]; + + private static readonly uint[] BlockModeMasks = [0x0u, 0x4u, 0x8u, 0xCu, 0x10Cu, 0x0u, 0x80u, 0x180u, 0x1A0u, 0x100u]; + + public static (string? Error, UInt128 PhysicalBlockBits) Pack(in IntermediateBlock.IntermediateBlockData data) + { + UInt128 physicalBlockBits = 0; + int expectedWeightsCount = data.WeightGridX * data.WeightGridY + * (data.DualPlaneChannel.HasValue ? 2 : 1); + int actualWeightsCount = data.WeightsCount > 0 + ? data.WeightsCount + : (data.Weights?.Length ?? 0); + if (actualWeightsCount != expectedWeightsCount) + { + return ("Incorrect number of weights!", 0); + } + + BitStream bitSink = new(0UL, 0); + + // First we need to encode the block mode. + string? errorMessage = PackBlockMode(data.WeightGridX, data.WeightGridY, data.WeightRange, data.DualPlaneChannel.HasValue, ref bitSink); + if (errorMessage != null) + { + return (errorMessage, 0); + } + + // number of partitions minus one + int partitionCount = data.EndpointCount; + bitSink.PutBits((uint)(partitionCount - 1), 2); + + if (partitionCount > 1) + { + int id = data.PartitionId ?? 0; + ArgumentOutOfRangeException.ThrowIfLessThan(id, 0); + bitSink.PutBits((uint)id, 10); + } + + (BitStream weightSink, int weightBitsCount) = EncodeWeights(data); + + (string? error, int extraConfig) = EncodeColorEndpointModes(data, partitionCount, ref bitSink); + if (error != null) + { + return (error, 0); + } + + int colorValueRange = data.EndpointRange ?? IntermediateBlock.EndpointRangeForBlock(data); + if (colorValueRange == -1) + { + throw new InvalidOperationException($"{nameof(colorValueRange)} must not be EndpointRangeInvalidWeightDimensions"); + } + + if (colorValueRange == -2) + { + return ("Intermediate block emits illegal color range", 0); + } + + BoundedIntegerSequenceEncoder colorEncoder = new(colorValueRange); + for (int i = 0; i < data.EndpointCount; i++) + { + IntermediateBlock.IntermediateEndpointData ep = data.Endpoints[i]; + for (int j = 0; j < ep.ColorCount; j++) + { + int color = ep.Colors[j]; + if (color > colorValueRange) + { + return ("Color outside available color range!", 0); + } + + colorEncoder.AddValue(color); + } + } + + colorEncoder.Encode(ref bitSink); + + int extraConfigBitPosition = IntermediateBlock.ExtraConfigBitPosition(data); + int extraConfigBits = 128 - weightBitsCount - extraConfigBitPosition; + + ArgumentOutOfRangeException.ThrowIfNegative(extraConfigBits); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(extraConfig, 1 << extraConfigBits); + + int bitsToSkip = extraConfigBitPosition - (int)bitSink.Bits; + ArgumentOutOfRangeException.ThrowIfNegative(bitsToSkip); + while (bitsToSkip > 0) + { + int skipping = Math.Min(32, bitsToSkip); + bitSink.PutBits(0u, skipping); + bitsToSkip -= skipping; + } + + if (extraConfigBits > 0) + { + bitSink.PutBits((uint)extraConfig, extraConfigBits); + } + + ArgumentOutOfRangeException.ThrowIfNotEqual(bitSink.Bits, 128u - weightBitsCount); + + // Flush out the bit writer + if (!bitSink.TryGetBits(128 - weightBitsCount, out UInt128 astcBits)) + { + throw new InvalidOperationException(); + } + + if (!weightSink.TryGetBits(weightBitsCount, out UInt128 revWeightBits)) + { + throw new InvalidOperationException(); + } + + UInt128 combined = astcBits | UInt128Extensions.ReverseBits(revWeightBits); + physicalBlockBits = combined; + + PhysicalBlock block = PhysicalBlock.Create(physicalBlockBits); + string? illegal = block.IdentifyInvalidEncodingIssues(); + + return (illegal, physicalBlockBits); + } + + public static (string? Error, UInt128 PhysicalBlockBits) Pack(IntermediateBlock.VoidExtentData data) + { + // Pack void extent + // Assemble the 128-bit value explicitly: low 64 bits = RGBA (4x16) + // high 64 bits = 12-bit header (0xDFC) followed by four 13-bit coords. + ulong high64 = ((ulong)data.A << 48) | ((ulong)data.B << 32) | ((ulong)data.G << 16) | data.R; + ulong low64 = 0UL; + + // Header occupies lowest 12 bits of the high word + low64 |= 0xDFCu; + for (int i = 0; i < 4; ++i) + { + low64 |= ((ulong)(data.Coords[i] & 0x1FFF)) << (12 + (13 * i)); + } + + UInt128 physicalBlockBits; + + // Decide representation: if the RGBA low word is zero we emit the + // compact single-ulong representation (low word = header+coords, + // high word = 0) to match the reference tests. Otherwise the + // low word holds RGBA and the high word holds header+coords. + if (high64 == 0UL) + { + physicalBlockBits = (UInt128)low64; + + // using compact void extent representation + } + else + { + physicalBlockBits = new UInt128(high64, low64); + + // using full void extent representation + } + + PhysicalBlock block = PhysicalBlock.Create(physicalBlockBits); + string? illegal = block.IdentifyInvalidEncodingIssues(); + if (illegal is not null) + { + throw new InvalidOperationException($"{nameof(Pack)}(void extent) produced illegal encoding"); + } + + return (illegal, physicalBlockBits); + } + + private static (string? Error, int[] Range) GetEncodedWeightRange(int range) + { + int[][] validRangeEncodings = [ + [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1], + [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1] + ]; + + int smallestRange = IntermediateBlock.ValidWeightRanges.First(); + int largestRange = IntermediateBlock.ValidWeightRanges.Last(); + if (range < smallestRange || largestRange < range) + { + return ($"Could not find block mode. Invalid weight range: {range} not in [{smallestRange}, {largestRange}]", new int[3]); + } + + int index = Array.FindIndex(IntermediateBlock.ValidWeightRanges, v => v >= range); + if (index < 0) + { + index = IntermediateBlock.ValidWeightRanges.Length - 1; + } + + int[] encoding = validRangeEncodings[index]; + return (null, [encoding[0], encoding[1], encoding[2]]); + } + + private static string? PackBlockMode(int dimX, int dimY, int range, bool dualPlane, ref BitStream bitSink) + { + bool highPrec = range > 7; + (string? maybeErr, int[]? rangeValues) = GetEncodedWeightRange(range); + if (maybeErr != null) + { + return maybeErr; + } + + // Ensure top two bits of r1 and r2 not both zero per reference + if ((rangeValues[1] | rangeValues[2]) <= 0) + { + throw new InvalidOperationException($"{nameof(rangeValues)}[1] | {nameof(rangeValues)}[2] must be > 0"); + } + + for (int mode = 0; mode < BlockModeInfoTable.Length; ++mode) + { + BlockModeInfo blockMode = BlockModeInfoTable[mode]; + bool isValidMode = true; + isValidMode &= blockMode.MinWeightGridDimX <= dimX; + isValidMode &= dimX <= blockMode.MaxWeightGridDimX; + isValidMode &= blockMode.MinWeightGridDimY <= dimY; + isValidMode &= dimY <= blockMode.MaxWeightGridDimY; + isValidMode &= !(blockMode.RequireSinglePlaneLowPrec && dualPlane); + isValidMode &= !(blockMode.RequireSinglePlaneLowPrec && highPrec); + + if (!isValidMode) + { + continue; + } + + uint encodedMode = BlockModeMasks[mode]; + void SetBit(uint value, int offset) + { + if (offset < 0) + { + return; + } + + encodedMode = (encodedMode & ~(1u << offset)) | ((value & 1u) << offset); + } + + SetBit((uint)rangeValues[0], blockMode.R0BitPos); + SetBit((uint)rangeValues[1], blockMode.R1BitPos); + SetBit((uint)rangeValues[2], blockMode.R2BitPos); + + int offsetX = dimX - blockMode.MinWeightGridDimX; + int offsetY = dimY - blockMode.MinWeightGridDimY; + + if (blockMode.WeightGridXOffsetBitPos >= 0) + { + encodedMode |= (uint)(offsetX << blockMode.WeightGridXOffsetBitPos); + } + else + { + ArgumentOutOfRangeException.ThrowIfNotEqual(offsetX, 0); + } + + if (blockMode.WeightGridYOffsetBitPos >= 0) + { + encodedMode |= (uint)(offsetY << blockMode.WeightGridYOffsetBitPos); + } + else + { + ArgumentOutOfRangeException.ThrowIfNotEqual(offsetY, 0); + } + + if (!blockMode.RequireSinglePlaneLowPrec) + { + SetBit(highPrec ? 1u : 0u, 9); + SetBit(dualPlane ? 1u : 0u, 10); + } + + if (bitSink.Bits != 0) + { + throw new InvalidOperationException($"{nameof(bitSink)}.{nameof(bitSink.Bits)} must be 0"); + } + + bitSink.PutBits(encodedMode, 11); + return null; + } + + return "Could not find viable block mode"; + } + + private static (BitStream WeightSink, int WeightBitsCount) EncodeWeights(in IntermediateBlock.IntermediateBlockData data) + { + BitStream weightSink = new(0UL, 0); + BoundedIntegerSequenceEncoder weightsEncoder = new(data.WeightRange); + int weightCount = data.WeightsCount > 0 + ? data.WeightsCount + : (data.Weights?.Length ?? 0); + if (data.Weights is null) + { + throw new InvalidOperationException($"{nameof(data.Weights)} is null in {nameof(EncodeWeights)}"); + } + + for (int i = 0; i < weightCount; i++) + { + weightsEncoder.AddValue(data.Weights[i]); + } + + weightsEncoder.Encode(ref weightSink); + + int weightBitsCount = (int)weightSink.Bits; + if ((int)weightSink.Bits != BoundedIntegerSequenceCodec.GetBitCountForRange(weightCount, data.WeightRange)) + { + throw new InvalidOperationException($"{nameof(weightSink)}.{nameof(weightSink.Bits)} does not match expected bit count"); + } + + return (weightSink, weightBitsCount); + } + + private static (string? Error, int ExtraConfig) EncodeColorEndpointModes(in IntermediateBlock.IntermediateBlockData data, int partitionCount, ref BitStream bitSink) + { + int extraConfig = 0; + bool sharedEndpointMode = IntermediateBlock.SharedEndpointModes(data); + + if (sharedEndpointMode) + { + if (partitionCount > 1) + { + bitSink.PutBits(0u, 2); + } + + bitSink.PutBits((uint)data.Endpoints[0].Mode, 4); + } + else + { + // compute min_class, max_class + int minClass = 2; + int maxClass = 0; + for (int i = 0; i < data.EndpointCount; i++) + { + int endpointModeClass = ((int)data.Endpoints[i].Mode) >> 2; + minClass = Math.Min(minClass, endpointModeClass); + maxClass = Math.Max(maxClass, endpointModeClass); + } + + if (maxClass - minClass > 1) + { + return ("Endpoint modes are invalid", 0); + } + + BitStream cemEncoder = new(0UL, 0); + cemEncoder.PutBits((uint)(minClass + 1), 2); + + for (int i = 0; i < data.EndpointCount; i++) + { + int endpointModeClass = ((int)data.Endpoints[i].Mode) >> 2; + int classSelectorBit = endpointModeClass - minClass; + cemEncoder.PutBits(classSelectorBit, 1); + } + + for (int i = 0; i < data.EndpointCount; i++) + { + int epMode = ((int)data.Endpoints[i].Mode) & 3; + cemEncoder.PutBits(epMode, 2); + } + + int cemBits = 2 + (partitionCount * 3); + if (!cemEncoder.TryGetBits(cemBits, out uint encodedCem)) + { + throw new InvalidOperationException(); + } + + extraConfig = (int)(encodedCem >> 6); + + bitSink.PutBits(encodedCem, Math.Min(6, cemBits)); + } + + // dual plane channel + if (data.DualPlaneChannel.HasValue) + { + int channel = data.DualPlaneChannel.Value; + ArgumentOutOfRangeException.ThrowIfLessThan(channel, 0); + ArgumentOutOfRangeException.ThrowIfGreaterThan(channel, 3); + extraConfig = (extraConfig << 2) | channel; + } + + return (null, extraConfig); + } + + private struct BlockModeInfo + { + public int MinWeightGridDimX; + public int MaxWeightGridDimX; + public int MinWeightGridDimY; + public int MaxWeightGridDimY; + public int R0BitPos; + public int R1BitPos; + public int R2BitPos; + public int WeightGridXOffsetBitPos; + public int WeightGridYOffsetBitPos; + public bool RequireSinglePlaneLowPrec; + } +} diff --git a/src/ImageSharp.Textures/Compression/Astc/TexelBlock/LogicalBlock.cs b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/LogicalBlock.cs new file mode 100644 index 00000000..be30dbb0 --- /dev/null +++ b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/LogicalBlock.cs @@ -0,0 +1,752 @@ +// 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.BlockDecoder; +using SixLabors.ImageSharp.Textures.Compression.Astc.ColorEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; +using static SixLabors.ImageSharp.Textures.Compression.Astc.Core.Rgba32Extensions; + +namespace SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; + +internal sealed class LogicalBlock +{ + private ColorEndpointPair[] endpoints; + private int endpointCount; + private readonly int[] weights; + private Partition partition; + private DualPlaneData? dualPlane; + + public LogicalBlock(Footprint footprint) + { + this.endpoints = [ColorEndpointPair.Ldr(default, default)]; + this.endpointCount = 1; + this.weights = new int[footprint.PixelCount]; + this.partition = new Partition(footprint, 1, 0) + { + Assignment = new int[footprint.PixelCount] + }; + } + + public LogicalBlock(Footprint footprint, in IntermediateBlock.IntermediateBlockData block) + { + this.endpoints = new ColorEndpointPair[block.EndpointCount]; + this.endpointCount = DecodeEndpoints(in block, this.endpoints); + this.partition = ComputePartition(footprint, in block); + this.weights = new int[footprint.PixelCount]; + this.CalculateWeights(footprint, in block); + } + + public LogicalBlock(Footprint footprint, IntermediateBlock.VoidExtentData block) + { + this.endpoints = new ColorEndpointPair[1]; + this.endpointCount = DecodeEndpoints(block, this.endpoints); + this.partition = ComputePartition(footprint); + this.weights = new int[footprint.PixelCount]; + } + + /// + /// Initializes a new instance of the class. + /// Decodes directly from raw bits + BlockInfo, + /// bypassing IntermediateBlock and using batch unquantize operations. + /// + private LogicalBlock(Footprint footprint, UInt128 bits, in BlockInfo info) + { + // --- BISE decode + batch unquantize color endpoint values --- + Span colors = stackalloc int[info.ColorValuesCount]; + FusedBlockDecoder.DecodeBiseValues( + bits, + info.ColorStartBit, + info.ColorBitCount, + info.ColorValuesRange, + info.ColorValuesCount, + colors); + Quantization.UnquantizeCEValuesBatch(colors, info.ColorValuesCount, info.ColorValuesRange); + + // --- Decode endpoints per partition --- + this.endpointCount = info.PartitionCount; + this.endpoints = new ColorEndpointPair[this.endpointCount]; + int colorIndex = 0; + for (int i = 0; i < this.endpointCount; i++) + { + ColorEndpointMode mode = info.GetEndpointMode(i); + int colorCount = mode.GetColorValuesCount(); + ReadOnlySpan slice = colors.Slice(colorIndex, colorCount); + this.endpoints[i] = EndpointCodec.DecodeColorsForModePolymorphicUnquantized(slice, mode); + colorIndex += colorCount; + } + + // --- Set up partition --- + this.partition = info.PartitionCount > 1 + ? Partition.GetASTCPartition( + footprint, + info.PartitionCount, + (int)BitOperations.GetBits(bits.Low(), 13, 10)) + : GenerateSinglePartition(footprint); + + // --- BISE decode + unquantize + infill weights --- + int gridSize = info.GridWidth * info.GridHeight; + bool isDualPlane = info.IsDualPlane; + int totalWeights = isDualPlane ? gridSize * 2 : gridSize; + + Span rawWeights = stackalloc int[totalWeights]; + FusedBlockDecoder.DecodeBiseWeights( + bits, + info.WeightBitCount, + info.WeightRange, + totalWeights, + rawWeights); + + DecimationInfo decimationInfo = DecimationTable.Get(footprint, info.GridWidth, info.GridHeight); + this.weights = new int[footprint.PixelCount]; + + if (!isDualPlane) + { + Quantization.UnquantizeWeightsBatch(rawWeights, gridSize, info.WeightRange); + DecimationTable.InfillWeights(rawWeights[..gridSize], decimationInfo, this.weights); + } + else + { + // De-interleave: even indices -> plane0, odd indices -> plane1 + Span plane0 = stackalloc int[gridSize]; + Span plane1 = stackalloc int[gridSize]; + for (int i = 0; i < gridSize; i++) + { + plane0[i] = rawWeights[i * 2]; + plane1[i] = rawWeights[(i * 2) + 1]; + } + + Quantization.UnquantizeWeightsBatch(plane0, gridSize, info.WeightRange); + Quantization.UnquantizeWeightsBatch(plane1, gridSize, info.WeightRange); + + DecimationTable.InfillWeights(plane0, decimationInfo, this.weights); + + this.dualPlane = new DualPlaneData + { + Channel = info.DualPlaneChannel, + Weights = new int[footprint.PixelCount] + }; + DecimationTable.InfillWeights(plane1, decimationInfo, this.dualPlane.Weights); + } + } + + public Footprint GetFootprint() => this.partition.Footprint; + + public void SetWeightAt(int x, int y, int weight) + { + if (weight is < 0 or > 64) + { + throw new ArgumentOutOfRangeException(nameof(weight)); + } + + this.weights[(y * this.GetFootprint().Width) + x] = weight; + } + + public int WeightAt(int x, int y) => this.weights[(y * this.GetFootprint().Width) + x]; + + public void SetDualPlaneWeightAt(int channel, int x, int y, int weight) + { + ArgumentOutOfRangeException.ThrowIfNegative(channel); + ArgumentOutOfRangeException.ThrowIfGreaterThan(weight, 64); + + if (!this.IsDualPlane()) + { + throw new InvalidOperationException("Not a dual plane block"); + } + + if (this.dualPlane is not null && this.dualPlane.Channel == channel) + { + this.dualPlane.Weights[(y * this.GetFootprint().Width) + x] = weight; + } + else + { + this.SetWeightAt(x, y, weight); + } + } + + public int DualPlaneWeightAt(int channel, int x, int y) + { + if (!this.IsDualPlane()) + { + return this.WeightAt(x, y); + } + + return this.dualPlane is not null && this.dualPlane.Channel == channel + ? this.dualPlane.Weights[(y * this.GetFootprint().Width) + x] + : this.WeightAt(x, y); + } + + public Rgba32 ColorAt(int x, int y) + { + Footprint footprint = this.GetFootprint(); + + ArgumentOutOfRangeException.ThrowIfNegative(x); + ArgumentOutOfRangeException.ThrowIfNegative(y); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(x, footprint.Width); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(y, footprint.Height); + + int index = (y * footprint.Width) + x; + int part = this.partition.Assignment[index]; + ref ColorEndpointPair endpoint = ref this.endpoints[part]; + + int weight = this.weights[index]; + if (!endpoint.IsHdr) + { + if (this.dualPlane is not null) + { + return SimdHelpers.InterpolateColorLdrDualPlane( + endpoint.LdrLow, endpoint.LdrHigh, weight, this.dualPlane.Channel, this.dualPlane.Weights[index]); + } + + return SimdHelpers.InterpolateColorLdr(endpoint.LdrLow, endpoint.LdrHigh, weight); + } + else + { + if (this.dualPlane is not null) + { + int dualPlaneChannel = this.dualPlane.Channel; + int dualPlaneWeight = this.dualPlane.Weights[index]; + int rWeight = dualPlaneChannel == 0 ? dualPlaneWeight : weight; + int gWeight = dualPlaneChannel == 1 ? dualPlaneWeight : weight; + int bWeight = dualPlaneChannel == 2 ? dualPlaneWeight : weight; + int aWeight = dualPlaneChannel == 3 ? dualPlaneWeight : weight; + return ClampedRgba32( + r: InterpolateChannelHdr(endpoint.HdrLow.R, endpoint.HdrHigh.R, rWeight) >> 8, + g: InterpolateChannelHdr(endpoint.HdrLow.G, endpoint.HdrHigh.G, gWeight) >> 8, + b: InterpolateChannelHdr(endpoint.HdrLow.B, endpoint.HdrHigh.B, bWeight) >> 8, + a: InterpolateChannelHdr(endpoint.HdrLow.A, endpoint.HdrHigh.A, aWeight) >> 8); + } + + return ClampedRgba32( + r: InterpolateChannelHdr(endpoint.HdrLow.R, endpoint.HdrHigh.R, weight) >> 8, + g: InterpolateChannelHdr(endpoint.HdrLow.G, endpoint.HdrHigh.G, weight) >> 8, + b: InterpolateChannelHdr(endpoint.HdrLow.B, endpoint.HdrHigh.B, weight) >> 8, + a: InterpolateChannelHdr(endpoint.HdrLow.A, endpoint.HdrHigh.A, weight) >> 8); + } + } + + /// + /// Returns the HDR color at the specified pixel position. + /// + /// + /// For HDR endpoints, returns full 16-bit precision (0-65535) per channel. + /// For LDR endpoints, upscales to HDR range. + /// + public Rgba64 ColorAtHdr(int x, int y) + { + Footprint footprint = this.GetFootprint(); + + ArgumentOutOfRangeException.ThrowIfNegative(x); + ArgumentOutOfRangeException.ThrowIfNegative(y); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(x, footprint.Width); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(y, footprint.Height); + + int index = (y * footprint.Width) + x; + int part = this.partition.Assignment[index]; + ref ColorEndpointPair endpoint = ref this.endpoints[part]; + + int weight = this.weights[index]; + if (endpoint.IsHdr) + { + if (this.dualPlane != null) + { + int dualPlaneChannel = this.dualPlane.Channel; + int dualPlaneWeight = this.dualPlane.Weights[index]; + int rWeight = dualPlaneChannel == 0 ? dualPlaneWeight : weight; + int gWeight = dualPlaneChannel == 1 ? dualPlaneWeight : weight; + int bWeight = dualPlaneChannel == 2 ? dualPlaneWeight : weight; + int aWeight = dualPlaneChannel == 3 ? dualPlaneWeight : weight; + return new Rgba64( + InterpolateChannelHdr(endpoint.HdrLow.R, endpoint.HdrHigh.R, rWeight), + InterpolateChannelHdr(endpoint.HdrLow.G, endpoint.HdrHigh.G, gWeight), + InterpolateChannelHdr(endpoint.HdrLow.B, endpoint.HdrHigh.B, bWeight), + InterpolateChannelHdr(endpoint.HdrLow.A, endpoint.HdrHigh.A, aWeight)); + } + + return new Rgba64( + InterpolateChannelHdr(endpoint.HdrLow.R, endpoint.HdrHigh.R, weight), + InterpolateChannelHdr(endpoint.HdrLow.G, endpoint.HdrHigh.G, weight), + InterpolateChannelHdr(endpoint.HdrLow.B, endpoint.HdrHigh.B, weight), + InterpolateChannelHdr(endpoint.HdrLow.A, endpoint.HdrHigh.A, weight)); + } + else + { + if (this.dualPlane != null) + { + int dualPlaneChannel = this.dualPlane.Channel; + int dualPlaneWeight = this.dualPlane.Weights[index]; + int rWeight = dualPlaneChannel == 0 ? dualPlaneWeight : weight; + int gWeight = dualPlaneChannel == 1 ? dualPlaneWeight : weight; + int bWeight = dualPlaneChannel == 2 ? dualPlaneWeight : weight; + int aWeight = dualPlaneChannel == 3 ? dualPlaneWeight : weight; + return new Rgba64( + (ushort)(InterpolateChannel(endpoint.LdrLow.R, endpoint.LdrHigh.R, rWeight) * 257), + (ushort)(InterpolateChannel(endpoint.LdrLow.G, endpoint.LdrHigh.G, gWeight) * 257), + (ushort)(InterpolateChannel(endpoint.LdrLow.B, endpoint.LdrHigh.B, bWeight) * 257), + (ushort)(InterpolateChannel(endpoint.LdrLow.A, endpoint.LdrHigh.A, aWeight) * 257)); + } + + return new Rgba64( + (ushort)(InterpolateChannel(endpoint.LdrLow.R, endpoint.LdrHigh.R, weight) * 257), + (ushort)(InterpolateChannel(endpoint.LdrLow.G, endpoint.LdrHigh.G, weight) * 257), + (ushort)(InterpolateChannel(endpoint.LdrLow.B, endpoint.LdrHigh.B, weight) * 257), + (ushort)(InterpolateChannel(endpoint.LdrLow.A, endpoint.LdrHigh.A, weight) * 257)); + } + } + + /// + /// Writes the HDR float values for the pixel at (x, y) into the output span. + /// + /// + /// For HDR endpoints, values are in LNS (Log-Normalized Space). After interpolation + /// in LNS, the result is converted to FP16 via then widened to float. + /// For Mode 14 (HDR RGB + LDR Alpha), the alpha channel is UNORM16 instead of LNS. + /// For LDR endpoints, the interpolated UNORM16 value is normalized to 0.0-1.0. + /// + public void WriteHdrPixel(int x, int y, Span output) + { + Footprint footprint = this.GetFootprint(); + + ArgumentOutOfRangeException.ThrowIfNegative(x); + ArgumentOutOfRangeException.ThrowIfNegative(y); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(x, footprint.Width); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(y, footprint.Height); + + int index = (y * footprint.Width) + x; + int part = this.partition.Assignment[index]; + ref ColorEndpointPair endpoint = ref this.endpoints[part]; + + int weight = this.weights[index]; + int dualPlaneChannel = this.dualPlane?.Channel ?? -1; + int dualPlaneWeight = this.dualPlane?.Weights[index] ?? weight; + + if (endpoint.IsHdr) + { + for (int channel = 0; channel < 4; ++channel) + { + int channelWeight = (channel == dualPlaneChannel) + ? dualPlaneWeight + : weight; + ushort interpolated = InterpolateChannelHdr(endpoint.HdrLow.GetChannel(channel), endpoint.HdrHigh.GetChannel(channel), channelWeight); + + if (channel == 3 && endpoint.AlphaIsLdr) + { + // Mode 14: alpha is UNORM16, normalize directly + output[channel] = interpolated / 65535.0f; + } + else if (endpoint.ValuesAreLns) + { + // Normal HDR block: convert from LNS to FP16, then to float + ushort halfFloatBits = LnsToSf16(interpolated); + output[channel] = (float)BitConverter.UInt16BitsToHalf(halfFloatBits); + } + else + { + // Void extent HDR: values are already FP16 bit patterns + output[channel] = (float)BitConverter.UInt16BitsToHalf(interpolated); + } + } + } + else + { + for (int channel = 0; channel < 4; ++channel) + { + int channelWeight = (channel == dualPlaneChannel) + ? dualPlaneWeight + : weight; + int p0 = endpoint.LdrLow.GetChannel(channel); + int p1 = endpoint.LdrHigh.GetChannel(channel); + ushort unorm16 = InterpolateLdrAsUnorm16(p0, p1, channelWeight); + output[channel] = unorm16 / 65535.0f; + } + } + } + + /// + /// Writes all pixels in the block directly to the output buffer in RGBA byte format. + /// Avoids per-pixel method call overhead, type dispatch, and Rgba32 allocation. + /// + public void WriteAllPixelsLdr(Footprint footprint, Span buffer) + { + ref ColorEndpointPair endpoint0 = ref this.endpoints[0]; + + if (!endpoint0.IsHdr && this.partition.PartitionCount == 1) + { + // Fast path: single-partition LDR block (most common case) + int lowR = endpoint0.LdrLow.R, lowG = endpoint0.LdrLow.G, lowB = endpoint0.LdrLow.B, lowA = endpoint0.LdrLow.A; + int highR = endpoint0.LdrHigh.R, highG = endpoint0.LdrHigh.G, highB = endpoint0.LdrHigh.B, highA = endpoint0.LdrHigh.A; + + if (this.dualPlane == null) + { + this.WriteLdrSinglePartition(buffer, footprint, lowR, lowG, lowB, lowA, highR, highG, highB, highA); + } + else + { + int dualPlaneChannel = this.dualPlane.Channel; + int[] dpWeights = this.dualPlane.Weights; + int pixelCount = footprint.PixelCount; + for (int i = 0; i < pixelCount; i++) + { + SimdHelpers.WriteSinglePixelLdrDualPlane( + buffer, + i * 4, + lowR, + lowG, + lowB, + lowA, + highR, + highG, + highB, + highA, + this.weights[i], + dualPlaneChannel, + dpWeights[i]); + } + } + } + else + { + // General path: multi-partition or HDR blocks + this.WriteAllPixelsGeneral(footprint, buffer); + } + } + + public void SetPartition(Partition p) + { + if (!p.Footprint.Equals(this.partition.Footprint)) + { + throw new InvalidOperationException("New partitions may not be for a different footprint"); + } + + this.partition = p; + if (this.endpointCount < p.PartitionCount) + { + ColorEndpointPair[] newEndpoints = new ColorEndpointPair[p.PartitionCount]; + Array.Copy(this.endpoints, newEndpoints, this.endpointCount); + for (int i = this.endpointCount; i < p.PartitionCount; i++) + { + newEndpoints[i] = ColorEndpointPair.Ldr(default, default); + } + + this.endpoints = newEndpoints; + } + + this.endpointCount = p.PartitionCount; + } + + public void SetEndpoints(Rgba32 firstEndpoint, Rgba32 secondEndpoint, int subset) + { + ArgumentOutOfRangeException.ThrowIfNegative(subset); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(subset, this.partition.PartitionCount); + + this.endpoints[subset] = ColorEndpointPair.Ldr(firstEndpoint, secondEndpoint); + } + + public void SetDualPlaneChannel(int channel) + { + if (channel < 0) + { + this.dualPlane = null; + } + else if (this.dualPlane != null) + { + this.dualPlane.Channel = channel; + } + else + { + this.dualPlane = new DualPlaneData { Channel = channel, Weights = (int[])this.weights.Clone() }; + } + } + + public bool IsDualPlane() => this.dualPlane is not null; + + public static LogicalBlock? UnpackLogicalBlock(Footprint footprint, UInt128 bits, in BlockInfo info) + { + if (!info.IsValid) + { + return null; + } + + if (info.IsVoidExtent) + { + // Void extent blocks are rare; fall back to existing PhysicalBlock path + PhysicalBlock pb = PhysicalBlock.Create(bits); + IntermediateBlock.VoidExtentData? voidExtentData = IntermediateBlock.UnpackVoidExtent(pb); + if (voidExtentData is null) + { + return null; + } + + return new LogicalBlock(footprint, voidExtentData.Value); + } + else + { + return new LogicalBlock(footprint, bits, in info); + } + } + + /// + /// Converts a 16-bit LNS (Log-Normalized Space) value to a 16-bit SF16 (FP16) bit pattern. + /// + /// + /// The LNS value encodes a 5-bit exponent in the upper bits and an 11-bit mantissa + /// in the lower bits. The mantissa is transformed using a piecewise linear function + /// before being combined with the exponent to form the FP16 result. + /// + internal static ushort LnsToSf16(int lns) + { + int mantissaComponent = lns & 0x7FF; // Lower 11 bits: mantissa component + int exponentComponent = (lns >> 11) & 0x1F; // Upper 5 bits: exponent component + + int mantissaTransformed; + if (mantissaComponent < 512) + { + mantissaTransformed = mantissaComponent * 3; + } + else if (mantissaComponent < 1536) + { + mantissaTransformed = (mantissaComponent * 4) - 512; + } + else + { + mantissaTransformed = (mantissaComponent * 5) - 2048; + } + + int result = (exponentComponent << 10) | (mantissaTransformed >> 3); + return (ushort)Math.Min(result, 0x7BFF); // Clamp to max finite FP16 + } + + private static int DecodeEndpoints(in IntermediateBlock.IntermediateBlockData block, ColorEndpointPair[] endpointPair) + { + int endpointRange = block.EndpointRange ?? IntermediateBlock.EndpointRangeForBlock(block); + if (endpointRange <= 0) + { + throw new InvalidOperationException("Invalid endpoint range"); + } + + for (int i = 0; i < block.EndpointCount; i++) + { + IntermediateBlock.IntermediateEndpointData ed = block.Endpoints[i]; + ReadOnlySpan colorSpan = ((ReadOnlySpan)ed.Colors)[..ed.ColorCount]; + endpointPair[i] = EndpointCodec.DecodeColorsForModePolymorphic(colorSpan, endpointRange, ed.Mode); + } + + return block.EndpointCount; + } + + private static int DecodeEndpoints(IntermediateBlock.VoidExtentData block, ColorEndpointPair[] endpointPair) + { + if (block.IsHdr) + { + // HDR void extent: ushort values are FP16 bit patterns (not LNS) + Rgba64 hdrColor = new(block.R, block.G, block.B, block.A); + endpointPair[0] = ColorEndpointPair.Hdr(hdrColor, hdrColor, valuesAreLns: false); + } + else + { + // LDR void extent: ushort values are UNORM16, convert to byte range + Rgba32 ldrColor = new( + (byte)(block.R >> 8), + (byte)(block.G >> 8), + (byte)(block.B >> 8), + (byte)(block.A >> 8)); + endpointPair[0] = ColorEndpointPair.Ldr(ldrColor, ldrColor); + } + + return 1; + } + + private static Partition GenerateSinglePartition(Footprint footprint) => new(footprint, 1, 0) + { + Assignment = new int[footprint.PixelCount] + }; + + private static Partition ComputePartition(Footprint footprint, in IntermediateBlock.IntermediateBlockData block) + => block.PartitionId.HasValue + ? Partition.GetASTCPartition(footprint, block.EndpointCount, block.PartitionId.Value) + : GenerateSinglePartition(footprint); + + private static Partition ComputePartition(Footprint footprint) + => GenerateSinglePartition(footprint); + + private void CalculateWeights(Footprint footprint, in IntermediateBlock.IntermediateBlockData block) + { + int gridSize = block.WeightGridX * block.WeightGridY; + int weightFrequency = block.DualPlaneChannel.HasValue ? 2 : 1; + + // Get decimation info once for both planes + DecimationInfo decimationInfo = DecimationTable.Get(footprint, block.WeightGridX, block.WeightGridY); + + // stackalloc avoids per-block heap allocation (max 12×12 = 144 ints = 576 bytes) + Span unquantized = stackalloc int[gridSize]; + for (int i = 0; i < gridSize; ++i) + { + unquantized[i] = Quantization.UnquantizeWeightFromRange( + block.Weights[i * weightFrequency], block.WeightRange); + } + + DecimationTable.InfillWeights(unquantized, decimationInfo, this.weights); + + if (block.DualPlaneChannel.HasValue) + { + DualPlaneData dualPlane = new() + { + Channel = block.DualPlaneChannel.Value, + Weights = new int[footprint.PixelCount] + }; + this.dualPlane = dualPlane; + for (int i = 0; i < gridSize; ++i) + { + unquantized[i] = Quantization.UnquantizeWeightFromRange( + block.Weights[(i * weightFrequency) + 1], block.WeightRange); + } + + DecimationTable.InfillWeights(unquantized, decimationInfo, this.dualPlane.Weights); + } + } + + private static int InterpolateChannel(int p0, int p1, int weight) + { + int c0 = (p0 << 8) | p0; + int c1 = (p1 << 8) | p1; + int c = ((c0 * (64 - weight)) + (c1 * weight) + 32) / 64; + int quantized = ((c * byte.MaxValue) + short.MaxValue) / (ushort.MaxValue + 1); + return Math.Clamp(quantized, 0, byte.MaxValue); + } + + /// + /// Interpolates an LDR channel value and returns the full 16-bit UNORM result + /// (before reduction to byte). Used by the HDR output path for LDR endpoints. + /// + private static ushort InterpolateLdrAsUnorm16(int p0, int p1, int weight) + { + int c0 = (p0 << 8) | p0; + int c1 = (p1 << 8) | p1; + int c = ((c0 * (64 - weight)) + (c1 * weight) + 32) / 64; + return (ushort)Math.Clamp(c, 0, 0xFFFF); + } + + /// + /// Interpolates an HDR channel value between two endpoints using the specified weight. + /// + /// + /// HDR endpoints are already 16-bit values (FP16 bit patterns). Unlike LDR interpolation + /// which expands 8-bit to 16-bit before interpolating, HDR interpolation operates directly + /// on the 16-bit values + /// + private static ushort InterpolateChannelHdr(int p0, int p1, int weight) + { + int c = ((p0 * (64 - weight)) + (p1 * weight) + 32) / 64; + return (ushort)Math.Clamp(c, 0, 0xFFFF); + } + + private void WriteLdrSinglePartition( + Span buffer, + Footprint footprint, + int lowR, + int lowG, + int lowB, + int lowA, + int highR, + int highG, + int highB, + int highA) + { + int pixelCount = footprint.PixelCount; + for (int i = 0; i < pixelCount; i++) + { + SimdHelpers.WriteSinglePixelLdr( + buffer, + i * 4, + lowR, + lowG, + lowB, + lowA, + highR, + highG, + highB, + highA, + this.weights[i]); + } + } + + private void WriteAllPixelsGeneral(Footprint footprint, Span buffer) + { + int pixelCount = footprint.PixelCount; + for (int i = 0; i < pixelCount; i++) + { + int part = this.partition.Assignment[i]; + ref ColorEndpointPair endpoint = ref this.endpoints[part]; + + int weight = this.weights[i]; + if (!endpoint.IsHdr) + { + if (this.dualPlane is not null) + { + SimdHelpers.WriteSinglePixelLdrDualPlane( + buffer, + i * 4, + endpoint.LdrLow.R, + endpoint.LdrLow.G, + endpoint.LdrLow.B, + endpoint.LdrLow.A, + endpoint.LdrHigh.R, + endpoint.LdrHigh.G, + endpoint.LdrHigh.B, + endpoint.LdrHigh.A, + weight, + this.dualPlane.Channel, + this.dualPlane.Weights[i]); + } + else + { + SimdHelpers.WriteSinglePixelLdr( + buffer, + i * 4, + endpoint.LdrLow.R, + endpoint.LdrLow.G, + endpoint.LdrLow.B, + endpoint.LdrLow.A, + endpoint.LdrHigh.R, + endpoint.LdrHigh.G, + endpoint.LdrHigh.B, + endpoint.LdrHigh.A, + weight); + } + } + else + { + int dualPlaneChannel = this.dualPlane?.Channel ?? -1; + int dualPlaneWeight = this.dualPlane?.Weights[i] ?? weight; + int rWeight = dualPlaneChannel == 0 ? dualPlaneWeight : weight; + int gWeight = dualPlaneChannel == 1 ? dualPlaneWeight : weight; + int bWeight = dualPlaneChannel == 2 ? dualPlaneWeight : weight; + int aWeight = dualPlaneChannel == 3 ? dualPlaneWeight : weight; + buffer[(i * 4) + 0] = (byte)(InterpolateChannelHdr( + endpoint.HdrLow.R, + endpoint.HdrHigh.R, + rWeight) >> 8); + buffer[(i * 4) + 1] = (byte)(InterpolateChannelHdr( + endpoint.HdrLow.G, + endpoint.HdrHigh.G, + gWeight) >> 8); + buffer[(i * 4) + 2] = (byte)(InterpolateChannelHdr( + endpoint.HdrLow.B, + endpoint.HdrHigh.B, + bWeight) >> 8); + buffer[(i * 4) + 3] = (byte)(InterpolateChannelHdr( + endpoint.HdrLow.A, + endpoint.HdrHigh.A, + aWeight) >> 8); + } + } + } + + private class DualPlaneData + { + public int Channel { get; set; } + + public int[] Weights { get; set; } = []; + } +} diff --git a/src/ImageSharp.Textures/Compression/Astc/TexelBlock/PhysicalBlock.cs b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/PhysicalBlock.cs new file mode 100644 index 00000000..2c3c8481 --- /dev/null +++ b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/PhysicalBlock.cs @@ -0,0 +1,216 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Compression.Astc.ColorEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; + +/// +/// A physical ASTC texel block (128 bits). +/// Delegates all block mode decoding to . +/// +internal readonly struct PhysicalBlock +{ + public const int SizeInBytes = 16; + private readonly BlockInfo info; + + private PhysicalBlock(UInt128 bits, BlockInfo info) + { + this.BlockBits = bits; + this.info = info; + } + + public UInt128 BlockBits { get; } + + public bool IsVoidExtent => this.info.IsVoidExtent; + + public bool IsIllegalEncoding => !this.info.IsValid; + + public bool IsDualPlane + => this.info.IsValid && !this.info.IsVoidExtent && this.info.IsDualPlane; + + /// + /// Factory method to create a PhysicalBlock from raw bits + /// + public static PhysicalBlock Create(UInt128 bits) + => new(bits, BlockInfo.Decode(bits)); + + public static PhysicalBlock Create(ulong low) => Create((UInt128)low); + + public static PhysicalBlock Create(ulong low, ulong high) => Create(new UInt128(high, low)); + + internal (int Width, int Height)? GetWeightGridDimensions() + => this.info.IsValid && !this.info.IsVoidExtent + ? (this.info.GridWidth, this.info.GridHeight) + : null; + + internal int? GetWeightRange() + => this.info.IsValid && !this.info.IsVoidExtent + ? this.info.WeightRange + : null; + + internal int[]? GetVoidExtentCoordinates() + { + if (!this.info.IsVoidExtent) + { + return null; + } + + // If void extent coords are all 1's then these are not valid void extent coords + ulong voidExtentMask = 0xFFFFFFFFFFFFFDFFUL; + ulong constBlockMode = 0xFFFFFFFFFFFFFDFCUL; + + return this.info.IsValid && (voidExtentMask & this.BlockBits.Low()) != constBlockMode + ? DecodeVoidExtentCoordinates(this.BlockBits) + : null; + } + + /// + /// Get the dual plane channel if dual plane is enabled + /// + /// The dual plane channel if enabled, otherwise null. + internal int? GetDualPlaneChannel() + => this.info.IsValid && this.info.IsDualPlane + ? this.info.DualPlaneChannel + : null; + + internal string? IdentifyInvalidEncodingIssues() + { + if (this.info.IsValid) + { + return null; + } + + return this.info.IsVoidExtent + ? IdentifyVoidExtentIssues(this.BlockBits) + : "Invalid block encoding"; + } + + internal int? GetWeightBitCount() + => this.info.IsValid && !this.info.IsVoidExtent + ? this.info.WeightBitCount + : null; + + internal int? GetWeightStartBit() + => this.info.IsValid && !this.info.IsVoidExtent + ? 128 - this.info.WeightBitCount + : null; + + internal int? GetPartitionsCount() + => this.info.IsValid && !this.info.IsVoidExtent + ? this.info.PartitionCount + : null; + + internal int? GetPartitionId() + { + if (!this.info.IsValid || this.info.IsVoidExtent || this.info.PartitionCount == 1) + { + return null; + } + + return (int)BitOperations.GetBits(this.BlockBits.Low(), 13, 10); + } + + internal ColorEndpointMode? GetEndpointMode(int partition) + { + if (!this.info.IsValid || this.info.IsVoidExtent) + { + return null; + } + + if (partition < 0 || partition >= this.info.PartitionCount) + { + return null; + } + + return this.info.GetEndpointMode(partition); + } + + internal int? GetColorStartBit() + { + if (this.info.IsVoidExtent) + { + return 64; + } + + return this.info.IsValid + ? this.info.ColorStartBit + : null; + } + + internal int? GetColorValuesCount() + { + if (this.info.IsVoidExtent) + { + return 4; + } + + return this.info.IsValid + ? this.info.ColorValuesCount + : null; + } + + internal int? GetColorBitCount() + { + if (this.info.IsVoidExtent) + { + return 64; + } + + return this.info.IsValid + ? this.info.ColorBitCount + : null; + } + + internal int? GetColorValuesRange() + { + if (this.info.IsVoidExtent) + { + return (1 << 16) - 1; + } + + return this.info.IsValid + ? this.info.ColorValuesRange + : null; + } + + internal static int[] DecodeVoidExtentCoordinates(UInt128 astcBits) + { + ulong lowBits = astcBits.Low(); + int[] coords = new int[4]; + for (int i = 0; i < 4; ++i) + { + coords[i] = (int)BitOperations.GetBits(lowBits, 12 + (13 * i), 13); + } + + return coords; + } + + /// + /// Full error-string version for void extent issues (used for error reporting) + /// + private static string? IdentifyVoidExtentIssues(UInt128 bits) + { + if (BitOperations.GetBits(bits, 10, 2).Low() != 0x3UL) + { + return "Reserved bits set for void extent block"; + } + + ulong lowBits = bits.Low(); + int c0 = (int)BitOperations.GetBits(lowBits, 12, 13); + int c1 = (int)BitOperations.GetBits(lowBits, 25, 13); + int c2 = (int)BitOperations.GetBits(lowBits, 38, 13); + int c3 = (int)BitOperations.GetBits(lowBits, 51, 13); + + const int all1s = (1 << 13) - 1; + bool coordsAll1s = c0 == all1s && c1 == all1s && c2 == all1s && c3 == all1s; + + if (!coordsAll1s && (c0 >= c1 || c2 >= c3)) + { + return "Void extent texture coordinates are invalid"; + } + + return null; + } +} diff --git a/src/ImageSharp.Textures/Compression/Astc/TexelBlock/PhysicalBlockMode.cs b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/PhysicalBlockMode.cs new file mode 100644 index 00000000..fe2a4f9c --- /dev/null +++ b/src/ImageSharp.Textures/Compression/Astc/TexelBlock/PhysicalBlockMode.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; + +/// +/// The overall block modes defined in table C.2.8. There are 10 +/// weight grid encoding schemes + void extent. +/// +internal enum PhysicalBlockMode +{ + WidthB4HeightA2, + WidthB8HeightA2, + WidthA2HeightB8, + WidthA2HeightB6, + WidthB2HeightA2, + Width12HeightA2, + WidthA2Height12, + Width6Height10, + Width10Height6, + WidthA6HeightB6, + VoidExtent, +} diff --git a/src/ImageSharp.Textures/ImageSharp.Textures.csproj b/src/ImageSharp.Textures/ImageSharp.Textures.csproj index bad97213..38c21b95 100644 --- a/src/ImageSharp.Textures/ImageSharp.Textures.csproj +++ b/src/ImageSharp.Textures/ImageSharp.Textures.csproj @@ -39,7 +39,6 @@ - diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs index 99259857..32ccf740 100644 --- a/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs +++ b/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using AstcSharp.Core; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding; @@ -36,7 +36,7 @@ public static void DecodeBlock(ReadOnlySpan blockData, int blockWidth, int Footprint footprint = Footprint.FromFootprintType(FootprintFromDimensions(blockWidth, blockHeight)); - AstcSharp.AstcDecoder.DecompressBlock(blockData, footprint, decodedPixels); + Compression.Astc.AstcDecoder.DecompressBlock(blockData, footprint, decodedPixels); } /// diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index c88a39e7..8d0956c9 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -18,9 +18,9 @@ - + - + diff --git a/tests/ImageSharp.Textures.Astc.Reference.Tests/ImageSharp.Textures.Astc.Reference.Tests.csproj b/tests/ImageSharp.Textures.Astc.Reference.Tests/ImageSharp.Textures.Astc.Reference.Tests.csproj new file mode 100644 index 00000000..855545cd --- /dev/null +++ b/tests/ImageSharp.Textures.Astc.Reference.Tests/ImageSharp.Textures.Astc.Reference.Tests.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + + SixLabors.ImageSharp.Textures.Astc.Reference.Tests + SixLabors.ImageSharp.Textures.Astc.Reference.Tests + true + $(NoWarn);CS8002 + + + + + + + + + + + + + + + + + + + + <_AstcEncNativeFiles Include="$(NuGetPackageRoot)astcencodercsharp\5.3.0\runtimes\**\*.*" /> + + + + + diff --git a/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderHdrTests.cs b/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderHdrTests.cs new file mode 100644 index 00000000..e93b566d --- /dev/null +++ b/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderHdrTests.cs @@ -0,0 +1,252 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.ComponentModel; +using SixLabors.ImageSharp.Textures.Astc.Reference.Tests.Utils; +using SixLabors.ImageSharp.Textures.Compression.Astc; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; +using SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +namespace SixLabors.ImageSharp.Textures.Astc.Reference.Tests; + +/// +/// HDR comparison tests between SixLabors.ImageSharp.Textures.Astc and the ARM reference ASTC decoder. +/// These validate that SixLabors.ImageSharp.Textures.Astc produces HDR output matching the official ARM implementation. +/// +public class ReferenceDecoderHdrTests +{ + public static TheoryData AllFootprintTypes => + new() + { + FootprintType.Footprint4x4, + FootprintType.Footprint5x4, + FootprintType.Footprint5x5, + FootprintType.Footprint6x5, + FootprintType.Footprint6x6, + FootprintType.Footprint8x5, + FootprintType.Footprint8x6, + FootprintType.Footprint8x8, + FootprintType.Footprint10x5, + FootprintType.Footprint10x6, + FootprintType.Footprint10x8, + FootprintType.Footprint10x10, + FootprintType.Footprint12x10, + FootprintType.Footprint12x12, + }; + + [Theory] + [InlineData("HDR-A-1x1")] + [InlineData("hdr-tile")] + [InlineData("LDR-A-1x1")] + [InlineData("ldr-tile")] + public void DecompressHdr_WithHdrImage_ShouldMatch(string basename) + { + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "Astc", "HDR", basename + ".astc"); + + byte[] bytes = File.ReadAllBytes(filePath); + AstcFile astcFile = AstcFile.FromMemory(bytes); + (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(astcFile.Footprint.Type); + + Half[] expected = ReferenceDecoder.DecompressHdr( + astcFile.Blocks, astcFile.Width, astcFile.Height, blockX, blockY); + Span actual = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); + + CompareF16(actual, expected, astcFile.Width, astcFile.Height, basename); + } + + [Theory] + [InlineData("atlas_small_4x4")] + [InlineData("atlas_small_5x5")] + [InlineData("atlas_small_6x6")] + [InlineData("atlas_small_8x8")] + public void DecompressHdr_WithLdrImage_ShouldMatch(string basename) + { + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "Astc", basename + ".astc"); + byte[] bytes = File.ReadAllBytes(filePath); + AstcFile astcFile = AstcFile.FromMemory(bytes); + (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(astcFile.Footprint.Type); + + Half[] expected = ReferenceDecoder.DecompressHdr( + astcFile.Blocks, astcFile.Width, astcFile.Height, blockX, blockY); + Span actual = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); + + CompareF16(actual, expected, astcFile.Width, astcFile.Height, basename); + } + + [Theory] + [MemberData(nameof(AllFootprintTypes))] + public void DecompressHdr_SolidColor_ShouldMatch(FootprintType footprintType) + { + (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(footprintType); + int width = blockX; + int height = blockY; + + // Single block: R=G=B=2.0, A=1.0 (above LDR range) + Half[] pixels = new Half[width * height * 4]; + for (int index = 0; index < width * height; index++) + { + pixels[(index * 4) + 0] = (Half)2.0f; + pixels[(index * 4) + 1] = (Half)2.0f; + pixels[(index * 4) + 2] = (Half)2.0f; + pixels[(index * 4) + 3] = (Half)1.0f; + } + + byte[] compressed = ReferenceDecoder.CompressHdr(pixels, width, height, blockX, blockY); + Footprint footprint = Footprint.FromFootprintType(footprintType); + + Half[] expected = ReferenceDecoder.DecompressHdr(compressed, width, height, blockX, blockY); + Span actual = AstcDecoder.DecompressHdrImage(compressed, width, height, footprint); + + CompareF16(actual, expected, width, height, $"BrightSolid_{footprintType}"); + } + + [Theory] + [MemberData(nameof(AllFootprintTypes))] + public void DecompressHdr_Gradient_ShouldMatch(FootprintType footprintType) + { + (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(footprintType); + + // 2×2 blocks for HDR gradient + int width = blockX * 2; + int height = blockY * 2; + + // Gradient from 0.0 to 4.0 + Half[] pixels = new Half[width * height * 4]; + for (int row = 0; row < height; row++) + { + for (int col = 0; col < width; col++) + { + int idx = ((row * width) + col) * 4; + float fraction = (float)((row * width) + col) / ((width * height) - 1); + float value = fraction * 4.0f; + pixels[idx + 0] = (Half)value; + pixels[idx + 1] = (Half)value; + pixels[idx + 2] = (Half)value; + pixels[idx + 3] = (Half)1.0f; + } + } + + byte[] compressed = ReferenceDecoder.CompressHdr(pixels, width, height, blockX, blockY); + Footprint footprint = Footprint.FromFootprintType(footprintType); + + Half[] expected = ReferenceDecoder.DecompressHdr(compressed, width, height, blockX, blockY); + Span actual = AstcDecoder.DecompressHdrImage(compressed, width, height, footprint); + + CompareF16(actual, expected, width, height, $"HdrGradient_{footprintType}"); + } + + [Theory] + [MemberData(nameof(AllFootprintTypes))] + [Description("In ASTC, the encoder picks the best endpoint mode per block. A single image can have some blocks" + + " encoded with LDR modes and others with HDR modes, the encoder optimizes each block independently.")] + public void DecompressHdr_MixedLdrHdr_ShouldMatch(FootprintType footprintType) + { + (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(footprintType); + + // 2×2 blocks + int width = blockX * 2; + int height = blockY * 2; + int halfWidth = width / 2; + + Half[] pixels = new Half[width * height * 4]; + for (int row = 0; row < height; row++) + { + for (int col = 0; col < width; col++) + { + int idx = ((row * width) + col) * 4; + if (col < halfWidth) + { + // LDR left half: values in 0.0-1.0 + float fraction = (float)row / (height - 1); + pixels[idx + 0] = (Half)(fraction * 0.8f); + pixels[idx + 1] = (Half)(fraction * 0.5f); + pixels[idx + 2] = (Half)(fraction * 0.3f); + } + else + { + // HDR right half: values above 1.0 + float fraction = (float)row / (height - 1); + pixels[idx + 0] = (Half)(1.0f + (fraction * 3.0f)); + pixels[idx + 1] = (Half)(0.5f + (fraction * 2.0f)); + pixels[idx + 2] = (Half)(0.2f + (fraction * 1.5f)); + } + + pixels[idx + 3] = (Half)1.0f; + } + } + + byte[] compressed = ReferenceDecoder.CompressHdr(pixels, width, height, blockX, blockY); + Footprint footprint = Footprint.FromFootprintType(footprintType); + + Half[] expected = ReferenceDecoder.DecompressHdr(compressed, width, height, blockX, blockY); + Span actual = AstcDecoder.DecompressHdrImage(compressed, width, height, footprint); + + CompareF16(actual, expected, width, height, $"MixedLdrHdr_{footprintType}"); + } + + /// + /// Compare float output from SixLabors.ImageSharp.Textures.Astc against FP16 output from the ARM reference decoder. + /// SixLabors.ImageSharp.Textures.Astc outputs float values (bit-cast from FP16 for HDR, normalized for LDR). + /// The ARM reference outputs raw FP16 Half values which are converted to float for comparison. + /// + private static void CompareF16(Span actual, Half[] expected, int width, int height, string label) + { + int channelCount = width * height * 4; + Assert.True(actual.Length == channelCount, $"actual float output size should match for {label}"); + Assert.True(expected.Length == channelCount, $"expected F16 output size should match for {label}"); + + int mismatches = 0; + float worstRelDiff = 0; + int worstPixel = -1; + int worstChannel = -1; + + for (int index = 0; index < channelCount; index++) + { + float actualValue = actual[index]; + float expectedValue = (float)expected[index]; + + // Both NaN == match; one NaN == mismatch + if (float.IsNaN(actualValue) && float.IsNaN(expectedValue)) + { + continue; + } + + if (float.IsNaN(actualValue) || float.IsNaN(expectedValue)) + { + mismatches++; + continue; + } + + float absDiff = MathF.Abs(actualValue - expectedValue); + float maxVal = MathF.Max(MathF.Abs(actualValue), MathF.Max(MathF.Abs(expectedValue), 1e-6f)); + float relDiff = absDiff / maxVal; + + // Use a relative tolerance of 0.1% plus absolute tolerance of one FP16 ULP (~0.001 for values near 1.0) + if (absDiff > 0.001f && relDiff > 0.001f) + { + mismatches++; + if (relDiff > worstRelDiff) + { + worstRelDiff = relDiff; + worstPixel = index / 4; + worstChannel = index % 4; + } + } + } + + if (mismatches > 0) + { + string channelName = worstChannel switch { 0 => "R", 1 => "G", 2 => "B", _ => "A" }; + int pixelX = worstPixel % width; + int pixelY = worstPixel / width; + Assert.Fail( + $"[{label}] {mismatches}/{channelCount} F16 channel mismatches. " + + $"Worst: pixel ({pixelX},{pixelY}) channel {channelName}, " + + $"actual={actual[(worstPixel * 4) + worstChannel]:G5} vs " + + $"expected={(float)expected[(worstPixel * 4) + worstChannel]:G5} " + + $"(relDiff={worstRelDiff:P2})."); + } + } +} diff --git a/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderTests.cs b/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderTests.cs new file mode 100644 index 00000000..cf685d7d --- /dev/null +++ b/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderTests.cs @@ -0,0 +1,272 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Astc.Reference.Tests.Utils; +using SixLabors.ImageSharp.Textures.Compression.Astc; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; +using SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +namespace SixLabors.ImageSharp.Textures.Astc.Reference.Tests; + +/// +/// LDR comparison tests between SixLabors.ImageSharp.Textures.Astc and the ARM reference ASTC decoder. +/// These validate that SixLabors.ImageSharp.Textures.Astc produces output matching the official ARM implementation. +/// +public class ReferenceDecoderTests +{ + // Per-channel tolerance for RGBA8 comparisons. + // ASTC spec conformance allows ±1 for UNORM8 output due to rounding differences. + private const int Ldr8BitTolerance = 1; + + public static TheoryData AllFootprintTypes => + new() + { + FootprintType.Footprint4x4, + FootprintType.Footprint5x4, + FootprintType.Footprint5x5, + FootprintType.Footprint6x5, + FootprintType.Footprint6x6, + FootprintType.Footprint8x5, + FootprintType.Footprint8x6, + FootprintType.Footprint8x8, + FootprintType.Footprint10x5, + FootprintType.Footprint10x6, + FootprintType.Footprint10x8, + FootprintType.Footprint10x10, + FootprintType.Footprint12x10, + FootprintType.Footprint12x12, + }; + + [Theory] + [InlineData("atlas_small_4x4")] + [InlineData("atlas_small_5x5")] + [InlineData("atlas_small_6x6")] + [InlineData("atlas_small_8x8")] + [InlineData("checkerboard")] + [InlineData("checkered_4")] + [InlineData("checkered_5")] + [InlineData("checkered_6")] + [InlineData("checkered_7")] + [InlineData("checkered_8")] + [InlineData("checkered_9")] + [InlineData("checkered_10")] + [InlineData("checkered_11")] + [InlineData("checkered_12")] + [InlineData("footprint_4x4")] + [InlineData("footprint_5x4")] + [InlineData("footprint_5x5")] + [InlineData("footprint_6x5")] + [InlineData("footprint_6x6")] + [InlineData("footprint_8x5")] + [InlineData("footprint_8x6")] + [InlineData("footprint_8x8")] + [InlineData("footprint_10x5")] + [InlineData("footprint_10x6")] + [InlineData("footprint_10x8")] + [InlineData("footprint_10x10")] + [InlineData("footprint_12x10")] + [InlineData("footprint_12x12")] + [InlineData("rgb_4x4")] + [InlineData("rgb_5x4")] + [InlineData("rgb_6x6")] + [InlineData("rgb_8x8")] + [InlineData("rgb_12x12")] + public void DecompressLdr_WithImage_ShouldMatch(string basename) + { + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "Astc", basename + ".astc"); + byte[] bytes = File.ReadAllBytes(filePath); + AstcFile astcFile = AstcFile.FromMemory(bytes); + (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(astcFile.Footprint.Type); + + byte[] expected = ReferenceDecoder.DecompressLdr( + astcFile.Blocks, astcFile.Width, astcFile.Height, blockX, blockY); + Span actual = AstcDecoder.DecompressImage( + astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); + + CompareRgba8(actual, expected, astcFile.Width, astcFile.Height, basename); + } + + [Theory] + [MemberData(nameof(AllFootprintTypes))] + public void DecompressLdr_SolidColor_ShouldMatch(FootprintType footprintType) + { + (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(footprintType); + int width = blockX; + int height = blockY; + + // Single solid color block + byte[] pixels = new byte[width * height * 4]; + for (int index = 0; index < width * height; index++) + { + pixels[(index * 4) + 0] = 128; // R + pixels[(index * 4) + 1] = 64; // G + pixels[(index * 4) + 2] = 200; // B + pixels[(index * 4) + 3] = 255; // A + } + + byte[] compressed = ReferenceDecoder.CompressLdr(pixels, width, height, blockX, blockY); + Footprint footprint = Footprint.FromFootprintType(footprintType); + + byte[] expected = ReferenceDecoder.DecompressLdr(compressed, width, height, blockX, blockY); + Span actual = AstcDecoder.DecompressImage(compressed, width, height, footprint); + + CompareRgba8(actual, expected, width, height, $"SolidColor_{footprintType}"); + } + + [Theory] + [MemberData(nameof(AllFootprintTypes))] + public void DecompressLdr_Gradient_ShouldMatch(FootprintType footprintType) + { + (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(footprintType); + + // 2×2 blocks for gradient + int width = blockX * 2; + int height = blockY * 2; + + byte[] pixels = new byte[width * height * 4]; + for (int row = 0; row < height; row++) + { + for (int col = 0; col < width; col++) + { + int idx = ((row * width) + col) * 4; + pixels[idx + 0] = (byte)(255 * col / (width - 1)); // R: left-to-right + pixels[idx + 1] = (byte)(255 * row / (height - 1)); // G: top-to-bottom + pixels[idx + 2] = (byte)(255 - (255 * col / (width - 1))); // B: inverse of R + pixels[idx + 3] = 255; + } + } + + byte[] compressed = ReferenceDecoder.CompressLdr(pixels, width, height, blockX, blockY); + Footprint footprint = Footprint.FromFootprintType(footprintType); + + byte[] expected = ReferenceDecoder.DecompressLdr(compressed, width, height, blockX, blockY); + Span actual = AstcDecoder.DecompressImage(compressed, width, height, footprint); + + CompareRgba8(actual, expected, width, height, $"Gradient_{footprintType}"); + } + + [Theory] + [MemberData(nameof(AllFootprintTypes))] + public void DecompressLdr_RandomNoise_ShouldMatch(FootprintType footprintType) + { + (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(footprintType); + + // 2×2 blocks + int width = blockX * 2; + int height = blockY * 2; + + Random rng = new(42); // Fixed seed for reproducibility + byte[] pixels = new byte[width * height * 4]; + rng.NextBytes(pixels); + + // Force alpha to 255 so compression doesn't introduce alpha-related variance + for (int index = 3; index < pixels.Length; index += 4) + { + pixels[index] = byte.MaxValue; + } + + byte[] compressed = ReferenceDecoder.CompressLdr(pixels, width, height, blockX, blockY); + Footprint footprint = Footprint.FromFootprintType(footprintType); + + byte[] expected = ReferenceDecoder.DecompressLdr(compressed, width, height, blockX, blockY); + Span actual = AstcDecoder.DecompressImage(compressed, width, height, footprint); + + CompareRgba8(actual, expected, width, height, $"RandomNoise_{footprintType}"); + } + + [Theory] + [MemberData(nameof(AllFootprintTypes))] + public void DecompressLdr_NonBlockAlignedDimensions_ShouldMatch(FootprintType footprintType) + { + (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(footprintType); + + // Non-block-aligned dimensions: use dimensions that don't evenly divide by block size + int width = blockX + (blockX / 2) + 1; // e.g. for 4x4: 7, for 8x8: 13 + int height = blockY + (blockY / 2) + 1; + + Random rng = new(123); + byte[] pixels = new byte[width * height * 4]; + rng.NextBytes(pixels); + for (int index = 3; index < pixels.Length; index += 4) + { + pixels[index] = byte.MaxValue; + } + + byte[] compressed = ReferenceDecoder.CompressLdr(pixels, width, height, blockX, blockY); + Footprint footprint = Footprint.FromFootprintType(footprintType); + + byte[] expected = ReferenceDecoder.DecompressLdr(compressed, width, height, blockX, blockY); + Span actual = AstcDecoder.DecompressImage(compressed, width, height, footprint); + + CompareRgba8(actual, expected, width, height, $"NonAligned_{footprintType}"); + } + + [Fact] + public void DecompressLdr_VoidExtentBlock_ShouldMatch() + { + // Manually construct a void-extent constant-color block (128 bits): + // Bits [0..8] = 0b111111100 (0x1FC, void-extent marker) + // Bit [9] = 0 (LDR mode) + // Bits [10..11] = 0b11 (reserved, must be 11 for valid void-extent) + // Bits [12..63] = all 1s (no extent coordinates = constant color block) + // Bits [64..79] = R (UNORM16) + // Bits [80..95] = G (UNORM16) + // Bits [96..111] = B (UNORM16) + // Bits [112..127]= A (UNORM16) + byte[] block = new byte[16]; + ulong low = 0xFFFFFFFFFFFFFDFC; + ulong high = (0xFFFFUL << 48) | (0xC000UL << 32) | (0x4000UL << 16) | 0x8000; + BitConverter.TryWriteBytes(block.AsSpan(0, 8), low); + BitConverter.TryWriteBytes(block.AsSpan(8, 8), high); + + const int blockX = 4; + const int blockY = 4; + Footprint footprint = Footprint.FromFootprintType(FootprintType.Footprint4x4); + + byte[] expected = ReferenceDecoder.DecompressLdr(block, blockX, blockY, blockX, blockY); + Span actual = AstcDecoder.DecompressImage(block, blockX, blockY, footprint); + + CompareRgba8(actual, expected, blockX, blockY, "VoidExtent"); + } + + /// + /// Compare RGBA8 output from both decoders with per-channel tolerance. + /// + private static void CompareRgba8(Span actual, byte[] expected, int width, int height, string label) + { + int pixelCount = width * height * 4; + Assert.True(actual.Length == pixelCount, $"actual output size should match for {label}"); + Assert.True(expected.Length == pixelCount, $"expected output size should match for {label}"); + + int mismatches = 0; + int worstDiff = 0; + int worstPixel = -1; + int worstChannel = -1; + + for (int index = 0; index < pixelCount; index++) + { + int diff = Math.Abs(actual[index] - expected[index]); + if (diff > Ldr8BitTolerance) + { + mismatches++; + if (diff > worstDiff) + { + worstDiff = diff; + worstPixel = index / 4; + worstChannel = index % 4; + } + } + } + + if (mismatches > 0) + { + string channelName = worstChannel switch { 0 => "R", 1 => "G", 2 => "B", _ => "A" }; + int pixelX = worstPixel % width; + int pixelY = worstPixel / width; + Assert.Fail( + $"[{label}] {mismatches} channel mismatches exceed tolerance ±{Ldr8BitTolerance}. " + + $"Worst: pixel ({pixelX},{pixelY}) channel {channelName}, " + + $"actual={actual[(worstPixel * 4) + worstChannel]} vs expected={expected[(worstPixel * 4) + worstChannel]} (diff={worstDiff})"); + } + } +} diff --git a/tests/ImageSharp.Textures.Astc.Reference.Tests/Utils/ReferenceDecoder.cs b/tests/ImageSharp.Textures.Astc.Reference.Tests/Utils/ReferenceDecoder.cs new file mode 100644 index 00000000..a1d68cff --- /dev/null +++ b/tests/ImageSharp.Textures.Astc.Reference.Tests/Utils/ReferenceDecoder.cs @@ -0,0 +1,238 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using AstcEncoder; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Astc.Reference.Tests.Utils; + +/// +/// Wrapper around the ARM reference ASTC encoder/decoder (AstcEncoderCSharp package) +/// for use as a comparison baseline in tests. +/// +internal static class ReferenceDecoder +{ + private static readonly AstcencSwizzle IdentitySwizzle = new() + { + r = AstcencSwz.AstcencSwzR, + g = AstcencSwz.AstcencSwzG, + b = AstcencSwz.AstcencSwzB, + a = AstcencSwz.AstcencSwzA, + }; + + /// + /// Decompress ASTC blocks to RGBA8 (LDR) using the ARM reference decoder. + /// + public static byte[] DecompressLdr(ReadOnlySpan blocks, int w, int h, int blockX, int blockY) + { + AstcencError error = Astcenc.AstcencConfigInit( + AstcencProfile.AstcencPrfLdr, + (uint)blockX, + (uint)blockY, + 1, + Astcenc.AstcencPreFastest, + AstcencFlags.DecompressOnly, + out AstcencConfig config); + ThrowOnError(error, "ConfigInit(LDR)"); + + error = Astcenc.AstcencContextAlloc(ref config, 1, out AstcencContext context); + ThrowOnError(error, "ContextAlloc(LDR)"); + + try + { + int pixelCount = w * h; + byte[] outputBytes = new byte[pixelCount * 4]; // RGBA8 + + AstcencImage image = new() + { + dimX = (uint)w, + dimY = (uint)h, + dimZ = 1, + dataType = AstcencType.AstcencTypeU8, + data = outputBytes, + }; + + // We need a mutable copy of blocks for the Span parameter + byte[] blocksCopy = blocks.ToArray(); + error = Astcenc.AstcencDecompressImage(context, blocksCopy, ref image, IdentitySwizzle, 0); + ThrowOnError(error, "DecompressImage(LDR)"); + + return outputBytes; + } + finally + { + Astcenc.AstcencContextFree(context); + } + } + + /// + /// Decompress ASTC blocks to FP16 RGBA (HDR) using the ARM reference decoder. + /// + public static Half[] DecompressHdr(ReadOnlySpan blocks, int w, int h, int blockX, int blockY) + { + AstcencError error = Astcenc.AstcencConfigInit( + AstcencProfile.AstcencPrfHdr, + (uint)blockX, + (uint)blockY, + 1, + Astcenc.AstcencPreFastest, + AstcencFlags.DecompressOnly, + out AstcencConfig config); + ThrowOnError(error, "ConfigInit(HDR)"); + + error = Astcenc.AstcencContextAlloc(ref config, 1, out AstcencContext context); + ThrowOnError(error, "ContextAlloc(HDR)"); + + try + { + int pixelCount = w * h; + Half[] outputHalves = new Half[pixelCount * 4]; // RGBA FP16 + byte[] outputBytes = MemoryMarshal.AsBytes(outputHalves.AsSpan()).ToArray(); + + AstcencImage image = new() + { + dimX = (uint)w, + dimY = (uint)h, + dimZ = 1, + dataType = AstcencType.AstcencTypeF16, + data = outputBytes, + }; + + byte[] blocksCopy = blocks.ToArray(); + error = Astcenc.AstcencDecompressImage(context, blocksCopy, ref image, IdentitySwizzle, 0); + ThrowOnError(error, "DecompressImage(HDR)"); + + // Copy the decompressed bytes back into the Half array + MemoryMarshal.AsBytes(outputHalves.AsSpan()).Clear(); + outputBytes.AsSpan().CopyTo(MemoryMarshal.AsBytes(outputHalves.AsSpan())); + + return outputHalves; + } + finally + { + Astcenc.AstcencContextFree(context); + } + } + + /// + /// Compress RGBA8 pixel data to ASTC using the ARM reference encoder (LDR). + /// + public static byte[] CompressLdr(byte[] pixels, int w, int h, int blockX, int blockY) + { + AstcencError error = Astcenc.AstcencConfigInit( + AstcencProfile.AstcencPrfLdr, + (uint)blockX, + (uint)blockY, + 1, + Astcenc.AstcencPreMedium, + 0, + out AstcencConfig config); + ThrowOnError(error, "ConfigInit(CompressLDR)"); + + error = Astcenc.AstcencContextAlloc(ref config, 1, out AstcencContext context); + ThrowOnError(error, "ContextAlloc(CompressLDR)"); + + try + { + AstcencImage image = new() + { + dimX = (uint)w, + dimY = (uint)h, + dimZ = 1, + dataType = AstcencType.AstcencTypeU8, + data = pixels, + }; + + int blocksWide = (w + blockX - 1) / blockX; + int blocksHigh = (h + blockY - 1) / blockY; + byte[] compressedData = new byte[blocksWide * blocksHigh * 16]; + + error = Astcenc.AstcencCompressImage(context, ref image, IdentitySwizzle, compressedData, 0); + ThrowOnError(error, "CompressImage(LDR)"); + + return compressedData; + } + finally + { + Astcenc.AstcencContextFree(context); + } + } + + /// + /// Compress FP16 RGBA pixel data to ASTC using the ARM reference encoder (HDR). + /// + public static byte[] CompressHdr(Half[] pixels, int w, int h, int blockX, int blockY) + { + AstcencError error = Astcenc.AstcencConfigInit( + AstcencProfile.AstcencPrfHdr, + (uint)blockX, + (uint)blockY, + 1, + Astcenc.AstcencPreMedium, + 0, + out AstcencConfig config); + ThrowOnError(error, "ConfigInit(CompressHDR)"); + + error = Astcenc.AstcencContextAlloc(ref config, 1, out AstcencContext context); + ThrowOnError(error, "ContextAlloc(CompressHDR)"); + + try + { + byte[] pixelBytes = MemoryMarshal.AsBytes(pixels.AsSpan()).ToArray(); + + AstcencImage image = new() + { + dimX = (uint)w, + dimY = (uint)h, + dimZ = 1, + dataType = AstcencType.AstcencTypeF16, + data = pixelBytes, + }; + + int blocksWide = (w + blockX - 1) / blockX; + int blocksHigh = (h + blockY - 1) / blockY; + byte[] compressedData = new byte[blocksWide * blocksHigh * 16]; + + error = Astcenc.AstcencCompressImage(context, ref image, IdentitySwizzle, compressedData, 0); + ThrowOnError(error, "CompressImage(HDR)"); + + return compressedData; + } + finally + { + Astcenc.AstcencContextFree(context); + } + } + + /// + /// Map a FootprintType to its (blockX, blockY) dimensions. + /// + public static (int BlockX, int BlockY) ToBlockDimensions(FootprintType footprint) => footprint switch + { + FootprintType.Footprint4x4 => (4, 4), + FootprintType.Footprint5x4 => (5, 4), + FootprintType.Footprint5x5 => (5, 5), + FootprintType.Footprint6x5 => (6, 5), + FootprintType.Footprint6x6 => (6, 6), + FootprintType.Footprint8x5 => (8, 5), + FootprintType.Footprint8x6 => (8, 6), + FootprintType.Footprint8x8 => (8, 8), + FootprintType.Footprint10x5 => (10, 5), + FootprintType.Footprint10x6 => (10, 6), + FootprintType.Footprint10x8 => (10, 8), + FootprintType.Footprint10x10 => (10, 10), + FootprintType.Footprint12x10 => (12, 10), + FootprintType.Footprint12x12 => (12, 12), + _ => throw new ArgumentOutOfRangeException(nameof(footprint)), + }; + + private static void ThrowOnError(AstcencError error, string operation) + { + if (error != AstcencError.AstcencSuccess) + { + string message = Astcenc.GetErrorString(error) ?? error.ToString(); + throw new InvalidOperationException($"ARM ASTC encoder {operation} failed: {message}"); + } + } +} diff --git a/tests/ImageSharp.Textures.Benchmarks/AstcDecodingBenchmark.cs b/tests/ImageSharp.Textures.Benchmarks/AstcDecodingBenchmark.cs new file mode 100644 index 00000000..ed364df8 --- /dev/null +++ b/tests/ImageSharp.Textures.Benchmarks/AstcDecodingBenchmark.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +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.Benchmarks; + +[MemoryDiagnoser] +public class AstcDecodingBenchmark +{ + private AstcFile? astcFile; + + [GlobalSetup] + public void Setup() + { + string path = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "Astc", "atlas_small_4x4.astc"); + byte[] astcData = File.ReadAllBytes(path); + this.astcFile = AstcFile.FromMemory(astcData); + } + + [Benchmark] + public bool ParseBlock() + { + ReadOnlySpan blocks = this.astcFile!.Blocks; + Span blockBytes = stackalloc byte[16]; + blocks[..16].CopyTo(blockBytes); + ulong low = BitConverter.ToUInt64(blockBytes); + ulong high = BitConverter.ToUInt64(blockBytes[8..]); + PhysicalBlock phyiscalBlock = PhysicalBlock.Create((UInt128)low | ((UInt128)high << 64)); + + return !phyiscalBlock.IsIllegalEncoding; + } + + [Benchmark] + public bool DecodeEndpoints() + { + ReadOnlySpan blocks = this.astcFile!.Blocks; + Span blockBytes = stackalloc byte[16]; + blocks[..16].CopyTo(blockBytes); + ulong low = BitConverter.ToUInt64(blockBytes); + ulong high = BitConverter.ToUInt64(blockBytes[8..]); + PhysicalBlock physicalBlock = PhysicalBlock.Create((UInt128)low | ((UInt128)high << 64)); + + IntermediateBlock.IntermediateBlockData? blockData = IntermediateBlock.UnpackIntermediateBlock(physicalBlock); + + return blockData is not null; + } + + [Benchmark] + public bool Partitioning() + { + ReadOnlySpan blocks = this.astcFile!.Blocks; + Span blockBytes = stackalloc byte[16]; + blocks[..16].CopyTo(blockBytes); + ulong low = BitConverter.ToUInt64(blockBytes); + ulong high = BitConverter.ToUInt64(blockBytes[8..]); + UInt128 bits = (UInt128)low | ((UInt128)high << 64); + BlockInfo info = BlockInfo.Decode(bits); + LogicalBlock? logicalBlock = LogicalBlock.UnpackLogicalBlock(Footprint.Get4x4(), bits, in info) + ?? throw new InvalidOperationException("Failed to unpack block"); + + return logicalBlock is not null; + } +} diff --git a/tests/ImageSharp.Textures.Benchmarks/AstcImageDecodeBenchmark.cs b/tests/ImageSharp.Textures.Benchmarks/AstcImageDecodeBenchmark.cs new file mode 100644 index 00000000..e6a77578 --- /dev/null +++ b/tests/ImageSharp.Textures.Benchmarks/AstcImageDecodeBenchmark.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Textures.Compression.Astc.IO; +using SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; + +namespace SixLabors.ImageSharp.Textures.Benchmarks; + +[MemoryDiagnoser] +public class AstcImageDecodeBenchmark +{ + private AstcFile? astcFile; + + [GlobalSetup] + public void Setup() + { + string path = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "Astc", "atlas_small_4x4.astc"); + byte[] astcData = File.ReadAllBytes(path); + this.astcFile = AstcFile.FromMemory(astcData); + } + + [Benchmark] + public void ImageDecode() + { + ReadOnlySpan blocks = this.astcFile!.Blocks; + int numBlocks = blocks.Length / 16; + Span blockBytes = stackalloc byte[16]; + for (int i = 0; i < numBlocks; ++i) + { + blocks.Slice(i * 16, 16).CopyTo(blockBytes); + ulong low = BitConverter.ToUInt64(blockBytes); + ulong high = BitConverter.ToUInt64(blockBytes[8..]); + PhysicalBlock block = PhysicalBlock.Create((UInt128)low | ((UInt128)high << 64)); + _ = IntermediateBlock.UnpackIntermediateBlock(block); + } + } +} diff --git a/tests/ImageSharp.Textures.Benchmarks/Config.cs b/tests/ImageSharp.Textures.Benchmarks/Config.cs index f57970ad..06502c3f 100644 --- a/tests/ImageSharp.Textures.Benchmarks/Config.cs +++ b/tests/ImageSharp.Textures.Benchmarks/Config.cs @@ -6,23 +6,16 @@ using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; -namespace SixLabors.ImageSharp.Textures.Benchmarks +namespace SixLabors.ImageSharp.Textures.Benchmarks; + +public class Config : ManualConfig { - public class Config : ManualConfig - { - public Config() - { - this.AddDiagnoser(MemoryDiagnoser.Default); - } + public Config() => this.AddDiagnoser(MemoryDiagnoser.Default); - public class ShortRun : Config - { - public ShortRun() - { - this.AddJob( - Job.Default.WithRuntime(ClrRuntime.Net472).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3), - Job.Default.WithRuntime(CoreRuntime.Core31).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3)); - } - } + public class ShortRun : Config + { + public ShortRun() => this.AddJob( + Job.Default.WithRuntime(ClrRuntime.Net472).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3), + Job.Default.WithRuntime(CoreRuntime.Core31).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3)); } } diff --git a/tests/ImageSharp.Textures.Benchmarks/ImageSharp.Textures.Benchmarks.csproj b/tests/ImageSharp.Textures.Benchmarks/ImageSharp.Textures.Benchmarks.csproj index 528ca0cf..40b23e91 100644 --- a/tests/ImageSharp.Textures.Benchmarks/ImageSharp.Textures.Benchmarks.csproj +++ b/tests/ImageSharp.Textures.Benchmarks/ImageSharp.Textures.Benchmarks.csproj @@ -1,20 +1,22 @@ - + Exe net8.0 + enable false false false - SixLabors.ImageSharp.Textures.Benchmarks SixLabors.ImageSharp.Textures.Benchmarks - - + + + + diff --git a/tests/ImageSharp.Textures.Benchmarks/Program.cs b/tests/ImageSharp.Textures.Benchmarks/Program.cs index 287b56e1..d8ace843 100644 --- a/tests/ImageSharp.Textures.Benchmarks/Program.cs +++ b/tests/ImageSharp.Textures.Benchmarks/Program.cs @@ -4,10 +4,9 @@ using System.Reflection; using BenchmarkDotNet.Running; -namespace SixLabors.ImageSharp.Textures.Benchmarks +namespace SixLabors.ImageSharp.Textures.Benchmarks; + +public class Program { - public class Program - { - public static void Main(string[] args) => new BenchmarkSwitcher(typeof(Program).GetTypeInfo().Assembly).Run(args); - } + public static void Main(string[] args) => new BenchmarkSwitcher(typeof(Program).GetTypeInfo().Assembly).Run(args); } diff --git a/tests/ImageSharp.Textures.InteractiveTest/ImageSharp.Textures.InteractiveTest.csproj b/tests/ImageSharp.Textures.InteractiveTest/ImageSharp.Textures.InteractiveTest.csproj index 08d6c73a..891d5645 100644 --- a/tests/ImageSharp.Textures.InteractiveTest/ImageSharp.Textures.InteractiveTest.csproj +++ b/tests/ImageSharp.Textures.InteractiveTest/ImageSharp.Textures.InteractiveTest.csproj @@ -10,6 +10,8 @@ SixLabors.ImageSharp.Textures.InteractiveTest false + + $(NoWarn);CS8002 diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/AstcDecoderTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/AstcDecoderTests.cs new file mode 100644 index 00000000..1ad62f19 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/AstcDecoderTests.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Textures.Compression.Astc; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; +using SixLabors.ImageSharp.Textures.Compression.Astc.IO; +using SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; + +#nullable enable +public class AstcDecoderTests +{ + [Fact] + public void DecompressImage_WithZeroWidth_ShouldReturnEmpty() + { + byte[] data = new byte[256]; + const int height = 16; + + Span result = AstcDecoder.DecompressImage(data, 0, height, FootprintType.Footprint4x4); + + Assert.Empty(result.ToArray()); + } + + [Fact] + public void DecompressImage_WithZeroHeight_ShouldReturnEmpty() + { + byte[] data = new byte[256]; + const int width = 16; + + Span result = AstcDecoder.DecompressImage(data, width, 0, FootprintType.Footprint4x4); + + Assert.Empty(result.ToArray()); + } + + [Fact] + public void DecompressImage_WithDataSizeNotMultipleOfBlockSize_ShouldReturnEmpty() + { + byte[] data = new byte[256]; + const int width = 16; + const int height = 16; + byte[] invalidData = data.AsSpan(0, data.Length - 1).ToArray(); + + Span result = AstcDecoder.DecompressImage(invalidData, width, height, FootprintType.Footprint4x4); + + Assert.Empty(result.ToArray()); + } + + [Fact] + public void DecompressImage_WithMismatchedBlockCount_ShouldReturnEmpty() + { + byte[] data = new byte[256]; + const int width = 16; + const int height = 16; + byte[] mismatchedData = data.AsSpan(0, data.Length - PhysicalBlock.SizeInBytes).ToArray(); + + Span result = AstcDecoder.DecompressImage(mismatchedData, width, height, FootprintType.Footprint4x4); + + Assert.Empty(result.ToArray()); + } + + [Theory] + [InlineData(TestImages.Astc.Atlas_Small_4x4)] + [InlineData(TestImages.Astc.Atlas_Small_5x5)] + [InlineData(TestImages.Astc.Atlas_Small_6x6)] + [InlineData(TestImages.Astc.Atlas_Small_8x8)] + [InlineData(TestImages.Astc.Checkerboard)] + [InlineData(TestImages.Astc.Checkered_4)] + [InlineData(TestImages.Astc.Checkered_5)] + [InlineData(TestImages.Astc.Checkered_6)] + [InlineData(TestImages.Astc.Checkered_7)] + [InlineData(TestImages.Astc.Checkered_8)] + [InlineData(TestImages.Astc.Checkered_9)] + [InlineData(TestImages.Astc.Checkered_10)] + [InlineData(TestImages.Astc.Checkered_11)] + [InlineData(TestImages.Astc.Checkered_12)] + [InlineData(TestImages.Astc.Footprint_4x4)] + [InlineData(TestImages.Astc.Footprint_5x4)] + [InlineData(TestImages.Astc.Footprint_5x5)] + [InlineData(TestImages.Astc.Footprint_6x5)] + [InlineData(TestImages.Astc.Footprint_6x6)] + [InlineData(TestImages.Astc.Footprint_8x5)] + [InlineData(TestImages.Astc.Footprint_8x6)] + [InlineData(TestImages.Astc.Footprint_8x8)] + [InlineData(TestImages.Astc.Footprint_10x5)] + [InlineData(TestImages.Astc.Footprint_10x6)] + [InlineData(TestImages.Astc.Footprint_10x8)] + [InlineData(TestImages.Astc.Footprint_10x10)] + [InlineData(TestImages.Astc.Footprint_12x10)] + [InlineData(TestImages.Astc.Footprint_12x12)] + [InlineData(TestImages.Astc.Rgb_4x4)] + [InlineData(TestImages.Astc.Rgb_5x4)] + [InlineData(TestImages.Astc.Rgb_6x6)] + [InlineData(TestImages.Astc.Rgb_8x8)] + [InlineData(TestImages.Astc.Rgb_12x12)] + public void DecompressImage_WithTestdataFile_ShouldReturnExpectedByteCount(string inputFile) + { + string filePath = TestFile.GetInputFileFullPath(inputFile); + byte[] bytes = File.ReadAllBytes(filePath); + AstcFile astc = AstcFile.FromMemory(bytes); + + Span result = AstcDecoder.DecompressImage(astc); + + Assert.Equal(astc.Width * astc.Height * 4, result.Length); + } + + [Theory] + [InlineData(TestImages.Astc.Atlas_Small_4x4, FootprintType.Footprint4x4, 256, 256)] + [InlineData(TestImages.Astc.Atlas_Small_5x5, FootprintType.Footprint5x5, 256, 256)] + [InlineData(TestImages.Astc.Atlas_Small_6x6, FootprintType.Footprint6x6, 256, 256)] + [InlineData(TestImages.Astc.Atlas_Small_8x8, FootprintType.Footprint8x8, 256, 256)] + public void DecompressImage_WithValidData_ShouldDecodeAllBlocks( + string inputFile, + FootprintType footprintType, + int width, + int height) + { + byte[] astcData = TestFile.Create(inputFile).Bytes[16..]; + Footprint footprint = Footprint.FromFootprintType(footprintType); + int blockWidth = footprint.Width; + int blockHeight = footprint.Height; + int blocksWide = (width + blockWidth - 1) / blockWidth; + int blocksHigh = (height + blockHeight - 1) / blockHeight; + int expectedBlockCount = blocksWide * blocksHigh; + + // Check ASTC data structure + Assert.Equal(0, astcData.Length % PhysicalBlock.SizeInBytes); + Assert.Equal(expectedBlockCount, astcData.Length / PhysicalBlock.SizeInBytes); + + // Verify all blocks can be unpacked + for (int i = 0; i < astcData.Length; i += PhysicalBlock.SizeInBytes) + { + byte[] block = astcData.AsSpan(i, PhysicalBlock.SizeInBytes).ToArray(); + UInt128 bits = new(BitConverter.ToUInt64(block, 8), BitConverter.ToUInt64(block, 0)); + BlockInfo info = BlockInfo.Decode(bits); + LogicalBlock? logicalBlock = LogicalBlock.UnpackLogicalBlock(footprint, bits, in info); + + Assert.NotNull(logicalBlock); + } + } + + [Theory] + [InlineData(TestImages.Astc.Atlas_Small_4x4, TestImages.Astc.Expected.Atlas_Small_4x4, FootprintType.Footprint4x4, 256, 256)] + [InlineData(TestImages.Astc.Atlas_Small_5x5, TestImages.Astc.Expected.Atlas_Small_5x5, FootprintType.Footprint5x5, 256, 256)] + [InlineData(TestImages.Astc.Atlas_Small_6x6, TestImages.Astc.Expected.Atlas_Small_6x6, FootprintType.Footprint6x6, 256, 256)] + [InlineData(TestImages.Astc.Atlas_Small_8x8, TestImages.Astc.Expected.Atlas_Small_8x8, FootprintType.Footprint8x8, 256, 256)] + public void DecompressImage_WithAstcFile_ShouldMatchExpected( + string inputFile, + string expectedFile, + FootprintType footprint, + int width, + int height) + { + string astcPath = TestFile.GetInputFileFullPath(inputFile); + byte[] astcBytes = File.ReadAllBytes(astcPath); + AstcFile file = AstcFile.FromMemory(astcBytes); + + // Check file header + Assert.Equal(footprint, file.Footprint.Type); + Assert.Equal(width, file.Width); + Assert.Equal(height, file.Height); + + byte[] decodedPixels = AstcDecoder.DecompressImage(file).ToArray(); + using Image actualImage = Image.LoadPixelData(decodedPixels, width, height); + actualImage.Mutate(x => x.Flip(FlipMode.Vertical)); + + string expectedImagePath = TestFile.GetInputFileFullPath(expectedFile); + using Image expectedImage = Image.Load(expectedImagePath); + ImageComparer.TolerantPercentage(0.1f).VerifySimilarity(expectedImage, actualImage); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/BitOperationsTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/BitOperationsTests.cs new file mode 100644 index 00000000..ce542d9a --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/BitOperationsTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; + +public class BitOperationsTests +{ + [Fact] + public void GetBits_UInt128WithLowBits_ShouldExtractCorrectly() + { + UInt128 value = new(0x1234567890ABCDEF, 0xFEDCBA0987654321); + + UInt128 result = BitOperations.GetBits(value, 0, 8); + + Assert.Equal(0x21UL, result.Low()); + } + + [Fact] + public void GetBits_UInt128WithZeroLength_ShouldReturnZero() + { + UInt128 value = new(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF); + + UInt128 result = BitOperations.GetBits(value, 0, 0); + + Assert.Equal(UInt128.Zero, result); + } + + [Fact] + public void GetBits_ULongWithLowBits_ShouldExtractCorrectly() + { + ulong value = 0xFEDCBA0987654321; + + ulong result = BitOperations.GetBits(value, 0, 8); + + Assert.Equal(0x21UL, result); + } + + [Fact] + public void GetBits_ULongWithZeroLength_ShouldReturnZero() + { + ulong value = 0xFFFFFFFFFFFFFFFF; + + ulong result = BitOperations.GetBits(value, 0, 0); + + Assert.Equal(0UL, result); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(10, 20)] + [InlineData(128, 255)] + [InlineData(255, 128)] + [InlineData(64, 64)] + public void TransferPrecision_WithSameInput_ShouldBeDeterministic(int inputA, int inputB) + { + (int a1, int b1) = BitOperations.TransferPrecision(inputA, inputB); + (int a2, int b2) = BitOperations.TransferPrecision(inputA, inputB); + + Assert.Equal(a2, a1); + Assert.Equal(b2, b1); + } + + [Fact] + public void TransferPrecision_WithAllValidByteInputs_ShouldNotThrow() + { + for (int a = byte.MinValue; a <= byte.MaxValue; a++) + { + for (int b = byte.MinValue; b <= byte.MaxValue; b++) + { + BitOperations.TransferPrecision(a, b); + } + } + } + + [Theory] + [InlineData(0, 0)] + [InlineData(5, 10)] + [InlineData(10, 255)] + [InlineData(31, 128)] + [InlineData(-32, 200)] + [InlineData(-1, 100)] + public void TransferPrecisionInverse_WithSameInput_ShouldBeDeterministic(int inputA, int inputB) + { + (int a1, int b1) = BitOperations.TransferPrecisionInverse(inputA, inputB); + (int a2, int b2) = BitOperations.TransferPrecisionInverse(inputA, inputB); + + Assert.Equal(a2, a1); + Assert.Equal(b2, b1); + } + + [Theory] + [InlineData(-33, 128)] // a too small + [InlineData(32, 128)] // a too large + [InlineData(0, -1)] // b too small + [InlineData(0, 256)] // b too large + public void TransferPrecisionInverse_WithInvalidInput_ShouldThrowArgumentOutOfRangeException(int a, int b) + { + Action action = () => BitOperations.TransferPrecisionInverse(a, b); + + Assert.Throws(action); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(10, 20)] + [InlineData(31, 255)] + [InlineData(-32, 128)] + [InlineData(-1, 200)] + public void TransferPrecision_AfterInverse_ShouldReturnOriginalValues(int originalA, int originalB) + { + (int encodedA, int encodedB) = BitOperations.TransferPrecisionInverse(originalA, originalB); + + // Apply regular to decode + (int decodedA, int decodedB) = BitOperations.TransferPrecision(encodedA, encodedB); + + Assert.Equal(originalA, decodedA); + Assert.Equal(originalB, decodedB); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/BitStreamTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/BitStreamTests.cs new file mode 100644 index 00000000..9b4e6115 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/BitStreamTests.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; + +public class BitStreamTests +{ + [Fact] + public void Constructor_WithBitsAndLength_ShouldInitializeCorrectly() + { + BitStream stream = new(0b1010101010101010UL, 32); + + Assert.Equal(32u, stream.Bits); + } + + [Fact] + public void Constructor_WithoutParameters_ShouldInitializeEmpty() + { + BitStream stream = default; + + Assert.Equal(0u, stream.Bits); + } + + [Fact] + public void TryGetBits_WithSingleBitFromZero_ShouldReturnZero() + { + BitStream stream = new(0UL, 1); + + bool success = stream.TryGetBits(1, out uint bits); + + Assert.True(success); + Assert.Equal(0U, bits); + } + + [Fact] + public void TryGetBits_StreamEnd_ShouldReturnFalse() + { + BitStream stream = new(0UL, 1); + stream.TryGetBits(1, out _); + + bool success = stream.TryGetBits(1, out uint _); + + Assert.False(success); + } + + [Fact] + public void TryGetBits_WithAlternatingBitPattern_ShouldExtractCorrectly() + { + BitStream stream = new(0b1010101010101010UL, 32); + + Assert.True(stream.TryGetBits(1, out uint bits1)); + Assert.Equal(0U, bits1); + + Assert.True(stream.TryGetBits(3, out uint bits2)); + Assert.Equal(0b101U, bits2); + + Assert.True(stream.TryGetBits(8, out uint bits3)); + Assert.Equal(0b10101010U, bits3); + + Assert.Equal(20u, stream.Bits); + + Assert.True(stream.TryGetBits(20, out uint bits4)); + Assert.Equal(0b1010U, bits4); + Assert.Equal(0u, stream.Bits); + } + + [Fact] + public void TryGetBits_With64BitsOfOnes_ShouldReturnAllOnes() + { + const ulong allBits = 0xFFFFFFFFFFFFFFFFUL; + BitStream stream = new(allBits, 64); + + // Check initial state + Assert.Equal(64u, stream.Bits); + + bool success = stream.TryGetBits(64, out ulong bits); + + Assert.True(success); + Assert.Equal(allBits, bits); + Assert.Equal(0u, stream.Bits); + } + + [Fact] + public void TryGetBits_With40BitsFromFullBits_ShouldReturnLower40Bits() + { + const ulong allBits = 0xFFFFFFFFFFFFFFFFUL; + const ulong expected40Bits = 0x000000FFFFFFFFFFUL; + BitStream stream = new(allBits, 64); + + // Check initial state + Assert.Equal(64u, stream.Bits); + + bool success = stream.TryGetBits(40, out ulong bits); + + Assert.True(success); + Assert.Equal(expected40Bits, bits); + Assert.Equal(24u, stream.Bits); + } + + [Fact] + public void TryGetBits_WithZeroBits_ShouldReturnZeroAndNotConsume() + { + const ulong allBits = 0xFFFFFFFFFFFFFFFFUL; + const ulong expected40Bits = 0x000000FFFFFFFFFFUL; + BitStream stream = new(allBits, 32); + + Assert.True(stream.TryGetBits(0, out ulong bits1)); + Assert.Equal(0UL, bits1); + + Assert.True(stream.TryGetBits(32, out ulong bits2)); + Assert.Equal(expected40Bits & 0xFFFFFFFFUL, bits2); + + Assert.True(stream.TryGetBits(0, out ulong bits3)); + Assert.Equal(0UL, bits3); + Assert.Equal(0u, stream.Bits); + } + + [Fact] + public void PutBits_WithSmallValues_ShouldAccumulateCorrectly() + { + BitStream stream = default; + + stream.PutBits(0U, 1); + stream.PutBits(0b11U, 2); + + Assert.Equal(3u, stream.Bits); + Assert.True(stream.TryGetBits(3, out uint bits)); + Assert.Equal(0b110U, bits); + } + + [Fact] + public void PutBits_With64BitsOfOnes_ShouldStoreCorrectly() + { + const ulong allBits = 0xFFFFFFFFFFFFFFFFUL; + BitStream stream = default; + + stream.PutBits(allBits, 64); + + Assert.Equal(64u, stream.Bits); + Assert.True(stream.TryGetBits(64, out ulong bits)); + Assert.Equal(allBits, bits); + Assert.Equal(0u, stream.Bits); + } + + [Fact] + public void PutBits_With40BitsOfOnes_ShouldMaskTo40Bits() + { + const ulong allBits = 0xFFFFFFFFFFFFFFFFUL; + const ulong expected40Bits = 0x000000FFFFFFFFFFUL; + BitStream stream = default; + + stream.PutBits(allBits, 40); + + Assert.True(stream.TryGetBits(40, out ulong bits)); + Assert.Equal(expected40Bits, bits); + Assert.Equal(0u, stream.Bits); + } + + [Fact] + public void PutBits_WithZeroBitsInterspersed_ShouldReturnValue() + { + const ulong allBits = 0xFFFFFFFFFFFFFFFFUL; + const ulong expected40Bits = 0x000000FFFFFFFFFFUL; + BitStream stream = default; + + stream.PutBits(0U, 0); + stream.PutBits((uint)(allBits & 0xFFFFFFFFUL), 32); + stream.PutBits(0U, 0); + + Assert.True(stream.TryGetBits(32, out ulong bits)); + Assert.Equal(expected40Bits & 0xFFFFFFFFUL, bits); + Assert.Equal(0u, stream.Bits); + } + + [Fact] + public void PutBits_ThenGetBits_ShouldReturnValue() + { + BitStream stream = default; + const uint value1 = 0b101; + const uint value2 = 0b11001100; + + stream.PutBits(value1, 3); + stream.PutBits(value2, 8); + + Assert.True(stream.TryGetBits(3, out uint retrieved1)); + Assert.Equal(value1, retrieved1); + Assert.True(stream.TryGetBits(8, out uint retrieved2)); + Assert.Equal(value2, retrieved2); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs new file mode 100644 index 00000000..f5d154d3 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs @@ -0,0 +1,384 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.PixelFormats; +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.Tests.Formats.Astc; + +public class EndpointCodecTests +{ + [Theory] + [InlineData(EndpointEncodingMode.DirectLuma)] + [InlineData(EndpointEncodingMode.DirectLumaAlpha)] + [InlineData(EndpointEncodingMode.BaseScaleRgb)] + [InlineData(EndpointEncodingMode.BaseScaleRgba)] + [InlineData(EndpointEncodingMode.DirectRbg)] + [InlineData(EndpointEncodingMode.DirectRgba)] + internal void EncodeColorsForMode_WithVariousRanges_ShouldProduceValidQuantizedValues(EndpointEncodingMode mode) + { + Rgba32 low = new(0, 0, 0, 0); + Rgba32 high = new(255, 255, 255, 255); + + for (int quantRange = 5; quantRange < 256; quantRange++) + { + List values = []; + EndpointEncoder.EncodeColorsForMode(low, high, quantRange, mode, out ColorEndpointMode _, values); + + // Assert value count matches expected + Assert.Equal(mode.GetValuesCount(), values.Count); + + // Assert all values are within quantization range + Assert.All(values, v => Assert.InRange(v, 0, quantRange)); + } + } + + [Theory] + [InlineData(EndpointEncodingMode.DirectLuma)] + [InlineData(EndpointEncodingMode.DirectLumaAlpha)] + [InlineData(EndpointEncodingMode.BaseScaleRgb)] + [InlineData(EndpointEncodingMode.BaseScaleRgba)] + [InlineData(EndpointEncodingMode.DirectRbg)] + [InlineData(EndpointEncodingMode.DirectRgba)] + internal void EncodeDecodeColors_WithBlackAndWhite_ShouldPreserveColors(EndpointEncodingMode mode) + { + Rgba32 white = new(255, 255, 255, 255); + Rgba32 black = new(0, 0, 0, 255); + + for (int quantRange = 5; quantRange < 256; ++quantRange) + { + (Rgba32 low, Rgba32 high) = EncodeAndDecodeColors(white, black, quantRange, mode); + + Assert.True(low == white); + Assert.True(high == black); + } + } + + [Fact] + public void UsesBlueContract_WithDirectModes_ShouldDetectCorrectly() + { + List values = [132, 127, 116, 112, 183, 180, 31, 22]; + + Assert.True(EndpointEncoder.UsesBlueContract(255, ColorEndpointMode.LdrRgbDirect, values)); + Assert.True(EndpointEncoder.UsesBlueContract(255, ColorEndpointMode.LdrRgbaDirect, values)); + } + + [Fact] + public void UsesBlueContract_WithOffsetModes_ShouldDetectBasedOnBitFlags() + { + List baseValues = [132, 127, 116, 112, 183, 180, 31, 22]; + + List valuesClearedBit6 = [.. baseValues]; + valuesClearedBit6[1] &= 0xBF; + valuesClearedBit6[3] &= 0xBF; + valuesClearedBit6[5] &= 0xBF; + valuesClearedBit6[7] &= 0xBF; + + Assert.False(EndpointEncoder.UsesBlueContract(255, ColorEndpointMode.LdrRgbBaseOffset, valuesClearedBit6)); + Assert.False(EndpointEncoder.UsesBlueContract(255, ColorEndpointMode.LdrRgbaBaseOffset, valuesClearedBit6)); + + List valuesSetBit6 = [.. baseValues]; + valuesSetBit6[1] |= 0x40; + valuesSetBit6[3] |= 0x40; + valuesSetBit6[5] |= 0x40; + valuesSetBit6[7] |= 0x40; + + Assert.True(EndpointEncoder.UsesBlueContract(255, ColorEndpointMode.LdrRgbBaseOffset, valuesSetBit6)); + Assert.True(EndpointEncoder.UsesBlueContract(255, ColorEndpointMode.LdrRgbaBaseOffset, valuesSetBit6)); + } + + [Fact] + public void EncodeColorsForMode_WithRgbDirectAndSpecificPairs_ShouldUseBlueContract() + { + (Rgba32, Rgba32)[] pairs = + [ + (new Rgba32(22, 18, 30, 59), new Rgba32(162, 148, 155, 59)), + (new Rgba32(22, 30, 27, 36), new Rgba32(228, 221, 207, 36)), + (new Rgba32(54, 60, 55, 255), new Rgba32(23, 30, 27, 255)) + ]; + + const int endpointRange = 31; + + foreach ((Rgba32 low, Rgba32 high) in pairs) + { + List values = []; + EndpointEncoder.EncodeColorsForMode(low, high, endpointRange, EndpointEncodingMode.DirectRbg, out ColorEndpointMode astcMode, values); + + Assert.True(EndpointEncoder.UsesBlueContract(endpointRange, astcMode, values)); + } + } + + [Fact] + public void EncodeDecodeColors_WithLumaDirect_ShouldProduceLumaValues() + { + EndpointEncodingMode mode = EndpointEncodingMode.DirectLuma; + + (Rgba32 low, Rgba32 high) = EncodeAndDecodeColors( + new Rgba32(247, 248, 246, 255), + new Rgba32(2, 3, 1, 255), + 255, + mode); + + Assert.True(low == new Rgba32(247, 247, 247, 255)); + Assert.True(high == new Rgba32(2, 2, 2, 255)); + + (Rgba32 low2, Rgba32 high2) = EncodeAndDecodeColors( + new Rgba32(80, 80, 50, 255), + new Rgba32(99, 255, 6, 255), + 255, + mode); + + Assert.True(low2 == new Rgba32(70, 70, 70, 255)); + Assert.True(high2 == new Rgba32(120, 120, 120, 255)); + + (Rgba32 low3, Rgba32 high3) = EncodeAndDecodeColors( + new Rgba32(247, 248, 246, 255), + new Rgba32(2, 3, 1, 255), + 15, + mode); + + Assert.True(low3 == new Rgba32(255, 255, 255, 255)); + Assert.True(high3 == new Rgba32(0, 0, 0, 255)); + + (Rgba32 low4, Rgba32 high4) = EncodeAndDecodeColors( + new Rgba32(64, 127, 192, 255), + new Rgba32(0, 0, 0, 255), + 63, + mode); + + Assert.True(low4 == new Rgba32(130, 130, 130, 255)); + Assert.True(high4 == new Rgba32(0, 0, 0, 255)); + } + + [Fact] + public void EncodeDecodeColors_WithLumaAlphaDirect_ShouldPreserveLumaAndAlpha() + { + EndpointEncodingMode mode = EndpointEncodingMode.DirectLumaAlpha; + + // Grey with varying alpha + (Rgba32 low, Rgba32 high) = EncodeAndDecodeColors( + new Rgba32(64, 127, 192, 127), + new Rgba32(0, 0, 0, 20), + 63, + mode); + + Assert.True((low == new Rgba32(130, 130, 130, 125)) || + low.IsCloseTo(new Rgba32(130, 130, 130, 125), 1)); + Assert.True((high == new Rgba32(0, 0, 0, 20)) || + high.IsCloseTo(new Rgba32(0, 0, 0, 20), 1)); + + // Different alpha values + (Rgba32 low2, Rgba32 high2) = EncodeAndDecodeColors( + new Rgba32(247, 248, 246, 250), + new Rgba32(2, 3, 1, 172), + 255, + mode); + + Assert.True(low2 == new Rgba32(247, 247, 247, 250)); + Assert.True(high2 == new Rgba32(2, 2, 2, 172)); + } + + [Fact] + public void EncodeDecodeColors_WithRgbDirectAndRandomColors_ShouldPreserveColors() + { + EndpointEncodingMode mode = EndpointEncodingMode.DirectRbg; + Random random = new(unchecked((int)0xdeadbeef)); + + for (int i = 0; i < 100; ++i) + { + Rgba32 low = new((byte)random.Next(0, 256), (byte)random.Next(0, 256), (byte)random.Next(0, 256), 255); + Rgba32 high = new((byte)random.Next(0, 256), (byte)random.Next(0, 256), (byte)random.Next(0, 256), 255); + (Rgba32 low1, Rgba32 high1) = EncodeAndDecodeColors(low, high, 255, mode); + + Assert.True(low1 == low); + Assert.True(high1 == high); + } + } + + [Fact] + public void EncodeDecodeColors_WithRgbDirectAndSpecificColors_ShouldMatchExpected() + { + EndpointEncodingMode mode = EndpointEncodingMode.DirectRbg; + + (Rgba32 low, Rgba32 high) = EncodeAndDecodeColors( + new Rgba32(64, 127, 192, 255), + new Rgba32(0, 0, 0, 255), + 63, + mode); + + Assert.True(low == new Rgba32(65, 125, 190, 255)); + Assert.True(high == new Rgba32(0, 0, 0, 255)); + + (Rgba32 low2, Rgba32 high2) = EncodeAndDecodeColors( + new Rgba32(0, 0, 0, 255), + new Rgba32(64, 127, 192, 255), + 63, + mode); + + Assert.True(low2 == new Rgba32(0, 0, 0, 255)); + Assert.True(high2 == new Rgba32(65, 125, 190, 255)); + } + + [Fact] + public void EncodeDecodeColors_WithRgbBaseScaleAndIdenticalColors_ShouldBeCloseToOriginal() + { + EndpointEncodingMode mode = EndpointEncodingMode.BaseScaleRgb; + Random random = new(unchecked((int)0xdeadbeef)); + + for (int i = 0; i < 100; ++i) + { + Rgba32 color = new((byte)random.Next(0, 256), (byte)random.Next(0, 256), (byte)random.Next(0, 256), 255); + (Rgba32 low, Rgba32 high) = EncodeAndDecodeColors(color, color, 255, mode); + + Assert.True(low.IsCloseTo(color, 1)); + Assert.True(high.IsCloseTo(color, 1)); + } + } + + [Fact] + public void EncodeDecodeColors_WithRgbBaseScaleAndDifferentColors_ShouldMatchExpected() + { + EndpointEncodingMode mode = EndpointEncodingMode.BaseScaleRgb; + Rgba32 low = new(20, 4, 40, 255); + Rgba32 high = new(80, 16, 160, 255); + + (Rgba32 decodedLow, Rgba32 decodedHigh) = EncodeAndDecodeColors(low, high, 255, mode); + Assert.True(decodedLow.IsCloseTo(low, 0)); + Assert.True(decodedHigh.IsCloseTo(high, 0)); + + (Rgba32 low2, Rgba32 high2) = EncodeAndDecodeColors(low, high, 127, mode); + Assert.True(low2.IsCloseTo(low, 1)); + Assert.True(high2.IsCloseTo(high, 1)); + } + + internal static TheoryData RgbBaseOffsetColorPairs() => new() + { + { new Rgba32(80, 16, 112, 255), new Rgba32(87, 18, 132, 255) }, + { new Rgba32(80, 74, 82, 255), new Rgba32(90, 92, 110, 255) }, + { new Rgba32(0, 0, 0, 255), new Rgba32(2, 2, 2, 255) }, + }; + + [Theory] +#pragma warning disable xUnit1016 // MemberData is internal because Rgba32Extensions are internal + [MemberData(nameof(RgbBaseOffsetColorPairs))] +#pragma warning restore xUnit1016 + internal void DecodeColorsForMode_WithRgbBaseOffset_AndSpecificColorPairs_ShouldDecodeCorrectly( + Rgba32 expectedLow, Rgba32 expectedHigh) + { + int[] values = EncodeRgbBaseOffset(expectedLow, expectedHigh); + (Rgba32 decLow, Rgba32 decHigh) = EndpointCodec.DecodeColorsForMode(values, 255, ColorEndpointMode.LdrRgbBaseOffset); + + Assert.True(decLow == expectedLow); + Assert.True(decHigh == expectedHigh); + } + + [Fact] + public void DecodeColorsForMode_WithRgbBaseOffset_AndIdenticalColors_ShouldDecodeCorrectly() + { + Random random = new(unchecked((int)0xdeadbeef)); + + for (int i = 0; i < 100; ++i) + { + int r = random.Next(0, 256); + int g = random.Next(0, 256); + int b = random.Next(0, 256); + + // Ensure even channels (reference test skips odd) + if (((r | g | b) & 1) != 0) + { + continue; + } + + Rgba32 color = new((byte)r, (byte)g, (byte)b, 255); + int[] values = EncodeRgbBaseOffset(color, color); + (Rgba32 decLow, Rgba32 decHigh) = EndpointCodec.DecodeColorsForMode(values, 255, ColorEndpointMode.LdrRgbBaseOffset); + + Assert.True(decLow == color); + Assert.True(decHigh == color); + } + } + + private static int[] EncodeRgbBaseOffset(Rgba32 low, Rgba32 high) + { + List values = []; + for (int i = 0; i < 3; ++i) + { + bool isLarge = low.GetChannel(i) >= 128; + values.Add((low.GetChannel(i) * 2) & 0xFF); + int diff = (high.GetChannel(i) - low.GetChannel(i)) * 2; + if (isLarge) + { + diff |= 0x80; + } + + values.Add(diff); + } + + return [.. values]; + } + + [Fact] + public void DecodeCheckerboard_ShouldDecodeToGrayscaleEndpoints() + { + string astcFilePath = TestFile.GetInputFileFullPath(TestImages.Astc.Checkerboard); + byte[] astcData = File.ReadAllBytes(astcFilePath); + + int blocksDecoded = 0; + + for (int i = 0; i < astcData.Length; i += PhysicalBlock.SizeInBytes) + { + // Read block bytes + UInt128 blockData = BinaryPrimitives.ReadUInt128LittleEndian(astcData.AsSpan(i, PhysicalBlock.SizeInBytes)); + PhysicalBlock physicalBlock = PhysicalBlock.Create(blockData); + + // Unpack to intermediate block + IntermediateBlock.IntermediateBlockData? intermediateBlock = IntermediateBlock.UnpackIntermediateBlock(physicalBlock); + Assert.NotNull(intermediateBlock); + IntermediateBlock.IntermediateBlockData ib = intermediateBlock!.Value; + + // Verify endpoints exist + Assert.True(ib.EndpointCount > 0, "block should have endpoints"); + + int colorRange = IntermediateBlock.EndpointRangeForBlock(ib); + Assert.True(colorRange > 0, "color range should be valid"); + + // Check all endpoint pairs decode successfully to grayscale colors + for (int ep = 0; ep < ib.EndpointCount; ep++) + { + IntermediateBlock.IntermediateEndpointData endpoints = ib.Endpoints[ep]; + ReadOnlySpan colorSpan = ((ReadOnlySpan)endpoints.Colors)[..endpoints.ColorCount]; + (Rgba32 low, Rgba32 high) = EndpointCodec.DecodeColorsForMode( + colorSpan, + colorRange, + endpoints.Mode); + + // Assert - Checkerboard should produce grayscale colors (R == G == B) + Assert.True(low.R == low.G, $"block {i} low endpoint should be grayscale"); + Assert.True(low.G == low.B, $"block {i} low endpoint should be grayscale"); + Assert.True(high.R == high.G, $"block {i} high endpoint should be grayscale"); + Assert.True(high.G == high.B, $"block {i} high endpoint should be grayscale"); + } + + blocksDecoded++; + } + + // Verify we decoded a reasonable number of blocks + Assert.True(blocksDecoded > 0); + } + + private static (Rgba32 Low, Rgba32 High) EncodeAndDecodeColors( + Rgba32 low, + Rgba32 high, + int quantRange, + EndpointEncodingMode mode) + { + List values = []; + bool needsSwap = EndpointEncoder.EncodeColorsForMode(low, high, quantRange, mode, out ColorEndpointMode astcMode, values); + (Rgba32 decLow, Rgba32 decHigh) = EndpointCodec.DecodeColorsForMode(values.ToArray(), quantRange, astcMode); + + return needsSwap ? (decHigh, decLow) : (decLow, decHigh); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/FootprintTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/FootprintTests.cs new file mode 100644 index 00000000..f13db289 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/FootprintTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; + +public class FootprintTests +{ + [Theory] + [InlineData(FootprintType.Footprint4x4, 4, 4)] + [InlineData(FootprintType.Footprint5x4, 5, 4)] + [InlineData(FootprintType.Footprint5x5, 5, 5)] + [InlineData(FootprintType.Footprint6x5, 6, 5)] + [InlineData(FootprintType.Footprint6x6, 6, 6)] + [InlineData(FootprintType.Footprint8x5, 8, 5)] + [InlineData(FootprintType.Footprint8x6, 8, 6)] + [InlineData(FootprintType.Footprint8x8, 8, 8)] + [InlineData(FootprintType.Footprint10x5, 10, 5)] + [InlineData(FootprintType.Footprint10x6, 10, 6)] + [InlineData(FootprintType.Footprint10x8, 10, 8)] + [InlineData(FootprintType.Footprint10x10, 10, 10)] + [InlineData(FootprintType.Footprint12x10, 12, 10)] + [InlineData(FootprintType.Footprint12x12, 12, 12)] + public void FromFootprintType_WithValidType_ShouldReturnCorrectDimensions( + FootprintType type, int expectedWidth, int expectedHeight) + { + Footprint footprint = Footprint.FromFootprintType(type); + + Assert.Equal(type, footprint.Type); + Assert.Equal(expectedWidth, footprint.Width); + Assert.Equal(expectedHeight, footprint.Height); + Assert.Equal(expectedWidth * expectedHeight, footprint.PixelCount); + } + + [Fact] + public void FromFootprintType_WithAllValidTypes_ShouldReturnUniqueFootprints() + { + FootprintType[] allTypes = + [ + FootprintType.Footprint4x4, FootprintType.Footprint5x4, FootprintType.Footprint5x5, + FootprintType.Footprint6x5, FootprintType.Footprint6x6, FootprintType.Footprint8x5, + FootprintType.Footprint8x6, FootprintType.Footprint8x8, FootprintType.Footprint10x5, + FootprintType.Footprint10x6, FootprintType.Footprint10x8, FootprintType.Footprint10x10, + FootprintType.Footprint12x10, FootprintType.Footprint12x12 + ]; + + List footprints = [.. allTypes.Select(Footprint.FromFootprintType)]; + + Assert.Equal(allTypes.Length, footprints.Count); + Assert.Equal(footprints.Count, footprints.Distinct().Count()); + } + + [Fact] + public void Footprint_PixelCount_ShouldEqualWidthTimesHeight() + { + Footprint footprint = Footprint.FromFootprintType(FootprintType.Footprint10x8); + + Assert.Equal(footprint.Width * footprint.Height, footprint.PixelCount); + Assert.Equal(80, footprint.PixelCount); + } + + [Fact] + public void Footprint_ValueEquality_WithSameType_ShouldBeEqual() + { + Footprint footprint1 = Footprint.FromFootprintType(FootprintType.Footprint6x6); + Footprint footprint2 = Footprint.FromFootprintType(FootprintType.Footprint6x6); + + Assert.Equal(footprint2, footprint1); + Assert.True(footprint1 == footprint2); + } + + [Fact] + public void Footprint_ValueEquality_WithDifferentType_ShouldNotBeEqual() + { + Footprint footprint1 = Footprint.FromFootprintType(FootprintType.Footprint6x6); + Footprint footprint2 = Footprint.FromFootprintType(FootprintType.Footprint8x8); + + Assert.NotEqual(footprint2, footprint1); + Assert.True(footprint1 != footprint2); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrComparisonTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrComparisonTests.cs new file mode 100644 index 00000000..062c9bbf --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrComparisonTests.cs @@ -0,0 +1,211 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Textures.Compression.Astc; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; +using SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc.HDR; + +/// +/// Comparing HDR and LDR ASTC decoding behavior using real reference files. +/// +public class HdrComparisonTests +{ + [Fact] + public void HdrFile_DecodedWithHdrApi_ShouldPreserveExtendedRange() + { + // HDR files should decode to values potentially exceeding 1.0 + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + // Decode with HDR API + Span hdrResult = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); + + // Verify we get Float16 output + Assert.Equal(4, hdrResult.Length); // 1 pixel, 4 channels + + // HDR content can have values > 1.0 (this file may or may not, but should allow it) + foreach (float value in hdrResult) + { + Assert.False(float.IsNaN(value)); + Assert.False(float.IsInfinity(value)); + Assert.True(value >= 0.0f); + } + } + + [Fact] + public void LdrFile_DecodedWithHdrApi_ShouldUpscaleToHdrRange() + { + // LDR files decoded with HDR API should produce values in 0.0-1.0 range + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Ldr_A_1x1); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + // Decode with HDR API + Span hdrResult = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); + + Assert.Equal(4, hdrResult.Length); + + // LDR content should map to 0.0-1.0 range when decoded with HDR API + foreach (float value in hdrResult) + { + Assert.True(value >= 0.0f); + Assert.True(value <= 1.0f); + } + } + + [Fact] + public void HdrFile_DecodedWithLdrApi_ShouldClampToByteRange() + { + // HDR files decoded with LDR API should clamp to 0-255 + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + // Decode with LDR API + Span ldrResult = AstcDecoder.DecompressImage(astcFile); + + Assert.Equal(4, ldrResult.Length); + + // All values must be in LDR range + foreach (byte value in ldrResult) + { + Assert.True(value >= 0); + Assert.True(value <= 255); + } + } + + [Fact] + public void LdrFile_DecodedWithBothApis_ShouldProduceConsistentValues() + { + // LDR content should produce equivalent results with both APIs + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Ldr_A_1x1); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + // Decode with both APIs + Span ldrResult = AstcDecoder.DecompressImage(astcFile); + Span hdrResult = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); + + // Compare results - LDR byte should map to HDR float / 255.0 + for (int i = 0; i < 4; i++) + { + byte ldrValue = ldrResult[i]; + float hdrValue = hdrResult[i]; + + float expectedHdr = ldrValue / 255.0f; + + Assert.True(Math.Abs(hdrValue - expectedHdr) < 0.01f); + } + } + + [Fact] + public void HdrTile_ShouldDecodeSuccessfully() + { + // Test larger HDR tile decoding + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_Tile); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + Span hdrResult = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); + + // Should produce Width * Height * 4 values + Assert.Equal(astcFile.Width * astcFile.Height * 4, hdrResult.Length); + + foreach (float value in hdrResult) + { + Assert.False(float.IsNaN(value)); + Assert.False(float.IsInfinity(value)); + } + } + + [Fact] + public void LdrTile_ShouldDecodeSuccessfully() + { + // Test larger LDR tile decoding + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Ldr_Tile); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + // Decode with both APIs + Span ldrResult = AstcDecoder.DecompressImage(astcFile); + Span hdrResult = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); + + // Both should produce correct output sizes + Assert.Equal(astcFile.Width * astcFile.Height * 4, ldrResult.Length); + Assert.Equal(astcFile.Width * astcFile.Height * 4, hdrResult.Length); + } + + [Fact] + public void SameFootprint_HdrVsLdr_ShouldBothDecode() + { + // Verify files with same footprint decode correctly + string hdrPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); + string ldrPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Ldr_A_1x1); + + byte[] hdrData = File.ReadAllBytes(hdrPath); + byte[] ldrData = File.ReadAllBytes(ldrPath); + + AstcFile hdrFile = AstcFile.FromMemory(hdrData); + AstcFile ldrFile = AstcFile.FromMemory(ldrData); + + // Both are 1x1 with 6x6 footprint + Assert.Equal(ldrFile.Width, hdrFile.Width); + Assert.Equal(ldrFile.Height, hdrFile.Height); + Assert.Equal(ldrFile.Footprint.Width, hdrFile.Footprint.Width); + Assert.Equal(ldrFile.Footprint.Height, hdrFile.Footprint.Height); + + // Both should decode successfully with HDR API + Span hdrDecoded = AstcDecoder.DecompressHdrImage( + hdrFile.Blocks, hdrFile.Width, hdrFile.Height, hdrFile.Footprint); + Span ldrDecoded = AstcDecoder.DecompressHdrImage( + ldrFile.Blocks, ldrFile.Width, ldrFile.Height, ldrFile.Footprint); + + Assert.Equal(4, hdrDecoded.Length); + Assert.Equal(4, ldrDecoded.Length); + } + + [Fact] + public void HdrColor_FromLdr_ShouldMatchLdrToHdrApiConversion() + { + // Verify that HdrColor.FromRgba() produces same results as decoding LDR with HDR API + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Ldr_A_1x1); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + // Decode with LDR API to get byte values + Span ldrBytes = AstcDecoder.DecompressImage(astcFile); + + // Convert LDR bytes to HDR using extension method + Rgba32 ldrColor = new(ldrBytes[0], ldrBytes[1], ldrBytes[2], ldrBytes[3]); + Rgba64 hdrFromLdr = new(ldrColor); + + // Decode with HDR API + Span hdrDirect = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); + + // Compare: UNORM16 normalized values should match HDR API output + for (int i = 0; i < 4; i++) + { + float fromConversion = hdrFromLdr.GetChannel(i) / 65535.0f; + float fromDirect = hdrDirect[i]; + + Assert.True(Math.Abs(fromConversion - fromDirect) < 0.0001f); + } + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrDecoderTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrDecoderTests.cs new file mode 100644 index 00000000..12e85350 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrDecoderTests.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Textures.Compression.Astc; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc.HDR; + +public class HdrDecoderTests +{ + [Fact] + public void DecompressToFloat16_WithValidBlock_ShouldProduceCorrectOutputSize() + { + // Create a simple 4x4 block (16 bytes) + byte[] astcData = new byte[16]; + + Footprint footprint = Footprint.FromFootprintType(FootprintType.Footprint4x4); + + // Decompress using HDR API + Span hdrResult = AstcDecoder.DecompressHdrImage(astcData, 4, 4, footprint); + + // Verify output size: 4x4 pixels, 4 Half values (RGBA) per pixel + Assert.Equal(4 * 4 * 4, hdrResult.Length); // 64 Half values total + + foreach (float value in hdrResult) + { + Assert.False(float.IsNaN(value)); + Assert.False(float.IsInfinity(value)); + + // Values should be in reasonable range for normalized colors + Assert.True(value >= 0.0f); + Assert.True(value <= 1.1f); // Allow slight overshoot for HDR + } + } + + [Fact] + public void DecompressToFloat16_WithDifferentFootprints_ShouldWork() + { + // Test that HDR API works with various footprint types + FootprintType[] footprints = + [ + FootprintType.Footprint4x4, + FootprintType.Footprint5x5, + FootprintType.Footprint6x6, + FootprintType.Footprint8x8 + ]; + + foreach (FootprintType footprint in footprints) + { + // Create a simple test: 1 block (footprint size) of zeros + Footprint fp = Footprint.FromFootprintType(footprint); + byte[] astcData = new byte[16]; // One ASTC block (all zeros = void extent block) + + Span result = AstcDecoder.DecompressHdrImage(astcData, fp.Width, fp.Height, footprint); + + // Should produce footprint.Width * footprint.Height pixels, each with 4 Half values + Assert.Equal(fp.Width * fp.Height * 4, result.Length); + } + } + + [Fact] + public void ASTCDecompressToFloat16_WithInvalidData_ShouldReturnEmpty() + { + byte[] emptyData = []; + + Span result = AstcDecoder.DecompressHdrImage(emptyData, 64, 64, FootprintType.Footprint4x4); + + Assert.Equal(0, result.Length); + } + + [Fact] + public void DecompressToFloat16_WithZeroDimensions_ShouldReturnEmpty() + { + byte[] astcData = new byte[16]; + Footprint footprint = Footprint.FromFootprintType(FootprintType.Footprint4x4); + + Span result = AstcDecoder.DecompressHdrImage(astcData, 0, 0, footprint); + + Assert.Equal(0, result.Length); + } + + [Fact] + public void HdrColor_LdrRoundTrip_ShouldPreserveValues() + { + Rgba32 ldrColor = new(50, 100, 150, 200); + + Rgba64 hdrColor = new(ldrColor); + Rgba32 backToLdr = hdrColor.ToRgba32(); + + Assert.Equal(ldrColor.R, backToLdr.R); + Assert.Equal(ldrColor.G, backToLdr.G); + Assert.Equal(ldrColor.B, backToLdr.B); + Assert.Equal(ldrColor.A, backToLdr.A); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrImageTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrImageTests.cs new file mode 100644 index 00000000..da88207b --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrImageTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.ComponentModel; +using SixLabors.ImageSharp.Textures.Compression.Astc; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; +using SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc.HDR; + +/// +/// Tests using real HDR ASTC files from the ARM astc-encoder reference repository. +/// These tests validate that our HDR implementation produces valid output for +/// actual HDR-compressed ASTC data. +/// +public class HdrImageTests +{ + [Fact] + [Description("Verify that the ASTC file header is correctly parsed for HDR content, including footprint detection")] + public void DecodeHdrFile_VerifyFootprintDetection() + { + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + // The HDR-A-1x1.astc file has a 6x6 footprint based on the header + Assert.Equal(6, astcFile.Footprint.Width); + Assert.Equal(6, astcFile.Footprint.Height); + Assert.Equal(FootprintType.Footprint6x6, astcFile.Footprint.Type); + } + + [Fact] + public void DecodeHdrAstcFile_1x1Pixel_ShouldProduceValidHdrOutput() + { + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + Span hdrResult = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, + astcFile.Width, + astcFile.Height, + astcFile.Footprint); + + // Should produce 1 pixel with 4 values (RGBA) + Assert.Equal(4, hdrResult.Length); + + // HDR values can exceed 1.0 + // Just verify they're in a reasonable range (0.0 to 10.0) + foreach (float value in hdrResult) + { + Assert.True(value >= 0.0f); + Assert.True(value < 10.0f); + } + } + + [Fact] + public void DecodeHdrAstcFile_Tile_ShouldProduceValidHdrOutput() + { + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_Tile); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + Span hdrResult = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, + astcFile.Width, + astcFile.Height, + astcFile.Footprint); + + // Should produce Width * Height pixels, each with 4 values + Assert.Equal(astcFile.Width * astcFile.Height * 4, hdrResult.Length); + + // Verify at least some HDR values exceed 1.0 (typical for HDR content) + int valuesGreaterThanOne = 0; + foreach (float v in hdrResult) + { + if (v > 1.0f) + { + valuesGreaterThanOne++; + } + } + + Assert.Equal(64, valuesGreaterThanOne); + } + + [Fact] + [Description("Verify that HDR ASTC files can be decoded with the LDR API, producing clamped values")] + public void DecodeHdrAstcFile_WithLdrApi_ShouldClampValues() + { + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); + + if (!File.Exists(astcPath)) + { + return; + } + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + // Decode using LDR API + Span ldrResult = AstcDecoder.DecompressImage(astcFile); + + // Should produce 1 pixel with 4 bytes (RGBA) + Assert.Equal(4, ldrResult.Length); + + // All values should be in LDR range + foreach (byte value in ldrResult) + { + Assert.True(value >= byte.MinValue); + Assert.True(value <= byte.MaxValue); + } + } + + [Fact] + [Description("Verify that HDR and LDR APIs produce consistent relative channel values for the same HDR ASTC file")] + public void HdrAndLdrApis_OnSameHdrFile_ShouldProduceConsistentRelativeValues() + { + string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); + + byte[] astcData = File.ReadAllBytes(astcPath); + AstcFile astcFile = AstcFile.FromMemory(astcData); + + // Decode with both APIs + Span hdrResult = AstcDecoder.DecompressHdrImage( + astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); + Span ldrResult = AstcDecoder.DecompressImage(astcFile); + + // Both should produce output for 1 pixel + Assert.Equal(4, hdrResult.Length); + Assert.Equal(4, ldrResult.Length); + + // The relative ordering of RGB channels should be consistent between APIs. + // If HDR channel i > channel j, then LDR channel i should be >= channel j + // (accounting for clamping at 255). + for (int i = 0; i < 3; i++) + { + for (int j = i + 1; j < 3; j++) + { + if (hdrResult[i] > hdrResult[j]) + { + Assert.True(ldrResult[i] >= ldrResult[j]); + } + else if (hdrResult[i] < hdrResult[j]) + { + Assert.True(ldrResult[i] <= ldrResult[j]); + } + } + } + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/Rgba64ExtensionsTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/Rgba64ExtensionsTests.cs new file mode 100644 index 00000000..14da3cbf --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/Rgba64ExtensionsTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc.HDR; + +public class Rgba64ExtensionsTests +{ + [Fact] + public void GetChannel_WithValidIndices_ShouldReturnCorrectChannels() + { + Rgba64 color = new(1000, 2000, 3000, 4000); + + Assert.Equal(1000, color.GetChannel(0)); + Assert.Equal(2000, color.GetChannel(1)); + Assert.Equal(3000, color.GetChannel(2)); + Assert.Equal(4000, color.GetChannel(3)); + } + + [Fact] + public void GetChannel_WithInvalidIndex_ShouldThrowException() + { + Rgba64 color = new(1000, 2000, 3000, 4000); + + void Act() => _ = color.GetChannel(4); + + Assert.Throws(Act); + } + + [Fact] + public void FromLdr_WithMinMaxValues_ShouldScaleCorrectly() + { + Rgba32 ldrColor = new(0, 127, 255, 200); + + Rgba64 hdrColor = new(ldrColor); + + Assert.Equal(0, hdrColor.R); // 0 * 257 = 0 + Assert.Equal(32639, hdrColor.G); // 127 * 257 = 32639 + Assert.Equal(65535, hdrColor.B); // 255 * 257 = 65535 + Assert.Equal(51400, hdrColor.A); // 200 * 257 = 51400 + } + + [Fact] + public void IsCloseTo_WithSimilarColors_ShouldReturnTrue() + { + Rgba64 color1 = new(1000, 2000, 3000, 4000); + Rgba64 color2 = new(1005, 1995, 3002, 3998); + + bool result = color1.IsCloseTo(color2, 10); + + Assert.True(result); + } + + [Fact] + public void IsCloseTo_WithDifferentColors_ShouldReturnFalse() + { + Rgba64 color1 = new(1000, 2000, 3000, 4000); + Rgba64 color2 = new(1020, 2000, 3000, 4000); + + bool result = color1.IsCloseTo(color2, 10); + + Assert.False(result); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/IntegerSequenceCodecTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/IntegerSequenceCodecTests.cs new file mode 100644 index 00000000..a23b0303 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/IntegerSequenceCodecTests.cs @@ -0,0 +1,333 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.ComponentModel; +using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.IO; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; + +public class IntegerSequenceCodecTests +{ + [Fact] + [Description("1 to 31 are the densest packing of valid encodings and those supported by the codec.")] + public void GetPackingModeBitCount_ForValidRange_ShouldNotReturnUnknownMode() + { + for (int i = 1; i < 32; ++i) + { + (BiseEncodingMode mode, int _) = BoundedIntegerSequenceCodec.GetPackingModeBitCount(i); + Assert.True(mode != BiseEncodingMode.Unknown, $"Range {i} should not yield Unknown encoding mode"); + } + } + + [Fact] + public void GetPackingModeBitCount_ForValidRange_ShouldMatchExpectedValues() + { + (BiseEncodingMode Mode, int BitCount)[] expected = + [ + (BiseEncodingMode.BitEncoding, 1), // Range 1 + (BiseEncodingMode.TritEncoding, 0), // Range 2 + (BiseEncodingMode.BitEncoding, 2), // Range 3 + (BiseEncodingMode.QuintEncoding, 0), // Range 4 + (BiseEncodingMode.TritEncoding, 1), // Range 5 + (BiseEncodingMode.BitEncoding, 3), // Range 6 + (BiseEncodingMode.BitEncoding, 3), // Range 7 + (BiseEncodingMode.QuintEncoding, 1), // Range 8 + (BiseEncodingMode.QuintEncoding, 1), // Range 9 + (BiseEncodingMode.TritEncoding, 2), // Range 10 + (BiseEncodingMode.TritEncoding, 2), // Range 11 + (BiseEncodingMode.BitEncoding, 4), // Range 12 + (BiseEncodingMode.BitEncoding, 4), // Range 13 + (BiseEncodingMode.BitEncoding, 4), // Range 14 + (BiseEncodingMode.BitEncoding, 4), // Range 15 + (BiseEncodingMode.QuintEncoding, 2), // Range 16 + (BiseEncodingMode.QuintEncoding, 2), // Range 17 + (BiseEncodingMode.QuintEncoding, 2), // Range 18 + (BiseEncodingMode.QuintEncoding, 2), // Range 19 + (BiseEncodingMode.TritEncoding, 3), // Range 20 + (BiseEncodingMode.TritEncoding, 3), // Range 21 + (BiseEncodingMode.TritEncoding, 3), // Range 22 + (BiseEncodingMode.TritEncoding, 3), // Range 23 + (BiseEncodingMode.BitEncoding, 5), // Range 24 + (BiseEncodingMode.BitEncoding, 5), // Range 25 + (BiseEncodingMode.BitEncoding, 5), // Range 26 + (BiseEncodingMode.BitEncoding, 5), // Range 27 + (BiseEncodingMode.BitEncoding, 5), // Range 28 + (BiseEncodingMode.BitEncoding, 5), // Range 29 + (BiseEncodingMode.BitEncoding, 5), // Range 30 + (BiseEncodingMode.BitEncoding, 5) // Range 31 + ]; + + for (int i = 1; i < 32; ++i) + { + (BiseEncodingMode mode, int bitCount) = BoundedIntegerSequenceCodec.GetPackingModeBitCount(i); + (BiseEncodingMode expectedMode, int expectedBitCount) = expected[i - 1]; + + Assert.True(mode == expectedMode, $"range {i} mode should match"); + Assert.True(bitCount == expectedBitCount, $"range {i} bit count should match"); + } + } + + [Theory] + [InlineData(0)] + [InlineData(256)] + public void GetPackingModeBitCount_WithInvalidRange_ShouldThrowArgumentOutOfRangeException(int range) + { + Action action = () => BoundedIntegerSequenceCodec.GetPackingModeBitCount(range); + + Assert.Throws(action); + } + + [Theory] + [InlineData(1)] + [InlineData(10)] + [InlineData(32)] + [InlineData(63)] + public void GetBitCount_WithBitEncodingMode1Bit_ShouldReturnValueCount(int valueCount) + { + int bitCount = BoundedIntegerSequenceCodec.GetBitCount(BiseEncodingMode.BitEncoding, valueCount, 1); + int bitCountForRange = BoundedIntegerSequenceCodec.GetBitCountForRange(valueCount, 1); + + Assert.Equal(valueCount, bitCount); + Assert.Equal(valueCount, bitCountForRange); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(1, 2)] + [InlineData(10, 20)] + [InlineData(32, 64)] + public void GetBitCount_WithBitEncodingMode2Bits_ShouldReturnTwiceValueCount(int valueCount, int expected) + { + int bitCount = BoundedIntegerSequenceCodec.GetBitCount(BiseEncodingMode.BitEncoding, valueCount, 2); + int bitCountForRange = BoundedIntegerSequenceCodec.GetBitCountForRange(valueCount, 3); + + Assert.Equal(expected, bitCount); + Assert.Equal(expected, bitCountForRange); + } + + [Fact] + public void GetBitCount_WithTritEncoding15Values_ShouldReturnExpectedBitCount() + { + const int valueCount = 15; + const int bits = 3; + int expectedBitCount = (8 * 3) + (15 * 3); // 69 bits + + int bitCount = BoundedIntegerSequenceCodec.GetBitCount(BiseEncodingMode.TritEncoding, valueCount, bits); + int bitCountForRange = BoundedIntegerSequenceCodec.GetBitCountForRange(valueCount, 23); + + Assert.Equal(expectedBitCount, bitCount); + Assert.Equal(bitCount, bitCountForRange); + } + + [Fact] + public void GetBitCount_WithTritEncoding13Values_ShouldReturnExpectedBitCount() + { + const int valueCount = 13; + const int bits = 2; + const int expectedBitCount = 47; + + int bitCount = BoundedIntegerSequenceCodec.GetBitCount(BiseEncodingMode.TritEncoding, valueCount, bits); + int bitCountForRange = BoundedIntegerSequenceCodec.GetBitCountForRange(valueCount, 11); + + Assert.Equal(expectedBitCount, bitCount); + Assert.Equal(bitCount, bitCountForRange); + } + + [Fact] + public void GetBitCount_WithQuintEncoding6Values_ShouldReturnExpectedBitCount() + { + const int valueCount = 6; + const int bits = 4; + int expectedBitCount = (7 * 2) + (6 * 4); // 38 bits + + int bitCount = BoundedIntegerSequenceCodec.GetBitCount(BiseEncodingMode.QuintEncoding, valueCount, bits); + int bitCountForRange = BoundedIntegerSequenceCodec.GetBitCountForRange(valueCount, 79); + + Assert.Equal(expectedBitCount, bitCount); + Assert.Equal(bitCount, bitCountForRange); + } + + [Fact] + public void GetBitCount_WithQuintEncoding7Values_ShouldReturnExpectedBitCount() + { + const int valueCount = 7; + const int bits = 3; + int expectedBitCount = (7 * 2) + // First two quint blocks + (6 * 3) + // First two blocks of bits + 3 + // Last quint block without high order four bits + 3; // Last block with one set of three bits + + int bitCount = BoundedIntegerSequenceCodec.GetBitCount(BiseEncodingMode.QuintEncoding, valueCount, bits); + + Assert.Equal(expectedBitCount, bitCount); + } + + [Fact] + public void EncodeDecode_WithQuintValues_ShouldEncodeAndDecodeExpectedValues() + { + const int valueRange = 79; + BoundedIntegerSequenceEncoder encoder = new(valueRange); + int[] values = [3, 79, 37]; + + foreach (int value in values) + { + encoder.AddValue(value); + } + + // Encode + BitStream bitSink = default; + encoder.Encode(ref bitSink); + + // Verify encoded data + Assert.Equal(19u, bitSink.Bits); + Assert.True(bitSink.TryGetBits(19, out ulong encoded)); + Assert.Equal(0x4A7D3UL, encoded); + + // Decode + BitStream bitSrc = new(encoded, 19); + BoundedIntegerSequenceDecoder decoder = new(valueRange); + int[] decoded = decoder.Decode(3, ref bitSrc); + + Assert.Equal(values, decoded); + } + + [Fact] + public void DecodeThenEncode_WithQuintValues_ShouldPreserveEncoding() + { + int[] expectedValues = [16, 18, 17, 4, 7, 14, 10, 0]; + const ulong encoding = 0x2b9c83dc; + const int range = 19; + + // Decode + BitStream bitSrc = new(encoding, 64); + BoundedIntegerSequenceDecoder decoder = new(range); + int[] decoded = decoder.Decode(expectedValues.Length, ref bitSrc); + + // Check decoded values + Assert.Equal(expectedValues.Length, decoded.Length); + Assert.Equal(expectedValues, decoded); + + // Re-encode + BitStream bitSink = default; + BoundedIntegerSequenceEncoder encoder = new(range); + foreach (int value in expectedValues) + { + encoder.AddValue(value); + } + + encoder.Encode(ref bitSink); + + // Re-encoded should match original + Assert.Equal(35u, bitSink.Bits); + Assert.True(bitSink.TryGetBits(35, out ulong reencoded)); + Assert.Equal(encoding, reencoded); + } + + [Fact] + public void EncodeDecode_WithTritValues_ShouldEncodeAndDecodeExpectedValues() + { + const int valueRange = 11; + BoundedIntegerSequenceEncoder encoder = new(valueRange); + int[] values = [7, 5, 3, 6, 10]; + + foreach (int value in values) + { + encoder.AddValue(value); + } + + // Encode + BitStream bitSink = default; + encoder.Encode(ref bitSink); + + // Verify encoded data + Assert.Equal(18u, bitSink.Bits); + Assert.True(bitSink.TryGetBits(18, out ulong encoded)); + Assert.Equal(0x37357UL, encoded); + + // Decode + BitStream bitSrc = new(encoded, 19); + BoundedIntegerSequenceDecoder decoder = new(valueRange); + int[] decoded = decoder.Decode(5, ref bitSrc); + + Assert.Equal(values, decoded); + } + + [Fact] + public void DecodeThenEncode_WithTritValues_ShouldPreserveEncoding() + { + int[] expectedValues = [6, 0, 0, 2, 0, 0, 0, 0, 8, 0, 0, 0, 0, 8, 8, 0]; + const ulong encoding = 0x0004c0100001006UL; + const int range = 11; + + // Decode + BitStream bitSrc = new(encoding, 64); + BoundedIntegerSequenceDecoder decoder = new(range); + int[] decoded = decoder.Decode(expectedValues.Length, ref bitSrc); + + // Check decoded values + Assert.Equal(expectedValues.Length, decoded.Length); + Assert.Equal(expectedValues, decoded); + + // Re-encode + BitStream bitSink = default; + BoundedIntegerSequenceEncoder encoder = new(range); + foreach (int value in expectedValues) + { + encoder.AddValue(value); + } + + encoder.Encode(ref bitSink); + + // Assert re-encoded matches original + Assert.Equal(58u, bitSink.Bits); + Assert.True(bitSink.TryGetBits(58, out ulong reencoded)); + Assert.Equal(encoding, reencoded); + } + + [Fact] + public void EncodeDecode_WithRandomValues_ShouldAlwaysRoundTripCorrectly() + { + Random random = new(unchecked(0xbad7357)); + const int testCount = 1600; + + for (int test = 0; test < testCount; test++) + { + int valueCount = 4 + (random.Next(0, 256) % 44); + int range = 1 + (random.Next(0, 256) % 63); + + int bitCount = BoundedIntegerSequenceCodec.GetBitCountForRange(valueCount, range); + if (bitCount >= 64) + { + continue; + } + + // Generate random values + List generated = new(valueCount); + for (int i = 0; i < valueCount; i++) + { + generated.Add(random.Next(range + 1)); + } + + // Encode + BitStream bitSink = default; + BoundedIntegerSequenceEncoder encoder = new(range); + foreach (int value in generated) + { + encoder.AddValue(value); + } + + encoder.Encode(ref bitSink); + + Assert.True(bitSink.TryGetBits((int)bitSink.Bits, out ulong encoded)); + + // Decode + BitStream bitSrc = new(encoded, 64); + BoundedIntegerSequenceDecoder decoder = new(range); + int[] decoded = decoder.Decode(valueCount, ref bitSrc); + + Assert.Equal(generated.Count, decoded.Length); + Assert.Equal(generated, decoded); + } + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/IntermediateBlockTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/IntermediateBlockTests.cs new file mode 100644 index 00000000..4b392735 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/IntermediateBlockTests.cs @@ -0,0 +1,462 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +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.Tests.Formats.Astc; + +#nullable enable +public class IntermediateBlockTests +{ + private static readonly UInt128 ErrorBlock = UInt128.Zero; + + [Fact] + public void UnpackVoidExtent_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock errorBlock = PhysicalBlock.Create(ErrorBlock); + + IntermediateBlock.VoidExtentData? result = IntermediateBlock.UnpackVoidExtent(errorBlock); + + Assert.Null(result); + } + + [Fact] + public void UnpackIntermediateBlock_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock errorBlock = PhysicalBlock.Create(ErrorBlock); + + IntermediateBlock.IntermediateBlockData? result = IntermediateBlock.UnpackIntermediateBlock(errorBlock); + + Assert.Null(result); + } + + [Fact] + public void EndpointRangeForBlock_WithoutWeights_ShouldReturnNegativeOne() + { + IntermediateBlock.IntermediateBlockData data = new() + { + WeightRange = 15, + WeightGridX = 6, + WeightGridY = 6 + }; + + int result = IntermediateBlock.EndpointRangeForBlock(data); + + Assert.Equal(-1, result); + } + + [Fact] + public void Pack_WithIncorrectNumberOfWeights_ShouldReturnError() + { + IntermediateBlock.IntermediateBlockData data = new() + { + WeightRange = 15, + WeightGridX = 6, + WeightGridY = 6 + }; + + (string? error, UInt128 _) = IntermediateBlockPacker.Pack(data); + + Assert.NotNull(error); + Assert.Contains("Incorrect number of weights", error); + } + + [Fact] + public void EndpointRangeForBlock_WithNotEnoughBits_ShouldReturnNegativeTwo() + { + IntermediateBlock.IntermediateBlockData data = new() + { + WeightRange = 1, + PartitionId = 0, + WeightGridX = 8, + WeightGridY = 8, + EndpointCount = 3 + }; + data.Endpoints[0] = new() { Mode = ColorEndpointMode.LdrRgbDirect }; + data.Endpoints[1] = new() { Mode = ColorEndpointMode.LdrRgbDirect }; + data.Endpoints[2] = new() { Mode = ColorEndpointMode.LdrRgbDirect }; + + int result = IntermediateBlock.EndpointRangeForBlock(data); + + Assert.Equal(-2, result); + } + + [Fact] + public void Pack_WithNotEnoughBitsForColors_ShouldReturnError() + { + IntermediateBlock.IntermediateBlockData data = new() + { + WeightRange = 1, + PartitionId = 0, + WeightGridX = 8, + WeightGridY = 8, + Weights = new int[64], + EndpointCount = 3 + }; + data.Endpoints[0] = new() { Mode = ColorEndpointMode.LdrRgbDirect }; + data.Endpoints[1] = new() { Mode = ColorEndpointMode.LdrRgbDirect }; + data.Endpoints[2] = new() { Mode = ColorEndpointMode.LdrRgbDirect }; + + (string? error, UInt128 _) = IntermediateBlockPacker.Pack(data); + + Assert.NotNull(error); + Assert.Contains("illegal color range", error); + } + + [Fact] + public void EndpointRangeForBlock_WithIncreasingWeightGrid_ShouldDecreaseColorRange() + { + IntermediateBlock.IntermediateBlockData data = new() + { + WeightRange = 2, + DualPlaneChannel = null, + EndpointCount = 2 + }; + data.Endpoints[0] = new() { Mode = ColorEndpointMode.LdrRgbDirect }; + data.Endpoints[1] = new() { Mode = ColorEndpointMode.LdrRgbDirect }; + + List<(int W, int H)> weightParams = []; + for (int y = 2; y < 8; ++y) + { + for (int x = 2; x < 8; ++x) + { + weightParams.Add((x, y)); + } + } + + weightParams.Sort((a, b) => (a.W * a.H).CompareTo(b.W * b.H)); + + int lastColorRange = byte.MaxValue; + foreach ((int w, int h) in weightParams) + { + data.WeightGridX = w; + data.WeightGridY = h; + int colorRange = IntermediateBlock.EndpointRangeForBlock(data); + + Assert.True(colorRange <= lastColorRange); + lastColorRange = Math.Min(colorRange, lastColorRange); + } + + Assert.True(lastColorRange < byte.MaxValue); + } + + [Fact] + public void EndpointRange_WithStandardBlock_ShouldBe255() + { + PhysicalBlock block = PhysicalBlock.Create((UInt128)0x0000000001FE000173UL); + + IntermediateBlock.IntermediateBlockData? data = IntermediateBlock.UnpackIntermediateBlock(block); + + Assert.Equal(255, block.GetColorValuesRange()); + Assert.NotNull(data); + IntermediateBlock.IntermediateBlockData ib = data!.Value; + Assert.Equal(1, ib.EndpointCount); + Assert.Equal(ColorEndpointMode.LdrLumaDirect, ib.Endpoints[0].Mode); + Assert.Equal(byte.MinValue, ib.Endpoints[0].Colors[0]); + Assert.Equal(byte.MaxValue, ib.Endpoints[0].Colors[1]); + Assert.Equal(2, ib.Endpoints[0].ColorCount); + Assert.Equal(byte.MaxValue, ib.EndpointRange); + } + + [Fact] + public void UnpackIntermediateBlock_WithStandardBlock_ShouldReturnCorrectData() + { + PhysicalBlock block = PhysicalBlock.Create((UInt128)0x0000000001FE000173UL); + + IntermediateBlock.IntermediateBlockData? result = IntermediateBlock.UnpackIntermediateBlock(block); + + Assert.NotNull(result); + IntermediateBlock.IntermediateBlockData data = result!.Value; + + Assert.Equal(6, data.WeightGridX); + Assert.Equal(5, data.WeightGridY); + Assert.Equal(7, data.WeightRange); + Assert.Null(data.PartitionId); + Assert.Null(data.DualPlaneChannel); + + Assert.Equal(30, data.WeightsCount); + Assert.All(data.Weights.AsSpan(0, data.WeightsCount).ToArray(), item => Assert.Equal(0, item)); + + Assert.Equal(1, data.EndpointCount); + IntermediateBlock.IntermediateEndpointData endpoint = data.Endpoints[0]; + Assert.Equal(ColorEndpointMode.LdrLumaDirect, endpoint.Mode); + Assert.Equal(2, endpoint.ColorCount); + Assert.Equal(byte.MinValue, endpoint.Colors[0]); + Assert.Equal(byte.MaxValue, endpoint.Colors[1]); + } + + [Fact] + public void Pack_WithStandardBlockData_ShouldProduceExpectedBits() + { + IntermediateBlock.IntermediateBlockData data = new() + { + WeightGridX = 6, + WeightGridY = 5, + WeightRange = 7, + PartitionId = null, + DualPlaneChannel = null, + Weights = new int[30] + }; + + IntermediateBlock.IntermediateEndpointData endpoint = new() + { + Mode = ColorEndpointMode.LdrLumaDirect, + ColorCount = 2 + }; + endpoint.Colors[0] = byte.MinValue; + endpoint.Colors[1] = byte.MaxValue; + data.Endpoints[0] = endpoint; + data.EndpointCount = 1; + + (string? error, UInt128 packed) = IntermediateBlockPacker.Pack(data); + + Assert.Null(error); + Assert.Equal((UInt128)0x0000000001FE000173UL, packed); + } + + [Fact] + public void Pack_WithLargeGapInBits_ShouldPreserveOriginalEncoding() + { + UInt128 original = new(0xBEDEAD0000000000UL, 0x0000000001FE032EUL); + PhysicalBlock block = PhysicalBlock.Create(original); + IntermediateBlock.IntermediateBlockData? data = IntermediateBlock.UnpackIntermediateBlock(block); + + Assert.NotNull(data); + IntermediateBlock.IntermediateBlockData intermediate = data!.Value; + + // Check unpacked values + Assert.Equal(2, intermediate.WeightGridX); + Assert.Equal(3, intermediate.WeightGridY); + Assert.Equal(15, intermediate.WeightRange); + Assert.Null(intermediate.PartitionId); + Assert.Null(intermediate.DualPlaneChannel); + Assert.Equal(1, intermediate.EndpointCount); + Assert.Equal(ColorEndpointMode.LdrLumaDirect, intermediate.Endpoints[0].Mode); + Assert.Equal(2, intermediate.Endpoints[0].ColorCount); + Assert.Equal(255, intermediate.Endpoints[0].Colors[0]); + Assert.Equal(0, intermediate.Endpoints[0].Colors[1]); + + // Repack + (string? error, UInt128 repacked) = IntermediateBlockPacker.Pack(intermediate); + + Assert.Null(error); + Assert.Equal(original, repacked); + } + + [Fact] + public void UnpackVoidExtent_WithAllOnesPattern_ShouldReturnZeroColors() + { + PhysicalBlock block = PhysicalBlock.Create((UInt128)0xFFFFFFFFFFFFFDFCUL); + + IntermediateBlock.VoidExtentData? result = IntermediateBlock.UnpackVoidExtent(block); + + Assert.NotNull(result); + IntermediateBlock.VoidExtentData data = result!.Value; + + Assert.Equal(0, data.R); + Assert.Equal(0, data.G); + Assert.Equal(0, data.B); + Assert.Equal(0, data.A); + + Assert.All(data.Coords, c => Assert.Equal((1 << 13) - 1, c)); + } + + [Fact] + public void UnpackVoidExtent_WithColorData_ShouldReturnCorrectColors() + { + UInt128 blockBits = new(0xdeadbeefdeadbeefUL, 0xFFF8003FFE000DFCUL); + PhysicalBlock block = PhysicalBlock.Create(blockBits); + + IntermediateBlock.VoidExtentData? result = IntermediateBlock.UnpackVoidExtent(block); + + Assert.NotNull(result); + IntermediateBlock.VoidExtentData data = result!.Value; + + Assert.Equal(0xbeef, data.R); + Assert.Equal(0xdead, data.G); + Assert.Equal(0xbeef, data.B); + Assert.Equal(0xdead, data.A); + + Assert.Equal(0, data.Coords[0]); + Assert.Equal(8191, data.Coords[1]); + Assert.Equal(0, data.Coords[2]); + Assert.Equal(8191, data.Coords[3]); + } + + [Fact] + public void Pack_WithZeroColorVoidExtent_ShouldProduceAllOnesPattern() + { + IntermediateBlock.VoidExtentData data = new() + { + R = 0, + G = 0, + B = 0, + A = 0, + Coords = new ushort[4] + }; + + for (int i = 0; i < 4; ++i) + { + data.Coords[i] = (1 << 13) - 1; + } + + (string? error, UInt128 packed) = IntermediateBlockPacker.Pack(data); + + Assert.Null(error); + Assert.Equal((UInt128)0xFFFFFFFFFFFFFDFCUL, packed); + } + + [Fact] + public void Pack_WithColorVoidExtent_ShouldProduceExpectedBits() + { + IntermediateBlock.VoidExtentData data = new() + { + R = 0xbeef, + G = 0xdead, + B = 0xbeef, + A = 0xdead, + Coords = [0, 8191, 0, 8191] + }; + + (string? error, UInt128 packed) = IntermediateBlockPacker.Pack(data); + + Assert.Null(error); + Assert.Equal(new UInt128(0xdeadbeefdeadbeefUL, 0xFFF8003FFE000DFCUL), packed); + } + + [Theory] + [InlineData(0xe8e8eaea20000980UL, 0x20000200cb73f045UL)] + [InlineData(0x3300c30700cb01c5UL, 0x0573907b8c0f6879UL)] + public void PackUnpack_WithSameCEM_ShouldRoundTripCorrectly(ulong high, ulong low) + { + UInt128 original = new(high, low); + PhysicalBlock block = PhysicalBlock.Create(original); + + IntermediateBlock.IntermediateBlockData? unpacked = IntermediateBlock.UnpackIntermediateBlock(block); + + Assert.NotNull(unpacked); + IntermediateBlock.IntermediateBlockData ib = unpacked!.Value; + + (string? error, UInt128 repacked) = IntermediateBlockPacker.Pack(ib); + + Assert.Null(error); + Assert.Equal(original, repacked); + } + + [Theory] + [InlineData(TestImages.Astc.Checkered_4, 4)] + [InlineData(TestImages.Astc.Checkered_5, 5)] + [InlineData(TestImages.Astc.Checkered_6, 6)] + [InlineData(TestImages.Astc.Checkered_7, 7)] + [InlineData(TestImages.Astc.Checkered_8, 8)] + [InlineData(TestImages.Astc.Checkered_9, 9)] + [InlineData(TestImages.Astc.Checkered_10, 10)] + [InlineData(TestImages.Astc.Checkered_11, 11)] + [InlineData(TestImages.Astc.Checkered_12, 12)] + public void PackUnpack_WithTestDataBlocks_ShouldPreserveBlockProperties(string inputFile, int checkeredDim) + { + const int astcDim = 8; + int imgDim = checkeredDim * astcDim; + byte[] astcData = LoadASTCFile(inputFile); + int numBlocks = (imgDim / astcDim) * (imgDim / astcDim); + + Assert.Equal(0, astcData.Length % PhysicalBlock.SizeInBytes); + + for (int i = 0; i < numBlocks; ++i) + { + ReadOnlySpan slice = new(astcData, i * PhysicalBlock.SizeInBytes, PhysicalBlock.SizeInBytes); + UInt128 blockBits = new( + BitConverter.ToUInt64(slice.Slice(8, 8)), + BitConverter.ToUInt64(slice[..8])); + PhysicalBlock originalBlock = PhysicalBlock.Create(blockBits); + + // Unpack and repack + UInt128 repacked; + if (originalBlock.IsVoidExtent) + { + IntermediateBlock.VoidExtentData? voidData = IntermediateBlock.UnpackVoidExtent(originalBlock); + Assert.NotNull(voidData); + + (string? error, UInt128 packed) = IntermediateBlockPacker.Pack(voidData!.Value); + Assert.Null(error); + repacked = packed; + } + else + { + IntermediateBlock.IntermediateBlockData? intermediateData = IntermediateBlock.UnpackIntermediateBlock(originalBlock); + Assert.NotNull(intermediateData); + IntermediateBlock.IntermediateBlockData ibData = intermediateData!.Value; + + // Verify endpoint range was set + Assert.Equal(originalBlock.GetColorValuesRange(), ibData.EndpointRange); + + // Clear endpoint range before repacking (to test calculation) + ibData.EndpointRange = null; + (string? error, UInt128 packed) = IntermediateBlockPacker.Pack(ibData); + Assert.Null(error); + repacked = packed; + } + + // Verify repacked block + PhysicalBlock repackedBlock = PhysicalBlock.Create(repacked); + VerifyBlockPropertiesMatch(repackedBlock, originalBlock); + } + } + + private static void VerifyBlockPropertiesMatch(PhysicalBlock repacked, PhysicalBlock original) + { + Assert.False(repacked.IsIllegalEncoding); + + // Verify color bits match + int repackedColorBitCount = repacked.GetColorBitCount() ?? 0; + UInt128 repackedColorMask = UInt128Extensions.OnesMask(repackedColorBitCount); + UInt128 repackedColorBits = (repacked.BlockBits >> (repacked.GetColorStartBit() ?? 0)) & repackedColorMask; + + int originalColorBitCount = original.GetColorBitCount() ?? 0; + UInt128 originalColorMask = UInt128Extensions.OnesMask(originalColorBitCount); + UInt128 originalColorBits = (original.BlockBits >> (original.GetColorStartBit() ?? 0)) & originalColorMask; + + Assert.Equal(originalColorMask, repackedColorMask); + Assert.Equal(originalColorBits, repackedColorBits); + + // Verify void extent properties + Assert.Equal(original.IsVoidExtent, repacked.IsVoidExtent); + Assert.Equal(original.GetVoidExtentCoordinates(), repacked.GetVoidExtentCoordinates()); + + // Verify weight properties + Assert.Equal(original.GetWeightGridDimensions(), repacked.GetWeightGridDimensions()); + Assert.Equal(original.GetWeightRange(), repacked.GetWeightRange()); + Assert.Equal(original.GetWeightBitCount(), repacked.GetWeightBitCount()); + Assert.Equal(original.GetWeightStartBit(), repacked.GetWeightStartBit()); + + // Verify dual plane properties + Assert.Equal(original.IsDualPlane, repacked.IsDualPlane); + Assert.Equal(original.GetDualPlaneChannel(), repacked.GetDualPlaneChannel()); + + // Verify partition properties + Assert.Equal(original.GetPartitionsCount(), repacked.GetPartitionsCount()); + Assert.Equal(original.GetPartitionId(), repacked.GetPartitionId()); + + // Verify color value properties + Assert.Equal(original.GetColorValuesCount(), repacked.GetColorValuesCount()); + Assert.Equal(original.GetColorValuesRange(), repacked.GetColorValuesRange()); + + // Verify endpoint modes for all partitions + int numParts = repacked.GetPartitionsCount().GetValueOrDefault(0); + for (int j = 0; j < numParts; ++j) + { + Assert.True(repacked.GetEndpointMode(j) == original.GetEndpointMode(j), $"Endpoint mode mismatch at partition {j}"); + } + } + + private static byte[] LoadASTCFile(string inputFile) + { + string filename = TestFile.GetInputFileFullPath(inputFile); + Assert.True(File.Exists(filename), $"Testdata missing: {filename}"); + byte[] data = File.ReadAllBytes(filename); + Assert.True(data.Length >= 16, "ASTC file too small"); + return [.. data.Skip(16)]; + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/LogicalAstcBlockTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/LogicalAstcBlockTests.cs new file mode 100644 index 00000000..192a7b7d --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/LogicalAstcBlockTests.cs @@ -0,0 +1,441 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Textures.Compression.Astc.ColorEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; +using SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; + +#nullable enable +public class LogicalAstcBlockTests +{ + [Theory] + [InlineData(FootprintType.Footprint4x4)] + [InlineData(FootprintType.Footprint5x5)] + [InlineData(FootprintType.Footprint8x8)] + [InlineData(FootprintType.Footprint10x10)] + [InlineData(FootprintType.Footprint12x12)] + public void Constructor_WithValidFootprintType_ShoulReturnExpectedFootprint(FootprintType footprintType) + { + Footprint footprint = Footprint.FromFootprintType(footprintType); + LogicalBlock logicalBlock = new(footprint); + + Assert.Equal(footprint, logicalBlock.GetFootprint()); + Assert.Equal(footprintType, logicalBlock.GetFootprint().Type); + } + + [Fact] + public void GetFootprint_AfterConstruction_ShouldReturnOriginalFootprint() + { + Footprint footprint = Footprint.Get8x8(); + LogicalBlock logicalBlock = new(footprint); + + Footprint result = logicalBlock.GetFootprint(); + + Assert.Equal(footprint, result); + } + + [Theory] + [InlineData(0)] + [InlineData(32)] + [InlineData(64)] + public void SetWeightAt_WithValidWeight_ShouldStoreCorrectly(int weight) + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + + logicalBlock.SetWeightAt(1, 1, weight); + + Assert.Equal(weight, logicalBlock.WeightAt(1, 1)); + } + + [Theory] + [InlineData(-1)] + [InlineData(65)] + [InlineData(100)] + public void SetWeightAt_WithInvalidWeight_ShouldThrowArgumentOutOfRangeException(int weight) + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + + Action action = () => logicalBlock.SetWeightAt(0, 0, weight); + + Assert.Throws(action); + } + + [Fact] + public void WeightAt_WithDefaultWeights_ShouldReturnZero() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + + int weight = logicalBlock.WeightAt(2, 2); + + Assert.Equal(0, weight); + } + + [Fact] + public void IsDualPlane_ByDefault_ShouldBeFalse() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + + bool result = logicalBlock.IsDualPlane(); + + Assert.False(result); + } + + [Fact] + public void SetDualPlaneChannel_WithValidChannel_ShouldEnableDualPlane() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + + logicalBlock.SetDualPlaneChannel(0); + + Assert.True(logicalBlock.IsDualPlane()); + } + + [Fact] + public void SetDualPlaneChannel_WithNegativeValue_ShouldDisableDualPlane() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + logicalBlock.SetDualPlaneChannel(0); + + logicalBlock.SetDualPlaneChannel(-1); + + Assert.False(logicalBlock.IsDualPlane()); + } + + [Fact] + public void SetDualPlaneWeightAt_WhenNotDualPlane_ShouldThrowInvalidOperationException() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + + Action action = () => logicalBlock.SetDualPlaneWeightAt(0, 2, 3, 1); + + var ex = Assert.Throws(action); + Assert.Contains("Not a dual plane block", ex.Message); + } + + [Fact] + public void SetDualPlaneWeightAt_AfterEnablingDualPlane_ShouldPreserveOriginalWeight() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + logicalBlock.SetWeightAt(2, 3, 2); + logicalBlock.SetDualPlaneChannel(0); + + logicalBlock.SetDualPlaneWeightAt(0, 2, 3, 1); + + Assert.Equal(2, logicalBlock.WeightAt(2, 3)); + Assert.Equal(1, logicalBlock.DualPlaneWeightAt(0, 2, 3)); + } + + [Fact] + public void DualPlaneWeightAt_ForNonDualPlaneChannel_ShouldReturnOriginalWeight() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + logicalBlock.SetWeightAt(2, 3, 2); + logicalBlock.SetDualPlaneChannel(0); + logicalBlock.SetDualPlaneWeightAt(0, 2, 3, 1); + + for (int i = 1; i < 4; ++i) + { + Assert.Equal(2, logicalBlock.DualPlaneWeightAt(i, 2, 3)); + } + } + + [Fact] + public void DualPlaneWeightAt_WhenNotDualPlane_ShouldReturnWeightAt() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + logicalBlock.SetWeightAt(2, 3, 42); + + int result = logicalBlock.DualPlaneWeightAt(0, 2, 3); + + Assert.Equal(42, result); + } + + [Fact] + public void SetDualPlaneWeightAt_ThenDisableDualPlane_ShouldResetToOriginalWeight() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + logicalBlock.SetWeightAt(2, 3, 2); + logicalBlock.SetDualPlaneChannel(0); + logicalBlock.SetDualPlaneWeightAt(0, 2, 3, 1); + + logicalBlock.SetDualPlaneChannel(-1); + + Assert.False(logicalBlock.IsDualPlane()); + Assert.Equal(2, logicalBlock.WeightAt(2, 3)); + for (int i = 0; i < 4; ++i) + { + Assert.Equal(2, logicalBlock.DualPlaneWeightAt(i, 2, 3)); + } + } + + [Fact] + public void SetEndpoints_WithValidColors_ShouldStoreCorrectly() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + Rgba32 color1 = new(byte.MaxValue, byte.MinValue, byte.MinValue, byte.MaxValue); + Rgba32 color2 = new(byte.MinValue, byte.MaxValue, byte.MinValue, byte.MaxValue); + + logicalBlock.SetEndpoints(color1, color2, 0); + + // No direct getter, but we can verify through ColorAt + logicalBlock.SetWeightAt(0, 0, 0); + logicalBlock.SetWeightAt(1, 1, 64); + + Rgba32 colorAtMinWeight = logicalBlock.ColorAt(0, 0); + Rgba32 colorAtMaxWeight = logicalBlock.ColorAt(1, 1); + + Assert.Equal(color1.R, colorAtMinWeight.R); + Assert.True(Math.Abs(colorAtMaxWeight.R - color2.R) <= 1); + } + + [Fact] + public void ColorAt_WithCheckerboardWeights_ShouldInterpolateCorrectly() + { + LogicalBlock logicalBlock = new(Footprint.Get8x8()); + + // Create checkerboard weight pattern + for (int j = 0; j < 8; ++j) + { + for (int i = 0; i < 8; ++i) + { + if (((i ^ j) & 1) == 1) + { + logicalBlock.SetWeightAt(i, j, 0); + } + else + { + logicalBlock.SetWeightAt(i, j, 64); + } + } + } + + Rgba32 endpointA = new(123, 45, 67, 89); + Rgba32 endpointB = new(101, 121, 31, 41); + logicalBlock.SetEndpoints(endpointA, endpointB, 0); + + for (int j = 0; j < 8; ++j) + { + for (int i = 0; i < 8; ++i) + { + Rgba32 color = logicalBlock.ColorAt(i, j); + if (((i ^ j) & 1) == 1) + { + // Weight 0 = first endpoint + Assert.Equal(endpointA.R, color.R); + Assert.Equal(endpointA.G, color.G); + Assert.Equal(endpointA.B, color.B); + Assert.Equal(endpointA.A, color.A); + } + else + { + // Weight 64 = second endpoint + Assert.Equal(endpointB.R, color.R); + Assert.Equal(endpointB.G, color.G); + Assert.Equal(endpointB.B, color.B); + Assert.Equal(endpointB.A, color.A); + } + } + } + } + + [Theory] + [InlineData(-1, 0)] + [InlineData(0, -1)] + [InlineData(4, 0)] + [InlineData(0, 4)] + public void ColorAt_WithOutOfBoundsCoordinates_ShouldThrowArgumentOutOfRangeException(int x, int y) + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + + Action action = () => logicalBlock.ColorAt(x, y); + + Assert.Throws(action); + } + + [Fact] + public void SetPartition_WithValidPartition_ShouldUpdateCorrectly() + { + Footprint footprint = Footprint.Get8x8(); + LogicalBlock logicalBlock = new(footprint); + + // Create partition with 2 subsets, all pixels assigned to subset 0 + Partition newPartition = new(footprint, 2, 5) + { + Assignment = new int[footprint.PixelCount] + }; + + logicalBlock.SetPartition(newPartition); + + // Should be able to set endpoints for both valid partitions (0 and 1) + Rgba32 redEndpoint = new(byte.MaxValue, byte.MinValue, byte.MinValue, byte.MaxValue); + Rgba32 blackEndpoint = new(byte.MinValue, byte.MinValue, byte.MinValue, byte.MaxValue); + Rgba32 greenEndpoint = new(byte.MinValue, byte.MaxValue, byte.MinValue, byte.MaxValue); + + Action setEndpoint0 = () => logicalBlock.SetEndpoints(redEndpoint, blackEndpoint, 0); + Action setEndpoint1 = () => logicalBlock.SetEndpoints(greenEndpoint, blackEndpoint, 1); + + setEndpoint0(); + setEndpoint1(); + + // Should not be able to set endpoints for non-existent partition 2 + Action setEndpoint2 = () => logicalBlock.SetEndpoints(redEndpoint, blackEndpoint, 2); + Assert.Throws(setEndpoint2); + } + + [Fact] + public void SetPartition_WithDifferentFootprint_ShouldThrowInvalidOperationException() + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + Partition wrongPartition = new(Footprint.Get8x8(), 1, 0) + { + Assignment = new int[64] + }; + + Action action = () => logicalBlock.SetPartition(wrongPartition); + + var ex = Assert.Throws(action); + Assert.Contains("New partitions may not be for a different footprint", ex.Message); + } + + [Theory] + [InlineData(-1)] + [InlineData(2)] + public void SetEndpoints_WithInvalidSubset_ShouldThrowArgumentOutOfRangeException(int subset) + { + LogicalBlock logicalBlock = new(Footprint.Get4x4()); + Rgba32 color1 = new(byte.MaxValue, byte.MinValue, byte.MinValue, byte.MaxValue); + Rgba32 color2 = new(byte.MinValue, byte.MaxValue, byte.MinValue, byte.MaxValue); + + Action action = () => logicalBlock.SetEndpoints(color1, color2, subset); + + Assert.Throws(action); + } + + [Fact] + public void UnpackLogicalBlock_WithErrorBlock_ShouldReturnNull() + { + UInt128 bits = UInt128.Zero; + BlockInfo info = BlockInfo.Decode(bits); + + LogicalBlock? result = LogicalBlock.UnpackLogicalBlock(Footprint.Get8x8(), bits, in info); + + Assert.Null(result); + } + + [Fact] + public void UnpackLogicalBlock_WithVoidExtentBlock_ShouldReturnLogicalBlock() + { + UInt128 bits = (UInt128)0xFFFFFFFFFFFFFDFCUL; + BlockInfo info = BlockInfo.Decode(bits); + + LogicalBlock? result = LogicalBlock.UnpackLogicalBlock(Footprint.Get8x8(), bits, in info); + + Assert.NotNull(result); + Assert.Equal(Footprint.Get8x8(), result!.GetFootprint()); + } + + [Fact] + public void UnpackLogicalBlock_WithStandardBlock_ShouldReturnLogicalBlock() + { + UInt128 bits = (UInt128)0x0000000001FE000173UL; + BlockInfo info = BlockInfo.Decode(bits); + + LogicalBlock? result = LogicalBlock.UnpackLogicalBlock(Footprint.Get6x5(), bits, in info); + + Assert.NotNull(result); + Assert.Equal(Footprint.Get6x5(), result!.GetFootprint()); + } + + [Theory] + + // Synthetic test images + [InlineData(TestImages.Astc.Footprint_4x4, TestImages.Astc.Expected.Footprint_4x4, FootprintType.Footprint4x4, 32, 32)] + [InlineData(TestImages.Astc.Footprint_5x4, TestImages.Astc.Expected.Footprint_5x4, FootprintType.Footprint5x4, 32, 32)] + [InlineData(TestImages.Astc.Footprint_5x5, TestImages.Astc.Expected.Footprint_5x5, FootprintType.Footprint5x5, 32, 32)] + [InlineData(TestImages.Astc.Footprint_6x5, TestImages.Astc.Expected.Footprint_6x5, FootprintType.Footprint6x5, 32, 32)] + [InlineData(TestImages.Astc.Footprint_6x6, TestImages.Astc.Expected.Footprint_6x6, FootprintType.Footprint6x6, 32, 32)] + [InlineData(TestImages.Astc.Footprint_8x5, TestImages.Astc.Expected.Footprint_8x5, FootprintType.Footprint8x5, 32, 32)] + [InlineData(TestImages.Astc.Footprint_8x6, TestImages.Astc.Expected.Footprint_8x6, FootprintType.Footprint8x6, 32, 32)] + [InlineData(TestImages.Astc.Footprint_8x8, TestImages.Astc.Expected.Footprint_8x8, FootprintType.Footprint8x8, 32, 32)] + [InlineData(TestImages.Astc.Footprint_10x5, TestImages.Astc.Expected.Footprint_10x5, FootprintType.Footprint10x5, 32, 32)] + [InlineData(TestImages.Astc.Footprint_10x6, TestImages.Astc.Expected.Footprint_10x6, FootprintType.Footprint10x6, 32, 32)] + [InlineData(TestImages.Astc.Footprint_10x8, TestImages.Astc.Expected.Footprint_10x8, FootprintType.Footprint10x8, 32, 32)] + [InlineData(TestImages.Astc.Footprint_10x10, TestImages.Astc.Expected.Footprint_10x10, FootprintType.Footprint10x10, 32, 32)] + [InlineData(TestImages.Astc.Footprint_12x10, TestImages.Astc.Expected.Footprint_12x10, FootprintType.Footprint12x10, 32, 32)] + [InlineData(TestImages.Astc.Footprint_12x12, TestImages.Astc.Expected.Footprint_12x12, FootprintType.Footprint12x12, 32, 32)] + + // RGB without alpha images + [InlineData(TestImages.Astc.Rgb_4x4, TestImages.Astc.Expected.Rgb_4x4, FootprintType.Footprint4x4, 224, 288)] + [InlineData(TestImages.Astc.Rgb_5x4, TestImages.Astc.Expected.Rgb_5x4, FootprintType.Footprint5x4, 224, 288)] + [InlineData(TestImages.Astc.Rgb_6x6, TestImages.Astc.Expected.Rgb_6x6, FootprintType.Footprint6x6, 224, 288)] + [InlineData(TestImages.Astc.Rgb_8x8, TestImages.Astc.Expected.Rgb_8x8, FootprintType.Footprint8x8, 224, 288)] + [InlineData(TestImages.Astc.Rgb_12x12, TestImages.Astc.Expected.Rgb_12x12, FootprintType.Footprint12x12, 224, 288)] + + // RGB with alpha images + [InlineData(TestImages.Astc.Atlas_Small_4x4, TestImages.Astc.Expected.Atlas_Small_4x4, FootprintType.Footprint4x4, 256, 256)] + [InlineData(TestImages.Astc.Atlas_Small_5x5, TestImages.Astc.Expected.Atlas_Small_5x5, FootprintType.Footprint5x5, 256, 256)] + [InlineData(TestImages.Astc.Atlas_Small_6x6, TestImages.Astc.Expected.Atlas_Small_6x6, FootprintType.Footprint6x6, 256, 256)] + [InlineData(TestImages.Astc.Atlas_Small_8x8, TestImages.Astc.Expected.Atlas_Small_8x8, FootprintType.Footprint8x8, 256, 256)] + public void UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly( + string inputFile, + string expectedFile, + FootprintType footprintType, + int width, + int height) + { + Footprint footprint = Footprint.FromFootprintType(footprintType); + byte[] astcData = TestFile.Create(inputFile).Bytes[16..]; + + using Image decodedImage = DecodeAstcBlocksToImage(footprint, astcData, width, height); + + string expectedPath = TestFile.GetInputFileFullPath(expectedFile); + using Image expectedImage = Image.Load(expectedPath); + ImageComparer.TolerantPercentage(1.0f).VerifySimilarity(expectedImage, decodedImage); + } + + private static Image DecodeAstcBlocksToImage(Footprint footprint, byte[] astcData, int width, int height) + { + // ASTC uses x/y ordering, so we flip Y to match ImageSharp's row/column origin. + Image image = new(width, height); + int blockWidth = footprint.Width; + int blockHeight = footprint.Height; + int blocksWide = (width + blockWidth - 1) / blockWidth; + + for (int i = 0; i < astcData.Length; i += PhysicalBlock.SizeInBytes) + { + int blockIndex = i / PhysicalBlock.SizeInBytes; + int blockX = blockIndex % blocksWide; + int blockY = blockIndex / blocksWide; + + byte[] blockSpan = astcData.AsSpan(i, PhysicalBlock.SizeInBytes).ToArray(); + UInt128 bits = new( + BitConverter.ToUInt64(blockSpan, 8), + BitConverter.ToUInt64(blockSpan, 0)); + BlockInfo info = BlockInfo.Decode(bits); + LogicalBlock? logicalBlock = LogicalBlock.UnpackLogicalBlock(footprint, bits, in info); + Assert.NotNull(logicalBlock); + + for (int y = 0; y < blockHeight; ++y) + { + for (int x = 0; x < blockWidth; ++x) + { + int px = (blockWidth * blockX) + x; + int py = (blockHeight * blockY) + y; + if (px >= width || py >= height) + { + continue; + } + + Rgba32 decoded = logicalBlock!.ColorAt(x, y); + image[px, height - 1 - py] = decoded; + } + } + } + + return image; + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/PartitionTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/PartitionTests.cs new file mode 100644 index 00000000..7d32a625 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/PartitionTests.cs @@ -0,0 +1,224 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using SixLabors.ImageSharp.Textures.Compression.Astc.ColorEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; + +public class PartitionTests +{ + [Fact] + public void PartitionMetric_WithSimplePartitions_ShouldCalculateCorrectDistance() + { + Partition partitionA = new(Footprint.Get6x6(), 2) + { + Assignment = + [ + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1 + ] + }; + + Partition partitionB = new(Footprint.Get6x6(), 2) + { + Assignment = + [ + 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ] + }; + + int distance = Partition.PartitionMetric(partitionA, partitionB); + + Assert.Equal(2, distance); + } + + [Fact] + public void PartitionMetric_WithDifferentPartCounts_ShouldCalculateCorrectDistance() + { + Partition partitionA = new(Footprint.Get4x4(), 2) + { + Assignment = + [ + 2, 2, 2, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 1 + ] + }; + + Partition partitionB = new(Footprint.Get4x4(), 3) + { + Assignment = + [ + 1, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + ] + }; + + int distance = Partition.PartitionMetric(partitionA, partitionB); + + Assert.Equal(3, distance); + } + + [Fact] + public void PartitionMetric_WithDifferentMapping_ShouldCalculateCorrectDistance() + { + Partition partitionA = new(Footprint.Get4x4(), 2) + { + Assignment = + [ + 0, 1, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2 + ] + }; + + Partition partitionB = new(Footprint.Get4x4(), 3) + { + Assignment = + [ + 1, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + ] + }; + + int distance = Partition.PartitionMetric(partitionA, partitionB); + + Assert.Equal(1, distance); + } + + [Fact] + public void GetASTCPartition_WithSpecificParameters_ShouldReturnExpectedAssignment() + { + int[] expected = + [ + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2 + ]; + + Partition partition = Partition.GetASTCPartition(Footprint.Get10x6(), 3, 557); + + Assert.Equal(expected, partition.Assignment); + } + + [Fact] + public void GetASTCPartition_WithDifferentIds_ShouldProduceUniqueAssignments() + { + Partition partition0 = Partition.GetASTCPartition(Footprint.Get6x6(), 2, 0); + Partition partition1 = Partition.GetASTCPartition(Footprint.Get6x6(), 2, 1); + + Assert.NotEqual(partition1.Assignment, partition0.Assignment); + } + + [Fact] + public void FindClosestASTCPartition_ShouldPreservePartitionCount() + { + Partition partition = new(Footprint.Get6x6(), 2) + { + Assignment = + [ + 0, 0, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1 + ] + }; + + Partition closestAstcPartition = Partition.FindClosestASTCPartition(partition); + + Assert.Equal(partition.PartitionCount, closestAstcPartition.PartitionCount); + } + + [Fact] + public void FindClosestASTCPartition_WithModifiedPartition_ShouldReturnValidASTCPartition() + { + Partition astcPartition = Partition.GetASTCPartition(Footprint.Get12x12(), 3, 0x3CB); + Partition modifiedPartition = new(astcPartition.Footprint, astcPartition.PartitionCount) + { + Assignment = [.. astcPartition.Assignment] + }; + modifiedPartition.Assignment[0]++; + + // Find closest ASTC partition + Partition closestPartition = Partition.FindClosestASTCPartition(modifiedPartition); + + // The closest partition should be a valid ASTC partition with the same footprint and number of parts + Assert.Equal(astcPartition.Footprint, closestPartition.Footprint); + Assert.Equal(astcPartition.PartitionCount, closestPartition.PartitionCount); + Assert.NotNull(closestPartition.PartitionId); + + // Verify we can retrieve the same partition again using its ID + Partition verifyPartition = Partition.GetASTCPartition( + closestPartition.Footprint, + closestPartition.PartitionCount, + closestPartition.PartitionId!.Value); + Assert.Equal(closestPartition, verifyPartition); + } + + [Theory] + [InlineData(FootprintType.Footprint4x4)] + [InlineData(FootprintType.Footprint5x4)] + [InlineData(FootprintType.Footprint5x5)] + [InlineData(FootprintType.Footprint6x5)] + [InlineData(FootprintType.Footprint6x6)] + [InlineData(FootprintType.Footprint8x5)] + [InlineData(FootprintType.Footprint8x6)] + [InlineData(FootprintType.Footprint8x8)] + [InlineData(FootprintType.Footprint10x5)] + [InlineData(FootprintType.Footprint10x6)] + [InlineData(FootprintType.Footprint10x8)] + [InlineData(FootprintType.Footprint10x10)] + [InlineData(FootprintType.Footprint12x10)] + [InlineData(FootprintType.Footprint12x12)] + public void FindClosestASTCPartition_WithRandomPartitions_ShouldReturnFewerOrEqualSubsets(FootprintType footprintType) + { + Footprint footprint = Footprint.FromFootprintType(footprintType); + Random random = new(unchecked((int)0xdeadbeef)); + + const int numTests = 15; // Tests per footprint type + for (int i = 0; i < numTests; i++) + { + // Create random partition + int numParts = 2 + random.Next(3); // 2, 3, or 4 parts + int[] assignment = new int[footprint.PixelCount]; + for (int j = 0; j < footprint.PixelCount; j++) + { + assignment[j] = random.Next(numParts); + } + + Partition partition = new(footprint, numParts) + { + Assignment = assignment + }; + + Partition astcPartition = Partition.FindClosestASTCPartition(partition); + + // Matched partition should have fewer or equal subsets + Assert.True( + astcPartition.PartitionCount <= partition.PartitionCount, + $"Footprint {footprintType}, Test #{i}: Selected partition with ID {astcPartition.PartitionId?.ToString(CultureInfo.InvariantCulture) ?? "null"}"); + } + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/PhysicalAstcBlockTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/PhysicalAstcBlockTests.cs new file mode 100644 index 00000000..383f5bcf --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/PhysicalAstcBlockTests.cs @@ -0,0 +1,615 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Compression.Astc.ColorEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; + +public class PhysicalAstcBlockTests +{ + private static readonly UInt128 ErrorBlock = UInt128.Zero; + + [Fact] + public void Create_WithUInt64_ShouldRoundTripBlockBits() + { + const ulong expectedLow = 0x0000000001FE000173UL; + + PhysicalBlock block = PhysicalBlock.Create(expectedLow); + + Assert.Equal((UInt128)expectedLow, block.BlockBits); + } + + [Fact] + public void Create_WithUInt128_ShouldRoundTripBlockBits() + { + UInt128 expected = (UInt128)0x12345678ABCDEF00UL | ((UInt128)0xCAFEBABEDEADBEEFUL << 64); + + PhysicalBlock block = PhysicalBlock.Create(expected); + + Assert.Equal(expected, block.BlockBits); + } + + [Fact] + public void Create_WithMatchingUInt64AndUInt128_ShouldProduceIdenticalBlocks() + { + const ulong value = 0x0000000001FE000173UL; + + PhysicalBlock block1 = PhysicalBlock.Create(value); + PhysicalBlock block2 = PhysicalBlock.Create((UInt128)value); + + Assert.Equal(block2.BlockBits, block1.BlockBits); + } + + [Fact] + public void IsVoidExtent_WithKnownVoidExtentPattern_ShouldReturnTrue() + { + PhysicalBlock block = PhysicalBlock.Create((UInt128)0xFFFFFFFFFFFFFDFCUL); + + Assert.True(block.IsVoidExtent); + } + + [Fact] + public void IsVoidExtent_WithStandardBlock_ShouldReturnFalse() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000173UL); + + Assert.False(block.IsVoidExtent); + } + + [Fact] + public void IsVoidExtent_WithErrorBlock_ShouldReturnFalse() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + Assert.False(block.IsVoidExtent); + } + + [Fact] + public void GetVoidExtentCoordinates_WithValidVoidExtentBlock_ShouldReturnExpectedCoordinates() + { + PhysicalBlock block = PhysicalBlock.Create(0xFFF8003FFE000DFCUL); + + int[] coords = block.GetVoidExtentCoordinates(); + + Assert.NotNull(coords); + Assert.Equal(4, coords.Length); + Assert.Equal(0, coords![0]); + Assert.Equal(8191, coords[1]); + Assert.Equal(0, coords[2]); + Assert.Equal(8191, coords[3]); + } + + [Fact] + public void GetVoidExtentCoordinates_WithAllOnesPattern_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(0xFFFFFFFFFFFFFDFCUL); + + int[] coords = block.GetVoidExtentCoordinates(); + + Assert.True(block.IsVoidExtent); + Assert.Null(coords); + } + + [Fact] + public void Create_WithInvalidVoidExtentCoordinates_ShouldBeIllegalEncoding() + { + PhysicalBlock block1 = PhysicalBlock.Create(0x0008004002001DFCUL); + PhysicalBlock block2 = PhysicalBlock.Create(0x0007FFC001FFFDFCUL); + + Assert.True(block1.IsIllegalEncoding); + Assert.True(block2.IsIllegalEncoding); + } + + [Fact] + public void Create_WithModifiedHighBitsOnVoidExtent_ShouldStillBeValid() + { + PhysicalBlock original = PhysicalBlock.Create(0xFFF8003FFE000DFCUL, 0UL); + PhysicalBlock modified = PhysicalBlock.Create(0xFFF8003FFE000DFCUL, 0xdeadbeefdeadbeef); + + Assert.False(original.IsIllegalEncoding); + Assert.True(original.IsVoidExtent); + Assert.False(modified.IsIllegalEncoding); + Assert.True(modified.IsVoidExtent); + } + + [Fact] + public void GetWeightRange_WithValidBlock_ShouldReturn7() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000173UL); + + int? weightRange = block.GetWeightRange(); + + Assert.NotNull(weightRange); + Assert.Equal(7, weightRange); + } + + [Fact] + public void GetWeightRange_WithTooManyBits_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000373UL); + + int? weightRange = block.GetWeightRange(); + + Assert.Null(weightRange); + } + + [Fact] + public void GetWeightRange_WithOneBitPerWeight_ShouldReturn1() + { + PhysicalBlock block = PhysicalBlock.Create(0x4000000000800D44UL); + + int? weightRange = block.GetWeightRange(); + + Assert.NotNull(weightRange); + Assert.Equal(1, weightRange); + } + + [Fact] + public void GetWeightRange_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + int? weightRange = block.GetWeightRange(); + + Assert.Null(weightRange); + } + + [Fact] + public void GetWeightGridDimensions_WithValidBlock_ShouldReturn6x5() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000173UL); + + (int Width, int Height)? dims = block.GetWeightGridDimensions(); + + Assert.NotNull(dims); + Assert.Equal(6, dims!.Value.Width); + Assert.Equal(5, dims.Value.Height); + } + + [Fact] + public void GetWeightGridDimensions_WithTooManyBitsForGrid_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000373UL); + + (int Width, int Height)? dims = block.GetWeightGridDimensions(); + + Assert.Null(dims); + string error = block.IdentifyInvalidEncodingIssues(); + Assert.Contains("Invalid block encoding", error); + } + + [Fact] + public void GetWeightGridDimensions_WithDualPlaneBlock_ShouldReturn3x5() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE0005FFUL); + + (int Width, int Height)? dims = block.GetWeightGridDimensions(); + + Assert.NotNull(dims); + Assert.Equal(3, dims!.Value.Width); + Assert.Equal(5, dims.Value.Height); + } + + [Fact] + public void GetWeightGridDimensions_WithNonSharedCEM_ShouldReturn8x8() + { + PhysicalBlock block = PhysicalBlock.Create(0x4000000000800D44UL); + + (int Width, int Height)? dims = block.GetWeightGridDimensions(); + + Assert.NotNull(dims); + Assert.Equal(8, dims!.Value.Width); + Assert.Equal(8, dims.Value.Height); + } + + [Fact] + public void GetWeightGridDimensions_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + (int Width, int Height)? dims = block.GetWeightGridDimensions(); + + Assert.Null(dims); + } + + [Fact] + public void IsDualPlane_WithSinglePlaneBlock_ShouldReturnFalse() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000173UL); + + Assert.False(block.IsDualPlane); + } + + [Fact] + public void IsDualPlane_WithDualPlaneBlock_ShouldReturnTrue() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE0005FFUL); + + Assert.True(block.IsDualPlane); + } + + [Fact] + public void IsDualPlane_WithErrorBlock_ShouldReturnFalse() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + Assert.False(block.IsDualPlane); + } + + [Fact] + public void IsDualPlane_WithInvalidEncoding_ShouldReturnFalse() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000573UL); + + Assert.False(block.IsDualPlane); + Assert.Null(block.GetWeightGridDimensions()); + Assert.Contains("Invalid block encoding", block.IdentifyInvalidEncodingIssues()); + } + + [Fact] + public void IsDualPlane_WithValidSinglePlaneBlock_ShouldHaveValidEncoding() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000108UL); + + Assert.False(block.IsDualPlane); + Assert.False(block.IsIllegalEncoding); + } + + [Fact] + public void GetWeightBitCount_WithStandardBlock_ShouldReturn90() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000173UL); + + int? bitCount = block.GetWeightBitCount(); + + Assert.Equal(90, bitCount); + } + + [Fact] + public void GetWeightBitCount_WithDualPlaneBlock_ShouldReturn90() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE0005FFUL); + + int? bitCount = block.GetWeightBitCount(); + + Assert.Equal(90, bitCount); + } + + [Fact] + public void GetWeightBitCount_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + int? bitCount = block.GetWeightBitCount(); + + Assert.Null(bitCount); + } + + [Fact] + public void GetWeightBitCount_WithVoidExtent_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(0xFFF8003FFE000DFCUL); + + int? bitCount = block.GetWeightBitCount(); + + Assert.Null(bitCount); + } + + [Fact] + public void GetWeightBitCount_WithInvalidBlock_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000573UL); + + int? bitCount = block.GetWeightBitCount(); + + Assert.Null(bitCount); + } + + [Fact] + public void GetWeightStartBit_WithNonSharedCEM_ShouldReturn64() + { + PhysicalBlock block = PhysicalBlock.Create(0x4000000000800D44UL); + + int? startBit = block.GetWeightStartBit(); + + Assert.Equal(64, startBit); + } + + [Fact] + public void GetWeightStartBit_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + int? startBit = block.GetWeightStartBit(); + + Assert.Null(startBit); + } + + [Fact] + public void GetWeightStartBit_WithVoidExtent_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(0xFFF8003FFE000DFCUL); + + int? startBit = block.GetWeightStartBit(); + + Assert.Null(startBit); + } + + [Fact] + public void IsIllegalEncoding_WithValidBlocks_ShouldReturnFalse() + { + Assert.False(PhysicalBlock.Create(0x0000000001FE000173UL).IsIllegalEncoding); + Assert.False(PhysicalBlock.Create(0x0000000001FE0005FFUL).IsIllegalEncoding); + Assert.False(PhysicalBlock.Create(0x0000000001FE000108UL).IsIllegalEncoding); + } + + [Fact] + public void IdentifyInvalidEncodingIssues_WithZeroBlock_ShouldReturnReservedBlockModeError() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + string error = block.IdentifyInvalidEncodingIssues(); + + Assert.NotNull(error); + Assert.Contains("Invalid block encoding", error); + } + + [Fact] + public void IdentifyInvalidEncodingIssues_WithTooManyWeightBits_ShouldReturnError() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000573UL); + + string error = block.IdentifyInvalidEncodingIssues(); + + Assert.NotNull(error); + Assert.Contains("Invalid block encoding", error); + } + + [Theory] + [InlineData(0x0000000001FE0005A8UL)] + [InlineData(0x0000000001FE000588UL)] + [InlineData(0x0000000001FE00002UL)] + public void IdentifyInvalidEncodingIssues_WithInvalidBlocks_ShouldReturnError(ulong blockBits) + { + PhysicalBlock block = PhysicalBlock.Create(blockBits); + + string error = block.IdentifyInvalidEncodingIssues(); + + Assert.NotNull(error); + } + + [Fact] + public void IdentifyInvalidEncodingIssues_WithDualPlaneFourPartitions_ShouldReturnError() + { + PhysicalBlock block = PhysicalBlock.Create(0x000000000000001D1FUL); + + string error = block.IdentifyInvalidEncodingIssues(); + + Assert.Null(block.GetPartitionsCount()); + Assert.NotNull(error); + Assert.Contains("Invalid block encoding", error); + } + + [Theory] + [InlineData(0x000000000000000973UL)] + [InlineData(0x000000000000001173UL)] + [InlineData(0x000000000000001973UL)] + public void GetPartitionsCount_WithInvalidPartitionConfig_ShouldReturnNull(ulong blockBits) + { + PhysicalBlock block = PhysicalBlock.Create(blockBits); + + int? partitions = block.GetPartitionsCount(); + + Assert.Null(partitions); + } + + [Theory] + [InlineData(0x0000000001FE000173UL, 1)] + [InlineData(0x0000000001FE0005FFUL, 1)] + [InlineData(0x0000000001FE000108UL, 1)] + [InlineData(0x4000000000800D44UL, 2)] + public void GetPartitionsCount_WithValidBlock_ShouldReturnExpectedCount(ulong blockBits, int expectedCount) + { + PhysicalBlock block = PhysicalBlock.Create(blockBits); + + int? count = block.GetPartitionsCount(); + + Assert.Equal(expectedCount, count); + } + + [Theory] + [InlineData(0x4000000000FFED44UL, 0x3FF)] + [InlineData(0x4000000000AAAD44UL, 0x155)] + public void GetPartitionId_WithValidMultiPartitionBlock_ShouldReturnExpectedId(ulong blockBits, int expectedId) + { + PhysicalBlock block = PhysicalBlock.Create(blockBits); + + int? partitionId = block.GetPartitionId(); + + Assert.Equal(expectedId, partitionId); + } + + [Fact] + public void GetPartitionId_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + int? partitionId = block.GetPartitionId(); + + Assert.Null(partitionId); + } + + [Fact] + public void GetPartitionId_WithVoidExtent_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(0xFFF8003FFE000DFCUL); + + int? partitionId = block.GetPartitionId(); + + Assert.Null(partitionId); + } + + [Fact] + public void GetEndpointMode_WithFourPartitionBlock_ShouldReturnSameModeForAll() + { + PhysicalBlock block = PhysicalBlock.Create(0x000000000000001961UL); + + for (int i = 0; i < 4; ++i) + { + ColorEndpointMode? mode = block.GetEndpointMode(i); + Assert.Equal(ColorEndpointMode.LdrLumaDirect, mode); + } + } + + [Fact] + public void GetEndpointMode_WithNonSharedCEM_ShouldReturnDifferentModes() + { + PhysicalBlock block = PhysicalBlock.Create(0x4000000000800D44UL); + + ColorEndpointMode? mode0 = block.GetEndpointMode(0); + ColorEndpointMode? mode1 = block.GetEndpointMode(1); + + Assert.Equal(ColorEndpointMode.LdrLumaDirect, mode0); + Assert.Equal(ColorEndpointMode.LdrLumaBaseOffset, mode1); + } + + [Fact] + public void GetEndpointMode_WithVoidExtent_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(0xFFF8003FFE000DFCUL); + + ColorEndpointMode? mode = block.GetEndpointMode(0); + + Assert.Null(mode); + } + + [Theory] + [InlineData(1)] + [InlineData(-1)] + [InlineData(100)] + public void GetEndpointMode_WithInvalidPartitionIndex_ShouldReturnNull(int index) + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000173UL); + + ColorEndpointMode? mode = block.GetEndpointMode(index); + + Assert.Null(mode); + } + + [Fact] + public void GetColorValuesCount_WithStandardBlock_ShouldReturn2() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000173UL); + + int? count = block.GetColorValuesCount(); + + Assert.Equal(2, count); + } + + [Fact] + public void GetColorValuesCount_WithVoidExtent_ShouldReturn4() + { + PhysicalBlock block = PhysicalBlock.Create(0xFFF8003FFE000DFCUL); + + int? count = block.GetColorValuesCount(); + + Assert.Equal(4, count); + } + + [Fact] + public void GetColorValuesCount_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + int? count = block.GetColorValuesCount(); + + Assert.Null(count); + } + + [Fact] + public void GetColorBitCount_WithStandardBlock_ShouldReturn16() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000173UL); + + int? bitCount = block.GetColorBitCount(); + + Assert.Equal(16, bitCount); + } + + [Fact] + public void GetColorBitCount_WithVoidExtent_ShouldReturn64() + { + PhysicalBlock block = PhysicalBlock.Create(0xFFF8003FFE000DFCUL); + + int? bitCount = block.GetColorBitCount(); + + Assert.Equal(64, bitCount); + } + + [Fact] + public void GetColorBitCount_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + int? bitCount = block.GetColorBitCount(); + + Assert.Null(bitCount); + } + + [Fact] + public void GetColorValuesRange_WithStandardBlock_ShouldReturn255() + { + PhysicalBlock block = PhysicalBlock.Create(0x0000000001FE000173UL); + + int? range = block.GetColorValuesRange(); + + Assert.Equal(255, range); + } + + [Fact] + public void GetColorValuesRange_WithVoidExtent_ShouldReturnMaxUInt16() + { + PhysicalBlock block = PhysicalBlock.Create(0xFFF8003FFE000DFCUL); + + int? range = block.GetColorValuesRange(); + + Assert.Equal((1 << 16) - 1, range); + } + + [Fact] + public void GetColorValuesRange_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + int? range = block.GetColorValuesRange(); + + Assert.Null(range); + } + + [Theory] + [InlineData(0x0000000001FE000173UL, 17)] + [InlineData(0x0000000001FE0005FFUL, 17)] + [InlineData(0x0000000001FE000108UL, 17)] + [InlineData(0x4000000000FFED44UL, 29)] + [InlineData(0x4000000000AAAD44UL, 29)] + [InlineData(0xFFF8003FFE000DFCUL, 64)] + public void GetColorStartBit_WithVariousBlocks_ShouldReturnExpectedValue(ulong blockBits, int expectedStartBit) + { + PhysicalBlock block = PhysicalBlock.Create(blockBits); + + int? startBit = block.GetColorStartBit(); + + Assert.Equal(expectedStartBit, startBit); + } + + [Fact] + public void GetColorStartBit_WithErrorBlock_ShouldReturnNull() + { + PhysicalBlock block = PhysicalBlock.Create(ErrorBlock); + + int? startBit = block.GetColorStartBit(); + + Assert.Null(startBit); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/QuantizationTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/QuantizationTests.cs new file mode 100644 index 00000000..5f97230b --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/QuantizationTests.cs @@ -0,0 +1,399 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding.Quantize; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; + +public class QuantizationTests +{ + [Fact] + public void QuantizeCEValueToRange_WithMaxValue_ShouldNotExceedRange() + { + for (int range = Quantization.EndpointRangeMinValue; range <= byte.MaxValue; range++) + { + Assert.True(Quantization.QuantizeCEValueToRange(byte.MaxValue, range) <= range); + } + } + + [Fact] + public void QuantizeWeightToRange_WithMaxValue_ShouldNotExceedRange() + { + for (int range = 1; range < Quantization.WeightRangeMaxValue; range++) + { + Assert.True(Quantization.QuantizeWeightToRange(64, range) <= range); + } + } + + [Fact] + public void QuantizeCEValueToRange_WithVariousValues_ShouldNotExceedRange() + { + int[] ranges = BoundedIntegerSequenceCodec.MaxRanges; + int[] testValues = [0, 4, 15, 22, 66, 91, 126]; + + foreach (int range in ranges.Where(r => r >= Quantization.EndpointRangeMinValue)) + { + foreach (int value in testValues) + { + Assert.True(Quantization.QuantizeCEValueToRange(value, range) <= range); + } + } + } + + [Fact] + public void QuantizeWeightToRange_WithVariousValues_ShouldNotExceedRange() + { + int[] ranges = BoundedIntegerSequenceCodec.MaxRanges; + int[] testValues = [0, 4, 15, 22]; + + foreach (int range in ranges.Where(r => r <= Quantization.WeightRangeMaxValue)) + { + foreach (int value in testValues) + { + Assert.True(Quantization.QuantizeWeightToRange(value, range) <= range); + } + } + } + + [Fact] + public void QuantizeWeight_ThenUnquantize_ShouldReturnOriginalQuantizedValue() + { + int[] ranges = BoundedIntegerSequenceCodec.MaxRanges; + + foreach (int range in ranges.Where(r => r <= Quantization.WeightRangeMaxValue)) + { + for (int quantizedValue = 0; quantizedValue <= range; ++quantizedValue) + { + int unquantized = Quantization.UnquantizeWeightFromRange(quantizedValue, range); + int requantized = Quantization.QuantizeWeightToRange(unquantized, range); + + Assert.Equal(quantizedValue, requantized); + } + } + } + + [Fact] + public void QuantizeCEValue_ThenUnquantize_ShouldReturnOriginalQuantizedValue() + { + int[] ranges = BoundedIntegerSequenceCodec.MaxRanges; + + foreach (int range in ranges.Where(r => r >= Quantization.EndpointRangeMinValue)) + { + for (int quantizedValue = 0; quantizedValue <= range; ++quantizedValue) + { + int unquantized = Quantization.UnquantizeCEValueFromRange(quantizedValue, range); + int requantized = Quantization.QuantizeCEValueToRange(unquantized, range); + + Assert.Equal(quantizedValue, requantized); + } + } + } + + [Theory] + [InlineData(2, 7)] + [InlineData(7, 7)] + [InlineData(39, 63)] + [InlineData(66, 79)] + [InlineData(91, 191)] + [InlineData(126, 255)] + [InlineData(255, 255)] + public void UnquantizeCEValueFromRange_ShouldProduceValidByteValue(int quantizedValue, int range) + { + int result = Quantization.UnquantizeCEValueFromRange(quantizedValue, range); + + Assert.True(result < 256); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(2, 7)] + [InlineData(7, 7)] + [InlineData(29, 31)] + public void UnquantizeWeightFromRange_ShouldNotExceed64(int quantizedValue, int range) + { + int result = Quantization.UnquantizeWeightFromRange(quantizedValue, range); + + Assert.True(result <= 64); + } + + [Fact] + public void Quantize_WithDesiredRange_ShouldMatchExpectedRangeOutput() + { + int[] ranges = BoundedIntegerSequenceCodec.MaxRanges; + int rangeIndex = 0; + + for (int desiredRange = 1; desiredRange <= byte.MaxValue; ++desiredRange) + { + while (rangeIndex + 1 < ranges.Length && ranges[rangeIndex + 1] <= desiredRange) + { + ++rangeIndex; + } + + int expectedRange = ranges[rangeIndex]; + + // Test CE values + if (desiredRange >= Quantization.EndpointRangeMinValue) + { + int[] testValues = [0, 13, 173, 208, 255]; + foreach (int value in testValues) + { + Assert.Equal( + Quantization.QuantizeCEValueToRange(value, expectedRange), + Quantization.QuantizeCEValueToRange(value, desiredRange)); + } + } + + // Test weight values + if (desiredRange <= Quantization.WeightRangeMaxValue) + { + int[] testValues = [0, 12, 23, 63]; + foreach (int value in testValues) + { + Assert.Equal( + Quantization.QuantizeWeightToRange(value, expectedRange), + Quantization.QuantizeWeightToRange(value, desiredRange)); + } + } + } + + Assert.Equal(ranges.Length - 1, rangeIndex); + } + + [Fact] + public void QuantizeCEValueToRange_WithRangeByteMax_ShouldBeIdentity() + { + for (int value = byte.MinValue; value <= byte.MaxValue; value++) + { + Assert.Equal(value, Quantization.QuantizeCEValueToRange(value, byte.MaxValue)); + } + } + + [Fact] + public void QuantizeCEValueToRange_ShouldBeMonotonicIncreasing() + { + for (int numBits = 3; numBits < 8; numBits++) + { + int range = (1 << numBits) - 1; + int lastQuantizedValue = -1; + + for (int value = byte.MinValue; value <= byte.MaxValue; value++) + { + int quantizedValue = Quantization.QuantizeCEValueToRange(value, range); + + Assert.True(quantizedValue >= lastQuantizedValue); + lastQuantizedValue = quantizedValue; + } + + Assert.Equal(range, lastQuantizedValue); + } + } + + [Fact] + public void QuantizeWeightToRange_ShouldBeMonotonicallyIncreasing() + { + for (int numBits = 3; numBits < 8; ++numBits) + { + int range = (1 << numBits) - 1; + + if (range > Quantization.WeightRangeMaxValue) + { + continue; + } + + int lastQuantizedValue = -1; + + for (int value = 0; value <= 64; ++value) + { + int quantizedValue = Quantization.QuantizeWeightToRange(value, range); + + Assert.True(quantizedValue >= lastQuantizedValue); + lastQuantizedValue = quantizedValue; + } + + Assert.Equal(range, lastQuantizedValue); + } + } + + [Fact] + public void QuantizeCEValueToRange_WithSmallBitRanges_ShouldQuantizeLowValuesToZero() + { + for (int numBits = 1; numBits <= 8; ++numBits) + { + int range = (1 << numBits) - 1; + + if (range < Quantization.EndpointRangeMinValue) + { + continue; + } + + const int cevBits = 8; + int halfMaxQuantBits = Math.Max(0, cevBits - numBits - 1); + int largestCevToZero = (1 << halfMaxQuantBits) - 1; + + Assert.Equal(0, Quantization.QuantizeCEValueToRange(largestCevToZero, range)); + } + } + + [Fact] + public void QuantizeWeightToRange_WithSmallBitRanges_ShouldQuantizeLowValuesToZero() + { + for (int numBits = 1; numBits <= 8; numBits++) + { + int range = (1 << numBits) - 1; + + if (range > Quantization.WeightRangeMaxValue) + { + continue; + } + + const int weightBits = 6; + int halfMaxQuantBits = Math.Max(0, weightBits - numBits - 1); + int largestWeightToZero = (1 << halfMaxQuantBits) - 1; + + Assert.Equal(0, Quantization.QuantizeWeightToRange(largestWeightToZero, range)); + } + } + + [Fact] + public void UnquantizeWeightFromRange_WithQuintRange_ShouldMatchExpected() + { + List values = [4, 6, 4, 6, 7, 5, 7, 5]; + List quintExpected = [14, 21, 14, 21, 43, 50, 43, 50]; + + List quantized = [.. values.Select(v => Quantization.UnquantizeWeightFromRange(v, 9))]; + + Assert.Equal(quintExpected, quantized); + } + + [Fact] + public void UnquantizeWeightFromRange_WithTritRange_ShouldMatchExpected() + { + List values = [4, 6, 4, 6, 7, 5, 7, 5]; + List tritExpected = [5, 23, 5, 23, 41, 59, 41, 59]; + + List quantized = [.. values.Select(v => Quantization.UnquantizeWeightFromRange(v, 11))]; + + Assert.Equal(tritExpected, quantized); + } + + [Fact] + public void QuantizeCEValueToRange_WithInvalidMinRange_ShouldThrowArgumentOutOfRangeException() + { + for (int range = 0; range < Quantization.EndpointRangeMinValue; range++) + { + Action action = () => Quantization.QuantizeCEValueToRange(0, range); + Assert.Throws(action); + } + } + + [Fact] + public void UnquantizeCEValueFromRange_WithInvalidMinRange_ShouldThrowArgumentOutOfRangeException() + { + for (int range = 0; range < Quantization.EndpointRangeMinValue; range++) + { + Action action = () => Quantization.UnquantizeCEValueFromRange(0, range); + Assert.Throws(action); + } + } + + [Fact] + public void QuantizeWeightToRange_WithZeroRange_ShouldThrowArgumentOutOfRangeException() + { + Action action = () => Quantization.QuantizeWeightToRange(0, 0); + + Assert.Throws(action); + } + + [Fact] + public void UnquantizeWeightFromRange_WithZeroRange_ShouldThrowArgumentOutOfRangeException() + { + Action action = () => Quantization.UnquantizeWeightFromRange(0, 0); + + Assert.Throws(action); + } + + [Theory] + [InlineData(-1, 10)] + [InlineData(256, 7)] + [InlineData(10000, 17)] + public void QuantizeCEValueToRange_WithInvalidValue_ShouldThrowArgumentOutOfRangeException(int value, int range) + { + Action action = () => Quantization.QuantizeCEValueToRange(value, range); + + Assert.Throws(action); + } + + [Theory] + [InlineData(-1, 10)] + [InlineData(8, 7)] + [InlineData(-1000, 17)] + public void UnquantizeCEValueFromRange_WithInvalidValue_ShouldThrowArgumentOutOfRangeException(int value, int range) + { + Action action = () => Quantization.UnquantizeCEValueFromRange(value, range); + + Assert.Throws(action); + } + + [Theory] + [InlineData(0, -7)] + [InlineData(0, 257)] + public void QuantizeCEValueToRange_WithInvalidRange_ShouldThrowArgumentOutOfRangeException(int value, int range) + { + Action action = () => Quantization.QuantizeCEValueToRange(value, range); + + Assert.Throws(action); + } + + [Theory] + [InlineData(0, -17)] + [InlineData(0, 256)] + public void UnquantizeCEValueFromRange_WithInvalidRange_ShouldThrowArgumentOutOfRangeException(int value, int range) + { + Action action = () => Quantization.UnquantizeCEValueFromRange(value, range); + + Assert.Throws(action); + } + + [Theory] + [InlineData(-1, 10)] + [InlineData(256, 7)] + [InlineData(10000, 17)] + public void QuantizeWeightToRange_WithInvalidValue_ShouldThrowArgumentOutOfRangeException(int value, int range) + { + Action action = () => Quantization.QuantizeWeightToRange(value, range); + + Assert.Throws(action); + } + + [Theory] + [InlineData(-1, 10)] + [InlineData(8, 7)] + [InlineData(-1000, 17)] + public void UnquantizeWeightFromRange_WithInvalidValue_ShouldThrowArgumentOutOfRangeException(int value, int range) + { + Action action = () => Quantization.UnquantizeWeightFromRange(value, range); + + Assert.Throws(action); + } + + [Theory] + [InlineData(0, -7)] + [InlineData(0, 32)] + public void QuantizeWeightToRange_WithInvalidRange_ShouldThrowArgumentOutOfRangeException(int value, int range) + { + Action action = () => Quantization.QuantizeWeightToRange(value, range); + + Assert.Throws(action); + } + + [Theory] + [InlineData(0, -17)] + [InlineData(0, 64)] + public void UnquantizeWeightFromRange_WithInvalidRange_ShouldThrowArgumentOutOfRangeException(int value, int range) + { + Action action = () => Quantization.UnquantizeWeightFromRange(value, range); + + Assert.Throws(action); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/WeightInfillTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/WeightInfillTests.cs new file mode 100644 index 00000000..300da335 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/WeightInfillTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Compression.Astc.BiseEncoding; +using SixLabors.ImageSharp.Textures.Compression.Astc.Core; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; + +public class WeightInfillTests +{ + [Theory] + [InlineData(4, 4, 3, 32)] + [InlineData(4, 4, 7, 48)] + [InlineData(2, 4, 7, 24)] + [InlineData(2, 4, 1, 8)] + [InlineData(4, 5, 2, 32)] + [InlineData(4, 4, 2, 26)] + [InlineData(4, 5, 5, 52)] + [InlineData(4, 4, 5, 42)] + [InlineData(3, 3, 4, 21)] + [InlineData(4, 4, 4, 38)] + [InlineData(3, 7, 4, 49)] + [InlineData(4, 3, 19, 52)] + [InlineData(4, 4, 19, 70)] + public void CountBitsForWeights_WithVariousParameters_ShouldReturnCorrectBitCount( + int width, int height, int range, int expectedBitCount) + { + int bitCount = BoundedIntegerSequenceCodec.GetBitCountForRange(width * height, range); + + Assert.Equal(expectedBitCount, bitCount); + } + + [Fact] + public void InfillWeights_With3x3Grid_ShouldBilinearlyInterpolateTo5x5() + { + int[] weights = [1, 3, 5, 3, 5, 7, 5, 7, 9]; + int[] expected = [1, 2, 3, 4, 5, 2, 3, 4, 5, 6, 3, 4, 5, 6, 7, 4, 5, 6, 7, 8, 5, 6, 7, 8, 9]; + + Footprint footprint = Footprint.Get5x5(); + DecimationInfo di = DecimationTable.Get(footprint, 3, 3); + int[] result = new int[footprint.PixelCount]; + DecimationTable.InfillWeights(weights, di, result); + + Assert.Equal(expected.Length, result.Length); + Assert.Equal(expected, result); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs index 71482d92..fbad9f5b 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs @@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Textures.Tests.Formats.Ktx; +[GroupOutput("Ktx")] [Trait("Format", "Ktx")] [Trait("Format", "Astc")] public class KtxAstcDecoderTests @@ -19,7 +20,7 @@ public class KtxAstcDecoderTests [Theory] [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Astc.Rgb32_8x8)] - public void KtxAstcDecoder_CanDecode_Rgba32_Blocksizes(TestTextureProvider provider) + public void CanDecode_Rgba32_Blocksizes(TestTextureProvider provider) { using Texture texture = provider.GetTexture(KtxDecoder); provider.SaveTextures(texture); diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderTests.cs index 1fbacc67..02dff1a2 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderTests.cs @@ -12,6 +12,7 @@ namespace SixLabors.ImageSharp.Textures.Tests.Formats.Ktx { + [GroupOutput("Ktx")] [Trait("Format", "Ktx")] public class KtxDecoderTests { @@ -19,7 +20,7 @@ public class KtxDecoderTests [Theory] [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.PvrTexToolCli, TestImages.Ktx.Rgba)] - public void KtxDecoder_CanDecode_Rgba8888(TestTextureProvider provider) + public void CanDecode_Rgba8888(TestTextureProvider provider) { using Texture texture = provider.GetTexture(KtxDecoder); provider.SaveTextures(texture); diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs index 63e8d375..38342e4b 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs @@ -12,6 +12,7 @@ namespace SixLabors.ImageSharp.Textures.Tests.Formats.Ktx2; +[GroupOutput("Ktx2")] [Trait("Format", "Ktx2")] [Trait("Format", "Astc")] public class Ktx2AstcDecoderCubemapTests @@ -20,7 +21,7 @@ public class Ktx2AstcDecoderCubemapTests [Theory] [WithFile(TestTextureFormat.Ktx2, TestTextureType.Cubemap, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Srgb_6x6_Cube)] - public void Ktx2AstcDecoder_CanDecode_All_Faces(TestTextureProvider provider) + public void CanDecode_All_Faces(TestTextureProvider provider) { using Texture texture = provider.GetTexture(KtxDecoder); provider.SaveTextures(texture); diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderFlatTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderFlatTests.cs index b8efc920..7281bf0a 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderFlatTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderFlatTests.cs @@ -13,6 +13,7 @@ namespace SixLabors.ImageSharp.Textures.Tests.Formats.Ktx2; +[GroupOutput("Ktx2")] [Trait("Format", "Ktx2")] [Trait("Format", "Astc")] public partial class Ktx2AstcDecoderFlatTests @@ -34,8 +35,9 @@ public partial class Ktx2AstcDecoderFlatTests [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) + public void CanDecode_Rgba32_Blocksizes(TestTextureProvider provider) { + string blockSize = GetBlockSizeFromFileName(provider.InputFile); using Texture texture = provider.GetTexture(KtxDecoder); provider.SaveTextures(texture); FlatTexture flatTexture = texture as FlatTexture; @@ -50,10 +52,7 @@ public void Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes(TestTextureProvider prov 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); + firstMipMapImage.CompareToReferenceOutput(ImageComparer.Exact, provider, testOutputDetails: blockSize); } [Theory] @@ -71,7 +70,7 @@ public void Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes(TestTextureProvider prov [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) + public void CanDecode_Rgba32_Unorm(TestTextureProvider provider) { string blockSize = GetBlockSizeFromFileName(provider.InputFile); using Texture texture = provider.GetTexture(KtxDecoder); @@ -104,7 +103,7 @@ public void Ktx2AstcDecoder_CanDecode_Rgba32_Unorm(TestTextureProvider provider) [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) + public void CanDecode_Rgba32_Srgb(TestTextureProvider provider) { string blockSize = GetBlockSizeFromFileName(provider.InputFile); using Texture texture = provider.GetTexture(KtxDecoder); @@ -124,7 +123,7 @@ public void Ktx2AstcDecoder_CanDecode_Rgba32_Srgb(TestTextureProvider provider) [Theory] [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Srgb_Large)] - public void Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_Large(TestTextureProvider provider) + public void CanDecode_Rgba32_Srgb_Large(TestTextureProvider provider) { using Texture texture = provider.GetTexture(KtxDecoder); provider.SaveTextures(texture); @@ -140,7 +139,7 @@ public void Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_Large(TestTextureProvider prov [Theory] [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Rgb32_Srgb_6x6_MipMap)] - public void Ktx2AstcDecoder_CanDecode_MipMaps(TestTextureProvider provider) + public void CanDecode_MipMaps(TestTextureProvider provider) { int mimMapLevel = 0; @@ -160,7 +159,7 @@ public void Ktx2AstcDecoder_CanDecode_MipMaps(TestTextureProvider provider) [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) + public void CanDecode_Rgba32_Supercompressed(TestTextureProvider provider) { string fileName = Path.GetFileNameWithoutExtension(provider.InputFile); string compressionDetails = fileName.Contains("ZLIB", StringComparison.Ordinal) diff --git a/tests/ImageSharp.Textures.Tests/TestImages.cs b/tests/ImageSharp.Textures.Tests/TestImages.cs index c405b47a..60b01b47 100644 --- a/tests/ImageSharp.Textures.Tests/TestImages.cs +++ b/tests/ImageSharp.Textures.Tests/TestImages.cs @@ -8,6 +8,84 @@ namespace SixLabors.ImageSharp.Textures.Tests; /// public static class TestImages { + public static class Astc + { + public const string Atlas_Small_4x4 = "Astc/atlas_small_4x4.astc"; + public const string Atlas_Small_5x5 = "Astc/atlas_small_5x5.astc"; + public const string Atlas_Small_6x6 = "Astc/atlas_small_6x6.astc"; + public const string Atlas_Small_8x8 = "Astc/atlas_small_8x8.astc"; + + public const string Checkerboard = "Astc/checkerboard.astc"; + + public const string Checkered_4 = "Astc/checkered_4.astc"; + public const string Checkered_5 = "Astc/checkered_5.astc"; + public const string Checkered_6 = "Astc/checkered_6.astc"; + public const string Checkered_7 = "Astc/checkered_7.astc"; + public const string Checkered_8 = "Astc/checkered_8.astc"; + public const string Checkered_9 = "Astc/checkered_9.astc"; + public const string Checkered_10 = "Astc/checkered_10.astc"; + public const string Checkered_11 = "Astc/checkered_11.astc"; + public const string Checkered_12 = "Astc/checkered_12.astc"; + + public const string Footprint_4x4 = "Astc/footprint_4x4.astc"; + public const string Footprint_5x4 = "Astc/footprint_5x4.astc"; + public const string Footprint_5x5 = "Astc/footprint_5x5.astc"; + public const string Footprint_6x5 = "Astc/footprint_6x5.astc"; + public const string Footprint_6x6 = "Astc/footprint_6x6.astc"; + public const string Footprint_8x5 = "Astc/footprint_8x5.astc"; + public const string Footprint_8x6 = "Astc/footprint_8x6.astc"; + public const string Footprint_8x8 = "Astc/footprint_8x8.astc"; + public const string Footprint_10x5 = "Astc/footprint_10x5.astc"; + public const string Footprint_10x6 = "Astc/footprint_10x6.astc"; + public const string Footprint_10x8 = "Astc/footprint_10x8.astc"; + public const string Footprint_10x10 = "Astc/footprint_10x10.astc"; + public const string Footprint_12x10 = "Astc/footprint_12x10.astc"; + public const string Footprint_12x12 = "Astc/footprint_12x12.astc"; + + public const string Rgb_4x4 = "Astc/rgb_4x4.astc"; + public const string Rgb_5x4 = "Astc/rgb_5x4.astc"; + public const string Rgb_6x6 = "Astc/rgb_6x6.astc"; + public const string Rgb_8x8 = "Astc/rgb_8x8.astc"; + public const string Rgb_12x12 = "Astc/rgb_12x12.astc"; + + public static class Expected + { + public const string Atlas_Small_4x4 = "Astc/Expected/atlas_small_4x4.bmp"; + public const string Atlas_Small_5x5 = "Astc/Expected/atlas_small_5x5.bmp"; + public const string Atlas_Small_6x6 = "Astc/Expected/atlas_small_6x6.bmp"; + public const string Atlas_Small_8x8 = "Astc/Expected/atlas_small_8x8.bmp"; + + public const string Footprint_4x4 = "Astc/Expected/footprint_4x4.bmp"; + public const string Footprint_5x4 = "Astc/Expected/footprint_5x4.bmp"; + public const string Footprint_5x5 = "Astc/Expected/footprint_5x5.bmp"; + public const string Footprint_6x5 = "Astc/Expected/footprint_6x5.bmp"; + public const string Footprint_6x6 = "Astc/Expected/footprint_6x6.bmp"; + public const string Footprint_8x5 = "Astc/Expected/footprint_8x5.bmp"; + public const string Footprint_8x6 = "Astc/Expected/footprint_8x6.bmp"; + public const string Footprint_8x8 = "Astc/Expected/footprint_8x8.bmp"; + public const string Footprint_10x5 = "Astc/Expected/footprint_10x5.bmp"; + public const string Footprint_10x6 = "Astc/Expected/footprint_10x6.bmp"; + public const string Footprint_10x8 = "Astc/Expected/footprint_10x8.bmp"; + public const string Footprint_10x10 = "Astc/Expected/footprint_10x10.bmp"; + public const string Footprint_12x10 = "Astc/Expected/footprint_12x10.bmp"; + public const string Footprint_12x12 = "Astc/Expected/footprint_12x12.bmp"; + + public const string Rgb_4x4 = "Astc/Expected/rgb_4x4.bmp"; + public const string Rgb_5x4 = "Astc/Expected/rgb_5x4.bmp"; + public const string Rgb_6x6 = "Astc/Expected/rgb_6x6.bmp"; + public const string Rgb_8x8 = "Astc/Expected/rgb_8x8.bmp"; + public const string Rgb_12x12 = "Astc/Expected/rgb_12x12.bmp"; + } + + public static class Hdr + { + public const string Hdr_A_1x1 = "Astc/HDR/HDR-A-1x1.astc"; + public const string Ldr_A_1x1 = "Astc/HDR/LDR-A-1x1.astc"; + public const string Hdr_Tile = "Astc/HDR/hdr-tile.astc"; + public const string Ldr_Tile = "Astc/HDR/ldr-tile.astc"; + } + } + public static class Ktx { public const string Rgba = "rgba8888.ktx"; @@ -23,66 +101,66 @@ public static class Ktx2 public static class Astc { // 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"; + public const string Rgba32_4x4 = "Flat/Astc/astc_rgba32_4x4.ktx2"; + public const string Rgba32_5x4 = "Flat/Astc/astc_rgba32_5x4.ktx2"; + public const string Rgba32_5x5 = "Flat/Astc/astc_rgba32_5x5.ktx2"; + public const string Rgba32_6x5 = "Flat/Astc/astc_rgba32_6x5.ktx2"; + public const string Rgba32_6x6 = "Flat/Astc/astc_rgba32_6x6.ktx2"; + public const string Rgba32_8x5 = "Flat/Astc/astc_rgba32_8x5.ktx2"; + public const string Rgba32_8x6 = "Flat/Astc/astc_rgba32_8x6.ktx2"; + public const string Rgba32_8x8 = "Flat/Astc/astc_rgba32_8x8.ktx2"; + public const string Rgba32_10x5 = "Flat/Astc/astc_rgba32_10x5.ktx2"; + public const string Rgba32_10x6 = "Flat/Astc/astc_rgba32_10x6.ktx2"; + public const string Rgba32_10x8 = "Flat/Astc/astc_rgba32_10x8.ktx2"; + public const string Rgba32_10x10 = "Flat/Astc/astc_rgba32_10x10.ktx2"; + public const string Rgba32_12x10 = "Flat/Astc/astc_rgba32_12x10.ktx2"; + public const string Rgba32_12x12 = "Flat/Astc/astc_rgba32_12x12.ktx2"; + + public const string Rgb32_sRgb_4x4 = "Flat/Astc/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_5x4 = "Flat/Astc/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_5x5 = "Flat/Astc/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_6x5 = "Flat/Astc/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_6x6 = "Flat/Astc/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_8x5 = "Flat/Astc/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_8x6 = "Flat/Astc/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_8x8 = "Flat/Astc/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_10x5 = "Flat/Astc/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_10x6 = "Flat/Astc/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_10x8 = "Flat/Astc/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_10x10 = "Flat/Astc/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_12x10 = "Flat/Astc/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2"; + public const string Rgb32_sRgb_12x12 = "Flat/Astc/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2"; + + public const string Rgb32_Unorm_4x4 = "Flat/Astc/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_5x4 = "Flat/Astc/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_5x5 = "Flat/Astc/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_6x5 = "Flat/Astc/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_6x6 = "Flat/Astc/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_8x5 = "Flat/Astc/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_8x6 = "Flat/Astc/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_8x8 = "Flat/Astc/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_10x5 = "Flat/Astc/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_10x6 = "Flat/Astc/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_10x8 = "Flat/Astc/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_10x10 = "Flat/Astc/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_12x10 = "Flat/Astc/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2"; + public const string Rgb32_Unorm_12x12 = "Flat/Astc/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2"; + + public const string Rgb32_Srgb_Large = "Flat/Astc/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"; + public const string Rgb32_Srgb_6x6_MipMap = "Flat/Astc/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"; + public const string Rgb32_Unorm_4x4_Zlib1 = "Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2"; + public const string Rgb32_Unorm_4x4_Zlib9 = "Flat/Astc/Supercompressed/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"; + public const string Rgb32_Unorm_4x4_Zstd1 = "Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2"; + public const string Rgb32_Unorm_4x4_Zstd9 = "Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2"; // Cubemap textures - public const string Rgb32_Srgb_6x6_Cube = "astc_ldr_cubemap_6x6.ktx2"; + public const string Rgb32_Srgb_6x6_Cube = "Cubemap/Astc/astc_ldr_cubemap_6x6.ktx2"; } } } diff --git a/tests/ImageSharp.Textures.Tests/TestUtilities/Attributes/WithFileAttribute.cs b/tests/ImageSharp.Textures.Tests/TestUtilities/Attributes/WithFileAttribute.cs index b8eed784..54b906c4 100644 --- a/tests/ImageSharp.Textures.Tests/TestUtilities/Attributes/WithFileAttribute.cs +++ b/tests/ImageSharp.Textures.Tests/TestUtilities/Attributes/WithFileAttribute.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; using SixLabors.ImageSharp.Textures.Tests.Enums; using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; using Xunit.Sdk; @@ -19,37 +18,55 @@ public class WithFileAttribute : DataAttribute private readonly TestTextureType textureType; private readonly TestTextureTool textureTool; private readonly string inputFile; - private readonly bool isRegex; - public WithFileAttribute(TestTextureFormat textureFormat, TestTextureType textureType, TestTextureTool textureTool, string inputFile, bool isRegex = false) + public WithFileAttribute(TestTextureFormat textureFormat, TestTextureType textureType, TestTextureTool textureTool, string inputFile) { this.textureFormat = textureFormat; this.textureType = textureType; this.textureTool = textureTool; this.inputFile = inputFile; - this.isRegex = isRegex; } public override IEnumerable GetData(MethodInfo testMethod) { ArgumentNullException.ThrowIfNull(testMethod); + string outputSubfolderName = testMethod.DeclaringType?.GetCustomAttribute()?.Subfolder ?? string.Empty; + string testGroupName = testMethod.DeclaringType?.Name ?? string.Empty; + string[] featureLevels = this.textureTool == TestTextureTool.TexConv ? new[] { "9.1", "9.2", "9.3", "10.0", "10.1", "11.0", "11.1", "12.0", "12.1" } : new[] { string.Empty }; foreach (string featureLevel in featureLevels) { - string path = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.textureFormat.ToString()); + string basePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.textureFormat.ToString()); if (!string.IsNullOrEmpty(featureLevel)) { - path = Path.Combine(path, featureLevel); + basePath = Path.Combine(basePath, featureLevel); } - string[] files = Directory.GetFiles(path); - string[] filteredFiles = files.Where(f => this.isRegex ? new Regex(this.inputFile).IsMatch(Path.GetFileName(f)) : Path.GetFileName(f).Equals(this.inputFile, StringComparison.OrdinalIgnoreCase)).ToArray(); - foreach (string file in filteredFiles) + if (!Directory.Exists(basePath)) + { + continue; + } + + // First try direct path construction (handles subdirectory paths like "Flat/Astc/file.ktx2"). + string file = Path.Combine(basePath, this.inputFile); + if (File.Exists(file)) + { + TestTextureProvider testTextureProvider = new(testMethod.Name, this.textureFormat, this.textureType, this.textureTool, file, false, testGroupName, outputSubfolderName); + yield return new object[] { testTextureProvider }; + continue; + } + + // Fall back to case-insensitive filename matching to handle + // cross-platform casing differences (e.g. ".DDS" vs ".dds"). + string match = Directory.GetFiles(basePath) + .FirstOrDefault(f => Path.GetFileName(f).Equals(this.inputFile, StringComparison.OrdinalIgnoreCase)); + + if (match is not null) { - var testTextureProvider = new TestTextureProvider(testMethod.Name, this.textureFormat, this.textureType, this.textureTool, file, false); + TestTextureProvider testTextureProvider = new(testMethod.Name, this.textureFormat, this.textureType, this.textureTool, match, false, testGroupName, outputSubfolderName); yield return new object[] { testTextureProvider }; } } diff --git a/tests/ImageSharp.Textures.Tests/TestUtilities/TextureProviders/TestTextureProvider.cs b/tests/ImageSharp.Textures.Tests/TestUtilities/TextureProviders/TestTextureProvider.cs index 372164db..2a80934b 100644 --- a/tests/ImageSharp.Textures.Tests/TestUtilities/TextureProviders/TestTextureProvider.cs +++ b/tests/ImageSharp.Textures.Tests/TestUtilities/TextureProviders/TestTextureProvider.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Globalization; using System.IO; using System.Text; -using System.Globalization; using SixLabors.ImageSharp.Textures.Formats; using SixLabors.ImageSharp.Textures.Tests.Enums; using SixLabors.ImageSharp.Textures.TextureFormats; @@ -48,7 +48,9 @@ public TestTextureProvider( TestTextureType textureType, TestTextureTool textureTool, string inputFile, - bool isRegex) + bool isRegex, + string testGroupName = "", + string outputSubfolderName = "") { this.MethodName = methodName; this.TextureFormat = textureFormat; @@ -59,8 +61,8 @@ public TestTextureProvider( this.Utility = new ImagingTestCaseUtility { SourceFileOrDescription = inputFile, - TestName = methodName }; + this.Utility.Init(testGroupName, methodName, outputSubfolderName); } private void SaveMipMaps(MipMap[] mipMaps, string name) diff --git a/tests/ImageSharp.Textures.Tests/TextureFormats/Decoding/AstcDecoderTests.cs b/tests/ImageSharp.Textures.Tests/TextureFormats/Decoding/AstcDecoderTests.cs index f98efec7..8d885ef9 100644 --- a/tests/ImageSharp.Textures.Tests/TextureFormats/Decoding/AstcDecoderTests.cs +++ b/tests/ImageSharp.Textures.Tests/TextureFormats/Decoding/AstcDecoderTests.cs @@ -327,5 +327,4 @@ public void DecompressImage_WithExtraBlockData_ReturnExpectedSize() Assert.Equal(256 * 256 * 4, result.Length); } - } diff --git a/tests/Images/Input/Astc/HDR/HDR-A-1x1.astc b/tests/Images/Input/Astc/HDR/HDR-A-1x1.astc new file mode 100644 index 00000000..9eccd535 --- /dev/null +++ b/tests/Images/Input/Astc/HDR/HDR-A-1x1.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:677e500aebe8cb2ea7cab79dae3b66c8eb549718d3c66660092d618b6828a116 +size 32 diff --git a/tests/Images/Input/Astc/HDR/HDR-A-1x1.exr b/tests/Images/Input/Astc/HDR/HDR-A-1x1.exr new file mode 100644 index 00000000..c1f5f61c --- /dev/null +++ b/tests/Images/Input/Astc/HDR/HDR-A-1x1.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e09a64448ffd3e0a4da4b4c46836242d1446ecdfcd5a15f5feb6a17ba9b4100 +size 335 diff --git a/tests/Images/Input/Astc/HDR/LDR-A-1x1.astc b/tests/Images/Input/Astc/HDR/LDR-A-1x1.astc new file mode 100644 index 00000000..234207d1 --- /dev/null +++ b/tests/Images/Input/Astc/HDR/LDR-A-1x1.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e414d79e31dd49bb3e3ef0cdc4c6d918c24d9f1583307c0ee14e97e4032c5eb3 +size 32 diff --git a/tests/Images/Input/Astc/HDR/LDR-A-1x1.png b/tests/Images/Input/Astc/HDR/LDR-A-1x1.png new file mode 100644 index 00000000..e1eb6355 --- /dev/null +++ b/tests/Images/Input/Astc/HDR/LDR-A-1x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:335e2a1d19f3d572d5664c899853f4e7eb9caa595ea8888e5f2cddb85f64c9da +size 509 diff --git a/tests/Images/Input/Astc/HDR/hdr-complex.exr b/tests/Images/Input/Astc/HDR/hdr-complex.exr new file mode 100644 index 00000000..9a3d6700 --- /dev/null +++ b/tests/Images/Input/Astc/HDR/hdr-complex.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6a16a419ec8f487be26cb223c3770923a03e22d9327ae2903c1888761217107 +size 825 diff --git a/tests/Images/Input/Astc/HDR/hdr-tile.astc b/tests/Images/Input/Astc/HDR/hdr-tile.astc new file mode 100644 index 00000000..5720bd63 --- /dev/null +++ b/tests/Images/Input/Astc/HDR/hdr-tile.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66b60ac14cecd6e8bc895c16333470d50be57de6f594fc801a65df22008792ec +size 80 diff --git a/tests/Images/Input/Astc/HDR/hdr.exr b/tests/Images/Input/Astc/HDR/hdr.exr new file mode 100644 index 00000000..fd39b9b5 --- /dev/null +++ b/tests/Images/Input/Astc/HDR/hdr.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90aff5c37cdfd98c9ffb3b1b5aa082ebf6f420c0bd3bceeee4b554d18465f030 +size 971 diff --git a/tests/Images/Input/Astc/HDR/hdr.hdr b/tests/Images/Input/Astc/HDR/hdr.hdr new file mode 100644 index 0000000000000000000000000000000000000000..abf4097ce8d5b382f1abb32f082679fa9554e56c GIT binary patch literal 217 zcmY$k4{~(zbo6s};Bxa1@^uWcH8#>s z$}EX5%1MncN>57V;?j*&uu#yBP_W=)Vq)NEVQBf+!O+0~q?^g&|7&q*`PXs)h#6X% LS{lG+kSPWL0Dny8 literal 0 HcmV?d00001 diff --git a/tests/Images/Input/Astc/HDR/ldr-tile.astc b/tests/Images/Input/Astc/HDR/ldr-tile.astc new file mode 100644 index 00000000..789d257c --- /dev/null +++ b/tests/Images/Input/Astc/HDR/ldr-tile.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8512766071566cc8612da34bae7d44c9f9161f7b292290bd6f52e2f5cc5ee80 +size 80 diff --git a/tests/Images/Input/Astc/atlas_small_4x4.astc b/tests/Images/Input/Astc/atlas_small_4x4.astc new file mode 100644 index 00000000..d4d69c12 --- /dev/null +++ b/tests/Images/Input/Astc/atlas_small_4x4.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b32ef22043e36153c7ac54cd49ec3ed9eb81b788f3259cb2f53360e1b14a3a21 +size 65552 diff --git a/tests/Images/Input/Astc/atlas_small_5x5.astc b/tests/Images/Input/Astc/atlas_small_5x5.astc new file mode 100644 index 00000000..f946faad --- /dev/null +++ b/tests/Images/Input/Astc/atlas_small_5x5.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c44b3c4c94608aa265fb529709aeb2e4276e3ba95106f86ad10242ec518c6c0a +size 43280 diff --git a/tests/Images/Input/Astc/atlas_small_6x6.astc b/tests/Images/Input/Astc/atlas_small_6x6.astc new file mode 100644 index 00000000..f40ab54f --- /dev/null +++ b/tests/Images/Input/Astc/atlas_small_6x6.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d834cfba3512722c3faa3268a12fa33c04757746c6861831e9861e0ddded8e85 +size 29600 diff --git a/tests/Images/Input/Astc/atlas_small_8x8.astc b/tests/Images/Input/Astc/atlas_small_8x8.astc new file mode 100644 index 00000000..d8538b3e --- /dev/null +++ b/tests/Images/Input/Astc/atlas_small_8x8.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5e1e478453cf5f2f7ef34314ed0ffacb0c81dea2abe370331b5f70db95d85e3 +size 16400 diff --git a/tests/Images/Input/Astc/checkerboard.astc b/tests/Images/Input/Astc/checkerboard.astc new file mode 100644 index 00000000..8881cc32 --- /dev/null +++ b/tests/Images/Input/Astc/checkerboard.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf0f52348fdd9ceba15072932caa1336d74f49ddd0588088f5a52fcc916c767f +size 80 diff --git a/tests/Images/Input/Astc/checkered_10.astc b/tests/Images/Input/Astc/checkered_10.astc new file mode 100644 index 00000000..9f60d3ba --- /dev/null +++ b/tests/Images/Input/Astc/checkered_10.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cca15624b6d3f55348f6ebbe21076258ef59e55af6e2834e4d4018b1943d210 +size 1616 diff --git a/tests/Images/Input/Astc/checkered_11.astc b/tests/Images/Input/Astc/checkered_11.astc new file mode 100644 index 00000000..238f4b89 --- /dev/null +++ b/tests/Images/Input/Astc/checkered_11.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:848917b722ca204701e769f60f5733dd14b939d63c829f3f246a7b3b4e2271cd +size 1952 diff --git a/tests/Images/Input/Astc/checkered_12.astc b/tests/Images/Input/Astc/checkered_12.astc new file mode 100644 index 00000000..f96fef67 --- /dev/null +++ b/tests/Images/Input/Astc/checkered_12.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acdbf5759fea7bb5cb931d4215e2d30d4d78373ce345825f13cc39e57aa41500 +size 2320 diff --git a/tests/Images/Input/Astc/checkered_4.astc b/tests/Images/Input/Astc/checkered_4.astc new file mode 100644 index 00000000..683afb6e --- /dev/null +++ b/tests/Images/Input/Astc/checkered_4.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a134cda205d05e8afa6c248519f6346bf1ba11deab0f6d7dfd71a2bd8819fde3 +size 272 diff --git a/tests/Images/Input/Astc/checkered_5.astc b/tests/Images/Input/Astc/checkered_5.astc new file mode 100644 index 00000000..2db06d3f --- /dev/null +++ b/tests/Images/Input/Astc/checkered_5.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a269b6a297ff64a8fe8c19b5798b2521dedddd87973d3fd3bf05c5d06bf2794a +size 416 diff --git a/tests/Images/Input/Astc/checkered_6.astc b/tests/Images/Input/Astc/checkered_6.astc new file mode 100644 index 00000000..5bf762c9 --- /dev/null +++ b/tests/Images/Input/Astc/checkered_6.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b96a8c7965e7f8e6bf420c90555b86caa763f68aa82e36913e0d439029c9fe49 +size 592 diff --git a/tests/Images/Input/Astc/checkered_7.astc b/tests/Images/Input/Astc/checkered_7.astc new file mode 100644 index 00000000..1d2e9576 --- /dev/null +++ b/tests/Images/Input/Astc/checkered_7.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95e3e1fdd47fe9a93a6a10f08c3884c7e2a30042dc0f3af23c213d8e9a010921 +size 800 diff --git a/tests/Images/Input/Astc/checkered_8.astc b/tests/Images/Input/Astc/checkered_8.astc new file mode 100644 index 00000000..bdbb2cc0 --- /dev/null +++ b/tests/Images/Input/Astc/checkered_8.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6b417040cedad2222cc0ecaf9d072cd4a3566062c082d676dcb6699dcf43d80 +size 1040 diff --git a/tests/Images/Input/Astc/checkered_9.astc b/tests/Images/Input/Astc/checkered_9.astc new file mode 100644 index 00000000..3d8e3628 --- /dev/null +++ b/tests/Images/Input/Astc/checkered_9.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9908f8d9ebf4f7c81e7deb0737058df036450d0342d669fb88cd81da2ba0cab +size 1312 diff --git a/tests/Images/Input/Astc/footprint_10x10.astc b/tests/Images/Input/Astc/footprint_10x10.astc new file mode 100644 index 00000000..a4b401dd --- /dev/null +++ b/tests/Images/Input/Astc/footprint_10x10.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:436425158e3f7ed7590765dc09a98d65a1095d667dda92dd9b92f6dd89bde296 +size 272 diff --git a/tests/Images/Input/Astc/footprint_10x5.astc b/tests/Images/Input/Astc/footprint_10x5.astc new file mode 100644 index 00000000..41d52afb --- /dev/null +++ b/tests/Images/Input/Astc/footprint_10x5.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4bd8cf2124ad4e62379e251b17c729c4867b1f516d0475700c491551f7250e9 +size 464 diff --git a/tests/Images/Input/Astc/footprint_10x6.astc b/tests/Images/Input/Astc/footprint_10x6.astc new file mode 100644 index 00000000..641b83ca --- /dev/null +++ b/tests/Images/Input/Astc/footprint_10x6.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:660c1298a0dcbf79410b2c3595fc755965643f0c33c83b2d0e59de54a5cf2063 +size 400 diff --git a/tests/Images/Input/Astc/footprint_10x8.astc b/tests/Images/Input/Astc/footprint_10x8.astc new file mode 100644 index 00000000..ab410fb4 --- /dev/null +++ b/tests/Images/Input/Astc/footprint_10x8.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5649aba1ef59f2b401a17c9642efa6d48f72767c5fd48718c26a6d551710338a +size 272 diff --git a/tests/Images/Input/Astc/footprint_12x10.astc b/tests/Images/Input/Astc/footprint_12x10.astc new file mode 100644 index 00000000..888a9d21 --- /dev/null +++ b/tests/Images/Input/Astc/footprint_12x10.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d65db0cea158fea1d1a045eeed0e9067808b632093e3b5e4f87a175f6e9c5709 +size 208 diff --git a/tests/Images/Input/Astc/footprint_12x12.astc b/tests/Images/Input/Astc/footprint_12x12.astc new file mode 100644 index 00000000..2e366934 --- /dev/null +++ b/tests/Images/Input/Astc/footprint_12x12.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dcc9c84c82076c71fe01aa95e4911da2572575c29663f45aa770e70c9c7a8b1f +size 160 diff --git a/tests/Images/Input/Astc/footprint_4x4.astc b/tests/Images/Input/Astc/footprint_4x4.astc new file mode 100644 index 00000000..28597ef3 --- /dev/null +++ b/tests/Images/Input/Astc/footprint_4x4.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a50bf8146c466b5941b2b21f969d49b9b7d770b4de810e1791e8ed33a0016f94 +size 1040 diff --git a/tests/Images/Input/Astc/footprint_5x4.astc b/tests/Images/Input/Astc/footprint_5x4.astc new file mode 100644 index 00000000..ba4f59d5 --- /dev/null +++ b/tests/Images/Input/Astc/footprint_5x4.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43bbfa85525a18ce3bb7eac66fc24ac0cf9b80c1b457746081ec3e8f843cc6c3 +size 912 diff --git a/tests/Images/Input/Astc/footprint_5x5.astc b/tests/Images/Input/Astc/footprint_5x5.astc new file mode 100644 index 00000000..bfcf6e18 --- /dev/null +++ b/tests/Images/Input/Astc/footprint_5x5.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbc031ec24fdee8803ab4814c5c8a907538512b733a19e1f229be5c8c5106ca6 +size 800 diff --git a/tests/Images/Input/Astc/footprint_6x5.astc b/tests/Images/Input/Astc/footprint_6x5.astc new file mode 100644 index 00000000..e4dec9ec --- /dev/null +++ b/tests/Images/Input/Astc/footprint_6x5.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b23ebfc2bcb2c98a2c07b3cf03b70dd537adc5212256a846823ea001b33ef5ab +size 688 diff --git a/tests/Images/Input/Astc/footprint_6x6.astc b/tests/Images/Input/Astc/footprint_6x6.astc new file mode 100644 index 00000000..68781840 --- /dev/null +++ b/tests/Images/Input/Astc/footprint_6x6.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbeb362a78688f484bd097e60288ea1d6fc6b2261c0a35c6bf46286542bce9a8 +size 592 diff --git a/tests/Images/Input/Astc/footprint_8x5.astc b/tests/Images/Input/Astc/footprint_8x5.astc new file mode 100644 index 00000000..a11bbc46 --- /dev/null +++ b/tests/Images/Input/Astc/footprint_8x5.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b22c69da56f5ad765ce1d52ffd83043d4a99a68a1c5e208e93ccccf6e507dc7 +size 464 diff --git a/tests/Images/Input/Astc/footprint_8x6.astc b/tests/Images/Input/Astc/footprint_8x6.astc new file mode 100644 index 00000000..e0101d00 --- /dev/null +++ b/tests/Images/Input/Astc/footprint_8x6.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2db67a88b455e91c32ef5e706f230df911dcc547852d89220c2a2923afb96580 +size 400 diff --git a/tests/Images/Input/Astc/footprint_8x8.astc b/tests/Images/Input/Astc/footprint_8x8.astc new file mode 100644 index 00000000..183b5500 --- /dev/null +++ b/tests/Images/Input/Astc/footprint_8x8.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae221e4ec900d72c7dcb638b9a23133931d5b8637b74b72ea52ff7586f9e8d35 +size 272 diff --git a/tests/Images/Input/Astc/rgb_12x12.astc b/tests/Images/Input/Astc/rgb_12x12.astc new file mode 100644 index 00000000..d4b2428f --- /dev/null +++ b/tests/Images/Input/Astc/rgb_12x12.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66d7f756934331f129409c63f98b0a59ec126d448c3c96274bf293aefd7b1477 +size 7312 diff --git a/tests/Images/Input/Astc/rgb_4x4.astc b/tests/Images/Input/Astc/rgb_4x4.astc new file mode 100644 index 00000000..810be4bf --- /dev/null +++ b/tests/Images/Input/Astc/rgb_4x4.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25945fd30b22c5da62d0f335add72f5c6ed4485d3d8c091ff238aa3ba1b84b77 +size 64528 diff --git a/tests/Images/Input/Astc/rgb_5x4.astc b/tests/Images/Input/Astc/rgb_5x4.astc new file mode 100644 index 00000000..ab255eca --- /dev/null +++ b/tests/Images/Input/Astc/rgb_5x4.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e664d806c5ef2812a2fad23060a57e5e8e375e3c8ea2895ba34efb2ad87d26a9 +size 51856 diff --git a/tests/Images/Input/Astc/rgb_6x6.astc b/tests/Images/Input/Astc/rgb_6x6.astc new file mode 100644 index 00000000..f5d6250f --- /dev/null +++ b/tests/Images/Input/Astc/rgb_6x6.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a24a5364544a1eefd05963d82466523e6b8704c4eab767e43d134d81a9bf877 +size 29200 diff --git a/tests/Images/Input/Astc/rgb_8x8.astc b/tests/Images/Input/Astc/rgb_8x8.astc new file mode 100644 index 00000000..344d1782 --- /dev/null +++ b/tests/Images/Input/Astc/rgb_8x8.astc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a767e28d2cdfc6b7d87a98d7d2ce9b9406e63d200fc5846c085465127bf1923 +size 16144 diff --git a/tests/Images/Input/Ktx2/astc_ldr_cubemap_6x6.ktx2 b/tests/Images/Input/Ktx2/Cubemap/Astc/astc_ldr_cubemap_6x6.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_ldr_cubemap_6x6.ktx2 rename to tests/Images/Input/Ktx2/Cubemap/Astc/astc_ldr_cubemap_6x6.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_1.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZLIB_9.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_1.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/Supercompressed/valid_ASTC_4x4_UNORM_BLOCK_2D_ZSTD_9.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_ldr_10x5_FlightHelmet_baseColor.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_ldr_10x5_FlightHelmet_baseColor.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_ldr_10x5_FlightHelmet_baseColor.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_ldr_10x5_FlightHelmet_baseColor.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_ldr_6x6_arraytex_7_mipmap.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_ldr_6x6_arraytex_7_mipmap.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_ldr_6x6_arraytex_7_mipmap.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_ldr_6x6_arraytex_7_mipmap.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_10x10.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_10x10.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_10x10.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_10x10.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_10x5.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_10x5.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_10x5.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_10x5.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_10x6.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_10x6.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_10x6.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_10x6.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_10x8.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_10x8.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_10x8.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_10x8.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_12x10.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_12x10.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_12x10.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_12x10.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_12x12.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_12x12.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_12x12.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_12x12.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_4x4.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_4x4.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_4x4.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_4x4.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_5x4.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_5x4.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_5x4.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_5x4.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_5x5.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_5x5.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_5x5.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_5x5.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_6x5.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_6x5.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_6x5.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_6x5.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_6x6.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_6x6.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_6x6.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_6x6.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_8x5.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_8x5.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_8x5.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_8x5.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_8x6.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_8x6.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_8x6.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_8x6.ktx2 diff --git a/tests/Images/Input/Ktx2/astc_rgba32_8x8.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_8x8.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/astc_rgba32_8x8.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/astc_rgba32_8x8.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x10_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x10_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x5_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x5_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x6_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x6_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x8_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_10x8_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_12x10_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_12x10_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_12x12_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_12x12_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_4x4_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_4x4_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_5x4_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_5x4_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_5x5_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_5x5_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_6x5_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_6x5_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_6x6_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_6x6_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x5_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x5_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x6_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x6_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x8_SRGB_BLOCK_2D.ktx2 diff --git a/tests/Images/Input/Ktx2/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2 similarity index 100% rename from tests/Images/Input/Ktx2/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2 rename to tests/Images/Input/Ktx2/Flat/Astc/valid_ASTC_8x8_UNORM_BLOCK_2D.ktx2 diff --git a/tests/Images/ReferenceOutput/KtxAstcDecoder_CanDecode_Rgba32_Blocksizes.png b/tests/Images/ReferenceOutput/Ktx/KtxAstcDecoderTests/CanDecode_Rgba32_Blocksizes.png similarity index 100% rename from tests/Images/ReferenceOutput/KtxAstcDecoder_CanDecode_Rgba32_Blocksizes.png rename to tests/Images/ReferenceOutput/Ktx/KtxAstcDecoderTests/CanDecode_Rgba32_Blocksizes.png diff --git a/tests/Images/ReferenceOutput/KtxDecoder_CanDecode_Rgba8888.png b/tests/Images/ReferenceOutput/Ktx/KtxDecoderTests/CanDecode_Rgba8888.png similarity index 100% rename from tests/Images/ReferenceOutput/KtxDecoder_CanDecode_Rgba8888.png rename to tests/Images/ReferenceOutput/Ktx/KtxDecoderTests/CanDecode_Rgba8888.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negX.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_negX.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negX.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_negX.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negY.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_negY.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negY.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_negY.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negZ.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_negZ.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_negZ.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_negZ.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posX.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_posX.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posX.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_posX.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posY.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_posY.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posY.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_posY.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posZ.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_posZ.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_All_Faces_posZ.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderCubemapTests/CanDecode_All_Faces_posZ.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_0.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_0.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_0.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_0.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_1.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_1.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_1.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_1.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_2.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_2.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_2.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_2.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_3.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_3.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_3.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_3.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_4.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_4.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_MipMaps_4.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_MipMaps_4.png diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x10.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x10.png new file mode 100644 index 00000000..ca570b4a --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8649fc53816ebf74c8f98dab0e70476f3308852eb883465181a9bf84037f58fb +size 91369 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x5.png new file mode 100644 index 00000000..b11f2767 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a731ccbbb49ecba544369ae3e4f30646157594dd8f5353656a3031a416e0d43 +size 112109 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x6.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x6.png new file mode 100644 index 00000000..56328b30 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00d9777982ab047f1c712196f59d6e153af272fba82262dcb61d6bddcf4575d9 +size 108518 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x8.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x8.png new file mode 100644 index 00000000..5ad07b37 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_10x8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:424436fe9515bad28b340c2999340f6402d978b329d1d56246ed0413c7304879 +size 104500 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_12x10.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_12x10.png new file mode 100644 index 00000000..2b49b584 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_12x10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:741049c6ab995701d2ee8553904b5890370ffac6500b6dcb1933e46f3345467d +size 89195 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_12x12.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_12x12.png new file mode 100644 index 00000000..7f52fa54 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_12x12.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9c1fd822fc4681597a6866d60359553c570e2ea3971460f486c368ef6efda4c +size 84802 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_4x4.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_4x4.png new file mode 100644 index 00000000..53d5ade0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_4x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:489431552033ca7036ff468758d46a4ba4bb5ff7ea776153935c06b3c637ae79 +size 145887 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_5x4.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_5x4.png new file mode 100644 index 00000000..ec637f5a --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_5x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d12d37905204a2aba3553221dadad5c56f5a413b4ce3bb95c9619cbf44f4150 +size 139563 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_5x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_5x5.png new file mode 100644 index 00000000..4aa505a9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_5x5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4aba4d35e1df5e035ca44c143a9a9d90be3a3f4ca5ae60fab0f4123d7b2e4b7 +size 135851 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_6x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_6x5.png new file mode 100644 index 00000000..1b7ebdd4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_6x5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4aae99d90b433d4aff92ccc19a441c2282aa7a174372fa548b5ce00af36794d +size 127662 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_6x6.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_6x6.png new file mode 100644 index 00000000..0d962ba4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_6x6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e683c93018c85aacd2876d560cd411853d2e097d1c71873300d29b427eec155 +size 127440 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x5.png new file mode 100644 index 00000000..bd4d63ed --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4926624965af600a44cb1fec70e5c62b755690cdca50026f07b244140e40ade +size 119346 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x6.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x6.png new file mode 100644 index 00000000..447246a9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3dc79650aff8e5f80564a3e9e31b80dcc38ac8580cb0da4bf9fc008211bfcd9 +size 114594 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x8.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x8.png new file mode 100644 index 00000000..e4066843 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes_8x8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3423ebecc2ab40069e0e73bbe9563cc55ef118c7c15bafe4af854226bfd4bab +size 110284 diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x10.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x10.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x10.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x10.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x5.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x5.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x5.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x6.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x6.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x6.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x6.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x8.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x8.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_10x8.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_10x8.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x10.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_12x10.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x10.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_12x10.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x12.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_12x12.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_12x12.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_12x12.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_4x4.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_4x4.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_4x4.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_4x4.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x4.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_5x4.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x4.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_5x4.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_5x5.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_5x5.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_5x5.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_6x5.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x5.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_6x5.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x6.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_6x6.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_6x6.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_6x6.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_8x5.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x5.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_8x5.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x6.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_8x6.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x6.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_8x6.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x8.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_8x8.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_8x8.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_8x8.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_Large.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_Large.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Srgb_Large.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Srgb_Large.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_1.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZLIB_1.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_1.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZLIB_1.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_9.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZLIB_9.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZLIB_9.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZLIB_9.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_1.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZSTD_1.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_1.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZSTD_1.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_9.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZSTD_9.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Supercompressed_ZSTD_9.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Supercompressed_ZSTD_9.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x10.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x10.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x10.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x10.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x5.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x5.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x5.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x6.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x6.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x6.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x6.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x8.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x8.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_10x8.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_10x8.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x10.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_12x10.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x10.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_12x10.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x12.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_12x12.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_12x12.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_12x12.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_4x4.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_4x4.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_4x4.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_4x4.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x4.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_5x4.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x4.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_5x4.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_5x5.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_5x5.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_5x5.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_6x5.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x5.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_6x5.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x6.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_6x6.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_6x6.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_6x6.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x5.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_8x5.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x5.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_8x5.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x6.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_8x6.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x6.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_8x6.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x8.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_8x8.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Unorm_8x8.png rename to tests/Images/ReferenceOutput/Ktx2/Ktx2AstcDecoderFlatTests/CanDecode_Rgba32_Unorm_8x8.png diff --git a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes.png b/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes.png deleted file mode 100644 index 598f93a5..00000000 --- a/tests/Images/ReferenceOutput/Ktx2AstcDecoder_CanDecode_Rgba32_Blocksizes.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:72bf89c439781bb8e9bf582589fec9e2dcfd3581472d1d8182b445a1099b5879 -size 164227 From 0aba87641e20eb5108148ede6b86ddb54a95ec22 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:11:48 +0100 Subject: [PATCH 03/10] Use existing test utils for Astc tests --- .../Enums/TestTextureFormat.cs | 5 + .../Enums/TestTextureTool.cs | 7 +- .../Formats/Astc/AstcDecoderTests.cs | 83 ++++++---- .../Formats/Astc/EndpointCodecTests.cs | 2 +- .../Formats/Astc/HDR/HdrComparisonTests.cs | 155 ++++++++---------- .../Formats/Astc/HDR/HdrImageTests.cs | 115 ++++++------- .../Formats/Astc/IntermediateBlockTests.cs | 2 +- .../Formats/Astc/LogicalAstcBlockTests.cs | 73 ++++----- tests/ImageSharp.Textures.Tests/TestImages.cs | 111 +++++-------- .../{atlas_small_4x4.astc => rgba_4x4.astc} | 0 .../{atlas_small_5x5.astc => rgba_5x5.astc} | 0 .../{atlas_small_6x6.astc => rgba_6x6.astc} | 0 .../{atlas_small_8x8.astc => rgba_8x8.astc} | 0 ...hAstcRgbFile_ShouldMatchExpected_12x12.png | 3 + ...ithAstcRgbFile_ShouldMatchExpected_4x4.png | 3 + ...ithAstcRgbFile_ShouldMatchExpected_5x4.png | 3 + ...ithAstcRgbFile_ShouldMatchExpected_6x6.png | 3 + ...ithAstcRgbFile_ShouldMatchExpected_8x8.png | 3 + ...thAstcRgbaFile_ShouldMatchExpected_4x4.png | 3 + ...thAstcRgbaFile_ShouldMatchExpected_5x5.png | 3 + ...thAstcRgbaFile_ShouldMatchExpected_6x6.png | 3 + ...thAstcRgbaFile_ShouldMatchExpected_8x8.png | 3 + ..._FromImage_ShouldDecodeCorrectly_10x10.png | 3 + ...k_FromImage_ShouldDecodeCorrectly_10x5.png | 3 + ...k_FromImage_ShouldDecodeCorrectly_10x6.png | 3 + ...k_FromImage_ShouldDecodeCorrectly_10x8.png | 3 + ..._FromImage_ShouldDecodeCorrectly_12x10.png | 3 + ..._FromImage_ShouldDecodeCorrectly_12x12.png | 3 + ...ck_FromImage_ShouldDecodeCorrectly_4x4.png | 3 + ...ck_FromImage_ShouldDecodeCorrectly_5x4.png | 3 + ...ck_FromImage_ShouldDecodeCorrectly_5x5.png | 3 + ...ck_FromImage_ShouldDecodeCorrectly_6x5.png | 3 + ...ck_FromImage_ShouldDecodeCorrectly_6x6.png | 3 + ...ck_FromImage_ShouldDecodeCorrectly_8x5.png | 3 + ...ck_FromImage_ShouldDecodeCorrectly_8x6.png | 3 + ...ck_FromImage_ShouldDecodeCorrectly_8x8.png | 3 + 36 files changed, 322 insertions(+), 300 deletions(-) rename tests/Images/Input/Astc/{atlas_small_4x4.astc => rgba_4x4.astc} (100%) rename tests/Images/Input/Astc/{atlas_small_5x5.astc => rgba_5x5.astc} (100%) rename tests/Images/Input/Astc/{atlas_small_6x6.astc => rgba_6x6.astc} (100%) rename tests/Images/Input/Astc/{atlas_small_8x8.astc => rgba_8x8.astc} (100%) create mode 100644 tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_12x12.png create mode 100644 tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_4x4.png create mode 100644 tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_5x4.png create mode 100644 tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_6x6.png create mode 100644 tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_8x8.png create mode 100644 tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_4x4.png create mode 100644 tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_5x5.png create mode 100644 tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_6x6.png create mode 100644 tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_8x8.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x10.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x5.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x6.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x8.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_12x10.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_12x12.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_4x4.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_5x4.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_5x5.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_6x5.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_6x6.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x5.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x6.png create mode 100644 tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x8.png diff --git a/tests/ImageSharp.Textures.Tests/Enums/TestTextureFormat.cs b/tests/ImageSharp.Textures.Tests/Enums/TestTextureFormat.cs index f737c793..0cd31bfb 100644 --- a/tests/ImageSharp.Textures.Tests/Enums/TestTextureFormat.cs +++ b/tests/ImageSharp.Textures.Tests/Enums/TestTextureFormat.cs @@ -19,5 +19,10 @@ public enum TestTextureFormat /// Khronos Texture, version 2. /// Ktx2, + + /// + /// Adaptive Scalable Texture Compression. + /// + Astc, } } diff --git a/tests/ImageSharp.Textures.Tests/Enums/TestTextureTool.cs b/tests/ImageSharp.Textures.Tests/Enums/TestTextureTool.cs index 169d4daf..760b78cb 100644 --- a/tests/ImageSharp.Textures.Tests/Enums/TestTextureTool.cs +++ b/tests/ImageSharp.Textures.Tests/Enums/TestTextureTool.cs @@ -23,6 +23,11 @@ public enum TestTextureTool /// /// The PVR tex tool cli. /// - PvrTexToolCli + PvrTexToolCli, + + /// + /// ARM ASTC encoder (astcenc). + /// + AstcEnc, } } diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/AstcDecoderTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/AstcDecoderTests.cs index 1ad62f19..136311cf 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Astc/AstcDecoderTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/AstcDecoderTests.cs @@ -7,11 +7,19 @@ using SixLabors.ImageSharp.Textures.Compression.Astc.Core; using SixLabors.ImageSharp.Textures.Compression.Astc.IO; using SixLabors.ImageSharp.Textures.Compression.Astc.TexelBlock; +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.ImageComparison; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; #nullable enable + +[GroupOutput("Astc")] +[Trait("Format", "Astc")] public class AstcDecoderTests { [Fact] @@ -63,10 +71,10 @@ public void DecompressImage_WithMismatchedBlockCount_ShouldReturnEmpty() } [Theory] - [InlineData(TestImages.Astc.Atlas_Small_4x4)] - [InlineData(TestImages.Astc.Atlas_Small_5x5)] - [InlineData(TestImages.Astc.Atlas_Small_6x6)] - [InlineData(TestImages.Astc.Atlas_Small_8x8)] + [InlineData(TestImages.Astc.Rgba_4x4)] + [InlineData(TestImages.Astc.Rgba_5x5)] + [InlineData(TestImages.Astc.Rgba_6x6)] + [InlineData(TestImages.Astc.Rgba_8x8)] [InlineData(TestImages.Astc.Checkerboard)] [InlineData(TestImages.Astc.Checkered_4)] [InlineData(TestImages.Astc.Checkered_5)] @@ -98,7 +106,7 @@ public void DecompressImage_WithMismatchedBlockCount_ShouldReturnEmpty() [InlineData(TestImages.Astc.Rgb_12x12)] public void DecompressImage_WithTestdataFile_ShouldReturnExpectedByteCount(string inputFile) { - string filePath = TestFile.GetInputFileFullPath(inputFile); + string filePath = TestFile.GetInputFileFullPath(Path.Combine("Astc", inputFile)); byte[] bytes = File.ReadAllBytes(filePath); AstcFile astc = AstcFile.FromMemory(bytes); @@ -108,17 +116,17 @@ public void DecompressImage_WithTestdataFile_ShouldReturnExpectedByteCount(strin } [Theory] - [InlineData(TestImages.Astc.Atlas_Small_4x4, FootprintType.Footprint4x4, 256, 256)] - [InlineData(TestImages.Astc.Atlas_Small_5x5, FootprintType.Footprint5x5, 256, 256)] - [InlineData(TestImages.Astc.Atlas_Small_6x6, FootprintType.Footprint6x6, 256, 256)] - [InlineData(TestImages.Astc.Atlas_Small_8x8, FootprintType.Footprint8x8, 256, 256)] + [InlineData(TestImages.Astc.Rgba_4x4, FootprintType.Footprint4x4, 256, 256)] + [InlineData(TestImages.Astc.Rgba_5x5, FootprintType.Footprint5x5, 256, 256)] + [InlineData(TestImages.Astc.Rgba_6x6, FootprintType.Footprint6x6, 256, 256)] + [InlineData(TestImages.Astc.Rgba_8x8, FootprintType.Footprint8x8, 256, 256)] public void DecompressImage_WithValidData_ShouldDecodeAllBlocks( string inputFile, FootprintType footprintType, int width, int height) { - byte[] astcData = TestFile.Create(inputFile).Bytes[16..]; + byte[] astcData = TestFile.Create(Path.Combine("Astc", inputFile)).Bytes[16..]; Footprint footprint = Footprint.FromFootprintType(footprintType); int blockWidth = footprint.Width; int blockHeight = footprint.Height; @@ -143,32 +151,47 @@ public void DecompressImage_WithValidData_ShouldDecodeAllBlocks( } [Theory] - [InlineData(TestImages.Astc.Atlas_Small_4x4, TestImages.Astc.Expected.Atlas_Small_4x4, FootprintType.Footprint4x4, 256, 256)] - [InlineData(TestImages.Astc.Atlas_Small_5x5, TestImages.Astc.Expected.Atlas_Small_5x5, FootprintType.Footprint5x5, 256, 256)] - [InlineData(TestImages.Astc.Atlas_Small_6x6, TestImages.Astc.Expected.Atlas_Small_6x6, FootprintType.Footprint6x6, 256, 256)] - [InlineData(TestImages.Astc.Atlas_Small_8x8, TestImages.Astc.Expected.Atlas_Small_8x8, FootprintType.Footprint8x8, 256, 256)] - public void DecompressImage_WithAstcFile_ShouldMatchExpected( - string inputFile, - string expectedFile, - FootprintType footprint, - int width, - int height) + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgb_4x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgb_5x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgb_6x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgb_8x8)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgb_12x12)] + public void DecompressImage_WithAstcRgbFile_ShouldMatchExpected(TestTextureProvider provider) + { + byte[] astcBytes = File.ReadAllBytes(provider.InputFile); + AstcFile file = AstcFile.FromMemory(astcBytes); + + string blockSize = $"{file.Footprint.Width}x{file.Footprint.Height}"; + + byte[] decodedPixels = AstcDecoder.DecompressImage(file).ToArray(); + using Image actualImage = Image.LoadPixelData(decodedPixels, file.Width, file.Height); + actualImage.Mutate(x => x.Flip(FlipMode.Vertical)); + + actualImage.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.03f), + provider, + testOutputDetails: blockSize); + } + + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_4x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_5x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_6x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_8x8)] + public void DecompressImage_WithAstcRgbaFile_ShouldMatchExpected(TestTextureProvider provider) { - string astcPath = TestFile.GetInputFileFullPath(inputFile); - byte[] astcBytes = File.ReadAllBytes(astcPath); + byte[] astcBytes = File.ReadAllBytes(provider.InputFile); AstcFile file = AstcFile.FromMemory(astcBytes); - // Check file header - Assert.Equal(footprint, file.Footprint.Type); - Assert.Equal(width, file.Width); - Assert.Equal(height, file.Height); + string blockSize = $"{file.Footprint.Width}x{file.Footprint.Height}"; byte[] decodedPixels = AstcDecoder.DecompressImage(file).ToArray(); - using Image actualImage = Image.LoadPixelData(decodedPixels, width, height); + using Image actualImage = Image.LoadPixelData(decodedPixels, file.Width, file.Height); actualImage.Mutate(x => x.Flip(FlipMode.Vertical)); - string expectedImagePath = TestFile.GetInputFileFullPath(expectedFile); - using Image expectedImage = Image.Load(expectedImagePath); - ImageComparer.TolerantPercentage(0.1f).VerifySimilarity(expectedImage, actualImage); + actualImage.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.03f), + provider, + testOutputDetails: blockSize); } } diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs index f5d154d3..5ec42416 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs @@ -323,7 +323,7 @@ private static int[] EncodeRgbBaseOffset(Rgba32 low, Rgba32 high) [Fact] public void DecodeCheckerboard_ShouldDecodeToGrayscaleEndpoints() { - string astcFilePath = TestFile.GetInputFileFullPath(TestImages.Astc.Checkerboard); + string astcFilePath = TestFile.GetInputFileFullPath(Path.Combine("Astc", TestImages.Astc.Checkerboard)); byte[] astcData = File.ReadAllBytes(astcFilePath); int blocksDecoded = 0; diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrComparisonTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrComparisonTests.cs index 062c9bbf..0677b6a3 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrComparisonTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrComparisonTests.cs @@ -5,117 +5,102 @@ using SixLabors.ImageSharp.Textures.Compression.Astc; using SixLabors.ImageSharp.Textures.Compression.Astc.Core; using SixLabors.ImageSharp.Textures.Compression.Astc.IO; +using SixLabors.ImageSharp.Textures.Tests.Enums; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc.HDR; /// /// Comparing HDR and LDR ASTC decoding behavior using real reference files. /// +[Trait("Format", "Astc")] public class HdrComparisonTests { - [Fact] - public void HdrFile_DecodedWithHdrApi_ShouldPreserveExtendedRange() + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Hdr_A_1x1)] + public void HdrFile_DecodedWithHdrApi_ShouldPreserveExtendedRange(TestTextureProvider provider) { - // HDR files should decode to values potentially exceeding 1.0 - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); - // Decode with HDR API Span hdrResult = AstcDecoder.DecompressHdrImage( astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); - // Verify we get Float16 output - Assert.Equal(4, hdrResult.Length); // 1 pixel, 4 channels + Assert.Equal(4, hdrResult.Length); - // HDR content can have values > 1.0 (this file may or may not, but should allow it) - foreach (float value in hdrResult) - { - Assert.False(float.IsNaN(value)); - Assert.False(float.IsInfinity(value)); - Assert.True(value >= 0.0f); - } + // All channels exceed 1.0, confirming HDR extended range. + Assert.Equal(1.625f, hdrResult[0], 0.001f); + Assert.Equal(1.84375f, hdrResult[1], 0.001f); + Assert.Equal(2.125f, hdrResult[2], 0.001f); + Assert.Equal(1.0f, hdrResult[3], 0.001f); } - [Fact] - public void LdrFile_DecodedWithHdrApi_ShouldUpscaleToHdrRange() + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Ldr_A_1x1)] + public void LdrFile_DecodedWithHdrApi_ShouldProduceExpectedNormalizedValues(TestTextureProvider provider) { - // LDR files decoded with HDR API should produce values in 0.0-1.0 range - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Ldr_A_1x1); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); - // Decode with HDR API Span hdrResult = AstcDecoder.DecompressHdrImage( astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); Assert.Equal(4, hdrResult.Length); - // LDR content should map to 0.0-1.0 range when decoded with HDR API - foreach (float value in hdrResult) - { - Assert.True(value >= 0.0f); - Assert.True(value <= 1.0f); - } + // LDR content maps to 0.0-1.0 range: values correspond to byte/255. + Assert.Equal(43 / 255f, hdrResult[0], 0.001f); + Assert.Equal(173 / 255f, hdrResult[1], 0.001f); + Assert.Equal(0f, hdrResult[2], 0.001f); + Assert.Equal(1.0f, hdrResult[3], 0.001f); } - [Fact] - public void HdrFile_DecodedWithLdrApi_ShouldClampToByteRange() + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Hdr_A_1x1)] + public void HdrFile_DecodedWithLdrApi_ShouldProduceExpectedClampedValues(TestTextureProvider provider) { - // HDR files decoded with LDR API should clamp to 0-255 - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); - // Decode with LDR API Span ldrResult = AstcDecoder.DecompressImage(astcFile); Assert.Equal(4, ldrResult.Length); - // All values must be in LDR range - foreach (byte value in ldrResult) - { - Assert.True(value >= 0); - Assert.True(value <= 255); - } + Assert.Equal(62, ldrResult[0]); + Assert.Equal(63, ldrResult[1]); + Assert.Equal(64, ldrResult[2]); + Assert.Equal(60, ldrResult[3]); } - [Fact] - public void LdrFile_DecodedWithBothApis_ShouldProduceConsistentValues() + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Ldr_A_1x1)] + public void LdrFile_DecodedWithBothApis_ShouldProduceConsistentValues(TestTextureProvider provider) { - // LDR content should produce equivalent results with both APIs - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Ldr_A_1x1); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); - // Decode with both APIs Span ldrResult = AstcDecoder.DecompressImage(astcFile); Span hdrResult = AstcDecoder.DecompressHdrImage( astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); - // Compare results - LDR byte should map to HDR float / 255.0 + // LDR: exact byte values. + Assert.Equal(43, ldrResult[0]); + Assert.Equal(173, ldrResult[1]); + Assert.Equal(0, ldrResult[2]); + Assert.Equal(255, ldrResult[3]); + + // HDR float should equal byte / 255. for (int i = 0; i < 4; i++) { - byte ldrValue = ldrResult[i]; - float hdrValue = hdrResult[i]; - - float expectedHdr = ldrValue / 255.0f; - - Assert.True(Math.Abs(hdrValue - expectedHdr) < 0.01f); + Assert.Equal(ldrResult[i] / 255f, hdrResult[i], 0.001f); } } - [Fact] - public void HdrTile_ShouldDecodeSuccessfully() + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Hdr_Tile)] + public void HdrTile_ShouldDecodeSuccessfully(TestTextureProvider provider) { - // Test larger HDR tile decoding - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_Tile); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); Span hdrResult = AstcDecoder.DecompressHdrImage( @@ -131,13 +116,11 @@ public void HdrTile_ShouldDecodeSuccessfully() } } - [Fact] - public void LdrTile_ShouldDecodeSuccessfully() + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Ldr_Tile)] + public void LdrTile_ShouldDecodeSuccessfully(TestTextureProvider provider) { - // Test larger LDR tile decoding - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Ldr_Tile); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); // Decode with both APIs @@ -153,39 +136,35 @@ public void LdrTile_ShouldDecodeSuccessfully() [Fact] public void SameFootprint_HdrVsLdr_ShouldBothDecode() { - // Verify files with same footprint decode correctly - string hdrPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); - string ldrPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Ldr_A_1x1); - - byte[] hdrData = File.ReadAllBytes(hdrPath); - byte[] ldrData = File.ReadAllBytes(ldrPath); + string hdrPath = TestFile.GetInputFileFullPath(Path.Combine("Astc", TestImages.Astc.Hdr.Hdr_A_1x1)); + string ldrPath = TestFile.GetInputFileFullPath(Path.Combine("Astc", TestImages.Astc.Hdr.Ldr_A_1x1)); - AstcFile hdrFile = AstcFile.FromMemory(hdrData); - AstcFile ldrFile = AstcFile.FromMemory(ldrData); + AstcFile hdrFile = AstcFile.FromMemory(File.ReadAllBytes(hdrPath)); + AstcFile ldrFile = AstcFile.FromMemory(File.ReadAllBytes(ldrPath)); - // Both are 1x1 with 6x6 footprint + // Both are 1x1 with 6x6 footprint. + Assert.Equal(1, hdrFile.Width); + Assert.Equal(1, hdrFile.Height); Assert.Equal(ldrFile.Width, hdrFile.Width); Assert.Equal(ldrFile.Height, hdrFile.Height); - Assert.Equal(ldrFile.Footprint.Width, hdrFile.Footprint.Width); - Assert.Equal(ldrFile.Footprint.Height, hdrFile.Footprint.Height); + Assert.Equal(FootprintType.Footprint6x6, hdrFile.Footprint.Type); + Assert.Equal(hdrFile.Footprint.Type, ldrFile.Footprint.Type); - // Both should decode successfully with HDR API Span hdrDecoded = AstcDecoder.DecompressHdrImage( hdrFile.Blocks, hdrFile.Width, hdrFile.Height, hdrFile.Footprint); Span ldrDecoded = AstcDecoder.DecompressHdrImage( ldrFile.Blocks, ldrFile.Width, ldrFile.Height, ldrFile.Footprint); - Assert.Equal(4, hdrDecoded.Length); - Assert.Equal(4, ldrDecoded.Length); + // HDR file has values > 1.0; LDR file stays in 0-1. + Assert.Equal(1.625f, hdrDecoded[0], 0.001f); + Assert.Equal(43 / 255f, ldrDecoded[0], 0.001f); } - [Fact] - public void HdrColor_FromLdr_ShouldMatchLdrToHdrApiConversion() + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Ldr_A_1x1)] + public void HdrColor_FromLdr_ShouldMatchLdrToHdrApiConversion(TestTextureProvider provider) { - // Verify that HdrColor.FromRgba() produces same results as decoding LDR with HDR API - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Ldr_A_1x1); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); // Decode with LDR API to get byte values diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrImageTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrImageTests.cs index da88207b..712ea1cb 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrImageTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/HDR/HdrImageTests.cs @@ -5,6 +5,9 @@ using SixLabors.ImageSharp.Textures.Compression.Astc; using SixLabors.ImageSharp.Textures.Compression.Astc.Core; using SixLabors.ImageSharp.Textures.Compression.Astc.IO; +using SixLabors.ImageSharp.Textures.Tests.Enums; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc.HDR; @@ -13,15 +16,15 @@ namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc.HDR; /// These tests validate that our HDR implementation produces valid output for /// actual HDR-compressed ASTC data. /// +[Trait("Format", "Astc")] public class HdrImageTests { - [Fact] + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Hdr_A_1x1)] [Description("Verify that the ASTC file header is correctly parsed for HDR content, including footprint detection")] - public void DecodeHdrFile_VerifyFootprintDetection() + public void DecodeHdrFile_VerifyFootprintDetection(TestTextureProvider provider) { - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); // The HDR-A-1x1.astc file has a 6x6 footprint based on the header @@ -30,12 +33,11 @@ public void DecodeHdrFile_VerifyFootprintDetection() Assert.Equal(FootprintType.Footprint6x6, astcFile.Footprint.Type); } - [Fact] - public void DecodeHdrAstcFile_1x1Pixel_ShouldProduceValidHdrOutput() + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Hdr_A_1x1)] + public void DecodeHdrAstcFile_1x1Pixel_ShouldProduceExpectedHdrValues(TestTextureProvider provider) { - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); Span hdrResult = AstcDecoder.DecompressHdrImage( @@ -44,24 +46,20 @@ public void DecodeHdrAstcFile_1x1Pixel_ShouldProduceValidHdrOutput() astcFile.Height, astcFile.Footprint); - // Should produce 1 pixel with 4 values (RGBA) Assert.Equal(4, hdrResult.Length); - // HDR values can exceed 1.0 - // Just verify they're in a reasonable range (0.0 to 10.0) - foreach (float value in hdrResult) - { - Assert.True(value >= 0.0f); - Assert.True(value < 10.0f); - } + // HDR values exceed 1.0 for this file. + Assert.Equal(1.625f, hdrResult[0], 0.001f); + Assert.Equal(1.84375f, hdrResult[1], 0.001f); + Assert.Equal(2.125f, hdrResult[2], 0.001f); + Assert.Equal(1.0f, hdrResult[3], 0.001f); } - [Fact] - public void DecodeHdrAstcFile_Tile_ShouldProduceValidHdrOutput() + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Hdr_Tile)] + public void DecodeHdrAstcFile_Tile_ShouldProduceValidHdrOutput(TestTextureProvider provider) { - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_Tile); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); Span hdrResult = AstcDecoder.DecompressHdrImage( @@ -86,68 +84,49 @@ public void DecodeHdrAstcFile_Tile_ShouldProduceValidHdrOutput() Assert.Equal(64, valuesGreaterThanOne); } - [Fact] + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Hdr_A_1x1)] [Description("Verify that HDR ASTC files can be decoded with the LDR API, producing clamped values")] - public void DecodeHdrAstcFile_WithLdrApi_ShouldClampValues() + public void DecodeHdrAstcFile_WithLdrApi_ShouldProduceExpectedClampedValues(TestTextureProvider provider) { - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); - - if (!File.Exists(astcPath)) - { - return; - } - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); - // Decode using LDR API Span ldrResult = AstcDecoder.DecompressImage(astcFile); - // Should produce 1 pixel with 4 bytes (RGBA) Assert.Equal(4, ldrResult.Length); - // All values should be in LDR range - foreach (byte value in ldrResult) - { - Assert.True(value >= byte.MinValue); - Assert.True(value <= byte.MaxValue); - } + Assert.Equal(62, ldrResult[0]); + Assert.Equal(63, ldrResult[1]); + Assert.Equal(64, ldrResult[2]); + Assert.Equal(60, ldrResult[3]); } - [Fact] - [Description("Verify that HDR and LDR APIs produce consistent relative channel values for the same HDR ASTC file")] - public void HdrAndLdrApis_OnSameHdrFile_ShouldProduceConsistentRelativeValues() + [Theory] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Hdr_A_1x1)] + [Description("Verify that HDR and LDR APIs produce consistent channel values for the same HDR ASTC file")] + public void HdrAndLdrApis_OnSameHdrFile_ShouldProduceConsistentValues(TestTextureProvider provider) { - string astcPath = TestFile.GetInputFileFullPath(TestImages.Astc.Hdr.Hdr_A_1x1); - - byte[] astcData = File.ReadAllBytes(astcPath); + byte[] astcData = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(astcData); - // Decode with both APIs Span hdrResult = AstcDecoder.DecompressHdrImage( astcFile.Blocks, astcFile.Width, astcFile.Height, astcFile.Footprint); Span ldrResult = AstcDecoder.DecompressImage(astcFile); - // Both should produce output for 1 pixel - Assert.Equal(4, hdrResult.Length); - Assert.Equal(4, ldrResult.Length); + // HDR values exceed 1.0; R < G < B ordering is preserved in both APIs. + Assert.Equal(1.625f, hdrResult[0], 0.001f); + Assert.Equal(1.84375f, hdrResult[1], 0.001f); + Assert.Equal(2.125f, hdrResult[2], 0.001f); - // The relative ordering of RGB channels should be consistent between APIs. - // If HDR channel i > channel j, then LDR channel i should be >= channel j - // (accounting for clamping at 255). - for (int i = 0; i < 3; i++) - { - for (int j = i + 1; j < 3; j++) - { - if (hdrResult[i] > hdrResult[j]) - { - Assert.True(ldrResult[i] >= ldrResult[j]); - } - else if (hdrResult[i] < hdrResult[j]) - { - Assert.True(ldrResult[i] <= ldrResult[j]); - } - } - } + Assert.Equal(62, ldrResult[0]); + Assert.Equal(63, ldrResult[1]); + Assert.Equal(64, ldrResult[2]); + + // Channel ordering R < G < B is preserved across both APIs. + Assert.True(hdrResult[0] < hdrResult[1]); + Assert.True(hdrResult[1] < hdrResult[2]); + Assert.True(ldrResult[0] < ldrResult[1]); + Assert.True(ldrResult[1] < ldrResult[2]); } } diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/IntermediateBlockTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/IntermediateBlockTests.cs index 4b392735..2b360e96 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Astc/IntermediateBlockTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/IntermediateBlockTests.cs @@ -453,7 +453,7 @@ private static void VerifyBlockPropertiesMatch(PhysicalBlock repacked, PhysicalB private static byte[] LoadASTCFile(string inputFile) { - string filename = TestFile.GetInputFileFullPath(inputFile); + string filename = TestFile.GetInputFileFullPath(Path.Combine("Astc", inputFile)); Assert.True(File.Exists(filename), $"Testdata missing: {filename}"); byte[] data = File.ReadAllBytes(filename); Assert.True(data.Length >= 16, "ASTC file too small"); diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/LogicalAstcBlockTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/LogicalAstcBlockTests.cs index 192a7b7d..d989e73e 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Astc/LogicalAstcBlockTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/LogicalAstcBlockTests.cs @@ -4,12 +4,20 @@ using SixLabors.ImageSharp.PixelFormats; 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; +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; namespace SixLabors.ImageSharp.Textures.Tests.Formats.Astc; #nullable enable + +[GroupOutput("Astc")] +[Trait("Format", "Astc")] public class LogicalAstcBlockTests { [Theory] @@ -351,50 +359,33 @@ public void UnpackLogicalBlock_WithStandardBlock_ShouldReturnLogicalBlock() } [Theory] - - // Synthetic test images - [InlineData(TestImages.Astc.Footprint_4x4, TestImages.Astc.Expected.Footprint_4x4, FootprintType.Footprint4x4, 32, 32)] - [InlineData(TestImages.Astc.Footprint_5x4, TestImages.Astc.Expected.Footprint_5x4, FootprintType.Footprint5x4, 32, 32)] - [InlineData(TestImages.Astc.Footprint_5x5, TestImages.Astc.Expected.Footprint_5x5, FootprintType.Footprint5x5, 32, 32)] - [InlineData(TestImages.Astc.Footprint_6x5, TestImages.Astc.Expected.Footprint_6x5, FootprintType.Footprint6x5, 32, 32)] - [InlineData(TestImages.Astc.Footprint_6x6, TestImages.Astc.Expected.Footprint_6x6, FootprintType.Footprint6x6, 32, 32)] - [InlineData(TestImages.Astc.Footprint_8x5, TestImages.Astc.Expected.Footprint_8x5, FootprintType.Footprint8x5, 32, 32)] - [InlineData(TestImages.Astc.Footprint_8x6, TestImages.Astc.Expected.Footprint_8x6, FootprintType.Footprint8x6, 32, 32)] - [InlineData(TestImages.Astc.Footprint_8x8, TestImages.Astc.Expected.Footprint_8x8, FootprintType.Footprint8x8, 32, 32)] - [InlineData(TestImages.Astc.Footprint_10x5, TestImages.Astc.Expected.Footprint_10x5, FootprintType.Footprint10x5, 32, 32)] - [InlineData(TestImages.Astc.Footprint_10x6, TestImages.Astc.Expected.Footprint_10x6, FootprintType.Footprint10x6, 32, 32)] - [InlineData(TestImages.Astc.Footprint_10x8, TestImages.Astc.Expected.Footprint_10x8, FootprintType.Footprint10x8, 32, 32)] - [InlineData(TestImages.Astc.Footprint_10x10, TestImages.Astc.Expected.Footprint_10x10, FootprintType.Footprint10x10, 32, 32)] - [InlineData(TestImages.Astc.Footprint_12x10, TestImages.Astc.Expected.Footprint_12x10, FootprintType.Footprint12x10, 32, 32)] - [InlineData(TestImages.Astc.Footprint_12x12, TestImages.Astc.Expected.Footprint_12x12, FootprintType.Footprint12x12, 32, 32)] - - // RGB without alpha images - [InlineData(TestImages.Astc.Rgb_4x4, TestImages.Astc.Expected.Rgb_4x4, FootprintType.Footprint4x4, 224, 288)] - [InlineData(TestImages.Astc.Rgb_5x4, TestImages.Astc.Expected.Rgb_5x4, FootprintType.Footprint5x4, 224, 288)] - [InlineData(TestImages.Astc.Rgb_6x6, TestImages.Astc.Expected.Rgb_6x6, FootprintType.Footprint6x6, 224, 288)] - [InlineData(TestImages.Astc.Rgb_8x8, TestImages.Astc.Expected.Rgb_8x8, FootprintType.Footprint8x8, 224, 288)] - [InlineData(TestImages.Astc.Rgb_12x12, TestImages.Astc.Expected.Rgb_12x12, FootprintType.Footprint12x12, 224, 288)] - - // RGB with alpha images - [InlineData(TestImages.Astc.Atlas_Small_4x4, TestImages.Astc.Expected.Atlas_Small_4x4, FootprintType.Footprint4x4, 256, 256)] - [InlineData(TestImages.Astc.Atlas_Small_5x5, TestImages.Astc.Expected.Atlas_Small_5x5, FootprintType.Footprint5x5, 256, 256)] - [InlineData(TestImages.Astc.Atlas_Small_6x6, TestImages.Astc.Expected.Atlas_Small_6x6, FootprintType.Footprint6x6, 256, 256)] - [InlineData(TestImages.Astc.Atlas_Small_8x8, TestImages.Astc.Expected.Atlas_Small_8x8, FootprintType.Footprint8x8, 256, 256)] - public void UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly( - string inputFile, - string expectedFile, - FootprintType footprintType, - int width, - int height) + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_4x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_5x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_5x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_6x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_6x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_8x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_8x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_8x8)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_10x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_10x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_10x8)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_10x10)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_12x10)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_12x12)] + public void UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly(TestTextureProvider provider) { - Footprint footprint = Footprint.FromFootprintType(footprintType); - byte[] astcData = TestFile.Create(inputFile).Bytes[16..]; + byte[] astcBytes = File.ReadAllBytes(provider.InputFile); + AstcFile file = AstcFile.FromMemory(astcBytes); + + string blockSize = $"{file.Footprint.Width}x{file.Footprint.Height}"; - using Image decodedImage = DecodeAstcBlocksToImage(footprint, astcData, width, height); + using Image decodedImage = DecodeAstcBlocksToImage(file.Footprint, file.Blocks.ToArray(), file.Width, file.Height); - string expectedPath = TestFile.GetInputFileFullPath(expectedFile); - using Image expectedImage = Image.Load(expectedPath); - ImageComparer.TolerantPercentage(1.0f).VerifySimilarity(expectedImage, decodedImage); + decodedImage.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.03f), + provider, + testOutputDetails: blockSize); } private static Image DecodeAstcBlocksToImage(Footprint footprint, byte[] astcData, int width, int height) diff --git a/tests/ImageSharp.Textures.Tests/TestImages.cs b/tests/ImageSharp.Textures.Tests/TestImages.cs index 60b01b47..9dc80543 100644 --- a/tests/ImageSharp.Textures.Tests/TestImages.cs +++ b/tests/ImageSharp.Textures.Tests/TestImages.cs @@ -10,79 +10,50 @@ public static class TestImages { public static class Astc { - public const string Atlas_Small_4x4 = "Astc/atlas_small_4x4.astc"; - public const string Atlas_Small_5x5 = "Astc/atlas_small_5x5.astc"; - public const string Atlas_Small_6x6 = "Astc/atlas_small_6x6.astc"; - public const string Atlas_Small_8x8 = "Astc/atlas_small_8x8.astc"; - - public const string Checkerboard = "Astc/checkerboard.astc"; - - public const string Checkered_4 = "Astc/checkered_4.astc"; - public const string Checkered_5 = "Astc/checkered_5.astc"; - public const string Checkered_6 = "Astc/checkered_6.astc"; - public const string Checkered_7 = "Astc/checkered_7.astc"; - public const string Checkered_8 = "Astc/checkered_8.astc"; - public const string Checkered_9 = "Astc/checkered_9.astc"; - public const string Checkered_10 = "Astc/checkered_10.astc"; - public const string Checkered_11 = "Astc/checkered_11.astc"; - public const string Checkered_12 = "Astc/checkered_12.astc"; - - public const string Footprint_4x4 = "Astc/footprint_4x4.astc"; - public const string Footprint_5x4 = "Astc/footprint_5x4.astc"; - public const string Footprint_5x5 = "Astc/footprint_5x5.astc"; - public const string Footprint_6x5 = "Astc/footprint_6x5.astc"; - public const string Footprint_6x6 = "Astc/footprint_6x6.astc"; - public const string Footprint_8x5 = "Astc/footprint_8x5.astc"; - public const string Footprint_8x6 = "Astc/footprint_8x6.astc"; - public const string Footprint_8x8 = "Astc/footprint_8x8.astc"; - public const string Footprint_10x5 = "Astc/footprint_10x5.astc"; - public const string Footprint_10x6 = "Astc/footprint_10x6.astc"; - public const string Footprint_10x8 = "Astc/footprint_10x8.astc"; - public const string Footprint_10x10 = "Astc/footprint_10x10.astc"; - public const string Footprint_12x10 = "Astc/footprint_12x10.astc"; - public const string Footprint_12x12 = "Astc/footprint_12x12.astc"; - - public const string Rgb_4x4 = "Astc/rgb_4x4.astc"; - public const string Rgb_5x4 = "Astc/rgb_5x4.astc"; - public const string Rgb_6x6 = "Astc/rgb_6x6.astc"; - public const string Rgb_8x8 = "Astc/rgb_8x8.astc"; - public const string Rgb_12x12 = "Astc/rgb_12x12.astc"; - - public static class Expected - { - public const string Atlas_Small_4x4 = "Astc/Expected/atlas_small_4x4.bmp"; - public const string Atlas_Small_5x5 = "Astc/Expected/atlas_small_5x5.bmp"; - public const string Atlas_Small_6x6 = "Astc/Expected/atlas_small_6x6.bmp"; - public const string Atlas_Small_8x8 = "Astc/Expected/atlas_small_8x8.bmp"; - - public const string Footprint_4x4 = "Astc/Expected/footprint_4x4.bmp"; - public const string Footprint_5x4 = "Astc/Expected/footprint_5x4.bmp"; - public const string Footprint_5x5 = "Astc/Expected/footprint_5x5.bmp"; - public const string Footprint_6x5 = "Astc/Expected/footprint_6x5.bmp"; - public const string Footprint_6x6 = "Astc/Expected/footprint_6x6.bmp"; - public const string Footprint_8x5 = "Astc/Expected/footprint_8x5.bmp"; - public const string Footprint_8x6 = "Astc/Expected/footprint_8x6.bmp"; - public const string Footprint_8x8 = "Astc/Expected/footprint_8x8.bmp"; - public const string Footprint_10x5 = "Astc/Expected/footprint_10x5.bmp"; - public const string Footprint_10x6 = "Astc/Expected/footprint_10x6.bmp"; - public const string Footprint_10x8 = "Astc/Expected/footprint_10x8.bmp"; - public const string Footprint_10x10 = "Astc/Expected/footprint_10x10.bmp"; - public const string Footprint_12x10 = "Astc/Expected/footprint_12x10.bmp"; - public const string Footprint_12x12 = "Astc/Expected/footprint_12x12.bmp"; - - public const string Rgb_4x4 = "Astc/Expected/rgb_4x4.bmp"; - public const string Rgb_5x4 = "Astc/Expected/rgb_5x4.bmp"; - public const string Rgb_6x6 = "Astc/Expected/rgb_6x6.bmp"; - public const string Rgb_8x8 = "Astc/Expected/rgb_8x8.bmp"; - public const string Rgb_12x12 = "Astc/Expected/rgb_12x12.bmp"; - } + public const string Rgb_4x4 = "rgb_4x4.astc"; + public const string Rgb_5x4 = "rgb_5x4.astc"; + public const string Rgb_6x6 = "rgb_6x6.astc"; + public const string Rgb_8x8 = "rgb_8x8.astc"; + public const string Rgb_12x12 = "rgb_12x12.astc"; + + public const string Rgba_4x4 = "rgba_4x4.astc"; + public const string Rgba_5x5 = "rgba_5x5.astc"; + public const string Rgba_6x6 = "rgba_6x6.astc"; + public const string Rgba_8x8 = "rgba_8x8.astc"; + + public const string Checkerboard = "checkerboard.astc"; + + public const string Checkered_4 = "checkered_4.astc"; + public const string Checkered_5 = "checkered_5.astc"; + public const string Checkered_6 = "checkered_6.astc"; + public const string Checkered_7 = "checkered_7.astc"; + public const string Checkered_8 = "checkered_8.astc"; + public const string Checkered_9 = "checkered_9.astc"; + public const string Checkered_10 = "checkered_10.astc"; + public const string Checkered_11 = "checkered_11.astc"; + public const string Checkered_12 = "checkered_12.astc"; + + public const string Footprint_4x4 = "footprint_4x4.astc"; + public const string Footprint_5x4 = "footprint_5x4.astc"; + public const string Footprint_5x5 = "footprint_5x5.astc"; + public const string Footprint_6x5 = "footprint_6x5.astc"; + public const string Footprint_6x6 = "footprint_6x6.astc"; + public const string Footprint_8x5 = "footprint_8x5.astc"; + public const string Footprint_8x6 = "footprint_8x6.astc"; + public const string Footprint_8x8 = "footprint_8x8.astc"; + public const string Footprint_10x5 = "footprint_10x5.astc"; + public const string Footprint_10x6 = "footprint_10x6.astc"; + public const string Footprint_10x8 = "footprint_10x8.astc"; + public const string Footprint_10x10 = "footprint_10x10.astc"; + public const string Footprint_12x10 = "footprint_12x10.astc"; + public const string Footprint_12x12 = "footprint_12x12.astc"; public static class Hdr { - public const string Hdr_A_1x1 = "Astc/HDR/HDR-A-1x1.astc"; - public const string Ldr_A_1x1 = "Astc/HDR/LDR-A-1x1.astc"; - public const string Hdr_Tile = "Astc/HDR/hdr-tile.astc"; - public const string Ldr_Tile = "Astc/HDR/ldr-tile.astc"; + public const string Hdr_A_1x1 = "HDR/HDR-A-1x1.astc"; + public const string Ldr_A_1x1 = "HDR/LDR-A-1x1.astc"; + public const string Hdr_Tile = "HDR/hdr-tile.astc"; + public const string Ldr_Tile = "HDR/ldr-tile.astc"; } } diff --git a/tests/Images/Input/Astc/atlas_small_4x4.astc b/tests/Images/Input/Astc/rgba_4x4.astc similarity index 100% rename from tests/Images/Input/Astc/atlas_small_4x4.astc rename to tests/Images/Input/Astc/rgba_4x4.astc diff --git a/tests/Images/Input/Astc/atlas_small_5x5.astc b/tests/Images/Input/Astc/rgba_5x5.astc similarity index 100% rename from tests/Images/Input/Astc/atlas_small_5x5.astc rename to tests/Images/Input/Astc/rgba_5x5.astc diff --git a/tests/Images/Input/Astc/atlas_small_6x6.astc b/tests/Images/Input/Astc/rgba_6x6.astc similarity index 100% rename from tests/Images/Input/Astc/atlas_small_6x6.astc rename to tests/Images/Input/Astc/rgba_6x6.astc diff --git a/tests/Images/Input/Astc/atlas_small_8x8.astc b/tests/Images/Input/Astc/rgba_8x8.astc similarity index 100% rename from tests/Images/Input/Astc/atlas_small_8x8.astc rename to tests/Images/Input/Astc/rgba_8x8.astc diff --git a/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_12x12.png b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_12x12.png new file mode 100644 index 00000000..fd2a6167 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_12x12.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfb710779914ca372a33c68f863e6dbca6def418877ad2d3f600ae761b882e7f +size 59770 diff --git a/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_4x4.png b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_4x4.png new file mode 100644 index 00000000..bd993cda --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_4x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54babe429c79135d58d06490cf404e1f8618b7e985286685bef7249052aeebe4 +size 86825 diff --git a/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_5x4.png b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_5x4.png new file mode 100644 index 00000000..00c25e5f --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_5x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2612bb6ddc10d1407bf8332278bac5a9ce75e5e49ee33c650562bd8413906f48 +size 84898 diff --git a/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_6x6.png b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_6x6.png new file mode 100644 index 00000000..3cfc9663 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_6x6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a5735593fc50b7b7f435b7af04b3b074c0d7156917199f3d0d7e24650aeb87d +size 78840 diff --git a/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_8x8.png b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_8x8.png new file mode 100644 index 00000000..2d7d375b --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbFile_ShouldMatchExpected_8x8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d00f291fc76d24410069341a7cc73af7244042071a7e439d63ab53b0b37a4051 +size 71810 diff --git a/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_4x4.png b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_4x4.png new file mode 100644 index 00000000..835a2b29 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_4x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46ed393d8219f3d0c5b0e0bd0ecfb0f4098818003ffb1a3e4d65df9e7a4ce349 +size 107733 diff --git a/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_5x5.png b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_5x5.png new file mode 100644 index 00000000..72088ad9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_5x5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee95e88b0755d5dcfc5ab61f1d57d90a6e2da3ed31e1ef5428e1551be41e06a9 +size 103808 diff --git a/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_6x6.png b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_6x6.png new file mode 100644 index 00000000..6920cdbe --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_6x6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47fb47857190fe2cd62c5fec1d94ccfe87f957a09f1033002728136528fbeaf2 +size 99065 diff --git a/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_8x8.png b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_8x8.png new file mode 100644 index 00000000..0e3eb1e8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/AstcDecoderTests/DecompressImage_WithAstcRgbaFile_ShouldMatchExpected_8x8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e541a93fdd814eb5a148d2f367a087a8c2b397b1686ef0e16659d4a3c65ea60f +size 94301 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x10.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x10.png new file mode 100644 index 00000000..ff83170b --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bdfcd075ec8fc1a20e04842892f43219f18dc0e9f79eaaba2fbda29d56d44d4 +size 904 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x5.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x5.png new file mode 100644 index 00000000..dbd88c58 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:661c2a15a1e70ba58a9035897a1b149b359526387599eb754ca742b9ed20b60e +size 923 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x6.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x6.png new file mode 100644 index 00000000..2c37720e --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b75da1d22138a5e71ac6d4ed466f25f962cfbfc5c24f2ebf59e8f1d0e4adb257 +size 994 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x8.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x8.png new file mode 100644 index 00000000..672af374 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_10x8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae3321808d2565642c7e536d602cb5d2ff6e94740259e459f1f65a12acfa13ec +size 826 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_12x10.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_12x10.png new file mode 100644 index 00000000..bbd04ee3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_12x10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:894a585619f8dae813353465e45b908d4451e07c15246c12199c71c36c0d9f22 +size 775 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_12x12.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_12x12.png new file mode 100644 index 00000000..5e89535c --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_12x12.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce5e9c6b9cab369796ee81eaf7f7ba06a4a3cf630fe7677a5a3e1f6ebc8a0725 +size 755 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_4x4.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_4x4.png new file mode 100644 index 00000000..61e1c26a --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_4x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5963c3aa3d538312892ee2486ea25d426dadddc7258ee4b0a60cf2c3eb590fe3 +size 1009 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_5x4.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_5x4.png new file mode 100644 index 00000000..19c4346e --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_5x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c43985aa23407b40a92c3214df9a3dcc496207c35f1343d4c5d03b859a1c3274 +size 832 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_5x5.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_5x5.png new file mode 100644 index 00000000..ee7a850c --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_5x5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dee3c549f661b728a39b12e4a81e0f325ddf370f47d63e3093287b8d7dd019a1 +size 1098 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_6x5.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_6x5.png new file mode 100644 index 00000000..2a504059 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_6x5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e71fb71dc43ad8df8d7aca6b38f08ba223ece2e0230bd137a7406110133c29b +size 969 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_6x6.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_6x6.png new file mode 100644 index 00000000..02e2e5e9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_6x6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718f970f65a71ec6ca25a8b1c93331a708ddf504cf31b5f3d75f6671e6c7b447 +size 989 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x5.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x5.png new file mode 100644 index 00000000..7f27764d --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac64b8d8441f506da461bdd6ccda189d90a470577d5681db6647cb159f0ca6f0 +size 931 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x6.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x6.png new file mode 100644 index 00000000..03652cb8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f58804183f111390d178aa74ad555042009d63af9e234b027c42a96c897ca17 +size 887 diff --git a/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x8.png b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x8.png new file mode 100644 index 00000000..d2ade874 --- /dev/null +++ b/tests/Images/ReferenceOutput/Astc/LogicalAstcBlockTests/UnpackLogicalBlock_FromImage_ShouldDecodeCorrectly_8x8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9254b747ef434ac21281b36ef72e3e47dc2db74049ae0a12384bec5e9581627d +size 660 From 44599c8ce01d4d75f048221bd99d4a8e05b76792 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:14:58 +0100 Subject: [PATCH 04/10] Add HDR support --- .../Common/Helpers/FloatHelper.cs | 69 +++--- .../Ktx/Enums/GlInternalPixelFormat.cs | 53 +++++ .../Formats/Ktx/IEndianHandler.cs | 27 +++ .../Formats/Ktx/KtxProcessor.cs | 115 ++++++++-- .../Formats/Ktx/NativeEndianHandler.cs | 39 ++++ .../Formats/Ktx/SwappingEndianHandler.cs | 77 +++++++ .../Formats/Ktx2/Ktx2Processor.cs | 14 +- .../ImageSharp.Textures.csproj | 1 + src/ImageSharp.Textures/PixelFormats/Fp32.cs | 4 +- .../PixelFormats/Generated/Rgb48Float.cs | 200 ++++++++++++++++++ .../TextureFormats/Decoding/AstcDecoder.cs | 10 + .../Decoding/Rgb111110PackedFloat.cs | 65 ++++++ .../TextureFormats/Decoding/Rgb48Float.cs | 36 ++++ .../TextureFormats/Decoding/RgbaAstc10x5.cs | 2 +- .../Decoding/Rgbe9995PackedFloat.cs | 71 +++++++ ...Sharp.Textures.Astc.Reference.Tests.csproj | 7 + .../ReferenceDecoderHdrTests.cs | 35 +-- .../ReferenceDecoderTests.cs | 77 +++---- ...derTests.cs => KtxAstcDecoderFlatTests.cs} | 4 +- .../Formats/Ktx/KtxHdrDecoderFlatTests.cs | 195 +++++++++++++++++ .../Formats/Ktx2/Ktx2HdrDecoderFlatTests.cs | 195 +++++++++++++++++ tests/ImageSharp.Textures.Tests/TestImages.cs | 26 +++ tests/Images/Input/Ktx/HDR/hdr-r16.ktx | 3 + tests/Images/Input/Ktx/HDR/hdr-r32.ktx | 3 + tests/Images/Input/Ktx/HDR/hdr-rg32.ktx | 3 + tests/Images/Input/Ktx/HDR/hdr-rg64.ktx | 3 + tests/Images/Input/Ktx/HDR/hdr-rgb48.ktx | 3 + tests/Images/Input/Ktx/HDR/hdr-rgb96.ktx | 3 + tests/Images/Input/Ktx/HDR/hdr-rgba128.ktx | 3 + tests/Images/Input/Ktx/HDR/hdr-rgba64.ktx | 3 + .../Flat/Astc/HDR/hdr-b10g11r11-ufloat.ktx2 | 3 + .../Ktx2/Flat/Astc/HDR/hdr-r16-unorm.ktx2 | 3 + .../Ktx2/Flat/Astc/HDR/hdr-r32-sfloat.ktx2 | 3 + .../Ktx2/Flat/Astc/HDR/hdr-rg32-unorm.ktx2 | 3 + .../Ktx2/Flat/Astc/HDR/hdr-rg64-sfloat.ktx2 | 3 + .../Ktx2/Flat/Astc/HDR/hdr-rgb48-unorm.ktx2 | 3 + .../Ktx2/Flat/Astc/HDR/hdr-rgb96-sfloat.ktx2 | 3 + .../Ktx2/Flat/Astc/HDR/hdr-rgb9e5-ufloat.ktx2 | 3 + .../Flat/Astc/HDR/hdr-rgba128-sfloat.ktx2 | 3 + .../Ktx2/Flat/Astc/HDR/hdr-rgba64-unorm.ktx2 | 3 + .../CanDecode_Rgba32_Blocksizes.png | 0 .../KtxHdrDecoderFlatTests/CanDecode_R16F.png | 3 + .../KtxHdrDecoderFlatTests/CanDecode_R32F.png | 3 + .../CanDecode_RG32F.png | 3 + .../CanDecode_RG64F.png | 3 + .../CanDecode_RGB48F.png | 3 + .../CanDecode_RGB96F.png | 3 + .../CanDecode_RGBA128F.png | 3 + .../CanDecode_RGBA64F.png | 3 + .../CanDecode_B10g11r11_Ufloat_Packed.png | 3 + .../CanDecode_R16_Unorm.png | 3 + .../CanDecode_R32_Sfloat.png | 3 + .../CanDecode_RG32_Unorm.png | 3 + .../CanDecode_RG48_Sfloat.png | 3 + .../CanDecode_RGB48_Unorm.png | 3 + .../CanDecode_RGB96_Sfloat.png | 3 + .../CanDecode_RGBA128_Sfloat.png | 3 + .../CanDecode_RGBA64_Unorm.png | 3 + .../CanDecode_Rgb9e5_Ufloat_Packed.png | 3 + 59 files changed, 1331 insertions(+), 99 deletions(-) create mode 100644 src/ImageSharp.Textures/Formats/Ktx/IEndianHandler.cs create mode 100644 src/ImageSharp.Textures/Formats/Ktx/NativeEndianHandler.cs create mode 100644 src/ImageSharp.Textures/Formats/Ktx/SwappingEndianHandler.cs create mode 100644 src/ImageSharp.Textures/PixelFormats/Generated/Rgb48Float.cs create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/Rgb111110PackedFloat.cs create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/Rgb48Float.cs create mode 100644 src/ImageSharp.Textures/TextureFormats/Decoding/Rgbe9995PackedFloat.cs rename tests/ImageSharp.Textures.Tests/Formats/Ktx/{KtxAstcDecoderTests.cs => KtxAstcDecoderFlatTests.cs} (91%) create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxHdrDecoderFlatTests.cs create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2HdrDecoderFlatTests.cs create mode 100644 tests/Images/Input/Ktx/HDR/hdr-r16.ktx create mode 100644 tests/Images/Input/Ktx/HDR/hdr-r32.ktx create mode 100644 tests/Images/Input/Ktx/HDR/hdr-rg32.ktx create mode 100644 tests/Images/Input/Ktx/HDR/hdr-rg64.ktx create mode 100644 tests/Images/Input/Ktx/HDR/hdr-rgb48.ktx create mode 100644 tests/Images/Input/Ktx/HDR/hdr-rgb96.ktx create mode 100644 tests/Images/Input/Ktx/HDR/hdr-rgba128.ktx create mode 100644 tests/Images/Input/Ktx/HDR/hdr-rgba64.ktx create mode 100644 tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-b10g11r11-ufloat.ktx2 create mode 100644 tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-r16-unorm.ktx2 create mode 100644 tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-r32-sfloat.ktx2 create mode 100644 tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rg32-unorm.ktx2 create mode 100644 tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rg64-sfloat.ktx2 create mode 100644 tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb48-unorm.ktx2 create mode 100644 tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb96-sfloat.ktx2 create mode 100644 tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb9e5-ufloat.ktx2 create mode 100644 tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgba128-sfloat.ktx2 create mode 100644 tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgba64-unorm.ktx2 rename tests/Images/ReferenceOutput/Ktx/{KtxAstcDecoderTests => KtxAstcDecoderFlatTests}/CanDecode_Rgba32_Blocksizes.png (100%) create mode 100644 tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_R16F.png create mode 100644 tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_R32F.png create mode 100644 tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RG32F.png create mode 100644 tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RG64F.png create mode 100644 tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGB48F.png create mode 100644 tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGB96F.png create mode 100644 tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGBA128F.png create mode 100644 tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGBA64F.png create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_B10g11r11_Ufloat_Packed.png create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_R16_Unorm.png create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_R32_Sfloat.png create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RG32_Unorm.png create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RG48_Sfloat.png create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGB48_Unorm.png create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGB96_Sfloat.png create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGBA128_Sfloat.png create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGBA64_Unorm.png create mode 100644 tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_Rgb9e5_Ufloat_Packed.png diff --git a/src/ImageSharp.Textures/Common/Helpers/FloatHelper.cs b/src/ImageSharp.Textures/Common/Helpers/FloatHelper.cs index 98b8de50..17db12a1 100644 --- a/src/ImageSharp.Textures/Common/Helpers/FloatHelper.cs +++ b/src/ImageSharp.Textures/Common/Helpers/FloatHelper.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Textures.Common.Helpers @@ -14,32 +15,31 @@ internal static class FloatHelper public static uint PackFloatToFloat32(float value) => Unsafe.As(ref value); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float UnpackFloat16ToFloat(uint value) - { - uint result = - ((value >> 15) << 31) | - ((((value >> 10) & 0x1f) - 15 + 127) << 23) | - ((value & 0x3ff) << 13); - return Unsafe.As(ref result); - } + public static float UnpackFloat16ToFloat(uint value) => (float)BitConverter.UInt16BitsToHalf((ushort)value); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint PackFloatToFloat16(float value) - { - uint temp = Unsafe.As(ref value); - return - ((temp >> 31) << 15) | - ((((temp >> 23) & 0xff) - 127 + 15) << 10) | - ((temp & 0x7fffff) >> 13); - } + public static uint PackFloatToFloat16(float value) => BitConverter.HalfToUInt16Bits((Half)value); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float UnpackFloat10ToFloat(uint value) + public static float UnpackFloat10ToFloat(uint value, uint bias = 10) { - uint result = - ((((value >> 5) & 0x1f) - 10 + 127) << 23) | - ((value & 0x1f) << 18); - return Unsafe.As(ref result); + uint e = (value >> 5) & 0x1Fu; + uint m = value & 0x1Fu; + + return e switch + { + // Zero + 0 when m == 0 => 0f, + + // Denormalized + 0 => m * BitConverter.UInt32BitsToSingle((128u - bias - 5u) << 23), + + // Inf/NaN + 31 => BitConverter.UInt32BitsToSingle((0xFFu << 23) | (m << 18)), + + // Normalized + _ => BitConverter.UInt32BitsToSingle(((e + 127u - bias) << 23) | (m << 18)), + }; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -52,12 +52,29 @@ public static uint PackFloatToFloat10(float value) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float UnpackFloat11ToFloat(uint value) + public static float UnpackFloat11ToFloat(uint value, uint bias = 11) { - uint result = - ((((value >> 6) & 0x1f) - 11 + 127) << 23) | - ((value & 0x3f) << 17); - return Unsafe.As(ref result); + uint e = (value >> 6) & 0x1Fu; + uint m = value & 0x3Fu; + + if (e == 0) + { + if (m == 0) + { + return 0f; + } + + // Denormalized: m * 2^(1 - bias - 6) + return m * BitConverter.UInt32BitsToSingle((128u - bias - 6u) << 23); + } + + if (e == 31) + { + uint ieee = (0xFFu << 23) | (m << 17); + return BitConverter.UInt32BitsToSingle(ieee); + } + + return BitConverter.UInt32BitsToSingle(((e + 127u - bias) << 23) | (m << 17)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp.Textures/Formats/Ktx/Enums/GlInternalPixelFormat.cs b/src/ImageSharp.Textures/Formats/Ktx/Enums/GlInternalPixelFormat.cs index 0597c744..3232cadb 100644 --- a/src/ImageSharp.Textures/Formats/Ktx/Enums/GlInternalPixelFormat.cs +++ b/src/ImageSharp.Textures/Formats/Ktx/Enums/GlInternalPixelFormat.cs @@ -45,6 +45,22 @@ internal enum GlInternalPixelFormat : uint Rgba16 = 0x805B, + R16F = 0x822D, + + Rg16F = 0x822F, + + R32F = 0x822E, + + Rg32F = 0x8230, + + Rgb16F = 0x881B, + + Rgba16F = 0x881A, + + Rgb32F = 0x8815, + + Rgba32F = 0x8814, + R8 = 0x8229, R8UnsignedInt = 0x8232, @@ -131,6 +147,11 @@ internal enum GlInternalPixelFormat : uint CompressedSrgb8Alpha8Etc2Eac = 0x9279, + /// + /// ASTC 4x4 block compression. Supports both LDR and HDR content. + /// HDR is determined by block encoding, not a separate format constant. + /// Note: Current decoder may not fully support HDR endpoint modes (2, 3, 7, 11, 14, 15). + /// CompressedRgbaAstc4x4Khr = 0x93B0, CompressedRgbaAstc5x4Khr = 0x93B1, @@ -158,5 +179,37 @@ internal enum GlInternalPixelFormat : uint CompressedRgbaAstc12x10Khr = 0x93BC, CompressedRgbaAstc12x12Khr = 0x93BD, + + /// + /// ASTC 4x4 block compression with sRGB color space. + /// HDR blocks in sRGB formats will decode incorrectly. + /// + CompressedSrgb8Alpha8Astc4x4Khr = 0x93D0, + + CompressedSrgb8Alpha8Astc5x4Khr = 0x93D1, + + CompressedSrgb8Alpha8Astc5x5Khr = 0x93D2, + + CompressedSrgb8Alpha8Astc6x5Khr = 0x93D3, + + CompressedSrgb8Alpha8Astc6x6Khr = 0x93D4, + + CompressedSrgb8Alpha8Astc8x5Khr = 0x93D5, + + CompressedSrgb8Alpha8Astc8x6Khr = 0x93D6, + + CompressedSrgb8Alpha8Astc8x8Khr = 0x93D7, + + CompressedSrgb8Alpha8Astc10x5Khr = 0x93D8, + + CompressedSrgb8Alpha8Astc10x6Khr = 0x93D9, + + CompressedSrgb8Alpha8Astc10x8Khr = 0x93DA, + + CompressedSrgb8Alpha8Astc10x10Khr = 0x93DB, + + CompressedSrgb8Alpha8Astc12x10Khr = 0x93DC, + + CompressedSrgb8Alpha8Astc12x12Khr = 0x93DD, } } diff --git a/src/ImageSharp.Textures/Formats/Ktx/IEndianHandler.cs b/src/ImageSharp.Textures/Formats/Ktx/IEndianHandler.cs new file mode 100644 index 00000000..6b424f26 --- /dev/null +++ b/src/ImageSharp.Textures/Formats/Ktx/IEndianHandler.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Textures.Formats.Ktx +{ + /// + /// Handles endianness conversions for KTX texture data. + /// + internal interface IEndianHandler + { + /// + /// Reads a UInt32 value from a buffer with appropriate endianness. + /// + /// The buffer containing the UInt32 data. + /// The UInt32 value. + uint ReadUInt32(ReadOnlySpan buffer); + + /// + /// Converts pixel data endianness if needed based on the type size. + /// + /// The pixel data to convert. + /// The size of each data element (2 for half-float, 4 for float). + void ConvertPixelData(Span data, uint typeSize); + } +} diff --git a/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs b/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs index 8dbafc62..56bc2697 100644 --- a/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs +++ b/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs @@ -20,11 +20,27 @@ internal class KtxProcessor /// private readonly byte[] buffer = new byte[4]; + /// + /// The endian handler for reading and converting data based on file endianness. + /// + private readonly IEndianHandler endianHandler; + /// /// Initializes a new instance of the class. /// /// The KTX header. - public KtxProcessor(KtxHeader ktxHeader) => this.KtxHeader = ktxHeader; + public KtxProcessor(KtxHeader ktxHeader) + { + this.KtxHeader = ktxHeader; + + bool isFileLittleEndian = ktxHeader.Endianness == KtxEndianness.LittleEndian; + bool isSystemLittleEndian = BitConverter.IsLittleEndian; + + // Use appropriate handler based on whether endianness matches + this.endianHandler = isFileLittleEndian == isSystemLittleEndian + ? new NativeEndianHandler(isFileLittleEndian) + : new SwappingEndianHandler(isFileLittleEndian); + } /// /// Gets the KTX header. @@ -87,32 +103,46 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, uint count) case GlInternalPixelFormat.CompressedRgb8Etc2: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc4x4Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc4x4Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc5x4Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc5x4Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc5x5Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc5x5Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc6x5Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc6x5Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc6x6Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc6x6Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc8x5Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc8x5Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc8x6Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc8x6Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc8x8Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc8x8Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc10x5Khr: - return this.AllocateMipMaps(stream, width, height, count); + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x5Khr: + return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc10x6Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x6Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc10x8Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x8Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc10x10Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x10Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc12x10Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc12x10Khr: return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc12x12Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc12x12Khr: return this.AllocateMipMaps(stream, width, height, count); } @@ -122,7 +152,6 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, uint count) if (this.KtxHeader.GlTypeSize is 2 or 4) { - // TODO: endianess is not respected here. Use stream reader which respects endianess. switch (this.KtxHeader.GlInternalFormat) { case GlInternalPixelFormat.Rgb5A1: @@ -135,6 +164,24 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, uint count) return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.Rgba32UnsignedInt: return this.AllocateMipMaps(stream, width, height, count); + + // HDR floating-point formats + case GlInternalPixelFormat.R16F: + return this.AllocateMipMaps(stream, width, height, count); + case GlInternalPixelFormat.Rg16F: + return this.AllocateMipMaps(stream, width, height, count); + case GlInternalPixelFormat.Rgb16F: + return this.AllocateMipMaps(stream, width, height, count); + case GlInternalPixelFormat.Rgba16F: + return this.AllocateMipMaps(stream, width, height, count); + case GlInternalPixelFormat.R32F: + return this.AllocateMipMaps(stream, width, height, count); + case GlInternalPixelFormat.Rg32F: + return this.AllocateMipMaps(stream, width, height, count); + case GlInternalPixelFormat.Rgb32F: + return this.AllocateMipMaps(stream, width, height, count); + case GlInternalPixelFormat.Rgba32F: + return this.AllocateMipMaps(stream, width, height, count); } } @@ -195,32 +242,46 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height) case GlInternalPixelFormat.CompressedRgb8Etc2: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc4x4Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc4x4Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc5x4Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc5x4Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc5x5Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc5x5Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc6x5Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc6x5Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc6x6Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc6x6Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc8x5Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc8x5Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc8x6Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc8x6Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc8x8Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc8x8Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc10x5Khr: - return this.AllocateCubeMap(stream, width, height); + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x5Khr: + return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc10x6Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x6Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc10x8Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x8Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc10x10Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x10Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc12x10Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc12x10Khr: return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc12x12Khr: + case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc12x12Khr: return this.AllocateCubeMap(stream, width, height); } @@ -229,7 +290,6 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height) if (this.KtxHeader.GlTypeSize is 2 or 4) { - // TODO: endianess is not respected here. Use stream reader which respects endianess. switch (this.KtxHeader.GlInternalFormat) { case GlInternalPixelFormat.Rgb5A1: @@ -242,6 +302,24 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height) return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.Rgba32UnsignedInt: return this.AllocateCubeMap(stream, width, height); + + // HDR floating-point formats + case GlInternalPixelFormat.R16F: + return this.AllocateCubeMap(stream, width, height); + case GlInternalPixelFormat.Rg16F: + return this.AllocateCubeMap(stream, width, height); + case GlInternalPixelFormat.Rgb16F: + return this.AllocateCubeMap(stream, width, height); + case GlInternalPixelFormat.Rgba16F: + return this.AllocateCubeMap(stream, width, height); + case GlInternalPixelFormat.R32F: + return this.AllocateCubeMap(stream, width, height); + case GlInternalPixelFormat.Rg32F: + return this.AllocateCubeMap(stream, width, height); + case GlInternalPixelFormat.Rgb32F: + return this.AllocateCubeMap(stream, width, height); + case GlInternalPixelFormat.Rgba32F: + return this.AllocateCubeMap(stream, width, height); } } @@ -263,15 +341,16 @@ private CubemapTexture AllocateCubeMap(Stream stream, int width, int hei var cubeMapTexture = new CubemapTexture(); var blockFormat = default(TBlock); + for (int i = 0; i < numberOfMipMaps; i++) { var dataForEachFace = this.ReadTextureDataSize(stream); - cubeMapTexture.PositiveX.MipMaps.Add(ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); - cubeMapTexture.NegativeX.MipMaps.Add(ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); - cubeMapTexture.PositiveY.MipMaps.Add(ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); - cubeMapTexture.NegativeY.MipMaps.Add(ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); - cubeMapTexture.PositiveZ.MipMaps.Add(ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); - cubeMapTexture.NegativeZ.MipMaps.Add(ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); + cubeMapTexture.PositiveX.MipMaps.Add(this.ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); + cubeMapTexture.NegativeX.MipMaps.Add(this.ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); + cubeMapTexture.PositiveY.MipMaps.Add(this.ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); + cubeMapTexture.NegativeY.MipMaps.Add(this.ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); + cubeMapTexture.PositiveZ.MipMaps.Add(this.ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); + cubeMapTexture.NegativeZ.MipMaps.Add(this.ReadFaceTexture(stream, width, height, blockFormat, dataForEachFace)); width >>= 1; height >>= 1; @@ -280,11 +359,15 @@ private CubemapTexture AllocateCubeMap(Stream stream, int width, int hei return cubeMapTexture; } - private static MipMap ReadFaceTexture(Stream stream, int width, int height, TBlock blockFormat, uint dataForEachFace) + private MipMap ReadFaceTexture(Stream stream, int width, int height, TBlock blockFormat, uint dataForEachFace) where TBlock : struct, IBlock { byte[] faceData = new byte[dataForEachFace]; ReadTextureData(stream, faceData); + + // Apply endianness conversion if needed + this.endianHandler.ConvertPixelData(faceData, this.KtxHeader.GlTypeSize); + return new MipMap(blockFormat, faceData, width, height); } @@ -316,12 +399,16 @@ private MipMap[] ReadMipMaps(Stream stream, int width, int height, uint var blockFormat = default(TBlock); var mipMaps = new MipMap[count]; + for (int i = 0; i < count; i++) { var pixelDataSize = this.ReadTextureDataSize(stream); byte[] mipMapData = new byte[pixelDataSize]; ReadTextureData(stream, mipMapData); + // Apply endianness conversion if needed + this.endianHandler.ConvertPixelData(mipMapData, this.KtxHeader.GlTypeSize); + mipMaps[i] = new MipMap(blockFormat, mipMapData, width, height); width >>= 1; @@ -348,9 +435,7 @@ private uint ReadTextureDataSize(Stream stream) throw new TextureFormatException("could not read texture data length from the stream"); } - var pixelDataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); - - return pixelDataSize; + return this.endianHandler.ReadUInt32(this.buffer); } } } diff --git a/src/ImageSharp.Textures/Formats/Ktx/NativeEndianHandler.cs b/src/ImageSharp.Textures/Formats/Ktx/NativeEndianHandler.cs new file mode 100644 index 00000000..3f0c6fc0 --- /dev/null +++ b/src/ImageSharp.Textures/Formats/Ktx/NativeEndianHandler.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Textures.Formats.Ktx +{ + /// + /// Handles endianness when file endianness matches system endianness (no conversion needed). + /// + internal sealed class NativeEndianHandler : IEndianHandler + { + private readonly bool isLittleEndian; + + /// + /// Initializes a new instance of the class. + /// + /// Whether the file is little-endian. + public NativeEndianHandler(bool isLittleEndian) + { + this.isLittleEndian = isLittleEndian; + } + + /// + public uint ReadUInt32(ReadOnlySpan buffer) + { + return this.isLittleEndian + ? BinaryPrimitives.ReadUInt32LittleEndian(buffer) + : BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + + /// + public void ConvertPixelData(Span data, uint typeSize) + { + // No conversion needed when endianness matches + } + } +} diff --git a/src/ImageSharp.Textures/Formats/Ktx/SwappingEndianHandler.cs b/src/ImageSharp.Textures/Formats/Ktx/SwappingEndianHandler.cs new file mode 100644 index 00000000..36ac8f4d --- /dev/null +++ b/src/ImageSharp.Textures/Formats/Ktx/SwappingEndianHandler.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Textures.Formats.Ktx +{ + /// + /// Handles endianness when file endianness differs from system endianness (requires byte swapping). + /// + internal sealed class SwappingEndianHandler : IEndianHandler + { + private readonly bool isLittleEndian; + + /// + /// Initializes a new instance of the class. + /// + /// Whether the file is little-endian. + public SwappingEndianHandler(bool isLittleEndian) + { + this.isLittleEndian = isLittleEndian; + } + + /// + public uint ReadUInt32(ReadOnlySpan buffer) + { + return this.isLittleEndian + ? BinaryPrimitives.ReadUInt32LittleEndian(buffer) + : BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + + /// + public void ConvertPixelData(Span data, uint typeSize) + { + if (typeSize == 2) + { + SwapEndian16(data); + } + else if (typeSize == 4) + { + SwapEndian32(data); + } + } + + /// + /// Swaps endianness for 16-bit values in-place. + /// + /// The data to swap. + private static void SwapEndian16(Span data) + { + for (int i = 0; i < data.Length; i += 2) + { + byte temp = data[i]; + data[i] = data[i + 1]; + data[i + 1] = temp; + } + } + + /// + /// Swaps endianness for 32-bit values in-place. + /// + /// The data to swap. + private static void SwapEndian32(Span data) + { + for (int i = 0; i < data.Length; i += 4) + { + byte temp0 = data[i]; + byte temp1 = data[i + 1]; + data[i] = data[i + 3]; + data[i + 1] = data[i + 2]; + data[i + 2] = temp1; + data[i + 3] = temp0; + } + } + } +} diff --git a/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs b/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs index ce516cda..81e3dc2a 100644 --- a/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs +++ b/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs @@ -60,6 +60,8 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, LevelIndex[] return AllocateMipMaps(memoryStream, width, height, levelIndices); case VkFormat.VK_FORMAT_R16_SFLOAT: return AllocateMipMaps(memoryStream, width, height, levelIndices); + case VkFormat.VK_FORMAT_R32_SFLOAT: + return AllocateMipMaps(memoryStream, width, height, levelIndices); case VkFormat.VK_FORMAT_R8G8_UNORM: case VkFormat.VK_FORMAT_R8G8_SNORM: case VkFormat.VK_FORMAT_R8G8_UINT: @@ -98,6 +100,10 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, LevelIndex[] return AllocateMipMaps(memoryStream, width, height, levelIndices); case VkFormat.VK_FORMAT_R32G32B32A32_SFLOAT: return AllocateMipMaps(memoryStream, width, height, levelIndices); + case VkFormat.VK_FORMAT_B10G11R11_UFLOAT_PACK32: + return AllocateMipMaps(memoryStream, width, height, levelIndices); + case VkFormat.VK_FORMAT_E5B9G9R9_UFLOAT_PACK32: + return AllocateMipMaps(memoryStream, width, height, levelIndices); case VkFormat.VK_FORMAT_B8G8R8_UNORM: case VkFormat.VK_FORMAT_B8G8R8_SNORM: case VkFormat.VK_FORMAT_B8G8R8_UINT: @@ -183,7 +189,7 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, LevelIndex[] 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); + 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); @@ -271,6 +277,10 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height, LevelI return AllocateCubeMap(stream, width, height, levelIndices); case VkFormat.VK_FORMAT_R32G32B32A32_SFLOAT: return AllocateCubeMap(stream, width, height, levelIndices); + case VkFormat.VK_FORMAT_B10G11R11_UFLOAT_PACK32: + return AllocateCubeMap(stream, width, height, levelIndices); + case VkFormat.VK_FORMAT_E5B9G9R9_UFLOAT_PACK32: + return AllocateCubeMap(stream, width, height, levelIndices); case VkFormat.VK_FORMAT_B8G8R8_UNORM: case VkFormat.VK_FORMAT_B8G8R8_SNORM: case VkFormat.VK_FORMAT_B8G8R8_UINT: @@ -354,7 +364,7 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height, LevelI 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); + 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); diff --git a/src/ImageSharp.Textures/ImageSharp.Textures.csproj b/src/ImageSharp.Textures/ImageSharp.Textures.csproj index 38c21b95..4560189c 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/PixelFormats/Fp32.cs b/src/ImageSharp.Textures/PixelFormats/Fp32.cs index 828dd0f4..44c694cf 100644 --- a/src/ImageSharp.Textures/PixelFormats/Fp32.cs +++ b/src/ImageSharp.Textures/PixelFormats/Fp32.cs @@ -66,7 +66,7 @@ public Fp32(float x) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromVector4(Vector4 vector) => this.PackedValue = (ushort)Pack(new Vector(vector.X)); + public void FromVector4(Vector4 vector) => this.PackedValue = Pack(new Vector(vector.X)); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -125,7 +125,7 @@ public Fp32(float x) public void FromRgba64(Rgba64 source) => this.FromScaledVector4(source.ToScaledVector4()); /// - public void ToRgba32(ref Rgba32 dest) => throw new NotImplementedException(); + public void ToRgba32(ref Rgba32 dest) => dest.FromScaledVector4(this.ToScaledVector4()); /// /// Expands the packed representation into a . diff --git a/src/ImageSharp.Textures/PixelFormats/Generated/Rgb48Float.cs b/src/ImageSharp.Textures/PixelFormats/Generated/Rgb48Float.cs new file mode 100644 index 00000000..f6a3c8a6 --- /dev/null +++ b/src/ImageSharp.Textures/PixelFormats/Generated/Rgb48Float.cs @@ -0,0 +1,200 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Textures.Common.Helpers; + +namespace SixLabors.ImageSharp.Textures.PixelFormats +{ + /// + /// Pixel type containing three 16-bit float values (half-precision floating-point). + /// The color components are stored in red, green, blue order. + /// + /// Ranges from [0, 0, 0] to [1, 1, 1] in vector form. + /// + /// + [StructLayout(LayoutKind.Explicit)] + public partial struct Rgb48Float : IPixel + { + /// + /// Gets or sets the red component. + /// + [FieldOffset(0)] + public ushort R; + + /// + /// Gets or sets the green component. + /// + [FieldOffset(2)] + public ushort G; + + /// + /// Gets or sets the blue component. + /// + [FieldOffset(4)] + public ushort B; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component as a half-precision float. + /// The green component as a half-precision float. + /// The blue component as a half-precision float. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgb48Float(ushort r, ushort g, ushort b) + { + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgb48Float(float r, float g, float b) + { + this.R = (ushort)FloatHelper.PackFloatToFloat16(r); + this.G = (ushort)FloatHelper.PackFloatToFloat16(g); + this.B = (ushort)FloatHelper.PackFloatToFloat16(b); + } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// + /// True if the parameter is equal to the parameter; otherwise, false. + /// + /// The on the right side of the operand. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgb48Float left, Rgb48Float right) => left.Equals(right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the parameter is not equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgb48Float left, Rgb48Float right) => !left.Equals(right); + + /// + public PixelOperations CreatePixelOperations() => new(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromScaledVector4(Vector4 vector) => this.FromVector4(vector); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToScaledVector4() => this.ToVector4(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromVector4(Vector4 vector) + { + vector = Vector4.Clamp(vector, Vector4.Zero, Vector4.One); + this.R = (ushort)FloatHelper.PackFloatToFloat16(vector.X); + this.G = (ushort)FloatHelper.PackFloatToFloat16(vector.Y); + this.B = (ushort)FloatHelper.PackFloatToFloat16(vector.Z); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToVector4() => new( + FloatHelper.UnpackFloat16ToFloat(this.R), + FloatHelper.UnpackFloat16ToFloat(this.G), + FloatHelper.UnpackFloat16ToFloat(this.B), + 1.0f); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromBgra5551(Bgra5551 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromArgb32(Argb32 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromBgr24(Bgr24 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromBgra32(Bgra32 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromAbgr32(Abgr32 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromL8(L8 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromL16(L16 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromLa16(La16 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromLa32(La32 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgb24(Rgb24 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgba32(Rgba32 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ToRgba32(ref Rgba32 dest) + { + dest.FromScaledVector4(this.ToScaledVector4()); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgb48(Rgb48 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FromRgba64(Rgba64 source) => this.FromScaledVector4(source.ToScaledVector4()); + + /// + public override bool Equals(object? obj) => obj is Rgb48Float other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Rgb48Float other) => + this.R.Equals(other.R) && + this.G.Equals(other.G) && + this.B.Equals(other.B); + + /// + public override string ToString() + { + var vector = this.ToVector4(); + return FormattableString.Invariant($"Rgb48Float({vector.X:#0.##}, {vector.Y:#0.##}, {vector.Z:#0.##})"); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); + } +} diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs index 32ccf740..bf32098b 100644 --- a/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs +++ b/src/ImageSharp.Textures/TextureFormats/Decoding/AstcDecoder.cs @@ -7,6 +7,13 @@ namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding; /// /// ASTC (Adaptive scalable texture compression) decoder for all valid block footprints. +/// +/// HDR Support Status: ASTC supports both LDR and HDR content using the same format constants. +/// HDR is determined by the block's endpoint encoding mode (modes 2, 3, 7, 11, 14, 15). +/// The current decoder may not correctly handle HDR endpoint modes and may produce incorrect +/// colors for HDR blocks. For guaranteed HDR support, use uncompressed formats like R16F, RGBA16F, or RGBA32F. +/// See docs/ASTC-HDR-Support.md for details. +/// /// internal static class AstcDecoder { @@ -21,6 +28,9 @@ internal static class AstcDecoder /// 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. + /// + /// If the block uses HDR endpoint modes (2, 3, 7, 11, 14, 15), the decoded colors may be incorrect. + /// public static void DecodeBlock(ReadOnlySpan blockData, int blockWidth, int blockHeight, Span decodedPixels) { if (blockData.Length != AstcBlockSize) diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/Rgb111110PackedFloat.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/Rgb111110PackedFloat.cs new file mode 100644 index 00000000..044abeeb --- /dev/null +++ b/src/ImageSharp.Textures/TextureFormats/Decoding/Rgb111110PackedFloat.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.Textures.Common.Helpers; + +namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding; + +/// +/// Texture format for the VK_FORMAT_B10G11R11_UFLOAT_PACK32 packed pixel type. +/// Each pixel is a 32-bit unsigned integer containing three unsigned float channels: +/// R (11-bit, bits 0-10), G (11-bit, bits 11-21), B (10-bit, bits 22-31). +/// +internal readonly struct Rgb111110PackedFloat : IBlock +{ + private const uint VulkanExponentBias = 15; + + /// + public int BitsPerPixel => 32; + + /// + public byte PixelDepthBytes => 4; + + /// + public byte DivSize => 1; + + /// + public byte CompressedBytesPerBlock => 4; + + /// + public bool Compressed => false; + + /// + 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) + { + int pixelCount = width * height; + byte[] output = new byte[pixelCount * 16]; + Span inputSpan = blockData.AsSpan(); + Span outputSpan = output.AsSpan(); + + for (int i = 0; i < pixelCount; i++) + { + uint packed = BinaryPrimitives.ReadUInt32LittleEndian(inputSpan[(i * 4)..]); + + float r = FloatHelper.UnpackFloat11ToFloat(packed & 0x7FFu, VulkanExponentBias); + float g = FloatHelper.UnpackFloat11ToFloat((packed >> 11) & 0x7FFu, VulkanExponentBias); + float b = FloatHelper.UnpackFloat10ToFloat((packed >> 22) & 0x3FFu, VulkanExponentBias); + + int outOffset = i * 16; + BinaryPrimitives.WriteSingleLittleEndian(outputSpan[outOffset..], r); + BinaryPrimitives.WriteSingleLittleEndian(outputSpan[(outOffset + 4)..], g); + BinaryPrimitives.WriteSingleLittleEndian(outputSpan[(outOffset + 8)..], b); + BinaryPrimitives.WriteSingleLittleEndian(outputSpan[(outOffset + 12)..], 1.0f); + } + + return output; + } +} diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/Rgb48Float.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/Rgb48Float.cs new file mode 100644 index 00000000..5eddc19c --- /dev/null +++ b/src/ImageSharp.Textures/TextureFormats/Decoding/Rgb48Float.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding +{ + /// + /// Texture for data with three channels (RGB) as 16 bit floats (48 bits total). + /// + internal struct Rgb48Float : IBlock + { + /// + public int BitsPerPixel => 48; + + /// + public byte PixelDepthBytes => 6; + + /// + public byte DivSize => 1; + + /// + public byte CompressedBytesPerBlock => 6; + + /// + public bool Compressed => false; + + /// + 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) => blockData; + } +} diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs index d75b58a9..88a89d5b 100644 --- a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs +++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding; /// /// Texture compressed with RgbaAstc10x5. /// -internal readonly struct RgbaAstc10X5 : IBlock +internal readonly struct RgbaAstc10x5 : IBlock { public static Size BlockSize => new(10, 5); diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/Rgbe9995PackedFloat.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/Rgbe9995PackedFloat.cs new file mode 100644 index 00000000..ca2fb351 --- /dev/null +++ b/src/ImageSharp.Textures/TextureFormats/Decoding/Rgbe9995PackedFloat.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding; + +/// +/// Texture format for the VK_FORMAT_E5B9G9R9_UFLOAT_PACK32 packed pixel type. +/// Each pixel is a 32-bit unsigned integer containing three 9-bit mantissas +/// (R bits 0-8, G bits 9-17, B bits 18-26) and a 5-bit shared exponent (bits 27-31). +/// +internal readonly struct Rgbe9995PackedFloat : IBlock +{ + /// + public int BitsPerPixel => 32; + + /// + public byte PixelDepthBytes => 4; + + /// + public byte DivSize => 1; + + /// + public byte CompressedBytesPerBlock => 4; + + /// + public bool Compressed => false; + + /// + 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) + { + int pixelCount = width * height; + byte[] output = new byte[pixelCount * 16]; + Span inputSpan = blockData.AsSpan(); + Span outputSpan = output.AsSpan(); + + for (int i = 0; i < pixelCount; i++) + { + uint packed = BinaryPrimitives.ReadUInt32LittleEndian(inputSpan[(i * 4)..]); + + uint rm = packed & 0x1FFu; + uint gm = (packed >> 9) & 0x1FFu; + uint bm = (packed >> 18) & 0x1FFu; + uint exponent = (packed >> 27) & 0x1Fu; + + // Construct 2^(exponent - 24) exactly via IEEE bit pattern. + // exponent range [0,31] maps to IEEE exponent [103,134]. + float scale = BitConverter.UInt32BitsToSingle((exponent + 103u) << 23); + + float r = rm * scale; + float g = gm * scale; + float b = bm * scale; + + int outOffset = i * 16; + BinaryPrimitives.WriteSingleLittleEndian(outputSpan[outOffset..], r); + BinaryPrimitives.WriteSingleLittleEndian(outputSpan[(outOffset + 4)..], g); + BinaryPrimitives.WriteSingleLittleEndian(outputSpan[(outOffset + 8)..], b); + BinaryPrimitives.WriteSingleLittleEndian(outputSpan[(outOffset + 12)..], 1.0f); + } + + return output; + } +} diff --git a/tests/ImageSharp.Textures.Astc.Reference.Tests/ImageSharp.Textures.Astc.Reference.Tests.csproj b/tests/ImageSharp.Textures.Astc.Reference.Tests/ImageSharp.Textures.Astc.Reference.Tests.csproj index 855545cd..1fd27a7e 100644 --- a/tests/ImageSharp.Textures.Astc.Reference.Tests/ImageSharp.Textures.Astc.Reference.Tests.csproj +++ b/tests/ImageSharp.Textures.Astc.Reference.Tests/ImageSharp.Textures.Astc.Reference.Tests.csproj @@ -15,6 +15,7 @@ + @@ -23,6 +24,12 @@ + + + + + + diff --git a/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderHdrTests.cs b/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderHdrTests.cs index e93b566d..d55380e5 100644 --- a/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderHdrTests.cs +++ b/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderHdrTests.cs @@ -6,6 +6,10 @@ using SixLabors.ImageSharp.Textures.Compression.Astc; using SixLabors.ImageSharp.Textures.Compression.Astc.Core; using SixLabors.ImageSharp.Textures.Compression.Astc.IO; +using SixLabors.ImageSharp.Textures.Tests; +using SixLabors.ImageSharp.Textures.Tests.Enums; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; namespace SixLabors.ImageSharp.Textures.Astc.Reference.Tests; @@ -13,6 +17,8 @@ namespace SixLabors.ImageSharp.Textures.Astc.Reference.Tests; /// HDR comparison tests between SixLabors.ImageSharp.Textures.Astc and the ARM reference ASTC decoder. /// These validate that SixLabors.ImageSharp.Textures.Astc produces HDR output matching the official ARM implementation. /// +[Trait("Format", "Astc")] +[Trait("Format", "Hdr")] public class ReferenceDecoderHdrTests { public static TheoryData AllFootprintTypes => @@ -35,17 +41,16 @@ public class ReferenceDecoderHdrTests }; [Theory] - [InlineData("HDR-A-1x1")] - [InlineData("hdr-tile")] - [InlineData("LDR-A-1x1")] - [InlineData("ldr-tile")] - public void DecompressHdr_WithHdrImage_ShouldMatch(string basename) + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Hdr_A_1x1)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Hdr_Tile)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Ldr_A_1x1)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Hdr.Ldr_Tile)] + public void DecompressHdr_WithHdrImage_ShouldMatch(TestTextureProvider provider) { - string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "Astc", "HDR", basename + ".astc"); - - byte[] bytes = File.ReadAllBytes(filePath); + byte[] bytes = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(bytes); (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(astcFile.Footprint.Type); + string basename = Path.GetFileNameWithoutExtension(provider.InputFile); Half[] expected = ReferenceDecoder.DecompressHdr( astcFile.Blocks, astcFile.Width, astcFile.Height, blockX, blockY); @@ -56,16 +61,16 @@ public void DecompressHdr_WithHdrImage_ShouldMatch(string basename) } [Theory] - [InlineData("atlas_small_4x4")] - [InlineData("atlas_small_5x5")] - [InlineData("atlas_small_6x6")] - [InlineData("atlas_small_8x8")] - public void DecompressHdr_WithLdrImage_ShouldMatch(string basename) + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_4x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_5x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_6x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_8x8)] + public void DecompressHdr_WithLdrImage_ShouldMatch(TestTextureProvider provider) { - string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "Astc", basename + ".astc"); - byte[] bytes = File.ReadAllBytes(filePath); + byte[] bytes = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(bytes); (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(astcFile.Footprint.Type); + string basename = Path.GetFileNameWithoutExtension(provider.InputFile); Half[] expected = ReferenceDecoder.DecompressHdr( astcFile.Blocks, astcFile.Width, astcFile.Height, blockX, blockY); diff --git a/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderTests.cs b/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderTests.cs index cf685d7d..85bab018 100644 --- a/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderTests.cs +++ b/tests/ImageSharp.Textures.Astc.Reference.Tests/ReferenceDecoderTests.cs @@ -5,6 +5,10 @@ using SixLabors.ImageSharp.Textures.Compression.Astc; using SixLabors.ImageSharp.Textures.Compression.Astc.Core; using SixLabors.ImageSharp.Textures.Compression.Astc.IO; +using SixLabors.ImageSharp.Textures.Tests; +using SixLabors.ImageSharp.Textures.Tests.Enums; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; namespace SixLabors.ImageSharp.Textures.Astc.Reference.Tests; @@ -12,6 +16,7 @@ namespace SixLabors.ImageSharp.Textures.Astc.Reference.Tests; /// LDR comparison tests between SixLabors.ImageSharp.Textures.Astc and the ARM reference ASTC decoder. /// These validate that SixLabors.ImageSharp.Textures.Astc produces output matching the official ARM implementation. /// +[Trait("Format", "Astc")] public class ReferenceDecoderTests { // Per-channel tolerance for RGBA8 comparisons. @@ -38,45 +43,45 @@ public class ReferenceDecoderTests }; [Theory] - [InlineData("atlas_small_4x4")] - [InlineData("atlas_small_5x5")] - [InlineData("atlas_small_6x6")] - [InlineData("atlas_small_8x8")] - [InlineData("checkerboard")] - [InlineData("checkered_4")] - [InlineData("checkered_5")] - [InlineData("checkered_6")] - [InlineData("checkered_7")] - [InlineData("checkered_8")] - [InlineData("checkered_9")] - [InlineData("checkered_10")] - [InlineData("checkered_11")] - [InlineData("checkered_12")] - [InlineData("footprint_4x4")] - [InlineData("footprint_5x4")] - [InlineData("footprint_5x5")] - [InlineData("footprint_6x5")] - [InlineData("footprint_6x6")] - [InlineData("footprint_8x5")] - [InlineData("footprint_8x6")] - [InlineData("footprint_8x8")] - [InlineData("footprint_10x5")] - [InlineData("footprint_10x6")] - [InlineData("footprint_10x8")] - [InlineData("footprint_10x10")] - [InlineData("footprint_12x10")] - [InlineData("footprint_12x12")] - [InlineData("rgb_4x4")] - [InlineData("rgb_5x4")] - [InlineData("rgb_6x6")] - [InlineData("rgb_8x8")] - [InlineData("rgb_12x12")] - public void DecompressLdr_WithImage_ShouldMatch(string basename) + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Checkerboard)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Checkered_4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Checkered_5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Checkered_6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Checkered_7)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Checkered_8)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Checkered_9)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Checkered_10)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Checkered_11)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Checkered_12)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_4x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_5x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_5x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_6x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_6x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_8x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_8x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_8x8)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_10x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_10x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_10x8)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_10x10)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_12x10)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Footprint_12x12)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgb_4x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgb_5x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgb_6x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgb_8x8)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgb_12x12)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_4x4)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_5x5)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_6x6)] + [WithFile(TestTextureFormat.Astc, TestTextureType.Flat, TestTextureTool.AstcEnc, TestImages.Astc.Rgba_8x8)] + public void DecompressLdr_WithImage_ShouldMatch(TestTextureProvider provider) { - string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "Astc", basename + ".astc"); - byte[] bytes = File.ReadAllBytes(filePath); + byte[] bytes = File.ReadAllBytes(provider.InputFile); AstcFile astcFile = AstcFile.FromMemory(bytes); (int blockX, int blockY) = ReferenceDecoder.ToBlockDimensions(astcFile.Footprint.Type); + string basename = Path.GetFileNameWithoutExtension(provider.InputFile); byte[] expected = ReferenceDecoder.DecompressLdr( astcFile.Blocks, astcFile.Width, astcFile.Height, blockX, blockY); diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderFlatTests.cs similarity index 91% rename from tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs rename to tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderFlatTests.cs index fbad9f5b..a1e8d8df 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxAstcDecoderFlatTests.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Textures.Tests.Formats.Ktx; [GroupOutput("Ktx")] [Trait("Format", "Ktx")] [Trait("Format", "Astc")] -public class KtxAstcDecoderTests +public class KtxAstcDecoderFlatTests { private static readonly KtxDecoder KtxDecoder = new(); @@ -36,6 +36,6 @@ public void CanDecode_Rgba32_Blocksizes(TestTextureProvider provider) Image firstMipMapImage = firstMipMap as Image; - firstMipMapImage.CompareToReferenceOutput(provider, appendPixelTypeToFileName: false); + firstMipMapImage.CompareToReferenceOutput(provider); } } diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxHdrDecoderFlatTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxHdrDecoderFlatTests.cs new file mode 100644 index 00000000..c49e5ca3 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxHdrDecoderFlatTests.cs @@ -0,0 +1,195 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Textures.Formats.Ktx; +using SixLabors.ImageSharp.Textures.PixelFormats; +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; + +/// +/// Tests for HDR (High Dynamic Range) formats in KTX files. +/// +[GroupOutput("Ktx")] +[Trait("Format", "Ktx")] +[Trait("Format", "Hdr")] +public class KtxHdrDecoderFlatTests +{ + private static readonly KtxDecoder KtxDecoder = new(); + + [Theory] + [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Hdr.R16)] + public void CanDecode_R16F(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(KtxDecoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + Assert.NotNull(firstMipMap); + Assert.Equal(16, firstMipMap.Width); + Assert.Equal(16, firstMipMap.Height); + Assert.Equal(16, firstMipMap.PixelType.BitsPerPixel); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Hdr.R32)] + public void CanDecode_R32F(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(KtxDecoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + Assert.NotNull(firstMipMap); + Assert.Equal(16, firstMipMap.Width); + Assert.Equal(16, firstMipMap.Height); + Assert.Equal(32, firstMipMap.PixelType.BitsPerPixel); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Hdr.Rg32)] + public void CanDecode_RG32F(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(KtxDecoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + Assert.NotNull(firstMipMap); + Assert.Equal(16, firstMipMap.Width); + Assert.Equal(16, firstMipMap.Height); + Assert.Equal(32, firstMipMap.PixelType.BitsPerPixel); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Hdr.Rg64)] + public void CanDecode_RG64F(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(KtxDecoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + Assert.NotNull(firstMipMap); + Assert.Equal(16, firstMipMap.Width); + Assert.Equal(16, firstMipMap.Height); + Assert.Equal(64, firstMipMap.PixelType.BitsPerPixel); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Hdr.Rgb48)] + public void CanDecode_RGB48F(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(KtxDecoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + Assert.NotNull(firstMipMap); + Assert.Equal(16, firstMipMap.Width); + Assert.Equal(16, firstMipMap.Height); + Assert.Equal(48, firstMipMap.PixelType.BitsPerPixel); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Hdr.Rgb96)] + public void CanDecode_RGB96F(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(KtxDecoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + Assert.NotNull(firstMipMap); + Assert.Equal(16, firstMipMap.Width); + Assert.Equal(16, firstMipMap.Height); + Assert.Equal(96, firstMipMap.PixelType.BitsPerPixel); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + // TODO: This test is failing because the decoded image has 0 alpha, but the png is saved with 1 alpha. + // Not sure if this is an issue with the decoder, or the way the reference image was saved. + // The RGBA32F image has 1 alpha + [Theory] + [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Hdr.Rgba64)] + public void CanDecode_RGBA64F(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(KtxDecoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + Assert.NotNull(firstMipMap); + Assert.Equal(16, firstMipMap.Width); + Assert.Equal(16, firstMipMap.Height); + Assert.Equal(64, firstMipMap.PixelType.BitsPerPixel); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Hdr.Rgba128)] + public void CanDecode_RGBA128F(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(KtxDecoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Assert.NotNull(firstMipMap); + Assert.Equal(16, firstMipMap.Width); + Assert.Equal(16, firstMipMap.Height); + Assert.Equal(128, firstMipMap.PixelType.BitsPerPixel); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2HdrDecoderFlatTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2HdrDecoderFlatTests.cs new file mode 100644 index 00000000..d7fcb05b --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2HdrDecoderFlatTests.cs @@ -0,0 +1,195 @@ +// 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.PixelFormats; +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; + +/// +/// Tests for HDR (High Dynamic Range) formats in KTX2 files. +/// +[GroupOutput("Ktx2")] +[Trait("Format", "Ktx2")] +[Trait("Format", "Hdr")] +public class Ktx2HdrDecoderFlatTests +{ + private static readonly Ktx2Decoder Ktx2Decoder = new(); + + [Theory] + [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Hdr.R16)] + public void CanDecode_R16_Unorm(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(Ktx2Decoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Hdr.Rg32)] + public void CanDecode_RG32_Unorm(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(Ktx2Decoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Hdr.Rgb48)] + public void CanDecode_RGB48_Unorm(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(Ktx2Decoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Hdr.Rgba64)] + public void CanDecode_RGBA64_Unorm(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(Ktx2Decoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Hdr.R32)] + public void CanDecode_R32_Sfloat(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(Ktx2Decoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Hdr.Rg64)] + public void CanDecode_RG48_Sfloat(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(Ktx2Decoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.0003f), provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Hdr.Rgb96)] + public void CanDecode_RGB96_Sfloat(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(Ktx2Decoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.0003f), provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Hdr.Rgba128)] + public void CanDecode_RGBA128_Sfloat(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(Ktx2Decoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.0003f), provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Hdr.Rgb9e5)] + public void CanDecode_Rgb9e5_Ufloat_Packed(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(Ktx2Decoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.0003f), provider); + } + + [Theory] + [WithFile(TestTextureFormat.Ktx2, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx2.Astc.Hdr.B10g11r11)] + public void CanDecode_B10g11r11_Ufloat_Packed(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(Ktx2Decoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.True(flatTexture.MipMaps.Count > 0); + + Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.0003f), provider); + } +} diff --git a/tests/ImageSharp.Textures.Tests/TestImages.cs b/tests/ImageSharp.Textures.Tests/TestImages.cs index 9dc80543..8dacea4d 100644 --- a/tests/ImageSharp.Textures.Tests/TestImages.cs +++ b/tests/ImageSharp.Textures.Tests/TestImages.cs @@ -65,6 +65,18 @@ public static class Astc { public const string Rgb32_8x8 = "astc-rgba32-8x8.ktx"; } + + public static class Hdr + { + public const string R16 = "HDR/hdr-r16.ktx"; + public const string R32 = "HDR/hdr-r32.ktx"; + public const string Rg32 = "HDR/hdr-rg32.ktx"; + public const string Rg64 = "HDR/hdr-rg64.ktx"; + public const string Rgb48 = "HDR/hdr-rgb48.ktx"; + public const string Rgb96 = "HDR/hdr-rgb96.ktx"; + public const string Rgba64 = "HDR/hdr-rgba64.ktx"; + public const string Rgba128 = "HDR/hdr-rgba128.ktx"; + } } public static class Ktx2 @@ -132,6 +144,20 @@ public static class Astc // Cubemap textures public const string Rgb32_Srgb_6x6_Cube = "Cubemap/Astc/astc_ldr_cubemap_6x6.ktx2"; + + public static class Hdr + { + public const string R16 = "Flat/Astc/HDR/hdr-r16-unorm.ktx2"; + public const string Rg32 = "Flat/Astc/HDR/hdr-rg32-unorm.ktx2"; + public const string Rgb48 = "Flat/Astc/HDR/hdr-rgb48-unorm.ktx2"; + public const string Rgba64 = "Flat/Astc/HDR/hdr-rgba64-unorm.ktx2"; + public const string R32 = "Flat/Astc/HDR/hdr-r32-sfloat.ktx2"; + public const string Rg64 = "Flat/Astc/HDR/hdr-rg64-sfloat.ktx2"; + public const string Rgb96 = "Flat/Astc/HDR/hdr-rgb96-sfloat.ktx2"; + public const string Rgba128 = "Flat/Astc/HDR/hdr-rgba128-sfloat.ktx2"; + public const string Rgb9e5 = "Flat/Astc/HDR/hdr-rgb9e5-ufloat.ktx2"; + public const string B10g11r11 = "Flat/Astc/HDR/hdr-b10g11r11-ufloat.ktx2"; + } } } } diff --git a/tests/Images/Input/Ktx/HDR/hdr-r16.ktx b/tests/Images/Input/Ktx/HDR/hdr-r16.ktx new file mode 100644 index 00000000..543c56c3 --- /dev/null +++ b/tests/Images/Input/Ktx/HDR/hdr-r16.ktx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:673dc70925fd38014cafcb442ec9f47fc472b1b5a98f58cdb91179b373e5c337 +size 612 diff --git a/tests/Images/Input/Ktx/HDR/hdr-r32.ktx b/tests/Images/Input/Ktx/HDR/hdr-r32.ktx new file mode 100644 index 00000000..c219d899 --- /dev/null +++ b/tests/Images/Input/Ktx/HDR/hdr-r32.ktx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:838af51c0be998e9dba70ac6ae108a23bb59999bec690bc8514cc8e0722705e8 +size 1124 diff --git a/tests/Images/Input/Ktx/HDR/hdr-rg32.ktx b/tests/Images/Input/Ktx/HDR/hdr-rg32.ktx new file mode 100644 index 00000000..f854a16b --- /dev/null +++ b/tests/Images/Input/Ktx/HDR/hdr-rg32.ktx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b25e42621da7ed4534a3e7d9554fc673dcdac713e6bd87b50f62f4a325c605f +size 1124 diff --git a/tests/Images/Input/Ktx/HDR/hdr-rg64.ktx b/tests/Images/Input/Ktx/HDR/hdr-rg64.ktx new file mode 100644 index 00000000..71adb8b7 --- /dev/null +++ b/tests/Images/Input/Ktx/HDR/hdr-rg64.ktx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9270e24b52b55281d59c40b569a6d0961e22bbdbfef7505094ff312965701fe6 +size 2148 diff --git a/tests/Images/Input/Ktx/HDR/hdr-rgb48.ktx b/tests/Images/Input/Ktx/HDR/hdr-rgb48.ktx new file mode 100644 index 00000000..49031761 --- /dev/null +++ b/tests/Images/Input/Ktx/HDR/hdr-rgb48.ktx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40ecfe7e3a2c7c35752c2af0da5680ea24050c9f67f3a91616fea459b28ab1f8 +size 1636 diff --git a/tests/Images/Input/Ktx/HDR/hdr-rgb96.ktx b/tests/Images/Input/Ktx/HDR/hdr-rgb96.ktx new file mode 100644 index 00000000..aa08b528 --- /dev/null +++ b/tests/Images/Input/Ktx/HDR/hdr-rgb96.ktx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad5d531706a35c64016356fb1225735e4f50e85a4927a9bc03dd539b6c84326e +size 3172 diff --git a/tests/Images/Input/Ktx/HDR/hdr-rgba128.ktx b/tests/Images/Input/Ktx/HDR/hdr-rgba128.ktx new file mode 100644 index 00000000..4f80308a --- /dev/null +++ b/tests/Images/Input/Ktx/HDR/hdr-rgba128.ktx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9e69c9253210520a9858257489df4963e35699b277d7d06fd561f831e47a069 +size 4196 diff --git a/tests/Images/Input/Ktx/HDR/hdr-rgba64.ktx b/tests/Images/Input/Ktx/HDR/hdr-rgba64.ktx new file mode 100644 index 00000000..cc3b110d --- /dev/null +++ b/tests/Images/Input/Ktx/HDR/hdr-rgba64.ktx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7dce9c3758e195c02a896cd2f06d099c58d9ef120c562ed31a808c7916de7742 +size 2148 diff --git a/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-b10g11r11-ufloat.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-b10g11r11-ufloat.ktx2 new file mode 100644 index 00000000..53c0fe7a --- /dev/null +++ b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-b10g11r11-ufloat.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70e007f16feb55b7c01b68a8eff058f2f1122f3376055123362d3c7b2a1f51b9 +size 262372 diff --git a/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-r16-unorm.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-r16-unorm.ktx2 new file mode 100644 index 00000000..0098757a --- /dev/null +++ b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-r16-unorm.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a34d39d10fb88793d415ffca0492957cd854769f2630b04efd48203fe16baa49 +size 131288 diff --git a/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-r32-sfloat.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-r32-sfloat.ktx2 new file mode 100644 index 00000000..6e4a657c --- /dev/null +++ b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-r32-sfloat.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:429fdd2494ea5795b25e1e0eb0f1d839f5e7e7f520cc7b473aeb29ec5fe6ebfd +size 262340 diff --git a/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rg32-unorm.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rg32-unorm.ktx2 new file mode 100644 index 00000000..dd885106 --- /dev/null +++ b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rg32-unorm.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fa4fe7815555ff74f65e20cc563dc21ffb0f6d388c8c0b8f981d090409b7583 +size 262376 diff --git a/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rg64-sfloat.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rg64-sfloat.ktx2 new file mode 100644 index 00000000..179163ad --- /dev/null +++ b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rg64-sfloat.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:623b462927b06d1ded69b26acad09543285c39046d5fbeed88826d34ffa17db0 +size 524504 diff --git a/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb48-unorm.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb48-unorm.ktx2 new file mode 100644 index 00000000..f831a11c --- /dev/null +++ b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb48-unorm.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5746ab34c636b4287a5ba12b35c4fa5738ba0bffe60464552706e3fae43266de +size 393468 diff --git a/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb96-sfloat.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb96-sfloat.ktx2 new file mode 100644 index 00000000..d83695af --- /dev/null +++ b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb96-sfloat.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84219133e4c6e57a5fc3eaef4ad0718a35db84a8db25ba81e83713d0f7606f98 +size 786660 diff --git a/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb9e5-ufloat.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb9e5-ufloat.ktx2 new file mode 100644 index 00000000..e55642e1 --- /dev/null +++ b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgb9e5-ufloat.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9f15010ffe354d5971aee92f4fa7515525a311c29ba9163cf78f688c1339db4 +size 262420 diff --git a/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgba128-sfloat.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgba128-sfloat.ktx2 new file mode 100644 index 00000000..f963de5f --- /dev/null +++ b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgba128-sfloat.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2967a358889f07748f7f5d7cc6a92098286cf92ba4587e093c954e905cf93322 +size 1048832 diff --git a/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgba64-unorm.ktx2 b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgba64-unorm.ktx2 new file mode 100644 index 00000000..468d4653 --- /dev/null +++ b/tests/Images/Input/Ktx2/Flat/Astc/HDR/hdr-rgba64-unorm.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:781a6e0a36cac8ba57fbebb69937a849f3c192f18836f0648de2675199f51c1d +size 524552 diff --git a/tests/Images/ReferenceOutput/Ktx/KtxAstcDecoderTests/CanDecode_Rgba32_Blocksizes.png b/tests/Images/ReferenceOutput/Ktx/KtxAstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes.png similarity index 100% rename from tests/Images/ReferenceOutput/Ktx/KtxAstcDecoderTests/CanDecode_Rgba32_Blocksizes.png rename to tests/Images/ReferenceOutput/Ktx/KtxAstcDecoderFlatTests/CanDecode_Rgba32_Blocksizes.png diff --git a/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_R16F.png b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_R16F.png new file mode 100644 index 00000000..9211d075 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_R16F.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfd253f555e9d6b5a3493c93f6bfdfcfa7d9cab759d7994e544dd5b72f0753a9 +size 107 diff --git a/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_R32F.png b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_R32F.png new file mode 100644 index 00000000..54679af8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_R32F.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0eace3f18dd415ea9b2f2526337ccde3be2ab059c7c18c1af180e9417faaabe +size 106 diff --git a/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RG32F.png b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RG32F.png new file mode 100644 index 00000000..78fb7d17 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RG32F.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c9ae3c313d3f05f9e7be64a1ad3a4c83adb97825f077172ef940b37b1d8a737 +size 106 diff --git a/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RG64F.png b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RG64F.png new file mode 100644 index 00000000..78fb7d17 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RG64F.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c9ae3c313d3f05f9e7be64a1ad3a4c83adb97825f077172ef940b37b1d8a737 +size 106 diff --git a/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGB48F.png b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGB48F.png new file mode 100644 index 00000000..78fb7d17 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGB48F.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c9ae3c313d3f05f9e7be64a1ad3a4c83adb97825f077172ef940b37b1d8a737 +size 106 diff --git a/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGB96F.png b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGB96F.png new file mode 100644 index 00000000..78fb7d17 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGB96F.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c9ae3c313d3f05f9e7be64a1ad3a4c83adb97825f077172ef940b37b1d8a737 +size 106 diff --git a/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGBA128F.png b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGBA128F.png new file mode 100644 index 00000000..78fb7d17 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGBA128F.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c9ae3c313d3f05f9e7be64a1ad3a4c83adb97825f077172ef940b37b1d8a737 +size 106 diff --git a/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGBA64F.png b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGBA64F.png new file mode 100644 index 00000000..78fb7d17 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx/KtxHdrDecoderFlatTests/CanDecode_RGBA64F.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c9ae3c313d3f05f9e7be64a1ad3a4c83adb97825f077172ef940b37b1d8a737 +size 106 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_B10g11r11_Ufloat_Packed.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_B10g11r11_Ufloat_Packed.png new file mode 100644 index 00000000..fcdbdb7f --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_B10g11r11_Ufloat_Packed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc67fed3d858bd56a456c75c69c367bde658e68e0795bced41aa29fb5a0f5e7c +size 227017 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_R16_Unorm.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_R16_Unorm.png new file mode 100644 index 00000000..a7044824 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_R16_Unorm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0b7058fce48a62a5eb3d625d25949f2c01504a0f236fd1e1cbe0ff272412f42 +size 94158 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_R32_Sfloat.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_R32_Sfloat.png new file mode 100644 index 00000000..3d255754 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_R32_Sfloat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a228e1639a530072e7e9f4d10cf1804ba8a7ac65399fdcde2cb8ebc793a177b +size 154831 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RG32_Unorm.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RG32_Unorm.png new file mode 100644 index 00000000..2486d47c --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RG32_Unorm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27b8d7aae11c12235a8d42854779c6b6b19664078142771be5e1ebc60275f14d +size 215160 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RG48_Sfloat.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RG48_Sfloat.png new file mode 100644 index 00000000..443964c1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RG48_Sfloat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d22797d7ebb6eff8ddbda0071f16d9449e426366dac982df2455478df2796c6c +size 192522 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGB48_Unorm.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGB48_Unorm.png new file mode 100644 index 00000000..cff17484 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGB48_Unorm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e32e01525b5a69c1f1a8b49667a8acad6db847c72792b3dbbe62a0582344d6f +size 268328 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGB96_Sfloat.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGB96_Sfloat.png new file mode 100644 index 00000000..f3e8944c --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGB96_Sfloat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:877f98dd24091056b6cc6d01c702710f09d1bb028940bbde2cee6d77ff0fedf1 +size 246275 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGBA128_Sfloat.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGBA128_Sfloat.png new file mode 100644 index 00000000..fd3a220d --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGBA128_Sfloat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b3c540284ff2a69196867f7f0a3ae9a68f8b79d6e55109bd2dbaf77b13815e1 +size 275826 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGBA64_Unorm.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGBA64_Unorm.png new file mode 100644 index 00000000..cff17484 --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_RGBA64_Unorm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e32e01525b5a69c1f1a8b49667a8acad6db847c72792b3dbbe62a0582344d6f +size 268328 diff --git a/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_Rgb9e5_Ufloat_Packed.png b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_Rgb9e5_Ufloat_Packed.png new file mode 100644 index 00000000..7a4456fd --- /dev/null +++ b/tests/Images/ReferenceOutput/Ktx2/Ktx2HdrDecoderFlatTests/CanDecode_Rgb9e5_Ufloat_Packed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b29b3f9588a89f41e6d4e0c47c9174fa343fe7caf8efb641f0f23b6dbd24a773 +size 187544 From 724bd619fd09a6de90ab506ab07ca1f9aac6835b Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:53:40 +0100 Subject: [PATCH 05/10] Fix overflow issue --- .../PixelFormats/Generated/D32_FLOAT_S8X24_UINT.cs | 5 ++--- .../PixelFormats/Generated/PixelGenerator.ignore | 14 ++++++-------- .../PixelFormats/Generated/Rgba64Float.cs | 9 ++++----- .../Formats/Ktx/KtxHdrDecoderFlatTests.cs | 3 --- 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp.Textures/PixelFormats/Generated/D32_FLOAT_S8X24_UINT.cs b/src/ImageSharp.Textures/PixelFormats/Generated/D32_FLOAT_S8X24_UINT.cs index 5c8c2fb6..8d9760ab 100644 --- a/src/ImageSharp.Textures/PixelFormats/Generated/D32_FLOAT_S8X24_UINT.cs +++ b/src/ImageSharp.Textures/PixelFormats/Generated/D32_FLOAT_S8X24_UINT.cs @@ -171,9 +171,8 @@ public override string ToString() private static ulong Pack(ref Vector2 vector) { vector = Vector2.Clamp(vector, Vector2.Zero, Vector2.One); - return (ulong)( - (uint)FloatHelper.PackFloatToFloat32(vector.X) - | (((uint)Math.Round(vector.Y * 255F) & 255) << 32)); + return (ulong)FloatHelper.PackFloatToFloat32(vector.X) + | ((ulong)((uint)Math.Round(vector.Y * 255F) & 255) << 32); } } } diff --git a/src/ImageSharp.Textures/PixelFormats/Generated/PixelGenerator.ignore b/src/ImageSharp.Textures/PixelFormats/Generated/PixelGenerator.ignore index a36f7e81..e33af5c6 100644 --- a/src/ImageSharp.Textures/PixelFormats/Generated/PixelGenerator.ignore +++ b/src/ImageSharp.Textures/PixelFormats/Generated/PixelGenerator.ignore @@ -904,11 +904,10 @@ namespace SixLabors.ImageSharp.Textures.PixelFormats private static ulong Pack(ref Vector4 vector) { vector = Vector4.Clamp(vector, Vector4.Zero, Vector4.One); - return (ulong)( - (uint)FloatHelper.PackFloatToFloat16(vector.X) - | ((uint)FloatHelper.PackFloatToFloat16(vector.Y) << 16) - | ((uint)FloatHelper.PackFloatToFloat16(vector.Z) << 32) - | ((uint)FloatHelper.PackFloatToFloat16(vector.W) << 48)); + return (ulong)FloatHelper.PackFloatToFloat16(vector.X) + | ((ulong)FloatHelper.PackFloatToFloat16(vector.Y) << 16) + | ((ulong)FloatHelper.PackFloatToFloat16(vector.Z) << 32) + | ((ulong)FloatHelper.PackFloatToFloat16(vector.W) << 48); } } } @@ -1598,9 +1597,8 @@ namespace SixLabors.ImageSharp.Textures.PixelFormats private static ulong Pack(ref Vector2 vector) { vector = Vector2.Clamp(vector, Vector2.Zero, Vector2.One); - return (ulong)( - (uint)FloatHelper.PackFloatToFloat32(vector.X) - | (((uint)Math.Round(vector.Y * 255F) & 255) << 32)); + return (ulong)FloatHelper.PackFloatToFloat32(vector.X) + | ((ulong)((uint)Math.Round(vector.Y * 255F) & 255) << 32); } } } diff --git a/src/ImageSharp.Textures/PixelFormats/Generated/Rgba64Float.cs b/src/ImageSharp.Textures/PixelFormats/Generated/Rgba64Float.cs index 8cd8db4f..8be2ec6b 100644 --- a/src/ImageSharp.Textures/PixelFormats/Generated/Rgba64Float.cs +++ b/src/ImageSharp.Textures/PixelFormats/Generated/Rgba64Float.cs @@ -171,11 +171,10 @@ public override string ToString() private static ulong Pack(ref Vector4 vector) { vector = Vector4.Clamp(vector, Vector4.Zero, Vector4.One); - return (ulong)( - (uint)FloatHelper.PackFloatToFloat16(vector.X) - | ((uint)FloatHelper.PackFloatToFloat16(vector.Y) << 16) - | ((uint)FloatHelper.PackFloatToFloat16(vector.Z) << 32) - | ((uint)FloatHelper.PackFloatToFloat16(vector.W) << 48)); + return (ulong)FloatHelper.PackFloatToFloat16(vector.X) + | ((ulong)FloatHelper.PackFloatToFloat16(vector.Y) << 16) + | ((ulong)FloatHelper.PackFloatToFloat16(vector.Z) << 32) + | ((ulong)FloatHelper.PackFloatToFloat16(vector.W) << 48); } } } diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxHdrDecoderFlatTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxHdrDecoderFlatTests.cs index c49e5ca3..d5803904 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxHdrDecoderFlatTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxHdrDecoderFlatTests.cs @@ -147,9 +147,6 @@ public void CanDecode_RGB96F(TestTextureProvider provider) firstMipMapImage.CompareToReferenceOutput(provider); } - // TODO: This test is failing because the decoded image has 0 alpha, but the png is saved with 1 alpha. - // Not sure if this is an issue with the decoder, or the way the reference image was saved. - // The RGBA32F image has 1 alpha [Theory] [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.ToKtx, TestImages.Ktx.Hdr.Rgba64)] public void CanDecode_RGBA64F(TestTextureProvider provider) From abd3b4cbd3c298ae39c57ca64c4418000579b2f3 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:56:23 +0100 Subject: [PATCH 06/10] Move KTX1 test images to subdirectories --- tests/ImageSharp.Textures.Tests/TestImages.cs | 20 +++++++++---------- .../Ktx/{ => Cubemap}/cubemap ll has-mips.ktx | 0 .../Ktx/{ => Cubemap}/cubemap ll no-mips.ktx | 0 .../Ktx/{ => Cubemap}/cubemap ul has-mips.ktx | 0 .../Ktx/{ => Cubemap}/cubemap ul no-mips.ktx | 0 .../Ktx/{ => Flat/Astc}/astc-rgba32-8x8.ktx | 0 .../Input/Ktx/{ => Flat}/HDR/hdr-r16.ktx | 0 .../Input/Ktx/{ => Flat}/HDR/hdr-r32.ktx | 0 .../Input/Ktx/{ => Flat}/HDR/hdr-rg32.ktx | 0 .../Input/Ktx/{ => Flat}/HDR/hdr-rg64.ktx | 0 .../Input/Ktx/{ => Flat}/HDR/hdr-rgb48.ktx | 0 .../Input/Ktx/{ => Flat}/HDR/hdr-rgb96.ktx | 0 .../Input/Ktx/{ => Flat}/HDR/hdr-rgba128.ktx | 0 .../Input/Ktx/{ => Flat}/HDR/hdr-rgba64.ktx | 0 .../Input/Ktx/{ => Flat}/flat ll has-mips.ktx | 0 .../Input/Ktx/{ => Flat}/flat ll no-mips.ktx | 0 .../Input/Ktx/{ => Flat}/flat ul has-mips.ktx | 0 .../Input/Ktx/{ => Flat}/flat ul no-mips.ktx | 0 .../{ => Flat}/flat-pot-alpha ll has-mips.ktx | 0 .../{ => Flat}/flat-pot-alpha ll no-mips.ktx | 0 .../{ => Flat}/flat-pot-alpha ul has-mips.ktx | 0 .../{ => Flat}/flat-pot-alpha ul no-mips.ktx | 0 .../flat-rect-alpha ll has-mips.ktx | 0 .../{ => Flat}/flat-rect-alpha ll no-mips.ktx | 0 .../flat-rect-alpha ul has-mips.ktx | 0 .../{ => Flat}/flat-rect-alpha ul no-mips.ktx | 0 .../flat-square-alpha ll has-mips.ktx | 0 .../flat-square-alpha ll no-mips.ktx | 0 .../flat-square-alpha ul has-mips.ktx | 0 .../flat-square-alpha ul no-mips.ktx | 0 .../Images/Input/Ktx/{ => Flat}/rgba8888.ktx | 0 31 files changed, 10 insertions(+), 10 deletions(-) rename tests/Images/Input/Ktx/{ => Cubemap}/cubemap ll has-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Cubemap}/cubemap ll no-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Cubemap}/cubemap ul has-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Cubemap}/cubemap ul no-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat/Astc}/astc-rgba32-8x8.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/HDR/hdr-r16.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/HDR/hdr-r32.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/HDR/hdr-rg32.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/HDR/hdr-rg64.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/HDR/hdr-rgb48.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/HDR/hdr-rgb96.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/HDR/hdr-rgba128.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/HDR/hdr-rgba64.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat ll has-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat ll no-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat ul has-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat ul no-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-pot-alpha ll has-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-pot-alpha ll no-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-pot-alpha ul has-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-pot-alpha ul no-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-rect-alpha ll has-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-rect-alpha ll no-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-rect-alpha ul has-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-rect-alpha ul no-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-square-alpha ll has-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-square-alpha ll no-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-square-alpha ul has-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/flat-square-alpha ul no-mips.ktx (100%) rename tests/Images/Input/Ktx/{ => Flat}/rgba8888.ktx (100%) diff --git a/tests/ImageSharp.Textures.Tests/TestImages.cs b/tests/ImageSharp.Textures.Tests/TestImages.cs index 8dacea4d..a984f450 100644 --- a/tests/ImageSharp.Textures.Tests/TestImages.cs +++ b/tests/ImageSharp.Textures.Tests/TestImages.cs @@ -59,23 +59,23 @@ public static class Hdr public static class Ktx { - public const string Rgba = "rgba8888.ktx"; + public const string Rgba = "Flat/rgba8888.ktx"; public static class Astc { - public const string Rgb32_8x8 = "astc-rgba32-8x8.ktx"; + public const string Rgb32_8x8 = "Flat/Astc/astc-rgba32-8x8.ktx"; } public static class Hdr { - public const string R16 = "HDR/hdr-r16.ktx"; - public const string R32 = "HDR/hdr-r32.ktx"; - public const string Rg32 = "HDR/hdr-rg32.ktx"; - public const string Rg64 = "HDR/hdr-rg64.ktx"; - public const string Rgb48 = "HDR/hdr-rgb48.ktx"; - public const string Rgb96 = "HDR/hdr-rgb96.ktx"; - public const string Rgba64 = "HDR/hdr-rgba64.ktx"; - public const string Rgba128 = "HDR/hdr-rgba128.ktx"; + public const string R16 = "Flat/HDR/hdr-r16.ktx"; + public const string R32 = "Flat/HDR/hdr-r32.ktx"; + public const string Rg32 = "Flat/HDR/hdr-rg32.ktx"; + public const string Rg64 = "Flat/HDR/hdr-rg64.ktx"; + public const string Rgb48 = "Flat/HDR/hdr-rgb48.ktx"; + public const string Rgb96 = "Flat/HDR/hdr-rgb96.ktx"; + public const string Rgba64 = "Flat/HDR/hdr-rgba64.ktx"; + public const string Rgba128 = "Flat/HDR/hdr-rgba128.ktx"; } } diff --git a/tests/Images/Input/Ktx/cubemap ll has-mips.ktx b/tests/Images/Input/Ktx/Cubemap/cubemap ll has-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/cubemap ll has-mips.ktx rename to tests/Images/Input/Ktx/Cubemap/cubemap ll has-mips.ktx diff --git a/tests/Images/Input/Ktx/cubemap ll no-mips.ktx b/tests/Images/Input/Ktx/Cubemap/cubemap ll no-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/cubemap ll no-mips.ktx rename to tests/Images/Input/Ktx/Cubemap/cubemap ll no-mips.ktx diff --git a/tests/Images/Input/Ktx/cubemap ul has-mips.ktx b/tests/Images/Input/Ktx/Cubemap/cubemap ul has-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/cubemap ul has-mips.ktx rename to tests/Images/Input/Ktx/Cubemap/cubemap ul has-mips.ktx diff --git a/tests/Images/Input/Ktx/cubemap ul no-mips.ktx b/tests/Images/Input/Ktx/Cubemap/cubemap ul no-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/cubemap ul no-mips.ktx rename to tests/Images/Input/Ktx/Cubemap/cubemap ul no-mips.ktx diff --git a/tests/Images/Input/Ktx/astc-rgba32-8x8.ktx b/tests/Images/Input/Ktx/Flat/Astc/astc-rgba32-8x8.ktx similarity index 100% rename from tests/Images/Input/Ktx/astc-rgba32-8x8.ktx rename to tests/Images/Input/Ktx/Flat/Astc/astc-rgba32-8x8.ktx diff --git a/tests/Images/Input/Ktx/HDR/hdr-r16.ktx b/tests/Images/Input/Ktx/Flat/HDR/hdr-r16.ktx similarity index 100% rename from tests/Images/Input/Ktx/HDR/hdr-r16.ktx rename to tests/Images/Input/Ktx/Flat/HDR/hdr-r16.ktx diff --git a/tests/Images/Input/Ktx/HDR/hdr-r32.ktx b/tests/Images/Input/Ktx/Flat/HDR/hdr-r32.ktx similarity index 100% rename from tests/Images/Input/Ktx/HDR/hdr-r32.ktx rename to tests/Images/Input/Ktx/Flat/HDR/hdr-r32.ktx diff --git a/tests/Images/Input/Ktx/HDR/hdr-rg32.ktx b/tests/Images/Input/Ktx/Flat/HDR/hdr-rg32.ktx similarity index 100% rename from tests/Images/Input/Ktx/HDR/hdr-rg32.ktx rename to tests/Images/Input/Ktx/Flat/HDR/hdr-rg32.ktx diff --git a/tests/Images/Input/Ktx/HDR/hdr-rg64.ktx b/tests/Images/Input/Ktx/Flat/HDR/hdr-rg64.ktx similarity index 100% rename from tests/Images/Input/Ktx/HDR/hdr-rg64.ktx rename to tests/Images/Input/Ktx/Flat/HDR/hdr-rg64.ktx diff --git a/tests/Images/Input/Ktx/HDR/hdr-rgb48.ktx b/tests/Images/Input/Ktx/Flat/HDR/hdr-rgb48.ktx similarity index 100% rename from tests/Images/Input/Ktx/HDR/hdr-rgb48.ktx rename to tests/Images/Input/Ktx/Flat/HDR/hdr-rgb48.ktx diff --git a/tests/Images/Input/Ktx/HDR/hdr-rgb96.ktx b/tests/Images/Input/Ktx/Flat/HDR/hdr-rgb96.ktx similarity index 100% rename from tests/Images/Input/Ktx/HDR/hdr-rgb96.ktx rename to tests/Images/Input/Ktx/Flat/HDR/hdr-rgb96.ktx diff --git a/tests/Images/Input/Ktx/HDR/hdr-rgba128.ktx b/tests/Images/Input/Ktx/Flat/HDR/hdr-rgba128.ktx similarity index 100% rename from tests/Images/Input/Ktx/HDR/hdr-rgba128.ktx rename to tests/Images/Input/Ktx/Flat/HDR/hdr-rgba128.ktx diff --git a/tests/Images/Input/Ktx/HDR/hdr-rgba64.ktx b/tests/Images/Input/Ktx/Flat/HDR/hdr-rgba64.ktx similarity index 100% rename from tests/Images/Input/Ktx/HDR/hdr-rgba64.ktx rename to tests/Images/Input/Ktx/Flat/HDR/hdr-rgba64.ktx diff --git a/tests/Images/Input/Ktx/flat ll has-mips.ktx b/tests/Images/Input/Ktx/Flat/flat ll has-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat ll has-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat ll has-mips.ktx diff --git a/tests/Images/Input/Ktx/flat ll no-mips.ktx b/tests/Images/Input/Ktx/Flat/flat ll no-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat ll no-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat ll no-mips.ktx diff --git a/tests/Images/Input/Ktx/flat ul has-mips.ktx b/tests/Images/Input/Ktx/Flat/flat ul has-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat ul has-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat ul has-mips.ktx diff --git a/tests/Images/Input/Ktx/flat ul no-mips.ktx b/tests/Images/Input/Ktx/Flat/flat ul no-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat ul no-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat ul no-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-pot-alpha ll has-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-pot-alpha ll has-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-pot-alpha ll has-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-pot-alpha ll has-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-pot-alpha ll no-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-pot-alpha ll no-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-pot-alpha ll no-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-pot-alpha ll no-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-pot-alpha ul has-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-pot-alpha ul has-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-pot-alpha ul has-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-pot-alpha ul has-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-pot-alpha ul no-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-pot-alpha ul no-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-pot-alpha ul no-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-pot-alpha ul no-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-rect-alpha ll has-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-rect-alpha ll has-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-rect-alpha ll has-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-rect-alpha ll has-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-rect-alpha ll no-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-rect-alpha ll no-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-rect-alpha ll no-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-rect-alpha ll no-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-rect-alpha ul has-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-rect-alpha ul has-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-rect-alpha ul has-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-rect-alpha ul has-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-rect-alpha ul no-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-rect-alpha ul no-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-rect-alpha ul no-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-rect-alpha ul no-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-square-alpha ll has-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-square-alpha ll has-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-square-alpha ll has-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-square-alpha ll has-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-square-alpha ll no-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-square-alpha ll no-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-square-alpha ll no-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-square-alpha ll no-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-square-alpha ul has-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-square-alpha ul has-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-square-alpha ul has-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-square-alpha ul has-mips.ktx diff --git a/tests/Images/Input/Ktx/flat-square-alpha ul no-mips.ktx b/tests/Images/Input/Ktx/Flat/flat-square-alpha ul no-mips.ktx similarity index 100% rename from tests/Images/Input/Ktx/flat-square-alpha ul no-mips.ktx rename to tests/Images/Input/Ktx/Flat/flat-square-alpha ul no-mips.ktx diff --git a/tests/Images/Input/Ktx/rgba8888.ktx b/tests/Images/Input/Ktx/Flat/rgba8888.ktx similarity index 100% rename from tests/Images/Input/Ktx/rgba8888.ktx rename to tests/Images/Input/Ktx/Flat/rgba8888.ktx From 86ff81a3a57548187b49999e034e8b32e75b6c0b Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:00:57 +0100 Subject: [PATCH 07/10] Remove unused AstcSharp reference --- src/ImageSharp.Textures/ImageSharp.Textures.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp.Textures/ImageSharp.Textures.csproj b/src/ImageSharp.Textures/ImageSharp.Textures.csproj index 4560189c..38c21b95 100644 --- a/src/ImageSharp.Textures/ImageSharp.Textures.csproj +++ b/src/ImageSharp.Textures/ImageSharp.Textures.csproj @@ -39,7 +39,6 @@ - From 94d0f721ae3c0034663da26e9713be9327a8e6ee Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:23:33 +0100 Subject: [PATCH 08/10] Small fixes and variable naming --- .../Compression/Astc/ColorEncoding/EndpointEncoder.cs | 2 +- .../Astc/ColorEncoding/EndpointEncodingMode.cs | 2 +- .../ColorEncoding/EndpointEncodingModeExtensions.cs | 2 +- .../Compression/Astc/IO/AstcFile.cs | 2 +- src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs | 4 ++-- .../Formats/Ktx2/Ktx2DecoderCore.cs | 2 +- src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs | 4 ++-- .../TextureFormats/Decoding/RgbaAstc10x5.cs | 2 +- .../TextureFormats/Decoding/RgbaAstc12x12.cs | 2 +- .../AstcDecodingBenchmark.cs | 4 ++-- .../Formats/Astc/EndpointCodecTests.cs | 10 +++++----- .../Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs | 2 ++ 12 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncoder.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncoder.cs index b55a02a7..9cbe8c1f 100644 --- a/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncoder.cs +++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncoder.cs @@ -140,7 +140,7 @@ public static bool EncodeColorsForMode(Rgba32 endpointLowRgba, Rgba32 endpointHi break; } - case EndpointEncodingMode.DirectRbg: + case EndpointEncodingMode.DirectRgb: case EndpointEncodingMode.DirectRgba: return EncodeColorsRGBA(endpointLowRgba, endpointHighRgba, maxValue, encodingMode == EndpointEncodingMode.DirectRgba, out astcMode, values); default: diff --git a/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingMode.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingMode.cs index b398fa33..adeb4bc6 100644 --- a/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingMode.cs +++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingMode.cs @@ -9,6 +9,6 @@ internal enum EndpointEncodingMode DirectLumaAlpha, BaseScaleRgb, BaseScaleRgba, - DirectRbg, + DirectRgb, DirectRgba } diff --git a/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingModeExtensions.cs b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingModeExtensions.cs index 76fb4a68..3f0597f3 100644 --- a/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingModeExtensions.cs +++ b/src/ImageSharp.Textures/Compression/Astc/ColorEncoding/EndpointEncodingModeExtensions.cs @@ -9,7 +9,7 @@ internal static class EndpointEncodingModeExtensions { EndpointEncodingMode.DirectLuma => 2, EndpointEncodingMode.DirectLumaAlpha or EndpointEncodingMode.BaseScaleRgb => 4, - EndpointEncodingMode.DirectRbg or EndpointEncodingMode.BaseScaleRgba => 6, + EndpointEncodingMode.DirectRgb or EndpointEncodingMode.BaseScaleRgba => 6, _ => 8 }; } diff --git a/src/ImageSharp.Textures/Compression/Astc/IO/AstcFile.cs b/src/ImageSharp.Textures/Compression/Astc/IO/AstcFile.cs index f0c64ee4..2388f24d 100644 --- a/src/ImageSharp.Textures/Compression/Astc/IO/AstcFile.cs +++ b/src/ImageSharp.Textures/Compression/Astc/IO/AstcFile.cs @@ -64,6 +64,6 @@ public static AstcFile FromMemory(byte[] data) (10, 10) => Footprint.FromFootprintType(FootprintType.Footprint10x10), (12, 10) => Footprint.FromFootprintType(FootprintType.Footprint12x10), (12, 12) => Footprint.FromFootprintType(FootprintType.Footprint12x12), - _ => throw new ArgumentOutOfRangeException($"Unsupported block dimensions: {this.header.BlockWidth}x{this.header.BlockHeight}"), + _ => throw new NotSupportedException($"Unsupported block dimensions: {this.header.BlockWidth}x{this.header.BlockHeight}"), }; } diff --git a/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs b/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs index 56bc2697..3ea03401 100644 --- a/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs +++ b/src/ImageSharp.Textures/Formats/Ktx/KtxProcessor.cs @@ -128,7 +128,7 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, uint count) return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc10x5Khr: case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x5Khr: - return this.AllocateMipMaps(stream, width, height, count); + return this.AllocateMipMaps(stream, width, height, count); case GlInternalPixelFormat.CompressedRgbaAstc10x6Khr: case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x6Khr: return this.AllocateMipMaps(stream, width, height, count); @@ -267,7 +267,7 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height) return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc10x5Khr: case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x5Khr: - return this.AllocateCubeMap(stream, width, height); + return this.AllocateCubeMap(stream, width, height); case GlInternalPixelFormat.CompressedRgbaAstc10x6Khr: case GlInternalPixelFormat.CompressedSrgb8Alpha8Astc10x6Khr: return this.AllocateCubeMap(stream, width, height); diff --git a/src/ImageSharp.Textures/Formats/Ktx2/Ktx2DecoderCore.cs b/src/ImageSharp.Textures/Formats/Ktx2/Ktx2DecoderCore.cs index b5367090..0942b48a 100644 --- a/src/ImageSharp.Textures/Formats/Ktx2/Ktx2DecoderCore.cs +++ b/src/ImageSharp.Textures/Formats/Ktx2/Ktx2DecoderCore.cs @@ -113,7 +113,7 @@ public Texture DecodeTexture(Stream stream) } } - if (stream.Position < maxEndPosition && stream.CanSeek) + if (stream.CanSeek && stream.Position < maxEndPosition) { stream.Position = maxEndPosition; } diff --git a/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs b/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs index 81e3dc2a..a6c7be1d 100644 --- a/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs +++ b/src/ImageSharp.Textures/Formats/Ktx2/Ktx2Processor.cs @@ -189,7 +189,7 @@ public MipMap[] DecodeMipMaps(Stream stream, int width, int height, LevelIndex[] 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); + 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); @@ -364,7 +364,7 @@ public CubemapTexture DecodeCubeMap(Stream stream, int width, int height, LevelI 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); + 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); diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs index 88a89d5b..d75b58a9 100644 --- a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs +++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc10x5.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding; /// /// Texture compressed with RgbaAstc10x5. /// -internal readonly struct RgbaAstc10x5 : IBlock +internal readonly struct RgbaAstc10X5 : IBlock { public static Size BlockSize => new(10, 5); diff --git a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x12.cs b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x12.cs index 0c0d9347..ab1a911d 100644 --- a/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x12.cs +++ b/src/ImageSharp.Textures/TextureFormats/Decoding/RgbaAstc12x12.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Textures.TextureFormats.Decoding; public static Size BlockSize => new(12, 12); /// - public int BitsPerPixel => 128 / (BlockSize.Width * BlockSize.Height); + public int BitsPerPixel => 1; // Actually 0.888... but we round up for simplicity /// public byte PixelDepthBytes => 4; diff --git a/tests/ImageSharp.Textures.Benchmarks/AstcDecodingBenchmark.cs b/tests/ImageSharp.Textures.Benchmarks/AstcDecodingBenchmark.cs index ed364df8..00fdacb2 100644 --- a/tests/ImageSharp.Textures.Benchmarks/AstcDecodingBenchmark.cs +++ b/tests/ImageSharp.Textures.Benchmarks/AstcDecodingBenchmark.cs @@ -29,9 +29,9 @@ public bool ParseBlock() blocks[..16].CopyTo(blockBytes); ulong low = BitConverter.ToUInt64(blockBytes); ulong high = BitConverter.ToUInt64(blockBytes[8..]); - PhysicalBlock phyiscalBlock = PhysicalBlock.Create((UInt128)low | ((UInt128)high << 64)); + PhysicalBlock physicalBlock = PhysicalBlock.Create((UInt128)low | ((UInt128)high << 64)); - return !phyiscalBlock.IsIllegalEncoding; + return !physicalBlock.IsIllegalEncoding; } [Benchmark] diff --git a/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs index 5ec42416..37e15f46 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Astc/EndpointCodecTests.cs @@ -16,7 +16,7 @@ public class EndpointCodecTests [InlineData(EndpointEncodingMode.DirectLumaAlpha)] [InlineData(EndpointEncodingMode.BaseScaleRgb)] [InlineData(EndpointEncodingMode.BaseScaleRgba)] - [InlineData(EndpointEncodingMode.DirectRbg)] + [InlineData(EndpointEncodingMode.DirectRgb)] [InlineData(EndpointEncodingMode.DirectRgba)] internal void EncodeColorsForMode_WithVariousRanges_ShouldProduceValidQuantizedValues(EndpointEncodingMode mode) { @@ -41,7 +41,7 @@ internal void EncodeColorsForMode_WithVariousRanges_ShouldProduceValidQuantizedV [InlineData(EndpointEncodingMode.DirectLumaAlpha)] [InlineData(EndpointEncodingMode.BaseScaleRgb)] [InlineData(EndpointEncodingMode.BaseScaleRgba)] - [InlineData(EndpointEncodingMode.DirectRbg)] + [InlineData(EndpointEncodingMode.DirectRgb)] [InlineData(EndpointEncodingMode.DirectRgba)] internal void EncodeDecodeColors_WithBlackAndWhite_ShouldPreserveColors(EndpointEncodingMode mode) { @@ -105,7 +105,7 @@ public void EncodeColorsForMode_WithRgbDirectAndSpecificPairs_ShouldUseBlueContr foreach ((Rgba32 low, Rgba32 high) in pairs) { List values = []; - EndpointEncoder.EncodeColorsForMode(low, high, endpointRange, EndpointEncodingMode.DirectRbg, out ColorEndpointMode astcMode, values); + EndpointEncoder.EncodeColorsForMode(low, high, endpointRange, EndpointEncodingMode.DirectRgb, out ColorEndpointMode astcMode, values); Assert.True(EndpointEncoder.UsesBlueContract(endpointRange, astcMode, values)); } @@ -184,7 +184,7 @@ public void EncodeDecodeColors_WithLumaAlphaDirect_ShouldPreserveLumaAndAlpha() [Fact] public void EncodeDecodeColors_WithRgbDirectAndRandomColors_ShouldPreserveColors() { - EndpointEncodingMode mode = EndpointEncodingMode.DirectRbg; + EndpointEncodingMode mode = EndpointEncodingMode.DirectRgb; Random random = new(unchecked((int)0xdeadbeef)); for (int i = 0; i < 100; ++i) @@ -201,7 +201,7 @@ public void EncodeDecodeColors_WithRgbDirectAndRandomColors_ShouldPreserveColors [Fact] public void EncodeDecodeColors_WithRgbDirectAndSpecificColors_ShouldMatchExpected() { - EndpointEncodingMode mode = EndpointEncodingMode.DirectRbg; + EndpointEncodingMode mode = EndpointEncodingMode.DirectRgb; (Rgba32 low, Rgba32 high) = EncodeAndDecodeColors( new Rgba32(64, 127, 192, 255), diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs index 38342e4b..da6da444 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2AstcDecoderCubemapTests.cs @@ -27,6 +27,8 @@ public void CanDecode_All_Faces(TestTextureProvider provider) provider.SaveTextures(texture); CubemapTexture cubemapTexture = texture as CubemapTexture; + Assert.NotNull(cubemapTexture); + using Image posXImage = cubemapTexture.PositiveX.MipMaps[0].GetImage(); (posXImage as Image).CompareToReferenceOutput( ImageComparer.Exact, From e7ce5fa1ce5976327862cdc1a4274d362b7f0d34 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:31:04 +0100 Subject: [PATCH 09/10] Astc files are little endian --- src/ImageSharp.Textures/Compression/Astc/IO/AstcFileHeader.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp.Textures/Compression/Astc/IO/AstcFileHeader.cs b/src/ImageSharp.Textures/Compression/Astc/IO/AstcFileHeader.cs index 20e92a52..3963b5a3 100644 --- a/src/ImageSharp.Textures/Compression/Astc/IO/AstcFileHeader.cs +++ b/src/ImageSharp.Textures/Compression/Astc/IO/AstcFileHeader.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; + namespace SixLabors.ImageSharp.Textures.Compression.Astc.IO; /// @@ -27,7 +29,7 @@ public static AstcFileHeader FromMemory(Span data) // - magic (4), // - blockdim (3), // - xsize,y,z (each 3 little-endian bytes) - uint magic = BitConverter.ToUInt32(data); + uint magic = BinaryPrimitives.ReadUInt32LittleEndian(data); ArgumentOutOfRangeException.ThrowIfNotEqual(magic, Magic); return new AstcFileHeader( From 1d3acec106162eec020290377ea28b777630a792 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:11:54 +0100 Subject: [PATCH 10/10] Update shared infra --- shared-infrastructure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-infrastructure b/shared-infrastructure index 57699ffb..4a5a9fe7 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 57699ffb797bc2389c5d6cbb3b1800f2eb5fb947 +Subproject commit 4a5a9fe756e75c92ef9042b0ea4d94bc35e6ace9