Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/cmake/build_Ktx.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO

set_cache (Ktx_BUILD_VERSION v5.0.0-rc1 "Ktx version for local builds")
set (Ktx_GIT_REPOSITORY "https://github.com/KhronosGroup/KTX-Software.git")
set (Ktx_GIT_TAG "${Ktx_BUILD_VERSION}")
set (Ktx_GIT_COMMIT "6269d2752ed04446c2d4749f54f3aad4f94555b5")
set_cache (Ktx_BUILD_SHARED_LIBS OFF ${LOCAL_BUILD_SHARED_LIBS_DEFAULT}
DOC "Should a local Ktx build, if necessary, build shared libraries" ADVANCED)

string (MAKE_C_IDENTIFIER ${Ktx_BUILD_VERSION} Ktx_VERSION_IDENT)

# for detailed build instructions, see:
# https://github.com/KhronosGroup/KTX-Software/blob/main/BUILDING.md
# KTX-Software not only provides Ktx but also a set of cli tools and load
# test applications that we do not need.
build_dependency_with_cmake(Ktx
VERSION ${Ktx_BUILD_VERSION}
GIT_REPOSITORY ${Ktx_GIT_REPOSITORY}
GIT_TAG ${Ktx_GIT_TAG}
GIT_COMMIT ${Ktx_GIT_COMMIT}
# lib only contains CMakeLists.txt from tag v5.0.0 but that requires CMake min version 3.23
# which in turn causes the CI to fail. Just give up and build the whole thing...
SOURCE_SUBDIR lib # To only build Ktx, cmake has to point to: KTX-Software/lib
CMAKE_ARGS
-D BUILD_SHARED_LIBS=${Ktx_BUILD_SHARED_LIBS}
-D CMAKE_INSTALL_LIBDIR=lib
-D CMAKE_POSITION_INDEPENDENT_CODE=ON
-D LIBKTX_VERSION_READ_ONLY=OFF
-D LIBKTX_VERSION_FULL=ON
-D LIBKTX_FEATURE_KTX1=ON # Setting this to OFF causes linker issues
-D LIBKTX_FEATURE_KTX2=ON
-D LIBKTX_FEATURE_VK_UPLOAD=OFF
-D LIBKTX_FEATURE_GL_UPLOAD=OFF
-D LIBKTX_FEATURE_ETC_UNPACK=OFF # This has some weird licensing and I don't feel comfortable including it ...
# as per KTX-Software:
# > Intel Macs have support for SSE, but if you're building universal
# > binaries, you have to disable SSE or the build will fail.
)

# Set some things up that we'll need for a subsequent find_package to work
set (Ktx_ROOT ${Ktx_LOCAL_INSTALL_DIR})
set (Ktx_DIR ${Ktx_LOCAL_INSTALL_DIR}/lib/cmake/ktx)

# Signal to caller that we need to find again at the installed location
# set (Ktx_REFIND TRUE)
# set (Ktx_REFIND_ARGS CONFIG)

find_package (Ktx CONFIG REQUIRED
HINTS
${Ktx_LOCAL_INSTALL_DIR}/lib/cmake/ktx/
${Ktx_LOCAL_INSTALL_DIR}
)

if (Ktx_BUILD_SHARED_LIBS)
# install_local_dependency_libs (pkgname libname)
install_local_dependency_libs (Ktx ktx) # notice libname is lowercase
endif ()
5 changes: 5 additions & 0 deletions src/cmake/externalpackages.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ else ()
get_target_property(FMT_INCLUDE_DIR fmt::fmt-header-only INTERFACE_INCLUDE_DIRECTORIES)
endif ()

# Ktx for KTX textures
checked_find_package (Ktx
VERSION_MIN 5.0.0
BUILD_LOCAL missing
)

###########################################################################

Expand Down
6 changes: 5 additions & 1 deletion src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,9 @@ macro (oiio_add_all_tests)
oiio_add_tests (dpx
ENABLEVAR ENABLE_DPX
IMAGEDIR oiio-images/dpx URL "Recent checkout of OpenImageIO-images")
oiio_add_tests (ktx
ENABLEVAR ENABLE_KTX
IMAGEDIR oiio-images/ktx2)
oiio_add_tests (dds
ENABLEVAR ENABLE_DDS
IMAGEDIR oiio-images/dds URL "Recent checkout of OpenImageIO-images")
Expand Down Expand Up @@ -539,9 +542,10 @@ function (oiio_get_test_data name)
endfunction()

