diff --git a/src/cmake/build_Ktx.cmake b/src/cmake/build_Ktx.cmake
new file mode 100644
index 0000000000..70d8735c7c
--- /dev/null
+++ b/src/cmake/build_Ktx.cmake
@@ -0,0 +1,59 @@
+# Copyright Contributors to the OpenImageIO project.
+# SPDX-License-Identifier: Apache-2.0
+# https://github.com/AcademySoftwareFoundation/OpenImageIO
+
+set_cache (Ktx_BUILD_VERSION v5.0.0-rc1 "Ktx version for local builds")
+set (Ktx_GIT_REPOSITORY "https://github.com/KhronosGroup/KTX-Software.git")
+set (Ktx_GIT_TAG "${Ktx_BUILD_VERSION}")
+set (Ktx_GIT_COMMIT "6269d2752ed04446c2d4749f54f3aad4f94555b5")
+set_cache (Ktx_BUILD_SHARED_LIBS OFF ${LOCAL_BUILD_SHARED_LIBS_DEFAULT}
+ DOC "Should a local Ktx build, if necessary, build shared libraries" ADVANCED)
+
+string (MAKE_C_IDENTIFIER ${Ktx_BUILD_VERSION} Ktx_VERSION_IDENT)
+
+# 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.
+build_dependency_with_cmake(Ktx
+ VERSION ${Ktx_BUILD_VERSION}
+ GIT_REPOSITORY ${Ktx_GIT_REPOSITORY}
+ GIT_TAG ${Ktx_GIT_TAG}
+ GIT_COMMIT ${Ktx_GIT_COMMIT}
+ # lib only contains CMakeLists.txt from tag v5.0.0 but that requires CMake min version 3.23
+ # which in turn causes the CI to fail. Just give up and build the whole thing...
+ SOURCE_SUBDIR lib # To only build Ktx, cmake has to point to: KTX-Software/lib
+ CMAKE_ARGS
+ -D BUILD_SHARED_LIBS=${Ktx_BUILD_SHARED_LIBS}
+ -D CMAKE_INSTALL_LIBDIR=lib
+ -D CMAKE_POSITION_INDEPENDENT_CODE=ON
+ -D LIBKTX_VERSION_READ_ONLY=OFF
+ -D LIBKTX_VERSION_FULL=ON
+ -D LIBKTX_FEATURE_KTX1=ON # Setting this to OFF causes linker issues
+ -D LIBKTX_FEATURE_KTX2=ON
+ -D LIBKTX_FEATURE_VK_UPLOAD=OFF
+ -D LIBKTX_FEATURE_GL_UPLOAD=OFF
+ -D LIBKTX_FEATURE_ETC_UNPACK=OFF # This has some weird licensing and I don't feel comfortable including it ...
+ # as per KTX-Software:
+ # > Intel Macs have support for SSE, but if you're building universal
+ # > binaries, you have to disable SSE or the build will fail.
+ )
+
+# Set some things up that we'll need for a subsequent find_package to work
+set (Ktx_ROOT ${Ktx_LOCAL_INSTALL_DIR})
+set (Ktx_DIR ${Ktx_LOCAL_INSTALL_DIR}/lib/cmake/ktx)
+
+# Signal to caller that we need to find again at the installed location
+# set (Ktx_REFIND TRUE)
+# set (Ktx_REFIND_ARGS CONFIG)
+
+find_package (Ktx CONFIG REQUIRED
+ HINTS
+ ${Ktx_LOCAL_INSTALL_DIR}/lib/cmake/ktx/
+ ${Ktx_LOCAL_INSTALL_DIR}
+ )
+
+if (Ktx_BUILD_SHARED_LIBS)
+ # install_local_dependency_libs (pkgname libname)
+ install_local_dependency_libs (Ktx ktx) # notice libname is lowercase
+endif ()
diff --git a/src/cmake/externalpackages.cmake b/src/cmake/externalpackages.cmake
index 8739565fcb..401145e5db 100644
--- a/src/cmake/externalpackages.cmake
+++ b/src/cmake/externalpackages.cmake
@@ -259,6 +259,11 @@ else ()
get_target_property(FMT_INCLUDE_DIR fmt::fmt-header-only INTERFACE_INCLUDE_DIRECTORIES)
endif ()
+# Ktx for KTX textures
+checked_find_package (Ktx
+ VERSION_MIN 5.0.0
+ BUILD_LOCAL missing
+)
###########################################################################
diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake
index 93aa7345e6..c0a7a85606 100644
--- a/src/cmake/testing.cmake
+++ b/src/cmake/testing.cmake
@@ -336,6 +336,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")
@@ -539,9 +542,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})
+ BRANCH main)
oiio_get_test_data (openexr-images
REPO https://github.com/AcademySoftwareFoundation/openexr-images.git
BRANCH main)
diff --git a/src/ktx.imageio/CMakeLists.txt b/src/ktx.imageio/CMakeLists.txt
new file mode 100644
index 0000000000..a035da5584
--- /dev/null
+++ b/src/ktx.imageio/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Copyright Contributors to the OpenImageIO project.
+# SPDX-License-Identifier: Apache-2.0
+# https://github.com/AcademySoftwareFoundation/OpenImageIO
+
+if (Ktx_FOUND)
+ add_oiio_plugin (ktxinput.cpp ktxoutput.cpp LINK_LIBRARIES KTX::ktx)
+else ()
+ message (WARNING "KTX plugin will not be built, no libktx")
+endif ()
diff --git a/src/ktx.imageio/README.md b/src/ktx.imageio/README.md
new file mode 100644
index 0000000000..adfd9e3df4
--- /dev/null
+++ b/src/ktx.imageio/README.md
@@ -0,0 +1,207 @@
+# 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 format before uploading). That being said, this
+plugin is 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.
+
+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 tow
+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 me some time to understand how libktx works and what it provides
+(and why). Some terminology is also defined here.
+
+## KTX2 - Brief Introduction
+
+KTX2 (the 2 here is to distinguish it from deprecated KTX/KTX1) is a binary
+container format that is intended for usage for fast loading of textures to the
+GPU. KTX2 contains GPU-native formats (e.g., block-compressed format BC7) with
+an optional additional layer of compression (hereafter referred to as
+*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 mip levels.
+
+## 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 (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).
+
+### 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.
+
+## 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/)
+
+[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..eb7f193950
--- /dev/null
+++ b/src/ktx.imageio/ktx_pvt.h
@@ -0,0 +1,337 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+OIIO_PLUGIN_NAMESPACE_BEGIN
+
+// this is: "«KTX 20»\r\n\x1A\n"
+static const uint8_t KTX2_IDENTIFIER[12] { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32,
+ 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A };
+
+
+
+enum class TextureKind : uint32_t {
+ SINGLE_TEXTURE_1D,
+ SINGLE_TEXTURE_2D,
+ SINGLE_TEXTURE_3D,
+ CUBEMAP_TEXTURE,
+ ARRAY_TEXTURE_1D,
+ ARRAY_TEXTURE_2D,
+ ARRAY_TEXTURE_3D,
+ ARRAY_TEXTURE_CUBEMAP,
+};
+
+
+
+enum class BlockCompression : uint8_t {
+ NONE = 0,
+ BC1 = 1, ///< aka DXT1
+ BC2 = 2, ///< aka DXT3
+ BC3 = 3, ///< aka DXT5
+ BC4 = 4,
+ BC5 = 5,
+ BC6HU = 6,
+ BC6HS = 7,
+ BC7 = 8,
+ ETC1 = 9,
+ ETC2_RGB = 10,
+ ETC2_RGB_A1 = 11,
+ ETC2_RGBA = 12,
+ EAC_R11 = 13,
+ EAC_RG11 = 14,
+ ASTC = 15,
+};
+
+
+
+static inline std::string
+block_compression_name(BlockCompression cmp)
+{
+ switch (cmp) {
+ case BlockCompression::NONE: return "NONE";
+ case BlockCompression::BC1: return "BC1";
+ 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;
+};
+
+
+
+// TODO: VkFormat struct should be autogenerated via CMake and the Vulkan
+// version
+// Provided by VK_VERSION_1_0
+typedef 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,
+} VkFormat;
+
+
+
+struct FormatInfo {
+ int nbrchannels;
+ TypeDesc typedesc;
+ BlockCompression compression { BlockCompression::NONE };
+ VkFormat decompressed_format { VK_FORMAT_UNDEFINED };
+};
+
+
+
+//
+// Note:
+// Colorspace detection from VkFormat is simply wrong/not complete. You should
+// do colorspace detection via the color model and transfer function.
+// 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
+extract_info_from_format(VkFormat vkformat, FormatInfo& formatinfo)
+{
+ // clang-format off
+ switch (vkformat) {
+ // Raw, 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_SRGB: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::NONE }; return true;
+ case VK_FORMAT_R8G8B8A8_SRGB: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::NONE }; return true;
+ // ETC2 block-compressed formats
+ // TODO: decompress ETC2_RGB into RGB format (not RGBA). This requires some changes
+ 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_SRGB_BLOCK: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::BC1, VK_FORMAT_R8G8B8A8_SRGB }; 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_BC5_UNORM_BLOCK: formatinfo = { 2, TypeDesc::UINT8, BlockCompression::BC5, VK_FORMAT_R8G8_UNORM }; return true;
+ case VK_FORMAT_BC7_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC7, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+ // ASTC formats (2D blocks)
+ case VK_FORMAT_ASTC_4x4_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB }; return true;
+ default: break;
+ }
+ // clang-format on
+ return false;
+}
+
+
+
+// TODO
+inline void
+gl_to_vkformat()
+{
+}
+
+OIIO_PLUGIN_NAMESPACE_END
diff --git a/src/ktx.imageio/ktxinput.cpp b/src/ktx.imageio/ktxinput.cpp
new file mode 100644
index 0000000000..b6058b1e39
--- /dev/null
+++ b/src/ktx.imageio/ktxinput.cpp
@@ -0,0 +1,955 @@
+// Copyright Contributors to the OpenImageIO project.
+// SPDX-License-Identifier: Apache-2.0
+// https://github.com/AcademySoftwareFoundation/OpenImageIO
+
+#include
+#include
+
+// Per KTX-Software BUILDING.md:
+// > When linking to the static library, make sure to
+// > define `KHRONOS_STATIC` before including KTX header files.
+// > This is especially important on Windows.
+#ifndef BUILD_SHARED_LIBS
+# define KHRONOS_STATIC 1
+#endif
+
+#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 (
+ // as per the KTX1/2 specs:
+ // https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_keyvalue_data
+ feature == "arbitrary_metadata" ||
+ /* ktx supports 3D textures, cubmap textures, texture arrays, etc. */
+ feature == "multiimage" ||
+ /* ktx supports storage of mipmaps */
+ 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;
+
+ 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
+ {
+ lock_guard lock(*this);
+ return m_subimage;
+ }
+
+ int current_miplevel(void) const override
+ {
+ lock_guard lock(*this);
+ return m_miplevel;
+ }
+
+ bool seek_subimage(int subimage, int miplevel) override;
+
+private:
+ std::string m_filename;
+
+ /// Buffer to hold the decoded GPU-block-compressed format. This is only
+ /// used to hold BCn or ECT decompressed data for a particular
+ /// miplevel/subimage.
+ std::vector m_buf;
+
+ /// Non-owning pointer to KTX2 texture. The texture is managed by libktx
+ /// and should be destroyed via a 'ktxTexture_Destroy()' call.
+ ktxTexture* m_tex { nullptr };
+
+ /// m_tex2 reinterpret_cast'ed to KtxTexture2* for convenience.
+ ktxTexture2* m_tex2 { nullptr };
+
+ /// Non-owning pointer to first byte of the requested (miplevel, slice).
+ ///
+ /// For non-GPU-compressed formats, this points to first byte of the whole
+ /// texture data.
+ ///
+ /// For GPU-compressed formats:
+ /// - BCn: this is simply m_buf.data()
+ /// - ETC: this is simply m_buf.data()
+ /// - ASTC: this points to first byte of the whole decompressed texture
+ uint8_t* m_data_ptr { nullptr };
+
+ ktx_uint32_t m_pitch { 0 }; ///< Row pitch for current mip level.
+ ktx_size_t m_offset { 0 }; ///< Current offset from subimage call.
+ int m_subimage { -1 }; ///< What subimage are we looking at?
+ int m_nbrsubimages { -1 }; ///< Number of slices/faces in texture
+ int m_miplevel { -1 }; ///< What mip level are we looking at?
+ int m_nbrmiplevels { -1 }; ///< Number of mip levels
+
+ /// 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;
+
+ std::unique_ptr m_config; ///< Saved copy of configuration spec
+
+ /// TODO: add gl, direct3d, and metal format support
+ std::optional m_glFormat { std::nullopt };
+ std::optional m_dxgiFormat { std::nullopt };
+ std::optional m_metalFormat { std::nullopt };
+
+ /// Helper function: performs the actual pixel decoding.
+ bool internal_readimg(unsigned char* dst, int w, int h, int d);
+
+ bool ktx_magic_cmp(const uint8_t* KTX_MAGIC, const uint8_t* sig,
+ size_t start) const;
+
+ TextureKind get_texture_kind() const;
+
+ std::string get_colorspace() 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 v5.0.0-rc1"; } // hardcoded because I couldn't expose KTX_VERSION
+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)
+{
+ //
+ // OIIO API is limited for certain KTX texture types (e.g., 3D array
+ // textures, cubemap array textures). Therefore we add the option to specify
+ // which layer to use in case these textures are used. This is ignored for
+ // other types of textures (e.g., 2D array textures, cubemaps, etc.)
+ //
+ // m_array_layer_idx = config.get_int_attribute("ktx:ArrayLayerIndex",
+ // m_array_layer_idx);
+
+ // Check 'config' for any special requests
+ // if (config.get_int_attribute("oiio:UnassociatedAlpha", 0) == 1)
+ // m_keep_unassociated_alpha = true;
+ // m_linear_premult = config.get_int_attribute("png:linear_premult",
+ // OIIO::get_int_attribute(
+ // "png:linear_premult"));
+ ioproxy_retrieve_from_config(config);
+ m_config.reset(new ImageSpec(config)); // save config spec
+ return open(name, newspec);
+}
+
+
+
+/// Opens the file with given name and seek to the first subimage in the
+/// file. Various file attributes are put in `newspec` and a copy
+/// is also saved internally to the `ImageInput` (retrievable via
+/// `spec()`. From examining `newspec` or `spec()`, you can
+/// discern the resolution, if it's tiled, number of channels, native
+/// data format, and other metadata about the image.
+///
+/// @param name
+/// Filename to open, UTF-8 encoded.
+///
+/// @param newspec
+/// Reference to an ImageSpec in which to deposit a full
+/// description of the contents of the first subimage of the
+/// file.
+///
+/// @returns
+/// `true` if the file was found and opened successfully.
+bool
+KtxInput::open(const std::string& name, ImageSpec& newspec)
+{
+ m_filename = name;
+
+ if (!ioproxy_use_or_open(name)) {
+ errorfmt("ioproxy_use_or_open(\"{}\") failed", 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;
+ }
+
+ //
+ // IMPORTANT:
+ //
+ // 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).
+ //
+ // 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:
+ // 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) then let's profile and see what more experienced
+ // users might say about this.
+ //
+ // For under-the-hood details, see official libktx repo:
+ // https://github.com/KhronosGroup/KTX-Software/blob/main/lib/src/texture.c
+ //
+ if (proxytype == "file") {
+ auto fd = reinterpret_cast(m_io)->handle();
+ auto res = ktxTexture_CreateFromStdioStream(
+ fd, KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &m_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();
+ auto res = ktxTexture_CreateFromMemory(
+ buff.data(), buff.size(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT,
+ &m_tex);
+ if (KTX_SUCCESS != res) {
+ errorfmt(
+ "Failed to create ktx texture using ktxTexture_CreateFromMemory");
+ return false;
+ }
+ }
+
+ m_tex2 = reinterpret_cast(m_tex);
+ m_nbrmiplevels = m_tex2->numLevels;
+ m_nbrsubimages = m_tex2->numFaces;
+
+ m_spec = ImageSpec(m_tex2->baseWidth, m_tex2->baseHeight,
+ 4 /* dummy value - will be overwritten */);
+ m_spec.depth = m_spec.full_depth = m_tex2->baseDepth;
+ std::string colorspace = get_colorspace();
+ m_spec.set_colorspace(colorspace);
+
+ //
+ // Make sure to save everything that is needed to recreate this exact same
+ // KTX texture from OIIO API (i.e., fields of `ktxTextureCreateInfo`).
+ //
+ // Note:
+ // 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.).
+ //
+ // TODO: save original supercompressionScheme BEFORE infalting the texture
+ m_spec.extra_attribs.attribute("ktx:supercompressionscheme",
+ TypeDesc::UINT32, 1,
+ cspan(
+ m_tex2->supercompressionScheme));
+
+ m_spec.extra_attribs.attribute("ktx:texturekind", TypeDesc::UINT32, 1,
+ cspan(static_cast(
+ get_texture_kind())));
+
+ // save as string
+ m_spec.extra_attribs.attribute("ktx:version", "2.0");
+
+ // Contrary to the specs' layerCount, numLayers is always >= 1 (even for
+ // non-array types)
+ m_spec.extra_attribs.attribute("ktx:nlayers", TypeDesc::UINT32, 1,
+ cspan(m_tex->numLayers));
+
+ m_spec.extra_attribs.attribute("ktx:miplevels", TypeDesc::UINT32, 1,
+ cspan(m_tex->numLevels));
+
+ m_spec.extra_attribs.attribute("ktx:generatemipmaps", TypeDesc::UINT8, 1,
+ cspan(m_tex->generateMipmaps));
+
+ // Store colormodel so that if a KTX2 is requested to be generated, we know
+ // if a Basis Universal scheme has to be applied.
+ m_spec.extra_attribs.attribute("ktx:colormodel", TypeDesc::UINT32, 1,
+ cspan(
+ ktxTexture2_GetColorModel_e(m_tex2)));
+
+ // m_spec.extra_attribs.attribute("ktx:transferfunction", TypeDesc::UINT32, 1,
+ // cspan(transfer_function));
+
+ // TODO: do we actually need the dfd data to re-generate the same KTX2 file?
+ // uint32_t dfdTotalSize = *m_tex2->pDfd;
+ // m_spec.extra_attribs.attribute("ktx:dfd", TypeDesc::UINT8, dfdTotalSize,
+ // make_cspan(reinterpret_cast(
+ // m_tex2->pDfd),
+ // dfdTotalSize));
+
+ //
+ // 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:
+ //
+ // - KTXcubemapIncomplete: 1 byte bitfield
+ // - KTXorientation: null-terminated string
+ //
+ // - KTXglFormat:
+ // + UInt32 glInternalformat
+ // + UInt32 glFormat
+ // + UInt32 glType
+ //
+ // - KTXdxgiFormat__: UInt32
+ // - KTXmetalPixelFormat: UInt32
+ //
+ 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;
+
+ // "The key must be terminated by a NUL character"
+ // This will probably never occur, but it doesn't hurt to be safe
+ if (keylen <= 1)
+ continue;
+
+ if ((status = ktxHashListEntry_GetValue(kventry, &vallen, &val))
+ != KTX_SUCCESS)
+ continue;
+
+ // 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_KEY) {
+ // KTXwriter identifies the program used to write this KTX file
+ // Should be NUL terminated.
+ if (vallen <= 1)
+ continue;
+ auto char_ptr = reinterpret_cast(val);
+ m_spec.extra_attribs.attribute(
+ ktx_prefixed_attr_name,
+ std::string(char_ptr, char_ptr + (vallen - 1)));
+ } 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
+ // E.g., for KTX1 (OpenGL) without any re-orientation logic images are
+ // flipped over X axis (top becomes down).
+ //
+
+ // TODO: set orientation functions
+ } else if (attr_name == "KTXglFormat") {
+ OIIO_ASSERT(vallen == sizeof(KTXglFormat) /* 12 bytes */);
+ KTXglFormat glFormat;
+ glFormat.glInternalformat = *reinterpret_cast(val);
+ glFormat.glFormat = *(reinterpret_cast(val) + 1);
+ glFormat.glType = *(reinterpret_cast(val) + 2);
+ m_glFormat = glFormat;
+ m_spec.extra_attribs.attribute(
+ ktx_prefixed_attr_name, TypeDesc::UINT32, 3,
+ make_cspan(reinterpret_cast(val), 3));
+ } else if (attr_name == "KTXdxgiFormat__") {
+ OIIO_ASSERT(vallen == sizeof(uint32_t));
+ m_dxgiFormat = *reinterpret_cast(val);
+ m_spec.extra_attribs.attribute(ktx_prefixed_attr_name,
+ m_dxgiFormat.value());
+ } else if (attr_name == "KTXmetalPixelFormat") {
+ OIIO_ASSERT(vallen == sizeof(uint32_t));
+ m_metalFormat = *reinterpret_cast(val);
+ m_spec.extra_attribs.attribute(ktx_prefixed_attr_name,
+ m_metalFormat.value());
+ } else {
+ // otherwise store the arbitrary value as a byte string
+ m_spec.extra_attribs.attribute(
+ ktx_prefixed_attr_name, TypeDesc::UCHAR, vallen,
+ make_cspan(reinterpret_cast(val), vallen));
+ }
+
+ } while ((kventry = ktxHashList_Next(kventry)));
+
+ //
+ // We only support KTX_SS_NONE, KTX_SS_ZLIB, KTX_SS_ZSTD, and
+ // KTX_SS_BASIS_LZ supercompression schemes. New schemes may be added to the
+ // spec hence why we do a strict if check.
+ //
+ if (m_tex2->supercompressionScheme != KTX_SS_NONE
+ && m_tex2->supercompressionScheme != KTX_SS_ZSTD
+ && m_tex2->supercompressionScheme != KTX_SS_ZLIB
+ && m_tex2->supercompressionScheme != KTX_SS_BASIS_LZ) {
+ // vendor-specific or newly introduced supercompression schemes (not
+ // supported)
+ errorfmt("unsuppoted supercompression scheme: {}",
+ static_cast(m_tex2->supercompressionScheme));
+ return false;
+ }
+
+ //
+ // Store original VkFormat (i.e., after Basic Universal transcoding and
+ // before potential GPU block format decompression).
+ //
+ // Important:
+ // Call this BEFORE (potential) ktxTexture2_TranscodeBasis call
+ //
+ m_spec.extra_attribs.attribute("ktx:vkformat", TypeDesc::UINT32, 1,
+ cspan(static_cast(
+ m_tex2->vkFormat)));
+
+ //
+ // 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.
+ //
+ if (ktxTexture2_NeedsTranscoding(m_tex2)) {
+ if (auto status = ktxTexture2_TranscodeBasis(
+ m_tex2, ktx_transcode_fmt_e::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;
+ }
+ }
+
+ //
+ // 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.
+ // This is currently 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
+ // TODO
+ //
+ // 4. Compressed color models in Section 5.6 of [KDF14] or successors that
+ // do not have corresponding Vulkan formats.
+ // TODO
+ //
+ if (m_tex2->vkFormat == VK_FORMAT_UNDEFINED) {
+ // TODO: check case (4) - color model
+
+ // check case (3) - non-Vulkan GPU formats (here we simply map these
+ // formats to VkFormat and call it a day)
+ if (m_glFormat.has_value()) {
+ // TODO: add glformat support
+ errorfmt("Loading KTX textures with OpenGL formats but no vkFormat "
+ "(i.e., VK_FORMAT_UNDEFINED) is currently not supported");
+ return false;
+ } else if (m_dxgiFormat.has_value()) {
+ // TODO: add direct3d format support
+ errorfmt(
+ "Loading KTX textures with Direct3D formats but no vkFormat "
+ "(i.e., VK_FORMAT_UNDEFINED) is currently not supported");
+ return false;
+ } else if (m_metalFormat.has_value()) {
+ // TODO: add metal format support
+ errorfmt("Loading KTX textures with Metal formats but no vkFormat "
+ "(i.e., VK_FORMAT_UNDEFINED) is currently not supported");
+ return false;
+ }
+
+ // error for other cases (case (2) should not occur and case (1) is
+ // not supported)
+ 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;
+ }
+
+ //
+ // In case this KTX texture is GPU block compressed, we need to map its
+ // vkformat to the corresponding decompressed VkFormat.
+ //
+ // E.g., VK_FORMAT_BC7_SRGB_BLOCK --> VK_FORMAT_R8G8B8A8_SRGB
+ //
+ // We do this so that we can save the correct crucial stats in the spec
+ // (e.g., nchannels, colorspace, typedesc, etc.) and because the internal
+ // state of data in OIIO is always decompressed (i.e., we never return
+ // block-compressed data from read_native_scanline(s) functions).
+ //
+ auto format = static_cast(m_tex2->vkFormat);
+ if (m_tex2->isCompressed) {
+ FormatInfo format_info;
+ if (!extract_info_from_format(static_cast(format),
+ format_info)) {
+ errorfmt(
+ "Failed to extract info (e.g., nchannels, typedesc, etc.) from VkFormat: {}",
+ static_cast(format));
+ return false;
+ }
+ if (format_info.compression == BlockCompression::NONE
+ || format_info.decompressed_format == VK_FORMAT_UNDEFINED) {
+ errorfmt(
+ "KTX texture is GPU-block-compressed using unsuppoted format: {}",
+ static_cast(format));
+ return false;
+ }
+ format = format_info.decompressed_format;
+ m_cmp = format_info.compression;
+ }
+
+ //
+ // 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 (!extract_info_from_format(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;
+ }
+
+ // TODO: verify the x, y, z limits (probably not 65535)
+ 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;
+ if (m_tex) {
+ ktxTexture_Destroy(m_tex);
+ m_tex = nullptr;
+ }
+ ioproxy_clear();
+ return true;
+};
+
+
+
+//
+// In the context of KTX, `subimage` CAN be interpreted as (1D textures are
+// considered 2D textures with height set to 1):
+// 1. array layer (if texture is a 2D texture array)
+// 2. depth slice (if texture is 3D)
+// 3. cube map face (if texture is a cubemap)
+// 4. depth slice (of first 3D texture if texture is a 3D texture array)
+// 5. cube map face (of first cubemap texture if texture is a cubmap array)
+//
+// `miplevel` is simply interpreted as a mip level of the above `subimage`.
+//
+// In other cases, if subimage is > 0, it is invalid.
+//
+bool
+KtxInput::seek_subimage(int subimage, int miplevel)
+{
+ lock_guard lock(*this);
+
+ //
+ // 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 || subimage >= m_nbrsubimages
+ || miplevel >= m_nbrmiplevels)
+ /* don't errorfmt here */
+ 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;
+
+ // cast to ktx_uint32_t to stop the compiler/clangd from complaining
+ auto _subimage = static_cast(subimage);
+
+ ktx_uint32_t arr_layer { 0 }; // array layer
+ ktx_uint32_t face_slice { 0 }; // 3d texture slice or cubemap face
+
+ // is this a cubemap? (i.e., subimage means cubemap face)
+ if (m_tex->isCubemap)
+ face_slice = _subimage;
+
+ // is this an array texture? (i.e., subimage means array layer)
+ if (m_tex->isArray)
+ arr_layer = _subimage;
+
+ // is this a 3D texture? (i.e., subimage means face slice)
+ if (m_tex->numDimensions == 3)
+ face_slice = _subimage;
+
+ //
+ // 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_tex2->baseWidth >> miplevel, 1u);
+ const size_t height = std::max(m_tex2->baseHeight >> miplevel, 1u);
+ const size_t depth = std::max(m_tex2->baseDepth >> miplevel, 1u);
+
+ m_spec.width = width;
+ m_spec.height = height;
+ m_spec.depth = depth;
+
+ //
+ // Decode GPU-compression if any. 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.
+ // TODO: wait for my PR in libktx to implement decode_astc for
+ // per-miplvl/subimage decoding.
+ //
+ // BCn: libktx will provide decoders/encoders for BCn block compression
+ // via ktxTexture2_DecodeBCn. TODO: wait for my RP in libktx to get
+ // merged then add BCn support.
+ //
+ // ETC2: TODO: some licensing clarification is needed from the part of
+ // etcunpack usage in libktx.
+ //
+ // PVRTC: TODO: wait for libktx PR.
+ //
+ if (m_tex2->isCompressed /* i.e., is GPU block compressed? */) {
+ ktx_size_t offset;
+ if (auto status = ktxTexture2_GetImageOffset(m_tex2, miplevel,
+ arr_layer, face_slice,
+ &offset);
+ status != KTX_SUCCESS) {
+ return status;
+ }
+ // TODO: Are pointer indices [offset, offset + size[ safe?
+ // Encoded blocks
+ cspan src_span(m_tex2->pData + offset,
+ ktxTexture_GetImageSize(ktxTexture(m_tex),
+ miplevel));
+
+ switch (m_cmp) {
+ /* BCn LDR formats */
+ case BlockCompression::BC1:
+ case BlockCompression::BC2:
+ case BlockCompression::BC3:
+ case BlockCompression::BC4:
+ case BlockCompression::BC5:
+ case BlockCompression::BC7:
+ //
+ // TODO: wait for my PR in libktx to be merged
+ //
+ // 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_tex2->isCompressed will be false => this will only
+ // be called once.
+ //
+ // if (auto status = ktxTexture2_DecodeBCn(m_tex2);
+ // status != KTX_SUCCESS) {
+ // errorfmt("failed to decode BCn-compressed texture. "
+ // "ktxTexture2_DecodeBCn returned Ktx error code: {}",
+ // static_cast(status));
+ // return false;
+ // }
+ // break;
+ return false;
+
+ /* BCn HDR formats - TODO */
+ case BlockCompression::BC6HU:
+ case BlockCompression::BC6HS:
+ return false;
+
+ /* ETC formats */
+ case BlockCompression::ETC2_RGB:
+ case BlockCompression::ETC2_RGB_A1:
+ case BlockCompression::ETC2_RGBA:
+ return false;
+
+ /* ASTC formats */
+ case BlockCompression::ASTC:
+ //
+ // 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_tex2->isCompressed will be false => this will only
+ // be called once.
+ //
+ if (auto status = ktxTexture2_DecodeAstc(m_tex2);
+ status != KTX_SUCCESS) {
+ errorfmt("failed to decode ASTC-compressed texture. "
+ "ktxTexture2_DecodeAstc returned Ktx error code: {}",
+ static_cast(status));
+ return false;
+ }
+ break;
+
+ default:
+ errorfmt("Unknown/unsupported GPU block compression kind: {}",
+ static_cast(m_cmp));
+ return false;
+ }
+
+ m_pitch = width * m_spec.nchannels
+ * m_spec.format.size() /* 1 for LDR, 2 for HDR formats */;
+ m_data_ptr = m_buf.data();
+ }
+
+ // Do NOT change this to `else` statement because this handles the ASTC and
+ // BCn cases above (which, again, sets m_tex2->isCompressed to `false`)
+ if (!m_tex2->isCompressed) {
+ //
+ // GetImageOffset implements internal checks depending on texture kind (e.g.,
+ // 3D, cubemap, etc.) and incase of invalid input, KTX_INVALID_OPERATION is
+ // returned.
+ //
+ ktx_size_t offset;
+ if (auto status = ktxTexture_GetImageOffset(m_tex, miplevel, arr_layer,
+ face_slice, &offset);
+ status != KTX_SUCCESS) {
+ errorfmt("ktxTexture_GetImageOffset failed with exit code: {}",
+ static_cast(status));
+ return false;
+ }
+ m_pitch = ktxTexture_GetRowPitch(m_tex, miplevel);
+ m_data_ptr = m_tex2->pData + offset;
+ }
+ return true;
+}
+
+
+
+bool
+KtxInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/,
+ void* data)
+{
+ lock_guard lock(*this);
+ return read_native_scanlines(subimage, miplevel, y, y + 1,
+ as_writable_bytes(data, m_spec.scanline_bytes(
+ true)));
+}
+
+
+
+bool
+KtxInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
+ int yend, int /* z */, void* data)
+{
+ lock_guard lock(*this);
+
+ if (ybegin >= yend) {
+ errorfmt("Invalid scanline range requested: {}-{}", ybegin, yend);
+ return false;
+ }
+
+ // avoid calling seek_subimage because this will NOT be thread-safe and
+ // we have to introduce a lock which will make this slower (read note above
+ // about how libktx inflates all data in open()).
+ if (!seek_subimage(subimage, miplevel))
+ return false;
+
+ size_t size = m_spec.scanline_bytes(true) * size_t(yend - ybegin);
+ return read_native_scanlines(subimage, miplevel, ybegin, yend,
+ as_writable_bytes(data, size));
+}
+
+
+bool
+KtxInput::read_native_scanlines(int subimage, int miplevel, int ybegin,
+ int yend, span data)
+{
+ lock_guard lock(*this);
+ // is provided [ybegin, yend[ valid?
+ if (ybegin < 0 || ybegin >= yend || yend > m_spec.height) {
+ // out of range scanlines
+ errorfmt("KTX read_native_scanlines: Out of valid range scanline indices "
+ "(b={} e={}).",
+ ybegin, yend);
+ return false;
+ }
+
+ // can the provided span hold the requested scanlines?
+ if (!valid_raw_span_size(data, m_spec, 0, m_spec.width, ybegin, yend))
+ // errorfmt is set within valid_raw_span_size
+ return false;
+
+ // since miplevel is valid => get number of bytes in a row for this mip
+ memcpy(data.data(), m_data_ptr, m_pitch * (yend - ybegin));
+ // std::cout << "read_native_scanlines(" << subimage << ", " << miplevel
+ // << ", " << ybegin << ", " << yend << ")" << '\n';
+ 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(KTX2_IDENTIFIER, magic, 0);
+}
+
+
+bool
+KtxInput::ktx_magic_cmp(const uint8_t* KTX_MAGIC, const uint8_t* sig,
+ size_t start) const
+{
+ for (size_t i = start; (i - start) < sizeof(KTX_MAGIC); ++i)
+ if (sig[i] != KTX_MAGIC[i])
+ return false;
+ return true;
+}
+
+
+TextureKind
+KtxInput::get_texture_kind() const
+{
+ switch (m_tex->numDimensions) {
+ case 1:
+ if (m_tex->isArray)
+ return TextureKind::ARRAY_TEXTURE_1D;
+ return TextureKind::SINGLE_TEXTURE_1D;
+ case 2:
+ if (m_tex->isArray && m_tex->isCubemap)
+ return TextureKind::ARRAY_TEXTURE_CUBEMAP;
+ else if (m_tex->isArray)
+ return TextureKind::ARRAY_TEXTURE_2D;
+ return TextureKind::SINGLE_TEXTURE_2D;
+ case 3:
+ if (m_tex->isArray)
+ return TextureKind::ARRAY_TEXTURE_3D;
+ return TextureKind::SINGLE_TEXTURE_3D;
+ default: return TextureKind::SINGLE_TEXTURE_2D;
+ }
+}
+
+
+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
+ khr_df_transfer_e transfer_function = ktxTexture2_GetTransferFunction_e(
+ m_tex2);
+ khr_df_primaries_e primaries = ktxTexture2_GetPrimaries_e(m_tex2);
+ // 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: break;
+ }
+ break;
+ case KHR_DF_TRANSFER_LINEAR:
+ switch (primaries) {
+ case KHR_DF_PRIMARIES_BT709: return "lin_rec709_scene";
+ default: break;
+ }
+ break;
+ // case KHR_DF_TRANSFER_DCIP3: colorspace = "lin_rec709_scene"; return true;
+ default: break;
+ }
+ // TODO: need to generate test files before adding support for any other
+ // colorspaces
+ return "unknown";
+}
+
+OIIO_PLUGIN_NAMESPACE_END
diff --git a/src/ktx.imageio/ktxoutput.cpp b/src/ktx.imageio/ktxoutput.cpp
new file mode 100644
index 0000000000..fc28b1101a
--- /dev/null
+++ b/src/ktx.imageio/ktxoutput.cpp
@@ -0,0 +1,674 @@
+// Copyright Contributors to the OpenImageIO project.
+// SPDX-License-Identifier: Apache-2.0
+// https://github.com/AcademySoftwareFoundation/OpenImageIO
+
+// TODO: only set this if libktx is statically built/linked against
+// Per KTX-Software BUILDING.md:
+// > When linking to the static library, make sure to
+// > define `KHRONOS_STATIC` before including KTX header files.
+// > This is especially important on Windows.
+#ifndef BUILD_SHARED_LIBS
+# define KHRONOS_STATIC 1
+#endif
+
+#include "ktx_pvt.h"
+#include
+#include
+
+OIIO_PLUGIN_NAMESPACE_BEGIN
+
+class KtxOutput final : public ImageOutput {
+public:
+ KtxOutput() {}
+
+ ~KtxOutput() override { close(); }
+
+ const char* format_name(void) const override { return "ktx"; }
+
+ int supports(string_view feature) const override
+ {
+ return (
+ // as per the KTX1/2 specs:
+ // registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_keyvalue_data
+ feature == "arbitrary_metadata" ||
+ /* ktx supports 3D textures, cubmap textures, texture arrays, ... */
+ feature == "multiimage" ||
+ /* not sure ... this naming is confusing */
+ feature == "mipmap");
+ }
+
+ 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;
+
+ ktxTexture2* m_tex { nullptr };
+
+ uint32_t m_nlayers { 1 };
+
+ uint32_t m_miplevels { 1 };
+
+ ktxSupercmpScheme m_superCmp { KTX_SS_NONE };
+
+ khr_df_model_e m_colormodel { KHR_DF_MODEL_UNSPECIFIED };
+
+ BlockCompression m_cmp { BlockCompression::NONE };
+
+ /// libktx only supports writing whole images (i.e., (miplevel, layer, face_slice/depth)
+ /// 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).
+ std::vector m_img;
+
+ // TODO: what about volumetric textures? do we keep an array of these very
+ // large vectors? For the moment, these are just not supported.
+
+ void init();
+
+ VkFormat figure_vkformat_from_spec() const;
+
+ bool basisu_basislz_compress();
+
+ bool basisu_uastc_compress();
+
+ bool write_ktx2();
+
+ // void generate_mip_levels(const image_span& base_lvl_image,
+ // ImageInput& inputFile, uint32_t numMipLevels,
+ // uint32_t layerIndex, uint32_t faceIndex,
+ // uint32_t depthSliceIndex);
+};
+
+
+
+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& newspec,
+ OpenMode mode)
+{
+ // TODO: verify the x, y, z limits (probably not 65535)
+ if (!check_open(mode, newspec, { 0, 65535, 0, 65535, 0, 65535, 0, 4 }))
+ return false;
+
+ // Save name and spec for later use
+ m_filename = name;
+
+ ioproxy_retrieve_from_config(m_spec);
+ if (!ioproxy_use_or_open(m_filename))
+ return false;
+
+ // 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 ...
+
+ // keep this commented in case we need it
+ /*
+ cspan dfd;
+ ParamValue* dfdQ = m_spec.find_attribute("ktx:dfd", TypeDesc::UINT8);
+ if (dfdQ) {
+ dfd = dfdQ->as_cspan();
+ std::cout << "[ktxoutput] found dfd with len: " << dfdQ->nvalues()
+ << '\n';
+ }
+
+ // Copy dfd data from const span because ktxTextureCreateInfo does not take
+ // a const uint8_t ptr.
+ ktx_uint32_t* pDfd { nullptr };
+ std::vector dfd_copy;
+ if (!dfd.empty()) {
+ dfd_copy.assign(dfd.begin(), dfd.end());
+ pDfd = reinterpret_cast(dfd_copy.data());
+ std::cout << "pDfd is set to != nullptr" << '\n';
+ }
+ */
+
+ //
+ // 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.
+ //
+ ParamValue* superCmpSchemQ
+ = m_spec.find_attribute("ktx:supercompressionscheme", TypeDesc::UINT32);
+ if (superCmpSchemQ) {
+ m_superCmp = static_cast(
+ *reinterpret_cast(superCmpSchemQ->data()));
+ // std::cout << "[ktxoutput] found supercompression scheme: " << m_superCmp
+ // << '\n';
+ }
+
+
+ // Get transfer function (for color space conversions)
+ // ParamValue* tfQ = m_spec.find_attribute("ktx:transferfunction",
+ // TypeDesc::UINT32);
+ // if (tfQ) {
+ // m_tf = static_cast(
+ // *reinterpret_cast(tfQ->data()));
+ // std::cout << "[ktxoutput] found tf: " << m_tf << '\n';
+ // }
+
+ // Get color model (to detect GPU compression, Basis Universal format, etc.)
+ ParamValue* colorModelQ = m_spec.find_attribute("ktx:colormodel",
+ TypeDesc::UINT32);
+ if (colorModelQ) {
+ m_colormodel = static_cast(
+ *reinterpret_cast(colorModelQ->data()));
+ // std::cout << "[ktxoutput] found color model: " << m_colormodel << '\n';
+ }
+
+ // 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...
+ close();
+ errorfmt("unsupported super compression scheme: {}",
+ static_cast(m_superCmp));
+ return false;
+ }
+
+ // Get original VkFormat (i.e., before potential decompression/transcoding)
+ // or the vkformat explicitly set via the "ktx:vkformat" attribute.
+ auto vkFormat = VkFormat::VK_FORMAT_UNDEFINED;
+ ParamValue* vkFormatQ = m_spec.find_attribute("ktx:vkformat",
+ TypeDesc::UINT32);
+ if (vkFormatQ) {
+ vkFormat = static_cast(
+ *reinterpret_cast(vkFormatQ->data()));
+ // Get GPU-block-compression from provided VkFormat
+ if (vkFormat != VK_FORMAT_UNDEFINED) {
+ FormatInfo format_info;
+ if (!extract_info_from_format(vkFormat, format_info)) {
+ close();
+ errorfmt("Could not extract format info from provided "
+ "VkFormat: {}. This format is probably unsupported.",
+ static_cast(vkFormat));
+ return false;
+ }
+ m_cmp = format_info.compression;
+ }
+ // std::cout << "[ktxoutput] found vkformat: " << vkFormat << '\n';
+ }
+
+ //
+ // Since we are using libktx's ktxTexture2_CompressAstc, the format has to
+ // be set to an uncompressed VkFormat otherwise we get KTX_INVALID_OPERATION
+ // error code.
+ //
+ if (m_cmp == BlockCompression::ASTC)
+ vkFormat = VK_FORMAT_R8G8B8A8_SRGB;
+
+ // Id a basis universal format compression is not requested and
+ // "ktx:vkformat" is VK_FORMAT_UNDEFINED, then we error out. The user has to
+ // set the vkformat so that we know in which format we write the texture to.
+ if ((m_colormodel != KHR_DF_MODEL_ETC1S
+ && m_colormodel != KHR_DF_MODEL_UASTC)
+ && vkFormat == VK_FORMAT_UNDEFINED) {
+ // TODO: maybe don't error out and set the format depending on the nchannels?
+ // (e.g., 4 + srgb_rec709_scene colorspace => VK_FORMAT_R8G8B8A8_SRGB)
+ close();
+ errorfmt(
+ "VkFormat is set to VK_FORMAT_UNDEFINED even though the "
+ "supercompressionscheme is not Basis LZ. You have to set the "
+ "target VkFormat by setting the ImageSpec's attribute 'ktx:vkformat'.");
+ return false;
+ }
+
+ //
+ // 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?)
+ //
+ if ((m_colormodel == KHR_DF_MODEL_ETC1S
+ || m_colormodel == KHR_DF_MODEL_UASTC)
+ && vkFormat == VK_FORMAT_UNDEFINED) {
+ vkFormat = figure_vkformat_from_spec();
+ } else if (m_colormodel == KHR_DF_MODEL_ETC1S
+ || m_colormodel == KHR_DF_MODEL_UASTC) {
+ // TODO: It could be that the user explicitly provided a vkformat - in which
+ // case we have to make sure it aligns with the spec.
+ // if (!is_vkformat_aligned_with_spec()) ...
+ close();
+ errorfmt("Expected vkformat to be VK_FORMAT_UNDEFINED for Basis "
+ "Universal (UASTC or ETC1S) target KTX textures.");
+ return false;
+ }
+
+ // get number of mip levels
+ m_miplevels = 1;
+ ParamValue* miplevelsQ = m_spec.find_attribute("ktx:miplevels",
+ TypeDesc::UINT32);
+ if (miplevelsQ) {
+ m_miplevels = *reinterpret_cast(miplevelsQ->data());
+ }
+
+ if (m_miplevels > 1) {
+ close();
+ errorfmt("Cannot re-generate mip levels because there is no way to "
+ "know the original filter that was used to generate them.");
+ return false;
+ }
+
+ //
+ // TODO: sanity checks on provided attributes (e.g., certain
+ // supercompression schemes cannot be applied to certain basisu formats,
+ // etc.)
+ //
+
+ // get number of layers
+ m_nlayers = 1;
+ ParamValue* nlayersQ = m_spec.find_attribute("ktx:nlayers",
+ TypeDesc::UINT32);
+ if (nlayersQ) {
+ m_nlayers = *reinterpret_cast(nlayersQ->data());
+ }
+
+ // std::cout << "vkformat: " << vkFormat << '\n';
+ // std::cout << "mip levels: " << m_miplevels << '\n';
+ // std::cout << "nlayers: " << m_nlayers << '\n';
+ // std::cout << "[width, height, depth]: [" << m_spec.width << ", "
+ // << m_spec.height << ", " << m_spec.depth << "] \n";
+
+ // TODO: avoid some of these static casts into ktx_uint*_t types by storing
+ // uint*_t as unsigned integers and not as integers.
+ OIIO_ASSERT(vkFormat != VK_FORMAT_UNDEFINED); // otherwise segfault
+ ktxTextureCreateInfo create_info;
+ create_info.glInternalformat = 0; // Ignored as we'll create a KTX2 texture
+ create_info.vkFormat = vkFormat;
+ create_info.pDfd = nullptr;
+ create_info.baseWidth = static_cast(m_spec.width);
+ create_info.baseHeight = static_cast(m_spec.height);
+ create_info.baseDepth = static_cast(m_spec.depth);
+ create_info.numDimensions = 2; // TODO: this is currently hardcoded
+ create_info.numLevels = 1; // static_cast(m_miplevels)
+ create_info.numLayers = 1; // static_cast(nlayers)
+ create_info.numFaces = 1; // TODO: this is currently hardcoded
+ create_info.isArray = KTX_FALSE;
+ create_info.generateMipmaps = KTX_FALSE;
+
+ if (auto status = ktxTexture2_Create(&create_info,
+ KTX_TEXTURE_CREATE_ALLOC_STORAGE,
+ &m_tex);
+ status != KTX_SUCCESS) {
+ close();
+ errorfmt("ktxTexture_Create return KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+
+ // Reserve space for base level mipmap
+ if (!m_tex->isCompressed) {
+ m_img.resize(ktxTexture_GetImageSize(ktxTexture(m_tex), 0));
+ } else {
+ // TODO:
+ // Not compressed => make sure that vector's size matches the expected
+ // size from the set raw VkFormat:
+ // (e.g., VK_FORMAT_R8G8_SRGB => width * height * 3 )
+ m_img.resize(m_spec.scanline_bytes() * m_spec.height);
+ }
+
+ return true;
+}
+
+
+
+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);
+}
+
+
+
+// TODO: use the span alternative. Apparently, there isn't one that takes a
+// depth parameter (i.e., z).
+bool
+KtxOutput::write_scanlines(int ybegin, int yend,
+ int _ /* slice or face or layer */, TypeDesc format,
+ const void* data, stride_t xstride, stride_t ystride)
+{
+ // std::cout << "write_scanlines called with: ybegin=" << ybegin
+ // << "; yend=" << yend << "; z=" << z << "; format=" << format
+ // << "; xstride=" << xstride << '\n';
+
+ m_spec.auto_stride(xstride, format, spec().nchannels);
+ // const void* origdata = data;
+ if (format == TypeUnknown)
+ format = m_spec.format;
+
+ 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_img.data() + offset, pSrc, datalen);
+ // std::cout << "write_scanlines success" << '\n';
+ return true;
+}
+
+
+
+bool
+KtxOutput::close()
+{
+ // Check if already closed => if so, then the KTX2 file is already saved
+ if (!ioproxy_opened()) {
+ init();
+ return true;
+ }
+
+ bool result = true;
+ if (m_tex) {
+ // Apparently we can't do (or I don't know yet how to) partial writes
+ // using libktx. We can only write whole ktxTextures all together.
+ if (result)
+ result = write_ktx2(); // TODO: can this throw? (prob not)
+ ktxTexture_Destroy(ktxTexture(m_tex));
+ }
+ init();
+ return result;
+}
+
+
+
+void
+KtxOutput::init()
+{
+ // TODO: calling open() after close() on this hasn't been testes yet ...
+ m_tex = nullptr;
+ m_superCmp = KTX_SS_NONE;
+ m_colormodel = KHR_DF_MODEL_UNSPECIFIED;
+ // TODO: other stuff...
+ ioproxy_clear();
+}
+
+
+
+VkFormat
+KtxOutput::figure_vkformat_from_spec() const
+{
+ // TODO: check colorspace and return VkFormat accordingly
+ // TODO: check format (TypeDesc) and return VkFormat accordingly
+ switch (m_spec.nchannels) {
+ case 1: return VK_FORMAT_R8_SRGB;
+ case 2: return VK_FORMAT_R8G8_SRGB;
+ case 3: return VK_FORMAT_R8G8B8_SRGB;
+ case 4: return VK_FORMAT_R8G8B8A8_SRGB;
+ }
+ return VK_FORMAT_R8G8B8A8_SRGB;
+}
+
+
+
+//
+// Applies BasisLZ/ETC1S supercompression to this KTX2 texture. The ImageSpec is
+// queried (searched) for attribute that determine the BasisLZ/ETC1S compression
+// params (see ktxBasisParams struct in libktx).
+//
+bool
+KtxOutput::basisu_basislz_compress()
+{
+ // TODO: retrieve BasisLZ/ETC1S compression params. `ktx info` prints some
+ // Basis Supercompression Global Data that might be useful in figuring out
+ // what params the original data was compressed with so that we can reproduce
+ // it.
+ // TODO: expose as "ktx:" attribute(s)
+ ktxBasisParams params;
+ params.structSize = sizeof(ktxBasisParams);
+ params.codec = ktx_basis_codec_e::KTX_BASIS_CODEC_ETC1S;
+ params.threadCount = 1;
+ params.etc1sCompressionLevel = KTX_ETC1S_DEFAULT_COMPRESSION_LEVEL;
+ if (auto status = ktxTexture2_CompressBasisEx(m_tex, ¶ms);
+ status != KTX_SUCCESS) {
+ errorfmt("ktxTexture2_CompressBasisEx returned error code: ",
+ static_cast(status));
+ return false;
+ }
+ return true;
+}
+
+
+
+//
+// Applies UASTC basis universal 'compression' to this KTX2 texture.
+// The ImageSpec is queried (searched) for attribute that determine the UASTC
+// compression params (see ktxBasisParams struct in libktx).
+//
+bool
+KtxOutput::basisu_uastc_compress()
+{
+ // TODO: expose parameters
+ ktxBasisParams params;
+ params.structSize = sizeof(ktxBasisParams);
+ params.codec = ktx_basis_codec_e::KTX_BASIS_CODEC_UASTC_LDR_4x4;
+ params.threadCount = 1;
+ // .uastcFlags = KTX_PACK_UASTC_LEVEL_DEFAULT,
+ // TODO: set uastcRDONoMultithreading for testing
+ if (auto status = ktxTexture2_CompressBasisEx(m_tex, ¶ms);
+ status != KTX_SUCCESS) {
+ errorfmt("ktxTexture2_CompressBasisEx returned error code: ",
+ static_cast(status));
+ return false;
+ }
+ return true;
+}
+
+
+
+bool
+KtxOutput::write_ktx2()
+{
+ // TODO: this attribute should be ignored in testing.
+ // 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(&m_tex->kvDataHead, KTX_WRITER_KEY,
+ (ktx_uint32_t)strlen(writer) + 1, writer);
+ // std::cout << "KTXwrite: " << writer << '\n';
+
+ //
+ // In case data was read from an input KTX2 file with mipmaps, we have to
+ // write the base level then generate mipmaps up to the specified level
+ // (via the "ktx:miplevels" attribute). KTX-Software (not necessarily
+ // libktx), surely has a function somewhere that generates these mipmaps.
+ // Ideally, we should follow the exact same implementation used in
+ // KTX-Software to generate the mipmaps.
+ //
+ // 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.
+ //
+ // Note 2:
+ // There is apparently no metadata to know which filter (+ params) that
+ // was used to generate the mipmaps.
+ //
+ // Important:
+ // If the VkFormat related to the KTX texture creation is wrongly set, this
+ // will cause a segfault!
+ //
+ if (!m_tex->isCompressed) {
+ if (auto status = ktxTexture_SetImageFromMemory(ktxTexture(m_tex), 0, 0,
+ 0, m_img.data(),
+ m_img.size());
+ status != KTX_SUCCESS) {
+ has_error();
+ errorfmt(
+ "ktxTexture_SetImageFromMemory returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+ } else if (m_cmp == BlockCompression::BC1 || m_cmp == BlockCompression::BC3
+ || m_cmp == BlockCompression::BC4
+ || m_cmp == BlockCompression::BC5
+ || m_cmp == BlockCompression::BC7) {
+ // First set uncompressed texture
+ // if (auto status = ktxTexture_SetImageFromMemory(ktxTexture(m_tex), 0, 0,
+ // 0, m_img.data(),
+ // m_img.size());
+ // status != KTX_SUCCESS) {
+ // has_error();
+ // errorfmt(
+ // "ktxTexture_SetImageFromMemory returned KTX exit error code: {}",
+ // static_cast(status));
+ // return false;
+ // }
+
+ // Then compress the whole texture to BCn format
+ // TODO: expose BCn compression quality parameter as spec attribute
+ // if (auto status = ktxTexture2_CompressBCn(m_tex, nullptr);
+ // status != KTX_SUCCESS) {
+ // errorfmt("ktxTexture2_CompressBCn returned KTX exit error code: {}",
+ // static_cast(status));
+ // return false;
+ // }
+ return false;
+ } else if (m_cmp == BlockCompression::ASTC) {
+ // First set uncompressed images
+ if (auto status = ktxTexture_SetImageFromMemory(ktxTexture(m_tex), 0, 0,
+ 0, m_img.data(),
+ m_img.size());
+ status != KTX_SUCCESS) {
+ has_error();
+ errorfmt(
+ "ktxTexture_SetImageFromMemory returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+
+ // Then compress the whole texture to ASTC format
+ // TODO: expose ASTC compression quality parameter as spec attribute
+ if (auto status = ktxTexture2_CompressAstc(m_tex, 0);
+ status != KTX_SUCCESS) {
+ errorfmt("ktxTexture2_CompressAstc returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+ }
+
+ // If basis universal compression is requested (i.e., to BasisLZ/ETC1S
+ // or UASTC), compress the texture before writing.
+ if (m_colormodel == KHR_DF_MODEL_ETC1S) {
+ if (!basisu_basislz_compress())
+ return false;
+ } else if (m_colormodel == KHR_DF_MODEL_UASTC) {
+ if (!basisu_uastc_compress())
+ return false;
+ }
+
+ // Finally, apply the supercompression scheme (if any)
+ if (m_superCmp == KTX_SS_ZLIB) {
+ if (auto status = ktxTexture2_DeflateZLIB(m_tex, 0);
+ 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(m_tex, 0);
+ status != KTX_SUCCESS) {
+ errorfmt("ktxTexture2_DeflateZstd returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+ }
+
+ Filesystem::IOProxy* m_io = ioproxy();
+ if (!strcmp(m_io->proxytype(), "file")) {
+ auto fd = reinterpret_cast(m_io)->handle();
+ if (auto status = ktxTexture2_WriteToStdioStream(m_tex, fd);
+ status != KTX_SUCCESS) {
+ errorfmt(
+ "ktxTexture2_WriteToStdioStream returned KTX exit error code: {}",
+ static_cast(status));
+ return false;
+ }
+ return true;
+ }
+
+ // TODO: this hasn't been tested yet
+ 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(m_tex, &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 */
+ auto _ = std::unique_ptr(buff);
+ proxy->write(buff, buff_size);
+ return true;
+ }
+
+ // OIIO should guarantee that this never happens
+ errorfmt("unexpected IOProxy type: {}", m_io->proxytype());
+ return false;
+}
+
+
+
+// void
+// KtxOutput::generate_mip_levels(const image_span& base_lvl_image,
+// ImageInput& inputFile, uint32_t numMipLevels,
+// uint32_t layerIndex, uint32_t faceIndex,
+// uint32_t depthSliceIndex)
+// {
+// //if (isFormatINT(static_cast(texture->vkFormat)))
+// // fatal(rc::NOT_SUPPORTED, "Mipmap generation for SINT or UINT format {} is not supported.",
+// // toString(static_cast(texture->vkFormat)));
+//
+// for (uint32_t mipLevelIndex = 1; mipLevelIndex < numMipLevels;
+// ++mipLevelIndex) {
+// const auto mipImageWidth = std::max(1u, m_tex->baseWidth
+// >> (mipLevelIndex));
+// const auto mipImageHeight = std::max(1u, m_tex->baseHeight
+// >> (mipLevelIndex));
+//
+// ROI roi(0, mipImageHeight, 0, mipImageWidth, 0, 1, /*chans:*/ 0, 4);
+// ImageBuf dst = ImageBufAlgo::resample(Src, true, roi);
+// // if (options.normalize)
+// // image->normalize();
+//
+// // const auto imageData = convert(levelImage, options.vkFormat, inputFile,
+// // true);
+//
+// const auto ret = ktxTexture_SetImageFromMemory(
+// m_tex, mipLevelIndex, layerIndex,
+// faceIndex
+// + depthSliceIndex, // Faces and Depths are mutually exclusive, Addition is acceptable
+// NULL, 0);
+// // (ret == KTX_SUCCESS && "Internal error");
+// }
+// }
+
+OIIO_PLUGIN_NAMESPACE_END
diff --git a/src/libOpenImageIO/imageioplugin.cpp b/src/libOpenImageIO/imageioplugin.cpp
index 0ea86da08a..a3f19d229c 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..201a5c8a95
--- /dev/null
+++ b/testsuite/ktx/ref/out.txt
@@ -0,0 +1,180 @@
+Reading ../oiio-images/ktx2/2d_rgb8.ktx2
+../oiio-images/ktx2/2d_rgb8.ktx2 : 40 x 40, 3 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: 5BFA8296353869F0D950DC016A7E31C08D513EE8
+ channel list: R, G, B
+ ktx:colormodel: 1
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx create v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 29
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/2d_rgba8.ktx2
+../oiio-images/ktx2/2d_rgba8.ktx2 : 40 x 40, 4 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: 9B40384A9B89EE338F24212AB876C14F9BABDBC0
+ channel list: R, G, B, A
+ ktx:colormodel: 1
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx create v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 43
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/2d_uastc.ktx2
+../oiio-images/ktx2/2d_uastc.ktx2 : 40 x 40, 4 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: A41EE02136B25739FB99520244FF5D0C69A0353C
+ channel list: R, G, B, A
+ ktx:colormodel: 166
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx create v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 0
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/2d_etc1s.ktx2
+../oiio-images/ktx2/2d_etc1s.ktx2 : 40 x 40, 4 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: CD540B1FE2CE1F85BE11F7EC21446A8405844029
+ channel list: R, G, B, A
+ ktx:colormodel: 163
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx create v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 1
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 0
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/2d_astc4x4.ktx2
+../oiio-images/ktx2/2d_astc4x4.ktx2 : 40 x 40, 4 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: 0F24F23031B85B09E74A4E35D7EC519A9A9259CC
+ channel list: R, G, B, A
+ ktx:colormodel: 162
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx transcode v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 158
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/2d_bc1.ktx2
+../oiio-images/ktx2/2d_bc1.ktx2 : 40 x 40, 4 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: 0E7F8EBBCA431855C12D2BF5A638AC46922582CC
+ channel list: R, G, B, A
+ ktx:colormodel: 128
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx transcode v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 132
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/2d_bc3.ktx2
+../oiio-images/ktx2/2d_bc3.ktx2 : 40 x 40, 4 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: B88FED8C339C75154B4D79FEBFADA05CAA06B4E4
+ channel list: R, G, B, A
+ ktx:colormodel: 130
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx transcode v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 138
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/2d_bc4.ktx2
+../oiio-images/ktx2/2d_bc4.ktx2 : 40 x 40, 1 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: E0352908B50C8CEC4593866CE8704532EE42A600
+ channel list: R
+ ktx:colormodel: 131
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx transcode v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 139
+ oiio:ColorSpace: "lin_rec709_scene"
+Reading ../oiio-images/ktx2/2d_bc5.ktx2
+../oiio-images/ktx2/2d_bc5.ktx2 : 40 x 40, 2 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: F136624C2A6714A137A0155DAF47C86A31D69ECD
+ channel list: R, G
+ ktx:colormodel: 132
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx transcode v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 141
+ oiio:ColorSpace: "lin_rec709_scene"
+Reading ../oiio-images/ktx2/2d_bc7.ktx2
+../oiio-images/ktx2/2d_bc7.ktx2 : 40 x 40, 4 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: 1A8C2DBD9F198A97E15348E26D39B81B565D447C
+ channel list: R, G, B, A
+ ktx:colormodel: 134
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx transcode v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 146
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/2d_etc1.ktx2
+../oiio-images/ktx2/2d_etc1.ktx2 : 40 x 40, 4 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: 365A5301637FA43F7D7787874A440AA40C9C4E3E
+ channel list: R, G, B, A
+ ktx:colormodel: 161
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx transcode v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 148
+ oiio:ColorSpace: "srgb_rec709_scene"
+Reading ../oiio-images/ktx2/2d_etc2.ktx2
+../oiio-images/ktx2/2d_etc2.ktx2 : 40 x 40, 4 channel, uint8 ktx
+ MIP-map levels: 40x40 20x20 10x10 5x5 2x2 1x1
+ SHA-1: 6E71EEF6D68C31DE010657E64F9A2030048BCC0F
+ channel list: R, G, B, A
+ ktx:colormodel: 161
+ ktx:generatemipmaps: 0
+ ktx:KTXwriter: "ktx transcode v4.3.1~1 / libktx v4.3.0~1"
+ ktx:miplevels: 6
+ ktx:nlayers: 1
+ ktx:supercompressionscheme: 0
+ ktx:texturekind: 1
+ ktx:version: "2.0"
+ ktx:vkformat: 152
+ oiio:ColorSpace: "srgb_rec709_scene"
diff --git a/testsuite/ktx/run.py b/testsuite/ktx/run.py
new file mode 100644
index 0000000000..3a97ceddfa
--- /dev/null
+++ b/testsuite/ktx/run.py
@@ -0,0 +1,28 @@
+#!/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
+
+# save the error output
+redirect = ' >> out.txt 2>&1 '
+files = [
+ # Raw, uncompressed formats (sRGB)
+ "2d_rgb8.ktx2",
+ "2d_rgba8.ktx2",
+ # Basis Universal formats
+ "2d_uastc.ktx2",
+ "2d_etc1s.ktx2",
+ # GPU-block-compressed formats
+ "2d_astc4x4.ktx2",
+ "2d_bc1.ktx2",
+ "2d_bc3.ktx2",
+ "2d_bc4.ktx2",
+ "2d_bc5.ktx2",
+ "2d_bc7.ktx2",
+ "2d_etc1.ktx2",
+ "2d_etc2.ktx2",
+]
+
+for f in files:
+ command += info_command (OIIO_TESTSUITE_IMAGEDIR + "/" + f)