diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cbaf56c858..01c5ac1ad2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -183,7 +183,9 @@ jobs:
UHDR_CMAKE_CXX_COMPILER=g++
openjph_CMAKE_C_COMPILER=gcc
openjph_CMAKE_CXX_COMPILER=g++
- # Building libuhdr with icx results in test failures
+ KTX_CMAKE_C_COMPILER=gcc
+ KTX_CMAKE_CXX_COMPILER=g++
+ # Building libuhdr and libktx with icx results in test failures
optional_deps_append: "LibRaw;Ptex;Qt6"
- desc: VFX2026 gcc14/C++20 py3.13 exr3.4 ocio2.5
nametag: linux-vfx2026
@@ -493,8 +495,10 @@ jobs:
Robinmap_GIT_COMMIT=68ff7325b3898fca267a103bad5c509e8861144d
TIFF_BUILD_VERSION=4.0.0
TIFF_GIT_COMMIT=f7b79dc7dc86ccbaabe9882e2b9ffa5ee8dac917
+ ENABLE_KTX=0
# OpenJPEG_BUILD_VERSION=2.2.0
# OpenJPEG_GIT_COMMIT=3d7cde5fc9fbc5618d02160900d32e02ed12a00e
+ # libktx > 4.3.2 requires CMake >= 3.22
optional_deps_append: 'FFmpeg;LibRaw;Ptex;Qt6'
build_local_deps: 'TIFF'
- desc: oldest clang11/C++17 py3.9 exr3.1 ocio2.3
@@ -525,8 +529,10 @@ jobs:
TIFF_GIT_COMMIT=f7b79dc7dc86ccbaabe9882e2b9ffa5ee8dac917
openjph_CMAKE_C_COMPILER=gcc
openjph_CMAKE_CXX_COMPILER=g++
+ ENABLE_KTX=0
# OpenJPEG_BUILD_VERSION=2.2.0
# OpenJPEG_GIT_COMMIT=3d7cde5fc9fbc5618d02160900d32e02ed12a00e
+ # libktx > 4.3.2 requires CMake >= 3.22
optional_deps_append: 'FFmpeg;LibRaw;Ptex;Qt6'
build_local_deps: 'TIFF'
@@ -564,6 +570,8 @@ jobs:
Robinmap_GIT_COMMIT=68ff7325b3898fca267a103bad5c509e8861144d
TIFF_BUILD_VERSION=4.0.0
TIFF_GIT_COMMIT=f7b79dc7dc86ccbaabe9882e2b9ffa5ee8dac917
+ ENABLE_KTX=0
+ # libktx > 4.3.2 requires CMake >= 3.22
required_deps: none
build_local_deps: 'TIFF'
@@ -625,6 +633,9 @@ jobs:
ctest_test_timeout: 1200
setenvs: export MACOSX_DEPLOYMENT_TARGET=12.0
INSTALL_QT=0 INSTALL_OPENCV=0 INSTALL_OPENVDB=0
+ Ktx_ASTCENC_ISA=ASTCENC_ISA_SSE41
+ # Intel-based MacOS runners don't support AVX2 SIMD which is the
+ # default set by libktx. Explicitly set to SSE4.1 (2nd fastest after AVX2)
optional_deps_append: 'OpenCV;OpenVDB;Qt5;Qt6'
benchmark: 1
- desc: MacOS-14-ARM aclang15/C++20/py3.13
diff --git a/.gitignore b/.gitignore
index 2058a138ce..83f2475a9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ gastest.o
/*.jxl
/*.tx
/*.log
+/*.ktx2
# AI coding tool directories — generated locally by .agents/setup-agent
.claude/
diff --git a/src/cmake/build_Ktx.cmake b/src/cmake/build_Ktx.cmake
new file mode 100644
index 0000000000..1b534b37ee
--- /dev/null
+++ b/src/cmake/build_Ktx.cmake
@@ -0,0 +1,91 @@
+# Copyright Contributors to the OpenImageIO project.
+# SPDX-License-Identifier: Apache-2.0
+# https://github.com/AcademySoftwareFoundation/OpenImageIO
+
+# TODO: revert this before merging! I set this to my own PR branch that includes
+# BCn and that will be merged (as per the reviewer)
+set_cache (Ktx_BUILD_VERSION add-BCn-decoder "Ktx version for local builds")
+set (Ktx_GIT_REPOSITORY "https://github.com/walcht/KTX-Software.git")
+set_cache (Ktx_GIT_TAG "${Ktx_BUILD_VERSION}" "Git branch or tag")
+set_cache (Ktx_GIT_COMMIT "77075ec1d5125df4b938e2f5d96565b10189b1f3"
+ "commit hash to verify tag/branch against")
+set_cache (Ktx_BUILD_SHARED_LIBS OFF #${LOCAL_BUILD_SHARED_LIBS_DEFAULT}
+ DOC "Should a local Ktx build, if necessary, build shared libraries" ADVANCED)
+
+# TODO: if libktx is built as a shared library, astcenc have to be linked-against.
+# I don't know how to 'cleanly' do this in OIIO CMake (yet).
+# Even though all CIs pass, do not merge before addressing this!
+
+string (MAKE_C_IDENTIFIER ${Ktx_BUILD_VERSION} Ktx_VERSION_IDENT)
+
+# Override C/C++ compiler (useful when running CI with unsupported Intel's compiler ICX)
+set_cache (KTX_CMAKE_C_COMPILER ${CMAKE_C_COMPILER} "libktx build C compiler override" ADVANCED)
+set_cache (KTX_CMAKE_CXX_COMPILER ${CMAKE_CXX_COMPILER} "libktx build C++ compiler override" ADVANCED)
+
+# The only tested libktx versions are:
+# - v0.0.0 (main:HEAD which is versionless)
+# - v5.0.0-rc1 (newest supported)
+#
+# for detailed build instructions, see:
+# https://github.com/KhronosGroup/KTX-Software/blob/main/BUILDING.md
+#
+# KTX-Software not only provides Ktx but also a set of CLI tools and load
+# test applications that we do not need. We only need the libktx component
+# without any GPU texture loading functionalities.
+#
+# On x86_64, libktx defaults to using AVX2 for ASTC. For Intel-based MacOS, the
+# default AVX2 requires x86_64h which may not be available (e.g., Intel-based
+# MacOS Github Actions CIs). For ARM64 libktx defaults to using Neon. For
+# 'unknown' CPUs, SIMD is disabled (see KTX-Software/lib/CMakeLists.txt for details).
+#
+# Possible values:
+# - "": default, let libktx decide
+# - ASTCENC_ISA_NATIVE: native SIMD
+# - ASTCENC_ISA_NONE: disable SIMD
+# - ASTCENC_ISA_SVE_256: slowest on Arm
+# - ASTCENC_ISA_SVE_128: 2nd fastest on Arm
+# - ASTCENC_ISA_NEON: fasted on Arm
+# - ASTCENC_ISA_AVX2: fastest on x86_64 (may not be supported on Intel-based MacOS runners)
+# - ASTCENC_ISA_SSE41: 2nd fastest on x86_64 (guaranteed to be supported)
+# - ASTCENC_ISA_SSE2: slowest on x86_64 (guaranteed to be supported)
+#
+set_cache (Ktx_ASTCENC_ISA "" "ASTC specific SIMD instruction set. See astc-encoder/CMakeLists.txt for more details" ADVANCED)
+
+set(CMAKE_ARGS_LIST
+ -DCMAKE_POSITION_INDEPENDENT_CODE=ON
+ -DCMAKE_C_COMPILER=${KTX_CMAKE_C_COMPILER}
+ -DCMAKE_CXX_COMPILER=${KTX_CMAKE_CXX_COMPILER}
+ -DBUILD_SHARED_LIBS=${Ktx_BUILD_SHARED_LIBS}
+ -DCMAKE_INSTALL_LIBDIR=lib
+ -DLIBKTX_VERSION_READ_ONLY=OFF
+ -DLIBKTX_VERSION_FULL=ON
+ -DLIBKTX_FEATURE_KTX1=ON # Setting this to OFF causes linker issues
+ -DLIBKTX_FEATURE_KTX2=ON
+ -DLIBKTX_FEATURE_VK_UPLOAD=OFF
+ -DLIBKTX_FEATURE_GL_UPLOAD=OFF
+ -DLIBKTX_FEATURE_ETC_UNPACK=OFF # This has some weird licensing and I don't feel comfortable including it ...
+)
+
+if(Ktx_ASTCENC_ISA)
+ list(APPEND CMAKE_ARGS_LIST -D${Ktx_ASTCENC_ISA}=ON)
+endif()
+
+build_dependency_with_cmake(Ktx
+ VERSION ${Ktx_BUILD_VERSION}
+ GIT_REPOSITORY ${Ktx_GIT_REPOSITORY}
+ GIT_TAG ${Ktx_GIT_TAG}
+ GIT_COMMIT ${Ktx_GIT_COMMIT}
+ SOURCE_SUBDIR lib # To only build Ktx, cmake has to point to: KTX-Software/lib
+ CMAKE_ARGS ${CMAKE_ARGS_LIST}
+)
+
+# Set some things up that we'll need for a subsequent find_package to work
+set (Ktx_ROOT ${Ktx_LOCAL_INSTALL_DIR})
+
+# Signal to caller that we need to find again at the installed location
+set (Ktx_REFIND TRUE)
+set (Ktx_REFIND_ARGS CONFIG)
+
+if (Ktx_BUILD_SHARED_LIBS)
+ install_local_dependency_libs (Ktx ktx) # notice libname is lowercase
+endif ()
diff --git a/src/cmake/externalpackages.cmake b/src/cmake/externalpackages.cmake
index 5531f448dc..5c1e0bebe1 100644
--- a/src/cmake/externalpackages.cmake
+++ b/src/cmake/externalpackages.cmake
@@ -260,6 +260,20 @@ else ()
get_target_property(FMT_INCLUDE_DIR fmt::fmt-header-only INTERFACE_INCLUDE_DIRECTORIES)
endif ()
+# Note for Ktx plugin developers:
+# set VERSION_MIN to 0.0.0 if you have a locally-built tracking HEAD:main of
+# KTX-Software because building non-tagged libktx sets the version to 0.0.0.
+#
+# libktx 5.0.0 requires CMake >= 3.22. libktx v4.3.2 supports CMake v3.18 which
+# is used in "oldest gcc" CIs but supporting two versions of libktx is very
+# cumbersome.
+if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
+ checked_find_package (Ktx
+ # VERSION_MIN 5.0.0
+ VERSION_MIN 0.0.0 # If you target main branch of KTX-Software (versionless)
+ BUILD_LOCAL missing
+ )
+endif()
###########################################################################
diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake
index f59a027809..738c4dc511 100644
--- a/src/cmake/testing.cmake
+++ b/src/cmake/testing.cmake
@@ -337,6 +337,9 @@ macro (oiio_add_all_tests)
oiio_add_tests (dpx
ENABLEVAR ENABLE_DPX
IMAGEDIR oiio-images/dpx URL "Recent checkout of OpenImageIO-images")
+ oiio_add_tests (ktx
+ ENABLEVAR ENABLE_KTX
+ IMAGEDIR oiio-images/ktx2)
oiio_add_tests (dds
ENABLEVAR ENABLE_DDS
IMAGEDIR oiio-images/dds URL "Recent checkout of OpenImageIO-images")
@@ -545,9 +548,10 @@ function (oiio_get_test_data name)
endfunction()
function (oiio_setup_test_data)
+ # TODO: revert this after accepting OpenImageIO-images PR and before merging (just so that the CI passes)
oiio_get_test_data (oiio-images
- REPO https://github.com/AcademySoftwareFoundation/OpenImageIO-images.git
- BRANCH dev-${OpenImageIO_VERSION_MAJOR}.${OpenImageIO_VERSION_MINOR})
+ REPO https://github.com/walcht/OpenImageIO-images.git
+ BRANCH add-ktx2-images)
oiio_get_test_data (openexr-images
REPO https://github.com/AcademySoftwareFoundation/openexr-images.git
BRANCH main)
diff --git a/src/doc/builtinplugins.rst b/src/doc/builtinplugins.rst
index 8f12260530..8ff73cd334 100644
--- a/src/doc/builtinplugins.rst
+++ b/src/doc/builtinplugins.rst
@@ -1401,6 +1401,296 @@ control aspects of the writing itself:
(Does not work as expected at this moment. Box is written but content
unreadable in exif readers.)
+|
+
+.. _sec-bundledplugins-ktx:
+
+KTX
+===============================================
+
+KTX (Khronos Texture) is an efficient, lightweight container format for reliably
+distributing GPU textures to diverse platforms and applications.
+
+A KTX2 file can contain a 2D texture, a 3D texture (volume texture), a cubemap
+texture, any of said textures with mipmaps, or an array of any of said textures
+with or without mipmaps. KTX files use the file extension :file:`.ktx2`.
+
+OpenImageIO supports both reading and writing KTX2 files. Deprecated KTX1 files
+are currently not supported by OpenImageIO (hence why the file extension is
+:file:`.ktx2`).
+
+Although OpenImageIO supports writing KTX2 files, users are encouraged to use
+the official CLI tools provided by KTX-Software to generate KTX2 files. This is
+due to the high number of parameters that can be configured to generate a KTX2
+file which can be cumbersome to set when using OpenImageIO. OpenImageIO supports
+said parameters via setting KTX-specific attributes that share similar parameter
+names to those used by libktx.
+
+KTX is comparable, to some degree, with DDS formats when storing ready-to-upload
+BC1-BC7 GPU block-compressed texture data or plain uncompressed pixels. GPU
+block-compressed formats stay compressed in video memory and are decoded by
+the GPU on the fly while sampling. This has the drawback that each GPU format
+only works on hardware that supports it. KTX has additional support for latent
+codecs that can be transcoded on the fly to the most compatible and supported
+GPU format. KTX also has the additional benefit of supporting ASTC
+block-compressed formats which offer a great deal of flexibility (i.e., multiple
+configurations that affect quality metric vs. bitrate).
+
+KTX plugin support is mainly provided through the official libktx library.
+OpenImageIO does not do any encoding/deconding by its own and simply forwards
+all operations to libktx. libktx requires, however, a significant amount of
+parameters to configure the different formats/codecs KTX container supports.
+Consequently, the libktx attributes exposed by OpenImageIO follow the parameters
+of libktx v5.0.0. In case of any mismatch, users are encouraged to refer to the
+original parameters in libktx.
+
+Although this KTX plugin provides support for both input and output, users are
+discouraged from reading KTX inputs and re-writing them as that might
+significantly worsen the quality even if no changes were introduced. This does
+not hold true in case the input KTX container uses a lossless compression
+format but that is rarely the case with KTX files.
+
+It is important to note that KTX specification does not force the mention of
+which parameters were used to, say, generate a UASTC format. This plugin relies
+on the heuristic that KTX files are most likely created via the official KTX
+tools CLI which saves the non-default parameters in the metadata entry
+`KTXScWriterParams`. That being said, as stated above, knowledge of the
+parameters is only needed to regenerate a given KTX input which is not the
+intended use-case of this plugin within OpenImageIO.
+
+**Configuration settings for KTX input**
+
+When opening a KTX ImageInput with a *configuration* (see
+Section :ref:`sec-input-with-config`), the following special configuration
+attributes are supported:
+
+.. list-table::
+ :widths: 30 10 65
+ :header-rows: 1
+
+ * - Input Configuration Attribute
+ - Type
+ - Meaning
+ * - ``oiio:ioproxy``
+ - ptr
+ - Pointer to a ``Filesystem::IOProxy`` that will handle the I/O, for
+ example by reading from memory rather than the file system.
+
+**Configuration settings for KTX output**
+
+OpenImageIO's KTX2 plugin exposes a high number of parameters that are forwarded
+to libktx to configure the different compression formats/codecs. When opening a
+KTX2 ImageOutput, the following special configuration attributes are exposed:
+
+.. list-table::
+ :widths: 30 10 65
+ :header-rows: 1
+
+ * - ImageSpec Attribute
+ - Type
+ - Description
+ * - ``compression``
+ - string
+ - Optional GPU-block compression type. Can be one of the following values:
+ NONE ASTC
+ Defaults to NONE.
+ * - ``textureformat``
+ - string
+ - Set correctly to one of ``"Plain Texture"``, ``"Volume Texture"``, or
+ ``"CubeFace Environment"``. This attribute by itself does not convey
+ whether the texture is an array texture.
+ * - ``ktx:supercompressionscheme``
+ - string
+ - Supercompression scheme to apply. Can be one of the following values:
+ NONE ZSTD ZLIB
+ Defaults to NONE. BasisLZ does not have to be explicitly set because it
+ is set by default when ETC1S codec is used.
+ * - ``ktx:codec``
+ - string
+ - Basis Universal codec to apply. Can be one of the following values:
+ NONE UASTC UASTC-LDR UASTC-LDR-4x4 ETC1S UASTC-HDR UASTC-HDR-4x4
+ UASTC-HDR-6x6
+ Defaults to NONE.
+ * - ``ktx:version``
+ - string
+ - KTX specification version. Currently always set to ``"2.0"``.
+ * - ``ktx:nlayers``
+ - int
+ - Number of array entries for texture arrays. Has to be >= ``1`` for all
+ kinds of textures.
+ Defaults to ``1``.
+ * - ``ktx:nfaces``
+ - int
+ - Number of cubemap faces. Has to be >= ``1`` for all kinds of textures.
+ Defaults to ``1``.
+ * - ``ktx:miplevels``
+ - int
+ - Number of MIP levels. Has to be >= ``1`` for all kinds of textures.
+ Defaults to ``1``.
+ * - ``ktx:generatemipmaps``
+ - int (bool)
+ - Whether to generate mipmaps when texture is uploaded to 3D graphics GPU
+ API. This is not to be confused with runtime mipmap generation which is
+ expected to be done by the user via OpenImageIO.
+ Defaults to false.
+ * - ``ktx:normalMap``
+ - int (bool)
+ - Only valid for linear textures with two or more components. If the input
+ texture has three or four linear components it is assumed to be a three
+ component linear normal map storing unit length normals as
+ (R=X, G=Y, B=Z). A fourth component will be ignored. The map will be
+ converted to a two component X+Y normal map stored as (RGB=X, A=Y) prior
+ to encoding.
+ Defaults to false.
+ * - ``ktx:noSSE``
+ - int (bool)
+ - Forbid use of the SSE instruction set. Ignored if CPU does not support
+ SSE. SSE can only be disabled for the basis-lz and uastc encoders.
+ Ignored for other encoders.
+ Defaults to false.
+ * - ``ktx:preSwizzle``
+ - int (bool)
+ - If the texture has `KTXswizzle` metadata, apply it before compressing.
+ Swizzling, like `rabb` may yield drastically different error metrics if
+ done after supercompression. Usable for both ETC1S and UASTC.
+ Defaults to false.
+ * - ``ktx:inputSwizzle``
+ - char[4]
+ - A swizzle to apply before encoding. It must match the regular expression
+ `/^[rgba01]{4}$/`. Should not be specified if ``ktx:preSwizzle` is set.
+ Defaults to no swizzle (i.e., zero'ed array).
+ * - ``ktx:uastcFlags``
+ - uint
+ - A set of ``ktx_pack_uastc_flag_bits`` controlling UASTC encoding.
+ Defaults to 0 (maps to KTX_PACK_UASTC_LEVEL_FASTEST).
+ * - ``ktx:uastcHDRLevel``
+ - uint
+ - UASTC HDR 4x4: Sets the UASTC HDR 4x4 compressor's level. Valid range is
+ [0,4] - higher=slower but higher quality.
+ Defaults to 1.
+ * - ``ktx:uastcRDO``
+ - int (bool)
+ - Whether to enable Rate Distortion Optimization (RDO) post-processing.
+ Defaults to false.
+ * - ``ktx:uastcRDOQualityScalar``
+ - float
+ - UASTC 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 try is [.2,4].
+ Full range is [.001,50.0].
+ Defaults to 1.0.
+ * - ``ktx:uastcRDODictSize``
+ - uint
+ - UASTC RDO dictionary size in bytes. Lower values=faster, but give less
+ compression. Range is [64,65536].
+ Defaults to 4096.
+ * - ``ktx:uastcRDOMaxSmoothBlockErrorScale``
+ - float
+ - UASTC RDO max smooth block error scale. Range is [1,300]. 1.0 is
+ disabled. Larger values suppress more artifacts (and allocate more bits)
+ on smooth blocks.
+ Defaults to 10.0
+ * - ``ktx:uastcRDOMaxSmoothBlockStdDev``
+ - float
+ - UASTC RDO max smooth block standard deviation. Range is [.01,65536.0].
+ Larger values expand the range of blocks considered smooth.
+ Defaults to 18.0.
+ * - ``ktx:uastcRDODontFavorSimplerModes``
+ - int (bool)
+ - Do not favor simpler UASTC modes in RDO mode.
+ Defaults to false.
+ * - ``ktx:uastcRDONoMultithreading``
+ - int (bool)
+ - Disable RDO multithreading (slightly higher compression, deterministic).
+ Defaults to false.
+ * - ``ktx:uastcHDRQuality``
+ - uint
+ - UASTC HDR 4x4: Sets the UASTC HDR 4x4 compressor's level. Valid range is
+ [0,4] - higher=slower but higher quality. Level 0=fastest/lowest quality,
+ 3=highest practical setting, 4=exhaustive.
+ Defaults to 1.
+ * - ``ktx:uastcHDRUberMode``
+ - int (bool)
+ - 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.
+ Defaults to false.
+ * - ``ktx:uastcHDRUltraQuant``
+ - int (bool)
+ - UASTC HDR 4x4: Try to find better quantized CEM 7/11 endpoint values
+ (slower).
+ Defaults to false.
+ * - ``ktx:uastcHDRFavorAstc``
+ - int (bool)
+ - 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.
+ Defaults to false.
+ * - ``ktx:uastcHDRLambda``
+ - float
+ - 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.
+ Defaults to 0.
+ * - ``ktx:uastcHDRLevel``
+ - uint
+ - UASTC HDR 6x6i specific option: Controls the 6x6 HDR intermediate mode
+ encoder performance vs. max quality tradeoff. X may range from [0,12].
+ Defaults to 2.
+ * - ``ktx:etc1sCompressionLevel``
+ - uint
+ - ETC1S compression effort level. Range is [0,6]. Higher values are much
+ slower, but give slightly higher quality. Higher levels are intended
+ for video. Note this is NOT the same as the ETC1S quality level, and most
+ users shouldn't change this.
+ Defaults to 2.
+ * - ``ktx:etc1sQualityLevel``
+ - uint
+ - Compression quality. Range is [1,255]. Lower gives better
+ compression/lower quality/faster. Higher gives less compression
+ /higher quality/slower. This automatically determines values for
+ ``"ktx:etc1sMaxEndpoints"``, ``"ktx:etc1sMaxSelectors"``,
+ ``"ktx:etc1sEndpointRDOThreshold"`` and
+ ``"ktx:etc1sSelectorRDOThreshold"`` for the target quality level. Setting
+ these parameters overrides the values determined by
+ ``"ktx:etc1sQualityLevel"``.
+ Defaults to 128 if neither ``"ktx:etc1sMaxEndpoints"`` nor
+ ``"ktx:etc1sMaxSelectors"`` have been set.
+ * - ``ktx:etc1sMaxEndpoints``
+ - uint
+ - Manually set the max number of color endpoint clusters. Range is
+ [1,16128]. If this is set, ``ktx:etc1sMaxSelectors`` must also be set,
+ otherwise the value will be ignored.
+ Defaults to 0.
+ * - ``ktx:etc1sEndpointRDOThreshold``
+ - float
+ - Set endpoint RDO quality threshold. Lower is higher
+ quality but less quality per output bit (try [1.0,3.0]. This will
+ override the value chosen by @c qualityLevel.
+ Defaults to 1.25.
+ * - ``ktx:etc1sMaxSelectors``
+ - uint
+ - Manually set the max number of color selector clusters. Range is
+ [1,16128]. If this is set, ``ktx:etc1sMaxEndpoints`` must also be set,
+ otherwise the value will be ignored.
+ Defaults to 0.
+ * - ``ktx:etc1sSelectorRDOThreshold``
+ - float
+ - Set selector RDO quality threshold. Lower is higher quality but less
+ quality per output bit (try [1.0,3.0]). This will override the value
+ chosen by ``ktx:etc1sQualityLevel``.
+ Defaults to 1.5.
+ * - ``ktx:etc1sNoEndpointRDO``
+ - int (bool)
+ - Disable endpoint rate distortion optimizations. Slightly faster, less
+ noisy output, but lower quality per output bit.
+ Defaults to false.
+ * - ``ktx:etc1sNoSelectorRDO``
+ - int (bool)
+ - Disable selector rate distortion optimizations. Slightly faster, less
+ noisy output, but lower quality per output bit.
+ Defaults to false.
+
.. _sec-bundledplugins-ffmpeg:
Movie formats (using ffmpeg)
diff --git a/src/iv/imageviewer.cpp b/src/iv/imageviewer.cpp
index 0462db8220..8cb2977f6f 100644
--- a/src/iv/imageviewer.cpp
+++ b/src/iv/imageviewer.cpp
@@ -79,6 +79,7 @@ static const char *s_file_filters = ""
"JPEG (*.jpg *.jpe *.jpeg *.jif *.jfif *.jfi);;"
"JPEG-2000 (*.jp2 *.j2k);;"
"JPEG XL (*.jxl);;"
+ "Khronos Texture (*.ktx2);;"
"OpenEXR (*.exr);;"
"OpenVDB (*.vdb);;"
"PhotoShop (*.psd);;"
diff --git a/src/ktx.imageio/CMakeLists.txt b/src/ktx.imageio/CMakeLists.txt
new file mode 100644
index 0000000000..18b49079af
--- /dev/null
+++ b/src/ktx.imageio/CMakeLists.txt
@@ -0,0 +1,22 @@
+# Copyright Contributors to the OpenImageIO project.
+# SPDX-License-Identifier: Apache-2.0
+# https://github.com/AcademySoftwareFoundation/OpenImageIO
+
+if (Ktx_FOUND)
+ # Get MAJOR.MINOR.PATCH from set Ktx version
+ string(REPLACE "." ";" version_list "${Ktx_VERSION}")
+ list(GET version_list 0 Ktx_VERSION_MAJOR)
+ list(GET version_list 1 Ktx_VERSION_MINOR)
+ list(GET version_list 2 Ktx_VERSION_PATCH)
+
+ add_oiio_plugin (ktxinput.cpp ktxoutput.cpp
+ LINK_LIBRARIES KTX::ktx
+ DEFINITIONS
+ "-DKtx_VERSION_MAJOR=${Ktx_VERSION_MAJOR}"
+ "-DKtx_VERSION_MINOR=${Ktx_VERSION_MINOR}"
+ "-DKtx_VERSION_PATCH=${Ktx_VERSION_PATCH}"
+ )
+else ()
+ message (WARNING "KTX plugin will not be built, no libktx")
+ set (format_plugin_definitions ${format_plugin_definitions} DISABLE_KTX=1 PARENT_SCOPE)
+endif ()
diff --git a/src/ktx.imageio/README.md b/src/ktx.imageio/README.md
new file mode 100644
index 0000000000..b3f5266f5c
--- /dev/null
+++ b/src/ktx.imageio/README.md
@@ -0,0 +1,311 @@
+# About
+
+This KTX plugin support obviously nullifies the benefits of using KTX in the
+first place (i.e., to reduce upload time to GPUs or totally eliminate the need
+for transcoding to GPU-conformant formats before uploading). This plugin is,
+however, still useful so that end users don't have to convert back and forth
+between KTX <-> supported format (e.g., PNG). It is also useful to convert to
+and from KTX2 format.
+
+An important note about DDS -> KTX conversion:
+
+ - [KTX-Software][libktx] will provide tools for lossless DDS to KTX conversion
+ without having to decode then encode to KTX format. A PR is currently being
+ worked on.
+ - If you use OIIO for this conversion, then the quality will almost certainly
+ degrade (well, if the input KTX2 file is compressed using a lossy method,
+ which is almost always the case with KTX files).
+
+An example use-case would be Blender and its glTf import/export plugin.
+
+Ideally, at some point in the future, OIIO may introduce a new API to
+accommodate texture formats that are mainly used for fast texture uploads to
+GPUs. This is outside the scope of this basic format support addition.
+
+Below, you will find a set of notes on why this plugin is implemented the way
+it is. It took some time to understand how libktx works and what it provides
+(and why). Some terminology is also defined here (to avoid confusion, since KTX
+may use multiple layers of compression).
+
+## KTX2 - Brief Introduction
+
+KTX2 (`2` to distinguish it from deprecated KTX/KTX1) is a lightweight container
+format for very fast loading of textures to the GPU.
+
+KTX2 is mainly centered around the *intermediate* GPU codecs provided by Basis
+Universal. Contrary to the *usual* GPU-block-compressed textures such as
+BC1-BC7, ASTC, or ETC, the codecs introduced by Basis Universal provide
+additional flexibility, universal support (with fallback), and
+highly-configurable parameters for storage-size/compression/c. These codecs come
+at the additional cost, relative to native GPU formats, of very fast transcoding
+to the most compatible supported GPU format for the current GPU. To understand
+these better, refer to Basis Universal's [Wiki][basisu-wiki].
+
+KTX2 may also simply contain GPU-native formats (e.g., block-compressed format
+BC7) with an optional additional layer of compression (hereafter referred to as
+*supercompression*), usually ZStandard or ZLIB. These offer less flexibility
+relative to UASTC or ETC1S (Basis Universal's main codecs) but can be directly
+loaded to the GPU (no transcoding step if we don't count decompressing the
+supercompression).
+
+As per the specs, KTX2 formats may store downsampled texture data for each MIP
+level (not necessarily the whole pyramid). This introduces problems for the
+KTX2 writer (at `ktxoutput.cpp`) because the spec doesn't force the mention of
+which filter/downsampler was used to create the mipmaps. That being said,
+this limitation only occurs when trying to regenerate a KTX2 file which is not
+the intended use-case for this plugin in OIIO (nor is the intended use of KTX
+format, each read-write cycle of a KTX2 file significantly worsens its texture
+quality).
+
+## KTX vs. DDS
+
+KTX is comparable, to some degree, with DDS formats when storing ready-to-upload
+BC1-BC7 GPU block-compressed texture data or plain uncompressed pixels. GPU
+block-compressed formats stay compressed in video memory and are decoded by
+the GPU on the fly while sampling. This has the drawback that each GPU format
+only works on hardware that supports it. KTX has additional support for latent
+codecs that can be transcoded on the fly to the most compatible and supported
+GPU format.
+
+## Basis Universal Codecs
+
+### UASTC
+
+
+
+### ETC1S
+
+From [basis-ETC1S][Basis Universal's ETC1S wiki]:
+
+> ETC1S is a simplified subset of the Khronos ETC1 GPU texture format, which is
+very popular on Android. ... ETC1S is still 100% standard ETC1, so
+transcoding to ETC1 or the color block of ETC2 is a no-op. We chose ETC1S
+because it has the very valuable property that it can be quickly transcoded to
+almost any other GPU texture format at very high quality using only simple
+per-block operations with small 1D lookup tables. ...
+
+## GPU Block Compression Formats
+
+As opposed to compressed images, the term *compressed textures* usually refers
+to GPU block-compressed textures. Compressed textures have the following
+requirements:
+
+ - Random access (to some degree, you still pay the price for decoding a very
+ small number of neighboring pixels to access a given pixel).
+ - Fixed-rate encoding (consequent requirement for random access)
+ - Support for hardware-decoding on the GPU (i.e., extremely fast to decode
+ and results in better performance due to lower cache usage).
+
+### BCn
+
+All BCn formats encode a 4x4 block of pixels (could be 1 channel, or 2, or 3, or
+4 depending on the particular format) into a fixed-size data (i.e., no
+variable-rate encoding). BC7 is the go-to format on desktop hardware for LDR
+textures. BC6HU/BC6HS is the go-to format on desktop hardware for HDR formats.
+
+Microsoft has some fairly well-written explanation of each format. From the
+perspective of OIIO, we just don't care since the work to decode/encode these
+formats is offloaded to libktx.
+
+### ASTC
+
+libktx provides ASTC encoders/decoders and we don't have to deal with ASTC's
+extreme complexity (e.g., there are many different block sizes with many
+different bitrates).
+
+### ETC2
+
+libktx provides ETC2 encoders/decoders (have to double verify) but the
+dependency has some weird licensing (afraid non-permissive as Mark @KTX-Software
+pointed out). ETC formats are therefore not supported.
+
+## KTX Supercompression
+
+**supercompression**: a compression on top of another compression (i.e., layered
+compression) for better disk storage/network transmission. Unlike GPU block
+compression, supercompression has the flexibility to employ variable-rate
+encoding. In this context, supercompression is employed on top of fixed-rate,
+endpoint-compressed formats (like BCn, ASTC, etc.) that have hardware-decoding
+support in commodity GPUs (of course, depends on GPU - mobile vs desktop, etc.).
+
+Depending on the used Basis Universal codec (if any), supercompression may be
+applied. **For ETC1S, supercompression must be used (usually BasisLZ)**. This is
+the reason why you constantly see the notation "BasisLZ/ETC1S" which
+*probably (have to verify)* reads: *BasisLZ over ETC1S*.
+
+For UASTC, we *may* apply Zstandard supercompression (i.e., `KTX_SS_ZSTD`).
+
+### KTS\_SS\_BASIS\_LZ (BasisLZ)
+
+This is intended to be used to super-compress Basis Universal ETC1S format.
+The expected workflow is as follows:
+
+```
+Basis LZ -> transcode to GPU format (e.g., block-compressed BC7)
+```
+
+For OIIO use-case, we can directly use libktx to transcode into raw bytes:
+
+```
+Basis LZ → transcode (using ktxTexture2_TranscodeBasis) → raw RGBA
+```
+
+The `ktxTexture2_TranscodeBasis` function provided by libktx can transcode
+directly into raw RGBA values which is very handy. It however doesn't
+provide/expose the functionality to just decode a single miplevel/subimage
+(maybe this is simply not doable with Basis LZ - have to verify). Either way,
+I might open a PR to provide single image/texture decoders.
+
+## Supported Encoders/Decoders
+
+- Supported/Tested texture kinds:
+ - [ ] `SINGLE_TEXTURE_1D` (TODO)
+ - [X] `SINGLE_TEXTURE_2D`
+ - [ ] `SINGLE_TEXTURE_3D` (TODO)
+ - [ ] `CUBEMAP_TEXTURE` (TODO)
+ - [ ] `ARRAY_TEXTURE_1D` (TODO)
+ - [ ] `ARRAY_TEXTURE_2D` (TODO)
+ - [ ] `ARRAY_TEXTURE_3D` (not planned)
+ - [ ] `ARRAY_TEXTURE_CUBEMAP` (not planned)
+
+- Supported/Tested raw VkFormats (decoder + encoder):
+ - [X] `VK_FORMAT_R8_UNORM`
+ - [X] `VK_FORMAT_R8G8_SRGB`
+ - [X] `VK_FORMAT_R8G8B8_SRGB`
+ - [X] `VK_FORMAT_R8G8B8A8_SRGB`
+
+- Block-compressed formats (decoder + encoder):
+ - [X] ASTC
+ - [ ] BCn (waiting on libktx BCn support PR merge)
+
+- Basis Universal schemes (encoder + decoder):
+ - [X] `UASTC`
+ - [X] `ETC1S`
+
+- Supercompression schemes (decompressor + compressor):
+ - [X] `ZLIB`
+ - [X] `ZSTD`
+
+## Limitations
+
+- If original KTX2 format contained generated mip maps, there is simply no way
+to know which filter and its parameters that were used to regenerate these
+mipmaps. To avoid any issues, we simply early quit (return false) in `open()`
+if `get_int_attribute("ktx:miplevels") > 1`.
+
+- KTX2 supports many GPU-block-compression encoders and each one may have many
+different parameters that change the encoding quality (as usual, quality-speed
+trade-off). To regenerate same input KTX2 format, we rely on the heuristic that
+whatever created the original KTX2 input also supplied `KTXwriterScParams`
+metadata field which should provide all non-default arguments provided to
+`ktx create/encode` to create the texture.
+
+- As stated in the comments in `ktxinput.cpp`, if given ktx texture is
+supercompressed then it has to be all decompressed (i.e., NOT the decompression
+of the underlying GPU texture format but rather just the supercompression). This
+means that if you just need a particular subimage/miplevel, you pay the memory
+price of loading the whole KTX texture (which might be very large for 3D
+textures and texture arrays).
+
+ - Per the specs:
+ > Discussion: Should each mip level be supercompressed independently or should
+ > the scheme, zlib, zstd, etc., be applied to all levels as a unit? The latter
+ > may result in slightly smaller size though that is unclear. However it would
+ > also mean levels could not be streamed or randomly accessed.
+ >
+ > Resolved: Yes. The benefits of streaming and random access outweigh what is
+ > expected to be a small increase in size.
+
+- KTX2 writer writes the whole texture (i.e., all subimages/mipmaps) in the
+`close()` function (i.e., when the ImageOutput object is destroyed or requested
+to close). libktx does not provide a way to append or write subimages (is this
+problematic or contrary to the way OIIO expects us to write files?).
+
+- KTX1 format is not yet supported. Adding support for it after finishing KTX2
+*should be* relatively straightforward (Note: KTX1 is officially deprecated and
+KTX-Software provides tools to convert from KTX1 to KTX2). => support is not
+planned for the moment.
+
+- Only LDR formats (to be more precise, only TypeDesc::UINT8). Adding support
+for HDR is straightforward (conversions for large number of enum values from
+VkFormat have to be written).
+
+## Dependencies
+
+We only depend on libktx and nothing else. If CPU decoding/encoding of a format
+is not supported by libktx, open a PR there that adds support to it. I tried the
+approach of implementing formats here (e.g., BCn) and this results in extremely
+harder to maintain and much more complex code here (see first commit with
+12 000 changed lines).
+
+[libktx][libktx]: for general KTX@ format support (loading of KTX2 files,
+transcoding support, supercompression decompression support, etc.).
+
+ - Commit hash: see `OpenImageIO/src/cmake/build_Ktx.cmake`
+ - License: Many subresources.
+
+# Building OIIO
+
+To build (with required dependencies and fetch tests):
+
+ 1. add REQUIRED to force local build:
+
+ ```cmake
+ checked_find_package (Ktx REQUIRED VERSION_MIN 5.0.0 BUILD_LOCAL missing)
+ ```
+
+ or if you have a HEAD:main Ktx locally installed, change the above to:
+
+ ```cmake
+ checked_find_package (Ktx REQUIRED VERSION_MIN 0.0.0)
+ ```
+
+ 1. pystring dependency for OpenColor dependency will probably fail (just
+ change the tag commit hash and add required as such:
+ `checked_find_package(pystring REQUIRED VERSION_MIN 1.1.4)`)
+
+ 1. and force libjpeg-Turbo local build by adding `REQUIRED`:
+
+ ```cmake
+ checked_find_package (libjpeg-turbo REQUIRED
+ VERSION_MIN 2.1
+ DEFINITIONS USE_JPEG_TURBO=1)
+ ```
+
+ 1. build using CMake
+
+ ```bash
+ cmake -S . -B build/ -DCMAKE_BUILD_TYPE=DEBUG -DOIIO_DOWNLOAD_MISSING_TESTDATA=ON -DOpenImageIO_BUILD_MISSING_DEPS=required
+ ```
+
+ 1. build will probably because of 'pystring not found' error, so run it again.
+
+To test (of course, make sure image assets are downloaded - see the CMake flag above):
+
+ 1. Invoke ctest (just see how top-level Makefile invokes it)
+ ```bash
+ cd build/
+ ctest -V --force-new-ctest-process --output-on-failure
+ ```
+
+
+## Misc Notes for Developers
+
+Use `clang-format-17` for formatting and not whatever version you have on your
+machine (in my case, clang-format v18 caused clang-format CI to fail):
+
+```bash
+find . -regex '.*\.\(cpp\|hpp\|cc\|cxx\)' -exec clang-format-17 -style=file -i {} \;
+```
+
+## Resources
+
+- [KTX2 Specs](https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html)
+- [Official Implementation (KTX-Software)](https://github.com/KhronosGroup/KTX-Software)
+- [Basis Universal Supercompression Implementation (used by libktx)](https://github.com/BinomialLLC/basis_universal)
+- [Comparing-BCn-texture-decoders](https://aras-p.info/blog/2022/06/23/Comparing-BCn-texture-decoders/)
+- [basisu-wiki](https://github.com/BinomialLLC/basis_universal/wiki)
+- [gpu-textures-are-infrastructure](https://github.com/BinomialLLC/basis_universal/wiki#conceptual-architecture-gpu-textures-are-infrastructure)
+- [basis-ETC1S](https://github.com/BinomialLLC/basis_universal/wiki/.basis-File-Format-and-ETC1S-Texture-Video-Specification)
+
+[libktx]: https://github.com/KhronosGroup/KTX-Software.git
diff --git a/src/ktx.imageio/ktx_pvt.h b/src/ktx.imageio/ktx_pvt.h
new file mode 100644
index 0000000000..359064b1b8
--- /dev/null
+++ b/src/ktx.imageio/ktx_pvt.h
@@ -0,0 +1,594 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define Ktx_VERSION \
+ OIIO_MAKE_VERSION(Ktx_VERSION_MAJOR, Ktx_VERSION_MINOR, Ktx_VERSION_PATCH)
+
+OIIO_PLUGIN_NAMESPACE_BEGIN
+
+#define DBG if (0)
+
+enum class BlockCompression : uint8_t {
+ NONE = 0u,
+ BC1 = 1u, ///< aka DXT1
+ BC1A = 2u,
+ BC2 = 3u, ///< aka DXT3
+ BC3 = 4u, ///< aka DXT5
+ BC4 = 5u,
+ BC5 = 6u,
+ BC6HU = 7u,
+ BC6HS = 8u,
+ BC7 = 9u,
+ ETC1 = 10u,
+ ETC2_RGB = 11u,
+ ETC2_RGB_A1 = 12u,
+ ETC2_RGBA = 13u,
+ EAC_R11 = 14u,
+ EAC_RG11 = 15u,
+ ASTC = 16u,
+};
+
+
+
+static inline std::string
+block_compression_name(BlockCompression cmp)
+{
+ switch (cmp) {
+ case BlockCompression::NONE: return "NONE";
+ case BlockCompression::BC1: return "BC1";
+ case BlockCompression::BC1A: return "BC1A";
+ case BlockCompression::BC2: return "BC2";
+ case BlockCompression::BC3: return "BC3";
+ case BlockCompression::BC4: return "BC4";
+ case BlockCompression::BC5: return "BC5";
+ case BlockCompression::BC6HU: return "BC6HU";
+ case BlockCompression::BC6HS: return "BC6HS";
+ case BlockCompression::BC7: return "BC7";
+ case BlockCompression::ETC1: return "ETC1";
+ case BlockCompression::ETC2_RGB: return "ETC2_RGB";
+ case BlockCompression::ETC2_RGB_A1: return "ETC2_RGB_A1";
+ case BlockCompression::ETC2_RGBA: return "ETC2_RGBA";
+ case BlockCompression::EAC_R11: return "EAC_R11";
+ case BlockCompression::EAC_RG11: return "EAC_RG11";
+ case BlockCompression::ASTC: return "ASTC";
+ default: break;
+ }
+ return std::to_string(static_cast(cmp));
+}
+
+
+
+struct KTXglFormat {
+ uint32_t glInternalformat;
+ uint32_t glFormat;
+ uint32_t glType;
+};
+
+
+
+///
+/// Copied from KTX-Software/lib/src/vkformat_enum.h
+/// KTX-Software commit: b9bcfeb1df982f38b11d460cdd9878aa748ebafb
+/// Autogenerating this proved to be a bigger headache that simply copying and
+/// pasting the autogenerated enum from libktx.
+///
+enum VkFormat : uint32_t {
+ VK_FORMAT_UNDEFINED = 0,
+ VK_FORMAT_R4G4_UNORM_PACK8 = 1,
+ VK_FORMAT_R4G4B4A4_UNORM_PACK16 = 2,
+ VK_FORMAT_B4G4R4A4_UNORM_PACK16 = 3,
+ VK_FORMAT_R5G6B5_UNORM_PACK16 = 4,
+ VK_FORMAT_B5G6R5_UNORM_PACK16 = 5,
+ VK_FORMAT_R5G5B5A1_UNORM_PACK16 = 6,
+ VK_FORMAT_B5G5R5A1_UNORM_PACK16 = 7,
+ VK_FORMAT_A1R5G5B5_UNORM_PACK16 = 8,
+ VK_FORMAT_R8_UNORM = 9,
+ VK_FORMAT_R8_SNORM = 10,
+ VK_FORMAT_R8_USCALED = 11,
+ VK_FORMAT_R8_SSCALED = 12,
+ VK_FORMAT_R8_UINT = 13,
+ VK_FORMAT_R8_SINT = 14,
+ VK_FORMAT_R8_SRGB = 15,
+ VK_FORMAT_R8G8_UNORM = 16,
+ VK_FORMAT_R8G8_SNORM = 17,
+ VK_FORMAT_R8G8_USCALED = 18,
+ VK_FORMAT_R8G8_SSCALED = 19,
+ VK_FORMAT_R8G8_UINT = 20,
+ VK_FORMAT_R8G8_SINT = 21,
+ VK_FORMAT_R8G8_SRGB = 22,
+ VK_FORMAT_R8G8B8_UNORM = 23,
+ VK_FORMAT_R8G8B8_SNORM = 24,
+ VK_FORMAT_R8G8B8_USCALED = 25,
+ VK_FORMAT_R8G8B8_SSCALED = 26,
+ VK_FORMAT_R8G8B8_UINT = 27,
+ VK_FORMAT_R8G8B8_SINT = 28,
+ VK_FORMAT_R8G8B8_SRGB = 29,
+ VK_FORMAT_B8G8R8_UNORM = 30,
+ VK_FORMAT_B8G8R8_SNORM = 31,
+ VK_FORMAT_B8G8R8_USCALED = 32,
+ VK_FORMAT_B8G8R8_SSCALED = 33,
+ VK_FORMAT_B8G8R8_UINT = 34,
+ VK_FORMAT_B8G8R8_SINT = 35,
+ VK_FORMAT_B8G8R8_SRGB = 36,
+ VK_FORMAT_R8G8B8A8_UNORM = 37,
+ VK_FORMAT_R8G8B8A8_SNORM = 38,
+ VK_FORMAT_R8G8B8A8_USCALED = 39,
+ VK_FORMAT_R8G8B8A8_SSCALED = 40,
+ VK_FORMAT_R8G8B8A8_UINT = 41,
+ VK_FORMAT_R8G8B8A8_SINT = 42,
+ VK_FORMAT_R8G8B8A8_SRGB = 43,
+ VK_FORMAT_B8G8R8A8_UNORM = 44,
+ VK_FORMAT_B8G8R8A8_SNORM = 45,
+ VK_FORMAT_B8G8R8A8_USCALED = 46,
+ VK_FORMAT_B8G8R8A8_SSCALED = 47,
+ VK_FORMAT_B8G8R8A8_UINT = 48,
+ VK_FORMAT_B8G8R8A8_SINT = 49,
+ VK_FORMAT_B8G8R8A8_SRGB = 50,
+ VK_FORMAT_A8B8G8R8_UNORM_PACK32 = 51,
+ VK_FORMAT_A8B8G8R8_SNORM_PACK32 = 52,
+ VK_FORMAT_A8B8G8R8_USCALED_PACK32 = 53,
+ VK_FORMAT_A8B8G8R8_SSCALED_PACK32 = 54,
+ VK_FORMAT_A8B8G8R8_UINT_PACK32 = 55,
+ VK_FORMAT_A8B8G8R8_SINT_PACK32 = 56,
+ VK_FORMAT_A8B8G8R8_SRGB_PACK32 = 57,
+ VK_FORMAT_A2R10G10B10_UNORM_PACK32 = 58,
+ VK_FORMAT_A2R10G10B10_SNORM_PACK32 = 59,
+ VK_FORMAT_A2R10G10B10_USCALED_PACK32 = 60,
+ VK_FORMAT_A2R10G10B10_SSCALED_PACK32 = 61,
+ VK_FORMAT_A2R10G10B10_UINT_PACK32 = 62,
+ VK_FORMAT_A2R10G10B10_SINT_PACK32 = 63,
+ VK_FORMAT_A2B10G10R10_UNORM_PACK32 = 64,
+ VK_FORMAT_A2B10G10R10_SNORM_PACK32 = 65,
+ VK_FORMAT_A2B10G10R10_USCALED_PACK32 = 66,
+ VK_FORMAT_A2B10G10R10_SSCALED_PACK32 = 67,
+ VK_FORMAT_A2B10G10R10_UINT_PACK32 = 68,
+ VK_FORMAT_A2B10G10R10_SINT_PACK32 = 69,
+ VK_FORMAT_R16_UNORM = 70,
+ VK_FORMAT_R16_SNORM = 71,
+ VK_FORMAT_R16_USCALED = 72,
+ VK_FORMAT_R16_SSCALED = 73,
+ VK_FORMAT_R16_UINT = 74,
+ VK_FORMAT_R16_SINT = 75,
+ VK_FORMAT_R16_SFLOAT = 76,
+ VK_FORMAT_R16G16_UNORM = 77,
+ VK_FORMAT_R16G16_SNORM = 78,
+ VK_FORMAT_R16G16_USCALED = 79,
+ VK_FORMAT_R16G16_SSCALED = 80,
+ VK_FORMAT_R16G16_UINT = 81,
+ VK_FORMAT_R16G16_SINT = 82,
+ VK_FORMAT_R16G16_SFLOAT = 83,
+ VK_FORMAT_R16G16B16_UNORM = 84,
+ VK_FORMAT_R16G16B16_SNORM = 85,
+ VK_FORMAT_R16G16B16_USCALED = 86,
+ VK_FORMAT_R16G16B16_SSCALED = 87,
+ VK_FORMAT_R16G16B16_UINT = 88,
+ VK_FORMAT_R16G16B16_SINT = 89,
+ VK_FORMAT_R16G16B16_SFLOAT = 90,
+ VK_FORMAT_R16G16B16A16_UNORM = 91,
+ VK_FORMAT_R16G16B16A16_SNORM = 92,
+ VK_FORMAT_R16G16B16A16_USCALED = 93,
+ VK_FORMAT_R16G16B16A16_SSCALED = 94,
+ VK_FORMAT_R16G16B16A16_UINT = 95,
+ VK_FORMAT_R16G16B16A16_SINT = 96,
+ VK_FORMAT_R16G16B16A16_SFLOAT = 97,
+ VK_FORMAT_R32_UINT = 98,
+ VK_FORMAT_R32_SINT = 99,
+ VK_FORMAT_R32_SFLOAT = 100,
+ VK_FORMAT_R32G32_UINT = 101,
+ VK_FORMAT_R32G32_SINT = 102,
+ VK_FORMAT_R32G32_SFLOAT = 103,
+ VK_FORMAT_R32G32B32_UINT = 104,
+ VK_FORMAT_R32G32B32_SINT = 105,
+ VK_FORMAT_R32G32B32_SFLOAT = 106,
+ VK_FORMAT_R32G32B32A32_UINT = 107,
+ VK_FORMAT_R32G32B32A32_SINT = 108,
+ VK_FORMAT_R32G32B32A32_SFLOAT = 109,
+ VK_FORMAT_R64_UINT = 110,
+ VK_FORMAT_R64_SINT = 111,
+ VK_FORMAT_R64_SFLOAT = 112,
+ VK_FORMAT_R64G64_UINT = 113,
+ VK_FORMAT_R64G64_SINT = 114,
+ VK_FORMAT_R64G64_SFLOAT = 115,
+ VK_FORMAT_R64G64B64_UINT = 116,
+ VK_FORMAT_R64G64B64_SINT = 117,
+ VK_FORMAT_R64G64B64_SFLOAT = 118,
+ VK_FORMAT_R64G64B64A64_UINT = 119,
+ VK_FORMAT_R64G64B64A64_SINT = 120,
+ VK_FORMAT_R64G64B64A64_SFLOAT = 121,
+ VK_FORMAT_B10G11R11_UFLOAT_PACK32 = 122,
+ VK_FORMAT_E5B9G9R9_UFLOAT_PACK32 = 123,
+ VK_FORMAT_D16_UNORM = 124,
+ VK_FORMAT_X8_D24_UNORM_PACK32 = 125,
+ VK_FORMAT_D32_SFLOAT = 126,
+ VK_FORMAT_S8_UINT = 127,
+ VK_FORMAT_D16_UNORM_S8_UINT = 128,
+ VK_FORMAT_D24_UNORM_S8_UINT = 129,
+ VK_FORMAT_D32_SFLOAT_S8_UINT = 130,
+ VK_FORMAT_BC1_RGB_UNORM_BLOCK = 131,
+ VK_FORMAT_BC1_RGB_SRGB_BLOCK = 132,
+ VK_FORMAT_BC1_RGBA_UNORM_BLOCK = 133,
+ VK_FORMAT_BC1_RGBA_SRGB_BLOCK = 134,
+ VK_FORMAT_BC2_UNORM_BLOCK = 135,
+ VK_FORMAT_BC2_SRGB_BLOCK = 136,
+ VK_FORMAT_BC3_UNORM_BLOCK = 137,
+ VK_FORMAT_BC3_SRGB_BLOCK = 138,
+ VK_FORMAT_BC4_UNORM_BLOCK = 139,
+ VK_FORMAT_BC4_SNORM_BLOCK = 140,
+ VK_FORMAT_BC5_UNORM_BLOCK = 141,
+ VK_FORMAT_BC5_SNORM_BLOCK = 142,
+ VK_FORMAT_BC6H_UFLOAT_BLOCK = 143,
+ VK_FORMAT_BC6H_SFLOAT_BLOCK = 144,
+ VK_FORMAT_BC7_UNORM_BLOCK = 145,
+ VK_FORMAT_BC7_SRGB_BLOCK = 146,
+ VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK = 147,
+ VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK = 148,
+ VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK = 149,
+ VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK = 150,
+ VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK = 151,
+ VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK = 152,
+ VK_FORMAT_EAC_R11_UNORM_BLOCK = 153,
+ VK_FORMAT_EAC_R11_SNORM_BLOCK = 154,
+ VK_FORMAT_EAC_R11G11_UNORM_BLOCK = 155,
+ VK_FORMAT_EAC_R11G11_SNORM_BLOCK = 156,
+ VK_FORMAT_ASTC_4x4_UNORM_BLOCK = 157,
+ VK_FORMAT_ASTC_4x4_SRGB_BLOCK = 158,
+ VK_FORMAT_ASTC_5x4_UNORM_BLOCK = 159,
+ VK_FORMAT_ASTC_5x4_SRGB_BLOCK = 160,
+ VK_FORMAT_ASTC_5x5_UNORM_BLOCK = 161,
+ VK_FORMAT_ASTC_5x5_SRGB_BLOCK = 162,
+ VK_FORMAT_ASTC_6x5_UNORM_BLOCK = 163,
+ VK_FORMAT_ASTC_6x5_SRGB_BLOCK = 164,
+ VK_FORMAT_ASTC_6x6_UNORM_BLOCK = 165,
+ VK_FORMAT_ASTC_6x6_SRGB_BLOCK = 166,
+ VK_FORMAT_ASTC_8x5_UNORM_BLOCK = 167,
+ VK_FORMAT_ASTC_8x5_SRGB_BLOCK = 168,
+ VK_FORMAT_ASTC_8x6_UNORM_BLOCK = 169,
+ VK_FORMAT_ASTC_8x6_SRGB_BLOCK = 170,
+ VK_FORMAT_ASTC_8x8_UNORM_BLOCK = 171,
+ VK_FORMAT_ASTC_8x8_SRGB_BLOCK = 172,
+ VK_FORMAT_ASTC_10x5_UNORM_BLOCK = 173,
+ VK_FORMAT_ASTC_10x5_SRGB_BLOCK = 174,
+ VK_FORMAT_ASTC_10x6_UNORM_BLOCK = 175,
+ VK_FORMAT_ASTC_10x6_SRGB_BLOCK = 176,
+ VK_FORMAT_ASTC_10x8_UNORM_BLOCK = 177,
+ VK_FORMAT_ASTC_10x8_SRGB_BLOCK = 178,
+ VK_FORMAT_ASTC_10x10_UNORM_BLOCK = 179,
+ VK_FORMAT_ASTC_10x10_SRGB_BLOCK = 180,
+ VK_FORMAT_ASTC_12x10_UNORM_BLOCK = 181,
+ VK_FORMAT_ASTC_12x10_SRGB_BLOCK = 182,
+ VK_FORMAT_ASTC_12x12_UNORM_BLOCK = 183,
+ VK_FORMAT_ASTC_12x12_SRGB_BLOCK = 184,
+ VK_FORMAT_G8B8G8R8_422_UNORM = 1000156000,
+ VK_FORMAT_B8G8R8G8_422_UNORM = 1000156001,
+ VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM = 1000156002,
+ VK_FORMAT_G8_B8R8_2PLANE_420_UNORM = 1000156003,
+ VK_FORMAT_G8_B8_R8_3PLANE_422_UNORM = 1000156004,
+ VK_FORMAT_G8_B8R8_2PLANE_422_UNORM = 1000156005,
+ VK_FORMAT_G8_B8_R8_3PLANE_444_UNORM = 1000156006,
+ VK_FORMAT_R10X6_UNORM_PACK16 = 1000156007,
+ VK_FORMAT_R10X6G10X6_UNORM_2PACK16 = 1000156008,
+ VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16 = 1000156009,
+ VK_FORMAT_G10X6B10X6G10X6R10X6_422_UNORM_4PACK16 = 1000156010,
+ VK_FORMAT_B10X6G10X6R10X6G10X6_422_UNORM_4PACK16 = 1000156011,
+ VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_420_UNORM_3PACK16 = 1000156012,
+ VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16 = 1000156013,
+ VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_422_UNORM_3PACK16 = 1000156014,
+ VK_FORMAT_G10X6_B10X6R10X6_2PLANE_422_UNORM_3PACK16 = 1000156015,
+ VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_444_UNORM_3PACK16 = 1000156016,
+ VK_FORMAT_R12X4_UNORM_PACK16 = 1000156017,
+ VK_FORMAT_R12X4G12X4_UNORM_2PACK16 = 1000156018,
+ VK_FORMAT_R12X4G12X4B12X4A12X4_UNORM_4PACK16 = 1000156019,
+ VK_FORMAT_G12X4B12X4G12X4R12X4_422_UNORM_4PACK16 = 1000156020,
+ VK_FORMAT_B12X4G12X4R12X4G12X4_422_UNORM_4PACK16 = 1000156021,
+ VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_420_UNORM_3PACK16 = 1000156022,
+ VK_FORMAT_G12X4_B12X4R12X4_2PLANE_420_UNORM_3PACK16 = 1000156023,
+ VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_422_UNORM_3PACK16 = 1000156024,
+ VK_FORMAT_G12X4_B12X4R12X4_2PLANE_422_UNORM_3PACK16 = 1000156025,
+ VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_444_UNORM_3PACK16 = 1000156026,
+ VK_FORMAT_G16B16G16R16_422_UNORM = 1000156027,
+ VK_FORMAT_B16G16R16G16_422_UNORM = 1000156028,
+ VK_FORMAT_G16_B16_R16_3PLANE_420_UNORM = 1000156029,
+ VK_FORMAT_G16_B16R16_2PLANE_420_UNORM = 1000156030,
+ VK_FORMAT_G16_B16_R16_3PLANE_422_UNORM = 1000156031,
+ VK_FORMAT_G16_B16R16_2PLANE_422_UNORM = 1000156032,
+ VK_FORMAT_G16_B16_R16_3PLANE_444_UNORM = 1000156033,
+ VK_FORMAT_G8_B8R8_2PLANE_444_UNORM = 1000330000,
+ VK_FORMAT_G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16 = 1000330001,
+ VK_FORMAT_G12X4_B12X4R12X4_2PLANE_444_UNORM_3PACK16 = 1000330002,
+ VK_FORMAT_G16_B16R16_2PLANE_444_UNORM = 1000330003,
+ VK_FORMAT_A4R4G4B4_UNORM_PACK16 = 1000340000,
+ VK_FORMAT_A4B4G4R4_UNORM_PACK16 = 1000340001,
+ VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK = 1000066000,
+ VK_FORMAT_ASTC_5x4_SFLOAT_BLOCK = 1000066001,
+ VK_FORMAT_ASTC_5x5_SFLOAT_BLOCK = 1000066002,
+ VK_FORMAT_ASTC_6x5_SFLOAT_BLOCK = 1000066003,
+ VK_FORMAT_ASTC_6x6_SFLOAT_BLOCK = 1000066004,
+ VK_FORMAT_ASTC_8x5_SFLOAT_BLOCK = 1000066005,
+ VK_FORMAT_ASTC_8x6_SFLOAT_BLOCK = 1000066006,
+ VK_FORMAT_ASTC_8x8_SFLOAT_BLOCK = 1000066007,
+ VK_FORMAT_ASTC_10x5_SFLOAT_BLOCK = 1000066008,
+ VK_FORMAT_ASTC_10x6_SFLOAT_BLOCK = 1000066009,
+ VK_FORMAT_ASTC_10x8_SFLOAT_BLOCK = 1000066010,
+ VK_FORMAT_ASTC_10x10_SFLOAT_BLOCK = 1000066011,
+ VK_FORMAT_ASTC_12x10_SFLOAT_BLOCK = 1000066012,
+ VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK = 1000066013,
+ VK_FORMAT_PVRTC1_2BPP_UNORM_BLOCK_IMG = 1000054000,
+ VK_FORMAT_PVRTC1_4BPP_UNORM_BLOCK_IMG = 1000054001,
+ VK_FORMAT_PVRTC2_2BPP_UNORM_BLOCK_IMG = 1000054002,
+ VK_FORMAT_PVRTC2_4BPP_UNORM_BLOCK_IMG = 1000054003,
+ VK_FORMAT_PVRTC1_2BPP_SRGB_BLOCK_IMG = 1000054004,
+ VK_FORMAT_PVRTC1_4BPP_SRGB_BLOCK_IMG = 1000054005,
+ VK_FORMAT_PVRTC2_2BPP_SRGB_BLOCK_IMG = 1000054006,
+ VK_FORMAT_PVRTC2_4BPP_SRGB_BLOCK_IMG = 1000054007,
+ VK_FORMAT_ASTC_3x3x3_UNORM_BLOCK_EXT = 1000288000,
+ VK_FORMAT_ASTC_3x3x3_SRGB_BLOCK_EXT = 1000288001,
+ VK_FORMAT_ASTC_3x3x3_SFLOAT_BLOCK_EXT = 1000288002,
+ VK_FORMAT_ASTC_4x3x3_UNORM_BLOCK_EXT = 1000288003,
+ VK_FORMAT_ASTC_4x3x3_SRGB_BLOCK_EXT = 1000288004,
+ VK_FORMAT_ASTC_4x3x3_SFLOAT_BLOCK_EXT = 1000288005,
+ VK_FORMAT_ASTC_4x4x3_UNORM_BLOCK_EXT = 1000288006,
+ VK_FORMAT_ASTC_4x4x3_SRGB_BLOCK_EXT = 1000288007,
+ VK_FORMAT_ASTC_4x4x3_SFLOAT_BLOCK_EXT = 1000288008,
+ VK_FORMAT_ASTC_4x4x4_UNORM_BLOCK_EXT = 1000288009,
+ VK_FORMAT_ASTC_4x4x4_SRGB_BLOCK_EXT = 1000288010,
+ VK_FORMAT_ASTC_4x4x4_SFLOAT_BLOCK_EXT = 1000288011,
+ VK_FORMAT_ASTC_5x4x4_UNORM_BLOCK_EXT = 1000288012,
+ VK_FORMAT_ASTC_5x4x4_SRGB_BLOCK_EXT = 1000288013,
+ VK_FORMAT_ASTC_5x4x4_SFLOAT_BLOCK_EXT = 1000288014,
+ VK_FORMAT_ASTC_5x5x4_UNORM_BLOCK_EXT = 1000288015,
+ VK_FORMAT_ASTC_5x5x4_SRGB_BLOCK_EXT = 1000288016,
+ VK_FORMAT_ASTC_5x5x4_SFLOAT_BLOCK_EXT = 1000288017,
+ VK_FORMAT_ASTC_5x5x5_UNORM_BLOCK_EXT = 1000288018,
+ VK_FORMAT_ASTC_5x5x5_SRGB_BLOCK_EXT = 1000288019,
+ VK_FORMAT_ASTC_5x5x5_SFLOAT_BLOCK_EXT = 1000288020,
+ VK_FORMAT_ASTC_6x5x5_UNORM_BLOCK_EXT = 1000288021,
+ VK_FORMAT_ASTC_6x5x5_SRGB_BLOCK_EXT = 1000288022,
+ VK_FORMAT_ASTC_6x5x5_SFLOAT_BLOCK_EXT = 1000288023,
+ VK_FORMAT_ASTC_6x6x5_UNORM_BLOCK_EXT = 1000288024,
+ VK_FORMAT_ASTC_6x6x5_SRGB_BLOCK_EXT = 1000288025,
+ VK_FORMAT_ASTC_6x6x5_SFLOAT_BLOCK_EXT = 1000288026,
+ VK_FORMAT_ASTC_6x6x6_UNORM_BLOCK_EXT = 1000288027,
+ VK_FORMAT_ASTC_6x6x6_SRGB_BLOCK_EXT = 1000288028,
+ VK_FORMAT_ASTC_6x6x6_SFLOAT_BLOCK_EXT = 1000288029,
+ VK_FORMAT_R16G16_SFIXED5_NV = 1000464000,
+ VK_FORMAT_A1B5G5R5_UNORM_PACK16_KHR = 1000470000,
+ VK_FORMAT_A8_UNORM_KHR = 1000470001,
+ VK_FORMAT_MAX_ENUM = 0x7FFFFFFF
+};
+
+
+
+struct FormatInfo {
+ int nbrchannels;
+ TypeDesc typedesc;
+ BlockCompression compression { BlockCompression::NONE };
+ VkFormat uncompressed_format { VK_FORMAT_UNDEFINED };
+};
+
+
+
+///
+/// Colorspace detection should be done via the color model and transfer function
+/// and not via VkFormat. See:
+/// https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_data_format_descriptor
+///
+/// TODO: there are a lot of formats to test so currently only "widely" used
+/// formats are supported (every format included here has to have a corresponding
+/// test file with the same VkFormat).
+///
+inline bool
+get_info_from_vkformat(VkFormat vkformat, FormatInfo& formatinfo)
+{
+ // clang-format off
+ switch (vkformat) {
+ // LDR uncompressed formats
+ case VK_FORMAT_R8_UNORM: formatinfo = { 1, TypeDesc::UINT8, BlockCompression::NONE }; return true;
+ case VK_FORMAT_R8G8_UNORM: formatinfo = { 2, TypeDesc::UINT8, BlockCompression::NONE }; return true;
+ case VK_FORMAT_R8G8B8_UNORM:
+ case VK_FORMAT_R8G8B8_SRGB: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::NONE }; return true;
+ case VK_FORMAT_R8G8B8A8_UNORM:
+ case VK_FORMAT_R8G8B8A8_SRGB: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::NONE }; return true;
+
+ // HDR uncompressed formats
+ case VK_FORMAT_R16G16B16_SFLOAT: formatinfo = { 3, TypeDesc::HALF, BlockCompression::NONE }; return true;
+ case VK_FORMAT_R16G16B16A16_SFLOAT: formatinfo = { 4, TypeDesc::HALF, BlockCompression::NONE }; return true;
+
+ // ETC2 block-compressed formats
+ case VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::ETC2_RGB, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+ case VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ETC2_RGB_A1, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+ case VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ETC2_RGBA, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+
+ // BCn formats
+ case VK_FORMAT_BC1_RGB_UNORM_BLOCK: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::BC1, VK_FORMAT_R8G8B8A8_UNORM }; return true;
+ case VK_FORMAT_BC1_RGB_SRGB_BLOCK: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::BC1, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+ case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC1A, VK_FORMAT_R8G8B8A8_UNORM }; return true;
+ case VK_FORMAT_BC1_RGBA_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC1A, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+ case VK_FORMAT_BC2_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC2, VK_FORMAT_R8G8B8A8_UNORM }; return true;
+ case VK_FORMAT_BC2_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC2, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+ case VK_FORMAT_BC3_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC3, VK_FORMAT_R8G8B8A8_UNORM }; return true;
+ case VK_FORMAT_BC3_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC3, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+ case VK_FORMAT_BC4_UNORM_BLOCK: formatinfo = { 1, TypeDesc::UINT8, BlockCompression::BC4, VK_FORMAT_R8_UNORM }; return true;
+ case VK_FORMAT_BC4_SNORM_BLOCK: formatinfo = { 1, TypeDesc::UINT8, BlockCompression::BC4, VK_FORMAT_R8_SNORM }; return true;
+ case VK_FORMAT_BC5_UNORM_BLOCK: formatinfo = { 2, TypeDesc::UINT8, BlockCompression::BC5, VK_FORMAT_R8G8_UNORM }; return true;
+ case VK_FORMAT_BC5_SNORM_BLOCK: formatinfo = { 2, TypeDesc::UINT8, BlockCompression::BC5, VK_FORMAT_R8G8_SNORM }; return true;
+ case VK_FORMAT_BC6H_UFLOAT_BLOCK: formatinfo = { 3, TypeDesc::HALF, BlockCompression::BC6HU, VK_FORMAT_R16G16B16_SFLOAT }; return true;
+ case VK_FORMAT_BC6H_SFLOAT_BLOCK: formatinfo = { 3, TypeDesc::HALF, BlockCompression::BC6HS, VK_FORMAT_R16G16B16_SFLOAT }; return true;
+ case VK_FORMAT_BC7_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC7, VK_FORMAT_R8G8B8A8_UNORM }; return true;
+ case VK_FORMAT_BC7_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC7, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+
+ // LDR ASTC formats (2D blocks)
+ case VK_FORMAT_ASTC_4x4_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_5x4_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_5x5_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_6x5_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_6x6_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_8x5_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_8x6_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_8x8_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_10x5_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_10x6_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_10x8_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_10x10_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_12x10_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_12x12_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM }; return true;
+ case VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_5x4_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_5x5_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_6x5_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_6x6_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_8x5_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_8x6_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_8x8_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_10x5_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_10x6_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_10x8_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_10x10_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_12x10_SRGB_BLOCK:
+ case VK_FORMAT_ASTC_12x12_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+
+ // HDR ASTC formats (2D blocks)
+ case VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_5x4_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_5x5_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_6x5_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_6x6_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_8x5_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_8x6_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_8x8_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_10x5_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_10x6_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_10x8_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_10x10_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_12x10_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT }; return true;
+ default: break;
+ }
+ // clang-format on
+ return false;
+}
+
+
+
+inline VkFormat
+get_vkformat_from_info(int nchannels, TypeDesc format, bool srgb_colorspace)
+{
+ if (format != TypeDesc::UINT8 && format != TypeDesc::UINT16
+ && format != TypeDesc::HALF)
+ return VK_FORMAT_UNDEFINED;
+
+ const bool is_ldr = format == TypeDesc::UINT8;
+
+ switch (nchannels) {
+ case 1:
+ return is_ldr
+ ? (srgb_colorspace ? VK_FORMAT_R8_SRGB : VK_FORMAT_R8_UNORM)
+ : VK_FORMAT_R16_SFLOAT;
+ case 2:
+ return is_ldr ? (srgb_colorspace ? VK_FORMAT_R8G8_SRGB
+ : VK_FORMAT_R8G8_UNORM)
+ : VK_FORMAT_R16G16_SFLOAT;
+ case 3:
+ return is_ldr ? (srgb_colorspace ? VK_FORMAT_R8G8B8_SRGB
+ : VK_FORMAT_R8G8B8_UNORM)
+ : VK_FORMAT_R16G16B16_SFLOAT;
+ case 4:
+ return is_ldr ? (srgb_colorspace ? VK_FORMAT_R8G8B8A8_SRGB
+ : VK_FORMAT_R8G8B8A8_UNORM)
+ : VK_FORMAT_R16G16B16A16_SFLOAT;
+ default: return VK_FORMAT_UNDEFINED;
+ }
+}
+
+
+
+/// ASTC supports multiple blocks sizes (unlike BCn that is fixed to 4x4). This
+/// gets the block size (width * height) from the given VkFormat.
+inline uint32_t
+get_astc_block_size(VkFormat format)
+{
+ switch (format) {
+ case VK_FORMAT_ASTC_4x4_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_4x4_SRGB_BLOCK: return 4 * 4;
+
+ case VK_FORMAT_ASTC_5x4_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_5x4_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_5x4_SRGB_BLOCK: return 5 * 4;
+
+ case VK_FORMAT_ASTC_5x5_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_5x5_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_5x5_SRGB_BLOCK: return 5 * 5;
+
+ case VK_FORMAT_ASTC_6x5_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_6x5_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_6x5_SRGB_BLOCK: return 6 * 5;
+
+ case VK_FORMAT_ASTC_6x6_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_6x6_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_6x6_SRGB_BLOCK: return 6 * 6;
+
+ case VK_FORMAT_ASTC_8x5_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_8x5_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_8x5_SRGB_BLOCK: return 8 * 5;
+
+ case VK_FORMAT_ASTC_8x6_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_8x6_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_8x6_SRGB_BLOCK: return 8 * 6;
+
+ case VK_FORMAT_ASTC_8x8_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_8x8_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_8x8_SRGB_BLOCK: return 8 * 8;
+
+ case VK_FORMAT_ASTC_10x5_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_10x5_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_10x5_SRGB_BLOCK: return 10 * 5;
+
+ case VK_FORMAT_ASTC_10x6_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_10x6_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_10x6_SRGB_BLOCK: return 10 * 6;
+
+ case VK_FORMAT_ASTC_10x8_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_10x8_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_10x8_SRGB_BLOCK: return 10 * 8;
+
+ case VK_FORMAT_ASTC_10x10_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_10x10_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_10x10_SRGB_BLOCK: return 10 * 10;
+
+ case VK_FORMAT_ASTC_12x10_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_12x10_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_12x10_SRGB_BLOCK: return 12 * 10;
+
+ case VK_FORMAT_ASTC_12x12_UNORM_BLOCK:
+ case VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK:
+ case VK_FORMAT_ASTC_12x12_SRGB_BLOCK: return 12 * 12;
+ default: return 0;
+ }
+}
+
+
+
+inline bool
+ichar_equals(unsigned char a, unsigned char b)
+{
+ return std::tolower(a) == std::tolower(b);
+}
+
+
+
+/// Case-insensitive string equality
+inline bool
+iequals(std::string_view lhs, std::string_view rhs)
+{
+ return std::equal(lhs.cbegin(), lhs.cend(), rhs.cbegin(), rhs.cend(),
+ ichar_equals);
+}
+
+OIIO_PLUGIN_NAMESPACE_END
diff --git a/src/ktx.imageio/ktxinput.cpp b/src/ktx.imageio/ktxinput.cpp
new file mode 100644
index 0000000000..943ee3b933
--- /dev/null
+++ b/src/ktx.imageio/ktxinput.cpp
@@ -0,0 +1,996 @@
+// Copyright Contributors to the OpenImageIO project.
+// SPDX-License-Identifier: Apache-2.0
+// https://github.com/AcademySoftwareFoundation/OpenImageIO
+
+#include "ktx_pvt.h"
+#include
+#include
+
+OIIO_PLUGIN_NAMESPACE_BEGIN
+
+class KtxInput final : public ImageInput {
+public:
+ KtxInput() {}
+
+ ~KtxInput() override { close(); }
+
+ const char* format_name(void) const override { return "ktx"; }
+
+ int supports(string_view feature) const override
+ {
+ return (
+ feature == "ioproxy" ||
+ // as per the KTX1/2 specs:
+ // https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_keyvalue_data
+ feature == "arbitrary_metadata" ||
+ // KTX2 supports 2D texture arrays, 3D texture arrays, and cubemap
+ // arrays. That being said, 2D texture arrays is the only one
+ // supported by this OIIO plugin.
+ feature == "multiimage" ||
+ // KTX2 supports mipmaps. 3D texture mipmaps are treated as a
+ // per-slice mipmap.
+ feature == "mipmap");
+ }
+
+ bool valid_file(Filesystem::IOProxy* ioproxy) const override;
+
+ bool open(const std::string& name, ImageSpec& newspec) override;
+
+ bool open(const std::string& name, ImageSpec& newspec,
+ const ImageSpec& config) override;
+
+ bool read_native_scanline(int subimage, int miplevel, int y, int z,
+ void* data) override;
+
+ bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend,
+ int z, void* data) override;
+
+ // TODO: why there is no `read_native_scanlines` that takes a span
+ // but also a `z` slice index (same as unsafe `read_native_scanlines`)?
+ // bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend,
+ // span data) override;
+
+ const std::string& filename() const { return m_filename; }
+
+ bool close() override;
+
+ int current_subimage(void) const override { return m_subimage; }
+
+ int current_miplevel(void) const override { return m_miplevel; }
+
+ bool seek_subimage(int subimage, int miplevel) override;
+
+private:
+ std::string m_filename;
+
+ /// KTX2 texture.
+ std::unique_ptr m_tex {
+ nullptr, ktxTexture2_Destroy
+ };
+
+ int m_subimage { -1 }; ///< What subimage are we looking at. This is not
+ ///< used anywhere else except in current_subimage()
+
+ int m_miplevel { -1 }; ///< What mip level are we looking at. This is not
+ ///< used anywhere else except in current_miplevel()
+
+ /// GPU block compression kind (only set in case of GPU-block-compressed KTX
+ /// textures).
+ BlockCompression m_cmp { BlockCompression::NONE };
+
+ /// Original VkFormat (i.e., before applying any decompression or transcoding).
+ VkFormat m_vkformat { VK_FORMAT_UNDEFINED };
+
+ std::unique_ptr m_config; ///< Saved copy of configuration spec
+
+ /// Helper function: performs the actual pixel decoding.
+ bool internal_readimg(unsigned char* dst, int w, int h, int d);
+
+ /// Checks the magic
+ bool ktx_magic_cmp(const uint8_t* sig, size_t start) const;
+
+ std::string get_colorspace() const;
+
+ void parse_ktx_sc_params_metadata(const std::string& ktx_sc_params);
+
+ bool check(int subimage, int miplevel) const;
+};
+
+
+
+// Obligatory material to make this a recognizable imageio plugin:
+OIIO_PLUGIN_EXPORTS_BEGIN
+
+OIIO_EXPORT int ktx_imageio_version = OIIO_PLUGIN_VERSION;
+
+OIIO_EXPORT const char*
+ktx_imageio_library_version()
+{
+ return "ktx v" OIIO_STRINGIZE(Ktx_VERSION_MAJOR) "." OIIO_STRINGIZE(
+ Ktx_VERSION_MINOR) "." OIIO_STRINGIZE(Ktx_VERSION_PATCH);
+}
+OIIO_EXPORT ImageInput*
+ktx_input_imageio_create()
+{
+ return new KtxInput;
+}
+OIIO_EXPORT const char* ktx_input_extensions[] = { "ktx2", nullptr };
+
+OIIO_PLUGIN_EXPORTS_END
+
+
+
+bool
+KtxInput::open(const std::string& name, ImageSpec& newspec,
+ const ImageSpec& config)
+{
+ // Check 'config' for any special requests
+ ioproxy_retrieve_from_config(config);
+ m_config.reset(new ImageSpec(config)); // save config spec
+ return open(name, newspec);
+}
+
+
+
+bool
+KtxInput::open(const std::string& name, ImageSpec& newspec)
+{
+ m_filename = name;
+
+ if (!ioproxy_use_or_open(name))
+ return false;
+
+ // If an IOProxy was passed, it had better be a File or a MemReader
+ Filesystem::IOProxy* m_io = ioproxy();
+ std::string proxytype = m_io->proxytype();
+ if (proxytype != "file" && proxytype != "memreader") {
+ errorfmt("ktx reader can't handle proxy type {}", proxytype);
+ return false;
+ }
+
+ // check if magic to insure that this is a KTX2 file
+ if (!this->valid_file(m_io)) {
+ // close_file();
+ errorfmt("\"{}\" is not a KTX2 file, magic number doesn't match", name);
+ return false;
+ }
+
+ //
+ // KTX can hold layered compressions (i.e., on top of the potential GPU-
+ // compatible compression like ASTC, the whole data can be furthermore
+ // compressed using a super compression scheme). We call such compression
+ // `supercompression` and does NOT refer to the usual compression (e.g.,
+ // GPU-block compression).
+ //
+ // If KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT is provided for any
+ // of the ktxTexture_CreateFrom* calls, then libktx will allocate an
+ // internal buffer large enough to hold all data inflated IF AND ONLY IF
+ // supercompressionScheme == KTX_SS_ZSTD or KTX_SS_ZLIB.
+ //
+ // Whithin the same call, ALL the texture data is then loaded. This is not
+ // ideal especially when dealing with, for instance, 3D textures, or even
+ // worse, 3D array textures.
+ //
+ // TODO: per-subimage and per-miplevel ktx loading
+ // Implementing the per-subimage allocation approach requires some effort.
+ // For the moment, let's make sure this approach is working (i.e., all tests
+ // are passing).
+ //
+ // For under-the-hood details, see official libktx repo:
+ // https://github.com/KhronosGroup/KTX-Software/blob/main/lib/src/texture.c
+ //
+ if (proxytype == "file") {
+ FILE* fd = reinterpret_cast(m_io)->handle();
+ ktxTexture2* p_tex = nullptr;
+ auto res
+ = ktxTexture2_CreateFromStdioStream(fd, KTX_TEXTURE_CREATE_NO_FLAGS,
+ &p_tex);
+ m_tex.reset(p_tex);
+ if (KTX_SUCCESS != res) {
+ errorfmt("Failed to create ktx texture using "
+ "ktxTexture_CreateFromStdioStream");
+ return false;
+ }
+ } else /* (proxytype == "memreader") */ {
+ OIIO_ASSERT(proxytype == "memreader");
+ auto buff = reinterpret_cast(m_io)->buffer();
+ ktxTexture2* p_tex = nullptr;
+ auto res = ktxTexture2_CreateFromMemory(buff.data(), buff.size(),
+ KTX_TEXTURE_CREATE_NO_FLAGS,
+ &p_tex);
+ m_tex.reset(p_tex);
+ if (KTX_SUCCESS != res) {
+ errorfmt(
+ "Failed to create ktx texture using ktxTexture_CreateFromMemory");
+ return false;
+ }
+ }
+
+ if (m_tex->isArray && m_tex->numDimensions == 3) {
+ errorfmt("3D texture arrays are not supported");
+ return false;
+ }
+
+ if (m_tex->isArray && m_tex->numFaces > 1) {
+ errorfmt("Cubemap texture arrays are not supported");
+ return false;
+ }
+
+ const bool is_hdr = ktxTexture2_IsHDR(m_tex.get());
+ DBG std::cout << "is_hdr: " << is_hdr << '\n';
+
+ m_spec = ImageSpec(m_tex->baseWidth, m_tex->baseHeight,
+ 4 /* dummy value - will be overwritten */,
+ TypeDesc::UINT8);
+ m_spec.depth = m_spec.full_depth = m_tex->baseDepth;
+ std::string colorspace = get_colorspace();
+ if (iequals(colorspace, "unknown"))
+ return false;
+ m_spec.set_colorspace(colorspace);
+
+ // Set textureformat attribute
+ if (m_tex->numDimensions == 2 || m_tex->numDimensions == 1) {
+ if (m_tex->numFaces > 1)
+ m_spec.attribute("textureformat", "CubeFace Environment");
+ else
+ m_spec.attribute("textureformat", "Plain Texture");
+ } else if (m_tex->numDimensions == 3) {
+ m_spec.attribute("textureformat", "Volume Texture");
+ } else {
+ m_spec.attribute("textureformat", "unknown");
+ }
+
+ //
+ // Make sure to save everything that is needed to recreate this exact same
+ // KTX texture from OIIO API (i.e., fields of `ktxTextureCreateInfo`).
+ //
+ // KtxTexture fields may change after some libktx calls that take
+ // KtxTexture* argument because they may potentially modify the texture
+ // (e.g., in ktxTexture2_DecodeAstc supercompressionScheme is overwritten to
+ // none, ktxTexture2_TranscodeBasis overwrites texture format, etc.). So,
+ // store these now and NOT after libktx calls (e.g.,
+ // ktxTexture2_TranscodeBasis).
+ //
+ m_spec.extra_attribs.attribute("ktx:supercompressionscheme",
+ (uint32_t)m_tex->supercompressionScheme);
+ // save as string (for future use, in case KTX1 is added)
+ m_spec.extra_attribs.attribute("ktx:version", "2.0");
+ // Contrary to the specs' layerCount, numLayers is always >= 1
+ if (m_tex->numLayers > 1)
+ m_spec.extra_attribs.attribute("ktx:nlayers", m_tex->numLayers);
+ if (m_tex->numFaces > 1)
+ m_spec.extra_attribs.attribute("ktx:nfaces", m_tex->numFaces);
+ if (m_tex->numLevels > 1)
+ m_spec.extra_attribs.attribute("ktx:miplevels", m_tex->numLevels);
+ if (m_tex->generateMipmaps)
+ m_spec.extra_attribs.attribute("ktx:generatemipmaps", 1);
+
+ //
+ // Save arbitrary metadata. KTX allows for the storage of arbitrary
+ // key/value metadata pairs as per the specification here:
+ // https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_keyvalue_data
+ //
+ // KTX2 spec. defines a predifined set of key/value metadata at
+ // https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_predefined_keyvalue_pairs
+ //
+ // Predifined keys we care about:
+ //
+ // - KTXwriterScParams: null-terminated string
+ // - KTXcubemapIncomplete: 1 byte bitfield
+ // - KTXorientation: null-terminated string
+ //
+ // We don't care about the rest and we don't save them.
+ //
+ auto kventry = m_tex->kvDataHead;
+ if (kventry)
+ do {
+ auto status = KTX_SUCCESS;
+ unsigned int keylen { 0 };
+ unsigned int vallen { 0 };
+ char* key { nullptr };
+ void* val { nullptr };
+
+ if ((status = ktxHashListEntry_GetKey(kventry, &keylen, &key))
+ != KTX_SUCCESS)
+ continue;
+
+ // Per KTX2 spec: "The key must be terminated by a NUL character"
+ // We exit on any invalid key that is encountered
+ if (keylen <= 1 || key[keylen - 1] != '\0') {
+ errorfmt(
+ "An empty or non-null terminated metadata key string encountered. This is an invalid KTX file");
+ return false;
+ }
+
+ if ((status = ktxHashListEntry_GetValue(kventry, &vallen, &val))
+ != KTX_SUCCESS) {
+ errorfmt("ktxHashListEntry_GetValue returned error code {}",
+ static_cast(status));
+ return false;
+ }
+
+ // vallen checks are done below depending on the attribute name
+
+ auto attr_name = std::string(key, key + (keylen - 1));
+ auto ktx_prefixed_attr_name = fmt::format("ktx:{}", attr_name);
+
+ if (attr_name == KTX_WRITER_SCPARAMS_KEY) {
+ //
+ // KTXwriterScParams is used to report all kinds of non-default
+ // parameters used by ktx tools to write this KTX2 file.
+ // This includes non default Basis Universal params (i.e., for
+ // UASTC/ETC1S), non-default supercompression params,
+ // non-default mipmap generation params, etc.
+ // Should be NUL terminated.
+ //
+ auto cval = reinterpret_cast(val);
+ if (vallen <= 1 || cval[vallen - 1] != '\0') {
+ errorfmt(
+ "An empty or non-null terminated metadata value string for KTX_WRITER_SCPARAMS_KEY. This is an invalid KTX file");
+ return false;
+ }
+ std::string_view val_str(cval, vallen);
+ m_spec.extra_attribs.attribute(ktx_prefixed_attr_name, val_str);
+ } else if (attr_name == "KTXcubemapIncomplete") {
+ OIIO_ASSERT(vallen == 1);
+ // TODO: handle KTXcubemapIncomplete
+ } else if (attr_name == KTX_ORIENTATION_KEY) {
+ // KTX may define a different orientation than the one used by OIIO. See:
+ // https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_ktxorientation
+ // TODO: set orientation functions
+ }
+
+ } while ((kventry = ktxHashList_Next(kventry)));
+
+ // We don't support vendor-specific supercompression schemes
+ if (m_tex->supercompressionScheme > KTX_SS_END_RANGE) {
+ errorfmt("Unsuppoted supercompression scheme: {}",
+ static_cast(m_tex->supercompressionScheme));
+ return false;
+ }
+
+ // Load the actual image data (libktx owns the data buffer)
+ if (auto result = ktxTexture2_LoadImageData(m_tex.get(), NULL, 0);
+ result != KTX_SUCCESS) {
+ errorfmt("ktxTexture2_LoadImageData returned Ktx error code: {}",
+ static_cast(result));
+ return false;
+ }
+
+ //
+ // Do we need to transcode this texture (i.e., is this a Basis Universal
+ // texture format)?
+ //
+ // KTX2 provides transcoders that can directly target raw, uncompressed
+ // pixels (via the KTX_TTF_RGBA32 flag).
+ //
+ // Important:
+ // This modifies the KtxTexture2 (m_tex) therefore make sure to save
+ // essential properties for proper KTX2 regeneration.
+ //
+ // Note:
+ // Do not use ktxTexture2_NeedsTranscoding as it returns false for
+ // UASTC_HDR_4x4. For all transcodable formats, we want to transcode them
+ // here to uncompressed format (RGBA32 for LDR and RGBA_HALF for HDR).
+ //
+ if (ktxTexture2_IsTranscodable(m_tex.get())) {
+ // TODO: in case of HDR, use KTX_TTF_RGBA_HALF
+ if (auto status = ktxTexture2_TranscodeBasis(
+ m_tex.get(), is_hdr ? KTX_TTF_RGBA_HALF : KTX_TTF_RGBA32, 0);
+ status != KTX_SUCCESS) {
+ errorfmt("failed to transcode KTX2 texture to raw pixels. "
+ "ktxTexture2_TranscodeBasis returned Ktx error code: {}",
+ static_cast(status));
+ return false;
+ }
+ }
+
+ //
+ // Decode GPU-compression if any (using libktx). Supported formats:
+ //
+ // ASTC: libktx provides decoders for ASTC block compression via the
+ // ktxTexture2_DecodeAstc call.
+ // This currently decodes the whole texture (all miplevels, all
+ // slices, etc.) into memory.
+ //
+ // BCn: libktx will provide decoders/encoders for BCn block compression
+ // via ktxTexture2_DecodeBCn.
+ // TODO: wait libktx BCn PR to get merged then add BCn support.
+ //
+ // ETC2: libktx provides decoders but they fall under non-open-source
+ // license. To quote KTX-Software: "The file lib/etcdec.cxx is not
+ // open source. It is made available under the terms of an Ericsson
+ // license, found in the file itself."
+ //
+ // PVRTC: not planned (there are pending PRs in libktx).
+ //
+ if (m_tex->isCompressed /* i.e., is GPU block compressed? */) {
+ // m_cmp = get_block_compression_from_format(m_tex->vkFormat);
+ FormatInfo format_info;
+ if (!get_info_from_vkformat(static_cast(m_tex->vkFormat),
+ format_info)) {
+ close();
+ errorfmt("Could not extract format info from provided "
+ "VkFormat: {}. This format is unsupported",
+ m_tex->vkFormat);
+ return false;
+ }
+ m_cmp = format_info.compression;
+ m_spec.attribute("compression", block_compression_name(m_cmp));
+ switch (m_cmp) {
+#if 0 // TODO: wait for my PR in libktx to be merged
+ /* BCn GPU formats */
+ case BlockCompression::BC1:
+ case BlockCompression::BC1A:
+ case BlockCompression::BC2:
+ case BlockCompression::BC3:
+ case BlockCompression::BC4:
+ case BlockCompression::BC5:
+ case BlockCompression::BC6HU:
+ case BlockCompression::BC6HS:
+ case BlockCompression::BC7:
+ //
+ // Note:
+ // ktxTexture2_DecodeBCn internally creates a new ktxTexture2 texture
+ // and populates it with decoded data from the originally provided
+ // texture. At the end, it moves the decoded data to m_tex and
+ // destroys the temporarily created texture.
+ //
+ // This operation is expensive (both in memory and CPU cycles).
+ // After this, m_tex->isCompressed will be false => this will only
+ // be called once.
+ //
+ if (auto status = ktxTexture2_DecodeBCn(m_tex);
+ status != KTX_SUCCESS) {
+ errorfmt("failed to decode BCn-compressed texture. "
+ "ktxTexture2_DecodeBCn returned Ktx error code: {}",
+ static_cast(status));
+ return false;
+ }
+ break;
+#endif
+
+ /* ASTC formats */
+ case BlockCompression::ASTC: {
+ const uint32_t blocksize = get_astc_block_size(
+ static_cast(m_tex->vkFormat));
+ if (blocksize == 0) {
+ errorfmt(
+ "Failed to determine block size for ASTC-compressed texture from its VkFormat {}",
+ m_tex->vkFormat);
+ return false;
+ }
+ //
+ // Note:
+ // ktxTexture2_DecodeAstc internally creates a new ktxTexture2 texture
+ // and populates it with decoded data from the originally provided
+ // texture. At the end, it moves the decoded data to m_tex and
+ // destroys the temporarily created texture.
+ //
+ // This operation is expensive (both in memory and CPU cycles).
+ // After this, m_tex->isCompressed will be false => this will only
+ // be called once.
+ //
+ if (auto status = ktxTexture2_DecodeAstc(m_tex.get());
+ status != KTX_SUCCESS) {
+ errorfmt("Failed to decode ASTC-compressed texture. "
+ "ktxTexture2_DecodeAstc returned Ktx error code: {}",
+ static_cast(status));
+ return false;
+ }
+ break;
+ }
+
+ default:
+ errorfmt("{} GPU-compressed formats are not supported",
+ block_compression_name(m_cmp));
+ return false;
+ }
+ }
+
+ OIIO_ASSERT(!m_tex->isCompressed);
+
+ //
+ // This could mean one of the following as per the specs at:
+ // https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_use_of_vk_format_undefined
+ //
+ // 1. For custom formats that do not have any equivalent in GPU APIs.
+ // => Not supported.
+ //
+ // 2. ETC1S/UASTC supercompression scheme: makes no sense since we
+ // transcoded it above to uncompressed format.
+ //
+ // 3. For any formats from any GPU APIs that do not have Vulkan
+ // equivalents. E.g., OpenGL/Direct3D/Metal formats.
+ // In this case, one of the following metadata entries have to be
+ // present:
+ // - "KTXglFormat" for OpenGL
+ // - "KTXdxgiFormat__" for Direct3D
+ // - "KTXmetalPixelFormat" for Metal
+ // => Not supported.
+ //
+ // 4. Compressed color models in Section 5.6 of [KDF14] or successors that
+ // do not have corresponding Vulkan formats.
+ // => Not supported.
+ //
+ // We only keep this for better error messaging.
+ //
+ if (m_tex->vkFormat == VK_FORMAT_UNDEFINED) {
+ errorfmt(
+ "VkFormat of provided KTX texture is VK_FORMAT_UNDEFINED "
+ "which potentially means that a custom format with no equivalent "
+ "in GPU APIs is provided. This is not supported.");
+ return false;
+ }
+
+ auto format = static_cast(m_tex->vkFormat);
+
+ //
+ // Important:
+ // Call this AFTER transcoding the basis universal scheme (i.e., after
+ // ktxTexture2_TranscodeBasis) and AFTER detecting which GPU block
+ // compression scheme is used.
+ //
+ FormatInfo format_info;
+ if (!get_info_from_vkformat(static_cast(format), format_info)) {
+ errorfmt(
+ "Failed to extract info (e.g., nchannels, typedesc, etc.) from VkFormat: {}",
+ static_cast(format));
+ return false;
+ }
+ m_spec.set_format(format_info.typedesc);
+ m_spec.nchannels = format_info.nbrchannels;
+
+ if (m_tex->baseDepth > 1) { // Volume texture are limited to 4096x4096x4096
+ if (!check_open(m_spec, { 0, 4096, 0, 4096, 0, 4096, 0, 4 }))
+ return false;
+ } else if (m_tex->numFaces
+ > 1) { // Cubemap texture are limited to 16384x16384
+ if (!check_open(m_spec, { 0, 16384, 0, 16384 * 6, 0, 1, 0, 4 }))
+ return false;
+ } else { // 2D texture are limited to 32768x32768
+ if (!check_open(m_spec, { 0, 32768, 0, 32768, 0, 1, 0, 4 }))
+ return false;
+ } // Array textures are not supported
+
+ if (!check_open(m_spec, { 0, 65535, 0, 65535, 0, 65535, 0, 4 }))
+ return false;
+
+ if (!seek_subimage(0, 0))
+ // errorfmt is set via seek_subimage
+ return false;
+
+ newspec = m_spec;
+ return true;
+}
+
+
+
+bool
+KtxInput::close()
+{
+ // Check if already closed
+ if (!ioproxy_opened())
+ return true;
+ ioproxy_clear();
+ return true;
+};
+
+
+
+bool
+KtxInput::check(int subimage, int miplevel) const
+{
+ //
+ // Before doing any calls, check if provided subimage and mip lvl are valid.
+ // This is how OIIO figures out the number of subimages/miplevels.
+ //
+ if (subimage < 0 || miplevel < 0 || (uint32_t)subimage >= m_tex->numLayers
+ || (uint32_t)miplevel >= m_tex->numLevels)
+ // don't errorfmt here
+ return false;
+ return true;
+}
+
+
+
+//
+// Since we load the whole KTX texture data in open(), actual seeking makes no
+// sense in this context. You can just read at any pixel by just computing an
+// offset.
+//
+// What this seek_subimage does, is essentially parameter verification (i.e.,
+// are provided subimage and miplevel sane for the current texture kind).
+//
+// In the context of KTX:
+// - For 3D textures (i.e., depth > 1): `subimage` does NOT reflect the
+// 3D texture depth slice. subimage SHOULD be 0. TODO: how to read a 3D slice then?
+// - For Cubemaps (i.e., tile_width/tile_height > 1): `subimage` does NOT reflect
+// the cubemap face. subimage SHOULD be 0. TODO: how to read a cubemap face tile?
+// - Arrays of 2D textures: `subimage` maps to layer in libktx (i.e., index in
+// array). subimage SHOULD be [0, m_tex->numLayers[.
+// - Arrays of 3D textures: same as array of 2D textures except each `subimage`
+// refer to 3D texture instead of a 2D one.
+//
+// For all kinds of textures, `miplevel` is ALWAYS interpreted as a mip level of
+// the above `subimage` (e.g., `miplevel` 0 of 3D texture refers to base-level
+// volume).
+//
+bool
+KtxInput::seek_subimage(int subimage, int miplevel)
+{
+ if (!check(subimage, miplevel))
+ return false;
+
+ // if same subimage and miplevel as current => early out
+ if (this->current_subimage() == subimage
+ && this->current_miplevel() == miplevel)
+ return true;
+
+ m_subimage = subimage;
+ m_miplevel = miplevel;
+
+ //
+ // According to official libktx source code, this is how they compute
+ // dimensions of a miplevel. See:
+ // https://github.com/KhronosGroup/KTX-Software/lib/src/texture.c
+ //
+ const size_t width = std::max(m_tex->baseWidth >> miplevel, 1u);
+ const size_t height = std::max(m_tex->baseHeight >> miplevel, 1u);
+ const size_t depth = std::max(m_tex->baseDepth >> miplevel, 1u);
+
+ m_spec.width = width;
+ m_spec.height = height;
+ m_spec.depth = depth;
+
+ return true;
+}
+
+
+
+bool
+KtxInput::read_native_scanline(int subimage, int miplevel, int y, int z,
+ void* data)
+{
+ return read_native_scanlines(subimage, miplevel, y, y + 1, z, data);
+}
+
+
+
+bool
+KtxInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
+ int yend, int z, void* data)
+{
+ const int width = std::max(m_tex->baseWidth >> miplevel, 1u);
+ const int height = std::max(m_tex->baseHeight >> miplevel, 1u);
+ const int depth = std::max(m_tex->baseDepth >> miplevel, 1u);
+ const size_t pitch_in_bytes = m_spec.pixel_bytes() * width;
+ ktx_size_t offset;
+
+ if (!check(subimage, miplevel)) {
+ errorfmt("KTX read_native_scanlines: invalid subimage or miplevel");
+ return false;
+ }
+
+ if (ybegin < 0 || ybegin >= yend || yend > height || z < 0 || z >= depth) {
+ errorfmt(
+ "KTX read_native_scanlines: Out of valid range scanline indices");
+ return false;
+ }
+
+ if (const size_t libktx_image_size = ktxTexture2_GetImageSize(m_tex.get(),
+ miplevel),
+ expected_image_size = pitch_in_bytes * height;
+ libktx_image_size != expected_image_size) {
+ errorfmt("Expected image size of {} but libktx reported {}",
+ expected_image_size, libktx_image_size);
+ return false;
+ }
+
+#if 0
+ // TODO: ktxTexture_GetRowPitch reports 4-bytes aligned size in bytes (i.e.,
+ // it adds padding when it shouldn't).
+ if (auto libktx_pitch_in_bytes
+ = ktxTexture_GetRowPitch((ktxTexture*)m_tex.get(), miplevel);
+ pitch_in_bytes == libktx_pitch_in_bytes) {
+ errorfmt("Expected a pitch of size {} but libktx expects {}",
+ pitch_in_bytes, libktx_pitch_in_bytes);
+ return false;
+ }
+#endif
+
+ // Use this in case OIIO API provides read_native_scanlines with `data` as
+ // `span` and a `z` slice param
+#if 0
+ // Can the provided span hold the requested scanlines?
+ // This only accesses nchannels of the m_spec, so this is thread safe.
+ if (!valid_raw_span_size(data, m_spec, 0, width, ybegin, yend))
+ // errorfmt is set within valid_raw_span_size
+ return false;
+#endif
+
+ //
+ // GetImageOffset implements internal checks depending on texture kind (e.g.,
+ // 3D, cubemap, etc.) and incase of invalid input, KTX_INVALID_OPERATION is
+ // returned.
+ //
+ // TODO: face slice idx
+ //
+ if (auto status = ktxTexture2_GetImageOffset(m_tex.get(), miplevel,
+ subimage, z, &offset);
+ status != KTX_SUCCESS) {
+ errorfmt("ktxTexture_GetImageOffset failed with exit code: {}",
+ static_cast(status));
+ return false;
+ }
+
+ auto data_ptr = m_tex->pData + offset;
+
+ // since miplevel is valid => get number of bytes in a row for this mip
+ memcpy(data, data_ptr, pitch_in_bytes * size_t(yend - ybegin));
+ DBG std::cout << fmt::format(
+ "[ktxinput] read_native_scanlines(subimage={},miplevel={},ybegin={},yend={},z={})\n",
+ subimage, miplevel, ybegin, yend, z);
+ return true;
+}
+
+
+
+bool
+OpenImageIO::KtxInput::valid_file(Filesystem::IOProxy* ioproxy) const
+{
+ // Check magic number to assure this is a KTX2 file
+ if (!ioproxy || ioproxy->mode() != Filesystem::IOProxy::Read)
+ return false;
+
+ // per KTX2 specs: the first 12 bytes of a KTX2 file are used to identify it
+ uint8_t magic[12] {};
+ const size_t numRead = ioproxy->pread(magic, sizeof(magic), 0);
+
+ return (numRead == sizeof(magic)) && this->ktx_magic_cmp(magic, 0);
+}
+
+
+
+bool
+KtxInput::ktx_magic_cmp(const uint8_t* sig, size_t start) const
+{
+ // this is: "«KTX 20»\r\n\x1A\n"
+ const uint8_t KTX2_IDENTIFIER[12] { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32,
+ 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A };
+ for (size_t i = start; (i - start) < sizeof(KTX2_IDENTIFIER); ++i)
+ if (sig[i] != KTX2_IDENTIFIER[i])
+ return false;
+ return true;
+}
+
+
+
+std::string
+KtxInput::get_colorspace() const
+{
+ //
+ // for set of, see:
+ // https://github.com/KhronosGroup/KTX-Software/blob/main/external/dfdutils/KHR/khr_df.h
+ // for OIIO colorspaces, see:
+ // https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/src/libOpenImageIO/color_ocio.cpp
+ //
+ // Don't use ktxTexture2_GetPrimaries_e/ktxTexture2_GetTransferFunction_e as these are only
+ // available in newer versions of libktx (>= 5.0.0, I think)
+ //
+ const auto transfer_function = KHR_DFDVAL(m_tex->pDfd + 1, TRANSFER);
+ const auto primaries = KHR_DFDVAL(m_tex->pDfd + 1, PRIMARIES);
+ DBG std::cout << "tf: " << transfer_function << "; primaries: " << primaries
+ << '\n';
+ switch (transfer_function) {
+ case KHR_DF_TRANSFER_SRGB:
+ switch (primaries) {
+ case KHR_DF_PRIMARIES_BT709: return "srgb_rec709_scene";
+ default: errorfmt("Unsupported color primaries {}", primaries); break;
+ }
+ break;
+ case KHR_DF_TRANSFER_LINEAR:
+ switch (primaries) {
+ case KHR_DF_PRIMARIES_BT709: return "lin_rec709_scene";
+ default: errorfmt("Unsupported color primaries {}", primaries); break;
+ }
+ break;
+ default:
+ errorfmt("Unsupported color transfer function {}", transfer_function);
+ break;
+ }
+ // TODO: need to generate test files before adding support for any other
+ // colorspaces
+ return "unknown";
+}
+
+
+
+///
+/// Parses KTXwriterScParams metadata and sets relevant attributes accordingly.
+/// E.g., if Basis Universal non-default params were found, they are set.
+///
+/// One cannot know from the KTX2 file itself the kind of parameters that were
+/// used to create/encode it (e.g., RDO params).
+///
+/// This is only useful when reading a KTX2 via OIIO and re-writing it again
+/// (which is a bad idea since each re-write cycle worsens the quality). That
+/// being said, if the user intends to use OIIO this way, the written KTX2 file
+/// should be as similar as possible to the given input (assuming a read-write
+/// of a KTX2 file without any change to its data).
+///
+/// Note:
+/// std::from_chars fails on MacOS because it still apparently doesn't support
+/// floating point values ...
+///
+void
+KtxInput::parse_ktx_sc_params_metadata(const std::string& ktx_sc_params)
+{
+ const auto f = std::regex_constants::icase;
+
+ { // BasisLZ/ETC1S params (see KTX-Software/tools/ktx/encode_utils_basis.h)
+ std::regex etc1s_clevel("--clevel\\s+(\\d+)", f);
+ std::regex etc1s_qlevel("--qlevel\\s+(\\d+)", f);
+ std::regex etc1s_max_endpoints("--max-endpoints\\s+(\\d+)", f);
+ std::regex etc1s_endpoint_rdo_threshold(
+ "--endpoint-rdo-threshold\\s+((\\d*[.])?\\d+)", f);
+ std::regex etc1s_max_selectors("--max-selectors\\s+(\\d+)", f);
+ std::regex etc1s_selector_rdo_threshold(
+ "--selector-rdo-threshold\\s+((\\d*[.])?\\d+)", f);
+ std::regex etc1s_no_endpoint_rdo_re("--no-endpoint-rdo", f);
+ std::regex etc1s_no_selector_rdo_re("--no-selector-rdo", f);
+
+ if (std::smatch m; std::regex_search(ktx_sc_params, m, etc1s_clevel)
+ && m.size() == 2) {
+ auto level = static_cast(std::stol(m[1].str()));
+ m_spec.extra_attribs.attribute("ktx:etc1sCompressionLevel", level);
+ }
+
+ if (std::smatch m; std::regex_search(ktx_sc_params, m, etc1s_qlevel)
+ && m.size() == 2) {
+ auto level = static_cast(std::stol(m[1].str()));
+ m_spec.extra_attribs.attribute("ktx:etc1sQualityLevel", level);
+ }
+
+ if (std::smatch m;
+ std::regex_search(ktx_sc_params, m, etc1s_max_endpoints)
+ && m.size() == 2) {
+ auto level = static_cast(std::stol(m[1].str()));
+ m_spec.extra_attribs.attribute("ktx:etc1sMaxEndpoints", level);
+ }
+
+ if (std::smatch m;
+ std::regex_search(ktx_sc_params, m, etc1s_endpoint_rdo_threshold)
+ && m.size() == 2) {
+ auto val = std::stof(m[1].str());
+ m_spec.extra_attribs.attribute("ktx:etc1sEndpointRDOThreshold",
+ val);
+ }
+
+ if (std::smatch m;
+ std::regex_search(ktx_sc_params, m, etc1s_max_selectors)
+ && m.size() == 2) {
+ auto level = static_cast(std::stol(m[1].str()));
+ m_spec.extra_attribs.attribute("ktx:etc1sMaxSelectors", level);
+ }
+
+ if (std::smatch m;
+ std::regex_search(ktx_sc_params, m, etc1s_selector_rdo_threshold)
+ && m.size() == 2) {
+ auto val = std::stof(m[1].str());
+ m_spec.extra_attribs.attribute("ktx:etc1sSelectorRDOThreshold",
+ val);
+ }
+
+ if (std::regex_match(ktx_sc_params, etc1s_no_endpoint_rdo_re))
+ m_spec.extra_attribs.attribute("ktx:etc1sNoEndpointRDO", true);
+
+ if (std::regex_match(ktx_sc_params, etc1s_no_selector_rdo_re))
+ m_spec.extra_attribs.attribute("ktx:etc1sNoSelectorRDO", true);
+ }
+
+ { // UASTC params (see KTX-Software/tools/ktx/encode_utils_basis.h)
+ std::regex uastc_quality_re("--uastc-quality\\s+(\\d+)", f);
+ std::regex uastc_rdo_re("--uastc-rdo", f);
+ std::regex uastc_rdo_l_re("--uastc-rdo-l\\s+((\\d*[.])?\\d+)", f);
+ std::regex uastc_rdo_d_re("--uastc-rdo-d\\s+(\\d+)", f);
+ std::regex uastc_rdo_b_re("--uastc-rdo-b\\s+((\\d*[.])?\\d+)", f);
+ std::regex uastc_rdo_s_re("--uastc-rdo-s\\s+((\\d*[.])?\\d+)", f);
+ std::regex uastc_rdo_f_re("--uastc-rdo-f", f);
+ std::regex uastc_rdo_m_re("--uastc-rdo-m", f);
+ std::regex uastc_rdo_uber_mode_re("--uastc-hdr-uber-mode", f);
+ std::regex uastc_rdo_ultra_quant_re("--uastc-hdr-ultra-quant", f);
+ std::regex uastc_rdo_favor_astc_re("--uastc-hdr-favor-astc", f);
+ std::regex uastc_hdr_lambda_re("--uastc-hdr-lambda\\s+((\\d*[.])?\\d+)",
+ f);
+ std::regex uastc_hdr_6x6i_level_re("--uastc-hdr-6x6i-level\\s+(\\d+)",
+ f);
+
+ if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_quality_re)
+ && m.size() == 2) {
+ auto uastc_quality = static_cast(std::stol(m[1].str()));
+ const uint32_t uastc_flags
+ = (unsigned int)~KTX_PACK_UASTC_LEVEL_MASK | uastc_quality;
+ m_spec.extra_attribs.attribute("ktx:uastcFlags", uastc_flags);
+ m_spec.extra_attribs.attribute("ktx:uastcHDRQuality",
+ uastc_quality);
+ }
+
+ if (std::regex_match(ktx_sc_params, uastc_rdo_re))
+ m_spec.extra_attribs.attribute("ktx:uastcRDO", true);
+
+ if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_rdo_l_re)
+ && m.size() == 2) {
+ auto uastc_rdo_l = std::stof(m[1].str());
+ m_spec.extra_attribs.attribute("ktx:uastcRDOQualityScalar",
+ uastc_rdo_l);
+ }
+
+ if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_rdo_d_re)
+ && m.size() == 2) {
+ auto uastc_rdo_d = static_cast(std::stol(m[1].str()));
+ m_spec.extra_attribs.attribute("ktx:uastcRDODictSize", uastc_rdo_d);
+ }
+
+ if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_rdo_b_re)
+ && m.size() == 2) {
+ auto uastc_rdo_b = std::stof(m[1].str());
+ m_spec.extra_attribs.attribute(
+ "ktx:uastcRDOMaxSmoothBlockErrorScale", uastc_rdo_b);
+ }
+
+ if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_rdo_s_re)
+ && m.size() == 2) {
+ auto uastc_rdo_s = std::stof(m[1].str());
+ m_spec.extra_attribs.attribute("ktx:uastcRDOMaxSmoothBlockStdDev",
+ uastc_rdo_s);
+ }
+
+ if (std::regex_match(ktx_sc_params, uastc_rdo_f_re))
+ m_spec.extra_attribs.attribute("ktx:uastcRDODontFavorSimplerModes",
+ true);
+ if (std::regex_match(ktx_sc_params, uastc_rdo_m_re))
+ m_spec.extra_attribs.attribute("ktx:uastcRDONoMultithreading",
+ true);
+
+ if (std::regex_match(ktx_sc_params, uastc_rdo_m_re))
+ m_spec.extra_attribs.attribute("ktx:uastcHDRQuality", true);
+ if (std::regex_match(ktx_sc_params, uastc_rdo_uber_mode_re))
+ m_spec.extra_attribs.attribute("ktx:uastcHDRUberMode", true);
+ if (std::regex_match(ktx_sc_params, uastc_rdo_ultra_quant_re))
+ m_spec.extra_attribs.attribute("ktx:uastcHDRUltraQuant", true);
+ if (std::regex_match(ktx_sc_params, uastc_rdo_favor_astc_re))
+ m_spec.extra_attribs.attribute("ktx:uastcHDRFavorAstc", true);
+
+ if (std::smatch m;
+ std::regex_search(ktx_sc_params, m, uastc_hdr_lambda_re)
+ && m.size() == 2) {
+ auto uastc_hdr_lambda = std::stof(m[1].str());
+ m_spec.extra_attribs.attribute("ktx:uastcHDRLambda",
+ uastc_hdr_lambda);
+ }
+
+ if (std::smatch m;
+ std::regex_search(ktx_sc_params, m, uastc_hdr_6x6i_level_re)
+ && m.size() == 2) {
+ auto uastc_hdr_6x6i_level = static_cast(
+ std::stol(m[1].str()));
+ m_spec.extra_attribs.attribute("ktx:uastcHDRLevel",
+ uastc_hdr_6x6i_level);
+ }
+ }
+
+ { // Common params (see KTX-Software/tools/ktx/encode_utils_common.h)
+ std::regex common_normal_map("--normal-mode", f);
+ std::regex common_no_sse("--no-sse", f);
+
+ if (std::regex_match(ktx_sc_params, common_normal_map))
+ m_spec.extra_attribs.attribute("ktx:normalMap", true);
+ if (std::regex_match(ktx_sc_params, common_no_sse))
+ m_spec.extra_attribs.attribute("ktx:noSSE", true);
+ }
+
+ // preSwizzle and inputSwizzle are not exposed in ktx tools
+}
+
+OIIO_PLUGIN_NAMESPACE_END
diff --git a/src/ktx.imageio/ktxoutput.cpp b/src/ktx.imageio/ktxoutput.cpp
new file mode 100644
index 0000000000..4cc1079ef1
--- /dev/null
+++ b/src/ktx.imageio/ktxoutput.cpp
@@ -0,0 +1,898 @@
+// Copyright Contributors to the OpenImageIO project.
+// SPDX-License-Identifier: Apache-2.0
+// https://github.com/AcademySoftwareFoundation/OpenImageIO
+
+#include "OpenImageIO/imageio.h"
+#include "ktx_pvt.h"
+#include
+
+OIIO_PLUGIN_NAMESPACE_BEGIN
+
+// TODO:
+// - Should we add support to 2D texture arrays (i.e., "multiimage")?
+
+class KtxOutput final : public ImageOutput {
+public:
+ KtxOutput()
+ {
+#if defined(KHRONOS_STATIC)
+ // This has to be set if libktx is statically built
+ DBG std::cout << "KHRONOS_STATIC set to 1" << '\n';
+#endif
+ }
+
+ ~KtxOutput() override { close(); }
+
+ const char* format_name(void) const override { return "ktx"; }
+
+ int supports(string_view feature) const override
+ {
+ return (
+ feature == "alpha" || feature == "ioproxy" ||
+ // as per the KTX2 specs:
+ // registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_keyvalue_data
+ feature == "arbitrary_metadata" ||
+ // KTX2 supports 2D texture arrays, cubmap arrays, and 2D texture
+ // arrays. That being said, we only support 2D texture arrays.
+ // feature == "multiimage" ||
+ // ktx supports mipmaps
+ feature == "mipmap" ||
+ // Ktx supports 3D textures
+ feature == "volumes" || feature == "random_access"
+ // Can write in any order whatsoever
+ );
+ }
+
+ bool open(const std::string& name, const ImageSpec& spec,
+ OpenMode mode = Create) override;
+
+ bool write_scanline(int y, int z, TypeDesc format, const void* data,
+ stride_t xstride) override;
+
+ bool write_scanlines(int ybegin, int yend, int z, TypeDesc format,
+ const void* data, stride_t xstride = AutoStride,
+ stride_t ystride = AutoStride) override;
+
+ bool close() override;
+
+private:
+ std::string m_filename;
+
+ bool m_initialized { false }; ///< Has open() with mode == Create was
+
+ /// Uncompressed Vulkan format to create the initial texture with.
+ VkFormat m_vkformat { VK_FORMAT_UNDEFINED };
+
+ uint32_t m_miplevel_idx { 0 }; ///< Current MIP level
+
+ uint32_t m_max_nmiplevels { 1 }; ///< Max number allowable MIP levels
+
+ uint32_t m_basewidth { 0 }; ///< MIP level 0 width
+
+ uint32_t m_baseheight { 0 }; ///< MIP level 0 height
+
+ uint32_t m_basedepth { 0 }; ///< MIP level 0 depth
+
+ ktxSupercmpScheme m_superCmp { KTX_SS_NONE };
+
+ /// Whether to generate MIP maps when loading texture to graphics API. This
+ /// will be passed to ktxTextureCreateInfo's generateMipmaps param.
+ bool m_generate_mipmaps { false };
+
+ BlockCompression m_cmp { BlockCompression::NONE };
+
+ bool m_use_basis_universal { false };
+
+ ktxBasisParams m_basis_params { 0 };
+
+ ktxAstcParams m_astc_params { 0 }; ///< Only for ASTC block compression
+
+ // ktxBCnParams m_bcn_params { 0 }; ///< Only for BCn block compression
+
+ uint32_t m_zlib_level { 9 }; ///< Only for Zlib supercompression. Defaults
+ ///< to highest compression level.
+
+ uint32_t m_zstd_level { 22 }; ///< Only for ZSTD supercompression. Defaults
+ ///< to highest compression level.
+
+ std::vector m_scratch;
+
+ ///
+ /// Container for raw (i.e., uncompressed) texture data structured as
+ /// follows:
+ /// mip level -> slice/face -> pixels
+ ///
+ /// The number of slices/faces is known before hand (i.e., appending
+ /// slices/faces is not supported). This significantly simplifies the
+ /// implementation.
+ ///
+ /// Q. Why store all subimages/levels/slices/faces here?
+ /// A. libktx only supports writing whole images (i.e.,
+ /// miplevel+layer+slice/face hence why we keep a large std::vector at
+ /// all times. This is also needed because we apply compression upon file
+ /// closure and not each time on write_scanline(s).
+ ///
+ /// To access underlying data: [miplevel_idx][slice_idx + face_idx]
+ /// Note: slice_idx and face_idx are mutually exclusive so summing them is
+ /// perfectly fine.
+ ///
+ std::vector>> m_imgs;
+
+ void append_mipmaps_vector();
+
+ void init();
+
+ bool write_ktx2();
+
+ bool construct_basis_params(ktxBasisParams& params, std::string_view codec,
+ uint32_t threads = 1) const;
+
+ // bool construct_astc_params(ktxAstcParams& param) const;
+
+ // bool construct_bcn_params(ktxBCnParams& params) const;
+};
+
+
+
+OIIO_PLUGIN_EXPORTS_BEGIN
+
+OIIO_EXPORT ImageOutput*
+ktx_output_imageio_create()
+{
+ return new KtxOutput;
+}
+OIIO_EXPORT const char* ktx_output_extensions[] = { "ktx2", nullptr };
+
+OIIO_PLUGIN_EXPORTS_END
+
+
+
+bool
+KtxOutput::open(const std::string& name, const ImageSpec& userspec,
+ OpenMode mode)
+{
+ if (mode == Create) {
+ if (userspec.depth
+ > 1) { // Volume texture are limited to 4096x4096x4096
+ if (!check_open(mode, userspec, { 0, 4096, 0, 4096, 0, 4096, 0, 4 }))
+ return false;
+ } else if (userspec.tile_width
+ > 1) { // Cubemap texture are limited to 16384x16384
+ if (!check_open(mode, userspec,
+ { 0, 16384, 0, 16384 * 6, 0, 1, 0, 4 }))
+ return false;
+ } else { // 2D texture are limited to 32768x32768
+ if (!check_open(mode, userspec, { 0, 32768, 0, 32768, 0, 1, 0, 4 }))
+ return false;
+ }
+
+ m_basewidth = m_spec.width;
+ m_baseheight = m_spec.height;
+ m_basedepth = m_spec.depth;
+
+ // Save name and spec for later use
+ m_filename = name;
+
+ // If not uint8, default to uint8 (HDR not yet supported)
+ if (m_spec.format
+ != TypeDesc::UINT8 /* && m_spec.format != TypeDesc::UINT16 */)
+ m_spec.set_format(TypeDesc::UINT8);
+
+ ioproxy_retrieve_from_config(m_spec);
+ if (!ioproxy_use_or_open(m_filename))
+ return false;
+
+ // Try to figure the texture kind (i.e., plain 2D texture, volume, or cubemap)
+ // const auto textureformat = m_spec.get_string_attribute("textureformat");
+ // if (textureformat == "Volume Texture") {
+ // if (m_spec.depth <= 1) {
+ // errorfmt("Volume textures are expected to have a depth > 1");
+ // close();
+ // return false;
+ // }
+ // m_texturekind = TextureKind::SINGLE_TEXTURE_3D;
+ // } else if (textureformat == "CubeFace Environment") {
+ // // TODO: check on tiles
+ // m_texturekind = TextureKind::CUBEMAP_TEXTURE;
+ // } else /* unknown */ {
+ // m_texturekind = TextureKind::SINGLE_TEXTURE_2D;
+ // }
+
+ const auto compression = m_spec.get_string_attribute("compression",
+ "NONE");
+ if (iequals(compression, "NONE")) {
+ m_cmp = BlockCompression::NONE;
+ } else if (iequals(compression, "ASTC")) {
+ m_cmp = BlockCompression::ASTC;
+ } else {
+ errorfmt(
+ "Unsupported/Unknown compression from string attribute \"compression\": ",
+ compression);
+ return false;
+ }
+
+ // Currently only two colorspaces are tested, linear or REC709 sRGB
+ const auto colorspace = m_spec.get_string_attribute("oiio:ColorSpace",
+ "srgb_rec709_scene");
+ bool is_srgb = colorspace == "srgb_rec709_scene";
+ m_spec.set_colorspace(colorspace);
+
+ // TODO: get_int_attribute causes a segfault and I have no idea why ...
+ // Weirdly, calling find_attribute directly (and checking the resulting
+ // pointer) works, but not get_int_attribute ...
+
+ //
+ // Use sensible default in case the input data did not originate from a KTX2
+ // file and the user did not provide a supercompression scheme. KTX2 usually
+ // uses Basis LZ supercompression scheme to benefit from both: smaller
+ // disk filesizes and on-the-fly transcoding to a supported native GPU
+ // format.
+ //
+ const auto& supercompression_str
+ = m_spec.get_string_attribute("ktx:supercompressionscheme", "NONE");
+ if (iequals(supercompression_str, "NONE")) {
+ m_superCmp = ktxSupercmpScheme::KTX_SS_NONE;
+ } else if (iequals(supercompression_str, "ZSTD")) {
+ m_superCmp = ktxSupercmpScheme::KTX_SS_ZSTD;
+ } else if (iequals(supercompression_str, "ZLIB")) {
+ m_superCmp = ktxSupercmpScheme::KTX_SS_ZLIB;
+ } else {
+ close();
+ errorfmt("unsupported super compression scheme: {}",
+ static_cast(m_superCmp));
+ return false;
+ }
+ if (auto Q = m_spec.find_attribute("", TypeDesc::STRING)) {
+ const auto& supercompression_str = Q->get_string();
+
+ m_superCmp = static_cast(
+ *(uint32_t*)(Q->data()));
+ // Do an early check on supported supercompressionscheme values
+ if (m_superCmp != KTX_SS_BASIS_LZ && m_superCmp != KTX_SS_NONE) {
+ // doing an `errorfmt()` then `close()` causes a seg fault...
+ }
+ DBG std::cout << "[ktxoutput] supercompression scheme: "
+ << m_superCmp << '\n';
+ }
+
+ if (auto Q = m_spec.find_attribute("ktx:generatemipmaps",
+ TypeDesc::INT)) {
+ m_generate_mipmaps = static_cast(Q->get_int());
+ DBG std::cout << "[ktxoutput] generate mipmaps: " << std::boolalpha
+ << m_generate_mipmaps << '\n';
+ }
+
+ // We can check m_basis_params.codec != NONE but this won't work for
+ // libktx 4.3.2 which is why we just use a bool var
+ m_use_basis_universal = false;
+ if (auto Q = m_spec.find_attribute("ktx:codec", TypeDesc::STRING)) {
+ const auto& codec = Q->get_string();
+ if (!construct_basis_params(m_basis_params, codec)) {
+ close();
+ // construct_basis_params calls errorfmt
+ return false;
+ }
+ m_use_basis_universal = m_basis_params.codec
+ != KTX_BASIS_CODEC_NONE;
+ DBG std::cout
+ << "[ktxoutput] basis universal codec (from \"ktx:codec\"): "
+ << codec << '\n';
+ }
+
+ //
+ // User provided nothing about neither the target GPU block compression
+ // nor the target Basis Universal codec. Default to writing uncompressed
+ // VK_FORMAT with ZSTD supercompression (write as losseless KTX2
+ // output).
+ //
+ // If we intend to compress to BasisLZ/ETC1S or UASTC then we need to
+ // figure the VkFormat so that ktxTexture_SetImageFromMemory does not
+ // segfault. (makes sense, since we are creating a KTX texture and
+ // telling it to allocate storage, how would it know the size of a given
+ // subimage if we provide it with VK_FORMAT_UNDEFINED?)
+ //
+ m_vkformat = get_vkformat_from_info(m_spec.nchannels, m_spec.format,
+ is_srgb);
+ if (m_vkformat == VK_FORMAT_UNDEFINED) {
+ close();
+ errorfmt(
+ "Failed to determine VkFormat from nchannels={} format={} colorspace={}",
+ m_spec.nchannels, m_spec.format, colorspace);
+ return false;
+ }
+
+
+ if (m_spec.depth > 1)
+ m_max_nmiplevels
+ = (uint32_t)floor(
+ logf(std::min(std::min(m_spec.width, m_spec.height),
+ m_spec.depth))
+ / logf(2))
+ + 1;
+ else
+ m_max_nmiplevels = (uint32_t)floor(
+ logf(std::min(m_spec.width, m_spec.height))
+ / logf(2))
+ + 1;
+
+ DBG std::cout << "[ktxoutput] max number mip levels allowed: "
+ << m_max_nmiplevels << std::endl;
+
+ // Initialize slices/faces container if not already initialized by a
+ // previous call to open(name, subimages, specs)
+ OIIO_ASSERT(m_imgs.empty()
+ && "Expected mip levels container to be empty");
+ OIIO_ASSERT(m_miplevel_idx == 0);
+
+ // Reserve space for base-level mipmap (level 0)
+ append_mipmaps_vector();
+
+ m_initialized = true;
+ return true;
+ } // mode == Create
+
+ if (mode == AppendMIPLevel) {
+ if (!m_initialized) {
+ errorfmt("Cannot append a MIP level if no file has been opened");
+ return false;
+ }
+
+ if ((m_miplevel_idx + 1) >= m_max_nmiplevels) {
+ errorfmt("Maximum number of mip levels is reached");
+ return false;
+ }
+
+ uint32_t miplevel_width = std::max(1u,
+ m_basewidth >> (m_miplevel_idx + 1));
+ uint32_t miplevel_height = std::max(1u, m_baseheight
+ >> (m_miplevel_idx + 1));
+ uint32_t miplevel_depth = std::max(1u,
+ m_basedepth >> (m_miplevel_idx + 1));
+
+ // Copy the new mip level size. Keep everything else from the
+ // original level.
+ if ((uint32_t)userspec.width != miplevel_width
+ || (uint32_t)userspec.height != miplevel_height
+ || (uint32_t)userspec.depth != miplevel_depth) {
+ errorfmt(
+ "Expected (widht,height,depth): ({},{},{}) but got: ({},{},{})",
+ miplevel_width, miplevel_height, miplevel_depth, userspec.width,
+ userspec.height, userspec.depth);
+ return false;
+ }
+ m_spec.width = userspec.width;
+ m_spec.height = userspec.height;
+ m_spec.depth = userspec.depth;
+ ++m_miplevel_idx;
+
+ // Reserve memory for this mip level
+ append_mipmaps_vector();
+
+ return true;
+ } // mode == AppendMIPLevel
+
+ // (mode == AppendSubimage) is NOT supported. Pre-declaring the number of
+ // subimages is easier to implement (random access to volume slices is
+ // already provided).
+
+ return false;
+}
+
+
+
+bool
+KtxOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
+ stride_t xstride)
+{
+ return write_scanlines(y, y + 1, z, format, data, xstride);
+}
+
+
+
+bool
+KtxOutput::write_scanlines(int ybegin, int yend, int z, TypeDesc format,
+ const void* data, stride_t xstride, stride_t ystride)
+{
+ const int width = std::max(m_basewidth >> m_miplevel_idx, 1u);
+ const int height = std::max(m_baseheight >> m_miplevel_idx, 1u);
+ if (ybegin < 0 || ybegin >= yend || yend > height || z < 0
+ || z >= (int)m_basedepth) {
+ errorfmt(
+ "KTX write_scanlines: Out of valid range scanline indices. "
+ "Provided: ybegin={} yend={} z={}. "
+ "Constraints: ybegin: [0, min({},yend)[, yend>ybegin, z: [0,{}[",
+ ybegin, yend, z, height, m_basedepth);
+ return false;
+ }
+ stride_t zstride = AutoStride;
+ m_spec.auto_stride(xstride, ystride, zstride, format, m_spec.nchannels,
+ width, height);
+ //
+ // Convert to the native format the current specs expects. This is needed,
+ // for instance, to convert a given TypeDesc::FLOAT into native format that
+ // this KTX2 writer expects (i.e., TypeDesc::UINT8 or TypeDesc::UINT16).
+ //
+ // Returned data pointer may be the same as the provided pointer (i.e., no
+ // conversion is needed because supplied data is already in native format).
+ //
+ data = to_native_rectangle(/* m_spec.x */ 0, /* m_spec.x + */ width, ybegin,
+ yend, z, z + 1, format, data, xstride, ystride,
+ zstride, m_scratch, false, 0, ybegin, z);
+
+ // data should now be contiguous and of the expected format (UINT8 or
+ // UINT16) so simply memcpy into internal buffer that will be written on
+ // close().
+ const size_t pitch = m_spec.scanline_bytes();
+ auto pSrc = reinterpret_cast(data);
+ size_t offset = ybegin * pitch;
+ size_t datalen = (yend - ybegin) * pitch;
+ memcpy(m_imgs[m_miplevel_idx][z].data() + offset, pSrc, datalen);
+ DBG std::cout << "write_scanlines wrote " << datalen << " bytes"
+ << std::endl;
+ return true;
+}
+
+
+
+bool
+KtxOutput::close()
+{
+ DBG std::cout << "[ktxoutput] close() called" << std::endl;
+ // closing an un-opened ImageOutput instance is fine
+ if (!m_initialized)
+ return true;
+ // Check if already closed => if so, then the KTX2 file is already saved
+ if (!ioproxy_opened()) {
+ init();
+ return true;
+ }
+ bool result = write_ktx2(); // TODO: can this throw? (prob not)
+ init();
+ return result;
+}
+
+
+
+void
+KtxOutput::append_mipmaps_vector()
+{
+ m_imgs.emplace_back(m_spec.depth);
+ for (auto& slices_vec : m_imgs[m_miplevel_idx]) {
+ slices_vec.resize(m_spec.scanline_bytes() * m_spec.height);
+ }
+}
+
+
+
+void
+KtxOutput::init()
+{
+ // TODO: calling open() after close() on this hasn't been tested yet ...
+ m_initialized = false;
+ m_filename = std::string();
+ m_vkformat = VK_FORMAT_UNDEFINED;
+ m_miplevel_idx = 0;
+ m_max_nmiplevels = 1;
+ m_basewidth = 0;
+ m_baseheight = 0;
+ m_basedepth = 0;
+ m_superCmp = KTX_SS_NONE;
+ m_generate_mipmaps = false;
+ m_cmp = BlockCompression::NONE;
+ m_use_basis_universal = false;
+ m_basis_params = { 0 };
+ m_astc_params = { 0 };
+ // m_bcn_params = { 0 };
+ m_zlib_level = 9;
+ m_zstd_level = 22;
+ m_imgs.clear();
+ ioproxy_clear();
+}
+
+
+
+///
+/// Contruct ktxBasisParams struct from given input (from the provided
+/// ImageSpec). There are two ways to approach this:
+/// 1. Expect the whole ktxBasisParams to be provided by the user
+/// 2. Search for each individual field
+///
+/// Option 1) has the following benefits:
+/// - Expose only one attribute "ktx:basisparams"
+/// But:
+/// - The user has to be aware of which libktx version is used (very bad)
+/// - Less safe (?)
+///
+/// Options 2) has the following benefits:
+/// - User just has to provide each parameter separately thus => don't have
+/// to care about which libktx version is used.
+/// - Safer (?)
+/// But:
+/// - A lot of "ktx:" attributes have to be exposed. We can
+/// provide "high-level" parameters but that will make writing KTX2 output
+/// using OIIO significantly less customizable.
+///
+/// TODO: update comment when we finally agree on which approach.
+///
+/// For the moment, option 2) option 2) is opted for.
+///
+bool
+KtxOutput::construct_basis_params(ktxBasisParams& params,
+ std::string_view codec,
+ uint32_t threads) const
+{
+ // Set defaults
+ params = { 0 };
+ params.structSize = sizeof(ktxBasisParams);
+ params.threadCount = threads;
+
+ params.etc1sCompressionLevel = KTX_ETC1S_DEFAULT_COMPRESSION_LEVEL;
+
+ params.uastcFlags = KTX_PACK_UASTC_LEVEL_DEFAULT;
+
+ if (iequals(codec, "NONE")) {
+ // no need to fill remaining struct members
+ params.codec = KTX_BASIS_CODEC_NONE;
+ return true;
+ }
+
+ if (iequals(codec, "uastc") || iequals(codec, "uastc-ldr")
+ || iequals(codec, "uastc-ldr-4x4")) {
+ params.codec = KTX_BASIS_CODEC_UASTC_LDR_4x4;
+ } else if (iequals(codec, "uastc-hdr") || iequals(codec, "uastc-hdr-4x4")) {
+ params.codec = KTX_BASIS_CODEC_UASTC_HDR_4x4;
+ } else if (iequals(codec, "etc1s")) {
+ params.codec = KTX_BASIS_CODEC_ETC1S;
+ } else if (iequals(codec, "uastc-hdr-6x6")) {
+ params.codec = KTX_BASIS_CODEC_UASTC_HDR_6x6_INTERMEDIATE;
+ } else {
+ errorfmt(
+ "Provided Basis Universal codec \"{}\" is invalid. Supported values: \"uastc\" \"etc1s\" \"uastc-hdr-4x4\" \"uastc-hdr-6x6\"",
+ codec);
+ return false;
+ }
+
+ if (params.codec == KTX_BASIS_CODEC_ETC1S) {
+ // Params that only apply to ETC1S
+ if (auto Q = m_spec.find_attribute("ktx:etc1sCompressionLevel",
+ TypeDesc::UINT32))
+ params.etc1sCompressionLevel = *(uint32_t*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:etc1sQualityLevel",
+ TypeDesc::UINT32))
+ params.qualityLevel = *(uint32_t*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:etc1sMaxEndpoints",
+ TypeDesc::UINT32))
+ params.maxEndpoints = *(uint32_t*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:etc1sEndpointRDOThreshold",
+ TypeDesc::FLOAT))
+ params.endpointRDOThreshold = *(float*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:etc1sMaxSelectors",
+ TypeDesc::UINT32))
+ params.maxSelectors = *(uint32_t*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:etc1sSelectorRDOThreshold",
+ TypeDesc::FLOAT))
+ params.selectorRDOThreshold = *(float*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:etc1sNoEndpointRDO"))
+ params.noEndpointRDO = static_cast(Q->get_int());
+ if (auto Q = m_spec.find_attribute("ktx:etc1sNoSelectorRDO",
+ TypeDesc::INT))
+ params.noSelectorRDO = static_cast(Q->get_int());
+ } else if (params.codec == KTX_BASIS_CODEC_UASTC_LDR_4x4) {
+ // Params that only apply to UASTC
+ if (auto Q = m_spec.find_attribute("ktx:uastcFlags", TypeDesc::UINT32))
+ params.uastcFlags = *(uint32_t*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:uastcRDO", TypeDesc::INT))
+ params.uastcRDO = static_cast(Q->get_int());
+ if (auto Q = m_spec.find_attribute("ktx:uastcRDOQualityScalar",
+ TypeDesc::FLOAT))
+ params.uastcRDOQualityScalar = *(float*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:uastcRDODictSize",
+ TypeDesc::UINT32))
+ params.uastcRDODictSize = *(uint32_t*)Q->data();
+ if (auto Q
+ = m_spec.find_attribute("ktx:uastcRDOMaxSmoothBlockErrorScale",
+ TypeDesc::FLOAT))
+ params.uastcRDOMaxSmoothBlockErrorScale = *(float*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:uastcRDOMaxSmoothBlockStdDev",
+ TypeDesc::FLOAT))
+ params.uastcRDOMaxSmoothBlockStdDev = *(float*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:uastcRDODontFavorSimplerModes",
+ TypeDesc::INT))
+ params.uastcRDODontFavorSimplerModes = static_cast(
+ Q->get_int());
+ if (auto Q = m_spec.find_attribute("ktx:uastcRDONoMultithreading",
+ TypeDesc::INT))
+ params.uastcRDONoMultithreading = static_cast(Q->get_int());
+ } else if (params.codec == KTX_BASIS_CODEC_UASTC_HDR_4x4
+ || params.codec == KTX_BASIS_CODEC_UASTC_HDR_6x6_INTERMEDIATE) {
+ if (auto Q = m_spec.find_attribute("ktx:uastcHDRQuality",
+ TypeDesc::UINT32))
+ params.uastcHDRQuality = *(uint32_t*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:uastcHDRUberMode",
+ TypeDesc::INT))
+ params.uastcHDRUberMode = static_cast(Q->get_int());
+ if (auto Q = m_spec.find_attribute("ktx:uastcHDRUltraQuant",
+ TypeDesc::INT))
+ params.uastcHDRUltraQuant = static_cast(Q->get_int());
+ if (auto Q = m_spec.find_attribute("ktx:uastcHDRFavorAstc",
+ TypeDesc::INT))
+ params.uastcHDRFavorAstc = static_cast(Q->get_int());
+ if (auto Q = m_spec.find_attribute("ktx:uastcHDRLambda",
+ TypeDesc::FLOAT))
+ params.uastcHDRLambda = *(float*)Q->data();
+ if (auto Q = m_spec.find_attribute("ktx:uastcHDRLevel",
+ TypeDesc::UINT32))
+ params.uastcHDRLevel = *(uint32_t*)Q->data();
+ }
+
+ // Params that apply to both ETC1S and UASTC
+ if (auto Q = m_spec.find_attribute("ktx:noSSE", TypeDesc::INT))
+ params.noSSE = static_cast(Q->get_int());
+ if (auto Q = m_spec.find_attribute("ktx:normalMap", TypeDesc::INT))
+ params.normalMap = static_cast(Q->get_int());
+ if (auto Q = m_spec.find_attribute("ktx:inputSwizzle",
+ TypeDesc(TypeDesc::CHAR, 4)))
+ memcpy(params.inputSwizzle, Q->data(), 4);
+ if (auto Q = m_spec.find_attribute("ktx:preSwizzle", TypeDesc::INT))
+ params.preSwizzle = static_cast(Q->get_int());
+
+ return true;
+}
+
+
+
+bool
+KtxOutput::write_ktx2()
+{
+ ///
+ /// KTX2 texture RAII'fied via a unique_ptr.
+ /// Q. Why not create this in open()?
+ /// A. open() can be called with AppendMIPLevel mode which means we don't
+ /// actually know the number of miplevels to create this texture with
+ /// until a call to close(). libktx doesn't support changing texture
+ /// attributes after its creation.
+ ///
+ std::unique_ptr tex {
+ nullptr, ktxTexture2_Destroy
+ };
+
+ OIIO_ASSERT(
+ m_vkformat != VK_FORMAT_UNDEFINED
+ && "VkFormat should never be VK_FORMAT_UNDEFINED when creating a KTX2 texture");
+ ktxTextureCreateInfo create_info;
+ create_info.glInternalformat = 0; // Ignored as this is not a KTX1 texture
+ create_info.vkFormat = m_vkformat;
+ create_info.pDfd = nullptr;
+ create_info.baseWidth = m_basewidth;
+ create_info.baseHeight = m_baseheight;
+ create_info.baseDepth = m_basedepth;
+ create_info.numDimensions = m_basedepth > 1 ? 3u : 2u;
+ create_info.numLevels = m_miplevel_idx + 1;
+ create_info.numLayers = 1; // Can't support this with current OIIO API
+ create_info.numFaces = 1; // TODO
+ create_info.isArray = KTX_FALSE; // Can't support this with current OIIO API
+ create_info.generateMipmaps = m_generate_mipmaps;
+
+ DBG std::cout << "calling ktxTexture2_Create with: "
+ << "vkFormat=" << create_info.vkFormat << "; "
+ << "baseWidth=" << create_info.baseWidth << "; "
+ << "baseHeight=" << create_info.baseHeight << "; "
+ << "baseDepth=" << create_info.baseDepth << "; "
+ << "numDimensions=" << create_info.numDimensions << "; "
+ << "numLevels=" << create_info.numLevels << "; "
+ << "numLayers=" << create_info.numLayers << "; "
+ << "numFaces=" << create_info.numFaces << "; "
+ << "isArray=" << create_info.isArray << "; "
+ << "generateMipmaps=" << create_info.generateMipmaps << "; "
+ << std::endl;
+
+ ktxTexture2* p_tex = nullptr;
+ auto result = ktxTexture2_Create(&create_info,
+ KTX_TEXTURE_CREATE_ALLOC_STORAGE, &p_tex);
+ tex.reset(p_tex);
+
+ if (result != KTX_SUCCESS) {
+ errorfmt("ktxTexture2_Create returnned ktx_error_code: {}",
+ static_cast(result));
+ return false;
+ }
+
+ DBG std::cout << "ktxTexture2_Create created texture successfully"
+ << std::endl;
+
+ //
+ // At first, set uncompressed data for all miplevels, layers, slices, etc.
+ // The loop over slices and the loop over faces are mutually exclusive
+ // (i.e., if tex->numLayers > 1 then tex->numFaces == 1, and vice versa).
+ //
+ // Note 1:
+ // You may notice the `generateMipmaps` flag in the ktxTexture struct, it
+ // is just used to instruct Vulkan or OpenGL to generate mipmaps for the
+ // texture to be uploaded NOT for mipmap generation on the CPU.
+ //
+ // Important:
+ // If the VkFormat related to the KTX texture creation is wrongly set, this
+ // will cause a segfault!
+ //
+ for (uint32_t level_idx = 0; level_idx < tex->numLevels; ++level_idx) {
+ const uint32_t depth = std::max(tex->baseDepth >> level_idx, 1u);
+ // Since array layers are not supported, this loop will only execute once
+ for (ktx_uint32_t layer_idx = 0; layer_idx < tex->numLayers;
+ ++layer_idx) {
+ for (uint32_t face_idx = 0; face_idx < tex->numFaces; ++face_idx) {
+ for (uint32_t slice_idx = 0; slice_idx < depth; ++slice_idx) {
+ // Faces and Slices are mutually exclusive, addition is fine
+ auto data_ptr
+ = m_imgs[level_idx][slice_idx + face_idx].data();
+ const size_t data_size
+ = m_imgs[level_idx][slice_idx + face_idx].size();
+
+ // Before anything, be absolutely certain that what we are about
+ // to write is of the exact same size (in bytes) of what libktx
+ // expects us to write for this mip level.
+ const size_t expected_size
+ = ktxTexture2_GetImageSize(tex.get(), level_idx);
+ if (data_size != expected_size) {
+ errorfmt(
+ "libktx expects {} bytes to be written for this mip level {} but {} bytes are instead attempted to be written",
+ expected_size, level_idx, data_size);
+ return false;
+ }
+
+ auto status = ktxTexture_SetImageFromMemory(
+ (ktxTexture*)tex.get(), level_idx, 0,
+ face_idx + slice_idx, data_ptr, data_size);
+ if (status != KTX_SUCCESS) {
+ errorfmt(
+ "ktxTexture_SetImageFromMemory returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+ DBG std::cout << fmt::format(
+ "ktxTexture_SetImageFromMemory for slice_idx={} face_idx={} level_idx={} wrote {} bytes",
+ slice_idx, face_idx, level_idx, data_size)
+ << std::endl;
+ } // slices
+ } // faces
+ } // layers
+ } // mip levels
+
+
+ //
+ // After having written all necessary uncompressed data, check if the
+ // texture is expected to be compressed (i.e., data should be compressed
+ // using some specified GPU-block-compression format). If so, compress
+ // said data using libktx.
+ //
+ if (m_cmp != BlockCompression::NONE) {
+ switch (m_cmp) {
+ // ASTC
+ case BlockCompression::ASTC: {
+ // TODO: expose ASTC compression quality parameter as spec attribute
+ if (auto status = ktxTexture2_CompressAstcEx(tex.get(),
+ &m_astc_params);
+ status != KTX_SUCCESS) {
+ errorfmt(
+ "ktxTexture2_CompressAstc returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+ break;
+ }
+#if 0
+ // BCn
+ case BlockCompression::BC1:
+ case BlockCompression::BC3:
+ case BlockCompression::BC4:
+ case BlockCompression::BC5:
+ case BlockCompression::BC7:
+ // TODO: expose BCn compression quality parameter as spec attribute
+ if (auto status = ktxTexture2_CompressBCn(tex, nullptr);
+ status != KTX_SUCCESS) {
+ errorfmt("ktxTexture2_CompressBCn returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+#endif
+ default:
+ errorfmt("Writing/Encoding {} compression is not supported",
+ block_compression_name(m_cmp));
+ return false;
+ }
+ }
+
+ //
+ // If a Basis Universal compression is requested (i.e., to BasisLZ/ETC1S
+ // or UASTC), compress the texture before writing. This is mutually
+ // exclusive with the isCompressed check above (i.e., you can't have a
+ // texture that is compressed using some GPU-block-format that also uses
+ // some Basis Universal latent format).
+ //
+ if (m_use_basis_universal) {
+ if (auto status = ktxTexture2_CompressBasisEx(tex.get(),
+ &m_basis_params);
+ status != KTX_SUCCESS) {
+ errorfmt("ktxTexture2_CompressBasisEx returned error code: ",
+ static_cast(status));
+ return false;
+ }
+ }
+
+ //
+ // Finally, apply the supercompression scheme (if any). Supercompression
+ // can be applied (especially if RDO is used with BCn/ASTC) to significantly
+ // reduce disk file size at the expense of additional CPU data load time
+ // (i.e., data now has to be inflated before being uploaded to the GPU).
+ //
+ if (m_superCmp == KTX_SS_ZLIB) {
+ if (auto status = ktxTexture2_DeflateZLIB(tex.get(), m_zlib_level);
+ status != KTX_SUCCESS) {
+ errorfmt("ktxTexture2_DeflateZLIB returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+ } else if (m_superCmp == KTX_SS_ZSTD) {
+ if (auto status = ktxTexture2_DeflateZstd(tex.get(), m_zstd_level);
+ status != KTX_SUCCESS) {
+ errorfmt("ktxTexture2_DeflateZstd returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+ }
+
+ //
+ // Now write key/value data (KVD). Ideally, we should follow the KTX2 spec
+ // about which KVD we are encouraged to write and also follow same process
+ // as KTX-Software (e.g., write used mipmap filter in KTXScWriterParams
+ // entry, write KTXWriter fields, etc.)
+ //
+
+ // Add/overwrite the KTXwriter metadata entry. The specs encourages us to do
+ // so.
+ char writer[100];
+ snprintf(writer, sizeof(writer), "oiio version %d - plugin version %d",
+ OPENIMAGEIO_VERSION, OIIO_PLUGIN_VERSION);
+ ktxHashList_AddKVPair(&tex->kvDataHead, KTX_WRITER_KEY,
+ (ktx_uint32_t)strlen(writer) + 1, writer);
+ // std::cout << "KTXwrite: " << writer << '\n';
+
+
+ Filesystem::IOProxy* m_io = ioproxy();
+ if (!strcmp(m_io->proxytype(), "file")) {
+ auto fd = reinterpret_cast(m_io)->handle();
+ if (auto status = ktxTexture2_WriteToStdioStream(tex.get(), fd);
+ status != KTX_SUCCESS) {
+ errorfmt(
+ "ktxTexture2_WriteToStdioStream returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+ return true;
+ }
+
+ if (!strcmp(m_io->proxytype(), "vecoutput")) {
+ auto proxy = reinterpret_cast(m_io);
+ ktx_uint8_t* buff;
+ ktx_size_t buff_size;
+ if (auto status = ktxTexture2_WriteToMemory(tex.get(), &buff,
+ &buff_size);
+ status != KTX_SUCCESS) {
+ errorfmt(
+ "ktxTexture2_WriteToMemory returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+ // Cleanup when we go out of scope or on exception (make sure to use
+ // matching deallocator, i.e., free())
+ auto _ = std::unique_ptr(buff,
+ std::free);
+ proxy->write(buff, buff_size);
+ return true;
+ }
+
+ // OIIO should guarantee that this never happens
+ errorfmt("unexpected IOProxy type: {}", m_io->proxytype());
+ return false;
+}
+
+OIIO_PLUGIN_NAMESPACE_END
diff --git a/src/libOpenImageIO/imageioplugin.cpp b/src/libOpenImageIO/imageioplugin.cpp
index b63b51f14f..17aa1c6a5d 100644
--- a/src/libOpenImageIO/imageioplugin.cpp
+++ b/src/libOpenImageIO/imageioplugin.cpp
@@ -305,6 +305,7 @@ PLUGENTRY(tiff);
PLUGENTRY(targa);
PLUGENTRY(webp);
PLUGENTRY(zfile);
+PLUGENTRY(ktx);
#endif // defined(EMBED_PLUGINS)
@@ -439,6 +440,9 @@ catalog_builtin_plugins()
#if !defined(DISABLE_ZFILE)
DECLAREPLUG (zfile);
#endif
+#if !defined(DISABLE_KTX)
+ DECLAREPLUG (ktx);
+#endif
#endif
}
// clang-format on
diff --git a/testsuite/ktx/ref/out.txt b/testsuite/ktx/ref/out.txt
new file mode 100644
index 0000000000..88e011da0d
--- /dev/null
+++ b/testsuite/ktx/ref/out.txt
@@ -0,0 +1,315 @@
+Reading ../oiio-images/ktx2/r8g8b8a8_srgb.ktx2
+../oiio-images/ktx2/r8g8b8a8_srgb.ktx2 : 128 x 128, 4 channel, uint8 ktx
+ SHA-1: 827AAFAEA2C443CE1B6DD30196163B0E01B4B816
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/r8g8b8a8_srgb_mip.ktx2
+../oiio-images/ktx2/r8g8b8a8_srgb_mip.ktx2 : 64 x 64, 4 channel, uint8 ktx
+ MIP-map levels: 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: 03052CEA1E271CEB85C2A084E44DC23F5BD7345C
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:miplevels: 7
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/r8g8b8_srgb_mip.ktx2
+../oiio-images/ktx2/r8g8b8_srgb_mip.ktx2 : 64 x 64, 3 channel, uint8 ktx
+ MIP-map levels: 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: BE1D5EA24E907A4C4B3FB3C28EAC872A20F5B414
+ channel list: R, G, B
+ textureformat: "Plain Texture"
+ ktx:miplevels: 7
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/r8g8b8a8_srgb_3d_7.ktx2
+../oiio-images/ktx2/r8g8b8a8_srgb_3d_7.ktx2 : 16 x 16 x 7, 4 channel, volume uint8 ktx
+ SHA-1: B25CE648E0293F6B8C5D0433295EFDC7182AA0C6
+ channel list: R, G, B, A
+ textureformat: "Volume Texture"
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/r8g8b8a8_srgb_array_7_mip.ktx2
+../oiio-images/ktx2/r8g8b8a8_srgb_array_7_mip.ktx2 : 16 x 16, 4 channel, uint8 ktx
+ 7 subimages: 16x16 [u8,u8,u8,u8], 16x16 [u8,u8,u8,u8], 16x16 [u8,u8,u8,u8], 16x16 [u8,u8,u8,u8], 16x16 [u8,u8,u8,u8], 16x16 [u8,u8,u8,u8], 16x16 [u8,u8,u8,u8]
+ subimage 0: 16 x 16, 4 channel, uint8 ktx
+ MIP-map levels: 16x16 8x8 4x4 2x2 1x1
+ SHA-1: A324F1AFE01140C4D58618875715EE545B9F1ECC
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:miplevels: 5
+ ktx:nlayers: 7
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+ subimage 1: 16 x 16, 4 channel, uint8 ktx
+ MIP-map levels: 16x16 8x8 4x4 2x2 1x1
+ SHA-1: 710B4D8675FE67BFE9E6897E0514EBCB2255385F
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:miplevels: 5
+ ktx:nlayers: 7
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+ subimage 2: 16 x 16, 4 channel, uint8 ktx
+ MIP-map levels: 16x16 8x8 4x4 2x2 1x1
+ SHA-1: CCB0462997F53FCDAFD94FA2E6C862E0D2CA022A
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:miplevels: 5
+ ktx:nlayers: 7
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+ subimage 3: 16 x 16, 4 channel, uint8 ktx
+ MIP-map levels: 16x16 8x8 4x4 2x2 1x1
+ SHA-1: CF06A8313BD1A85F6677F1CA7915C3CFEF954C6E
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:miplevels: 5
+ ktx:nlayers: 7
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+ subimage 4: 16 x 16, 4 channel, uint8 ktx
+ MIP-map levels: 16x16 8x8 4x4 2x2 1x1
+ SHA-1: 54034063DBC1C1BB61CB60EC57E4498678DC2B13
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:miplevels: 5
+ ktx:nlayers: 7
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+ subimage 5: 16 x 16, 4 channel, uint8 ktx
+ MIP-map levels: 16x16 8x8 4x4 2x2 1x1
+ SHA-1: 408D3A129857F4942CA5A8D10CBA2D51C5F01901
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:miplevels: 5
+ ktx:nlayers: 7
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+ subimage 6: 16 x 16, 4 channel, uint8 ktx
+ MIP-map levels: 16x16 8x8 4x4 2x2 1x1
+ SHA-1: 2966B466A5D0AFB2531AEBFEDB7304D560DC3D74
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:miplevels: 5
+ ktx:nlayers: 7
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/color_grid_zstd_5.ktx2
+../oiio-images/ktx2/color_grid_zstd_5.ktx2 : 1024 x 1024, 4 channel, uint8 ktx
+ SHA-1: 4964D0A088333B6C31CD3BA19A2B8CC1CE879DA8
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:KTXwriterScParams: "--zstd 5"
+ ktx:supercompressionscheme: 2
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/r8g8b8a8_srgb_mip_astc.ktx2
+../oiio-images/ktx2/r8g8b8a8_srgb_mip_astc.ktx2 : 64 x 64, 4 channel, uint8 ktx
+ MIP-map levels: 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: 03052CEA1E271CEB85C2A084E44DC23F5BD7345C
+ channel list: R, G, B, A
+ compression: "ASTC"
+ textureformat: "Plain Texture"
+ ktx:miplevels: 7
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/ktx_app_astc_8x8.ktx2
+../oiio-images/ktx2/ktx_app_astc_8x8.ktx2 : 1024 x 1024, 4 channel, uint8 ktx
+ MIP-map levels: 1024x1024 512x512 256x256 128x128 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: 1EF0757CD38CA8D9AF19EDF0B961FF7D5C635274
+ channel list: R, G, B, A
+ compression: "ASTC"
+ textureformat: "Plain Texture"
+ ktx:miplevels: 11
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/color_grid_uastc_zstd_5.ktx2
+../oiio-images/ktx2/color_grid_uastc_zstd_5.ktx2 : 1024 x 1024, 4 channel, uint8 ktx
+ SHA-1: B631629A3A81C50B2CE264D8CCA4EC4E169DFD6E
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:KTXwriterScParams: "--zstd 5"
+ ktx:supercompressionscheme: 2
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/Iron_Bars_001_normal_uastc_zstd_10.ktx2
+../oiio-images/ktx2/Iron_Bars_001_normal_uastc_zstd_10.ktx2 : 1024 x 1024, 4 channel, uint8 ktx
+ MIP-map levels: 1024x1024 512x512 256x256 128x128 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: D428118F2CD6DE70F04C9BD6D092A5A5DCAB195D
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:KTXwriterScParams: "--normal-mode --zstd 10"
+ ktx:miplevels: 11
+ ktx:supercompressionscheme: 2
+ ktx:version: "2.0"
+ oiio:ColorSpace: "lin_rec709_scene"
+Reading ../oiio-images/ktx2/ktx_document_uastc_rdo_4_zstd_5.ktx2
+../oiio-images/ktx2/ktx_document_uastc_rdo_4_zstd_5.ktx2 : 1024 x 1024, 4 channel, uint8 ktx
+ MIP-map levels: 1024x1024 512x512 256x256 128x128 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: 8644BD94DE7A340007640D0063DF80FC2B744849
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:KTXwriterScParams: "--uastc-rdo --uastc-rdo-l 4 --threads 1 --zstd 5"
+ ktx:miplevels: 11
+ ktx:supercompressionscheme: 2
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/cubemap_goldengate_uastc_rdo_4_zstd_5.ktx2
+../oiio-images/ktx2/cubemap_goldengate_uastc_rdo_4_zstd_5.ktx2 : 1024 x 1024, 4 channel, uint8 ktx
+ MIP-map levels: 1024x1024 512x512 256x256 128x128 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: 730E2C2DABA5D591458C91C4D952638BC4EAADD6
+ channel list: R, G, B, A
+ textureformat: "CubeFace Environment"
+ ktx:KTXwriterScParams: "--uastc-rdo --uastc-rdo-l 4 --zstd 5"
+ ktx:miplevels: 11
+ ktx:nfaces: 6
+ ktx:supercompressionscheme: 2
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/Desk_small_zstd_15.ktx2
+../oiio-images/ktx2/Desk_small_zstd_15.ktx2 : 161 x 218, 3 channel, half ktx
+ SHA-1: 547517CC24207ECC0A73486D5224697E3FC38AB6
+ channel list: R, G, B
+ textureformat: "Plain Texture"
+ ktx:KTXwriterScParams: "--zstd 15"
+ ktx:supercompressionscheme: 2
+ ktx:version: "2.0"
+ oiio:ColorSpace: "lin_rec709_scene"
+Reading ../oiio-images/ktx2/Desk_uastc_hdr4x4_zstd_15.ktx2
+../oiio-images/ktx2/Desk_uastc_hdr4x4_zstd_15.ktx2 : 644 x 874, 4 channel, half ktx
+ SHA-1: BD3B955A6D2F724D306F9AF402BBCA15DCFECC20
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:KTXwriterScParams: "--uastc-hdr-ultra-quant true --zstd 15"
+ ktx:supercompressionscheme: 2
+ ktx:version: "2.0"
+ oiio:ColorSpace: "lin_rec709_scene"
+Reading ../oiio-images/ktx2/Desk_uastc_hdr6x6i.ktx2
+../oiio-images/ktx2/Desk_uastc_hdr6x6i.ktx2 : 644 x 874, 4 channel, half ktx
+ SHA-1: 8E0DE70C334CFA0A84AC7FE815F7066DFAF1413B
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:KTXwriterScParams: "--uastc-hdr-lambda 300"
+ ktx:supercompressionscheme: 4
+ ktx:version: "2.0"
+ oiio:ColorSpace: "lin_rec709_scene"
+Reading ../oiio-images/ktx2/kodim17_blze.ktx2
+../oiio-images/ktx2/kodim17_blze.ktx2 : 512 x 768, 4 channel, uint8 ktx
+ SHA-1: 4313438D8827BD8CB040EBD28A27A64EC7B62F68
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:supercompressionscheme: 1
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/r8g8b8a8_srgb_mip_blze.ktx2
+../oiio-images/ktx2/r8g8b8a8_srgb_mip_blze.ktx2 : 64 x 64, 4 channel, uint8 ktx
+ MIP-map levels: 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: A6350253C75FF2B9E24C35E89381C23EC1A77D29
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:miplevels: 7
+ ktx:supercompressionscheme: 1
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/color_grid_blze.ktx2
+../oiio-images/ktx2/color_grid_blze.ktx2 : 1024 x 1024, 4 channel, uint8 ktx
+ SHA-1: 8C159DD2A2DC4FF1B2D6B84A0DF4E925A0FCBF46
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:supercompressionscheme: 1
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/alpha_simple_blze.ktx2
+../oiio-images/ktx2/alpha_simple_blze.ktx2 : 8 x 8, 4 channel, uint8 ktx
+ SHA-1: 23BD7228F104BBA18152E7FDC50825286379F581
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:supercompressionscheme: 1
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/cubemap_yokohama_blze.ktx2
+../oiio-images/ktx2/cubemap_yokohama_blze.ktx2 : 2048 x 2048, 4 channel, uint8 ktx
+ MIP-map levels: 2048x2048 1024x1024 512x512 256x256 128x128 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: E1D8635C6E17878F937840E2DA2E8F5A1F61F501
+ channel list: R, G, B, A
+ textureformat: "CubeFace Environment"
+ ktx:miplevels: 12
+ ktx:nfaces: 6
+ ktx:supercompressionscheme: 1
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/FlightHelmet_baseColor_blze.ktx2
+../oiio-images/ktx2/FlightHelmet_baseColor_blze.ktx2 : 2048 x 2048, 4 channel, uint8 ktx
+ SHA-1: FD4B9268979E2925598D7D3FF663293FF3807926
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:supercompressionscheme: 1
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/Iron_Bars_001_normal_blze.ktx2
+../oiio-images/ktx2/Iron_Bars_001_normal_blze.ktx2 : 1024 x 1024, 4 channel, uint8 ktx
+ MIP-map levels: 1024x1024 512x512 256x256 128x128 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: 4B447C9281806EBDC73619B0622F7F3CCC317F05
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:KTXwriterScParams: "--normal-mode"
+ ktx:miplevels: 11
+ ktx:supercompressionscheme: 1
+ ktx:version: "2.0"
+ oiio:ColorSpace: "lin_rec709_scene"
+Reading ../oiio-images/ktx2/ktx_document_blze.ktx2
+../oiio-images/ktx2/ktx_document_blze.ktx2 : 1024 x 1024, 4 channel, uint8 ktx
+ MIP-map levels: 1024x1024 512x512 256x256 128x128 64x64 32x32 16x16 8x8 4x4 2x2 1x1
+ SHA-1: AE251370FDCA642A65531BAC14C939A2EC369DC5
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:miplevels: 11
+ ktx:supercompressionscheme: 1
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/alpha_complex_straight.ktx2
+../oiio-images/ktx2/alpha_complex_straight.ktx2 : 256 x 256, 4 channel, uint8 ktx
+ SHA-1: 4DC3A14E38526B307226E2DDE083608891EBD27F
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:KTXwriterScParams: "--zstd 10"
+ ktx:supercompressionscheme: 2
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/orient_down_metadata.ktx2
+../oiio-images/ktx2/orient_down_metadata.ktx2 : 128 x 128, 4 channel, uint8 ktx
+ SHA-1: BAA0E4B9FC815F0CCB235CA0E05E4EFE918FB3EC
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/orient_up_metadata.ktx2
+../oiio-images/ktx2/orient_up_metadata.ktx2 : 128 x 128, 4 channel, uint8 ktx
+ SHA-1: A446B2FB2A0D391584A2FE45E9F24BE35D896CAA
+ channel list: R, G, B, A
+ textureformat: "Plain Texture"
+ ktx:supercompressionscheme: 0
+ ktx:version: "2.0"
+ oiio:ColorSpace: "srgb_rec709_scene"
+Comparing "checker_original.png" and "checker_default.ktx2"
+PASS
+Comparing "checker_original.png" and "checker_uastc.ktx2"
+PASS
+Comparing "checker_original.png" and "checker_etc1s.ktx2"
+PASS
diff --git a/testsuite/ktx/run.py b/testsuite/ktx/run.py
new file mode 100644
index 0000000000..f649e2e775
--- /dev/null
+++ b/testsuite/ktx/run.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+
+# Copyright Contributors to the OpenImageIO project.
+# SPDX-License-Identifier: BSD-3-Clause and Apache-2.0
+# https://github.com/AcademySoftwareFoundation/OpenImageIO
+
+# All of the test files here are copied, as is, from KTX-Software repo and fall
+# under the Apache-2.0 license of KTX-Software:
+#
+# Copyright 2013-2020 Mark Callow SPDX-License-Identifier: Apache-2.0
+
+#
+# KTX-Software has two sets of ktx2 test files:
+# - a relatively small set for libktx: https://github.com/KhronosGroup/KTX-Software/tree/e2f948066c108b56b8d0052b460b2ac7d34886aa/tests/resources/ktx2
+# - a very larget tests set for ktx tools: https://github.com/KhronosGroup/KTX-Software-CTS/tree/6d23ae9e52cce2ebc6495c4692ec89f632ff70d4
+#
+# commit hashse:
+# - libktx test files: 6c474d8627999de8acf07d819c196f83d025cd44
+# - ktx tools test files (CTS): 6d23ae9e52cce2ebc6495c4692ec89f632ff70d4
+#
+# Since OIIO KTX2 plugin simply forwards all operations to libktx, there is no
+# need to do extensive testing on encoding/decoding functionalities. libktx
+# already does very extensive testing on thousands of ktx2 inputs. What we do
+# instead is that we test that we call libktx correctly and that parameters
+# (which are numerous) are passed correctly.
+#
+
+# save the error output
+redirect = ' >> out.txt 2>&1 '
+files = [
+
+ # raw (uncompressed + non-supercompressed) formats
+ "r8g8b8a8_srgb.ktx2",
+ "r8g8b8a8_srgb_mip.ktx2",
+ "r8g8b8_srgb_mip.ktx2",
+ "r8g8b8a8_srgb_3d_7.ktx2",
+ "r8g8b8a8_srgb_array_7_mip.ktx2",
+
+ # ETC-compressed formats (not supported)
+ # "r8g8b8a8_srgb_mip_etc2.ktx2",
+ # "etc2_unorm_array_7.ktx2",
+
+ # raw (uncompressed) and supercompressed formats
+ "color_grid_zstd_5.ktx2",
+ # "skybox_zstd_22.ktx2", # VK_FORMAT_B10G11R11_UFLOAT_PACK32 not yet supported
+
+ # ASTC-compressed formats
+ "r8g8b8a8_srgb_mip_astc.ktx2",
+ "ktx_app_astc_8x8.ktx2",
+ # "astc_8x8_unorm_array_7.ktx2", # VK_FORMAT_ASTC_8x8_SFLOAT_BLOCK
+
+ # BCn-compressed formats (to be supported)
+ # "bc3_unorm_array_7.ktx2",
+ # "pattern_02_bc2.ktx2",
+
+ # UASTC formats (widely used within KTX2 container format)
+ "color_grid_uastc_zstd_5.ktx2",
+ "Iron_Bars_001_normal_uastc_zstd_10.ktx2",
+ "ktx_document_uastc_rdo_4_zstd_5.ktx2",
+ "cubemap_goldengate_uastc_rdo_4_zstd_5.ktx2",
+
+ # HDR formats
+ "Desk_small_zstd_15.ktx2", # VK_FORMAT_R16G16B16_SFLOAT
+ "Desk_uastc_hdr4x4_zstd_15.ktx2",
+ "Desk_uastc_hdr6x6i.ktx2",
+ # "Desk_astc_hdr6x6.ktx2", # VK_FORMAT_ASTC_6x6_SFLOAT_BLOCK
+
+ # Basis LZ/ETC1S formats (widely used within KTX2 container format)
+ "kodim17_blze.ktx2",
+ "r8g8b8a8_srgb_mip_blze.ktx2",
+ "color_grid_blze.ktx2",
+ "alpha_simple_blze.ktx2",
+ "cubemap_yokohama_blze.ktx2",
+ "FlightHelmet_baseColor_blze.ktx2",
+ "Iron_Bars_001_normal_blze.ktx2",
+ "ktx_document_blze.ktx2",
+
+ # Misc (orientation flags, alpha configurations, etc.)
+ "alpha_complex_straight.ktx2",
+ "orient_down_metadata.ktx2",
+ "orient_up_metadata.ktx2",
+]
+
+for f in files:
+ # Just test `oiiotool --info` on libktx main test files
+ command += info_command (OIIO_TESTSUITE_IMAGEDIR + "/" + f)
+
+# Create a simple checker pattern RGBA PNG (has to be RGBA because Basis
+# Universal codecs cannot transcode to opaque uncompressed formats)
+command += (oiio_app("oiiotool")
+ + " --pattern checker 64x64 4 -d uint8 -o checker_original.png >> out.txt ;\n")
+
+# Default write (with nothing specified) should default to a lossless format
+# + supercompression scheme and should match exactly with original input
+command += oiiotool ("checker_original.png -o checker_default.ktx2")
+command += diff_command ("checker_original.png", "checker_default.ktx2", "--fail 0 --warn 0")
+
+# UASTC write test: check generation of an UASTC-based KTX2 file
+command += oiiotool ("checker_original.png --attrib ktx:codec uastc -o checker_uastc.ktx2")
+command += diff_command ("checker_original.png", "checker_uastc.ktx2")
+
+# ETC1S write test: check generation of an ETC1S-based KTX2 file
+command += oiiotool ("checker_original.png --attrib ktx:codec etc1s -o checker_etc1s.ktx2")
+command += diff_command ("checker_original.png", "checker_etc1s.ktx2")
+
+# We do not test read-write of compressed-ktx2 files because any read-write
+# cycle worsens quality and is absolutely not the intended purpose of ktx usage
+# within OIIO (or ktx usage in general).