function (oiio_setup_test_data)
# TODO: revert this after accepting OpenImageIO-images PR and before merging (just so that the CI passes)
oiio_get_test_data (oiio-images
REPO https://github.com/AcademySoftwareFoundation/OpenImageIO-images.git
BRANCH dev-${OpenImageIO_VERSION_MAJOR}.${OpenImageIO_VERSION_MINOR})
BRANCH main)
oiio_get_test_data (openexr-images
REPO https://github.com/AcademySoftwareFoundation/openexr-images.git
BRANCH main)
Expand Down
9 changes: 9 additions & 0 deletions src/ktx.imageio/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO

if (Ktx_FOUND)
add_oiio_plugin (ktxinput.cpp ktxoutput.cpp LINK_LIBRARIES KTX::ktx)
else ()
message (WARNING "KTX plugin will not be built, no libktx")
endif ()
207 changes: 207 additions & 0 deletions src/ktx.imageio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# About

This KTX plugin support obviously nullifies the benefits of using KTX in the
first place (i.e., to reduce upload time to GPUs or totally eliminate the need for
transcoding to GPU-conformant format before uploading). That being said, this
plugin is still useful so that end users don't have to convert back and forth
between KTX <-> supported format (e.g., PNG). It is also useful to convert to
and from KTX2 format.

An important note about DDS -> KTX conversion:

- [KTX-Software][libktx] will provide tools for lossless DDS to KTX conversion
without having to decode then encode to KTX format. A PR is currently being
worked on.
- If you use OIIO for this conversion, then the quality will almost certainly
degrade.

An example use-case would be Blender and its glTf import/export plugin.

Ideally, at some point in the future, OIIO may introduce a new API to
accommodate texture formats that are mainly used for fast texture uploads tow
GPUs. This is outside the scope of this basic format support addition.

Below you will find a set of notes on why this plugin is implemented the way
it is. It took me some time to understand how libktx works and what it provides
(and why). Some terminology is also defined here.

## KTX2 - Brief Introduction

KTX2 (the 2 here is to distinguish it from deprecated KTX/KTX1) is a binary
container format that is intended for usage for fast loading of textures to the
GPU. KTX2 contains GPU-native formats (e.g., block-compressed format BC7) with
an optional additional layer of compression (hereafter referred to as
*supercompression*).

As per the specs, KTX2 formats may store downsampled texture data for each mip
level (not necessarily the whole pyramid). This introduces problems for the
KTX2 writer (at `ktxoutput.cpp`) because the spec doesn't force the mention of
which filter/downsampler was used to create the mip levels.

## GPU Block Compression Formats

As opposed to compressed images, the term *compressed textures* usually refers
to GPU block-compressed textures. Compressed textures have the following
requirements:

- Random access (to some degree, you still pay the price for decoding a very
small number of neighboring pixels to access a given pixel).
- Fixed-rate encoding (requirement for random access)
- Support for hardware-decoding on the GPU (i.e., extremely fast to decode
and results in better performance due to lower cache usage).

### BCn

All BCn formats encode a 4x4 block of pixels (could be 1 channel, or 2, or 3, or
4 depending on the particular format) into a fixed-size data (i.e., no
variable-rate encoding). BC7 is the go-to format on desktop hardware for LDR
textures. BC6HU/BC6HS is the go-to format on desktop hardware for HDR formats.

Microsoft has some fairly well-written explanation of each format. From the
perspective of OIIO, we just don't care since the work to decode/encode these
formats is offloaded to libktx.

### ASTC

libktx provides ASTC encoders/decoders and we don't have to deal with ASTC's
extreme complexity (e.g., there are many different block sizes).

### ETC2

libktx provides ETC2 encoders/decoders (have to double verify) but the
dependency has some weird licensing (afraid non-permissive as Mark @KTX-Software
pointed out). ETC formats are therefore not supported.

## KTX Supercompression

**supercompression**: a compression on top of another compression (i.e., layered
compression) for better disk storage/network transmission. Unlike GPU block
compression, supercompression has the flexibility to employ variable-rate
encoding. In this context, supercompression is employed on top of fixed-rate,
endpoint-compressed formats (like BCn, ASTC, etc.) that have hardware-decoding
support in commodity GPUs (of course, depends on GPU - mobile vs desktop, etc.).

Depending on the used Basis Universal codec (if any), supercompression may be
applied. **For ETC1S, supercompression must be used (usually BasisLZ)**. This is
the reason why you constantly see the notation "BasisLZ/ETC1S" which
*probably (have to verify)* reads: *BasisLZ over ETC1S*.

For UASTC, we *may* apply Zstandard supercompression (i.e., `KTX_SS_ZSTD`).

### KTS\_SS\_BASIS\_LZ (BasisLZ)

This is intended to be used to super-compress Basis Universal ETC1S format.
The expected workflow is as follows:

```
Basis LZ -> transcode to GPU format (e.g., block-compressed BC7)
```

