262 lines
11 KiB
Plaintext
262 lines
11 KiB
Plaintext
////
|
|
- Copyright (c) 2021-2025, Sascha Willems
|
|
-
|
|
- SPDX-License-Identifier: Apache-2.0
|
|
-
|
|
- Licensed under the Apache License, Version 2.0 the "License";
|
|
- you may not use this file except in compliance with the License.
|
|
- You may obtain a copy of the License at
|
|
-
|
|
- http://www.apache.org/licenses/LICENSE-2.0
|
|
-
|
|
- Unless required by applicable law or agreed to in writing, software
|
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
- See the License for the specific language governing permissions and
|
|
- limitations under the License.
|
|
-
|
|
////
|
|
= Using Basis Universal supercompressed GPU texture codec with Vulkan
|
|
|
|
ifdef::site-gen-antora[]
|
|
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/performance/texture_compression_basisu[Khronos Vulkan samples github repository].
|
|
endif::[]
|
|
|
|
|
|
== Overview
|
|
|
|
This tutorial, along with the accompanying example code, demonstrates how to use Basis universal supercompressed GPU textures in a Vulkan application.
|
|
|
|
== The KTX2 format
|
|
|
|
https://www.khronos.org/ktx/[KTX] is a GPU texture *container format* for storing different texture types (2D, cubemap, etc.) and texture formats (uncompressed and compressed).
|
|
Version 2.0 added support for Basis Universal supercompressed textures.
|
|
|
|
== Basis Universal
|
|
|
|
https://github.com/BinomialLLC/basis_universal[Basis Universal] is a supercompressed GPU texture data interchange system that implements the UASTC and ETC1S compressed formats that serve as *transmission formats*.
|
|
Both can be quickly transcoded to a wide variety of GPU native compressed and uncompressed formats like RGB/RGBA, BCn, ETC1, ETC2, etc.
|
|
This means that unlike a KTX 2.0 file storing a BC3 textures, the data needs to be transcoded at runtime.
|
|
|
|
image::./images/2021-ktx-universal-gpu-compressed-textures.png[KTX and BasisU]
|
|
|
|
== Libraries for loading KTX 2.0 files
|
|
|
|
This sample (as well as the repository) uses the _libktx_ library from the https://github.com/KhronosGroup/KTX-Software[official Khronos KTX Software Repository] for loading and transcoding the Basis Universal compressed KTX 2.0 textures.
|
|
It's included in the framework via this link:../../../third_party/CMakeLists.txt[CMakeLists.txt] and also adds the Basis Universal transcoder:
|
|
|
|
[,CMake]
|
|
----
|
|
# libktx
|
|
set(KTX_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ktx)
|
|
|
|
set(KTX_SOURCES
|
|
...
|
|
|
|
# Basis Universal
|
|
${KTX_DIR}/lib/basis_sgd.h
|
|
${KTX_DIR}/lib/basis_transcode.cpp
|
|
${KTX_DIR}/lib/basisu/transcoder/basisu_containers.h
|
|
${KTX_DIR}/lib/basisu/transcoder/basisu_containers_impl.h
|
|
${KTX_DIR}/lib/basisu/transcoder/basisu_file_headers.h
|
|
${KTX_DIR}/lib/basisu/transcoder/basisu_global_selector_cb.h
|
|
${KTX_DIR}/lib/basisu/transcoder/basisu_global_selector_palette.h
|
|
${KTX_DIR}/lib/basisu/transcoder/basisu_transcoder_internal.h
|
|
${KTX_DIR}/lib/basisu/transcoder/basisu_transcoder_uastc.h
|
|
${KTX_DIR}/lib/basisu/transcoder/basisu_transcoder.cpp
|
|
${KTX_DIR}/lib/basisu/transcoder/basisu_transcoder.h
|
|
${KTX_DIR}/lib/basisu/transcoder/basisu.h
|
|
${KTX_DIR}/lib/basisu/zstd/zstd.c
|
|
|
|
...
|
|
# KTX2
|
|
${KTX_DIR}/lib/texture2.c
|
|
${KTX_DIR}/lib/texture2.h
|
|
----
|
|
|
|
An alternative to including _libktx_ via CMake would be using the pre-built binaries provided in the https://github.com/KhronosGroup/KTX-Software/releases[KTX-Software repository].
|
|
|
|
For projects that just want to use KTX 2.0 and Basis Universal texture compression, a light-weight alternative to _libktx_ is the basisu _basist::ktx2_transcoder_.
|
|
More information on how to use this can be found in Binomial's documentation, https://github.com/BinomialLLC/basis_universal/wiki/How-to-Use-and-Configure-the-Transcoder[How to configure and use the transcoder].
|
|
|
|
== Selecting a GPU native target format
|
|
|
|
As noted above, the KTX 2.0 files used in this sample store texture data in the Basis Universal ETC1S and UASTC transmission formats, which can't be natively used by the GPU.
|
|
|
|
So before transcoding the data to a native GPU format, we need to select a valid native GPU target format.
|
|
In this sample, we use a simple mechanism solely based on the Vulkan formats supported by the GPU:
|
|
|
|
[,cpp]
|
|
----
|
|
void TextureCompressionBasisu::get_available_target_formats()
|
|
{
|
|
available_target_formats.clear();
|
|
|
|
VkPhysicalDeviceFeatures device_features = get_device().get_gpu().get_features();
|
|
|
|
// Block compression
|
|
if (device_features.textureCompressionBC)
|
|
{
|
|
// BC7 is the preferred block compression if available
|
|
if (format_supported(VK_FORMAT_BC7_SRGB_BLOCK))
|
|
{
|
|
// Target formats from the KTX library, and prefixed with KTX_
|
|
available_target_formats.push_back(KTX_TTF_BC7_RGBA);
|
|
}
|
|
|
|
...
|
|
}
|
|
|
|
// Adaptive scalable texture compression
|
|
if (device_features.textureCompressionASTC_LDR)
|
|
{
|
|
...
|
|
}
|
|
|
|
// Ericsson texture compression
|
|
if (device_features.textureCompressionETC2)
|
|
{
|
|
...
|
|
}
|
|
|
|
// Always add uncompressed RGBA as a valid target
|
|
available_target_formats.push_back(KTX_TTF_RGBA32);
|
|
available_target_formats_names.push_back("KTX_TTF_RGBA32");
|
|
}
|
|
----
|
|
|
|
This gives us a list of possible target formats for the Basis Universal transcoder (prefixed with `KTX_`) that we can later on transcode to from the ETC1S and UASTC transmission formats.
|
|
|
|
Transcode target format selection in a real world application will most probably be a bit more complex.
|
|
A good reference for how to select target formats can be found in the https://github.com/KhronosGroup/3D-Formats-Guidelines/blob/main/KTXDeveloperGuide.md[KTX 2.0 / Basis Universal Textures -- Developer Guide].
|
|
|
|
== Loading the KTX 2.0 file
|
|
|
|
Loading and transcoding the KTX 2.0 texture image file handled by _libktx_ and done inside the `TextureCompressionBasisu::transcode_texture` function.
|
|
|
|
=== Loading the file from disk
|
|
|
|
Loading a KTX 2.0 file is the same as loading a KTX1.0 file, with the exception that we use the `ktxTexture2` class, which needs to be casted to `ktxTexture` in some function calls:
|
|
|
|
[,cpp]
|
|
----
|
|
// We are working with KTX 2.0 files, so we need to use the ktxTexture2 class
|
|
ktxTexture2 *ktx_texture;
|
|
// Load the KTX 2.0 file into memory. This is agnostic to the KTX version, so we cast the ktxTexture2 down to ktxTexture
|
|
KTX_error_code result = ktxTexture_CreateFromNamedFile(file_name.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, (ktxTexture **) &ktx_texture);
|
|
if (result != KTX_SUCCESS)
|
|
{
|
|
throw std::runtime_error("Could not load the requested image file.");
|
|
}
|
|
----
|
|
|
|
=== Transcoding into a native format
|
|
|
|
Once we have successfully loaded the file from disk, we can transcode it from ETCS1/UASTC to our desired target format from the list we created earlier.
|
|
|
|
We first check if the source KTX 2.0 file actually needs transcoding via `ktxTexture2_NeedsTranscoding`.
|
|
This is always the case for all KTX 2.0 texture files used in this sample, but if a file would e.g.
|
|
already contain a native format like BCn, then we wouldn't have to transcode it.
|
|
|
|
If the file needs transcoding, we then call the Basis Universal transcoder from the _libktx_ via `ktxTexture2_TranscodeBasis`.
|
|
This will transcode the texture data into the GPU native target format:
|
|
|
|
[,cpp]
|
|
----
|
|
if (ktxTexture2_NeedsTranscoding(ktx_texture))
|
|
{
|
|
result = ktxTexture2_TranscodeBasis(ktx_texture, target_format, 0);
|
|
if (result != KTX_SUCCESS)
|
|
{
|
|
throw std::runtime_error("Could not transcode the input texture to the selected target format.");
|
|
}
|
|
}
|
|
----
|
|
|
|
If we e.g.
|
|
select `KTX_TTF_BC7_RGBA` as the transcode target format for a UASTC compressed file, this will transcode the UASTC texture data to GPU native BC7 data.
|
|
|
|
=== Uploading the texture data
|
|
|
|
Once transcoded, the `ktxTexture` object contains the texture data in a native GPU format (e.g.
|
|
BC7 in the above sample), which can then be directly uploaded to a GPU that supports BC7 texture compression.
|
|
From this point on it's like working with regular textures.
|
|
The native Vulkan format we can then use to create the Vulkan image from can be taken from the `ktxTexture` object:
|
|
|
|
[,cpp]
|
|
----
|
|
VkFormat format = (VkFormat)ktx_texture->vkFormat;
|
|
|
|
// Create a buffer to store the transcoded ktx texture data for staging to the GPU
|
|
VkBufferCreateInfo buffer_create_info = vkb::initializers::buffer_create_info();
|
|
buffer_create_info.size = ktx_texture->dataSize;
|
|
|
|
...
|
|
|
|
// Copy the ktx texture into the host local buffer
|
|
uint8_t *data;
|
|
vkMapMemory(get_device().get_handle(), staging_memory, 0, memory_requirements.size, 0, (void **) &data);
|
|
memcpy(data, ktx_image_data, ktx_texture->dataSize);
|
|
vkUnmapMemory(get_device().get_handle(), staging_memory);
|
|
|
|
// Setup buffer copy regions for each mip level
|
|
std::vector<VkBufferImageCopy> buffer_copy_regions;
|
|
for (uint32_t mip_level = 0; mip_level < texture.mip_levels; mip_level++)
|
|
{
|
|
ktx_size_t offset;
|
|
KTX_error_code result = ktxTexture_GetImageOffset((ktxTexture *) ktx_texture, mip_level, 0, 0, &offset);
|
|
VkBufferImageCopy buffer_copy_region = {};
|
|
buffer_copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
buffer_copy_region.imageSubresource.mipLevel = mip_level;
|
|
buffer_copy_region.imageSubresource.baseArrayLayer = 0;
|
|
buffer_copy_region.imageSubresource.layerCount = 1;
|
|
buffer_copy_region.imageExtent.width = ktx_texture->baseWidth >> mip_level;
|
|
buffer_copy_region.imageExtent.height = ktx_texture->baseHeight >> mip_level;
|
|
buffer_copy_region.imageExtent.depth = 1;
|
|
buffer_copy_region.bufferOffset = offset;
|
|
buffer_copy_regions.push_back(buffer_copy_region);
|
|
}
|
|
|
|
...
|
|
|
|
VkImageCreateInfo image_create_info = vkb::initializers::image_create_info();
|
|
image_create_info.imageType = VK_IMAGE_TYPE_2D;
|
|
image_create_info.format = format;
|
|
image_create_info.mipLevels = texture.mip_levels;
|
|
image_create_info.arrayLayers = 1;
|
|
image_create_info.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
image_create_info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
image_create_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
image_create_info.extent = {texture.width, texture.height, 1};
|
|
image_create_info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
vkCreateImage(get_device().get_handle(), &image_create_info, nullptr, &texture.image);
|
|
|
|
...
|
|
|
|
// Upload data to the Vulkan image using a command buffer
|
|
|
|
VkCommandBuffer copy_command = device->create_command_buffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
|
|
...
|
|
vkCmdCopyBufferToImage(
|
|
copy_command,
|
|
staging_buffer,
|
|
texture.image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
static_cast<uint32_t>(buffer_copy_regions.size()),
|
|
buffer_copy_regions.data());
|
|
...
|
|
device->flush_command_buffer(copy_command, queue, true);
|
|
----
|
|
|
|
== The sample
|
|
|
|
image::./images/texture_compression_basisu_sample.jpg[Sample image]
|
|
|
|
The sample allows transcoding of a fixed set of ETC1S/UASTC to supported native GPU target formats at runtime.
|
|
The list of possible targets depends on the device's capabilities.
|
|
You can also zoom in and rotate the image to see the effect of different input and target format combinations.
|
|
|
|
NOTE: Transcoding speed suffers a lot in debug builds.
|
|
For best performance, running a release build is advised.
|