diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d5b49a177..703e80e740 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,7 +189,9 @@ jobs: libuhdr_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 @@ -564,13 +566,15 @@ jobs: TIFF_BUILD_VERSION=4.1.0 TIFF_GIT_COMMIT=e0d707dc1524d8c0e20f03396f234e0f1b07b3f4 LIBRAW_VERSION=0.21.0 - libuhdr_BUILD_VERSION=1.3.0 - libuhdr_GIT_TAG=v1.3.0 - libuhdr_GIT_COMMIT=2222dad8f259fbadd08a481953f1791cbc8f975d - libuhdr_CMAKE_C_COMPILER=gcc - libuhdr_CMAKE_CXX_COMPILER=g++ + ENABLE_KTX=0 + libuhdr_BUILD_VERSION=1.3.0 + libuhdr_GIT_TAG=v1.3.0 + libuhdr_GIT_COMMIT=2222dad8f259fbadd08a481953f1791cbc8f975d + libuhdr_CMAKE_C_COMPILER=gcc + libuhdr_CMAKE_CXX_COMPILER=g++ # 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 @@ -610,6 +614,7 @@ jobs: LIBRAW_VERSION=0.21.0 nanobind_GIT_TAG=v2.8.0 nanobind_GIT_COMMIT=0e7aa61a75052034453cd2b906a79fe222792697 + ENABLE_KTX=0 libuhdr_BUILD_VERSION=1.3.0 libuhdr_GIT_TAG=v1.3.0 libuhdr_GIT_COMMIT=2222dad8f259fbadd08a481953f1791cbc8f975d @@ -617,6 +622,7 @@ jobs: libuhdr_CMAKE_CXX_COMPILER=g++ # 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' @@ -656,6 +662,8 @@ jobs: Robinmap_GIT_COMMIT=188c45569cc2a5dd768077c193830b51d33a5020 TIFF_BUILD_VERSION=4.1.0 TIFF_GIT_COMMIT=e0d707dc1524d8c0e20f03396f234e0f1b07b3f4 + ENABLE_KTX=0 + # libktx > 4.3.2 requires CMake >= 3.22 required_deps: none build_local_deps: 'TIFF' @@ -718,6 +726,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-15-ARM aclang17/C++20/py3.13 diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml index 5ca915a71b..8876df4152 100644 --- a/.github/workflows/wheel.yml +++ b/.github/workflows/wheel.yml @@ -374,6 +374,9 @@ jobs: SKBUILD_BUILD_DIR: "/Users/runner/work/OpenImageIO/OpenImageIO/build" CCACHE_DIR: /Users/runner/.ccache CCACHE_COMPRESSION: yes + # 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) + Ktx_ASTCENC_ISA: "ASTCENC_ISA_SSE41" - name: ccache-save id: ccache-save 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..358b24fdd3 --- /dev/null +++ b/src/cmake/build_Ktx.cmake @@ -0,0 +1,90 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +# TODO: update to v5.0.0 once libktx v5.0.0 is released +set_cache (Ktx_BUILD_VERSION v5.0.0-rc2 "Ktx version for local builds") +set (Ktx_GIT_REPOSITORY "https://github.com/KhronosGroup/KTX-Software.git") +set_cache (Ktx_GIT_TAG "${Ktx_BUILD_VERSION}" "Git branch or tag") +set_cache (Ktx_GIT_COMMIT "16059fa93d834242a0aaed9bb54952d76b143fac" + "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 9c76c16a7a..ba3f176fa8 100644 --- a/src/cmake/externalpackages.cmake +++ b/src/cmake/externalpackages.cmake @@ -296,6 +296,15 @@ 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 libktx tracking +# main branch of KTX-Software because building non-tagged libktx sets +# the version to 0.0.0. +checked_find_package (Ktx + # VERSION_MIN 0.0.0 # If you target main branch of KTX-Software (versionless) + VERSION_MIN 5.0.0 + BUILD_LOCAL missing +) ########################################################################### diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 35bf4fc50c..eb8680af8d 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -394,6 +394,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 + FOUNDVAR Ktx_FOUND ENABLEVAR ENABLE_KTX + IMAGEDIR oiio-images/ktx2) oiio_add_tests (dds ENABLEVAR ENABLE_DDS IMAGEDIR oiio-images/dds URL "Recent checkout of OpenImageIO-images") @@ -650,9 +653,11 @@ function (oiio_get_test_data name) endfunction() function (oiio_setup_test_data) + # TODO: revert this to target dev branch before merging 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 ktx2-add-uncompressed-vkformat-files) + # BRANCH dev-${OpenImageIO_VERSION_MAJOR}.${OpenImageIO_VERSION_MINOR}) oiio_get_test_data (openexr-images REPO https://github.com/AcademySoftwareFoundation/openexr-images.git BRANCH main) diff --git a/src/doc/builtinplugins.md b/src/doc/builtinplugins.md index 89e5709a34..f6be94ac48 100644 --- a/src/doc/builtinplugins.md +++ b/src/doc/builtinplugins.md @@ -1460,6 +1460,432 @@ control aspects of the writing itself: 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. The official +KTX2 container format specification and other helpful info may be found at: + + +There are two main versions of KTX: KTX1 which was deprecated and KTX2 which is +the currently adopted standard. OpenImageIO only supports reading and writing +KTX2 files with the file extension {file}`.ktx2`. If you have a KTX1 file +(file extension {file}`.ktx` or {file}`.ktx1`), you are advised to use the CLI +provided by KTX Tools to losslessly convert it into a KTX2 file. + +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. KTX2 files use the file extension {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 (think of the many different compression schemes and their options) 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. + +KTX2 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 (e.g., for BCn, usually only +on desktop devices). KTX has additional support for latent codecs that can be +transcoded on the fly to the most compatible and supported GPU format. KTX2 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). KTX2 also provides supports for rate distortion +optimization (RDO) which can significantly reduce file size with slightly +noticeable quality loss. + +Since OpenImageIO also supports DDS files, one might be tempted to do +conversions to KTX2 using oiiotool CLI. This is not the ideal approach. Instead, +use the lossless DDS-to-KTX2 CLI conversion tool provided by KTX Tools. + +KTX2 plugin support is mainly provided through the official libktx library. +OpenImageIO does not do any encoding/deconding by its own and simply delegates +all operations to libktx. libktx requires, however, a significant amount of +parameters to configure the different formats/codecs KTX2 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 KTX2 plugin provides support for both input and output, users are +discouraged from reading KTX2 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 KTX2 container uses a lossless compression +format but that is rarely the case with KTX2 files. + +It is important to note that KTX2 specification does not force the mention of +which parameters were used to, say, generate a UASTC format. This plugin relies +on the heuristic that KTX2 files are most likely created via the official KTX2 +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 KTX2 input which is not the +intended use-case of this plugin within OpenImageIO. + +**Attributes** + +```{eval-rst} +.. list-table:: + :widths: 30 10 65 + :header-rows: 1 + + * - ImageSpec Attribute + - Type + - Meaning + * - ``oiio:ColorSpace`` + - string + - Color space (see Section :ref:`sec-metadata-color`). KTX2 supports a wide + range of color spaces. + * - ``oiio:subimages`` + - int + - Number of layers in a KTX2 array texture. Only set if input KTX2 texture + is an array texture. + * - ``oiio:miplevels`` + - int + - Number of mip levels for the input KTX2 texture. Only set if the texture + contains mip levels (i.e., number of miplevels > 1). + * - ``textureformat`` + - string + - Texture format/kind. Can be one of the following values: + ``"Plain Texture"``, ``"Volume Texture"``, or ``"CubeFace Environment"``. + This attribute by itself does not convey whether the texture is an array + texture and/or whether it is 1D (1D, 2D and 2D array textures are all + ``"Plain Texture"``'s). For the writer, this has to be set for cubemaps + or volumes with a single slice. + * - ``compression`` + - string + - Super-compression scheme (i.e., additional layer of lossless compression + on top of either uncompressed, GPU-block-compressed, or + Basis-Universal-encoded texture). This is not to be confused with + ``ktx:codec`` which describes the Basis Universal codec or GPU + block-compression scheme. For the writer, it is highly recommended to set + this if RDO is applied. Can be one of the following values: + ``"zstd"`` ``"zip"`` + BasisLZ is ignored because it is set by default when ETC1S codec is used. + ``"zip"`` refers to KTX's ``zlib`` super-compression scheme. + For ``"zstd"`` and ``"zip"`` the compression level + may be optionally appended to the compression name after a colon, like + this: ``"zstd:10"`` or ``"zip:1"``. The compression level range for + ``"zstd"`` is [1,22] (``1`` being potentially the fastest but the lowest + compression). Defaults to ``22``. The compression level range for + ``"zip"`` (i.e., ``ZLIB``) is [1,9] (likewise, ``1`` being potentially + the fastest but the lowest compression). Defaults to ``9``. If the writer + receives a request for a compression type it does not recognize or is not + supported, it will use ``"zstd"`` by default (i.e., ``"zstd"`` with + highest compression level). + * - ``ktx:codec`` + - string + - Basis Universal codec or GPU-block compression scheme. Can be one of the + following values: + ``"astc"`` ``"uastc"`` ``"uastc-ldr"`` ``"uastc-ldr-4x4"`` + ``"etc1s"`` ``"uastc-hdr"`` ``"uastc-hdr-4x4"`` ``"uastc-hdr-6x6"`` + Both ``"uastc"`` and ``"uastc-ldr"`` are aliases for ``"uastc-ldr-4x4"``. + Similarly, ``"uastc-hdr"`` is an alias for ``"uastc-hdr-4x4"``. + * - ``ktx:1d`` + - int (bool) + - Only set to ``true`` if the input KTX2 texture is 1D. + * - ``ktx:generate_mipmaps`` + - 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 API. For the writer, if + this is set to ``true`` then the ktxoutput instance cannot be opened with + ``AppendMIPLevel`` mode. If not set, defaults to ``false``. + * - ``ktx:normalized`` + - int (bool) + - Whether the Vulkan Format is/should be normalized if possible (UNORM or + SNORM based on the ImageSpec type). For the writer, this attribute is + ignored if the colorspace is SRGB or the ImageSpec type does not have a + corresponding UNORM/SNORM VkFormat output (e.g., UINT64). Defaults to + ``true`` if not set. + * - *other* + - + - All other arbitrary metadata read from the input KTX2 file are stored. + Particularly, if the input KTX2 file is created via the official KTX + Tools CLI, the metadata entry `KTXwriterScParams` is parsed and the + codec/compression parameters are stored as attributes. These attributes + are described in detail in the subsection + `Configuration settings for KTX output`. +``` + +**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: + +```{eval-rst} +.. 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: + +```{eval-rst} +.. list-table:: + :widths: 30 10 65 + :header-rows: 1 + + * - Output Configuration Attribute + - Type + - Meaning + * - ``ktx:tf`` + - string + - Force the created texture to have the specified transfer function, + ignoring the transfer function of the currently set color space. Case + insensitive. Can be one of the following values: + ``"none"`` ``"linear"`` ``"srgb"`` ``"srgb_eotf"`` ``"scrgb"`` + ``"scrgb_eotf"`` ``"itu"`` ``"itu_oetf"`` ``"bt601"`` + ``"bt601_oetf"`` ``"bt709"`` ``"bt709_oetf"`` ``"bt2020"`` + ``"bt2020_oetf"`` ``"smpte170m"`` ``"smpte170m_oetf"`` + ``"smpte170m_eotf"`` ``"ntsc"`` ``"ntsc_eotf"`` ``"slog"`` + ``"slog_oetf"`` ``"slog2"`` ``"slog2_oetf"`` ``"bt1886"`` + ``"bt1886_eotf"`` ``"hlg_oetf"`` ``"hlg_eotf"`` ``"pq_oetf"`` + ``"pg_eotf"`` ``"dcip3"`` ``"dcip3_eotf"`` ``"pal_oetf"`` + ``"pal625_eotf"`` ``"st240"`` ``"st240_oetf"`` ``"st240_eotf"`` + ``"acescc"`` ``"acescc_oetf"`` ``"acescct"`` ``"acescct_oetf"`` + ``"abobergb"`` ``"adobergb_eotf"`` + This is just an assignment and does not perform any conversions. + * - ``ktx:primaries`` + - string + - Force the created texture to have the specified color primaries, ignoring + the color primaries of the currently set color space. Can be one of the + following values: + ``"none"`` ``"bt709"`` ``"srgb"`` ``"bt601_ebu"`` ``"bt601_smpte"`` + ``"bt2020"`` ``"ciexyz"`` ``"aces"`` ``"acescc"`` ``"ntsc1953"`` + ``"pal525"`` ``"displayp3"`` ``"adobergb"`` + If the output KTX2 file is intended to be used within glTF then either + this MUST be set to ``"none"`` or ``oiio:ColorSpace`` to ``"data"``. + This attribute replicates the functionality of KTX Tools' + "--assign-primaries" CLI option (i.e., this is just an assignment and + does not perform any conversions). + * - ``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:no_sse`` + - 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:pre_swizzle`` + - 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:input_swizzle`` + - char[4] + - A swizzle to apply before encoding. It must match the regular expression + `/^[rgba01]{4}$/`. Should not be specified if ``ktx:pre_swizzle`` is set. + Defaults to no swizzle (i.e., zero'ed array). + * - ``ktx:uastc_flags`` + - uint + - A set of ``ktx_pack_uastc_flag_bits`` controlling UASTC encoding. The + least significant 4 bit are the most important and determine the quality + level for both UASTC-LDR and UASTC-HDR. + Defaults to 0 (maps to libktx' KTX_PACK_UASTC_LEVEL_FASTEST). + * - ``ktx:uastc_rdo`` + - int (bool) + - Whether to enable Rate Distortion Optimization (RDO) post-processing. + If this is set to true, then you are advised to set + ``ktx::super_compression_scheme`` to actually benefit from RDO size + reduction otherwise there is no point in using RDO without applying a + DEFLATE-based lossless compression. + Defaults to false. + * - ``ktx:uastc_rdo_quality_scalar`` + - 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]. Only used if ``ktx::uastc_rdo`` is set to + true. Ignored otherwise. + Defaults to 1.0. + * - ``ktx:uastc_rdo_dict_size`` + - uint + - UASTC RDO dictionary size in bytes. Lower values=faster, but give less + compression. Range is [64,65536]. Only used if ``ktx::uastc_rdo`` is set + to true. Ignored otherwise. + Defaults to 4096. + * - ``ktx:uastc_rdo_max_smooth_block_error_scale`` + - 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. Only used if ``ktx::uastc_rdo`` is set to true. Ignored + otherwise. + Defaults to 10.0 + * - ``ktx:uastc_rdo_max_smooth_block_std_dev`` + - float + - UASTC RDO max smooth block standard deviation. Range is [.01,65536.0]. + Larger values expand the range of blocks considered smooth. Only used if + ``ktx::uastc_rdo`` is set to true. Ignored otherwise. + Defaults to 18.0. + * - ``ktx:uastc_rdo_dont_favor_simpler_modes`` + - int (bool) + - Do not favor simpler UASTC modes in RDO mode. Only used if + ``ktx::uastc_rdo`` is set to true. Ignored otherwise. + Defaults to false. + * - ``ktx:uastc_rdo_no_multithreading`` + - int (bool) + - Disable RDO multithreading (slightly higher compression, deterministic). + Only used if ``ktx::uastc_rdo`` is set to true. Ignored otherwise. + Defaults to false. + * - ``ktx:uastc_hdr_level`` + - uint + - Sets the UASTC HDR 4x4 compressor's level. Valid range is [0,4]. + Higher values means slower execution but higher quality. Only valid if + ``compression`` is set to ``UASTC-LDR-4x4`` or any of its aliases. + Ignored otherwise. + Defaults to 1. + * - ``ktx:uastc_hdr_quality`` + - uint + - 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. Only used if ``compression`` + is set to ``UASTC-HDR-4x4`` or any of its aliases. Ignored otherwise. + Defaults to 1. + * - ``ktx:uastc_hdr_uber_mode`` + - int (bool) + - 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. Only used if ``compression`` is set to + ``UASTC-HDR-4x4`` or any of its aliases. Ignored otherwise. + Defaults to false. + * - ``ktx:uastc_hdr_ultra_quant`` + - int (bool) + - Try to find better quantized CEM 7/11 endpoint values (slower). Only used + if ``compression`` is set to ``UASTC-HDR-4x4`` or any of its aliases. + Ignored otherwise. + Defaults to false. + * - ``ktx:uastc_hdr_favor_astc`` + - int (bool) + - 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. Only used if ``compression`` is + set to ``UASTC-HDR-4x4`` or any of its aliases. Ignored otherwise. + Defaults to false. + * - ``ktx:uastc_hdr_lambda`` + - 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. Only used if + ``compression`` is set to ``UASTC-HDR-6x6`` or any of its aliases. + Ignored otherwise. + Defaults to 0. + * - ``ktx:uastc_hdr_level`` + - uint + - UASTC HDR 6x6i specific option: Controls the 6x6 HDR intermediate mode + encoder performance vs. max quality tradeoff. X may range from [0,12]. + Only used if ``compression`` is set to ``UASTC-HDR-6x6`` or any of its + aliases. Ignored otherwise. + Defaults to 2. + * - ``ktx:etc1s_compression_level`` + - 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. Only used if ``compression`` is set to + ``ETC1S``. Ignored otherwise. + Defaults to 2. + * - ``ktx:etc1s_quality_level`` + - 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:etc1s_max_endpoints``, ``ktx:etc1s_max_selectors``, + ``ktx:etc1s_endpoint_rdo_threshold`` and + ``ktx:etc1s_selector_rdo_threshold`` for the target quality level. + Setting these parameters overrides the values determined by + ``ktx:etc1s_quality_level``. + Defaults to 128 if neither ``ktx:etc1s_max_endpoints`` nor + ``ktx:etc1s_max_selectors`` have been set. Only used if ``compression`` + is set to ``ETC1S``. Ignored otherwise. + * - ``ktx:etc1s_max_endpoints`` + - uint + - Manually set the max number of color endpoint clusters. Range is + [1,16128]. If this is set, ``ktx:etc1s_max_selectors`` must also be set, + otherwise the value will be ignored. Only used if ``compression`` is set + to ``ETC1S``. Ignored otherwise. + Defaults to 0. + * - ``ktx:etc1s_endpoint_rdo_threshold`` + - 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. Only used if + ``compression`` is set to ``ETC1S``. Ignored otherwise. + Defaults to 1.25. + * - ``ktx:etc1s_max_selectors`` + - uint + - Manually set the max number of color selector clusters. Range is + [1,16128]. If this is set, ``ktx:etc1s_max_endpoints`` must also be set, + otherwise the value will be ignored. Only used if ``compression`` is set + to ``ETC1S``. Ignored otherwise. + Defaults to 0. + * - ``ktx:etc1s_selector_rdo_threshold`` + - 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:etc1s_quality_level``. Only used if ``compression`` is set + to ``ETC1S``. Ignored otherwise. + Defaults to 1.5. + * - ``ktx:etc1s_no_endpoint_rdo`` + - int (bool) + - Disable endpoint rate distortion optimizations. Slightly faster, less + noisy output, but lower quality per output bit. Only used if + ``compression`` is set to ``ETC1S``. Ignored otherwise. + Defaults to false. + * - ``ktx:etc1s_no_selector_rdo`` + - int (bool) + - Disable selector rate distortion optimizations. Slightly faster, less + noisy output, but lower quality per output bit. Only used if + ``compression`` is set to ``ETC1S``. Ignored otherwise. + Defaults to false. + * - ``ktx:astc_block_dimensions`` + - string + - ASTC block dimensions. Can be set to any of the following values: + ``"4x4"`` ``"5x4"`` ``"5x5"`` ``"6x5"`` ``"6x6"`` ``"8x5"`` + ``"8x6"`` ``"10x5"`` ``"10x6"`` ``"8x8"`` ``"10x8"`` ``"10x10"`` + ``"12x10"`` ``"12x12"`` ``"3x3x3"`` ``"4x3x3"`` ``"4x4x3"`` + ``"4x4x4"`` ``"5x4x4"`` ``"5x5x4"`` ``"5x5x5"`` ``"6x5x5"`` + ``"6x6x5"`` ``"6x6x6"`` + Defaults to ``"4x4"``. + * - ``ktx:astc_quality_level`` + - uint + - ASTC quality level. Range is [0, 100]. 0 is fastest/lowest compression. + 100 is slowest/highest compression. + Defaults to 60 (i.e., medium quality). + * - ``ktx:perceptual`` + - int (bool) + - Whether the codec should optimize for perceptual error, instead of direct + RMS error. This aims to improves perceived image quality, but typically + lowers the measured PSNR score. Perceptual methods are currently only + available for normal maps and RGB color data. + Defaults to false. +``` + (sec-bundledplugins-ffmpeg)= ## Movie formats (using ffmpeg) diff --git a/src/iv/imageviewer.cpp b/src/iv/imageviewer.cpp index 209aaef05c..32d1e2d243 100644 --- a/src/iv/imageviewer.cpp +++ b/src/iv/imageviewer.cpp @@ -70,6 +70,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..8f60a1ec83 --- /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-22` 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..1bf2e8edc5 --- /dev/null +++ b/src/ktx.imageio/ktx_pvt.h @@ -0,0 +1,949 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#pragma once + +#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, +}; + + + +// clang-format off +static std::unordered_map astc_block_dims_str_to_enum{ + { "4x4", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_4x4 }, + { "5x4", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_5x4 }, + { "5x5", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_5x5 }, + { "6x5", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_6x5 }, + { "6x6", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_6x6 }, + { "8x5", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_8x5 }, + { "8x6", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_8x6 }, + { "10x5", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_10x5 }, + { "10x6", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_10x6 }, + { "8x8", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_8x8 }, + { "10x8", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_10x8 }, + { "10x10", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_10x10 }, + { "12x10", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_12x10 }, + { "12x12", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_12x12 }, + { "3x3x3", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_3x3x3 }, + { "4x3x3", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_4x3x3 }, + { "4x4x3", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_4x4x3 }, + { "4x4x4", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_4x4x4 }, + { "5x4x4", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_5x4x4 }, + { "5x5x4", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_5x5x4 }, + { "5x5x5", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_5x5x5 }, + { "6x5x5", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_6x5x5 }, + { "6x6x5", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_6x6x5 }, + { "6x6x6", ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_6x6x6 }, +}; +// clang-format on + + + +// clang-format off +static std::unordered_map primaries_str_to_enum{ + { "none", khr_df_primaries_e::KHR_DF_PRIMARIES_UNSPECIFIED }, + { "bt709", khr_df_primaries_e::KHR_DF_PRIMARIES_BT709 }, + { "srgb", khr_df_primaries_e::KHR_DF_PRIMARIES_SRGB }, + { "bt601_ebu", khr_df_primaries_e::KHR_DF_PRIMARIES_BT601_EBU }, + { "bt601_smpte", khr_df_primaries_e::KHR_DF_PRIMARIES_BT601_SMPTE }, + { "bt2020", khr_df_primaries_e::KHR_DF_PRIMARIES_BT2020 }, + { "ciexyz", khr_df_primaries_e::KHR_DF_PRIMARIES_CIEXYZ }, + { "aces", khr_df_primaries_e::KHR_DF_PRIMARIES_ACES }, + { "acescc", khr_df_primaries_e::KHR_DF_PRIMARIES_ACESCC }, + { "ntsc1953", khr_df_primaries_e::KHR_DF_PRIMARIES_NTSC1953 }, + { "pal525", khr_df_primaries_e::KHR_DF_PRIMARIES_PAL525 }, + { "displayp3", khr_df_primaries_e::KHR_DF_PRIMARIES_DISPLAYP3 }, + { "adobergb", khr_df_primaries_e::KHR_DF_PRIMARIES_ADOBERGB }, +}; +// clang-format on + + + +// clang-format off +static std::unordered_map tf_str_to_enum{ + { "none", khr_df_transfer_e::KHR_DF_TRANSFER_UNSPECIFIED }, + { "linear", khr_df_transfer_e::KHR_DF_TRANSFER_LINEAR }, + { "srgb", khr_df_transfer_e::KHR_DF_TRANSFER_SRGB }, + { "srgb_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_SRGB_EOTF }, + { "scrgb", khr_df_transfer_e::KHR_DF_TRANSFER_SCRGB }, + { "scrgb_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_SCRGB_EOTF }, + { "itu", khr_df_transfer_e::KHR_DF_TRANSFER_ITU }, + { "itu_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_ITU_OETF }, + { "bt601", khr_df_transfer_e::KHR_DF_TRANSFER_BT601 }, + { "bt601_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_BT601_OETF }, + { "bt709", khr_df_transfer_e::KHR_DF_TRANSFER_BT709 }, + { "bt709_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_BT709_OETF }, + { "bt2020", khr_df_transfer_e::KHR_DF_TRANSFER_BT2020 }, + { "bt2020_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_BT2020_OETF }, + { "smpte170m", khr_df_transfer_e::KHR_DF_TRANSFER_SMTPE170M }, + { "smpte170m_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_SMTPE170M_OETF }, + { "smpte170m_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_SMTPE170M_EOTF }, + { "ntsc", khr_df_transfer_e::KHR_DF_TRANSFER_NTSC }, + { "ntsc_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_NTSC_EOTF }, + { "slog", khr_df_transfer_e::KHR_DF_TRANSFER_SLOG }, + { "slog_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_SLOG_OETF }, + { "slog2", khr_df_transfer_e::KHR_DF_TRANSFER_SLOG2 }, + { "slog2_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_SLOG2_OETF }, + { "bt1886", khr_df_transfer_e::KHR_DF_TRANSFER_BT1886 }, + { "bt1886_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_BT1886_EOTF }, + { "hlg_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_HLG_OETF }, + { "hlg_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_HLG_EOTF }, + { "pq_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_PQ_OETF}, + { "pg_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_PQ_EOTF }, + { "dcip3", khr_df_transfer_e::KHR_DF_TRANSFER_DCIP3 }, + { "dcip3_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_DCIP3_EOTF }, + { "pal_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_PAL_OETF }, + { "pal625_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_PAL625_EOTF }, + { "st240", khr_df_transfer_e::KHR_DF_TRANSFER_ST240 }, + { "st240_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_ST240_OETF }, + { "st240_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_ST240_EOTF }, + { "acescc", khr_df_transfer_e::KHR_DF_TRANSFER_ACESCC }, + { "acescc_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_ACESCC_OETF }, + { "acescct", khr_df_transfer_e::KHR_DF_TRANSFER_ACESCCT }, + { "acescct_oetf", khr_df_transfer_e::KHR_DF_TRANSFER_ACESCCT_OETF }, + { "abobergb", khr_df_transfer_e::KHR_DF_TRANSFER_ADOBERGB }, + { "adobergb_eotf", khr_df_transfer_e::KHR_DF_TRANSFER_ADOBERGB_EOTF }, +}; +// clang-format on + + + +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)); +} + + + +static inline std::string +basis_universal_codec_name(khr_df_model_e color_model) +{ + switch (color_model) { + // UASTC codecs + case khr_df_model_e::KHR_DF_MODEL_UASTC_LDR_4x4: return "uastc-ldr-4x4"; + case khr_df_model_e::KHR_DF_MODEL_UASTC_HDR_4x4: return "uastc-hdr-4x4"; + case khr_df_model_e::KHR_DF_MODEL_UASTC_HDR_6x6: return "uastc-hdr-6x6"; + // ETC1S codecs + case khr_df_model_e::KHR_DF_MODEL_ETC1S: return "etc1s"; + default: break; + } + return std::to_string(static_cast(color_model)); +} + + + +// clang-format off +// +// 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, // <-- NOT SUPPORTED + VK_FORMAT_R4G4B4A4_UNORM_PACK16 = 2, // <-- NOT SUPPORTED + VK_FORMAT_B4G4R4A4_UNORM_PACK16 = 3, // <-- NOT SUPPORTED + VK_FORMAT_R5G6B5_UNORM_PACK16 = 4, // <-- NOT SUPPORTED + VK_FORMAT_B5G6R5_UNORM_PACK16 = 5, // <-- NOT SUPPORTED + VK_FORMAT_R5G5B5A1_UNORM_PACK16 = 6, // <-- NOT SUPPORTED + VK_FORMAT_B5G5R5A1_UNORM_PACK16 = 7, // <-- NOT SUPPORTED + VK_FORMAT_A1R5G5B5_UNORM_PACK16 = 8, // <-- NOT SUPPORTED + VK_FORMAT_R8_UNORM = 9, + VK_FORMAT_R8_SNORM = 10, + VK_FORMAT_R8_USCALED = 11, // <-- NOT SUPPORTED + VK_FORMAT_R8_SSCALED = 12, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_R8G8_SSCALED = 19, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_R8G8B8_SSCALED = 26, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_B8G8R8_SSCALED = 33, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_R8G8B8A8_SSCALED = 40, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_B8G8R8A8_SSCALED = 47, // <-- NOT SUPPORTED + VK_FORMAT_B8G8R8A8_UINT = 48, + VK_FORMAT_B8G8R8A8_SINT = 49, + VK_FORMAT_B8G8R8A8_SRGB = 50, + VK_FORMAT_A8B8G8R8_UNORM_PACK32 = 51, // <-- NOT SUPPORTED + VK_FORMAT_A8B8G8R8_SNORM_PACK32 = 52, // <-- NOT SUPPORTED + VK_FORMAT_A8B8G8R8_USCALED_PACK32 = 53, // <-- NOT SUPPORTED + VK_FORMAT_A8B8G8R8_SSCALED_PACK32 = 54, // <-- NOT SUPPORTED + VK_FORMAT_A8B8G8R8_UINT_PACK32 = 55, // <-- NOT SUPPORTED + VK_FORMAT_A8B8G8R8_SINT_PACK32 = 56, // <-- NOT SUPPORTED + VK_FORMAT_A8B8G8R8_SRGB_PACK32 = 57, // <-- NOT SUPPORTED + VK_FORMAT_A2R10G10B10_UNORM_PACK32 = 58, // <-- NOT SUPPORTED + VK_FORMAT_A2R10G10B10_SNORM_PACK32 = 59, // <-- NOT SUPPORTED + VK_FORMAT_A2R10G10B10_USCALED_PACK32 = 60, // <-- NOT SUPPORTED + VK_FORMAT_A2R10G10B10_SSCALED_PACK32 = 61, // <-- NOT SUPPORTED + VK_FORMAT_A2R10G10B10_UINT_PACK32 = 62, // <-- NOT SUPPORTED + VK_FORMAT_A2R10G10B10_SINT_PACK32 = 63, // <-- NOT SUPPORTED + VK_FORMAT_A2B10G10R10_UNORM_PACK32 = 64, // <-- NOT SUPPORTED + VK_FORMAT_A2B10G10R10_SNORM_PACK32 = 65, // <-- NOT SUPPORTED + VK_FORMAT_A2B10G10R10_USCALED_PACK32 = 66, // <-- NOT SUPPORTED + VK_FORMAT_A2B10G10R10_SSCALED_PACK32 = 67, // <-- NOT SUPPORTED + VK_FORMAT_A2B10G10R10_UINT_PACK32 = 68, // <-- NOT SUPPORTED + VK_FORMAT_A2B10G10R10_SINT_PACK32 = 69, // <-- NOT SUPPORTED + VK_FORMAT_R16_UNORM = 70, + VK_FORMAT_R16_SNORM = 71, + VK_FORMAT_R16_USCALED = 72, // <-- NOT SUPPORTED + VK_FORMAT_R16_SSCALED = 73, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_R16G16_SSCALED = 80, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_R16G16B16_SSCALED = 87, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_R16G16B16A16_SSCALED = 94, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_E5B9G9R9_UFLOAT_PACK32 = 123, // <-- NOT SUPPORTED + VK_FORMAT_D16_UNORM = 124, // <-- NOT SUPPORTED + VK_FORMAT_X8_D24_UNORM_PACK32 = 125, // <-- NOT SUPPORTED + VK_FORMAT_D32_SFLOAT = 126, // <-- NOT SUPPORTED + VK_FORMAT_S8_UINT = 127, // <-- NOT SUPPORTED + VK_FORMAT_D16_UNORM_S8_UINT = 128, // <-- NOT SUPPORTED + VK_FORMAT_D24_UNORM_S8_UINT = 129, // <-- NOT SUPPORTED + VK_FORMAT_D32_SFLOAT_S8_UINT = 130, // <-- NOT SUPPORTED + VK_FORMAT_BC1_RGB_UNORM_BLOCK = 131, // TODO: BCn formats will be supported soon + 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, // <-- NOT SUPPORTED + VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK = 148, // <-- NOT SUPPORTED + VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK = 149, // <-- NOT SUPPORTED + VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK = 150, // <-- NOT SUPPORTED + VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK = 151, // <-- NOT SUPPORTED + VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK = 152, // <-- NOT SUPPORTED + VK_FORMAT_EAC_R11_UNORM_BLOCK = 153, // <-- NOT SUPPORTED + VK_FORMAT_EAC_R11_SNORM_BLOCK = 154, // <-- NOT SUPPORTED + VK_FORMAT_EAC_R11G11_UNORM_BLOCK = 155, // <-- NOT SUPPORTED + VK_FORMAT_EAC_R11G11_SNORM_BLOCK = 156, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_B8G8R8G8_422_UNORM = 1000156001, // <-- NOT SUPPORTED + VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM = 1000156002, // <-- NOT SUPPORTED + VK_FORMAT_G8_B8R8_2PLANE_420_UNORM = 1000156003, // <-- NOT SUPPORTED + VK_FORMAT_G8_B8_R8_3PLANE_422_UNORM = 1000156004, // <-- NOT SUPPORTED + VK_FORMAT_G8_B8R8_2PLANE_422_UNORM = 1000156005, // <-- NOT SUPPORTED + VK_FORMAT_G8_B8_R8_3PLANE_444_UNORM = 1000156006, // <-- NOT SUPPORTED + VK_FORMAT_R10X6_UNORM_PACK16 = 1000156007, // <-- NOT SUPPORTED + VK_FORMAT_R10X6G10X6_UNORM_2PACK16 = 1000156008, // <-- NOT SUPPORTED + VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16 = 1000156009, // <-- NOT SUPPORTED + VK_FORMAT_G10X6B10X6G10X6R10X6_422_UNORM_4PACK16 = 1000156010, // <-- NOT SUPPORTED + VK_FORMAT_B10X6G10X6R10X6G10X6_422_UNORM_4PACK16 = 1000156011, // <-- NOT SUPPORTED + VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_420_UNORM_3PACK16 = 1000156012, // <-- NOT SUPPORTED + VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16 = 1000156013, // <-- NOT SUPPORTED + VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_422_UNORM_3PACK16 = 1000156014, // <-- NOT SUPPORTED + VK_FORMAT_G10X6_B10X6R10X6_2PLANE_422_UNORM_3PACK16 = 1000156015, // <-- NOT SUPPORTED + VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_444_UNORM_3PACK16 = 1000156016, // <-- NOT SUPPORTED + VK_FORMAT_R12X4_UNORM_PACK16 = 1000156017, // <-- NOT SUPPORTED + VK_FORMAT_R12X4G12X4_UNORM_2PACK16 = 1000156018, // <-- NOT SUPPORTED + VK_FORMAT_R12X4G12X4B12X4A12X4_UNORM_4PACK16 = 1000156019, // <-- NOT SUPPORTED + VK_FORMAT_G12X4B12X4G12X4R12X4_422_UNORM_4PACK16 = 1000156020, // <-- NOT SUPPORTED + VK_FORMAT_B12X4G12X4R12X4G12X4_422_UNORM_4PACK16 = 1000156021, // <-- NOT SUPPORTED + VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_420_UNORM_3PACK16 = 1000156022, // <-- NOT SUPPORTED + VK_FORMAT_G12X4_B12X4R12X4_2PLANE_420_UNORM_3PACK16 = 1000156023, // <-- NOT SUPPORTED + VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_422_UNORM_3PACK16 = 1000156024, // <-- NOT SUPPORTED + VK_FORMAT_G12X4_B12X4R12X4_2PLANE_422_UNORM_3PACK16 = 1000156025, // <-- NOT SUPPORTED + VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_444_UNORM_3PACK16 = 1000156026, // <-- NOT SUPPORTED + VK_FORMAT_G16B16G16R16_422_UNORM = 1000156027, // <-- NOT SUPPORTED + VK_FORMAT_B16G16R16G16_422_UNORM = 1000156028, // <-- NOT SUPPORTED + VK_FORMAT_G16_B16_R16_3PLANE_420_UNORM = 1000156029, // <-- NOT SUPPORTED + VK_FORMAT_G16_B16R16_2PLANE_420_UNORM = 1000156030, // <-- NOT SUPPORTED + VK_FORMAT_G16_B16_R16_3PLANE_422_UNORM = 1000156031, // <-- NOT SUPPORTED + VK_FORMAT_G16_B16R16_2PLANE_422_UNORM = 1000156032, // <-- NOT SUPPORTED + VK_FORMAT_G16_B16_R16_3PLANE_444_UNORM = 1000156033, // <-- NOT SUPPORTED + VK_FORMAT_G8_B8R8_2PLANE_444_UNORM = 1000330000, // <-- NOT SUPPORTED + VK_FORMAT_G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16 = 1000330001, // <-- NOT SUPPORTED + VK_FORMAT_G12X4_B12X4R12X4_2PLANE_444_UNORM_3PACK16 = 1000330002, // <-- NOT SUPPORTED + VK_FORMAT_G16_B16R16_2PLANE_444_UNORM = 1000330003, // <-- NOT SUPPORTED + VK_FORMAT_A4R4G4B4_UNORM_PACK16 = 1000340000, // <-- NOT SUPPORTED + VK_FORMAT_A4B4G4R4_UNORM_PACK16 = 1000340001, // <-- NOT SUPPORTED + 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, // <-- NOT SUPPORTED + VK_FORMAT_PVRTC1_4BPP_UNORM_BLOCK_IMG = 1000054001, // <-- NOT SUPPORTED + VK_FORMAT_PVRTC2_2BPP_UNORM_BLOCK_IMG = 1000054002, // <-- NOT SUPPORTED + VK_FORMAT_PVRTC2_4BPP_UNORM_BLOCK_IMG = 1000054003, // <-- NOT SUPPORTED + VK_FORMAT_PVRTC1_2BPP_SRGB_BLOCK_IMG = 1000054004, // <-- NOT SUPPORTED + VK_FORMAT_PVRTC1_4BPP_SRGB_BLOCK_IMG = 1000054005, // <-- NOT SUPPORTED + VK_FORMAT_PVRTC2_2BPP_SRGB_BLOCK_IMG = 1000054006, // <-- NOT SUPPORTED + VK_FORMAT_PVRTC2_4BPP_SRGB_BLOCK_IMG = 1000054007, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_3x3x3_UNORM_BLOCK_EXT = 1000288000, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_3x3x3_SRGB_BLOCK_EXT = 1000288001, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_3x3x3_SFLOAT_BLOCK_EXT = 1000288002, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_4x3x3_UNORM_BLOCK_EXT = 1000288003, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_4x3x3_SRGB_BLOCK_EXT = 1000288004, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_4x3x3_SFLOAT_BLOCK_EXT = 1000288005, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_4x4x3_UNORM_BLOCK_EXT = 1000288006, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_4x4x3_SRGB_BLOCK_EXT = 1000288007, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_4x4x3_SFLOAT_BLOCK_EXT = 1000288008, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_4x4x4_UNORM_BLOCK_EXT = 1000288009, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_4x4x4_SRGB_BLOCK_EXT = 1000288010, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_4x4x4_SFLOAT_BLOCK_EXT = 1000288011, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_5x4x4_UNORM_BLOCK_EXT = 1000288012, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_5x4x4_SRGB_BLOCK_EXT = 1000288013, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_5x4x4_SFLOAT_BLOCK_EXT = 1000288014, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_5x5x4_UNORM_BLOCK_EXT = 1000288015, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_5x5x4_SRGB_BLOCK_EXT = 1000288016, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_5x5x4_SFLOAT_BLOCK_EXT = 1000288017, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_5x5x5_UNORM_BLOCK_EXT = 1000288018, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_5x5x5_SRGB_BLOCK_EXT = 1000288019, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_5x5x5_SFLOAT_BLOCK_EXT = 1000288020, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_6x5x5_UNORM_BLOCK_EXT = 1000288021, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_6x5x5_SRGB_BLOCK_EXT = 1000288022, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_6x5x5_SFLOAT_BLOCK_EXT = 1000288023, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_6x6x5_UNORM_BLOCK_EXT = 1000288024, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_6x6x5_SRGB_BLOCK_EXT = 1000288025, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_6x6x5_SFLOAT_BLOCK_EXT = 1000288026, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_6x6x6_UNORM_BLOCK_EXT = 1000288027, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_6x6x6_SRGB_BLOCK_EXT = 1000288028, // <-- NOT SUPPORTED + VK_FORMAT_ASTC_6x6x6_SFLOAT_BLOCK_EXT = 1000288029, // <-- NOT SUPPORTED + VK_FORMAT_R16G16_SFIXED5_NV = 1000464000, // <-- NOT SUPPORTED + VK_FORMAT_A1B5G5R5_UNORM_PACK16_KHR = 1000470000, // <-- NOT SUPPORTED + VK_FORMAT_A8_UNORM_KHR = 1000470001, // <-- NOT SUPPORTED + VK_FORMAT_MAX_ENUM = 0x7FFFFFFF +}; +// clang-format on + + + +struct FormatInfo { + int nbrchannels; + TypeDesc typedesc; + BlockCompression compression { BlockCompression::NONE }; + VkFormat uncompressed_format { VK_FORMAT_UNDEFINED }; + /* Does the VkFormat contain a UNORM or SNORM specifier? */ + bool normalized_vkformat { true }; + /* ASTC block dimensions. Only valid if compression is `ASTC` */ + ktx_pack_astc_block_dimension_e astc_block_dims { + ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_4x4 + }; +}; + + + +// +// Get ImageSpec-related information, block compression type (if any), and +// uncompressed VkFormat (for block-compressed formats) for the given VkFormat. +// +// Notes: +// 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) { + /* ------------------------------------------------------------------ */ + /* ------------------ 8-bit uncompressed formats -------------------- */ + /* ------------------------------------------------------------------ */ + + // 8-bit R[G[B[A]]] uncompressed UNORM 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: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::NONE }; return true; + case VK_FORMAT_R8G8B8A8_UNORM: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::NONE }; return true; + + // 8-bit R[G[B[A]]] uncompressed SNORM formats + case VK_FORMAT_R8_SNORM: formatinfo = { 1, TypeDesc::INT8, BlockCompression::NONE }; return true; + case VK_FORMAT_R8G8_SNORM: formatinfo = { 2, TypeDesc::INT8, BlockCompression::NONE }; return true; + case VK_FORMAT_R8G8B8_SNORM: formatinfo = { 3, TypeDesc::INT8, BlockCompression::NONE }; return true; + case VK_FORMAT_R8G8B8A8_SNORM: formatinfo = { 4, TypeDesc::INT8, BlockCompression::NONE }; return true; + + // 8-bit R[G[B[A]]] uncompressed SRGB formats + case VK_FORMAT_R8_SRGB: formatinfo = { 1, TypeDesc::UINT8, BlockCompression::NONE }; return true; + case VK_FORMAT_R8G8_SRGB: formatinfo = { 2, TypeDesc::UINT8, BlockCompression::NONE }; return true; + case VK_FORMAT_R8G8B8_SRGB: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::NONE }; return true; + case VK_FORMAT_R8G8B8A8_SRGB: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::NONE }; return true; + + // 8-bit R[G[B[A]]] uncompressed UINT formats (not normalized) + case VK_FORMAT_R8_UINT: formatinfo = { 1, TypeDesc::UINT8, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R8G8_UINT: formatinfo = { 2, TypeDesc::UINT8, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R8G8B8_UINT: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R8G8B8A8_UINT: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + // 8-bit R[G[B[A]]] uncompressed SINT formats (not normalized) + case VK_FORMAT_R8_SINT: formatinfo = { 1, TypeDesc::INT8, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R8G8_SINT: formatinfo = { 2, TypeDesc::INT8, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R8G8B8_SINT: formatinfo = { 3, TypeDesc::INT8, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R8G8B8A8_SINT: formatinfo = { 4, TypeDesc::INT8, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + // TODO: BGRA formats (need to add std::array channelnames to FormatInfo) + + + /* ------------------------------------------------------------------ */ + /* ------------------ 16-bit uncompressed formats ------------------- */ + /* ------------------------------------------------------------------ */ + + // 16-bit uncompressed UNORM formats + case VK_FORMAT_R16_UNORM: formatinfo = { 1, TypeDesc::UINT16, BlockCompression::NONE }; return true; + case VK_FORMAT_R16G16_UNORM: formatinfo = { 2, TypeDesc::UINT16, BlockCompression::NONE }; return true; + case VK_FORMAT_R16G16B16_UNORM: formatinfo = { 3, TypeDesc::UINT16, BlockCompression::NONE }; return true; + case VK_FORMAT_R16G16B16A16_UNORM: formatinfo = { 4, TypeDesc::UINT16, BlockCompression::NONE }; return true; + + // 16-bit uncompressed SNORM formats + case VK_FORMAT_R16_SNORM: formatinfo = { 1, TypeDesc::INT16, BlockCompression::NONE }; return true; + case VK_FORMAT_R16G16_SNORM: formatinfo = { 2, TypeDesc::INT16, BlockCompression::NONE }; return true; + case VK_FORMAT_R16G16B16_SNORM: formatinfo = { 3, TypeDesc::INT16, BlockCompression::NONE }; return true; + case VK_FORMAT_R16G16B16A16_SNORM: formatinfo = { 4, TypeDesc::INT16, BlockCompression::NONE }; return true; + + // 16-bit uncompressed SFLOAT formats + case VK_FORMAT_R16_SFLOAT: formatinfo = { 1, TypeDesc::HALF, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R16G16_SFLOAT: formatinfo = { 2, TypeDesc::HALF, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R16G16B16_SFLOAT: formatinfo = { 3, TypeDesc::HALF, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R16G16B16A16_SFLOAT: formatinfo = { 4, TypeDesc::HALF, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + // 16-bit uncompressed UINT formats + case VK_FORMAT_R16_UINT: formatinfo = { 1, TypeDesc::UINT16, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R16G16_UINT: formatinfo = { 2, TypeDesc::UINT16, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R16G16B16_UINT: formatinfo = { 3, TypeDesc::UINT16, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R16G16B16A16_UINT: formatinfo = { 4, TypeDesc::UINT16, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + // 16-bit uncompressed SINT formats + case VK_FORMAT_R16_SINT: formatinfo = { 1, TypeDesc::INT16, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R16G16_SINT: formatinfo = { 2, TypeDesc::INT16, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R16G16B16_SINT: formatinfo = { 3, TypeDesc::INT16, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R16G16B16A16_SINT: formatinfo = { 4, TypeDesc::INT16, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + + /* ------------------------------------------------------------------ */ + /* ------------------ 32-bit uncompressed formats ------------------- */ + /* ------------------------------------------------------------------ */ + + // 32-bit uncompressed SFLOAT formats + case VK_FORMAT_R32_SFLOAT: formatinfo = { 1, TypeDesc::FLOAT, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R32G32_SFLOAT: formatinfo = { 2, TypeDesc::FLOAT, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R32G32B32_SFLOAT: formatinfo = { 3, TypeDesc::FLOAT, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R32G32B32A32_SFLOAT: formatinfo = { 4, TypeDesc::FLOAT, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + // 32-bit uncompressed UINT formats + case VK_FORMAT_R32_UINT: formatinfo = { 1, TypeDesc::UINT32, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R32G32_UINT: formatinfo = { 2, TypeDesc::UINT32, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R32G32B32_UINT: formatinfo = { 3, TypeDesc::UINT32, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R32G32B32A32_UINT: formatinfo = { 4, TypeDesc::UINT32, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + // 32-bit uncompressed SINT formats + case VK_FORMAT_R32_SINT: formatinfo = { 1, TypeDesc::INT32, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R32G32_SINT: formatinfo = { 2, TypeDesc::INT32, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R32G32B32_SINT: formatinfo = { 3, TypeDesc::INT32, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R32G32B32A32_SINT: formatinfo = { 4, TypeDesc::INT32, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + + /* ------------------------------------------------------------------ */ + /* ------------------ 64-bit uncompressed formats ------------------- */ + /* ------------------------------------------------------------------ */ + + // 64-bit uncompressed SFLOAT formats + case VK_FORMAT_R64_SFLOAT: formatinfo = { 1, TypeDesc::DOUBLE, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R64G64_SFLOAT: formatinfo = { 2, TypeDesc::DOUBLE, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R64G64B64_SFLOAT: formatinfo = { 3, TypeDesc::DOUBLE, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R64G64B64A64_SFLOAT: formatinfo = { 4, TypeDesc::DOUBLE, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + // 64-bit uncompressed UINT formats + case VK_FORMAT_R64_UINT: formatinfo = { 1, TypeDesc::UINT64, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R64G64_UINT: formatinfo = { 2, TypeDesc::UINT64, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R64G64B64_UINT: formatinfo = { 3, TypeDesc::UINT64, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R64G64B64A64_UINT: formatinfo = { 4, TypeDesc::UINT64, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + // 64-bit uncompressed SINT formats + case VK_FORMAT_R64_SINT: formatinfo = { 1, TypeDesc::INT64, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R64G64_SINT: formatinfo = { 2, TypeDesc::INT64, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R64G64B64_SINT: formatinfo = { 3, TypeDesc::INT64, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + case VK_FORMAT_R64G64B64A64_SINT: formatinfo = { 4, TypeDesc::INT64, BlockCompression::NONE, VK_FORMAT_UNDEFINED, false }; return true; + + + /* ------------------------------------------------------------------ */ + /* ---------------------- compressed formats ------------------------ */ + /* ------------------------------------------------------------------ */ + + // ETC2 block-compressed formats (not supported) + case VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::ETC2_RGB, VK_FORMAT_R8G8B8A8_SRGB }; return true; + case VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::ETC2_RGB, VK_FORMAT_R8G8B8A8_UNORM }; 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; + case VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ETC2_RGB_A1, VK_FORMAT_R8G8B8A8_UNORM }; return true; + case VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ETC2_RGBA, VK_FORMAT_R8G8B8A8_UNORM }; return true; + + // BCn formats + case VK_FORMAT_BC1_RGB_SRGB_BLOCK: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::BC1, VK_FORMAT_R8G8B8A8_SRGB }; return true; + case VK_FORMAT_BC1_RGB_UNORM_BLOCK: formatinfo = { 3, TypeDesc::UINT8, BlockCompression::BC1, 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_BC1_RGBA_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC1A, 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_BC2_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC2, 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_BC3_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC3, VK_FORMAT_R8G8B8A8_UNORM }; 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, false }; return true; + case VK_FORMAT_BC6H_SFLOAT_BLOCK: formatinfo = { 3, TypeDesc::HALF, BlockCompression::BC6HS, VK_FORMAT_R16G16B16_SFLOAT, false }; return true; + case VK_FORMAT_BC7_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC7, VK_FORMAT_R8G8B8A8_SRGB }; return true; + case VK_FORMAT_BC7_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::BC7, VK_FORMAT_R8G8B8A8_UNORM }; return true; + + // LDR ASTC linear formats (2D blocks) + case VK_FORMAT_ASTC_4x4_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_4x4 }; return true; + case VK_FORMAT_ASTC_5x4_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_5x4 }; return true; + case VK_FORMAT_ASTC_5x5_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_5x5 }; return true; + case VK_FORMAT_ASTC_6x5_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_6x5 }; return true; + case VK_FORMAT_ASTC_6x6_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_6x6 }; return true; + case VK_FORMAT_ASTC_8x5_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_8x5 }; return true; + case VK_FORMAT_ASTC_8x6_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_8x6 }; return true; + case VK_FORMAT_ASTC_8x8_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_8x8 }; return true; + case VK_FORMAT_ASTC_10x5_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_10x5 }; return true; + case VK_FORMAT_ASTC_10x6_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_10x6 }; return true; + case VK_FORMAT_ASTC_10x8_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_10x8 }; return true; + case VK_FORMAT_ASTC_10x10_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_10x10 }; return true; + case VK_FORMAT_ASTC_12x10_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_12x10 }; return true; + case VK_FORMAT_ASTC_12x12_UNORM_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_UNORM, true, KTX_PACK_ASTC_BLOCK_DIMENSION_12x12 }; return true; + + // LDR ASTC sRGB formats (2D blocks) + case VK_FORMAT_ASTC_4x4_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_4x4 }; return true; + case VK_FORMAT_ASTC_5x4_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_5x4 }; return true; + case VK_FORMAT_ASTC_5x5_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_5x5 }; return true; + case VK_FORMAT_ASTC_6x5_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_6x5 }; return true; + case VK_FORMAT_ASTC_6x6_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_6x6 }; return true; + case VK_FORMAT_ASTC_8x5_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_8x5 }; return true; + case VK_FORMAT_ASTC_8x6_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_8x6 }; return true; + case VK_FORMAT_ASTC_8x8_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_8x8 }; return true; + case VK_FORMAT_ASTC_10x5_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_10x5 }; return true; + case VK_FORMAT_ASTC_10x6_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_10x6 }; return true; + case VK_FORMAT_ASTC_10x8_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_10x8 }; return true; + case VK_FORMAT_ASTC_10x10_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_10x10 }; return true; + case VK_FORMAT_ASTC_12x10_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_12x10 }; return true; + case VK_FORMAT_ASTC_12x12_SRGB_BLOCK: formatinfo = { 4, TypeDesc::UINT8, BlockCompression::ASTC, VK_FORMAT_R8G8B8A8_SRGB, true, KTX_PACK_ASTC_BLOCK_DIMENSION_12x12 }; return true; + + // HDR ASTC formats (2D blocks) + case VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_4x4 }; return true; + case VK_FORMAT_ASTC_5x4_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_5x4 }; return true; + case VK_FORMAT_ASTC_5x5_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_5x5 }; return true; + case VK_FORMAT_ASTC_6x5_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_6x5 }; return true; + case VK_FORMAT_ASTC_6x6_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_6x6 }; return true; + case VK_FORMAT_ASTC_8x5_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_8x5 }; return true; + case VK_FORMAT_ASTC_8x6_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_8x6 }; return true; + case VK_FORMAT_ASTC_8x8_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_8x8 }; return true; + case VK_FORMAT_ASTC_10x5_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_10x5 }; return true; + case VK_FORMAT_ASTC_10x6_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_10x6 }; return true; + case VK_FORMAT_ASTC_10x8_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_10x8 }; return true; + case VK_FORMAT_ASTC_10x10_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_10x10 }; return true; + case VK_FORMAT_ASTC_12x10_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_12x10 }; return true; + case VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK: formatinfo = { 4, TypeDesc::HALF, BlockCompression::ASTC, VK_FORMAT_R16G16B16A16_SFLOAT, false, KTX_PACK_ASTC_BLOCK_DIMENSION_12x12 }; return true; + default: break; + } + // clang-format on + return false; +} + + + +// +// Gets the VkFormat from provided info (supplied from `ktxoutput->spec()`) +// +// Number of channels determines the number of channels in the returned +// VkFormat (e.g., 3 => RGB). This is assuming the default channel names: RGBA. +// +// The type is determined based on the provided format (which usually +// comes from `ktxoutput->spec()`) and also whether normalized is set. Just +// using the type alone is insufficient to determine the VkFormat because we +// can't, for instance, distinguish between VK_FORMAT_R8_UNORM and +// VK_FORMAT_R8_UINT. This table showcases the VkFormat and its upload data +// type: +// +// Vulkan format | Upload data type (per channel) +// ------------------------------------ | -------------------------------------- +// VK_FORMAT_R8[G8[B8[A8]]]_UNORM | UINT8 +// VK_FORMAT_R8[G8[B8[A8]]]_SNORM | INT8 +// VK_FORMAT_R8[G8[B8[A8]]]_UINT | UINT8 +// VK_FORMAT_R8[G8[B8[A8]]]_SINT | INT8 +// VK_FORMAT_.+_SRGB | UINT8 +// VK_FORMAT_R16[G16[B16[A16]]]_SFLOAT | HALF +// VK_FORMAT_R16[G16[B16[A16]]]_UNORM | UINT16 +// VK_FORMAT_R16[G16[B16[A16]]]_SNORM | INT16 +// VK_FORMAT_R16[G16[B16[A16]]]_UINT | UINT16 +// VK_FORMAT_R16[G16[B16[A16]]]_SINT | INT16 +// VK_FORMAT_R32[G32[B32[A32]]]_SFLOAT | FLOAT +// VK_FORMAT_R32[G32[B32[A32]]]_UINT | UINT32 +// VK_FORMAT_R32[G32[B32[A32]]]_SINT | INT32 +// VK_FORMAT_R64[G64[B64[A64]]]_SFLOAT | DOUBLE +// VK_FORMAT_R64[G64[B64[A64]]]_UINT | UINT64 +// VK_FORMAT_R64[G64[B64[A64]]]_SINT | INT64 +// VK_FORMAT_B8G8R8[A8]_UNORM | UINT8 +// VK_FORMAT_B8G8R8[A8]_SNORM | INT8 +// VK_FORMAT_B8G8R8[A8]_UINT | UINT8 +// VK_FORMAT_B8G8R8[A8]_SINT | INT8 +// VK_FORMAT_B8G8R8[A8]_SRGB | UINT8 +// +// As per the KTX2 specs, the _SRGB variants of VkFormat are determined based +// on the supplied colorspace (if sRGB => VkFormat MUST be SRGB; unlike DDS). +// +// Swizzling isn't supported yet (e.g., VK_FORMAT_B8G8R8A8_SRGB) but it will be +// soon (after robust testing on these formats is done). +// +// SRGB colorspace is prioritized over normalization (i.e., if `srg_colorspace` +// is `true` and `normalized` is also `true`, `normalized` is ignored if an SRGB +// output VkFormat is possible). +// +inline VkFormat +get_vkformat_from_info(ImageSpec const& spec, bool srgb_colorspace, + bool normalized) noexcept +{ + const auto& channelnames = spec.channelnames; + bool bgr_channels = false; + + // clang-format off + // For 1 channel, we support R (or Y which is treated as R) channel names + // The Y channel name is expected because default_channel_names sets it in case nbrchannels == 1 + if (spec.nchannels == 1 && channelnames[0] != "R" && channelnames[0] != "Y") + return VK_FORMAT_UNDEFINED; + + // For 2 channels, we only support RG (or YA which is treated as RG) channel names + if (spec.nchannels == 2 && !(channelnames[0] == "R" && channelnames[1] == "G") + && !(channelnames[0] == "Y" && channelnames[1] == "A")) + return VK_FORMAT_UNDEFINED; + + // For 3 channels, we support either RGB or BGR + if (spec.nchannels == 3) { + if (( channelnames[0] == "B" + && channelnames[1] == "G" + && channelnames[2] == "R")) + bgr_channels = true; + else if (!(channelnames[0] == "R" + && channelnames[1] == "G" + && channelnames[2] == "B")) + return VK_FORMAT_UNDEFINED; + } + + // For 4 channels, we support RGBA or BGRA + if (spec.nchannels == 4) { + if ( channelnames[0] == "B" + && channelnames[1] == "G" + && channelnames[2] == "R" + && channelnames[3] == "A") + bgr_channels = true; + else if (!(channelnames[0] == "R" + && channelnames[1] == "G" + && channelnames[2] == "B" + && channelnames[3] == "A")) + return VK_FORMAT_UNDEFINED; + } + + if (bgr_channels) { + switch(static_cast(spec.format.basetype)) { + case TypeDesc::UINT8: { + switch (spec.nchannels) { + case 3: return srgb_colorspace ? VK_FORMAT_B8G8R8_SRGB : normalized ? VK_FORMAT_B8G8R8_UNORM : VK_FORMAT_B8G8R8_UINT; + case 4: return srgb_colorspace ? VK_FORMAT_B8G8R8A8_SRGB : normalized ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_B8G8R8A8_UINT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::INT8: { + switch (spec.nchannels) { + case 3: return normalized ? VK_FORMAT_B8G8R8_SNORM : VK_FORMAT_B8G8R8_SINT; + case 4: return normalized ? VK_FORMAT_B8G8R8A8_SNORM : VK_FORMAT_B8G8R8A8_SINT; + default: return VK_FORMAT_UNDEFINED; + } + } + default: return VK_FORMAT_UNDEFINED; + } + } + + // else: R[G[B[A]]] channel names + + switch (static_cast(spec.format.basetype)) { + case TypeDesc::UINT8: { + switch (spec.nchannels) { + case 1: return srgb_colorspace ? VK_FORMAT_R8_SRGB : normalized ? VK_FORMAT_R8_UNORM : VK_FORMAT_R8_UINT; + case 2: return srgb_colorspace ? VK_FORMAT_R8G8_SRGB : normalized ? VK_FORMAT_R8G8_UNORM : VK_FORMAT_R8G8_UINT; + case 3: return srgb_colorspace ? VK_FORMAT_R8G8B8_SRGB : normalized ? VK_FORMAT_R8G8B8_UNORM : VK_FORMAT_R8G8B8_UINT; + case 4: return srgb_colorspace ? VK_FORMAT_R8G8B8A8_SRGB : normalized ? VK_FORMAT_R8G8B8A8_UNORM : VK_FORMAT_R8G8B8A8_UINT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::INT8: { + switch (spec.nchannels) { + case 1: return normalized ? VK_FORMAT_R8_SNORM : VK_FORMAT_R8_SINT; + case 2: return normalized ? VK_FORMAT_R8G8_SNORM : VK_FORMAT_R8G8_SINT; + case 3: return normalized ? VK_FORMAT_R8G8B8_SNORM : VK_FORMAT_R8G8B8_SINT; + case 4: return normalized ? VK_FORMAT_R8G8B8A8_SNORM : VK_FORMAT_R8G8B8A8_SINT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::HALF: { + switch (spec.nchannels) { + case 1: return VK_FORMAT_R16_SFLOAT; + case 2: return VK_FORMAT_R16G16_SFLOAT; + case 3: return VK_FORMAT_R16G16B16_SFLOAT; + case 4: return VK_FORMAT_R16G16B16A16_SFLOAT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::UINT16: { + switch (spec.nchannels) { + case 1: return normalized ? VK_FORMAT_R16_UNORM : VK_FORMAT_R16_UINT; + case 2: return normalized ? VK_FORMAT_R16G16_UNORM : VK_FORMAT_R16G16_UINT; + case 3: return normalized ? VK_FORMAT_R16G16B16_UNORM : VK_FORMAT_R16G16B16_UINT; + case 4: return normalized ? VK_FORMAT_R16G16B16A16_UNORM : VK_FORMAT_R16G16B16A16_UINT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::INT16: { + switch (spec.nchannels) { + case 1: return normalized ? VK_FORMAT_R16_SNORM : VK_FORMAT_R16_SINT; + case 2: return normalized ? VK_FORMAT_R16G16_SNORM : VK_FORMAT_R16G16_SINT; + case 3: return normalized ? VK_FORMAT_R16G16B16_SNORM : VK_FORMAT_R16G16B16_SINT; + case 4: return normalized ? VK_FORMAT_R16G16B16A16_SNORM : VK_FORMAT_R16G16B16A16_SINT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::FLOAT: { + switch (spec.nchannels) { + case 1: return VK_FORMAT_R32_SFLOAT; + case 2: return VK_FORMAT_R32G32_SFLOAT; + case 3: return VK_FORMAT_R32G32B32_SFLOAT; + case 4: return VK_FORMAT_R32G32B32A32_SFLOAT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::UINT32: { + switch (spec.nchannels) { + case 1: return VK_FORMAT_R32_UINT; + case 2: return VK_FORMAT_R32G32_UINT; + case 3: return VK_FORMAT_R32G32B32_UINT; + case 4: return VK_FORMAT_R32G32B32A32_UINT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::INT32: { + switch (spec.nchannels) { + case 1: return VK_FORMAT_R32_SINT; + case 2: return VK_FORMAT_R32G32_SINT; + case 3: return VK_FORMAT_R32G32B32_SINT; + case 4: return VK_FORMAT_R32G32B32A32_SINT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::DOUBLE: { + switch (spec.nchannels) { + case 1: return VK_FORMAT_R64_SFLOAT; + case 2: return VK_FORMAT_R64G64_SFLOAT; + case 3: return VK_FORMAT_R64G64B64_SFLOAT; + case 4: return VK_FORMAT_R64G64B64A64_SFLOAT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::UINT64: { + switch (spec.nchannels) { + case 1: return VK_FORMAT_R64_UINT; + case 2: return VK_FORMAT_R64G64_UINT; + case 3: return VK_FORMAT_R64G64B64_UINT; + case 4: return VK_FORMAT_R64G64B64A64_UINT; + default: return VK_FORMAT_UNDEFINED; + } + } + case TypeDesc::INT64: { + switch (spec.nchannels) { + case 1: return VK_FORMAT_R64_SINT; + case 2: return VK_FORMAT_R64G64_SINT; + case 3: return VK_FORMAT_R64G64B64_SINT; + case 4: return VK_FORMAT_R64G64B64A64_SINT; + default: return VK_FORMAT_UNDEFINED; + } + } + + default: return VK_FORMAT_UNDEFINED; + } + // clang-format on +} + + + +inline uint32_t +calculate_nbr_mip_levels(uint32_t dimension) noexcept +{ + uint32_t nmips = 0; + while (dimension > 0) { + dimension >>= 1; + ++nmips; + } + return nmips; +} + +OIIO_PLUGIN_NAMESPACE_END diff --git a/src/ktx.imageio/ktxinput.cpp b/src/ktx.imageio/ktxinput.cpp new file mode 100644 index 0000000000..a71a7436e7 --- /dev/null +++ b/src/ktx.imageio/ktxinput.cpp @@ -0,0 +1,1441 @@ +// 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; + + // Calls the span-based implementation with inferred data buffer length + bool read_native_scanline(int subimage, int miplevel, int y, int z, + void* data) override; + + // Calls the span-based implementation with inferred data buffer length + bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend, + int z, void* data) override; + + // Actual implementation for 1d/2d textures + bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend, + span data) override; + + // Calls the span-based implementation with inferred data buffer length + bool read_native_tile(int subimage, int miplevel, int x, int y, int z, + void* data) override; + + // Calls the span-based implementation with inferred data buffer length + bool read_native_tiles(int subimage, int miplevel, int xbegin, int xend, + int ybegin, int yend, int zbegin, int zend, + void* data) override; + + // Actual implementation for Cubemap textures + bool read_native_tiles(int subimage, int miplevel, int xbegin, int xend, + int ybegin, int yend, span data) override; + + // Actual implementation for volume textures + // Note: current OIIO API does not make any call to this method. This is + // just implemented for future use and to be called within read_native_tiles + // when the texture is a volume. + bool read_native_volumetric_tiles(int subimage, int miplevel, int xbegin, + int xend, int ybegin, int yend, + int zbegin, int zend, + span data) override; + + + const std::string& filename() const { return m_filename; } + + void init() noexcept; + + 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 }; + + std::unique_ptr m_config; // Saved copy of configuration spec + + // Checks the magic + bool ktx_magic_cmp(cspan sig) const noexcept; + + const char* get_colorspace() const noexcept; + + // std::string& because string_view are still not supported by std::regex + void parse_ktx_sc_params_metadata(const std::string& ktx_sc_params); + + bool check(int subimage, int miplevel) const noexcept; +}; + + + +// 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 "libktx 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)) { + errorfmt("\"{}\" is not a KTX2 file, magic number doesn't match", name); + return false; + } + + // + // Load just the header (the flag KTX_TEXTURE_CREATE_NO_FLAGS means the data + // isn't loaded). We load the actual images AFTER performing check_open()s. + // + // 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 " + "ktxTexture2_CreateFromStdioStream. Reason: {}", + ktxErrorString(res)); + 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 ktxTexture2_CreateFromMemory. Reason: {}", + ktxErrorString(res)); + return false; + } + } + + // We don't support 3D texture arrays because no graphics/compute API + // supports these. `ktx info` on a 3D texture KTX2 file outputs the + // following: + // warning-3014: File contains a 3D array texture. + // File contains a 3D array texture. No API supports these. + if (m_tex->isArray && m_tex->numDimensions == 3) { + errorfmt("3D texture arrays are not supported"); + return false; + } + + const bool is_hdr = ktxTexture2_IsHDR(m_tex.get()); + DBG std::cout << "[ktxinput] is_hdr: " << is_hdr << std::endl; + + // Create a default spec and seek_subimage to set the dimensions based on + // the texture type (format is set later based on the input VkFormat). + m_spec = ImageSpec(); + if (!seek_subimage(0, 0)) + return false; // errorfmt is set via seek_subimage + + // + // Make sure to set the nchannels, channelnames, and format BEFORE any + // check_open() calls. + // + // In case of a Basis Universal texture: there isn't a "fixed" nchannels. + // It depends on what our transcode target is. In OIIO, we will always + // either transcode to KTX_TTF_RGBA32 or KTX_TTF_RGB_HALF. The former is + // LDR while the latter is HDR. + // + if (ktxTexture2_IsTranscodable(m_tex.get())) { + m_spec.nchannels = is_hdr ? 3 : 4; + m_spec.format = is_hdr ? TypeDesc::HALF : TypeDesc::UINT8; + m_spec.default_channel_names(); + } else /* non-basisu-encoded texture; nchannels is fixed in 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_spec.nchannels = format_info.nbrchannels; + m_spec.format = format_info.typedesc; + m_spec.default_channel_names(); + } + + const char* colorspace = get_colorspace(); + m_spec.set_colorspace(colorspace); + + // Set textureformat attribute + if (m_tex->numDimensions == 1) { + m_spec.attribute("textureformat", "Plain Texture"); + } else if (m_tex->numDimensions == 2) { + if (m_tex->isCubemap) { + m_spec.attribute("textureformat", "CubeFace Environment"); + // In KTX (as per the specs) cubemap faces are always in this order + m_spec.attribute("ktx:CubeMapSides", "+x -x +y -y +z -z"); + } else { + m_spec.attribute("textureformat", "Plain Texture"); + } + } else if (m_tex->numDimensions == 3) { + m_spec.attribute("textureformat", "Volume Texture"); + } else { + assert(false); + return false; // should never occur + } + + DBG std::cout << "[ktxinput] \"textureformat\": \"" + << m_spec.get_string_attribute("textureformat") << '"' + << std::endl; + + // We don't support vendor-specific supercompression schemes + if (m_tex->supercompressionScheme > KTX_SS_END_RANGE) { + errorfmt("Unsuppoted vendor-specific supercompression scheme: {}", + ktxSupercompressionSchemeString(m_tex->supercompressionScheme)); + 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, or Basis Universal codecs). + // + // Make sure to save everything that is needed to recreate this "exact same" + // KTX2 texture from OIIO API (i.e., fields of `ktxTextureCreateInfo`). + // Of course, the term "exact same" takes into account the quality loss when + // reading lossy-compressed KTX2 files and saving them as KTX2 files again. + // + // KtxTexture fields may change after some libktx calls that take + // KtxTexture2* 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). + // + // For BasisLZ, since it is always required when using ETC1S Basis Universal + // codec, there is no reason to save it (not needed for roundtrip). + // + switch (m_tex->supercompressionScheme) { + case KTX_SS_ZSTD: m_spec.attribute("compression", "zstd"); break; + case KTX_SS_ZLIB: m_spec.attribute("compression", "zip" /* zlib */); break; + default: break; + } + + // is this a 1D texture? If so, save that it is because we can't figure it + // just based on dimensions (a texture with height 1 can be either a 1D or + // 2D texture). + if (m_tex->numDimensions == 1) + m_spec.extra_attribs.attribute("ktx:1d", true); + // Contrary to the specs' layerCount, numLayers is always >= 1 + if (m_tex->numLayers > 1) + m_spec.attribute("oiio:subimages", static_cast(m_tex->numLayers)); + // Save number of miplevels to give the option to users to avoid having to + // figure it out by calling `seek_subimage` multiple times + if (m_tex->numLevels > 1) + m_spec.extra_attribs.attribute("oiio:miplevels", m_tex->numLevels); + if (m_tex->generateMipmaps) + m_spec.extra_attribs.attribute("ktx:generate_mipmaps", true); + + // + // 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 specs 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) { + DBG std::cerr << "ktxHashListEntry_GetKey failed. Reason: " + << ktxErrorString(status) << std::endl; + 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 failed. Reason: {}", + ktxErrorString(status)); + return false; + } + + // vallen checks are done below depending on the attribute name + + // keylen includes NUL termination char! + auto attr_name = OIIO::string_view(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 = static_cast(val); + if (vallen <= 1 || cval[vallen - 1] != '\0') { + errorfmt( + "Invalid KTX2 file: an empty or non-null " + "terminated string for \"KTXwriterScParams\" metadata"); + return false; + } + // vallen includes NUL char! + parse_ktx_sc_params_metadata(std::string(cval, vallen - 1)); + } else if (attr_name == "KTXcubemapIncomplete") { + // + // KTX supports incomplete cubemaps (i.e., cubemaps with less + // than 6 faces). These incomplete cubemaps are flattened into + // 2D arrays. Per the specs: + // Applications wanting to store incomplete cubemaps should + // flatten faces into a 2D array [...] + // + errorfmt("Incomplete cubemaps are currently not supported"); + return false; + } 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 + OIIO_ASSERT(vallen <= 4); // at most '[rl][du][oi]\0' + auto cval = static_cast(val); + if (vallen <= 1 || vallen > 4 || cval[vallen - 1] != '\0') { + errorfmt( + "Invalid KTX2 file: an empty or non-null " + "terminated string for \"KTXorientation\" metadata"); + return false; + } + OIIO::string_view val_str(cval, vallen - 1); + const auto error_msg = fmt::format( + "Given KTX2 file does not comply with KTX2 specs." + " Metadata \"KTXorientation\" {} is invalid.", + val_str); + if (val_str.length() != m_tex->numDimensions) { + errorfmt("{}", error_msg); + return false; + } + int orientation = 1; + switch (m_tex->numDimensions) { + case 1: + if (val_str[0] == KTX_ORIENT_X_LEFT) { + orientation = 2; + } else if (val_str[0] == KTX_ORIENT_X_RIGHT) { + orientation = 1; + } else { + errorfmt("{}", error_msg); + return false; + } + break; + case 2: + case 3: + if (val_str[0] == KTX_ORIENT_X_LEFT + && val_str[1] == KTX_ORIENT_Y_DOWN) { + orientation = 2; + } else if (val_str[0] == KTX_ORIENT_X_LEFT + && val_str[1] == KTX_ORIENT_Y_UP) { + orientation = 3; + } else if (val_str[0] == KTX_ORIENT_X_RIGHT + && val_str[1] == KTX_ORIENT_Y_UP) { + orientation = 4; + } else if (val_str[0] == KTX_ORIENT_X_RIGHT + && val_str[1] == KTX_ORIENT_Y_DOWN) { + orientation = 1; + } else { + errorfmt("{}", error_msg); + return false; + } + break; + default: // should never occur + OIIO_ASSERT(false); + return false; + } + + DBG std::cout << fmt::format( + "[ktxinput] set OIIO orientation: {} from {}: {}\n", + orientation, KTX_ORIENTATION_KEY, val_str); + if (orientation != 1) + m_spec.attribute("Orientation", orientation); + } + + } while ((kventry = ktxHashList_Next(kventry))); + + // + // BEFORE actually loading/uncompressing the data, impose some reasonable + // limits on the dimensions. + // + // These ranges are set based on similar limits used in DDS plugin. + // + // The following calculations assume worst-case scenario with + // VK_FORMAT_R64G64B64A64_SFLOAT input textures (i.e., 4 channels with each + // 8 bytes). For: + // - 2D textures: 32768 * 32768 => 32 GBs + // - 2D array textures: 32768 * 32768 * 2048 => 65536 GBs + // - cubemaps: 16384 * 98304 => 48 GBs + // - cubemap textures: 16384 * 98304 * 2048 => 98304 GBs + // - 3D textures: 4096 * 4096 * 4096 => 2048 GBs + // + // For non-array textures: rely on the memory size checks inside check_open. + // For array texture: check_open checks are insufficient because they don't + // take into account nbr subimages (i.e., nbr layers in array)! + // + if (m_tex->isArray) { + if (m_tex->numLayers > 2048) { + errorfmt( + "number of layers in input KTX2 texture exceeds the limit: {} > {}", + m_tex->numLayers, 2048); + return false; + } + int limit_imagesize_MB = 0; + OIIO::getattribute("limits:imagesize_MB", TypeDesc::INT, + &limit_imagesize_MB); + const auto array_size_in_bytes = m_spec.image_bytes(true) + * imagesize_t(m_tex->numLayers); + if (limit_imagesize_MB + && array_size_in_bytes + > limit_imagesize_MB * imagesize_t(1024 * 1024)) { + errorfmt( + "input texture array is too large ({} MB) and exceeds the set memory limits of: {} MB", + float(array_size_in_bytes) / float(1024 * 1024), + limit_imagesize_MB); + return false; + } + } + if (m_tex->baseDepth > 1) { + // 3D texture (3D texture arrays not supported) + if (!check_open(m_spec, { 0, 4096, 0, 4096, 0, 4096, 0, 4 })) + return false; + } else if (m_tex->isCubemap) { + // Cubemap textures or Cubemap texture arrays + if (!check_open(m_spec, { 0, 16384, 0, 16384 * 6, 0, 1, 0, 4 })) + return false; + } else { + // 1D/2D textures or 1D/2D texture arrays + if (!check_open(m_spec, { 0, 32768, 0, 32768, 0, 1, 0, 4 })) + return false; + } + + // + // Load the image(s) data (libktx owns the data buffer). This is where the + // buffer allocation for un-supercompressed textures occurs. + // + // TODO: per-subimage and per-miplevel KTX2 loading + // Implementing the per-subimage allocation approach requires some effort. + // For the moment, let's make sure this approach is working. + if (auto result = ktxTexture2_LoadImageData(m_tex.get(), NULL, 0); + result != KTX_SUCCESS) { + errorfmt("ktxTexture2_LoadImageData failed. Reason: {}", + ktxErrorString(result)); + return false; + } + + // At this point: images are certainly not super-compressed but are + // potentially basisu-compressed, GPU-block-compressed, or simply + // uncompressed. + + // + // 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/roundtrip. + // + // 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())) { + khr_df_model_e color_model = ktxTexture2_GetColorModel_e(m_tex.get()); + if (auto status = ktxTexture2_TranscodeBasis( + m_tex.get(), is_hdr ? KTX_TTF_RGB_HALF : KTX_TTF_RGBA32, 0); + status != KTX_SUCCESS) { + errorfmt("failed to transcode KTX2 texture to raw pixels. " + "ktxTexture2_TranscodeBasis failed. Reason: {}", + ktxErrorString(status)); + return false; + } + // Save Basis Universal codec in the "ktx:codec" attribute (this + // attribute is also used to save GPU-block-based compressions) + m_spec.attribute("ktx:codec", basis_universal_codec_name(color_model)); + } + + // At this point: images are certainly not basisu-compressed and are either + // GPU-block-compressed (e.g., ASTC) or simply uncompressed. + + // + // 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 for 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? */) { + 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("ktx:codec", 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 failed. Reason: {}", + ktxErrorString(status)); + return false; + } + break; +#endif + + /* ASTC formats */ + case BlockCompression::ASTC: { + // save the ASTC block dimensions from its VkFormat + if (auto it = std::find_if(astc_block_dims_str_to_enum.cbegin(), + astc_block_dims_str_to_enum.cend(), + [&](auto const& pair) { + return pair.second + == format_info.astc_block_dims; + }); + it != astc_block_dims_str_to_enum.end()) { + m_spec.attribute("ktx:astc_block_dimensions", it->first); + } else { + // should only occur if astc_block_dims_str_to_enum and + // get_info_from_vkformat are not in sync + OIIO_ASSERT(false); + 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 failed. Reason: {}", + ktxErrorString(status)); + return false; + } + break; + } + + default: + errorfmt("{} GPU-compressed formats are not supported", + block_compression_name(m_cmp)); + return false; + } + } + + OIIO_ASSERT(!m_tex->isCompressed); + + // At this point: images are certainly uncompressed (non-basisu-encoded, + // non-supercompressed, and non-GPU-block-compressed). + + // + // 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; + } + + const 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; + // Save whether this is not a UNORM/SNORM VkFormat so that it can be + // reproduced + if (!format_info.normalized_vkformat) + m_spec.attribute("ktx:normalized", false); + + newspec = m_spec; + return true; +} + + + +void +KtxInput::init() noexcept +{ + m_filename = std::string(); + m_tex.reset(nullptr); + m_subimage = -1; + m_miplevel = -1; + m_cmp = BlockCompression::NONE; + m_config.reset(nullptr); + ioproxy_clear(); +} + + + +bool +KtxInput::close() +{ + // Re-initialize to default so that this same instance can be re-opened + init(); + return true; +}; + + + +bool +KtxInput::check(int subimage, int miplevel) const noexcept +{ + // + // 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) + return false; // don't errorfmt here + 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) and +// setting up dimensions and tile dimensions for cubemap or volume textures. +// +// In the context of KTX: +// - For 3D textures (i.e., depth > 1): `subimage` does NOT correspond to the +// 3D texture depth slice (unlike how libktx 'considers' 3D slices and cubemap +// faces equivalent; from point of view of the API). `subimage` SHOULD be 0. +// Volumes are expected to be read using `read_native_tiles` where each tile +// is simulated as a single scanline (same way as how it is done in DDS). +// - For Cubemaps (i.e., tile_width/tile_height > 1): `subimage` also does NOT +// correspond to the face slice (again, unlike how libktx accesses cubemap +// faces using slices/subimages). Incomplete cubemaps +// (i.e., number of faces < 6) are currently not supported. +// - Arrays of 2D/cubemap textures: `subimage` maps to layer in libktx (i.e., +// index in array). `subimage` SHOULD be [0, m_tex->numLayers[. +// - Arrays of 3D textures: currently not supported. +// +// 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; + + // + // 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 uint32_t width = std::max(m_tex->baseWidth >> miplevel, 1u); + const uint32_t height = std::max(m_tex->baseHeight >> miplevel, 1u); + const uint32_t depth = std::max(m_tex->baseDepth >> miplevel, 1u); + + // Checks against overflows + const uint32_t int_max = static_cast( + std::numeric_limits::max()); + if (width > int_max || height > int_max || depth > int_max) + return false; + + if (m_tex->isCubemap) { + // Overflow check + if (height > int_max / 6) + return false; + } + + m_subimage = subimage; + m_miplevel = miplevel; + + m_spec.width = m_spec.full_width = static_cast(width); + m_spec.height = m_spec.full_height = static_cast(height); + m_spec.depth = m_spec.full_depth = static_cast(depth); + + // + // For volume texture, pretend it's a tiled texture with: + // width x 1 x 1 tiles. + // Why? Current OIIO API does not provide safe span-based + // read_native_scanlines with a depth parameter (i.e., `z` parameter). + // + if (m_tex->numDimensions == 3) { + m_spec.tile_width = static_cast(width); + m_spec.tile_height = 1; + m_spec.tile_depth = 1; + } + + // Pretend a 1x6 layout (libktx doesn't provide access to cubemaps as whole + // textures but rather through a per-face index API) + if (m_tex->isCubemap) { + m_spec.height = m_spec.full_height = static_cast(height * 6); + m_spec.tile_width = static_cast(width); + m_spec.tile_height = static_cast(height); + m_spec.tile_depth = 1; + } + + return true; +} + + + +bool +KtxInput::read_native_scanline(int subimage, int miplevel, int y, int z, + void* data) +{ + const int width = std::max(m_tex->baseWidth >> miplevel, 1u); + // Use read_native_volumetric_tiles() to read scanlines from volume slices + if (z != 0) + return false; + // Simply call the span-base multi-scanline method with inferred length + return read_native_scanlines(subimage, miplevel, y, y + 1, + make_span((std::byte*)data, + m_spec.pixel_bytes() * width)); +} + + + +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 auto data_span = make_span((std::byte*)data, m_spec.pixel_bytes() + * width + * (yend - ybegin)); + // Use read_native_volumetric_tiles() to read scanlines from volume slices + if (z != 0) + return read_native_volumetric_tiles(subimage, miplevel, 0, width, + ybegin, yend, z, z + 1, data_span); + if (ybegin < 0 || ybegin >= yend || yend > height) { + errorfmt( + "KTX read_native_scanlines: Out of valid range scanlines range"); + return false; + } + // Simply call the span-base multi-scanline method with inferred length + return read_native_scanlines(subimage, miplevel, ybegin, yend, data_span); +} + + + +bool +KtxInput::read_native_scanlines(int subimage, int miplevel, int ybegin, + int yend, span data) +{ + if (!check(subimage, miplevel)) { + errorfmt("KTX read_native_scanlines: invalid subimage or miplevel"); + return false; + } + + // Use read_native_tiles() to read cubemap faces or + // read_native_volumetric_tiles() to read 3D volume slices + if (m_tex->isCubemap || m_tex->numDimensions == 3) { + return false; + } + + const int width = std::max(m_tex->baseWidth >> miplevel, 1u); + const int height = std::max(m_tex->baseHeight >> miplevel, 1u); + const size_t pitch_in_bytes = m_spec.pixel_bytes() * width; + ktx_size_t offset; + + if (ybegin < 0 || ybegin >= yend || yend > height) { + errorfmt( + "KTX read_native_scanlines: Out of valid range scanlines range"); + return false; + } + + // 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)) + return false; // errorfmt is set within valid_raw_span_size + + 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; + } + + // Check that our pitch (from spec) is equal to the one reported by libktx + 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; + } + + // + // GetImageOffset implements internal checks depending on texture kind + // (e.g., 3D, cubemap, etc.) and in case of invalid input, + // KTX_INVALID_OPERATION is returned. + // + if (auto status = ktxTexture2_GetImageOffset(m_tex.get(), miplevel, + subimage, 0, &offset); + status != KTX_SUCCESS) { + errorfmt("ktxTexture2_GetImageOffset failed. Reason: {}", + ktxErrorString(status)); + return false; + } + + auto src_ptr = m_tex->pData + offset + ktx_size_t(ybegin) * pitch_in_bytes; + memcpy(data.data(), src_ptr, pitch_in_bytes * size_t(yend - ybegin)); + DBG std::cout << fmt::format( + "[ktxinput] read_native_scanlines(subimage={},miplevel={},ybegin={},yend={})\n", + subimage, miplevel, ybegin, yend); + return true; +} + + + +bool +KtxInput::read_native_tile(int subimage, int miplevel, int x, int y, int z, + void* data) +{ + int tile_width = std::max(m_tex->baseWidth >> miplevel, 1u); + int tile_height = std::max(m_tex->baseHeight >> miplevel, 1u); + int tile_depth = std::max(m_tex->baseDepth >> miplevel, 1u); + // Use read_native_volumetric_tiles() for volume tiles + if (m_tex->numDimensions == 3) { + tile_height = 1; + tile_depth = 1; + return read_native_volumetric_tiles( + subimage, miplevel, x, x + tile_width, y, y + tile_height, z, + z + tile_depth, + make_span(static_cast(data), + m_spec.pixel_bytes() * tile_width)); + } + return read_native_tiles(subimage, miplevel, x, x + tile_width, y, + y + tile_height, + make_span(static_cast(data), + m_spec.pixel_bytes() * tile_width + * tile_height)); +} + + + +bool +KtxInput::read_native_tiles(int subimage, int miplevel, int xbegin, int xend, + int ybegin, int yend, int zbegin, int zend, + void* data) +{ + const int tile_width = std::max(m_tex->baseWidth >> miplevel, 1u); + // Use read_native_volumetric_tiles() for volume tiles + if (m_tex->numDimensions == 3) + return read_native_volumetric_tiles( + subimage, miplevel, xbegin, xend, ybegin, yend, zbegin, zend, + make_span((std::byte*)data, m_spec.pixel_bytes() * tile_width + * size_t(yend - ybegin) + * size_t(zend - zbegin))); + return read_native_tiles(subimage, miplevel, xbegin, xend, ybegin, yend, + make_span(static_cast(data), + m_spec.pixel_bytes() * tile_width + * size_t(yend - ybegin))); +} + + + +bool +KtxInput::read_native_tiles(int subimage, int miplevel, int xbegin, int xend, + int ybegin, int yend, span data) +{ + const int width = std::max(m_tex->baseWidth >> miplevel, 1u); + const int height = std::max(m_tex->baseHeight >> miplevel, 1u); + const size_t pitch_in_bytes = m_spec.pixel_bytes() * width; + ktx_size_t offset; + + // Is this a Cubemap? + if (!m_tex->isCubemap) + return false; + + if (!check(subimage, miplevel)) { + errorfmt("KTX read_native_scanlines: invalid subimage or miplevel"); + return false; + } + + // Check provided ROI (with 1x6 layout => tiles arranged along height) + if (xbegin != 0 || xend != width || ybegin < 0 || ybegin >= yend + || ybegin % height || yend > (height * 6) || yend % height) { + errorfmt( + "KTX read_native_tiles: out of valid range tile(s) ROI. " + "xbegin={} (should be 0), xend={} (should be {}), ybegin={} (% {} should be 0), yend={} (% {} should be 0)", + xbegin, xend, width, ybegin, height, yend, height); + return false; + } + + // 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, xbegin, xend, ybegin, yend)) + return false; // errorfmt is set within valid_raw_span_size + + 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; + } + + // Check that our pitch (from spec) is equal to the one reported by libktx + // TODO: this cast is probably UB. Wait for my suggestion to libktx to add + // ktxTexture2_GetRowPitch that simply takes a ktxTexture2* + 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; + } + + for (int start_face_idx = ybegin / height, face_idx = start_face_idx; + face_idx < (yend / height); ++face_idx) { + // + // GetImageOffset implements internal checks depending on texture kind + // (e.g., 3D, cubemap, etc.) and in case of invalid input, + // KTX_INVALID_OPERATION is returned. + // + if (auto status = ktxTexture2_GetImageOffset(m_tex.get(), miplevel, + subimage, face_idx, + &offset); + status != KTX_SUCCESS) { + errorfmt("ktxTexture2_GetImageOffset failed. Reason: {}", + ktxErrorString(status)); + return false; + } + size_t tile_size_in_bytes = pitch_in_bytes * height; + auto src_ptr = m_tex->pData + offset; + auto dst_ptr = data.data() + + size_t(face_idx - start_face_idx) * tile_size_in_bytes; + memcpy(dst_ptr, src_ptr, tile_size_in_bytes); + } + DBG std::cout << fmt::format( + "[ktxinput] read_native_tiles(subimage={},miplevel={},xbegin={},xend={},ybegin={},yend={})\n", + subimage, miplevel, xbegin, xend, ybegin, yend); + return true; +} + + + +bool +KtxInput::read_native_volumetric_tiles(int subimage, int miplevel, int xbegin, + int xend, int ybegin, int yend, + int zbegin, int zend, + span 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; + + // Is this a 3D volume texture? + if (m_tex->numDimensions != 3) + return false; + + if (!check(subimage, miplevel)) { + errorfmt( + "KTX read_native_volumetric_tiles: out of valid range tile(s) ROI"); + return false; + } + + if (xbegin != 0 || xend != width) { + errorfmt("KTX read_native_volumetric_tiles: for 3d volumes tiles are " + "reported as scanlines. xbegin must be 0 and xend must be the " + "width"); + return false; + } + + if (ybegin < 0 || ybegin >= yend || yend > height || zbegin < 0 + || zbegin >= zend || zend > depth) { + errorfmt( + "KTX read_native_volumetric_tiles: out of valid range scanline indices"); + return false; + } + + // 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, xbegin, xend, ybegin, yend, zbegin, + zend)) + return false; // errorfmt is set within valid_raw_span_size + + 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; + } + + // Check that our pitch (from spec) is equal to the one reported by libktx + 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; + } + + for (int slice_idx = zbegin; slice_idx < zend; ++slice_idx) { + if (auto status = ktxTexture2_GetImageOffset(m_tex.get(), miplevel, + subimage, slice_idx, + &offset); + status != KTX_SUCCESS) { + errorfmt("ktxTexture2_GetImageOffset failed. Reason: {}", + ktxErrorString(status)); + return false; + } + size_t tile_size_in_bytes = size_t(yend - ybegin) * pitch_in_bytes; + auto src_ptr = m_tex->pData + offset + + ktx_size_t(ybegin) * pitch_in_bytes; + auto dst_ptr = data.data() + + size_t(slice_idx - zbegin) * tile_size_in_bytes; + memcpy(dst_ptr, src_ptr, tile_size_in_bytes); + } + DBG std::cout << fmt::format( + "[ktxinput] read_native_volumetric_tiles(subimage={},miplevel={}," + "xbegin={},xend={},ybegin={},yend={},zbegin={},zend={})\n", + subimage, miplevel, xbegin, xend, ybegin, yend, zbegin, zend); + 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(make_cspan(magic, numRead)); +} + + + +bool +KtxInput::ktx_magic_cmp(cspan sig) const noexcept +{ + // 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 }; + if (sig.size() < sizeof(KTX2_IDENTIFIER)) + return false; + for (size_t i = 0; i < sizeof(KTX2_IDENTIFIER); ++i) + if (sig[i] != KTX2_IDENTIFIER[i]) + return false; + return true; +} + + +// +// Returns the OIIO colorspace identification string from given KTX2 transfer +// function and primaries. +// +// Since KTX2 textures are usually intended to be used within rendering context, +// assume scene rather than display colorspaces. +// +const char* +KtxInput::get_colorspace() const noexcept +{ + // + // 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 + // + const khr_df_transfer_e transfer_function + = ktxTexture2_GetTransferFunction_e(m_tex.get()); + const khr_df_primaries_e primaries = ktxTexture2_GetPrimaries_e( + m_tex.get()); + DBG std::cout << "[ktxinput] tf: " << transfer_function + << "; primaries: " << primaries << std::endl; + switch (transfer_function) { + case KHR_DF_TRANSFER_UNSPECIFIED: return "unknown"; + case KHR_DF_TRANSFER_SRGB: + switch (primaries) { + case KHR_DF_PRIMARIES_BT709: return "srgb_rec709_scene"; + case KHR_DF_PRIMARIES_ACESCC: return "srgb_ap1_scene"; + case KHR_DF_PRIMARIES_DISPLAYP3: return "srgb_p3d65_scene"; + default: break; + } + break; + case KHR_DF_TRANSFER_LINEAR: + switch (primaries) { + case KHR_DF_PRIMARIES_UNSPECIFIED: return "data"; + case KHR_DF_PRIMARIES_BT709: return "lin_rec709_scene"; + case KHR_DF_PRIMARIES_ACESCC: return "lin_ap1_scene"; + case KHR_DF_PRIMARIES_ACES: return "lin_ap0_scene"; + case KHR_DF_PRIMARIES_CIEXYZ: return "lin_ciexyzd65_scene"; + case KHR_DF_PRIMARIES_ADOBERGB: return "lin_adobergb_scene"; + case KHR_DF_PRIMARIES_DISPLAYP3: return "lin_p3d65_scene"; + case KHR_DF_PRIMARIES_BT2020: return "lin_rec2020_scene"; + default: break; + } + break; + case KHR_DF_TRANSFER_PQ_EOTF: + switch (primaries) { + case KHR_DF_PRIMARIES_BT2020: return "pq_rec2020_display"; + default: break; + } + case KHR_DF_TRANSFER_HLG_EOTF: + switch (primaries) { + case KHR_DF_PRIMARIES_BT2020: return "hlg_rec2020_display"; + default: break; + } + + default: break; + } + 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; + + // Formatting is turned off because having single-statement ifs on multiple + // lines is really hard to follow... + // clang-format off + { // 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); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, etc1s_clevel) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:etc1s_compression_level", Strutil::stoui(m[1].str())); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, etc1s_qlevel) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:etc1s_quality_level", Strutil::stoui(m[1].str())); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, etc1s_max_endpoints) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:etc1s_max_endpoints", Strutil::stoui(m[1].str())); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, etc1s_endpoint_rdo_threshold) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:etc1s_endpoint_rdo_threshold", Strutil::stof(m[1].str())); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, etc1s_max_selectors) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:etc1s_max_selectors", Strutil::stoui(m[1].str())); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, etc1s_selector_rdo_threshold) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:etc1s_selector_rdo_threshold", Strutil::stof(m[1].str())); + + if (Strutil::icontains(ktx_sc_params, "--no-endpoint-rdo")) m_spec.extra_attribs.attribute("ktx:etc1s_no_endpoint_rdo", true); + if (Strutil::icontains(ktx_sc_params, "--no-selector-rdo")) m_spec.extra_attribs.attribute("ktx:etc1s_no_selector_rdo", 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_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_hdr_lambda_re("--uastc-hdr-lambda\\s+((\\d*[.])?\\d+)", f); + std::regex uastc_hdr_6x6i_level_re("--uastc-hdr-6x6i-level\\s+(\\d+)", f); + + // KTX Tools sets uses `--uastc-quality` to set the quality for UASTC LDR/HDR. + // These are NOT UASTC quality flags but rather a quality setting with the valid range [0, 4] + if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_quality_re) && m.size() == 2) { + // KTX Tools users may set `--uastc-quality` to something higher than 4 hence why we clamp it here + // Least significant 4 bits are for the level (e.g., fastest, slowest, etc.) + // These are the most important bits. The ramaining bits are for advanced codec quality options and cannot + // be set by KTX Tools' `--uastc-quality` option. This is why we set the remaining bits to 0 (default). + const auto uastc_quality_level = std::clamp(Strutil::stoui(m[1].str()), 0, KTX_PACK_UASTC_MAX_LEVEL); + m_spec.extra_attribs.attribute("ktx:uastc_flags", uastc_quality_level); + m_spec.extra_attribs.attribute("ktx:uastc_hdr_quality", uastc_quality_level); + } + + if (Strutil::icontains(ktx_sc_params, "--uastc-rdo")) m_spec.extra_attribs.attribute("ktx:uastc_rdo", true); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_rdo_l_re) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:uastc_rdo_quality_scalar", Strutil::stof(m[1].str())); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_rdo_d_re) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:uastc_rdo_dict_size", Strutil::stoui(m[1].str())); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_rdo_b_re) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:uastc_rdo_max_smooth_block_error_scale", Strutil::stof(m[1].str())); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_rdo_s_re) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:uastc_rdo_max_smooth_block_std_dev", Strutil::stof(m[1].str())); + + if (Strutil::icontains(ktx_sc_params, "--uastc-rdo-f")) m_spec.extra_attribs.attribute("ktx:uastc_rdo_dont_favor_simpler_modes", true); + if (Strutil::icontains(ktx_sc_params, "--uastc-rdo-m")) m_spec.extra_attribs.attribute("ktx:uastc_rdo_no_multithreading", true); + if (Strutil::icontains(ktx_sc_params, "--uastc-hdr-uber-mode")) m_spec.extra_attribs.attribute("ktx:uastc_hdr_uber_mode", true); + if (Strutil::icontains(ktx_sc_params, "--uastc-hdr-ultra-quant")) m_spec.extra_attribs.attribute("ktx:uastc_hdr_ultra_quant", true); + if (Strutil::icontains(ktx_sc_params, "--uastc-hdr-favor-astc")) m_spec.extra_attribs.attribute("ktx:uastc_hdr_favor_astc", true); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_hdr_lambda_re) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:uastc_hdr_lambda", Strutil::stof(m[1].str())); + + if (std::smatch m; std::regex_search(ktx_sc_params, m, uastc_hdr_6x6i_level_re) && m.size() == 2) + m_spec.extra_attribs.attribute("ktx:uastc_hdr_level", Strutil::stoui(m[1].str())); + } + + { // Common params (see KTX-Software/tools/ktx/encode_utils_common.h) + std::regex zlib_level_re("--zlib\\s+(\\d+)", f); + std::regex zstd_level_re("--zstd\\s+(\\d+)", f); + + // If the input texture's super-compression scheme and this metadata disagree then just ignore this metadata. + if (std::smatch m; std::regex_search(ktx_sc_params, m, zlib_level_re) && m.size() == 2 + && Strutil::iequals(m_spec.get_string_attribute("compression"), "zip")) + m_spec.attribute("compression", fmt::format("zip:{}", Strutil::stoui(m[1].str()))); + + // If the input texture's super-compression scheme and this metadata disagree then just ignore this metadata. + if (std::smatch m; std::regex_search(ktx_sc_params, m, zstd_level_re) && m.size() == 2 + && Strutil::iequals(m_spec.get_string_attribute("compression"), "zstd")) + m_spec.attribute("compression", fmt::format("zstd:{}", Strutil::stoui(m[1].str()))); + + if (Strutil::icontains(ktx_sc_params, "--normal-mode")) m_spec.extra_attribs.attribute("ktx:normalmap", true); + if (Strutil::icontains(ktx_sc_params, "--no-sse")) m_spec.extra_attribs.attribute("ktx:no_sse", true); + } + // clang-format on + + // 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..539dea41f9 --- /dev/null +++ b/src/ktx.imageio/ktxoutput.cpp @@ -0,0 +1,1536 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include "OpenImageIO/color.h" +#include "ktx_pvt.h" + +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" << std::endl; +#endif + } + + ~KtxOutput() override { close(); } + + const char* format_name(void) const override { return "ktx"; } + + int supports(string_view feature) const override + { + // Note: make sure "origin" is NOT supported! All write_* methods assume + // 0s for x,y,z origins! + return ( + feature == "alpha" || feature == "ioproxy" || + // TODO: properly implement arbitrary_metadata + // as per the KTX2 specs: + // registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_keyvalue_data + // feature == "arbitrary_metadata" || + // TODO: KTX2 supports 2D texture arrays, but this plugin doesn't + // implement it yet. + // feature == "multiimage" || + // KTX2 supports mipmaps + feature == "mipmap" || + // KTX2 supports 3D textures + feature == "volumes" || + // Can write in any order whatsoever + feature == "random_access" || + // Can write Cubemaps and volume textures (these are simulated as + // tiles even though libktx actually treats volumes slices as + // subimages) + feature == "tiles"); + } + + bool open(const std::string& name, const ImageSpec& spec, + OpenMode mode = Create) override; + + // Calls the span-based single-scanline implementation + bool write_scanline(int y, int z, TypeDesc format, const void* data, + stride_t xstride) override; + + // Calls the span-based multi-scanline implementation + bool write_scanline(int y, TypeDesc format, + const image_span& data) override; + + // Calls the span-based multi-scanline implementation + bool write_scanlines(int ybegin, int yend, int z, TypeDesc format, + const void* data, stride_t xstride = AutoStride, + stride_t ystride = AutoStride) override; + + // Actual implementation + bool write_scanlines(int ybegin, int yend, TypeDesc format, + const image_span& data) override; + + // Calls the span-based single-tile implementation + bool write_tile(int x, int y, int z, TypeDesc format, const void* data, + stride_t xstride = AutoStride, + stride_t ystride = AutoStride, + stride_t zstride = AutoStride) override; + + // Calls the span-based multi-tile implementation + bool write_tile(int x, int y, int z, TypeDesc format, + const image_span& data) override; + + // Actual implementation + bool write_tiles(int xbegin, int xend, int ybegin, int yend, int zbegin, + int zend, TypeDesc format, + const image_span& data) 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 }; + + int m_miplevel_idx { -1 }; // Current MIP level + + int 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_face_baseheight { 0 }; // MIP level 0 height of a mipmap face + + uint32_t m_basedepth { 0 }; // MIP level 0 depth + + bool m_is1D { false }; // is this a 1D texture? + + bool m_is3D { false }; // is this a 3D texture? + + bool m_is_cubemap { false }; // is this a cubemap texture? + + 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 }; + + // Should the target VkFormat be UNORM/SNORM (if possible, given other + // ImageSpec inforamtion and depending on the ImageSpec::format)? + bool m_normalized { true }; + + BlockCompression m_cmp { BlockCompression::NONE }; + + bool m_use_basis_universal { false }; + + ktxBasisParams m_basis_params { 0 }; + + ktxAstcParams m_astc_params { 0 }; // Params for ASTC GPU 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. + + khr_df_transfer_e m_tf { KHR_DF_TRANSFER_UNSPECIFIED }; + + khr_df_primaries_e m_primaries { KHR_DF_PRIMARIES_UNSPECIFIED }; + + int m_nthreads { 1 }; + + // + // Container for raw (i.e., uncompressed) texture data structured as: + // mip level -> pixels (all faces/slices in same contiguous array) + // + // To access underlying data: m_imgs[miplevel_idx] + face_or_slice_offset + // + // NOTES: + // - The number of slices/faces is known before hand (i.e., appending + // slices/faces is NOT supported). + // - 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). The latter isn't even supported by + // libktx (as far as I know). + // + std::vector> m_imgs; + + bool validate_cubemap_userspec(const ImageSpec& userspec) const; + + bool validate_volume_userspec(const ImageSpec& userspec) const; + + bool validate_2d_texture_userspec(const ImageSpec& userspec) const; + + bool append_mipmaps_vector(imagesize_t image_bytes) noexcept; + + void init() noexcept; + + bool write_ktx2(); + + bool construct_basis_params(ktxBasisParams& params, + std::string_view codec) const; + + bool construct_astc_params(ktxAstcParams& params) const; + + bool try_parse_astc_block_dimensions( + string_view block_dims_str, + ktx_pack_astc_block_dimension_e& block_dims) const noexcept; + + bool try_parse_primaries(string_view primaries_str, + khr_df_primaries_e& primaries) const noexcept; + + bool try_parse_tf(string_view tf_str, khr_df_transfer_e& tf) const noexcept; + + // Copied as is from imageoutput.cpp because it is currently + // translation-unit static and not usable from here + bool check_span_size(TypeDesc datatype, imagesize_t npixels, + const image_span& data) const; + + bool get_tf_and_primaries(string_view oiio_colorspace, + khr_df_transfer_e& tf, + khr_df_primaries_e& primaries) 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.x != 0 || userspec.y != 0 || userspec.z != 0) { + close(); + errorfmt("Non-zero origins are not supported by KTX2 OIIO plugin"); + return false; + } + + // + // "textureformat" attribute is used to unambiguously determine the kind + // of texture to write. For cubemaps, user MUST set this to + // "CubeFace Environment" for the output to be a cubmap texture (this is + // done instead of just checking tile_width to avoid ambiguity). + // For volumes, user MUST either set depth > 1 or set the attribute to + // "Volume Texture" + // + const auto texformat = userspec.get_string_attribute("textureformat"); + + // + // Before anything, do some basic checks on the given dimensions + // depending on the given texture kind (2D, 3D, Cubemap, etc.). + // + // The check_open() calls below set the m_spec from the given userspec + // + // Note: + // userspec.tile_width > 1 is NOT a reliable way to check for cubemap + // outputs (e.g., it could simply be that provided userspec corresponds + // to a tiled image; exr for instance). + // + if (userspec.depth > 1 + || Strutil::iequals(texformat, "Volume Texture")) { + if (!validate_volume_userspec(userspec) + || !check_open(mode, userspec, + { 0, 4096, 0, 4096, 0, 4096, 0, 4 })) { + close(); + return false; + } + m_is3D = true; + } else if (Strutil::iequals(texformat, "CubeFace Environment")) { + if (!validate_cubemap_userspec(userspec) + || !check_open(mode, userspec, + { 0, 16384, 0, 16384 * 6, 0, 1, 0, 4 })) { + close(); + return false; + } + m_is_cubemap = true; + m_face_baseheight = m_spec.tile_height; + } else /* Plain Texture (default) */ { + if (!validate_2d_texture_userspec(userspec) + || !check_open(mode, userspec, + { 0, 32768, 0, 32768, 0, 1, 0, 4 })) { + close(); + return false; + } + } + + // Save base dimensions (useful to compute MIP-level dims) + m_basewidth = m_spec.width; + m_baseheight = m_spec.height; + m_basedepth = m_spec.depth; + + // Save name for later use + m_filename = name; + + m_nthreads = std::max(1, OIIO::get_int_attribute("threads", 0)); + + // no need to check on the format (TypeDesc) here; if it is not + // supported `get_vkformat_from_info` below will fail. + + const auto colorspace = m_spec.get_string_attribute("oiio:ColorSpace", + "unknown"); + m_spec.set_colorspace(colorspace); + + // Try to get tf + primaries from OIIO colorspace. + get_tf_and_primaries(colorspace, m_tf, m_primaries); + + // If user wants to override (not convert!) the tf and/or primaries; + // give them priority over the currently set colorspace (i.e., these + // are similar to --assign-primaries and --assign-tf in KTX Tools CLI). + + // + // Since ktxTexture2_Create always sets primaries to BT709 (because it + // just can't know what primaries the input is using), allow the user to + // override the primaries using "ktx:primaries". This is especially + // useful when targeting glTF which requires non-color KTX2's primaries + // to be set to UNSPECIFIED. + // + // You almost always want to set these for any non-sRGB VkFormats (since + // most of the time these are non-color data such as normal maps, height + // maps, etc.). + // + if (auto Q = m_spec.find_attribute("ktx:primaries", TypeDesc::STRING)) { + if (!try_parse_primaries(Q->get_string(), m_primaries)) { + close(); + errorfmt("Provided unsupported primaries: {}", Q->get_string()); + return false; + } + } + + if (auto Q = m_spec.find_attribute("ktx:tf", TypeDesc::STRING)) { + if (!try_parse_tf(Q->get_string(), m_tf)) { + close(); + errorfmt("Provided unsupported transfer function: {}", + Q->get_string()); + return false; + } + } + + // TODO: erroring on (m_tf == KHR_DF_TRANSFER_UNSPECIFIED) causes + // sourceprovenance.nanobind and other tests to fail. Just keep + // colorspace as "unknown"? + + bool is_srgb = m_tf == KHR_DF_TRANSFER_SRGB; + + if (auto Q = m_spec.find_attribute("ktx:1d", TypeDesc::INT)) { + auto ndims = Q->get_int(); + m_is1D = ndims == 1; + } + + // For 1D textures, we expect the height to either be 0 or 1. Code + // handles "0" heights fine (because of std::max(height, 1)) + if (m_is1D && m_baseheight > 1) { + close(); + errorfmt( + "1D textures expect a height of 0 or 1 but supplied height is {}", + m_baseheight); + return false; + } + + m_use_basis_universal = false; + if (auto Q = m_spec.find_attribute("ktx:codec", TypeDesc::STRING)) { + const auto& codec = Q->get_string(); + // Check if this is a GPU block-compressed format (e.g., ASTC, BCn) + // otherwise check if it is a Basis Universal codec. + if (Strutil::iequals(codec, "astc")) { + m_cmp = BlockCompression::ASTC; +#if 0 /* for when BCn gets added */ + } else if (Strutil::iequals(compression, "bc1")) { + m_cmp = BlockCompression::BC1; + } else if (Strutil::iequals(compression, "bc1a")) { + m_cmp = BlockCompression::BC1A; + } else if (Strutil::iequals(compression, "bc2")) { + m_cmp = BlockCompression::BC2; + } else if (Strutil::iequals(compression, "bc3")) { + m_cmp = BlockCompression::BC3; + } else if (Strutil::iequals(compression, "bc4")) { + m_cmp = BlockCompression::BC4; + } else if (Strutil::iequals(compression, "bc5")) { + m_cmp = BlockCompression::BC5; + } else if (Strutil::iequals(compression, "bc6hu")) { + m_cmp = BlockCompression::BC6HU; + } else if (Strutil::iequals(compression, "bc6hs")) { + m_cmp = BlockCompression::BC6HS; + } else if (Strutil::iequals(compression, "bc7")) { + m_cmp = BlockCompression::BC7; +#endif + } else if (!codec.empty() + && !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] GPU block compression or basis universal codec" + " (from \"ktx:codec\"): " + << codec << std::endl; + } + + // + // Use sensible defaults in case the input data did not originate from a + // KTX2 file and the user did not provide a supercompression scheme. + // KTX2 usually uses BasisLZ supercompression scheme to benefit from + // both: smaller disk filesizes and on-the-fly transcoding to a + // supported native GPU format. + // + const auto cmp_lvl = m_spec.decode_compression_metadata(); + // BasisLZ is automatically applied when input is ETC1S. It makes no + // sense to apply it manually. + if (Strutil::iequals(cmp_lvl.first, "zstd")) { + m_superCmp = ktxSupercmpScheme::KTX_SS_ZSTD; + if (cmp_lvl.second != -1) + m_zstd_level = std::clamp(cmp_lvl.second, 1u, 22u); + } else if (Strutil::iequals(cmp_lvl.first, "zip") + || Strutil::iequals(cmp_lvl.first, "deflate") + || Strutil::iequals(cmp_lvl.first, "zlib")) { + m_superCmp = ktxSupercmpScheme::KTX_SS_ZLIB; + if (cmp_lvl.second != -1) + m_zlib_level = std::clamp(cmp_lvl.second, 1u, 9u); + } else if (cmp_lvl.first.empty()) { + m_superCmp = ktxSupercmpScheme::KTX_SS_NONE; + } else { + // If anything else is specified just default to ZSTD because it is + // lossless and usually produces the highest compression. + m_superCmp = ktxSupercmpScheme::KTX_SS_ZSTD; + } + + if (auto Q = m_spec.find_attribute("ktx:generate_mipmaps", + TypeDesc::INT)) { + m_generate_mipmaps = static_cast(Q->get_int()); + DBG std::cout << "[ktxoutput] generate mipmaps: " << std::boolalpha + << m_generate_mipmaps << std::endl; + } + + if (auto Q = m_spec.find_attribute("ktx:normalized", TypeDesc::INT)) { + m_normalized = static_cast(Q->get_int()); + DBG std::cout << "[ktxoutput] normalized: " << std::boolalpha + << m_normalized << std::endl; + } + + // + // 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?) + // + // The approach is to simply determine the VkFormat from the ImageSpec. + // + m_vkformat = get_vkformat_from_info(m_spec, is_srgb, m_normalized); + if (m_vkformat == VK_FORMAT_UNDEFINED) { + close(); + errorfmt("Failed to determine VkFormat from nchannels={} format={} " + "colorspace={} normalize={}", + m_spec.nchannels, m_spec.format, colorspace, m_normalized); + return false; + } + + // max(dims) and not min because mip levels don't just stop at first + // dimension that reaches 1 (all dimensions must reach 1). + // Max nbr miplevels for cubemaps MUST be computed from dimensions of + // individual faces not combined faces (i.e., 1x6)! + m_max_nmiplevels = calculate_nbr_mip_levels( + m_is3D ? std::max({ m_spec.width, m_spec.height, m_spec.depth }) + : m_is_cubemap ? std::max(m_spec.tile_width, m_spec.tile_height) + : std::max(m_spec.width, m_spec.height)); + + DBG std::cout << "[ktxoutput] max number mip levels allowed: " + << m_max_nmiplevels << std::endl; + + // Initialize slices/faces container + OIIO_ASSERT(m_imgs.empty() + && "Expected mip levels container to be empty"); + OIIO_ASSERT(m_miplevel_idx == -1); + + // Reserve space for base-level mipmap (level 0) + if (!append_mipmaps_vector(m_spec.image_bytes())) { + // if we can't even allocate base-level mipmap => close() + close(); + errorfmt( + "Failed to create internal buffer to hold the base mip level " + "image. This is probably due to insufficient memory and/or " + "large image dimensions."); + return false; + } + + ioproxy_retrieve_from_config(m_spec); + if (!ioproxy_use_or_open(m_filename)) { + close(); + return false; + } + + m_initialized = true; + return true; + } // mode == Create + + if (mode == AppendMIPLevel) { + // Don't call close() here (use may want to access previously appended + // data in case of error) + if (!m_initialized) { + errorfmt("Cannot append a MIP level if no file has been opened. " + "Make sure to call open() with mode=\"Create\" first."); + return false; + } + + if (m_generate_mipmaps) { + errorfmt( + "Cannot append MIP levels if Graphics API mipmap generation is " + "requested (i.e., when \"ktx:generate_mipmaps\" is true)."); + return false; + } + + if ((m_miplevel_idx + 1) >= m_max_nmiplevels) { + errorfmt("Maximum number of mip levels {} is reached", + m_max_nmiplevels); + return false; + } + + const uint32_t miplevel_width + = std::max(1u, m_basewidth >> (m_miplevel_idx + 1)); + const uint32_t miplevel_height + = m_is_cubemap + ? std::max(1u, m_face_baseheight >> (m_miplevel_idx + 1)) * 6 + : std::max(1u, m_baseheight >> (m_miplevel_idx + 1)); + const uint32_t miplevel_depth + = std::max(1u, m_basedepth >> (m_miplevel_idx + 1)); + + if (userspec.width < 1 || userspec.height < 1 || userspec.depth < 1) { + errorfmt("MIP level dimensions must be positive"); + return false; + } + + // Check if provided userspec has same nbrchannels, channelnames, etc. + // as spec used in Open mode. + if (userspec.x != m_spec.x || userspec.y != m_spec.y + || userspec.z != m_spec.z || userspec.nchannels != m_spec.nchannels + || userspec.channelformats != m_spec.channelformats + || userspec.format != m_spec.format + || userspec.alpha_channel != m_spec.alpha_channel + || userspec.z_channel != m_spec.z_channel + || userspec.deep != m_spec.deep) { + errorfmt( + "Provided userspec for MIP level {} does not match the original" + " spec used in Open mode", + m_miplevel_idx + 1); + return false; + } + + if ((uint32_t)userspec.width != miplevel_width + || (uint32_t)userspec.height != miplevel_height + || (uint32_t)userspec.depth != miplevel_depth) { + errorfmt( + "Expected (width,height,depth): ({},{},{}) but got: ({},{},{})", + miplevel_width, miplevel_height, miplevel_depth, userspec.width, + userspec.height, userspec.depth); + return false; + } + + // Using validate_cubemap_userspec/validate_volume_userspec checks here + // on the userspec fail because oiiotool doesn't update the + // tile_width/tile_height for mip levels > 0 + if (!m_is_cubemap && !m_is3D) { + if (!validate_2d_texture_userspec(userspec)) + return false; + } + + // Reserve memory for this mip level and increment MIP-level index + if (!append_mipmaps_vector(userspec.image_bytes())) { + // Don't close as the caller may need to access the previously + // written data! + // close(); + errorfmt( + "Failed to create internal buffer to hold data for the mip " + "level {}. This is probably due to insufficient memory and/or " + "large image dimensions.", + m_miplevel_idx + 1); + return false; + } + + // Only update current spec after passing all checks + m_spec.width = m_spec.full_width = userspec.width; + m_spec.height = m_spec.full_height = userspec.height; + m_spec.depth = m_spec.full_depth = userspec.depth; + + if (m_is3D) { + // If this is a volume texture, also update tile_width + m_spec.tile_width = userspec.width; + m_spec.tile_height = 1; + m_spec.tile_depth = 1; + } else if (m_is_cubemap) { + // If this is a cubemap texture, update tile dims + m_spec.tile_width = userspec.width; + m_spec.tile_height = userspec.height / 6; + m_spec.tile_depth = 1; + } + + return true; + } // mode == AppendMIPLevel + + // (mode == AppendSubimage) is NOT supported. Pre-declaring the number of + // subimages is easier to implement (random access to array entries is + // already supported). + + 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_scanline(int y, TypeDesc format, + const image_span& data) +{ + return write_scanlines(y, y + 1, format, data); +} + + + +bool +KtxOutput::write_scanlines(int ybegin, int yend, int /* z */, TypeDesc format, + const void* data, stride_t xstride, stride_t ystride) +{ + const image_span data_img_span(static_cast(data), + m_spec.nchannels, m_spec.width, + yend - ybegin, 1, AutoStride, xstride, + ystride, AutoStride, + format.is_unknown() ? m_spec.format.size() + : format.size()); + return write_scanlines(ybegin, yend, format, data_img_span); +} + + + +bool +KtxOutput::write_scanlines(int ybegin, int yend, TypeDesc format, + const image_span& data) +{ + // For cubemaps and volumes use write_tiles() + if (m_is_cubemap || m_is3D) + return false; + + // No need to care about x,y,z origins; they are not supported and always 0 + if (ybegin < 0 || yend > m_spec.height || ybegin >= yend) { + errorfmt("write_scanlines: Invalid scanline range: [{},{})", ybegin, + yend); + return false; + } + + if (!check_span_size(format, m_spec.width * size_t(yend - ybegin), data)) + return false; + + if (m_miplevel_idx < 0 || size_t(m_miplevel_idx) >= m_imgs.size()) { + errorfmt("write_tiles: no valid MIP level is open"); + return false; + } + + return copy_to_image_buffer(0, m_spec.width, ybegin, yend, 0, 1, format, + data.data(), data.xstride(), data.ystride(), + data.zstride(), m_imgs[m_miplevel_idx].data()); +} + + + +bool +KtxOutput::write_tile(int x, int y, int z, TypeDesc format, const void* data, + stride_t xstride, stride_t ystride, stride_t zstride) +{ + const image_span data_img_span(static_cast(data), + m_spec.nchannels, m_spec.tile_width, + m_spec.tile_height, m_spec.tile_depth, + AutoStride, xstride, ystride, zstride, + format.is_unknown() ? m_spec.format.size() + : format.size()); + return write_tile(x, y, z, format, data_img_span); +} + + + +bool +KtxOutput::write_tile(int x, int y, int z, TypeDesc format, + const image_span& data) +{ + return write_tiles(x, x + m_spec.tile_width, y, y + m_spec.tile_height, z, + z + m_spec.tile_depth, format, data); +} + + + +bool +KtxOutput::write_tiles(int xbegin, int xend, int ybegin, int yend, int zbegin, + int zend, TypeDesc format, + const image_span& data) +{ + // Basis checks on provided ranges + if (!m_spec.valid_tile_range(xbegin, xend, ybegin, yend, zbegin, zend)) { + errorfmt("write_tiles: invalid tile range [{},{})x[{},{})x[{},{})", + xbegin, xend, ybegin, yend, zbegin, zend); + return false; + } + + if (!check_span_size(format, + size_t(int64_t(xend) - xbegin) + * size_t(int64_t(yend) - ybegin) + * size_t(int64_t(zend) - zbegin), + data)) + return false; // check_span_size sets errorfmt + + if (m_miplevel_idx < 0 || size_t(m_miplevel_idx) >= m_imgs.size()) { + errorfmt("write_tiles: no valid MIP level is open"); + return false; + } + + // copy_tile_to_image_buffer is a wrapper around this and does checks that + // we already have done + it copies the whole ImageSpec so just use this + // directly. + return copy_to_image_buffer(xbegin, xend, ybegin, yend, zbegin, zend, + format, data.data(), data.xstride(), + data.ystride(), data.zstride(), + m_imgs[m_miplevel_idx].data()); +} + + + +bool +KtxOutput::close() +{ + // closing an un-opened ImageOutput instance is fine + if (!m_initialized) { + // we could error before setting `m_initialized = true` and we could + // call open() again. Make sure to re-initialize. + init(); + return true; + } + // Check if already closed => if so, then the KTX2 file is already saved + if (!ioproxy_opened()) { + init(); + return true; + } + if (!write_ktx2()) { + // Do not init() + return false; + } + init(); + return true; +} + + + +bool +KtxOutput::validate_cubemap_userspec(const ImageSpec& userspec) const +{ + // In KTX2, cubemaps are 'simulated' as 1x6 layout (tiles along the + // height). KTX2 requires that cubemap faces are squared. + if (userspec.depth > 1 || userspec.width != userspec.tile_width + || userspec.height != userspec.tile_height * 6 + || userspec.tile_depth != 1 + || userspec.tile_width != userspec.tile_height) { + errorfmt("For cubemap textures: depth must be <= 1 (provided {});" + " tile width must be: {} (provided {});" + " tile height must be 1/6th the height: {} (provided {});" + " tile depth must be: 1 (provided {}) ", + userspec.depth, userspec.width, userspec.tile_width, + userspec.height, userspec.tile_height, userspec.tile_depth); + return false; + } + return true; +} + + + +bool +KtxOutput::validate_volume_userspec(const ImageSpec& userspec) const +{ + if (userspec.depth < 1 || userspec.tile_width != userspec.width + || userspec.tile_height != 1 || userspec.tile_depth != 1) { + errorfmt("For volume textures: depth must be >= 1 (provided {});" + " tile width must be: {} (provided {});" + " tile height must be: 1 (provided {});" + " tile depth must be: 1 (provided {})", + userspec.depth, userspec.width, userspec.tile_width, + userspec.tile_height, userspec.tile_depth); + return false; + } + return true; +} + + + +bool +KtxOutput::validate_2d_texture_userspec(const ImageSpec& userspec) const +{ + if (userspec.depth > 1) { + errorfmt("Provided depth is {} > 1 but \"textureformat\" attribute " + "is not set to \"Volume Texture\"", + userspec.depth); + return false; + } + if (userspec.tile_width != 0 || userspec.tile_height != 0 + || userspec.tile_depth > 1 /* tile_depth is by default 1 */) { + errorfmt("Provided a tiled texture but \"textureformat\" attribute " + "is not set to \"CubeFace Environment\""); + return false; + } + return true; +} + + + +bool +KtxOutput::append_mipmaps_vector(imagesize_t image_bytes) noexcept +{ + try { + m_imgs.emplace_back(image_bytes, uint8_t(0)); + ++m_miplevel_idx; + return true; + } catch (const std::bad_alloc&) { + return false; + } catch (const std::length_error&) { + return false; + } +} + + + +void +KtxOutput::init() noexcept +{ + m_filename = std::string(); + m_initialized = false; + m_vkformat = VK_FORMAT_UNDEFINED; + m_miplevel_idx = -1; + m_max_nmiplevels = 1; + m_basewidth = 0; + m_baseheight = 0; + m_face_baseheight = 0; + m_basedepth = 0; + m_is1D = false; + m_is3D = false; + m_is_cubemap = false; + m_superCmp = KTX_SS_NONE; + m_generate_mipmaps = false; + m_normalized = true; + m_cmp = BlockCompression::NONE; + m_use_basis_universal = false; + m_basis_params = { 0 }; + m_astc_params = { 0 }; + m_zlib_level = 9; + m_zstd_level = 22; + m_tf = KHR_DF_TRANSFER_UNSPECIFIED; + m_primaries = KHR_DF_PRIMARIES_UNSPECIFIED; + // clear & release the capacity even though m_imgs' capacity itself is + // negligeable (it just holds nbr miplevels elements) + // swap is noexcept and constructor an empty vector doesn't allocate + // anything => no worries about exceptions + std::vector>().swap(m_imgs); + ioproxy_clear(); +} + + + +// +// Construct ktxBasisParams struct from the Basis Universal attributes in +// current ImageSpec. +// +// Returns false if provided `codec` does not correspond to a valid Basis +// Universal codec. +// +bool +KtxOutput::construct_basis_params(ktxBasisParams& params, + std::string_view codec) const +{ + // Set defaults + params = { 0 }; + params.structSize = sizeof(ktxBasisParams); + params.threadCount = m_nthreads; + + params.etc1sCompressionLevel = KTX_ETC1S_DEFAULT_COMPRESSION_LEVEL; + + params.uastcFlags = KTX_PACK_UASTC_LEVEL_DEFAULT; + + if (Strutil::iequals(codec, "uastc") || Strutil::iequals(codec, "uastc-ldr") + || Strutil::iequals(codec, "uastc-ldr-4x4")) { + params.codec = KTX_BASIS_CODEC_UASTC_LDR_4x4; + } else if (Strutil::iequals(codec, "uastc-hdr") + || Strutil::iequals(codec, "uastc-hdr-4x4")) { + params.codec = KTX_BASIS_CODEC_UASTC_HDR_4x4; + } else if (Strutil::iequals(codec, "etc1s")) { + params.codec = KTX_BASIS_CODEC_ETC1S; + } else if (Strutil::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; + } + + // Formatting is turned off because having single-statement ifs on multiple + // lines is really hard to follow... + // clang-format off + if (params.codec == KTX_BASIS_CODEC_ETC1S) { + // Params that only apply to ETC1S + if (auto Q = m_spec.find_attribute("ktx:etc1s_compression_level", TypeDesc::UINT32)) params.etc1sCompressionLevel = *static_cast(Q->data()); + if (auto Q = m_spec.find_attribute("ktx:etc1s_quality_level", TypeDesc::UINT32)) params.qualityLevel = *static_cast(Q->data()); + if (auto Q = m_spec.find_attribute("ktx:etc1s_max_endpoints", TypeDesc::UINT32)) params.maxEndpoints = *static_cast(Q->data()); + if (auto Q = m_spec.find_attribute("ktx:etc1s_endpoint_rdo_threshold", TypeDesc::FLOAT)) params.endpointRDOThreshold = Q->get_float(); + if (auto Q = m_spec.find_attribute("ktx:etc1s_max_selectors", TypeDesc::UINT32)) params.maxSelectors = *static_cast(Q->data()); + if (auto Q = m_spec.find_attribute("ktx:etc1s_selector_rdo_threshold", TypeDesc::FLOAT)) params.selectorRDOThreshold = Q->get_float(); + if (auto Q = m_spec.find_attribute("ktx:etc1s_no_endpoint_rdo")) params.noEndpointRDO = static_cast(Q->get_int()); + if (auto Q = m_spec.find_attribute("ktx:etc1s_no_selector_rdo", 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 LDR + RDO + if (auto Q = m_spec.find_attribute("ktx:uastc_flags", TypeDesc::UINT32)) params.uastcFlags = *static_cast(Q->data()); + if (auto Q = m_spec.find_attribute("ktx:uastc_rdo", TypeDesc::INT)) params.uastcRDO = static_cast(Q->get_int()); + if (auto Q = m_spec.find_attribute("ktx:uastc_rdo_quality_scalar", TypeDesc::FLOAT)) params.uastcRDOQualityScalar = Q->get_float(); + if (auto Q = m_spec.find_attribute("ktx:uastc_rdo_dict_size", TypeDesc::UINT32)) params.uastcRDODictSize = *static_cast(Q->data()); + if (auto Q = m_spec.find_attribute("ktx:uastc_rdo_max_smooth_block_error_scale", TypeDesc::FLOAT)) params.uastcRDOMaxSmoothBlockErrorScale = Q->get_float(); + if (auto Q = m_spec.find_attribute("ktx:uastc_rdo_max_smooth_block_std_dev", TypeDesc::FLOAT)) params.uastcRDOMaxSmoothBlockStdDev = Q->get_float(); + if (auto Q = m_spec.find_attribute("ktx:uastc_rdo_dont_favor_simpler_modes", TypeDesc::INT)) params.uastcRDODontFavorSimplerModes = static_cast(Q->get_int()); + if (auto Q = m_spec.find_attribute("ktx:uastc_rdo_no_multithreading", 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) { + // Params that only apply to UASTC HDR + if (auto Q = m_spec.find_attribute("ktx:uastc_hdr_level", TypeDesc::UINT32)) params.uastcHDRLevel = *static_cast(Q->data()); + if (auto Q = m_spec.find_attribute("ktx:uastc_hdr_quality", TypeDesc::UINT32)) params.uastcHDRQuality = *static_cast(Q->data()); + if (auto Q = m_spec.find_attribute("ktx:uastc_hdr_uber_mode", TypeDesc::INT)) params.uastcHDRUberMode = static_cast(Q->get_int()); + if (auto Q = m_spec.find_attribute("ktx:uastc_hdr_ultra_quant", TypeDesc::INT)) params.uastcHDRUltraQuant = static_cast(Q->get_int()); + if (auto Q = m_spec.find_attribute("ktx:uastc_hdr_favor_astc", TypeDesc::INT)) params.uastcHDRFavorAstc = static_cast(Q->get_int()); + if (auto Q = m_spec.find_attribute("ktx:uastc_hdr_lambda", TypeDesc::FLOAT)) params.uastcHDRLambda = Q->get_float(); + } + + // Params that apply to both ETC1S and UASTC + if (auto Q = m_spec.find_attribute("ktx:no_sse", 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:input_swizzle", TypeDesc(TypeDesc::CHAR, 4))) memcpy(params.inputSwizzle, Q->data(), 4); + if (auto Q = m_spec.find_attribute("ktx:pre_swizzle", TypeDesc::INT)) params.preSwizzle = static_cast(Q->get_int()); + // clang-format on + + return true; +} + + + +// +// Construct ktxAstcParams struct from the ASTC GPU block compression attributes +// in current ImageSpec. +// +// Unlike `construct_basis_params`, this always succeeds. +// +bool +KtxOutput::construct_astc_params(ktxAstcParams& params) const +{ + // set sensible defaults + params = { 0 }; + params.structSize = sizeof(ktxAstcParams); + params.verbose = false; + params.threadCount = m_nthreads; + params.blockDimension + = ktx_pack_astc_block_dimension_e::KTX_PACK_ASTC_BLOCK_DIMENSION_4x4; + // KTX Tools does not expose this parameter so leave it as default + params.mode = KTX_PACK_ASTC_ENCODER_MODE_DEFAULT; + params.qualityLevel = KTX_PACK_ASTC_QUALITY_LEVEL_THOROUGH; + params.normalMap = false; + params.perceptual = false; + // assigning '\0' to first element means no swizzle + params.inputSwizzle[0] = '\0'; + // clang-format off + if (auto Q = m_spec.find_attribute("ktx:astc_block_dimensions", TypeDesc::STRING)) { + ktx_pack_astc_block_dimension_e block_dims; + // we don't care if this fails; just default to 4x4 + if (try_parse_astc_block_dimensions(Q->get_string(), block_dims)) { + params.blockDimension = block_dims; + } else { + errorfmt("Failed to parse ASTC block dimensions from " + "\"ktx:astc_block_dimensions\": {}", Q->get_string()); + return false; + } + } + if (auto Q = m_spec.find_attribute("ktx:astc_quality_level", TypeDesc::UINT32)) params.qualityLevel = *static_cast(Q->data()); + 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:perceptual", TypeDesc::INT)) params.perceptual = static_cast(Q->get_int()); + if (auto Q = m_spec.find_attribute("ktx:input_swizzle", TypeDesc(TypeDesc::CHAR, 4))) memcpy(params.inputSwizzle, Q->data(), 4); + // clang-format on + return true; +} + + + +bool +KtxOutput::try_parse_astc_block_dimensions( + string_view block_dims_str, + ktx_pack_astc_block_dimension_e& block_dims) const noexcept +{ + // Small lookup tables; just loop through and avoid string allocations + for (const auto& [k, v] : astc_block_dims_str_to_enum) { + if (Strutil::iequals(k, block_dims_str)) { + block_dims = v; + return true; + } + } + return false; +} + + + +bool +KtxOutput::try_parse_primaries(string_view primaries_str, + khr_df_primaries_e& primaries) const noexcept +{ + // Small lookup tables; just loop through and avoid string allocations + for (const auto& [k, v] : primaries_str_to_enum) { + if (Strutil::iequals(k, primaries_str)) { + primaries = v; + return true; + } + } + return false; +} + + + +bool +KtxOutput::try_parse_tf(string_view tf_str, + khr_df_transfer_e& tf) const noexcept +{ + // Small lookup tables; just loop through and avoid string allocations + for (const auto& [k, v] : tf_str_to_enum) { + if (Strutil::iequals(k, tf_str)) { + tf = v; + return true; + } + } + return false; +} + + + +// Copied as is from imageoutput.cpp because it is translation-unit static +// and not accessible from ktxoutput.cpp +bool +KtxOutput::check_span_size(TypeDesc datatype, imagesize_t npixels, + const image_span& data) const +{ + if (datatype.is_unknown()) { + size_t sz = npixels * m_spec.pixel_bytes(true); + if (sz != data.size_bytes()) { + errorfmt("image_span size is incorrect ({} bytes vs {} needed)", + data.size_bytes(), sz); + return false; + } + } else { + size_t nvals = npixels * size_t(m_spec.nchannels); + if (nvals != data.nvalues()) { + errorfmt("image_span size is incorrect ({} values vs {} needed)", + data.nvalues(), nvals); + return false; + } + } + return true; +} + + + +bool +KtxOutput::get_tf_and_primaries(string_view oiio_colorspace, + khr_df_transfer_e& tf, + khr_df_primaries_e& primaries) const +{ + const auto& colorconfig = ColorConfig::default_colorconfig(); + + // sRGB (this and non-color data are the only ones supported by glTF) + if (colorconfig.equivalent(oiio_colorspace, "srgb_rec709_scene") + || colorconfig.equivalent(oiio_colorspace, "srgb_rec709_display")) { + tf = KHR_DF_TRANSFER_SRGB; + primaries = KHR_DF_PRIMARIES_BT709; + return true; + } + + // sRGB ACEScg / ACEScc primaries (AP1) + if (colorconfig.equivalent(oiio_colorspace, "srgb_ap1_scene")) { + tf = KHR_DF_TRANSFER_SRGB; + primaries = KHR_DF_PRIMARIES_ACESCC; // ACEScc same primaries as ACEScg + return true; + } + + // Linear ACEScg / ACEScc primaries (AP1) + if (colorconfig.equivalent(oiio_colorspace, "lin_ap1_scene")) { + tf = KHR_DF_TRANSFER_LINEAR; + primaries = KHR_DF_PRIMARIES_ACESCC; // ACEScc same primaries as ACEScg + return true; + } + + // ACES2065-1 primaries (AP0) + if (colorconfig.equivalent(oiio_colorspace, "lin_ap0_scene")) { + tf = KHR_DF_TRANSFER_LINEAR; + primaries = KHR_DF_PRIMARIES_ACES; + return true; + } + + // non-color data (this and sRGB are the only ones supported by glTF) + // + // Note: + // If colorspace is `data` then primaries MUST be set to UNSPECIFIED and + // TF MUST be set to LINEAR. These are part of the requirements for + // integrating KTX2 within glTF as can be read from here: + // + // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_basisu/README.md#additional-requirements + // + if (colorconfig.equivalent(oiio_colorspace, "data")) { + tf = KHR_DF_TRANSFER_LINEAR; + primaries = KHR_DF_PRIMARIES_UNSPECIFIED; + return true; + } + + // Linear Rec.709 + if (colorconfig.equivalent(oiio_colorspace, "lin_rec709_scene") + || colorconfig.equivalent(oiio_colorspace, "lin_rec709_display")) { + tf = KHR_DF_TRANSFER_LINEAR; + primaries = KHR_DF_PRIMARIES_BT709; + return true; + } + + // CIE XYZ-D65 + if (colorconfig.equivalent(oiio_colorspace, "lin_ciexyzd65_scene")) { + tf = KHR_DF_TRANSFER_LINEAR; + primaries = KHR_DF_PRIMARIES_CIEXYZ; + return true; + } + + // Adobe RGB + if (colorconfig.equivalent(oiio_colorspace, "lin_adobergb_scene")) { + tf = KHR_DF_TRANSFER_LINEAR; + primaries = KHR_DF_PRIMARIES_ADOBERGB; + return true; + } + + // sRGB Display P3 + if (colorconfig.equivalent(oiio_colorspace, "srgb_p3d65_scene") + || colorconfig.equivalent(oiio_colorspace, "srgb_p3d65_display")) { + tf = KHR_DF_TRANSFER_SRGB; + primaries = KHR_DF_PRIMARIES_DISPLAYP3; + return true; + } + + // Linear Display P3 + if (colorconfig.equivalent(oiio_colorspace, "lin_p3d65_scene") + || colorconfig.equivalent(oiio_colorspace, "lin_p3d65_display")) { + tf = KHR_DF_TRANSFER_LINEAR; + primaries = KHR_DF_PRIMARIES_DISPLAYP3; + return true; + } + + // Rec. 2020 + if (colorconfig.equivalent(oiio_colorspace, "lin_rec2020_scene") + || colorconfig.equivalent(oiio_colorspace, "lin_rec2020_display")) { + tf = KHR_DF_TRANSFER_LINEAR; + primaries = KHR_DF_PRIMARIES_BT2020; + return true; + } + + // Rec. 2100 PQ + if (colorconfig.equivalent(oiio_colorspace, "pq_rec2020_display")) { + tf = KHR_DF_TRANSFER_PQ_EOTF; + primaries = KHR_DF_PRIMARIES_BT2020; + return true; + } + + // Rec. 2100 HLG + if (colorconfig.equivalent(oiio_colorspace, "hlg_rec2020_display")) { + tf = KHR_DF_TRANSFER_HLG_EOTF; + primaries = KHR_DF_PRIMARIES_BT2020; + return true; + } + + return false; +} + + + +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 appending mip levels. + // It would be really nice for libktx to support appending mip levels + // (at least to uncompressed textures) so that we can save significant + // memory allocations and memcpy's. + // + 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"); + // TODO: arrays are currently not supported + 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_is_cubemap ? m_face_baseheight : m_baseheight; + create_info.baseDepth = m_basedepth; + create_info.numDimensions = m_is1D ? 1u : m_is3D ? 3u : 2u; + create_info.numLevels = m_miplevel_idx + 1; + create_info.numLayers = 1; + create_info.numFaces = m_is_cubemap ? 6 : 1; + create_info.isArray = KTX_FALSE; + create_info.generateMipmaps = m_generate_mipmaps; + + DBG std::cout << "[ktxoutput] 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 failed. Reason: {}", + ktxErrorString(result)); + return false; + } + + if (m_is_cubemap) + OIIO_ASSERT(p_tex->numFaces == 6); + + // Set tf and primaries + result = ktxTexture2_SetPrimaries(tex.get(), m_primaries); + if (result != KTX_SUCCESS) { + errorfmt("ktxTexture2_SetPrimaries failed. Reason: {}", + ktxErrorString(result)); + return false; + } + + result = ktxTexture2_SetTransferFunction(tex.get(), m_tf); + if (result != KTX_SUCCESS) { + errorfmt("ktxTexture2_SetTransferFunction failed. Reason: {}", + ktxErrorString(result)); + return false; + } + + // + // 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 size_t miplvl_width = std::max(tex->baseWidth >> level_idx, 1u); + const size_t miplvl_height = std::max(tex->baseHeight >> level_idx, 1u); + const size_t miplvl_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 < miplvl_depth; + ++slice_idx) { + const size_t slice_or_face_size = miplvl_width + * miplvl_height + * m_spec.pixel_bytes(); + // Faces and Slices are mutually exclusive, addition is fine + const size_t slice_or_face_offset = (face_idx + slice_idx) + * slice_or_face_size; + auto data_ptr = m_imgs[level_idx].data(); + + // 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 (slice_or_face_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, slice_or_face_size); + return false; + } + + // TODO: this cast to (ktxTexture*) may be UB (lifetime + // of ktxTexture hasn't properly started, etc.). + // libktx doesn't expose: ktxTexture2_SetImageFromStdioStream + // so for the moment use this until this issue here gets + // solved: https://github.com/KhronosGroup/KTX-Software/issues/1273 + auto status = ktxTexture_SetImageFromMemory( + (ktxTexture*)tex.get(), level_idx, 0, + face_idx + slice_idx, data_ptr + slice_or_face_offset, + slice_or_face_size); + if (status != KTX_SUCCESS) { + errorfmt( + "ktxTexture_SetImageFromMemory failed. Reason: {}", + ktxErrorString(status)); + return false; + } + DBG std::cout << fmt::format( + "[ktxoutput] ktxTexture_SetImageFromMemory for " + "slice_idx={} face_idx={} level_idx={} wrote {} bytes", + slice_idx, face_idx, level_idx, slice_or_face_size) + << std::endl; + } // slices + } // faces + } // layers + } // mip levels + + // TODO: At this point we pay the memory price for at least 2X the size of + // the whole uncompressed image (with all its mipmaps, layers, faces/slices, + // etc.). It would be nice to just have a per-slice/face compression + // somehow implemented here. + + // + // 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: { + if (!construct_astc_params(m_astc_params)) + return false; + if (auto status = ktxTexture2_CompressAstcEx(tex.get(), + &m_astc_params); + status != KTX_SUCCESS) { + errorfmt("ktxTexture2_CompressAstc failed. Reason: {}", + ktxErrorString(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 failed. Reason: {}", + ktxErrorString(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 failed. Reason: {}", + ktxErrorString(status)); + return false; + } + } + + // + // Finally, apply the supercompression scheme (if any and only if the + // texture does NOT already have a supercompression applied). + // + // 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 (tex->supercompressionScheme == KTX_SS_NONE) { + if (m_superCmp == KTX_SS_ZLIB) { + if (auto status = ktxTexture2_DeflateZLIB(tex.get(), m_zlib_level); + status != KTX_SUCCESS) { + errorfmt("ktxTexture2_DeflateZLIB failed. Reason: {}", + ktxErrorString(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 failed. Reason: {}", + ktxErrorString(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); + if (auto status + = ktxHashList_AddKVPair(&tex->kvDataHead, KTX_WRITER_KEY, + (ktx_uint32_t)strlen(writer) + 1, writer); + status != KTX_SUCCESS) { + errorfmt("ktxHashList_AddKVPair failed. Reason: {}", + ktxErrorString(status)); + return false; + } + DBG std::cout << "[ktxoutput] KTXwrite: " << writer << '\n'; + } + + // + // Write KTXorientation metadata if textures are not oriented as + // right-downwards (default orientation in OIIO) + // see: https://openimageio.readthedocs.io/en/latest/stdmetadata.html + // + if (auto Q = m_spec.find_attribute("Orientation", TypeDesc::INT); + Q != nullptr && Q->get_int() != 1 /* normal orientation */) { + auto orientation = Q->get_int(); + char orientation_str[4] = { KTX_ORIENT_X_RIGHT, KTX_ORIENT_Y_DOWN, + KTX_ORIENT_Z_IN, '\0' }; // "rdi" + switch (orientation) { + case 2 /* flipped horizontally (top to bottom, right to left) */: + snprintf(orientation_str, sizeof(orientation_str), "%c%c%c", + KTX_ORIENT_X_LEFT, KTX_ORIENT_Y_DOWN, KTX_ORIENT_Z_OUT); + break; + case 3 /* rotated (bottom to top, right to left) */: + snprintf(orientation_str, sizeof(orientation_str), "%c%c%c", + KTX_ORIENT_X_LEFT, KTX_ORIENT_Y_UP, KTX_ORIENT_Z_IN); + break; + case 4 /* flipped vertically (bottom to top, left to right) */: + snprintf(orientation_str, sizeof(orientation_str), "%c%c%c", + KTX_ORIENT_X_RIGHT, KTX_ORIENT_Y_UP, KTX_ORIENT_Z_OUT); + break; + // Transposed images are not supported by KTX + case 5: + case 6: + case 7: + case 8: + errorfmt( + "Transposed orientation settings are not supported by KTX"); + return false; + default: // should never occur + OIIO_ASSERT(false); + errorfmt("Unsupported \"Orientation\" attribute: {}", orientation); + return false; + } + if (tex->numDimensions == 1) + // For 1D textures, we just need to write [rl] + orientation_str[1] = '\0'; + else if (tex->numDimensions == 2) + // For 2D textures, we just need to write [rl][du] + orientation_str[2] = '\0'; + if (auto status + = ktxHashList_AddKVPair(&tex->kvDataHead, KTX_ORIENTATION_KEY, + (ktx_uint32_t)strlen(orientation_str) + 1, + orientation_str); + status != KTX_SUCCESS) { + errorfmt("ktxHashList_AddKVPair failed. Reason: {}", + ktxErrorString(status)); + return false; + } + } + + Filesystem::IOProxy* m_io = ioproxy(); + if (!strcmp(m_io->proxytype(), "file")) { + auto fd = reinterpret_cast(m_io)->handle(); + if (auto status = ktxTexture2_WriteToStdioStream(tex.get(), fd); + status != KTX_SUCCESS) { + errorfmt("ktxTexture2_WriteToStdioStream failed. Reason: {}", + ktxErrorString(status)); + return false; + } + return true; + } + + if (!strcmp(m_io->proxytype(), "vecoutput")) { + auto proxy = reinterpret_cast(m_io); + // TODO: use ktxTexture2_WriteToStream instead + 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 failed. Reason: {}", + ktxErrorString(status)); + return false; + } + // RAII-based cleanup + auto _ = std::unique_ptr(buff, + std::free); + const auto nbr_written_bytes = proxy->write(buff, buff_size); + if (nbr_written_bytes != buff_size) { + errorfmt( + "vecoutput proxy failed to write {} bytes but just wrote instead {}", + buff_size, nbr_written_bytes); + return false; + } + return true; + } + + // TODO: memreader proxy + + // 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 641f5de775..de6d2f005e 100644 --- a/src/libOpenImageIO/imageioplugin.cpp +++ b/src/libOpenImageIO/imageioplugin.cpp @@ -285,6 +285,7 @@ PLUGENTRY(iff); PLUGENTRY(jpeg); PLUGENTRY(jpeg2000); PLUGENTRY(jpegxl); +PLUGENTRY(ktx); PLUGENTRY(null); PLUGENTRY(openexr); PLUGENTRY(openvdb); @@ -391,6 +392,9 @@ catalog_builtin_plugins() #if defined(USE_JXL) DECLAREPLUG (jpegxl); #endif +#if !defined(DISABLE_KTX) + DECLAREPLUG (ktx); +#endif #if !defined(DISABLE_NULL) DECLAREPLUG (null); #endif diff --git a/src/oiiotool/oiiotool.cpp b/src/oiiotool/oiiotool.cpp index 63a076d0d7..4915ac6ed9 100644 --- a/src/oiiotool/oiiotool.cpp +++ b/src/oiiotool/oiiotool.cpp @@ -6298,8 +6298,11 @@ output_file(Oiiotool& ot, cspan argv) adjust_output_options(filename, spec, ir->nativespec(s), ot, s, send, supports_tiles, fileoptions, (*ir)[s].was_direct_read()); - // If it's not tiled and MIP-mapped, remove any "textureformat" - if (!spec.tile_pixels() || ir->miplevels(s) <= 1) + // If it's not tiled and MIP-mapped (except for certain formats that + // support non-MIP-mapped textures), remove any "textureformat" + if (!spec.tile_pixels() + || (string_view(out->format_name()) != "ktx" + && ir->miplevels(s) <= 1)) spec.erase_attribute("textureformat"); subimagespecs[s] = spec; } diff --git a/testsuite/ktx/ref/out.txt b/testsuite/ktx/ref/out.txt new file mode 100644 index 0000000000..99540c8201 --- /dev/null +++ b/testsuite/ktx/ref/out.txt @@ -0,0 +1,870 @@ +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" + 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" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 7 +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" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 7 +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: 24752CDAB141EC8878F8C482C5CAD8B932F386D5 + channel list: R, G, B, A + tile size: 16 x 1 x 1 + textureformat: "Volume Texture" + 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" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 5 + oiio:subimages: 7 + 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" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 5 + oiio:subimages: 7 + 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" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 5 + oiio:subimages: 7 + 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" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 5 + oiio:subimages: 7 + 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" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 5 + oiio:subimages: 7 + 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" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 5 + oiio:subimages: 7 + 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" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 5 + oiio:subimages: 7 +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 + compression: "zstd:5" + textureformat: "Plain Texture" + 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 + textureformat: "Plain Texture" + ktx:astc_block_dimensions: "4x4" + ktx:codec: "astc" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 7 +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 + textureformat: "Plain Texture" + ktx:astc_block_dimensions: "8x8" + ktx:codec: "astc" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 11 +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 + compression: "zstd:5" + textureformat: "Plain Texture" + ktx:codec: "uastc-ldr-4x4" + 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 + compression: "zstd:10" + textureformat: "Plain Texture" + ktx:codec: "uastc-ldr-4x4" + ktx:normalmap: 1 + oiio:ColorSpace: "lin_rec709_scene" + oiio:miplevels: 11 +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 + compression: "zstd:5" + textureformat: "Plain Texture" + ktx:codec: "uastc-ldr-4x4" + ktx:uastc_rdo: 1 + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 11 +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 6144, 4 channel, uint8 ktx + MIP-map levels: 1024x6144 512x3072 256x1536 128x768 64x384 32x192 16x96 8x48 4x24 2x12 1x6 + SHA-1: 46F7D003AA78294995170727F8A8E154B5282347 + channel list: R, G, B, A + tile size: 1024 x 1024 + compression: "zstd:5" + textureformat: "CubeFace Environment" + ktx:codec: "uastc-ldr-4x4" + ktx:CubeMapSides: "+x -x +y -y +z -z" + ktx:uastc_rdo: 1 + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 11 +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 + compression: "zstd:15" + textureformat: "Plain Texture" + ktx:normalized: 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, 3 channel, half ktx + SHA-1: CEE57F5927C826E80B2DD7343708D2F2B6A396BA + channel list: R, G, B + compression: "zstd:15" + textureformat: "Plain Texture" + ktx:codec: "uastc-hdr-4x4" + ktx:normalized: 0 + ktx:uastc_hdr_ultra_quant: 1 + oiio:ColorSpace: "lin_rec709_scene" +Reading ../oiio-images/ktx2/Desk_uastc_hdr6x6i.ktx2 +../oiio-images/ktx2/Desk_uastc_hdr6x6i.ktx2 : 644 x 874, 3 channel, half ktx + SHA-1: D443FD9862C8E7A3890049BA4387846EA53DE7F9 + channel list: R, G, B + textureformat: "Plain Texture" + ktx:codec: "uastc-hdr-6x6" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Reading ../oiio-images/ktx2/Desk_astc_hdr6x6.ktx2 +../oiio-images/ktx2/Desk_astc_hdr6x6.ktx2 : 644 x 874, 4 channel, half ktx + SHA-1: 1930E94C0D33036B77F929E1C5E6E6AD5154974F + channel list: R, G, B, A + compression: "zstd" + textureformat: "Plain Texture" + ktx:astc_block_dimensions: "6x6" + ktx:codec: "astc" + ktx:normalized: 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:codec: "etc1s" + 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:codec: "etc1s" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 7 +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:codec: "etc1s" + 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:codec: "etc1s" + oiio:ColorSpace: "srgb_rec709_scene" +Reading ../oiio-images/ktx2/cubemap_yokohama_blze.ktx2 +../oiio-images/ktx2/cubemap_yokohama_blze.ktx2 : 2048 x 12288, 4 channel, uint8 ktx + MIP-map levels: 2048x12288 1024x6144 512x3072 256x1536 128x768 64x384 32x192 16x96 8x48 4x24 2x12 1x6 + SHA-1: 524C36457DDE6B0EEE54C4B52D4908280FB33DB5 + channel list: R, G, B, A + tile size: 2048 x 2048 + textureformat: "CubeFace Environment" + ktx:codec: "etc1s" + ktx:CubeMapSides: "+x -x +y -y +z -z" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 12 +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:codec: "etc1s" + 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:codec: "etc1s" + ktx:normalmap: 1 + oiio:ColorSpace: "lin_rec709_scene" + oiio:miplevels: 11 +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:codec: "etc1s" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 11 +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 + compression: "zstd:10" + textureformat: "Plain Texture" + 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" + 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 + Orientation: 4 (flipped vertically) + textureformat: "Plain Texture" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "checker_original.png" and "checker_default.ktx2" +PASS +Reading checker_default.ktx2 +checker_default.ktx2 : 64 x 64, 4 channel, uint8 ktx + SHA-1: 5ECD9AA14E69DB4B9DB20262CE412BBDF40F93C6 + channel list: R, G, B, A + textureformat: "Plain Texture" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "checker_original.png" and "checker_uastc.ktx2" +PASS +Reading checker_uastc.ktx2 +checker_uastc.ktx2 : 64 x 64, 4 channel, uint8 ktx + SHA-1: 5ECD9AA14E69DB4B9DB20262CE412BBDF40F93C6 + channel list: R, G, B, A + compression: "zstd" + textureformat: "Plain Texture" + ktx:codec: "uastc-ldr-4x4" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "checker_original.png" and "checker_etc1s.ktx2" +PASS +Reading checker_etc1s.ktx2 +checker_etc1s.ktx2 : 64 x 64, 4 channel, uint8 ktx + SHA-1: 5ECD9AA14E69DB4B9DB20262CE412BBDF40F93C6 + channel list: R, G, B, A + textureformat: "Plain Texture" + ktx:codec: "etc1s" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "checker_original.png" and "checker_astc_6x6.ktx2" +PASS +Reading checker_astc_6x6.ktx2 +checker_astc_6x6.ktx2 : 64 x 64, 4 channel, uint8 ktx + SHA-1: 5ECD9AA14E69DB4B9DB20262CE412BBDF40F93C6 + channel list: R, G, B, A + compression: "zip" + textureformat: "Plain Texture" + ktx:astc_block_dimensions: "6x6" + ktx:codec: "astc" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "../oiio-images/ktx2/orient_down_metadata.ktx2" and "orient_up_metadata.png" +PASS +Reading ../oiio-images/ktx2/valid_R8_SRGB_2D.ktx2 +../oiio-images/ktx2/valid_R8_SRGB_2D.ktx2 : 8 x 8, 1 channel, uint8 ktx + SHA-1: EF5707FBB1C3A95C3D3B34AF56AD936F4ED5356D + channel list: Y + textureformat: "Plain Texture" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8_SRGB_2D.ktx2" and "generated_valid_R8_SRGB_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R8_UINT_2D.ktx2 : 8 x 8, 1 channel, uint8 ktx + SHA-1: EF5707FBB1C3A95C3D3B34AF56AD936F4ED5356D + channel list: Y + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8_UINT_2D.ktx2" and "generated_valid_R8_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8_SINT_2D.ktx2 +../oiio-images/ktx2/valid_R8_SINT_2D.ktx2 : 8 x 8, 1 channel, int8 ktx + SHA-1: EF5707FBB1C3A95C3D3B34AF56AD936F4ED5356D + channel list: Y + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8_SINT_2D.ktx2" and "generated_valid_R8_SINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8_UNORM_2D.ktx2 +../oiio-images/ktx2/valid_R8_UNORM_2D.ktx2 : 8 x 8, 1 channel, uint8 ktx + SHA-1: EF5707FBB1C3A95C3D3B34AF56AD936F4ED5356D + channel list: Y + textureformat: "Plain Texture" + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8_UNORM_2D.ktx2" and "generated_valid_R8_UNORM_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8_SRGB_2D.ktx2 +../oiio-images/ktx2/valid_R8G8_SRGB_2D.ktx2 : 8 x 8, 2 channel, uint8 ktx + SHA-1: 58641E4148D293119BD2ACF92777B0CBCFEB18EB + channel list: R, G + textureformat: "Plain Texture" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8_SRGB_2D.ktx2" and "generated_valid_R8G8_SRGB_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R8G8_UINT_2D.ktx2 : 8 x 8, 2 channel, uint8 ktx + SHA-1: 58641E4148D293119BD2ACF92777B0CBCFEB18EB + channel list: R, G + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8_UINT_2D.ktx2" and "generated_valid_R8G8_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8_SINT_2D.ktx2 +../oiio-images/ktx2/valid_R8G8_SINT_2D.ktx2 : 8 x 8, 2 channel, int8 ktx + SHA-1: 58641E4148D293119BD2ACF92777B0CBCFEB18EB + channel list: R, G + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8_SINT_2D.ktx2" and "generated_valid_R8G8_SINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8_UNORM_2D.ktx2 +../oiio-images/ktx2/valid_R8G8_UNORM_2D.ktx2 : 8 x 8, 2 channel, uint8 ktx + SHA-1: 58641E4148D293119BD2ACF92777B0CBCFEB18EB + channel list: R, G + textureformat: "Plain Texture" + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8_UNORM_2D.ktx2" and "generated_valid_R8G8_UNORM_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8_SRGB_2D.ktx2 +../oiio-images/ktx2/valid_R8G8B8_SRGB_2D.ktx2 : 8 x 8, 3 channel, uint8 ktx + SHA-1: 65CB350B75461635FA13B5B7D810F0B0152322D2 + channel list: R, G, B + textureformat: "Plain Texture" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8_SRGB_2D.ktx2" and "generated_valid_R8G8B8_SRGB_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R8G8B8_UINT_2D.ktx2 : 8 x 8, 3 channel, uint8 ktx + SHA-1: 65CB350B75461635FA13B5B7D810F0B0152322D2 + channel list: R, G, B + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8_UINT_2D.ktx2" and "generated_valid_R8G8B8_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8_SINT_2D.ktx2 +../oiio-images/ktx2/valid_R8G8B8_SINT_2D.ktx2 : 8 x 8, 3 channel, int8 ktx + SHA-1: 65CB350B75461635FA13B5B7D810F0B0152322D2 + channel list: R, G, B + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8_SINT_2D.ktx2" and "generated_valid_R8G8B8_SINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8_UNORM_2D.ktx2 +../oiio-images/ktx2/valid_R8G8B8_UNORM_2D.ktx2 : 8 x 8, 3 channel, uint8 ktx + SHA-1: 65CB350B75461635FA13B5B7D810F0B0152322D2 + channel list: R, G, B + textureformat: "Plain Texture" + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8_UNORM_2D.ktx2" and "generated_valid_R8G8B8_UNORM_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8A8_SRGB_2D.ktx2 +../oiio-images/ktx2/valid_R8G8B8A8_SRGB_2D.ktx2 : 8 x 8, 4 channel, uint8 ktx + SHA-1: 55FD49E10D54C4CCDCD04E70FBCB6E8D71DE4EF6 + channel list: R, G, B, A + textureformat: "Plain Texture" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8A8_SRGB_2D.ktx2" and "generated_valid_R8G8B8A8_SRGB_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8A8_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R8G8B8A8_UINT_2D.ktx2 : 8 x 8, 4 channel, uint8 ktx + SHA-1: 55FD49E10D54C4CCDCD04E70FBCB6E8D71DE4EF6 + channel list: R, G, B, A + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8A8_UINT_2D.ktx2" and "generated_valid_R8G8B8A8_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8A8_SINT_2D.ktx2 +../oiio-images/ktx2/valid_R8G8B8A8_SINT_2D.ktx2 : 8 x 8, 4 channel, int8 ktx + SHA-1: 55FD49E10D54C4CCDCD04E70FBCB6E8D71DE4EF6 + channel list: R, G, B, A + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8A8_SINT_2D.ktx2" and "generated_valid_R8G8B8A8_SINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8A8_UNORM_2D.ktx2 +../oiio-images/ktx2/valid_R8G8B8A8_UNORM_2D.ktx2 : 8 x 8, 4 channel, uint8 ktx + SHA-1: 55FD49E10D54C4CCDCD04E70FBCB6E8D71DE4EF6 + channel list: R, G, B, A + textureformat: "Plain Texture" + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8A8_UNORM_2D.ktx2" and "generated_valid_R8G8B8A8_UNORM_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8A8_SRGB_CUBE.ktx2 +../oiio-images/ktx2/valid_R8G8B8A8_SRGB_CUBE.ktx2 : 8 x 48, 4 channel, uint8 ktx + SHA-1: 808B31CF8EE4751C14688719267AECCD6AC03D1F + channel list: R, G, B, A + tile size: 8 x 8 + textureformat: "CubeFace Environment" + ktx:CubeMapSides: "+x -x +y -y +z -z" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8A8_SRGB_CUBE.ktx2" and "generated_valid_R8G8B8A8_SRGB_CUBE.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8A8_SRGB_CUBE_MIP.ktx2 +../oiio-images/ktx2/valid_R8G8B8A8_SRGB_CUBE_MIP.ktx2 : 8 x 48, 4 channel, uint8 ktx + MIP-map levels: 8x48 4x24 2x12 1x6 + SHA-1: 808B31CF8EE4751C14688719267AECCD6AC03D1F + channel list: R, G, B, A + tile size: 8 x 8 + textureformat: "CubeFace Environment" + ktx:CubeMapSides: "+x -x +y -y +z -z" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 4 +Comparing "../oiio-images/ktx2/valid_R8G8B8A8_SRGB_CUBE_MIP.ktx2" and "generated_valid_R8G8B8A8_SRGB_CUBE_MIP.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8A8_SRGB_3D.ktx2 +../oiio-images/ktx2/valid_R8G8B8A8_SRGB_3D.ktx2 : 8 x 8 x 4, 4 channel, volume uint8 ktx + SHA-1: 73429FFCB6F1A23BD91878F088FFA632171A40B4 + channel list: R, G, B, A + tile size: 8 x 1 x 1 + textureformat: "Volume Texture" + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8A8_SRGB_3D.ktx2" and "generated_valid_R8G8B8A8_SRGB_3D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8A8_SRGB_3D_MIP.ktx2 +../oiio-images/ktx2/valid_R8G8B8A8_SRGB_3D_MIP.ktx2 : 8 x 8 x 4, 4 channel, volume uint8 ktx + MIP-map levels: 8x8 4x4 2x2 1x1 + SHA-1: 73429FFCB6F1A23BD91878F088FFA632171A40B4 + channel list: R, G, B, A + tile size: 8 x 1 x 1 + textureformat: "Volume Texture" + oiio:ColorSpace: "srgb_rec709_scene" + oiio:miplevels: 4 +Comparing "../oiio-images/ktx2/valid_R8G8B8A8_SRGB_3D_MIP.ktx2" and "generated_valid_R8G8B8A8_SRGB_3D_MIP.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16_SFLOAT_2D.ktx2 +../oiio-images/ktx2/valid_R16_SFLOAT_2D.ktx2 : 8 x 8, 1 channel, half ktx + SHA-1: 58641E4148D293119BD2ACF92777B0CBCFEB18EB + channel list: Y + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16_SFLOAT_2D.ktx2" and "generated_valid_R16_SFLOAT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R16_UINT_2D.ktx2 : 8 x 8, 1 channel, uint16 ktx + SHA-1: 58641E4148D293119BD2ACF92777B0CBCFEB18EB + channel list: Y + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16_UINT_2D.ktx2" and "generated_valid_R16_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16_SINT_2D.ktx2 +../oiio-images/ktx2/valid_R16_SINT_2D.ktx2 : 8 x 8, 1 channel, int16 ktx + SHA-1: 58641E4148D293119BD2ACF92777B0CBCFEB18EB + channel list: Y + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16_SINT_2D.ktx2" and "generated_valid_R16_SINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16_UNORM_2D.ktx2 +../oiio-images/ktx2/valid_R16_UNORM_2D.ktx2 : 8 x 8, 1 channel, uint16 ktx + SHA-1: 58641E4148D293119BD2ACF92777B0CBCFEB18EB + channel list: Y + textureformat: "Plain Texture" + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16_UNORM_2D.ktx2" and "generated_valid_R16_UNORM_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16_SFLOAT_2D.ktx2 +../oiio-images/ktx2/valid_R16G16_SFLOAT_2D.ktx2 : 8 x 8, 2 channel, half ktx + SHA-1: 55FD49E10D54C4CCDCD04E70FBCB6E8D71DE4EF6 + channel list: R, G + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16_SFLOAT_2D.ktx2" and "generated_valid_R16G16_SFLOAT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R16G16_UINT_2D.ktx2 : 8 x 8, 2 channel, uint16 ktx + SHA-1: 55FD49E10D54C4CCDCD04E70FBCB6E8D71DE4EF6 + channel list: R, G + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16_UINT_2D.ktx2" and "generated_valid_R16G16_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16_SINT_2D.ktx2 +../oiio-images/ktx2/valid_R16G16_SINT_2D.ktx2 : 8 x 8, 2 channel, int16 ktx + SHA-1: 55FD49E10D54C4CCDCD04E70FBCB6E8D71DE4EF6 + channel list: R, G + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16_SINT_2D.ktx2" and "generated_valid_R16G16_SINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16_UNORM_2D.ktx2 +../oiio-images/ktx2/valid_R16G16_UNORM_2D.ktx2 : 8 x 8, 2 channel, uint16 ktx + SHA-1: 55FD49E10D54C4CCDCD04E70FBCB6E8D71DE4EF6 + channel list: R, G + textureformat: "Plain Texture" + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16_UNORM_2D.ktx2" and "generated_valid_R16G16_UNORM_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16_SFLOAT_2D.ktx2 +../oiio-images/ktx2/valid_R16G16B16_SFLOAT_2D.ktx2 : 8 x 8, 3 channel, half ktx + SHA-1: BC3E3322547411D0D9C4C4542F23F32CD2040745 + channel list: R, G, B + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16B16_SFLOAT_2D.ktx2" and "generated_valid_R16G16B16_SFLOAT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R16G16B16_UINT_2D.ktx2 : 8 x 8, 3 channel, uint16 ktx + SHA-1: BC3E3322547411D0D9C4C4542F23F32CD2040745 + channel list: R, G, B + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16B16_UINT_2D.ktx2" and "generated_valid_R16G16B16_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16_SINT_2D.ktx2 +../oiio-images/ktx2/valid_R16G16B16_SINT_2D.ktx2 : 8 x 8, 3 channel, int16 ktx + SHA-1: BC3E3322547411D0D9C4C4542F23F32CD2040745 + channel list: R, G, B + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16B16_SINT_2D.ktx2" and "generated_valid_R16G16B16_SINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16_UNORM_2D.ktx2 +../oiio-images/ktx2/valid_R16G16B16_UNORM_2D.ktx2 : 8 x 8, 3 channel, uint16 ktx + SHA-1: BC3E3322547411D0D9C4C4542F23F32CD2040745 + channel list: R, G, B + textureformat: "Plain Texture" + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16B16_UNORM_2D.ktx2" and "generated_valid_R16G16B16_UNORM_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_2D.ktx2 +../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_2D.ktx2 : 8 x 8, 4 channel, half ktx + SHA-1: 3DBEB3CA9992329601D81225A7172F99D48C07A7 + channel list: R, G, B, A + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_2D.ktx2" and "generated_valid_R16G16B16A16_SFLOAT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16A16_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R16G16B16A16_UINT_2D.ktx2 : 8 x 8, 4 channel, uint16 ktx + SHA-1: 3DBEB3CA9992329601D81225A7172F99D48C07A7 + channel list: R, G, B, A + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16B16A16_UINT_2D.ktx2" and "generated_valid_R16G16B16A16_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16A16_SINT_2D.ktx2 +../oiio-images/ktx2/valid_R16G16B16A16_SINT_2D.ktx2 : 8 x 8, 4 channel, int16 ktx + SHA-1: 3DBEB3CA9992329601D81225A7172F99D48C07A7 + channel list: R, G, B, A + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16B16A16_SINT_2D.ktx2" and "generated_valid_R16G16B16A16_SINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16A16_UNORM_2D.ktx2 +../oiio-images/ktx2/valid_R16G16B16A16_UNORM_2D.ktx2 : 8 x 8, 4 channel, uint16 ktx + SHA-1: 3DBEB3CA9992329601D81225A7172F99D48C07A7 + channel list: R, G, B, A + textureformat: "Plain Texture" + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16B16A16_UNORM_2D.ktx2" and "generated_valid_R16G16B16A16_UNORM_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_CUBE.ktx2 +../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_CUBE.ktx2 : 8 x 48, 4 channel, half ktx + SHA-1: A5C5C36902B91E598E78C9A08638278D89999F19 + channel list: R, G, B, A + tile size: 8 x 8 + textureformat: "CubeFace Environment" + ktx:CubeMapSides: "+x -x +y -y +z -z" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_CUBE.ktx2" and "generated_valid_R16G16B16A16_SFLOAT_CUBE.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_CUBE_MIP.ktx2 +../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_CUBE_MIP.ktx2 : 8 x 48, 4 channel, half ktx + MIP-map levels: 8x48 4x24 2x12 1x6 + SHA-1: A5C5C36902B91E598E78C9A08638278D89999F19 + channel list: R, G, B, A + tile size: 8 x 8 + textureformat: "CubeFace Environment" + ktx:CubeMapSides: "+x -x +y -y +z -z" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" + oiio:miplevels: 4 +Comparing "../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_CUBE_MIP.ktx2" and "generated_valid_R16G16B16A16_SFLOAT_CUBE_MIP.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_3D.ktx2 +../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_3D.ktx2 : 8 x 8 x 4, 4 channel, volume half ktx + SHA-1: B38CCD97C685C18A6E2256735C923868928EB5A6 + channel list: R, G, B, A + tile size: 8 x 1 x 1 + textureformat: "Volume Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_3D.ktx2" and "generated_valid_R16G16B16A16_SFLOAT_3D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_3D_MIP.ktx2 +../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_3D_MIP.ktx2 : 8 x 8 x 4, 4 channel, volume half ktx + MIP-map levels: 8x8 4x4 2x2 1x1 + SHA-1: B38CCD97C685C18A6E2256735C923868928EB5A6 + channel list: R, G, B, A + tile size: 8 x 1 x 1 + textureformat: "Volume Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" + oiio:miplevels: 4 +Comparing "../oiio-images/ktx2/valid_R16G16B16A16_SFLOAT_3D_MIP.ktx2" and "generated_valid_R16G16B16A16_SFLOAT_3D_MIP.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32_SFLOAT_2D.ktx2 +../oiio-images/ktx2/valid_R32_SFLOAT_2D.ktx2 : 8 x 8, 1 channel, float ktx + SHA-1: 55FD49E10D54C4CCDCD04E70FBCB6E8D71DE4EF6 + channel list: Y + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R32_SFLOAT_2D.ktx2" and "generated_valid_R32_SFLOAT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R32_UINT_2D.ktx2 : 8 x 8, 1 channel, uint ktx + SHA-1: 55FD49E10D54C4CCDCD04E70FBCB6E8D71DE4EF6 + channel list: Y + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R32_UINT_2D.ktx2" and "generated_valid_R32_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32G32_SFLOAT_2D.ktx2 +../oiio-images/ktx2/valid_R32G32_SFLOAT_2D.ktx2 : 8 x 8, 2 channel, float ktx + SHA-1: 3DBEB3CA9992329601D81225A7172F99D48C07A7 + channel list: R, G + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R32G32_SFLOAT_2D.ktx2" and "generated_valid_R32G32_SFLOAT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32G32_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R32G32_UINT_2D.ktx2 : 8 x 8, 2 channel, uint ktx + SHA-1: 3DBEB3CA9992329601D81225A7172F99D48C07A7 + channel list: R, G + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R32G32_UINT_2D.ktx2" and "generated_valid_R32G32_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32G32B32_SFLOAT_2D.ktx2 +../oiio-images/ktx2/valid_R32G32B32_SFLOAT_2D.ktx2 : 8 x 8, 3 channel, float ktx + SHA-1: 98C222061C4FD5FDE9E59E82680B9748B9E61E8A + channel list: R, G, B + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R32G32B32_SFLOAT_2D.ktx2" and "generated_valid_R32G32B32_SFLOAT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32G32B32_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R32G32B32_UINT_2D.ktx2 : 8 x 8, 3 channel, uint ktx + SHA-1: 98C222061C4FD5FDE9E59E82680B9748B9E61E8A + channel list: R, G, B + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R32G32B32_UINT_2D.ktx2" and "generated_valid_R32G32B32_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_2D.ktx2 +../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_2D.ktx2 : 8 x 8, 4 channel, float ktx + SHA-1: A8A344301EFC4E8B2D2BF30FAC343888CFCAB26B + channel list: R, G, B, A + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_2D.ktx2" and "generated_valid_R32G32B32A32_SFLOAT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32G32B32A32_UINT_2D.ktx2 +../oiio-images/ktx2/valid_R32G32B32A32_UINT_2D.ktx2 : 8 x 8, 4 channel, uint ktx + SHA-1: A8A344301EFC4E8B2D2BF30FAC343888CFCAB26B + channel list: R, G, B, A + textureformat: "Plain Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R32G32B32A32_UINT_2D.ktx2" and "generated_valid_R32G32B32A32_UINT_2D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_CUBE.ktx2 +../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_CUBE.ktx2 : 8 x 48, 4 channel, float ktx + SHA-1: 1EADD5444326DDABF48CD58F3D0F5489CAC5C419 + channel list: R, G, B, A + tile size: 8 x 8 + textureformat: "CubeFace Environment" + ktx:CubeMapSides: "+x -x +y -y +z -z" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_CUBE.ktx2" and "generated_valid_R32G32B32A32_SFLOAT_CUBE.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_CUBE_MIP.ktx2 +../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_CUBE_MIP.ktx2 : 8 x 48, 4 channel, float ktx + MIP-map levels: 8x48 4x24 2x12 1x6 + SHA-1: 1EADD5444326DDABF48CD58F3D0F5489CAC5C419 + channel list: R, G, B, A + tile size: 8 x 8 + textureformat: "CubeFace Environment" + ktx:CubeMapSides: "+x -x +y -y +z -z" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" + oiio:miplevels: 4 +Comparing "../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_CUBE_MIP.ktx2" and "generated_valid_R32G32B32A32_SFLOAT_CUBE_MIP.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_3D.ktx2 +../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_3D.ktx2 : 8 x 8 x 4, 4 channel, volume float ktx + SHA-1: B046AC2B4302C89E178E30CA56A25854E811E69B + channel list: R, G, B, A + tile size: 8 x 1 x 1 + textureformat: "Volume Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_3D.ktx2" and "generated_valid_R32G32B32A32_SFLOAT_3D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_3D_MIP.ktx2 +../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_3D_MIP.ktx2 : 8 x 8 x 4, 4 channel, volume float ktx + MIP-map levels: 8x8 4x4 2x2 1x1 + SHA-1: B046AC2B4302C89E178E30CA56A25854E811E69B + channel list: R, G, B, A + tile size: 8 x 1 x 1 + textureformat: "Volume Texture" + ktx:normalized: 0 + oiio:ColorSpace: "lin_rec709_scene" + oiio:miplevels: 4 +Comparing "../oiio-images/ktx2/valid_R32G32B32A32_SFLOAT_3D_MIP.ktx2" and "generated_valid_R32G32B32A32_SFLOAT_3D_MIP.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8A8_SRGB_1D.ktx2 +../oiio-images/ktx2/valid_R8G8B8A8_SRGB_1D.ktx2 : 8 x 1, 4 channel, uint8 ktx + SHA-1: 1BE13675DB2B34763204FE7CC3C29D4885753916 + channel list: R, G, B, A + textureformat: "Plain Texture" + ktx:1d: 1 + oiio:ColorSpace: "srgb_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8A8_SRGB_1D.ktx2" and "generated_valid_R8G8B8A8_SRGB_1D.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZLIB_1.ktx2 +../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZLIB_1.ktx2 : 8 x 8, 4 channel, uint8 ktx + SHA-1: 4951AEF135340B1CAC5EED7F519225DAEA6899B2 + channel list: R, G, B, A + compression: "zip:1" + textureformat: "Plain Texture" + ktx:codec: "uastc-ldr-4x4" + ktx:uastc_flags: 0 + ktx:uastc_hdr_quality: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZLIB_1.ktx2" and "generated_valid_R8G8B8_UNORM_2D_UASTC_ZLIB_1.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZLIB_9.ktx2 +../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZLIB_9.ktx2 : 8 x 8, 4 channel, uint8 ktx + SHA-1: 4951AEF135340B1CAC5EED7F519225DAEA6899B2 + channel list: R, G, B, A + compression: "zip:9" + textureformat: "Plain Texture" + ktx:codec: "uastc-ldr-4x4" + ktx:uastc_flags: 0 + ktx:uastc_hdr_quality: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZLIB_9.ktx2" and "generated_valid_R8G8B8_UNORM_2D_UASTC_ZLIB_9.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZSTD_1.ktx2 +../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZSTD_1.ktx2 : 8 x 8, 4 channel, uint8 ktx + SHA-1: 4951AEF135340B1CAC5EED7F519225DAEA6899B2 + channel list: R, G, B, A + compression: "zstd:1" + textureformat: "Plain Texture" + ktx:codec: "uastc-ldr-4x4" + ktx:uastc_flags: 0 + ktx:uastc_hdr_quality: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZSTD_1.ktx2" and "generated_valid_R8G8B8_UNORM_2D_UASTC_ZSTD_1.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZSTD_9.ktx2 +../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZSTD_9.ktx2 : 8 x 8, 4 channel, uint8 ktx + SHA-1: 4951AEF135340B1CAC5EED7F519225DAEA6899B2 + channel list: R, G, B, A + compression: "zstd:9" + textureformat: "Plain Texture" + ktx:codec: "uastc-ldr-4x4" + ktx:uastc_flags: 0 + ktx:uastc_hdr_quality: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_UASTC_ZSTD_9.ktx2" and "generated_valid_R8G8B8_UNORM_2D_UASTC_ZSTD_9.ktx2" +PASS +Reading ../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_BLZE.ktx2 +../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_BLZE.ktx2 : 8 x 8, 4 channel, uint8 ktx + SHA-1: 22799EAF1D248CD3BB4700B55548B26FE77C3A07 + channel list: R, G, B, A + textureformat: "Plain Texture" + ktx:codec: "etc1s" + ktx:etc1s_quality_level: 0 + oiio:ColorSpace: "lin_rec709_scene" +Comparing "../oiio-images/ktx2/valid_R8G8B8_UNORM_2D_BLZE.ktx2" and "generated_valid_R8G8B8_UNORM_2D_BLZE.ktx2" +PASS diff --git a/testsuite/ktx/run.py b/testsuite/ktx/run.py new file mode 100644 index 0000000000..567d3cde20 --- /dev/null +++ b/testsuite/ktx/run.py @@ -0,0 +1,221 @@ +#!/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 hashes: +# - 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. +# +# Testing via C++ OpenImageIO API is also done separately in this repo: +# https://github.com/walcht/ktx-oiio-examples +# + +# 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", +] + +# Most basic testing; just test `oiiotool --info` on libktx main test files +for f in 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") +command += info_command ("checker_default.ktx2") + +# UASTC write test: check generation of an UASTC-based KTX2 file +command += oiiotool ("checker_original.png --attrib ktx:codec uastc " + + "--compression zstd --attrib ktx:uastc_rdo 1 -o checker_uastc.ktx2") +command += diff_command ("checker_original.png", "checker_uastc.ktx2") +command += info_command ("checker_uastc.ktx2") + +# ETC1S write test: ETC1S-based + ZSTD lvl 20 KTX2 file +command += oiiotool ("checker_original.png --attrib ktx:codec etc1s " + + "--compression zstd:20 -o checker_etc1s.ktx2") +command += diff_command ("checker_original.png", "checker_etc1s.ktx2") +command += info_command ("checker_etc1s.ktx2") + +# ASTC 6x6 block write test:ASTC-block-compressed + ZLIB lvl 5 KTX2 file +command += oiiotool ("checker_original.png --attrib ktx:codec astc " + + "--attrib ktx:astc_block_dimensions 6x6 " + + "--compression zip:5 -o checker_astc_6x6.ktx2") +command += diff_command ("checker_original.png", "checker_astc_6x6.ktx2") +command += info_command ("checker_astc_6x6.ktx2") + +# Test that re-orientation metadata is handled correctly. +# orient_[up|down]_metadata are essentially the same image with one flipped +# vertically (orient_up_metadata) +command += (oiio_app("oiiotool") + (OIIO_TESTSUITE_IMAGEDIR + "/" + "orient_up_metadata.ktx2") + + " --reorient -o orient_up_metadata.png >> out.txt ;\n") +command += diff_command (OIIO_TESTSUITE_IMAGEDIR + "/" + "orient_down_metadata.ktx2", "orient_up_metadata.png") + +# Roundtrip tests for small compressed/uncompressed files (8x8) of supported Vulkan formats. +vkformat_files = [ + # 8-bit formats + "valid_R8_SRGB_2D.ktx2", + "valid_R8_UINT_2D.ktx2", + "valid_R8_SINT_2D.ktx2", + "valid_R8_UNORM_2D.ktx2", + "valid_R8G8_SRGB_2D.ktx2", + "valid_R8G8_UINT_2D.ktx2", + "valid_R8G8_SINT_2D.ktx2", + "valid_R8G8_UNORM_2D.ktx2", + "valid_R8G8B8_SRGB_2D.ktx2", + "valid_R8G8B8_UINT_2D.ktx2", + "valid_R8G8B8_SINT_2D.ktx2", + "valid_R8G8B8_UNORM_2D.ktx2", + "valid_R8G8B8A8_SRGB_2D.ktx2", + "valid_R8G8B8A8_UINT_2D.ktx2", + "valid_R8G8B8A8_SINT_2D.ktx2", + "valid_R8G8B8A8_UNORM_2D.ktx2", + "valid_R8G8B8A8_SRGB_CUBE.ktx2", + "valid_R8G8B8A8_SRGB_CUBE_MIP.ktx2", + # "valid_R8G8B8A8_SRGB_CUBE_ARRAY.ktx2", + # "valid_R8G8B8A8_SRGB_CUBE_ARRAY_MIP.ktx2", + "valid_R8G8B8A8_SRGB_3D.ktx2", + "valid_R8G8B8A8_SRGB_3D_MIP.ktx2", + + # 16-bit formats + "valid_R16_SFLOAT_2D.ktx2", + "valid_R16_UINT_2D.ktx2", + "valid_R16_SINT_2D.ktx2", + "valid_R16_UNORM_2D.ktx2", + "valid_R16G16_SFLOAT_2D.ktx2", + "valid_R16G16_UINT_2D.ktx2", + "valid_R16G16_SINT_2D.ktx2", + "valid_R16G16_UNORM_2D.ktx2", + "valid_R16G16B16_SFLOAT_2D.ktx2", + "valid_R16G16B16_UINT_2D.ktx2", + "valid_R16G16B16_SINT_2D.ktx2", + "valid_R16G16B16_UNORM_2D.ktx2", + "valid_R16G16B16A16_SFLOAT_2D.ktx2", + "valid_R16G16B16A16_UINT_2D.ktx2", + "valid_R16G16B16A16_SINT_2D.ktx2", + "valid_R16G16B16A16_UNORM_2D.ktx2", + "valid_R16G16B16A16_SFLOAT_CUBE.ktx2", + "valid_R16G16B16A16_SFLOAT_CUBE_MIP.ktx2", + "valid_R16G16B16A16_SFLOAT_3D.ktx2", + "valid_R16G16B16A16_SFLOAT_3D_MIP.ktx2", + + # 32-bit formats + "valid_R32_SFLOAT_2D.ktx2", + "valid_R32_UINT_2D.ktx2", + "valid_R32G32_SFLOAT_2D.ktx2", + "valid_R32G32_UINT_2D.ktx2", + "valid_R32G32B32_SFLOAT_2D.ktx2", + "valid_R32G32B32_UINT_2D.ktx2", + "valid_R32G32B32A32_SFLOAT_2D.ktx2", + "valid_R32G32B32A32_UINT_2D.ktx2", + "valid_R32G32B32A32_SFLOAT_CUBE.ktx2", + "valid_R32G32B32A32_SFLOAT_CUBE_MIP.ktx2", + "valid_R32G32B32A32_SFLOAT_3D.ktx2", + "valid_R32G32B32A32_SFLOAT_3D_MIP.ktx2", + + # 1D textures (these are rarely used) + "valid_R8G8B8A8_SRGB_1D.ktx2", + + # UASTC + ZSTD/ZLIB levels (test super-compression level roundtrip) + "valid_R8G8B8_UNORM_2D_UASTC_ZLIB_1.ktx2", + "valid_R8G8B8_UNORM_2D_UASTC_ZLIB_9.ktx2", + "valid_R8G8B8_UNORM_2D_UASTC_ZSTD_1.ktx2", + "valid_R8G8B8_UNORM_2D_UASTC_ZSTD_9.ktx2", + + # ETC1S/BasisLZ roundtrip + "valid_R8G8B8_UNORM_2D_BLZE.ktx2", +] + +for filename in vkformat_files: + fp = OIIO_TESTSUITE_IMAGEDIR + "/" + filename + regenerated_filename = f"generated_{filename}"; + command += info_command (fp) + # Use `--native` to prevent oiiotool from converting input types to float. + # Without this, the exact same bytes may not be reproduced for, for + # instance, UINT32 types (which in turn results in the ktxdiff command below + # to fail). + command += oiiotool (f"{fp} --native -o {regenerated_filename}") + command += diff_command (fp, regenerated_filename) + + # For more robust testing; if `ktxdiff` is installed (part of KTX Tools) + # then you can compare the generated vs. original KTX2 files (headers and + # texture data) to verify that the VK_FORMAT is the same, primaries and tf + # are the same, etc. If you see any diff in out.txt then something failed. + # ktxdiff_path = "" + # command += f"{ktxdiff_path} {fp} {regenerated_filename} --tolerance 0 --skip-kvd >> out.txt 2>&1;\n"