For OIIO use-case, we can directly use libktx to transcode into raw bytes:

```
Basis LZ → transcode (using ktxTexture2_TranscodeBasis) → raw RGBA
```

The `ktxTexture2_TranscodeBasis` function provided by libktx can transcode
directly into raw RGBA values which is very handy. It however doesn't
provide/expose the functionality to just decode a single miplevel/subimage
(maybe this is simply not doable with Basis LZ - have to verify). Either way,
I might open a PR to provide single image/texture decoders.

## Supported Encoders/Decoders

- Supported/Tested texture kinds:
- [ ] `SINGLE_TEXTURE_1D` (TODO)
- [X] `SINGLE_TEXTURE_2D`
- [ ] `SINGLE_TEXTURE_3D` (TODO)
- [ ] `CUBEMAP_TEXTURE` (TODO)
- [ ] `ARRAY_TEXTURE_1D` (TODO)
- [ ] `ARRAY_TEXTURE_2D` (TODO)
- [ ] `ARRAY_TEXTURE_3D` (not planned)
- [ ] `ARRAY_TEXTURE_CUBEMAP` (not planned)

- Supported/Tested raw VkFormats (decoder + encoder):
- [X] `VK_FORMAT_R8_UNORM`
- [X] `VK_FORMAT_R8G8_SRGB`
- [X] `VK_FORMAT_R8G8B8_SRGB`
- [X] `VK_FORMAT_R8G8B8A8_SRGB`

- Block-compressed formats (decoder + encoder):
- [X] ASTC
- [ ] BCn (waiting on libktx BCn support PR merge)

- Basis Universal schemes (encoder + decoder):
- [X] `UASTC`
- [X] `ETC1S`

- Supercompression schemes (decompressor + compressor):
- [X] `ZLIB`
- [X] `ZSTD`

## Limitations

- If original KTX2 format contained generated mip maps, there is simply no way
to know which filter and its parameters that were used to regenerate these
mipmaps. To avoid any issues, we simply early quit (return false) in `open()`
if `get_int_attribute("ktx:miplevels") > 1`.

- KTX2 supports many GPU-block-compression encoders and each one may have many
different parameters that change the encoding quality (as usual, quality-speed
trade-off). To regenerate same input KTX2 format, we rely on the heuristic that
whatever created the original KTX2 input also supplied `KTXwriterScParams`
metadata field which should provide all non-default arguments provided to
`ktx create/encode` to create the texture.

- As stated in the comments in `ktxinput.cpp`, if given ktx texture is
supercompressed then it has to be all decompressed (i.e., NOT the decompression
of the underlying GPU texture format but rather just the supercompression). This
means that if you just need a particular subimage/miplevel, you pay the memory
price of loading the whole KTX texture (which might be very large for 3D
textures and texture arrays).

- Per the specs:
> Discussion: Should each mip level be supercompressed independently or should
> the scheme, zlib, zstd, etc., be applied to all levels as a unit? The latter
> may result in slightly smaller size though that is unclear. However it would
> also mean levels could not be streamed or randomly accessed.
>
> Resolved: Yes. The benefits of streaming and random access outweigh what is
> expected to be a small increase in size.

- KTX2 writer writes the whole texture (i.e., all subimages/mipmaps) in the
`close()` function (i.e., when the ImageOutput object is destroyed or requested
to close). libktx does not provide a way to append or write subimages (is this
problematic or contrary to the way OIIO expects us to write files?).

- <s>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).</s> => support is not
planned for the moment.

- Only LDR formats (to be more precise, only TypeDesc::UINT8). Adding support
for HDR is straightforward (conversions for large number of enum values from
VkFormat have to be written).

## Dependencies

We only depend on libktx and nothing else. If CPU decoding/encoding of a format
is not supported by libktx, open a PR there that adds support to it. I tried the
approach of implementing formats here (e.g., BCn) and this results in extremely
harder to maintain and much more complex code here (see first commit with
12 000 changed lines).

[libktx][libktx]: for general KTX@ format support (loading of KTX2 files,
transcoding support, supercompression decompression support, etc.).

- Commit hash: see `OpenImageIO/src/cmake/build_Ktx.cmake`
- License: Many subresources.

## Resources

- [KTX2 Specs](https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html)
- [Official Implementation (KTX-Software)](https://github.com/KhronosGroup/KTX-Software)
- [Basis Universal Supercompression Implementation (used by libktx)](https://github.com/BinomialLLC/basis_universal)
- [Comparing-BCn-texture-decoders](https://aras-p.info/blog/2022/06/23/Comparing-BCn-texture-decoders/)

[libktx]: https://github.com/KhronosGroup/KTX-Software.git
Loading