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