From 72cb5f99d5383b7566bf76394ca299d517059676 Mon Sep 17 00:00:00 2001 From: Vineek Date: Wed, 25 Feb 2026 16:00:18 +0200 Subject: [PATCH 01/11] Handle all transcodable formats --- tools/ktx/command_extract.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ktx/command_extract.cpp b/tools/ktx/command_extract.cpp index f764a5e9be..869afa72fe 100644 --- a/tools/ktx/command_extract.cpp +++ b/tools/ktx/command_extract.cpp @@ -364,7 +364,7 @@ void CommandExtract::executeExtract() { } // Transcoding - if (ktxTexture2_NeedsTranscoding(texture)) { + if (ktxTexture2_IsTranscodable(texture)) { texture = transcode(std::move(texture), options, *this); } else if (options.transcodeTarget) { From 579a2982b757b0d16e6f2c647438d861fc72753b Mon Sep 17 00:00:00 2001 From: Vineek Date: Wed, 25 Feb 2026 16:02:45 +0200 Subject: [PATCH 02/11] Correctly handle mip map levels for HDR6x6i format --- lib/src/basis_transcode.cpp | 54 ++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/lib/src/basis_transcode.cpp b/lib/src/basis_transcode.cpp index 7acb68ddec..63726e3381 100644 --- a/lib/src/basis_transcode.cpp +++ b/lib/src/basis_transcode.cpp @@ -931,16 +931,20 @@ transcodeUastcHDR6x6_intermediate(ktxTexture2* This, alpha_content_e alphaConten std::vector xcoderStates; xcoderStates.resize(This->isVideo ? This->numFaces : 1); + // Pointer and length of the image description seek table within the global supercompressed data. + // This array of structs contain offsets and length fields relative to each mipmap level's data. + const ktxUASTCHDR6x6IntermediateImageDesc* imageDescs = reinterpret_cast(This->_private->_supercompressionGlobalData); + const uint64_t totalImageDescs = This->_private->_sgdByteLength / sizeof(ktxUASTCHDR6x6IntermediateImageDesc); + for (ktx_int32_t level = This->numLevels - 1; level >= 0; level--) { ktx_uint32_t depth; uint64_t writeOffset = levelOffsetWrite; uint64_t writeOffsetBlocks = levelOffsetWrite / outputBlockByteLength; - ktx_size_t levelImageSizeIn, levelImageOffsetIn; ktx_size_t levelImageSizeOut, levelSizeOut; ktx_uint32_t levelImageCount; uint32_t levelWidth = MAX(1, This->baseWidth >> level); uint32_t levelHeight = MAX(1, This->baseHeight >> level); - // UASTC texel block dimensions + // UASTC HDR 6x6i texel block dimensions const uint32_t bw = 6, bh = 6; uint32_t levelBlocksX = (levelWidth + (bw - 1)) / bw; uint32_t levelBlocksY = (levelHeight + (bh - 1)) / bh; @@ -949,12 +953,25 @@ transcodeUastcHDR6x6_intermediate(ktxTexture2* This, alpha_content_e alphaConten depth = MAX(1, This->baseDepth >> level); levelImageCount = This->numLayers * This->numFaces * depth; - levelImageSizeIn = This->dataSize; levelImageSizeOut = ktxTexture_calcImageSize(ktxTexture(prototype), level, KTX_FORMAT_VERSION_TWO); - levelImageOffsetIn = ktxTexture2_levelDataOffset(This, level); - //levelImageOffsetIn = ktxTexture2_levelFileOffset(This, level); + // Offset and length of the mipmap level's data within the KTX2 file. + const uint64_t levelDataOffset = ktxTexture2_levelDataOffset(This, level); + const uint64_t levelDataLength = This->_private->_levelIndex[level].byteLength; + + // Sanity check the level data length (transcode_image() wants uint32_t). + if (levelDataLength > UINT32_MAX) { + // Either we've got a bug or the KTX2 file is too small/invalid. Either way we can't + // continue. + return KTX_FILE_DATA_ERROR; + } + + // Ensure the mipmap level's data is fully contained within the KTX2 file's data. + if ((levelDataOffset + levelDataLength) > This->dataSize) { + // Either we've got a bug or the KTX2 file is too small/invalid. Either way we can't safely continue. + return KTX_FILE_DATA_ERROR; + } levelSizeOut = 0; bool status; @@ -963,11 +980,27 @@ transcodeUastcHDR6x6_intermediate(ktxTexture2* This, alpha_content_e alphaConten // See comment before same lines in transcodeEtc1s. if (++stateIndex == xcoderStates.size()) stateIndex = 0; + // Compute the start index into the image seek table. + const uint32_t sgdImageDescIndex = (level * levelImageCount) + image; + + // Sanity check the SGD image desc index + if (sgdImageDescIndex >= totalImageDescs) { + // Either we've got a bug or the SGD is too small/invalid. Either way we can't continue. + return KTX_TRANSCODE_FAILED; + } + + // The offsets are relative to the mipmap level's data. + const uint32_t imageDescByteOfsFromStartOfLevelData = imageDescs[sgdImageDescIndex].rgbSliceByteOffset; + const uint32_t imageDescByteLen = imageDescs[sgdImageDescIndex].rgbSliceByteLength; + status = uit.transcode_image( ktx2transcoderFormat(outputFormat), pXcodedData + writeOffset, - (uint32_t)(xcodedDataLength - writeOffsetBlocks), This->pData, - (uint32_t)This->dataSize, levelBlocksX, levelBlocksY, levelWidth, levelHeight, - level, (uint32_t)levelImageOffsetIn, (uint32_t)levelImageSizeIn, transcodeFlags, + (uint32_t)(xcodedDataLength - writeOffsetBlocks), + This->pData + levelDataOffset, (uint32_t)levelDataLength, // pointer and length of the mipmap level's data within the KTX2 file + levelBlocksX, levelBlocksY, levelWidth, levelHeight, + level, + imageDescByteOfsFromStartOfLevelData, imageDescByteLen, // offset and length of the image within the mipmap level's data + transcodeFlags, alphaContent != eNone, This->isVideo, // is_video // imageDesc.imageFlags ^ cSliceDescFlagsFrameIsIFrame, @@ -977,10 +1010,11 @@ transcodeUastcHDR6x6_intermediate(ktxTexture2* This, alpha_content_e alphaConten -1, // channel0 -1 // channel1 ); - if (!status) return KTX_TRANSCODE_FAILED; + if (!status) + return KTX_TRANSCODE_FAILED; + writeOffset += levelImageSizeOut; levelSizeOut += levelImageSizeOut; - levelImageOffsetIn += levelImageSizeIn; } protoLevelIndex[level].byteOffset = levelOffsetWrite; // writeOffset will be equal to total size of the images in the level. From 4cd712f5bb1eab2e0a6718dfc6d6b04f7821fc2a Mon Sep 17 00:00:00 2001 From: Vineek Date: Thu, 26 Feb 2026 10:16:53 +0200 Subject: [PATCH 03/11] Merge latest cts --- tests/cts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cts b/tests/cts index 4a2a4a727c..69f4580858 160000 --- a/tests/cts +++ b/tests/cts @@ -1 +1 @@ -Subproject commit 4a2a4a727c047f7365b199ab674fec4908d34a26 +Subproject commit 69f45808588c2c3e95a4df9e1cde93a348927b8c From 4ce46a8d53d16d406fe964dbc41d54c1d432387d Mon Sep 17 00:00:00 2001 From: Vineek Date: Thu, 26 Feb 2026 10:18:51 +0200 Subject: [PATCH 04/11] Add line break for readability --- lib/src/basis_transcode.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/basis_transcode.cpp b/lib/src/basis_transcode.cpp index 63726e3381..21be9638f1 100644 --- a/lib/src/basis_transcode.cpp +++ b/lib/src/basis_transcode.cpp @@ -933,7 +933,8 @@ transcodeUastcHDR6x6_intermediate(ktxTexture2* This, alpha_content_e alphaConten // Pointer and length of the image description seek table within the global supercompressed data. // This array of structs contain offsets and length fields relative to each mipmap level's data. - const ktxUASTCHDR6x6IntermediateImageDesc* imageDescs = reinterpret_cast(This->_private->_supercompressionGlobalData); + const ktxUASTCHDR6x6IntermediateImageDesc* imageDescs = + reinterpret_cast(This->_private->_supercompressionGlobalData); const uint64_t totalImageDescs = This->_private->_sgdByteLength / sizeof(ktxUASTCHDR6x6IntermediateImageDesc); for (ktx_int32_t level = This->numLevels - 1; level >= 0; level--) { From 9a28e351da7bc3358bb86f0eae2bb11064e036c9 Mon Sep 17 00:00:00 2001 From: Vineek Date: Tue, 17 Mar 2026 13:33:44 +0200 Subject: [PATCH 05/11] Proper handling of --uastc-quality and --uastc-hdr-6x6i-level option values --- lib/src/basis_encode.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/basis_encode.cpp b/lib/src/basis_encode.cpp index 57518e5418..6c0fcf012b 100644 --- a/lib/src/basis_encode.cpp +++ b/lib/src/basis_encode.cpp @@ -899,9 +899,11 @@ ktxTexture2_CompressBasisEx(ktxTexture2* This, ktxBasisParams* params) !params->uastcRDONoMultithreading; } cparams.m_hdr_favor_astc = params->uastcHDRFavorAstc; + cparams.m_uastc_hdr_4x4_options.set_quality_level(static_cast(params->uastcHDRQuality)); cparams.m_uastc_hdr_4x4_options.m_allow_uber_mode = params->uastcHDRUberMode; cparams.m_uastc_hdr_4x4_options.m_ultra_quant = params->uastcHDRUltraQuant; cparams.m_astc_hdr_6x6_options.m_rec2020_bt2100_color_gamut = params->rec2020; + cparams.m_astc_hdr_6x6_options.set_user_level(static_cast(params->uastcHDRLevel)); if (params->uastcHDRLambda > 0.0f) { cparams.m_astc_hdr_6x6_options.m_lambda = params->uastcHDRLambda; From 726eb04ff4933c5195adaa70ca032c8cb4617d4d Mon Sep 17 00:00:00 2001 From: Mark Callow Date: Wed, 18 Mar 2026 21:57:36 +0900 Subject: [PATCH 06/11] Fix dumping of encoder input data for compilers supporting std::filesystem and for HDR. --- lib/src/basis_encode.cpp | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/src/basis_encode.cpp b/lib/src/basis_encode.cpp index 6c0fcf012b..310e157a66 100644 --- a/lib/src/basis_encode.cpp +++ b/lib/src/basis_encode.cpp @@ -49,6 +49,12 @@ #endif #include "dfdutils/dfd.h" +#define DUMP_ENCODER_INPUT_DATA 0 +#if DUMP_ENCODER_INPUT_DATA + #include + #include +#endif + using namespace basisu; using namespace basist; @@ -830,20 +836,23 @@ ktxTexture2_CompressBasisEx(ktxTexture2* This, ktxBasisParams* params) This->pData = NULL; This->dataSize = 0; -#define DUMP_ENCODER INPUT_DATA 0 #if DUMP_ENCODER_INPUT_DATA - #include - #include - std::filebuf dump; if (!dump.open("basisenc_input", std::ios::binary | std::ios::out | std::ios::trunc)) { std::cout << "Open dump file basicenc_input for write failed\n"; } - for (iit = cparams.m_source_images.begin(); iit < cparams.m_source_images.end(); ++iit) { - size_t byteCount = iit->get_total_pixels(); - // TODO: Handle 16-bit input. - byteCount *= sizeof(color_rgba); - dump.sputn((char*)iit->get_ptr(), byteCount); + if (cparams.m_hdr) { + for (iit_hdr = cparams.m_source_images_hdr.begin(); iit_hdr < cparams.m_source_images_hdr.end(); ++iit_hdr) { + size_t byteCount = iit_hdr->get_total_pixels(); + byteCount *= sizeof(color_rgba); + dump.sputn((char*)iit_hdr->get_ptr(), byteCount); + } + } else { + for (iit_ldr = cparams.m_source_images.begin(); iit_ldr < cparams.m_source_images.end(); ++iit_ldr) { + size_t byteCount = iit_ldr->get_total_pixels(); + byteCount *= sizeof(color_rgba); + dump.sputn((char*)iit_ldr->get_ptr(), byteCount); + } } dump.close(); #endif From 219b697a58c456e76e59bb662ac3b0c43cff462d Mon Sep 17 00:00:00 2001 From: Mark Callow Date: Thu, 19 Mar 2026 20:12:16 +0900 Subject: [PATCH 07/11] Expose HDR support in Python binding (#1150) Fixes #1145. Additional fixes: - Adds `num_layers` property that was curiously missing. - Exposes ktxTexture[12]_IsHDR in the c API. - Reformats documentation comments for UASTC HDR ktxBasisParams to reduce line width. --- interface/python_binding/buildscript.py | 21 ++- interface/python_binding/conf.py | 1 + .../python_binding/pyktx/khr_df_model.py | 126 ++++++++++++++++++ .../python_binding/pyktx/khr_df_primaries.py | 51 +++++++ .../python_binding/pyktx/khr_df_transfer.py | 94 +++++++++++++ .../python_binding/pyktx/ktx_basis_codec.py | 18 +++ .../python_binding/pyktx/ktx_basis_params.py | 85 +++++++++--- .../pyktx/ktx_supercmp_scheme.py | 3 + interface/python_binding/pyktx/ktx_texture.c | 1 + interface/python_binding/pyktx/ktx_texture.h | 1 + interface/python_binding/pyktx/ktx_texture.py | 33 ++++- interface/python_binding/pyktx/ktx_texture2.c | 23 +++- interface/python_binding/pyktx/ktx_texture2.h | 11 +- .../python_binding/pyktx/ktx_texture2.py | 54 ++++++-- .../python_binding/tests/test_ktx_texture2.py | 95 ++++++++++++- lib/include/ktx.h | 27 +++- tests/resources/genktx2 | 2 + tests/resources/ktx2/Desk_small_zstd_15.ktx2 | 3 + 18 files changed, 597 insertions(+), 52 deletions(-) create mode 100644 interface/python_binding/pyktx/khr_df_model.py create mode 100644 interface/python_binding/pyktx/khr_df_primaries.py create mode 100644 interface/python_binding/pyktx/khr_df_transfer.py create mode 100644 interface/python_binding/pyktx/ktx_basis_codec.py create mode 100644 tests/resources/ktx2/Desk_small_zstd_15.ktx2 diff --git a/interface/python_binding/buildscript.py b/interface/python_binding/buildscript.py index 2a1e04759f..13686bb914 100644 --- a/interface/python_binding/buildscript.py +++ b/interface/python_binding/buildscript.py @@ -81,11 +81,16 @@ uint32_t faceSlice, void *src, size_t srcSize); + bool ktxTexture_IsHDR(ktxTexture *); + bool ktxTexture_IsTranscodable(ktxTexture *); + bool ktxTexture_NeedsTranscoding(ktxTexture *); + int ktxTexture2_DecodeAstc(void *); int ktxTexture2_TranscodeBasis(void *, int outputFormat, int transcodeFlags); int ktxTexture2_DeflateZstd(void *, uint32_t compressionLevel); - uint32_t ktxTexture2_GetOETF(void *); + uint32_t ktxTexture2_GetColorModel_e(void *); + uint32_t ktxTexture2_GetPrimaries_e(void *); + uint32_t ktxTexture2_GetTransferFunction_e(void *); bool ktxTexture2_GetPremultipliedAlpha(void *); - bool ktxTexture2_NeedsTranscoding(void *); int ktxHashList_AddKVPair(ktxHashList *, const char *key, unsigned int valueLen, const void *value); int ktxHashList_DeleteKVPair(ktxHashList *, const char *key); @@ -108,6 +113,7 @@ uint32_t PY_ktxTexture_get_baseHeight(ktxTexture *); uint32_t PY_ktxTexture_get_baseDepth(ktxTexture *); uint32_t PY_ktxTexture_get_numDimensions(ktxTexture *); + uint32_t PY_ktxTexture_get_numLayers(ktxTexture *); uint32_t PY_ktxTexture_get_numLevels(ktxTexture *); uint32_t PY_ktxTexture_get_numFaces(ktxTexture *); uint32_t PY_ktxTexture_get_kvDataLen(ktxTexture *); @@ -159,7 +165,7 @@ bool perceptual, char *inputSwizzle); int PY_ktxTexture2_CompressBasisEx(void *texture, - bool uastc, + uint32_t codec, bool verbose, bool noSSE, uint32_t threadCount, @@ -182,7 +188,14 @@ float uastcRDOMaxSmoothBlockErrorScale, float uastcRDOMaxSmoothBlockStdDev, bool uastcRDODontFavorSimplerModes, - bool uastcRDONoMultithreading); + bool uastcRDONoMultithreading, + uint32_t uastcHDRQuality, + bool uastcHDRUberMode, + bool uastcHDRUltraQuant, + bool uastcHDRFavorAstc, + bool rec2020, + float uastcHDRLambda, + uint32_t uastcHDRLevel); uint32_t PY_ktxTexture2_get_vkFormat(void *); uint32_t PY_ktxTexture2_get_supercompressionScheme(void *); """ diff --git a/interface/python_binding/conf.py b/interface/python_binding/conf.py index a9ed752eae..16961d64ee 100644 --- a/interface/python_binding/conf.py +++ b/interface/python_binding/conf.py @@ -41,6 +41,7 @@ 'sphinx.ext.napoleon', ] +autodoc_member_order = 'bysource' templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] diff --git a/interface/python_binding/pyktx/khr_df_model.py b/interface/python_binding/pyktx/khr_df_model.py new file mode 100644 index 0000000000..2693c96c5b --- /dev/null +++ b/interface/python_binding/pyktx/khr_df_model.py @@ -0,0 +1,126 @@ +# Copyright (c) 2026, Mark Callow +# SPDX-License-Identifier: Apache-2.0 + +from enum import IntEnum + +class KhrDfModel(IntEnum): + """Model in which the color coordinate space is defined.""" + + UNSPECIFIED = 0 + """No interpretation of color channels defined.""" + + RGBSDA = 1 + """Color primaries (red, green, blue) + alpha, depth and stencil.""" + + YUVSDA = 2 + """Color differences (Y', Cb, Cr) + alpha, depth and stencil.""" + + YIQSDA = 3 + """Color differences (Y', I, Q) + alpha, depth and stencil.""" + + LABSDA = 4 + """Perceptual color (CIE L*a*b*) + alpha, depth and stencil.""" + + CMYKA = 5 + """Subtractive colors (cyan, magenta, yellow, black) + alpha.""" + + XYZW = 6 + """Non-color coordinate data (X, Y, Z, W).""" + + HSVA_ANG = 7 + """Hue, saturation, value, hue angle on color circle, plus alpha.""" + + HSLA_ANG = 8 + """Hue, saturation, lightness, hue angle on color circle, plus alpha.""" + + HSVA_HEX = 9 + """Hue, saturation, value, hue on color hexagon, plus alpha.""" + + HSLA_HEX = 10 + """Hue, saturation, lightness, hue on color hexagon, plus alpha.""" + + YCGCOA = 11 + """Lightweight approximate color difference (luma, orange, green).""" + + YCCBCCRC = 12 + """ITU BT.2020 constant luminance YcCbcCrc.""" + + ICTCP = 13 + """ITU BT.2100 constant intensity ICtCp.""" + + CIEXYZ = 14 + """CIE 1931 XYZ color coordinates (X, Y, Z).""" + + CIEXYY = 15 + """CIE 1931 xyY color coordinates (X, Y, Y).""" + + + DXT1A = 128 + BC1A = 128 + """Compressed formats start at 128.""" + """ + Direct3D (and S3) compressed formats. + DXT1 "channels" are RGB (0), Alpha (1). + DXT1/BC1 with one channel is opaque. + DXT1/BC1 with a cosited alpha sample is transparent. + """ + + DXT2 = 129 + DXT3 = 129 + BC2 = 129 + """DXT2/DXT3/BC2, with explicit 4-bit alpha.""" + + DXT4 = 130 + DXT5 = 130 + BC3 = 130 + """DXT4/DXT5/BC3, with interpolated alpha.""" + + ATI1N = 131 + DXT5A = 131 + BC4 = 131 + """ATI1n/DXT5A/BC4 - single channel interpolated 8-bit data. + (The UNORM/SNORM variation is recorded in the channel data).""" + + ATI2N_XY = 132 + DXN = 132 + BC5 = 132 + """ATI2n_XY/DXN/BC5 - two channel interpolated 8-bit data. + (The UNORM/SNORM variation is recorded in the channel data).""" + + BC6H = 133 + """BC6H - DX11 format for 16-bit float channels.""" + + BC7 = 134 + """BC7 - DX11 format.""" + + ETC1 = 160 + """A format of ETC1 indicates that the format shall be decodable + by an ETC1-compliant decoder and not rely on ETC2 features.""" + + ETC2 = 161 + """A format of ETC2 is permitted to use ETC2 encodings on top of + the baseline ETC1 specification. + The ETC2 format has channels "red", "green", "RGB" and "alpha", + which should be cosited samples. + Punch-through alpha can be distinguished from full alpha by + the plane size in bytes required for the texel block.""" + + ASTC = 162 + """Adaptive Scalable Texture Compression.""" + """ASTC HDR vs LDR is determined by the float flag in the channel.""" + """ASTC block size can be distinguished by texel block size.""" + + ETC1S = 163 + """ETC1S is a simplified subset of ETC1.""" + + PVRTC = 164 + PVRTC2 = 165 + """PowerVR Texture Compression.""" + + UASTC = 166 + UASTC_LDR_4x4 = 166 + UASTC_HDR_4x4 = 167 + UASTC_HDR_6x6 = 168 + """UASTC for BASIS supercompression.""" + + MAX = 0xFF diff --git a/interface/python_binding/pyktx/khr_df_primaries.py b/interface/python_binding/pyktx/khr_df_primaries.py new file mode 100644 index 0000000000..ef20144a9f --- /dev/null +++ b/interface/python_binding/pyktx/khr_df_primaries.py @@ -0,0 +1,51 @@ +# Copyright (c) 2026, Mark Callow +# SPDX-License-Identifier: Apache-2.0 + +from enum import IntEnum + +class KhrDfPrimaries(IntEnum): + """The primary colors of an image.""" + + UNSPECIFIED = 0 + """No color primaries defined""" + + BT709 = 1 + """Color primaries of ITU-R BT.709 and sRGB""" + + SRGB = 1 + """Synonym for BT709""" + + BT601_EBU = 2 + """Color primaries of ITU-R BT.601 (625-line EBU variant)""" + + BT601_SMPTE = 3 + """Color primaries of ITU-R BT.601 (525-line SMPTE C variant)""" + + BT2020 = 4 + """Color primaries of ITU-R BT.2020""" + + BT2100 = 4 + """ITU-R BT.2100 uses the same primaries as BT.2020""" + + CIEXYZ = 5 + """CIE theoretical color coordinate space""" + + ACES = 6 + """Academy Color Encoding System primaries""" + + ACESCC = 7 + """Color primaries of ACEScc""" + + NTSC1953 = 8 + """Legacy NTSC 1953 primaries""" + + PAL525 = 9 + """Legacy PAL 525-line primaries""" + + DISPLAYP3 = 10 + """Color primaries of Display P3""" + + ADOBERGB = 11 + """Color primaries of Adobe RGB (1998)""" + + MAX = 0xFF diff --git a/interface/python_binding/pyktx/khr_df_transfer.py b/interface/python_binding/pyktx/khr_df_transfer.py new file mode 100644 index 0000000000..140613fce2 --- /dev/null +++ b/interface/python_binding/pyktx/khr_df_transfer.py @@ -0,0 +1,94 @@ +# Copyright (c) 2026, Mark Callow +# SPDX-License-Identifier: Apache-2.0 + +from enum import IntEnum + +class KhrDfTransfer(IntEnum): + """The transfer function of an image.""" + + UNSPECIFIED = 0 + """No transfer function defined.""" + + LINEAR = 1 + """Linear transfer function (value proportional to intensity.""" + + SRGB = 2 + SRGB_EOTF = 2 + SCRGB = 2 + SCRGB_EOTF = 2 + """Perceptually-linear transfer function of sRGB (~2.2); also used for scRGB.""" + + ITU = 3 + ITU_OETF = 3 + BT601 = 3 + BT601_OETF = 3 + BT709 = 3 + BT709_OETF = 3 + BT2020 = 3 + BT2020_OETF = 3 + """Perceptually-linear transfer function of ITU BT.601, BT.709 and BT.2020 (~1/.45).""" + + SMTPE170M = 3 + SMTPE170M_OETF = 3 + SMTPE170M_EOTF = 3 + """SMTPE170M (digital NTSC) defines an alias for the ITU transfer function (~1/.45) and a linear OOTF.""" + + NTSC = 4 + NTSC_EOTF = 4 + """Perceptually-linear gamma function of original NTSC (simple 2.2 gamma).""" + + SLOG = 5 + SLOG_OETF = 5 + """Sony S-log used by Sony video cameras.""" + + SLOG2 = 6 + SLOG2_OETF = 6 + """Sony S-log 2 used by Sony video cameras.""" + + BT1886 = 7 + BT1886_EOTF = 7 + """ITU BT.1886 EOTF.""" + + HLG_OETF = 8 + """ITU BT.2100 HLG OETF (typical scene-referred content), linear light normalized 0..1.""" + + HLG_EOTF = 9 + """ITU BT.2100 HLG EOTF (nominal HDR display of HLG content), linear light normalized 0..1.""" + + PQ_EOTF = 10 + """ITU BT.2100 PQ EOTF (typical HDR display-referred PQ content).""" + + PQ_OETF = 11 + """ITU BT.2100 PQ OETF (nominal scene described by PQ HDR content).""" + + DCIP3 = 12 + DCIP3_EOTF = 12 + """DCI P3 transfer function.""" + + PAL_OETF = 13 + """Legacy PAL OETF.""" + + PAL625_EOTF = 14 + """Legacy PAL 625-line EOTF.""" + + ST240 = 15 + ST240_OETF = 15 + ST240_EOTF = 15 + """Legacy ST240 transfer function.""" + + ACESCC = 16 + ACESCC_OETF = 16 + """ACEScc transfer function.""" + + ACESCCT = 17 + ACESCCT_OETF = 17 + """ACEScct transfer function.""" + + ADOBERGB = 18 + ADOBERGB_EOTF = 18 + """Adobe RGB (1998) transfer function.""" + + HLG_UNNORMALIZED_OETF = 19 + """Legacy ITU BT.2100 HLG OETF (typical scene-referred content), linear light normalized 0..12.""" + + MAX = 0xFF diff --git a/interface/python_binding/pyktx/ktx_basis_codec.py b/interface/python_binding/pyktx/ktx_basis_codec.py new file mode 100644 index 0000000000..3bcbc44107 --- /dev/null +++ b/interface/python_binding/pyktx/ktx_basis_codec.py @@ -0,0 +1,18 @@ +# Copyright (c) 2026, Mark Callow +# SPDX-License-Identifier: Apache-2.0 + +from enum import IntEnum + +class KtxBasisCodec(IntEnum): + """Options specifiying basis codec.""" + + NONE = 0 + """NONE.""" + ETC1S = 1 + """BasisLZ.""" + UASTC_LDR_4x4 = 2 + """UASTC.""" + UASTC_HDR_4x4 = 3 + """UASTC_HDR_4x4.""" + UASTC_HDR_6x6_INTERMEDIATE = 4 + """UASTC_HDR_6x6i.""" diff --git a/interface/python_binding/pyktx/ktx_basis_params.py b/interface/python_binding/pyktx/ktx_basis_params.py index c30fa38f60..f35fb6a13a 100644 --- a/interface/python_binding/pyktx/ktx_basis_params.py +++ b/interface/python_binding/pyktx/ktx_basis_params.py @@ -2,15 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 from dataclasses import dataclass +from .ktx_basis_codec import KtxBasisCodec from .ktx_pack_uastc_flag_bits import KtxPackUastcFlagBits - @dataclass class KtxBasisParams: - """Data for passing extended params to KtxTexture2.compressBasis().""" + """Struct for passing extended params to KtxTexture2.compressBasis().""" - uastc: bool = False - """True to use UASTC base, false to use ETC1S base.""" + codec: int = KtxBasisCodec.ETC1S + """Basis Universal codec to use. Default is KtxBasisCodec.ETC1S/BasisLZ.""" verbose: bool = False """If true, prints Basis Universal encoder operation details to stdout. Not recommended for GUI apps.""" @@ -21,9 +21,12 @@ class KtxBasisParams: thread_count: int = 1 """Number of threads used for compression. Default is 1.""" + # Here and in the descriptions of all other codec specific options we abuse the + # fact that ':'in a docstring is used to indicate the preceding text is a + # type so that the target codec is clearly flagged on a separate "Type: " line. etc1s_compression_level: int = 0 """ - Encoding speed vs. quality tradeoff for etc1s. Range is [0,5]. + ETC1S/BasisLZ: Encoding speed vs. quality tradeoff for etc1s. Range is [0,5]. Higher values are slower, but give higher quality. There is no default. Callers must explicitly set this value. Callers can use @@ -32,7 +35,7 @@ class KtxBasisParams: quality_level: int = 0 """ - Compression quality. Range is [1,255]. + ETC1S/BasisLZ and UASTC LDR 4x4: Compression quality. Range is [1,255]. Lower gives better compression/lower quality/faster. Higher gives less compression/higher quality/slower. @@ -49,7 +52,7 @@ class KtxBasisParams: max_endpoints: int = 0 """ - Manually set the max number of color endpoint clusters. + ETC1S/BasisLZ: Manually set the max number of color endpoint clusters. Range is [1,16128]. Default is 0, unset. If this is set, max_selectors must also be set, otherwise the value will be ignored. @@ -57,7 +60,7 @@ class KtxBasisParams: endpoint_rdo_threshold: int = 0 """ - Set endpoint RDO quality threshold. The default is 1.25. + ETC1S/BasisLZ: Set endpoint RDO quality threshold. The default is 1.25. Lower is higher quality but less quality per output bit (try [1.0,3.0]. This will override the value chosen by quality_level. @@ -65,7 +68,7 @@ class KtxBasisParams: max_selectors: int = 0 """ - Manually set the max number of color selector clusters. Range is [1,16128]. + ETC1S/BasisLZ: Manually set the max number of color selector clusters. Range is [1,16128]. Default is 0, unset. If this is set, max_endpoints must also be set, otherwise the value will be ignored. @@ -73,7 +76,7 @@ class KtxBasisParams: selector_rdo_threshold: int = 0 """ - Set selector RDO quality threshold. The default is 1.5. + ETC1S/BasisLZ: Set selector RDO quality threshold. The default is 1.5. Lower is higher quality but less quality per output bit (try [1.0,3.0]). This will override the value chosen by @c qualityLevel. @@ -113,32 +116,32 @@ class KtxBasisParams: no_endpoint_rdo: bool = False """ - Disable endpoint rate distortion optimizations. + ETC1S/BasisLZ: Disable endpoint rate distortion optimizations. Slightly faster, less noisy output, but lower quality per output bit. """ no_selector_rdo: bool = False """ - Disable selector rate distortion optimizations. + ETC1S/BasisLZ: Disable selector rate distortion optimizations. Slightly faster, less noisy output, but lower quality per output bit. """ uastc_flags: int = KtxPackUastcFlagBits.FASTEST """ - A set of KtxPackUastcFlagBits controlling UASTC encoding. + UASTC LDR 4x4: A set of KtxPackUastcFlagBits controlling UASTC encoding. The most important value is the level given in the least-significant 4 bits which selects a speed vs quality tradeoff. """ uastc_rdo: bool = False - """Enable Rate Distortion Optimization (RDO) post-processing.""" + """UASTC LDR 4x4: Enable Rate Distortion Optimization (RDO) post-processing.""" uastc_rdo_quality_scalar: float = 0. """ - UASTC RDO quality scalar (lambda). + UASTC LDR 4x4: RDO quality scalar (lambda). Lower values yield higher quality/larger LZ compressed files, higher values yield lower quality/smaller LZ compressed files. A good range to @@ -147,26 +150,68 @@ class KtxBasisParams: uastc_rdo_dict_size: int = 0 """ - UASTC RDO dictionary size in bytes. Default is 4096. Lower + UASTC LDR 4x4: RDO dictionary size in bytes. Default is 4096. Lower values=faster, but give less compression. Range is [64,65536]. """ uastc_rdo_max_smooth_block_error_scale: float = 10. """ - UASTC RDO max smooth block error scale. Range is [1,300]. + UASTC LDR 4x4: RDO max smooth block error scale. Range is [1,300]. Default is 10.0, 1.0 is disabled. Larger values suppress more artifacts (and allocate more bits) on smooth blocks. """ uastc_rdo_max_smooth_block_std_dev: float = 18. """ - UASTC RDO max smooth block standard deviation. Range is + UASTC LDR 4x4: RDO max smooth block standard deviation. Range is [.01,65536.0]. Default is 18.0. Larger values expand the range of blocks considered smooth. """ uastc_rdo_dont_favor_simpler_modes: bool = False - """Do not favor simpler UASTC modes in RDO mode.""" + """UASTC LDR 4x4: Do not favor simpler UASTC modes in RDO mode.""" uastc_rdo_no_multithreading: bool = False - """Disable RDO multithreading (slightly higher compression, deterministic).""" + """UASTC LDR 4x4: Disable RDO multithreading (slightly higher compression, deterministic).""" + + # UASTC HDR params + + uastc_hdr_quality: int = 1 + """ + Valid range is [0,4] - higher=slower but higher quality. Default=1. + Level 0=fastest/lowest quality, 3=highest practical setting, 4=exhaustive + """ + + uastc_hdr_uber_mode: bool = False + """ + UASTC HDR 4x4: Allow the UASTC HDR 4x4 encoder to try varying the CEM 11 + selectors more for slightly higher quality (slower). This may negatively impact BC6H quality, however. + """ + + uastc_hdr_ultra_quant: bool = False + """UASTC HDR 4x4: Try to find better quantized CEM 7/11 endpoint values (slower).""" + + uastc_hdr_favor_astc: bool = False + """ + UASTC HDR 4x4: By default the UASTC HDR 4x4 encoder tries to strike a balance + or even slightly favor BC6H quality. If this option is specified, ASTC HDR 4x4 quality is favored instead. + """ + + rec_2020: bool = False + """ + UASTC HDR 6x6i specific option: The input image's gamut is Rec. 2020 vs. the + default Rec. 709 - for accurate colorspace error calculations. + """ + + uastc_hdr_lambda: float = 0. + """ + UASTC HDR 6x6i specific option: Enables rate distortion optimization (RDO). + The higher this value, the lower the quality, but the smaller the file size. + Try 100-20000, or higher values on some images. + """ + + uastc_hdr_level: int = 2 + """ + UASTC HDR 6x6i specific option: Controls the 6x6 HDR intermediate mode encoder + performance vs. max quality tradeoff. X may range from [0,12]. Default level is 2. + """ diff --git a/interface/python_binding/pyktx/ktx_supercmp_scheme.py b/interface/python_binding/pyktx/ktx_supercmp_scheme.py index e7634171f2..00ca13ee2a 100644 --- a/interface/python_binding/pyktx/ktx_supercmp_scheme.py +++ b/interface/python_binding/pyktx/ktx_supercmp_scheme.py @@ -18,3 +18,6 @@ class KtxSupercmpScheme(IntEnum): ZLIB = 3 """ZLIB supercompression.""" + + UASTC_HDR_6x6_INTERMEDIATE = 4 + """UASTC HDR 6x6 Intermediate supercompression.""" diff --git a/interface/python_binding/pyktx/ktx_texture.c b/interface/python_binding/pyktx/ktx_texture.c index 2479c996b6..f6bb0e9f05 100644 --- a/interface/python_binding/pyktx/ktx_texture.c +++ b/interface/python_binding/pyktx/ktx_texture.c @@ -88,6 +88,7 @@ KTX_IMPL(ktx_uint32_t, baseWidth); KTX_IMPL(ktx_uint32_t, baseHeight); KTX_IMPL(ktx_uint32_t, baseDepth); KTX_IMPL(ktx_uint32_t, numDimensions); +KTX_IMPL(ktx_uint32_t, numLayers); KTX_IMPL(ktx_uint32_t, numLevels); KTX_IMPL(ktx_uint32_t, numFaces); KTX_IMPL(ktx_uint32_t, kvDataLen); diff --git a/interface/python_binding/pyktx/ktx_texture.h b/interface/python_binding/pyktx/ktx_texture.h index 9be170c263..c3f75a01df 100644 --- a/interface/python_binding/pyktx/ktx_texture.h +++ b/interface/python_binding/pyktx/ktx_texture.h @@ -46,6 +46,7 @@ KTX_GETTER(ktx_uint32_t, baseWidth); KTX_GETTER(ktx_uint32_t, baseHeight); KTX_GETTER(ktx_uint32_t, baseDepth); KTX_GETTER(ktx_uint32_t, numDimensions); +KTX_GETTER(ktx_uint32_t, numLayers); KTX_GETTER(ktx_uint32_t, numLevels); KTX_GETTER(ktx_uint32_t, numFaces); KTX_GETTER(ktx_uint32_t, kvDataLen); diff --git a/interface/python_binding/pyktx/ktx_texture.py b/interface/python_binding/pyktx/ktx_texture.py index 2da6b15c52..8ea919890b 100644 --- a/interface/python_binding/pyktx/ktx_texture.py +++ b/interface/python_binding/pyktx/ktx_texture.py @@ -79,12 +79,23 @@ def base_depth(self) -> int: return lib.PY_ktxTexture_get_baseDepth(self._ptr) + # A colon in a docstring indicates the preceding text is the type. There is + # no way to escape it. See https://github.com/sphinx-doc/sphinx/issues/9273. + # Therefore we use a MODIFIER LETTER COLON '꞉', Unicode: U+A789, + # UTF-8: EA 9E 89 here and in any other place we want to a colon in the + # description. @property def num_dimensions(self) -> int: - """Number of dimensions in the texture: 1, 2 or 3.""" + """Number of dimensions in the texture꞉ 1, 2 or 3.""" return lib.PY_ktxTexture_get_numDimensions(self._ptr) + @property + def num_layers(self) -> int: + """Number of layers in the texture.""" + + return lib.PY_ktxTexture_get_numLayers(self._ptr) + @property def num_levels(self) -> int: """Number of mip levels in the texture.""" @@ -93,7 +104,7 @@ def num_levels(self) -> int: @property def num_faces(self) -> int: - """Number of faces: 6 for cube maps, 1 otherwise.""" + """Number of faces꞉ 6 for cube maps, 1 otherwise.""" return lib.PY_ktxTexture_get_numFaces(self._ptr) @@ -140,6 +151,24 @@ def data_size_uncompressed(self) -> int: return lib.ktxTexture_GetDataSizeUncompressed(self._ptr) + @property + def is_hdr(self) -> bool: + """Whether the images are in an HDR format.""" + + return lib.ktxTexture_IsHDR(self._ptr) + + @property + def is_transcodable(self) -> bool: + """If the images are in a format that can be transcoded.""" + + return lib.ktxTexture_IsTranscodable(self._ptr) + + @property + def needs_transcoding(self) -> bool: + """If the images are in a format that must be transcoded.""" + + return lib.ktxTexture_NeedsTranscoding(self._ptr) + def row_pitch(self, level: int) -> int: """ Return pitch between rows of a texture image level in bytes. diff --git a/interface/python_binding/pyktx/ktx_texture2.c b/interface/python_binding/pyktx/ktx_texture2.c index f1f6a4ce84..eb5ccd3a25 100644 --- a/interface/python_binding/pyktx/ktx_texture2.c +++ b/interface/python_binding/pyktx/ktx_texture2.c @@ -79,7 +79,7 @@ KTX_error_code PY_ktxTexture2_CompressAstcEx(ktxTexture2 *texture, } KTX_error_code PY_ktxTexture2_CompressBasisEx(ktxTexture2 *texture, - ktx_bool_t uastc, + int codec, ktx_bool_t verbose, ktx_bool_t noSSE, ktx_uint32_t threadCount, @@ -102,11 +102,19 @@ KTX_error_code PY_ktxTexture2_CompressBasisEx(ktxTexture2 *texture, float uastcRDOMaxSmoothBlockErrorScale, float uastcRDOMaxSmoothBlockStdDev, ktx_bool_t uastcRDODontFavorSimplerModes, - ktx_bool_t uastcRDONoMultithreading) + ktx_bool_t uastcRDONoMultithreading, + ktx_uint32_t uastcHDRQuality, + ktx_bool_t uastcHDRUberMode, + ktx_bool_t uastcHDRUltraQuant, + ktx_bool_t uastcHDRFavorAstc, + ktx_bool_t rec2020, + float uastcHDRLambda, + ktx_uint32_t uastcHDRLevel + ) { ktxBasisParams params = { .structSize = sizeof(ktxBasisParams), - .codec = (uastc) ? KTX_BASIS_CODEC_UASTC_LDR_4x4 : KTX_BASIS_CODEC_ETC1S, + .codec = codec, .verbose = verbose, .noSSE = noSSE, .threadCount = threadCount, @@ -128,7 +136,14 @@ KTX_error_code PY_ktxTexture2_CompressBasisEx(ktxTexture2 *texture, .uastcRDOMaxSmoothBlockErrorScale = uastcRDOMaxSmoothBlockErrorScale, .uastcRDOMaxSmoothBlockStdDev = uastcRDOMaxSmoothBlockStdDev, .uastcRDODontFavorSimplerModes = uastcRDODontFavorSimplerModes, - .uastcRDONoMultithreading = uastcRDONoMultithreading + .uastcRDONoMultithreading = uastcRDONoMultithreading, + .uastcHDRQuality = uastcHDRQuality, + .uastcHDRUberMode = uastcHDRUberMode, + .uastcHDRUltraQuant = uastcHDRUltraQuant, + .uastcHDRFavorAstc = uastcHDRFavorAstc, + .rec2020 = rec2020, + .uastcHDRLambda = uastcHDRLambda, + .uastcHDRLevel = uastcHDRLevel }; params.inputSwizzle[0] = inputSwizzle[0]; diff --git a/interface/python_binding/pyktx/ktx_texture2.h b/interface/python_binding/pyktx/ktx_texture2.h index 27427908a7..d97e8ef991 100644 --- a/interface/python_binding/pyktx/ktx_texture2.h +++ b/interface/python_binding/pyktx/ktx_texture2.h @@ -34,7 +34,7 @@ KTX_error_code PY_ktxTexture2_CompressAstcEx(ktxTexture2 *texture, char *inputSwizzle); KTX_error_code PY_ktxTexture2_CompressBasisEx(ktxTexture2 *texture, - ktx_bool_t uastc, + int codec, ktx_bool_t verbose, ktx_bool_t noSSE, ktx_uint32_t threadCount, @@ -57,7 +57,14 @@ KTX_error_code PY_ktxTexture2_CompressBasisEx(ktxTexture2 *texture, float uastcRDOMaxSmoothBlockErrorScale, float uastcRDOMaxSmoothBlockStdDev, ktx_bool_t uastcRDODontFavorSimplerModes, - ktx_bool_t uastcRDONoMultithreading); + ktx_bool_t uastcRDONoMultithreading, + ktx_uint32_t uastcHDRQuality, + ktx_bool_t uastcHDRUberMode, + ktx_bool_t uastcHDRUltraQuant, + ktx_bool_t uastcHDRFavorAstc, + ktx_bool_t rec2020, + float uastcHDRLambda, + ktx_uint32_t uastcHDRLevel); #define KTX2_GETTER(type, prop) \ type PY_ktxTexture2_get_##prop(ktxTexture2 *texture) diff --git a/interface/python_binding/pyktx/ktx_texture2.py b/interface/python_binding/pyktx/ktx_texture2.py index 4940a33abb..5efc4e36d5 100644 --- a/interface/python_binding/pyktx/ktx_texture2.py +++ b/interface/python_binding/pyktx/ktx_texture2.py @@ -1,6 +1,9 @@ # Copyright (c) 2023, Shukant Pal and Contributors # SPDX-License-Identifier: Apache-2.0 +from .khr_df_model import KhrDfModel +from .khr_df_primaries import KhrDfPrimaries +from .khr_df_transfer import KhrDfTransfer from .ktx_astc_params import KtxAstcParams from .ktx_basis_params import KtxBasisParams from .ktx_error_code import KtxErrorCode, KtxError @@ -71,22 +74,28 @@ def supercompression_scheme(self) -> KtxSupercmpScheme: return KtxSupercmpScheme(lib.PY_ktxTexture2_get_supercompressionScheme(self._ptr)) @property - def oetf(self) -> int: - """The opto-electrical transfer function of the images.""" + def color_model(self) -> KhrDfModel: + """The color model of the images.""" - return lib.ktxTexture2_GetOETF(self._ptr) + return KhrDfModel(lib.ktxTexture2_GetColorModel_e(self._ptr)) @property - def premultipled_alpha(self) -> bool: - """Whether the RGB components have been premultiplied by the alpha component.""" + def primaries(self) -> KhrDfPrimaries: + """The color primaries of the images.""" - return lib.ktxTexture2_GetPremultipliedAlpha(self._ptr) + return KhrDfPrimaries(lib.ktxTexture2_GetPrimaries_e(self._ptr)) + + @property + def transfer_function(self) -> KhrDfTransfer: + """The transfer function of the images.""" + + return KhrDfTransfer(lib.ktxTexture2_GetTransferFunction_e(self._ptr)) @property - def needs_transcoding(self) -> bool: - """If the images are in a transcodable format.""" + def premultipled_alpha(self) -> bool: + """Whether the RGB components have been premultiplied by the alpha component.""" - return lib.ktxTexture2_NeedsTranscoding(self._ptr) + return lib.ktxTexture2_GetPremultipliedAlpha(self._ptr) def compress_astc(self, params: Union[int, KtxAstcParams]) -> None: """ @@ -135,7 +144,7 @@ def compress_basis(self, params: Union[int, KtxBasisParams]) -> None: params.quality_level = quality error = lib.PY_ktxTexture2_CompressBasisEx(self._ptr, - params.uastc, + params.codec, params.verbose, params.no_sse, params.thread_count, @@ -158,11 +167,34 @@ def compress_basis(self, params: Union[int, KtxBasisParams]) -> None: params.uastc_rdo_max_smooth_block_error_scale, params.uastc_rdo_max_smooth_block_std_dev, params.uastc_rdo_dont_favor_simpler_modes, - params.uastc_rdo_no_multithreading) + params.uastc_rdo_no_multithreading, + params.uastc_hdr_quality, + params.uastc_hdr_uber_mode, + params.uastc_hdr_ultra_quant, + params.uastc_hdr_favor_astc, + params.rec_2020, + params.uastc_hdr_lambda, + params.uastc_hdr_level + ) if int(error) != KtxErrorCode.SUCCESS: raise KtxError('ktxTexture2_CompressBasisEx', KtxErrorCode(error)) + def decode_astc(self) -> None: + """ + Decode a ktx2 texture object, if it is ASTC encoded. + + The decompressed format is calculated from corresponding ASTC format. + There are only 3 possible options currently supported. RGBA8, SRGBA8 + and RGBA32. + + Note that 3d textures are decoded to a multi-slice 3d texture. + """ + + error = lib.ktxTexture2_DecodeAstc(self._ptr); + if int(error) != KtxErrorCode.SUCCESS: + raise KtxError('ktx2_DecodeAstc', KtxErrorCode(error)) + def deflate_zstd(self, compression_level: int) -> None: """ Deflate the data in a ktxTexture2 object using Zstandard. diff --git a/interface/python_binding/tests/test_ktx_texture2.py b/interface/python_binding/tests/test_ktx_texture2.py index 90b92a3b95..7100183ca7 100644 --- a/interface/python_binding/tests/test_ktx_texture2.py +++ b/interface/python_binding/tests/test_ktx_texture2.py @@ -11,8 +11,9 @@ def test_create_from_named_file(self): test_ktx_file = os.path.join(__test_images__, 'ktx2/alpha_complex_straight.ktx2') texture = KtxTexture2.create_from_named_file(test_ktx_file) - self.assertEqual(texture.num_levels, 1) self.assertEqual(texture.num_faces, 1) + self.assertEqual(texture.num_layers, 1) + self.assertEqual(texture.num_levels, 1) self.assertEqual(texture.vk_format, VkFormat.VK_FORMAT_R8G8B8A8_SRGB) self.assertEqual(texture.base_width, 256) self.assertEqual(texture.base_height, 256) @@ -23,6 +24,7 @@ def test_create_from_named_file_mipmapped(self): texture = KtxTexture2.create_from_named_file(test_ktx_file, KtxTextureCreateFlagBits.NO_FLAGS) self.assertEqual(texture.vk_format, VkFormat.VK_FORMAT_ASTC_8x8_SRGB_BLOCK) + self.assertEqual(texture.num_layers, 1) self.assertEqual(texture.num_levels, 11) self.assertEqual(texture.base_width, 1024) self.assertEqual(texture.base_height, 1024) @@ -76,14 +78,47 @@ def test_compress_basis(self): test_ktx_file = os.path.join(__test_images__, 'ktx2/r8g8b8a8_srgb_array_7_mip.ktx2') texture = KtxTexture2.create_from_named_file(test_ktx_file, KtxTextureCreateFlagBits.LOAD_IMAGE_DATA_BIT) + self.assertEqual(texture.color_model, KhrDfModel.RGBSDA) self.assertEqual(texture.is_compressed, False) self.assertEqual(texture.supercompression_scheme, KtxSupercmpScheme.NONE) - texture.compress_basis(KtxBasisParams(quality_level=1)) + texture.compress_basis(1) + self.assertEqual(texture.color_model, KhrDfModel.ETC1S) self.assertEqual(texture.is_compressed, True) self.assertEqual(texture.supercompression_scheme, KtxSupercmpScheme.BASIS_LZ) + def test_compress_basis_with_params(self): + test_ktx_file = os.path.join(__test_images__, 'ktx2/r8g8b8a8_srgb_array_7_mip.ktx2') + texture = KtxTexture2.create_from_named_file(test_ktx_file, KtxTextureCreateFlagBits.LOAD_IMAGE_DATA_BIT) + + self.assertEqual(texture.color_model, KhrDfModel.RGBSDA) + self.assertEqual(texture.is_compressed, False) + self.assertEqual(texture.supercompression_scheme, KtxSupercmpScheme.NONE) + + texture.compress_basis(KtxBasisParams(codec=KtxBasisCodec.UASTC_LDR_4x4,uastc_rdo=True)) + + self.assertEqual(texture.color_model, KhrDfModel.UASTC) + self.assertEqual(texture.color_model, KhrDfModel.UASTC_LDR_4x4) + self.assertEqual(texture.is_compressed, True) + self.assertEqual(texture.supercompression_scheme, KtxSupercmpScheme.NONE) + + def test_compress_basis_hdr(self): + test_ktx_file = os.path.join(__test_images__, 'ktx2/Desk_small_zstd_15.ktx2') + texture = KtxTexture2.create_from_named_file(test_ktx_file, KtxTextureCreateFlagBits.LOAD_IMAGE_DATA_BIT) + + self.assertEqual(texture.color_model, KhrDfModel.RGBSDA) + self.assertEqual(texture.is_compressed, False) + self.assertEqual(texture.supercompression_scheme, KtxSupercmpScheme.NONE) + + texture.compress_basis(KtxBasisParams(codec=KtxBasisCodec.UASTC_HDR_6x6_INTERMEDIATE, + uastc_hdr_lambda=100., + uastc_hdr_level=3)) + + self.assertEqual(texture.color_model, KhrDfModel.UASTC_HDR_6x6) + self.assertEqual(texture.is_compressed, True) + self.assertEqual(texture.supercompression_scheme, KtxSupercmpScheme.UASTC_HDR_6x6_INTERMEDIATE) + def test_transcode_basis(self): test_ktx_file = os.path.join(__test_images__, 'ktx2/color_grid_blze.ktx2') texture = KtxTexture2.create_from_named_file(test_ktx_file, KtxTextureCreateFlagBits.LOAD_IMAGE_DATA_BIT) @@ -103,3 +138,59 @@ def test_create(self): texture = KtxTexture2.create(info, KtxTextureCreateStorage.ALLOC) self.assertEqual(texture.vk_format, VkFormat.VK_FORMAT_ASTC_4x4_SRGB_BLOCK) texture.set_image_from_memory(0, 0, 0, bytes(texture.data_size)) + + def test_queries(self): + test_ktx_file = os.path.join(__test_images__, 'ktx2/alpha_simple_blze.ktx2') + texture = KtxTexture2.create_from_named_file(test_ktx_file, KtxTextureCreateFlagBits.LOAD_IMAGE_DATA_BIT) + self.assertTrue(texture.needs_transcoding) + self.assertTrue(texture.is_transcodable) + self.assertFalse(texture.is_hdr); + self.assertFalse(texture.premultipled_alpha) + self.assertEqual(texture.color_model, KhrDfModel.ETC1S) + self.assertEqual(texture.primaries, KhrDfPrimaries.BT709) + self.assertEqual(texture.transfer_function, KhrDfTransfer.SRGB) + + def test_compress_astc(self): + test_ktx_file = os.path.join(__test_images__, 'ktx2/r8g8b8a8_srgb_array_7_mip.ktx2') + texture = KtxTexture2.create_from_named_file(test_ktx_file, KtxTextureCreateFlagBits.LOAD_IMAGE_DATA_BIT) + self.assertFalse(texture.is_compressed) + self.assertEqual(texture.num_layers, 7) + self.assertEqual(texture.transfer_function, KhrDfTransfer.SRGB) + texture.compress_astc(KtxPackAstcQualityLevels.FAST) + self.assertTrue(texture.is_compressed) + self.assertEqual(texture.color_model, KhrDfModel.ASTC) + self.assertEqual(texture.num_layers, 7) + self.assertEqual(texture.transfer_function, KhrDfTransfer.SRGB) + + def test_compress_astc_with_params(self): + test_ktx_file = os.path.join(__test_images__, 'ktx2/r8g8b8a8_srgb_array_7_mip.ktx2') + texture = KtxTexture2.create_from_named_file(test_ktx_file, KtxTextureCreateFlagBits.LOAD_IMAGE_DATA_BIT) + self.assertFalse(texture.is_compressed) + self.assertEqual(texture.num_layers, 7) + self.assertEqual(texture.transfer_function, KhrDfTransfer.SRGB) + texture.compress_astc(KtxAstcParams(quality_level=KtxPackAstcQualityLevels.FAST, + block_dimension=KtxPackAstcBlockDimension.D8x8)) + self.assertTrue(texture.is_compressed) + self.assertEqual(texture.color_model, KhrDfModel.ASTC) + self.assertEqual(texture.num_layers, 7) + self.assertEqual(texture.transfer_function, KhrDfTransfer.SRGB) + + def test_decode_astc(self): + test_ktx_file = os.path.join(__test_images__, 'ktx2/astc_8x8_unorm_array_7.ktx2') + texture = KtxTexture2.create_from_named_file(test_ktx_file, KtxTextureCreateFlagBits.LOAD_IMAGE_DATA_BIT) + self.assertEqual(texture.color_model, KhrDfModel.ASTC) + self.assertTrue(texture.is_compressed) + self.assertTrue(texture.is_array) + self.assertFalse(texture.is_hdr); + self.assertFalse(texture.premultipled_alpha) + self.assertEqual(texture.num_layers, 7) + self.assertFalse(texture.needs_transcoding) + texture.decode_astc(); + self.assertFalse(texture.is_compressed) + self.assertEqual(texture.color_model, KhrDfModel.RGBSDA) + self.assertEqual(texture.transfer_function, KhrDfTransfer.LINEAR) + self.assertTrue(texture.is_array) + self.assertFalse(texture.is_hdr); + self.assertFalse(texture.premultipled_alpha) + self.assertEqual(texture.num_layers, 7) + self.assertFalse(texture.needs_transcoding) diff --git a/lib/include/ktx.h b/lib/include/ktx.h index 964f3d7a3f..235e12df1d 100644 --- a/lib/include/ktx.h +++ b/lib/include/ktx.h @@ -1061,6 +1061,9 @@ ktxTexture1_CreateFromStream(ktxStream* stream, KTX_API void KTX_APIENTRY ktxTexture1_Destroy(ktxTexture1* This); +KTX_API ktx_bool_t KTX_APIENTRY +ktxTexture1_IsHDR(ktxTexture1* This); + KTX_API ktx_bool_t KTX_APIENTRY ktxTexture1_NeedsTranscoding(ktxTexture1* This); @@ -1180,6 +1183,9 @@ ktxTexture2_GetPremultipliedAlpha(ktxTexture2* This); KTX_API khr_df_primaries_e KTX_APIENTRY ktxTexture2_GetPrimaries_e(ktxTexture2* This); +KTX_API ktx_bool_t KTX_APIENTRY +ktxTexture2_IsHDR(ktxTexture2* This); + KTX_API ktx_bool_t KTX_APIENTRY ktxTexture2_NeedsTranscoding(ktxTexture2* This); @@ -1573,26 +1579,33 @@ typedef struct ktxBasisParams { deterministic). */ ktx_uint32_t uastcHDRQuality; - /*!< UASTC HDR 4x4: Sets the UASTC HDR 4x4 compressor's level. Valid range is [0,4] - higher=slower but higher quality. HDR default=1. - Level 0=fastest/lowest quality, 3=highest practical setting, 4=exhaustive + /*!< UASTC HDR 4x4: Sets the UASTC HDR 4x4 compressor's level. + Valid range is [0,4] - higher=slower but higher quality. Default=1. + Level 0=fastest/lowest quality, 3=highest practical setting, 4=exhaustive */ ktx_bool_t uastcHDRUberMode; - /*!< UASTC HDR 4x4: Allow the UASTC HDR 4x4 encoder to try varying the CEM 11 selectors more for slightly higher quality (slower). This may negatively impact BC6H quality, however. + /*!< UASTC HDR 4x4: Allow the UASTC HDR 4x4 encoder to try varying the CEM 11 + selectors more for slightly higher quality (slower). This may negatively impact BC6H quality, however. */ ktx_bool_t uastcHDRUltraQuant; /*!< UASTC HDR 4x4: Try to find better quantized CEM 7/11 endpoint values (slower) */ ktx_bool_t uastcHDRFavorAstc; - /*!< UASTC HDR 4x4: By default the UASTC HDR 4x4 encoder tries to strike a balance or even slightly favor BC6H quality. If this option is specified, ASTC HDR 4x4 quality is favored instead. + /*!< UASTC HDR 4x4: By default the UASTC HDR 4x4 encoder tries to strike a balance + or even slightly favor BC6H quality. If this option is specified, ASTC HDR 4x4 quality is favored instead. */ ktx_bool_t rec2020; - /*!< UASTC HDR 6x6i specific option: The input image's gamut is Rec. 2020 vs. the default Rec. 709 - for accurate colorspace error calculations. + /*!< UASTC HDR 6x6i specific option: The input image's gamut is Rec. 2020 vs. the + default Rec. 709 - for accurate colorspace error calculations. */ float uastcHDRLambda; - /*!< UASTC HDR 6x6i specific option: Enables rate distortion optimization (RDO). The higher this value, the lower the quality, but the smaller the file size. Try 100-20000, or higher values on some images. + /*!< UASTC HDR 6x6i specific option: Enables rate distortion optimization (RDO). + The higher this value, the lower the quality, but the smaller the file size. Try 100-20000, or higher values + on some images. */ ktx_uint32_t uastcHDRLevel; - /*!< UASTC HDR 6x6i specific option: Controls the 6x6 HDR intermediate mode encoder performance vs. max quality tradeoff. X may range from [0,12]. Default level is 2. + /*!< UASTC HDR 6x6i specific option: Controls the 6x6 HDR intermediate mode encoder + performance vs. max quality tradeoff. X may range from [0,12]. Default level is 2. */ } ktxBasisParams; diff --git a/tests/resources/genktx2 b/tests/resources/genktx2 index 485e71be58..fad242a4ba 100755 --- a/tests/resources/genktx2 +++ b/tests/resources/genktx2 @@ -141,3 +141,5 @@ $ktx convert --testrun -t ktx ktx/pattern_02_bc2.ktx ktx2/pattern_02_bc2.ktx2 # HDR images $ktx create --testrun --format R16G16B16_SFLOAT --encode uastc-hdr-4x4 --uastc-hdr-ultra-quant --zstd 15 input/exr/Desk.exr ktx2/Desk_uastc_hdr4x4_zstd_15.ktx2 $ktx create --testrun --format R16G16B16_SFLOAT --encode uastc-hdr-6x6i --uastc-hdr-lambda 300 input/exr/Desk.exr ktx2/Desk_uastc_hdr6x6i.ktx2 +# Small uncompressed file for the pyktx tests. +$ktx create --testrun --format R16G16B16_SFLOAT --scale 0.25 --zstd 15 input/exr/Desk.exr ktx2/Desk_small_zstd_15.ktx2 diff --git a/tests/resources/ktx2/Desk_small_zstd_15.ktx2 b/tests/resources/ktx2/Desk_small_zstd_15.ktx2 new file mode 100644 index 0000000000..7528b3b9b4 --- /dev/null +++ b/tests/resources/ktx2/Desk_small_zstd_15.ktx2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2337d10de15d343f360511a9f055741b81c7f46b5364249ee16b25f6c4df324 +size 194396 From 158e70fb453bc3f922e129548eb06b684ff6e75f Mon Sep 17 00:00:00 2001 From: Mark Callow Date: Thu, 19 Mar 2026 21:42:32 +0900 Subject: [PATCH 08/11] Update basisu for fixes backported from 2.0 (#1151) These are: - Critical fixes for extreme inputs/outputs - Compiler warning - Fix parsing of uint64 in KTX2 header - Fix initialization of astc_helper tables for transcoding. Remove our workaround for the astc_helper tables not being initialized. Fix ktxdiff to choose correct transcode target when comparing HDR payloads. --- external/basis_universal/.gitrepo | 6 +- .../encoder/basisu_astc_hdr_6x6_enc.cpp | 17 ++- .../encoder/basisu_astc_hdr_common.cpp | 4 + .../transcoder/basisu_transcoder.cpp | 130 +++++++++++------- lib/src/basis_transcode.cpp | 1 - tests/ktxdiff/ktxdiff_main.cpp | 4 +- 6 files changed, 103 insertions(+), 59 deletions(-) diff --git a/external/basis_universal/.gitrepo b/external/basis_universal/.gitrepo index 8519ab5814..743d153a47 100644 --- a/external/basis_universal/.gitrepo +++ b/external/basis_universal/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/KhronosGroup/basis_universal.git - branch = cmake_fixes - commit = daf79c6ee7343f547d29c36d180331a73300607b - parent = d5787860a2a47e8ecc79f437566c0d62cc5cebee + branch = fixes_for_ktx_v5 + commit = b13792b21d48045c9e44661b8b99c5a39ecf27c3 + parent = 5444378e164ad1ed00b52a4d8f9d414dab22fd1e method = merge cmdver = 0.4.9 diff --git a/external/basis_universal/encoder/basisu_astc_hdr_6x6_enc.cpp b/external/basis_universal/encoder/basisu_astc_hdr_6x6_enc.cpp index fd1efe520e..df99f001db 100644 --- a/external/basis_universal/encoder/basisu_astc_hdr_6x6_enc.cpp +++ b/external/basis_universal/encoder/basisu_astc_hdr_6x6_enc.cpp @@ -1645,6 +1645,7 @@ static bool estimate_partition3_6x6( float brightest_inten = 0.0f, darkest_inten = BIG_FLOAT_VAL; vec3F cluster_centroids[NUM_SUBSETS]; + clear_obj(cluster_centroids); for (uint32_t i = 0; i < BLOCK_T; i++) { @@ -1702,7 +1703,7 @@ static bool estimate_partition3_6x6( for (uint32_t s = 0; s < NUM_ITERS; s++) { memset(num_cluster_pixels, 0, sizeof(num_cluster_pixels)); - memset(new_cluster_means, 0, sizeof(new_cluster_means)); + memset((void *)new_cluster_means, 0, sizeof(new_cluster_means)); for (uint32_t i = 0; i < BLOCK_T; i++) { @@ -1855,14 +1856,16 @@ static bool encode_block_3_subsets( uint8_t blk_weights[NUM_SUBSETS][BLOCK_W * BLOCK_H]; uint32_t best_submode[NUM_SUBSETS]; + bool failed_flag = false; double e = 0.0f; for (uint32_t part_iter = 0; part_iter < NUM_SUBSETS; part_iter++) { assert(part_total_pixels[part_iter]); + double part_e; if (cem == 7) { - e += encode_astc_hdr_block_mode_7( + part_e = encode_astc_hdr_block_mode_7( part_total_pixels[part_iter], (basist::half_float(*)[3])part_half_pixels[part_iter], (vec4F*)part_pixels_q16[part_iter], best_log_blk.m_weight_ise_range, @@ -1877,7 +1880,7 @@ static bool encode_block_3_subsets( { assert(cem == 11); - e += encode_astc_hdr_block_mode_11( + part_e = encode_astc_hdr_block_mode_11( part_total_pixels[part_iter], (basist::half_float(*)[3])part_half_pixels[part_iter], (vec4F*)part_pixels_q16[part_iter], best_log_blk.m_weight_ise_range, @@ -1890,8 +1893,16 @@ static bool encode_block_3_subsets( FIRST_MODE11_SUBMODE_INDEX, MAX_MODE11_SUBMODE_INDEX, false, mode11_opt_mode); } + if (part_e == BIG_FLOAT_VAL) + { + failed_flag = true; + break; + } + e += part_e; } // part_iter + if (failed_flag) + continue; uint8_t ise_weights[BLOCK_W * BLOCK_H]; uint32_t src_pixel_index[NUM_SUBSETS] = { 0 }; diff --git a/external/basis_universal/encoder/basisu_astc_hdr_common.cpp b/external/basis_universal/encoder/basisu_astc_hdr_common.cpp index 65be44c1ee..a66ab5837c 100644 --- a/external/basis_universal/encoder/basisu_astc_hdr_common.cpp +++ b/external/basis_universal/encoder/basisu_astc_hdr_common.cpp @@ -3216,6 +3216,8 @@ double encode_astc_hdr_block_mode_11( float l = BIG_FLOAT_VAL, h = -BIG_FLOAT_VAL; vec3F low_color_q16, high_color_q16; + low_color_q16.clear(); + high_color_q16.clear(); for (uint32_t i = 0; i < num_pixels; i++) { @@ -3617,6 +3619,8 @@ double encode_astc_hdr_block_downsampled_mode_11( float l = BIG_FLOAT_VAL, h = -BIG_FLOAT_VAL; vec3F low_color_q16, high_color_q16; + low_color_q16.clear(); + high_color_q16.clear(); for (uint32_t i = 0; i < num_pixels; i++) { diff --git a/external/basis_universal/transcoder/basisu_transcoder.cpp b/external/basis_universal/transcoder/basisu_transcoder.cpp index f55d34e1d4..b22b75716c 100644 --- a/external/basis_universal/transcoder/basisu_transcoder.cpp +++ b/external/basis_universal/transcoder/basisu_transcoder.cpp @@ -2041,7 +2041,7 @@ namespace basist #if BASISD_SUPPORT_UASTC_HDR // TODO: Examine this, optimize for startup time/mem utilization. - astc_helpers::init_tables(false); + astc_helpers::init_tables(true); astc_hdr_core_init(); #endif @@ -19056,7 +19056,7 @@ namespace basist return false; } - if (((m_header.m_dfd_byte_offset + m_header.m_dfd_byte_length) > m_data_size) || (m_header.m_dfd_byte_offset < sizeof(ktx2_header))) + if (((m_header.m_dfd_byte_offset.get_uint64() + m_header.m_dfd_byte_length.get_uint64()) > m_data_size) || (m_header.m_dfd_byte_offset < sizeof(ktx2_header))) { BASISU_DEVEL_ERROR("ktx2_transcoder::init: Invalid DFD offset and/or length\n"); return false; @@ -19823,8 +19823,8 @@ namespace basist BASISU_DEVEL_ERROR("ktx2_transcoder::read_key_values: Invalid KVD byte offset\n"); return false; } - - if ((m_header.m_kvd_byte_offset + m_header.m_kvd_byte_length) > m_data_size) + + if ((m_header.m_kvd_byte_offset.get_uint64() + m_header.m_kvd_byte_length.get_uint64()) > m_data_size) { BASISU_DEVEL_ERROR("ktx2_transcoder::read_key_values: Invalid KVD byte offset and/or length\n"); return false; @@ -22181,50 +22181,6 @@ namespace basist static const int FAST_BC6H_COMPLEX_STD_DEV_THRESH = 512; static const int FAST_BC6H_VERY_COMPLEX_STD_DEV_THRESH = 2048; - static void assign_weights_simple_4( - const basist::half_float* pPixels, - uint8_t* pWeights, - int min_r, int min_g, int min_b, - int max_r, int max_g, int max_b, int64_t block_max_var) - { - BASISU_NOTE_UNUSED(block_max_var); - - float fmin_r = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)min_r); - float fmin_g = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)min_g); - float fmin_b = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)min_b); - - float fmax_r = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)max_r); - float fmax_g = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)max_g); - float fmax_b = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)max_b); - - float fdir_r = fmax_r - fmin_r; - float fdir_g = fmax_g - fmin_g; - float fdir_b = fmax_b - fmin_b; - - float l = inv_sqrt(fdir_r * fdir_r + fdir_g * fdir_g + fdir_b * fdir_b); - if (l != 0.0f) - { - fdir_r *= l; - fdir_g *= l; - fdir_b *= l; - } - - float lr = ftoh(fmin_r * fdir_r + fmin_g * fdir_g + fmin_b * fdir_b); - float hr = ftoh(fmax_r * fdir_r + fmax_g * fdir_g + fmax_b * fdir_b); - - float frr = (hr == lr) ? 0.0f : (14.93333f / (float)(hr - lr)); - - lr = (-lr * frr) + 0.53333f; - for (uint32_t i = 0; i < 16; i++) - { - const float r = fast_half_to_float_pos_not_inf_or_nan(pPixels[i * 3 + 0]); - const float g = fast_half_to_float_pos_not_inf_or_nan(pPixels[i * 3 + 1]); - const float b = fast_half_to_float_pos_not_inf_or_nan(pPixels[i * 3 + 2]); - const float w = ftoh(r * fdir_r + g * fdir_g + b * fdir_b); - - pWeights[i] = (uint8_t)basisu::clamp((int)(w * frr + lr), 0, 15); - } - } static double assign_weights_4( const vec3F* pFloat_pixels, const float* pPixel_scales, @@ -22415,6 +22371,78 @@ namespace basist return total_err; } + static void assign_weights_simple_4( + const basist::half_float* pPixels, + uint8_t* pWeights, + int min_r, int min_g, int min_b, + int max_r, int max_g, int max_b, int64_t block_max_var, + const fast_bc6h_params& params) + { + BASISU_NOTE_UNUSED(block_max_var); + float fmin_r = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)min_r); + float fmin_g = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)min_g); + float fmin_b = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)min_b); + float fmax_r = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)max_r); + float fmax_g = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)max_g); + float fmax_b = fast_half_to_float_pos_not_inf_or_nan((basist::half_float)max_b); + + float fdir_r = fmax_r - fmin_r; + float fdir_g = fmax_g - fmin_g; + float fdir_b = fmax_b - fmin_b; + + float l = inv_sqrt(fdir_r * fdir_r + fdir_g * fdir_g + fdir_b * fdir_b); + if (l != 0.0f) + { + fdir_r *= l; + fdir_g *= l; + fdir_b *= l; + } + + float lf = fmin_r * fdir_r + fmin_g * fdir_g + fmin_b * fdir_b; + float hf = fmax_r * fdir_r + fmax_g * fdir_g + fmax_b * fdir_b; + + if ((lf >= basist::MAX_HALF_FLOAT) || (hf >= basist::MAX_HALF_FLOAT)) + { + // v2.1: Can't use the faster half float based tricks below, need some sort of backup + vec3F float_pixels[16]; + float pixel_scales[16]; + + for (uint32_t i = 0; i < 16; i++) + { + float_pixels[i].c[0] = fast_half_to_float_pos_not_inf_or_nan(pPixels[i * 3 + 0]); + float_pixels[i].c[1] = fast_half_to_float_pos_not_inf_or_nan(pPixels[i * 3 + 1]); + float_pixels[i].c[2] = fast_half_to_float_pos_not_inf_or_nan(pPixels[i * 3 + 2]); + + pixel_scales[i] = 1.0f / (basisu::squaref(float_pixels[i].c[0]) + basisu::squaref(float_pixels[i].c[1]) + basisu::squaref(float_pixels[i].c[2]) + (float)MIN_HALF_FLOAT); + } + + assign_weights_4( + float_pixels, pixel_scales, + pWeights, + min_r, min_g, min_b, + max_r, max_g, max_b, block_max_var, false, + params); + + return; + } + + float lr = ftoh(lf); + float hr = ftoh(hf); + + float frr = (hr == lr) ? 0.0f : (14.93333f / (float)(hr - lr)); + + lr = (-lr * frr) + 0.53333f; + for (uint32_t i = 0; i < 16; i++) + { + const float r = fast_half_to_float_pos_not_inf_or_nan(pPixels[i * 3 + 0]); + const float g = fast_half_to_float_pos_not_inf_or_nan(pPixels[i * 3 + 1]); + const float b = fast_half_to_float_pos_not_inf_or_nan(pPixels[i * 3 + 2]); + const float w = ftoh(basisu::minimumf(r * fdir_r + g * fdir_g + b * fdir_b, basist::MAX_HALF_FLOAT)); + + pWeights[i] = (uint8_t)basisu::clamp((int)(w * frr + lr), 0, 15); + } + } + static void assign_weights3(uint8_t trial_weights[16], uint32_t best_pat_bits, uint32_t subset_min_r[2], uint32_t subset_min_g[2], uint32_t subset_min_b[2], @@ -22848,7 +22876,7 @@ namespace basist BASISU_NOTE_UNUSED(base_bitmask); const uint32_t num_delta_bits[3] = { g_bc6h_mode_sig_bits[mode][1], g_bc6h_mode_sig_bits[mode][2], g_bc6h_mode_sig_bits[mode][3] }; - const int delta_bitmasks[3] = { (1 << num_delta_bits[0]) - 1, (1 << num_delta_bits[1]) - 1, (1 << num_delta_bits[2]) - 1 }; + //const int delta_bitmasks[3] = { (1 << num_delta_bits[0]) - 1, (1 << num_delta_bits[1]) - 1, (1 << num_delta_bits[2]) - 1 }; for (uint32_t subset_index = 0; subset_index < 2; subset_index++) { @@ -23175,7 +23203,7 @@ namespace basist bc6h_quant_dequant_endpoints(min_r, min_g, min_b, max_r, max_g, max_b, 10); - assign_weights_simple_4(pPixels, log_blk.m_weights, min_r, min_g, min_b, max_r, max_g, max_b, block_max_var); + assign_weights_simple_4(pPixels, log_blk.m_weights, min_r, min_g, min_b, max_r, max_g, max_b, block_max_var, params); log_blk.m_endpoints[0][0] = basist::bc6h_half_to_blog((basist::half_float)min_r, 10); log_blk.m_endpoints[0][1] = basist::bc6h_half_to_blog((basist::half_float)max_r, 10); @@ -23290,7 +23318,7 @@ namespace basist max_g = pPixels[max_idx * 3 + 1]; max_b = pPixels[max_idx * 3 + 2]; - assert((max_r < MAX_HALF_FLOAT_AS_INT_BITS) && (max_g < MAX_HALF_FLOAT_AS_INT_BITS) && (max_b < MAX_HALF_FLOAT_AS_INT_BITS)); + assert((max_r <= MAX_HALF_FLOAT_AS_INT_BITS) && (max_g <= MAX_HALF_FLOAT_AS_INT_BITS) && (max_b <= MAX_HALF_FLOAT_AS_INT_BITS)); bc6h_quant_dequant_endpoints(min_r, min_g, min_b, max_r, max_g, max_b, 10); diff --git a/lib/src/basis_transcode.cpp b/lib/src/basis_transcode.cpp index 21be9638f1..39bdb670dd 100644 --- a/lib/src/basis_transcode.cpp +++ b/lib/src/basis_transcode.cpp @@ -428,7 +428,6 @@ ktx2transcoderFormat(ktx_transcode_fmt_e ktx_fmt) { std::lock_guard lock(init_mutex); if (!transcoderInitialized.load(std::memory_order_relaxed)) { basisu_transcoder_init(); - astc_helpers::init_tables(true); transcoderInitialized.store(true, std::memory_order_release); } } diff --git a/tests/ktxdiff/ktxdiff_main.cpp b/tests/ktxdiff/ktxdiff_main.cpp index 1f67c3f430..4294bd4d8c 100644 --- a/tests/ktxdiff/ktxdiff_main.cpp +++ b/tests/ktxdiff/ktxdiff_main.cpp @@ -200,7 +200,9 @@ void Texture::loadKTX() { error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": ktxTexture2_CreateFromNamedFile: {}\n", filepath, ktxErrorString(ec)); if (ktxTexture2_NeedsTranscoding(handle)) { - ec = ktxTexture2_TranscodeBasis(handle, KTX_TTF_RGBA32, 0); + ktx_transcode_fmt_e outputFmt = ktxTexture_IsHDR(ktxTexture(handle)) ? + KTX_TTF_RGBA_HALF : KTX_TTF_RGBA32; + ec = ktxTexture2_TranscodeBasis(handle, outputFmt, 0); if (ec != KTX_SUCCESS) error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": ktxTexture2_TranscodeBasis: {}\n", filepath, ktxErrorString(ec)); transcoded = true; From fd84c2a9de77d47ea627f6683555a21dd59e02d1 Mon Sep 17 00:00:00 2001 From: Vineek Date: Mon, 23 Mar 2026 10:32:43 +0200 Subject: [PATCH 09/11] Update cts reference --- tests/cts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cts b/tests/cts index cbbf4bcd82..cd012933d0 160000 --- a/tests/cts +++ b/tests/cts @@ -1 +1 @@ -Subproject commit cbbf4bcd8246f20e4e879dca5daeea73b8a10353 +Subproject commit cd012933d0f057589dffd807c57dae6e851944d1 From 2f8e872fcb11e4837a7c28b11c6f2e1e962dcafc Mon Sep 17 00:00:00 2001 From: Nick Vitsas Date: Thu, 26 Mar 2026 11:40:18 +0200 Subject: [PATCH 10/11] Update ktxdiff for proper float32 comparisons --- tests/ktxdiff/ktxdiff_main.cpp | 204 ++++++++++++++++++++++++++++++--- 1 file changed, 186 insertions(+), 18 deletions(-) diff --git a/tests/ktxdiff/ktxdiff_main.cpp b/tests/ktxdiff/ktxdiff_main.cpp index 4294bd4d8c..78a5bd891f 100644 --- a/tests/ktxdiff/ktxdiff_main.cpp +++ b/tests/ktxdiff/ktxdiff_main.cpp @@ -1,5 +1,6 @@ // Copyright 2022-2023 The Khronos Group Inc. // Copyright 2022-2023 RasterGrid Kft. +// float16_to_float32 code Copyright 2011-2021 Arm Limited // SPDX-License-Identifier: Apache-2.0 #include "ktx.h" @@ -24,7 +25,7 @@ template [[nodiscard]] constexpr inline T ceil_div(const T x, const T y) noexcept { assert(y != 0); - return (x + y - 1) / y; + return (x + y - 1) / y; } // C++20 - std::bit_cast @@ -199,7 +200,7 @@ void Texture::loadKTX() { if (ec != KTX_SUCCESS) error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": ktxTexture2_CreateFromNamedFile: {}\n", filepath, ktxErrorString(ec)); - if (ktxTexture2_NeedsTranscoding(handle)) { + if (ktxTexture2_IsTranscodable(handle)) { ktx_transcode_fmt_e outputFmt = ktxTexture_IsHDR(ktxTexture(handle)) ? KTX_TTF_RGBA_HALF : KTX_TTF_RGBA32; ec = ktxTexture2_TranscodeBasis(handle, outputFmt, 0); @@ -236,6 +237,146 @@ void Texture::loadMetadata() { // ------------------------------------------------------------------------------------------------- +// float16 to float conversion code is taken from ARM's ASTC encoder under +// its Apache 2.0 license. Copyright is credited at the top of this file. + +// Union for manipulation of float bit patterns +typedef union +{ + uint32_t u; + int32_t s; + float f; +} if32; + +typedef uint16_t sf16; +typedef uint32_t sf32; + +#if defined(__GNUC__) && (defined(__i386) || defined(__amd64)) +#elif defined(__arm__) && defined(__ARMCC_VERSION) +#elif defined(__arm__) && defined(__GNUC__) +#else + /* table used for the slow default versions. */ + static const uint8_t clz_table[256] = + { + 8, 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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, 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, 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, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; +#endif + +/* 32-bit count-leading-zeros function: use the Assembly instruction whenever possible. */ +static uint32_t clz32(uint32_t inp) +{ + #if defined(__GNUC__) && (defined(__i386) || defined(__amd64)) + uint32_t bsr; + __asm__("bsrl %1, %0": "=r"(bsr):"r"(inp | 1)); + return 31 - bsr; + #else + #if defined(__arm__) && defined(__ARMCC_VERSION) + return __clz(inp); /* armcc builtin */ + #else + #if defined(__arm__) && defined(__GNUC__) + uint32_t lz; + __asm__("clz %0, %1": "=r"(lz):"r"(inp)); + return lz; + #else + /* slow default version */ + uint32_t summa = 24; + if (inp >= UINT32_C(0x10000)) + { + inp >>= 16; + summa -= 16; + } + if (inp >= UINT32_C(0x100)) + { + inp >>= 8; + summa -= 8; + } + return summa + clz_table[inp]; + #endif + #endif + #endif +} + +/* convert from FP16 to FP32. */ +static sf32 sf16_to_sf32(sf16 inp) +{ + uint32_t inpx = inp; + + /* + This table contains, for every FP16 sign/exponent value combination, + the difference between the input FP16 value and the value obtained + by shifting the correct FP32 result right by 13 bits. + This table allows us to handle every case except denormals and NaN + with just 1 table lookup, 2 shifts and 1 add. + */ + + #define WITH_MSB(a) (UINT32_C(a) | (1u << 31)) + static const uint32_t tbl[64] = + { + WITH_MSB(0x00000), 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, + 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, + 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, + 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, 0x1C000, WITH_MSB(0x38000), + WITH_MSB(0x38000), 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, + 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, + 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, + 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, 0x54000, WITH_MSB(0x70000) + }; + + uint32_t res = tbl[inpx >> 10]; + res += inpx; + + /* Normal cases: MSB of 'res' not set. */ + if ((res & WITH_MSB(0)) == 0) + { + return res << 13; + } + + /* Infinity and Zero: 10 LSB of 'res' not set. */ + if ((res & 0x3FF) == 0) + { + return res << 13; + } + + /* NaN: the exponent field of 'inp' is non-zero. */ + if ((inpx & 0x7C00) != 0) + { + /* All NaNs are quietened. */ + return (res << 13) | 0x400000; + } + + /* Denormal cases */ + uint32_t sign = (inpx & 0x8000) << 16; + uint32_t mskval = inpx & 0x7FFF; + uint32_t leadingzeroes = clz32(mskval); + mskval <<= leadingzeroes; + return (mskval >> 8) + ((0x85 - leadingzeroes) << 23) + sign; +} + +/* convert from half-float to native-float */ +float float16_to_float(uint16_t h) +{ + if32 i; + i.u = sf16_to_sf32(h); + return i.f; +} + +// ------------------------------------------------------------------------------------------------- + struct CompareResult { bool match = true; float difference = 0.f; @@ -273,15 +414,31 @@ CompareResult compareSFloat32(const char* rawLhs, const char* rawRhs, std::size_ return CompareResult{}; } +CompareResult compareSFloat16(const char* rawLhs, const char* rawRhs, std::size_t rawSize, float tolerance) { + const auto* lhs = reinterpret_cast(rawLhs); + const auto* rhs = reinterpret_cast(rawRhs); + const auto element_size = sizeof(uint16_t); + const auto count = rawSize / element_size; + + for (std::size_t i = 0; i < count; ++i) { + const auto diff = std::abs(float16_to_float(lhs[i]) - float16_to_float(rhs[i])); + if (diff > tolerance) + return CompareResult{false, diff, i, i * element_size}; + } + + return CompareResult{}; +} + auto decodeASTC(const char* compressedData, std::size_t compressedSize, uint32_t width, uint32_t height, - const std::string& filepath, bool isFormatSRGB, uint32_t blockSizeX, uint32_t blockSizeY, uint32_t blockSizeZ) { + const std::string& filepath, bool isFloat, bool isFormatSRGB, + uint32_t blockSizeX, uint32_t blockSizeY, uint32_t blockSizeZ) { const auto threadCount = 1u; static constexpr astcenc_swizzle swizzle{ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A}; astcenc_error ec = ASTCENC_SUCCESS; - const astcenc_profile profile = isFormatSRGB ? ASTCENC_PRF_LDR_SRGB : ASTCENC_PRF_LDR; + const astcenc_profile profile = isFloat ? ASTCENC_PRF_HDR : isFormatSRGB ? ASTCENC_PRF_LDR_SRGB : ASTCENC_PRF_LDR; astcenc_config config{}; ec = astcenc_config_init(profile, blockSizeX, blockSizeY, blockSizeZ, ASTCENC_PRE_MEDIUM, ASTCENC_FLG_DECOMPRESS_ONLY, &config); if (ec != ASTCENC_SUCCESS) @@ -303,11 +460,12 @@ auto decodeASTC(const char* compressedData, std::size_t compressedSize, uint32_t image.dim_x = width; image.dim_y = height; image.dim_z = 1; // 3D ASTC formats are currently not supported - const auto uncompressedSize = width * height * 4 * sizeof(uint8_t); + const auto uncompressedSize = width * height * 4 * (isFloat ? sizeof(uint16_t) : sizeof(uint8_t)); auto uncompressedBuffer = std::make_unique(uncompressedSize); auto* bufferPtr = uncompressedBuffer.get(); image.data = reinterpret_cast(&bufferPtr); - image.data_type = ASTCENC_TYPE_U8; + // F16 is also the target transcode format for HDR transcodable textures. + image.data_type = isFloat ? ASTCENC_TYPE_F16 : ASTCENC_TYPE_U8; ec = astcenc_decompress_image(context, reinterpret_cast(compressedData), compressedSize, &image, &swizzle, 0); if (ec != ASTCENC_SUCCESS) @@ -324,16 +482,24 @@ auto decodeASTC(const char* compressedData, std::size_t compressedSize, uint32_t CompareResult compareAstc(const char* lhs, const char* rhs, std::size_t size, uint32_t width, uint32_t height, const std::string& filepathLhs, const std::string& filepathRhs, - bool isFormatSRGB, uint32_t blockSizeX, uint32_t blockSizeY, uint32_t blockSizeZ, + bool isFloat, bool isFormatSRGB, uint32_t blockSizeX, uint32_t blockSizeY, uint32_t blockSizeZ, float tolerance) { - const auto uncompressedLhs = decodeASTC(lhs, size, width, height, filepathLhs, isFormatSRGB, blockSizeX, blockSizeY, blockSizeZ); - const auto uncompressedRhs = decodeASTC(rhs, size, width, height, filepathRhs, isFormatSRGB, blockSizeX, blockSizeY, blockSizeZ); - - return compareUnorm8( - reinterpret_cast(uncompressedLhs.data.get()), - reinterpret_cast(uncompressedRhs.data.get()), - uncompressedLhs.size, - tolerance); + const auto uncompressedLhs = decodeASTC(lhs, size, width, height, filepathLhs, isFloat, isFormatSRGB, blockSizeX, blockSizeY, blockSizeZ); + const auto uncompressedRhs = decodeASTC(rhs, size, width, height, filepathRhs, isFloat, isFormatSRGB, blockSizeX, blockSizeY, blockSizeZ); + + if (isFloat) { + return compareSFloat16( + reinterpret_cast(uncompressedLhs.data.get()), + reinterpret_cast(uncompressedRhs.data.get()), + uncompressedLhs.size, + tolerance); + } else { + return compareUnorm8( + reinterpret_cast(uncompressedLhs.data.get()), + reinterpret_cast(uncompressedRhs.data.get()), + uncompressedLhs.size, + tolerance); + } } bool compare(Texture& lhs, Texture& rhs, float tolerance) { @@ -410,12 +576,14 @@ bool compare(Texture& lhs, Texture& rhs, float tolerance) { const char* imageDataRhs = reinterpret_cast(rhs->pData) + imageOffset; CompareResult result; - if (lhs.transcoded || isFormatUNORM8) { + if ((lhs.transcoded && !isFloat) || isFormatUNORM8) { result = compareUnorm8(imageDataLhs, imageDataRhs, imageSize, tolerance); - } else if (isFormatAstc(vkFormat)) { + } else if (lhs.transcoded && isFloat) { + result = compareSFloat16(imageDataLhs, imageDataRhs, imageSize, tolerance); + } else if (isFormatAstc(vkFormat)) { result = compareAstc(imageDataLhs, imageDataRhs, imageSize, imageWidth, imageHeight, lhs.filepath, rhs.filepath, - isFormatSRGB, blockSizeX, blockSizeY, blockSizeZ, + isFloat, isFormatSRGB, blockSizeX, blockSizeY, blockSizeZ, tolerance); } else if (isFormatSFloat32) { result = compareSFloat32(imageDataLhs, imageDataRhs, imageSize, tolerance); From 898008d42445e8a2e61193acfd2712a67fc9dd8a Mon Sep 17 00:00:00 2001 From: Nick Vitsas Date: Thu, 26 Mar 2026 11:40:43 +0200 Subject: [PATCH 11/11] Update to latest CTS commit --- tests/cts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cts b/tests/cts index cd012933d0..028019aee3 160000 --- a/tests/cts +++ b/tests/cts @@ -1 +1 @@ -Subproject commit cd012933d0f057589dffd807c57dae6e851944d1 +Subproject commit 028019aee346bdb2386aefea51a290fb83d90c0a