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)