Files
Vulkan-Samples/samples/performance/texture_compression_comparison/texture_compression_comparison.cpp
T
2025-09-04 10:54:47 +08:00

345 lines
13 KiB
C++

/* Copyright (c) 2021-2025, Holochip
*
* 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.
*/
#include "texture_compression_comparison.h"
#include "rendering/subpasses/forward_subpass.h"
#include "scene_graph/components/camera.h"
#include "scene_graph/components/image.h"
#include "scene_graph/components/material.h"
#include "scene_graph/components/mesh.h"
namespace
{
constexpr std::array<const char *, 19> error_codes = {
"KTX_SUCCESS",
"KTX_FILE_DATA_ERROR",
"KTX_FILE_ISPIPE",
"KTX_FILE_OPEN_FAILED",
"KTX_FILE_OVERFLOW",
"KTX_FILE_READ_ERROR",
"KTX_FILE_SEEK_ERROR",
"KTX_FILE_UNEXPECTED_EOF",
"KTX_FILE_WRITE_ERROR",
"KTX_GL_ERROR",
"KTX_INVALID_OPERATION",
"KTX_INVALID_VALUE",
"KTX_NOT_FOUND",
"KTX_OUT_OF_MEMORY",
"KTX_TRANSCODE_FAILED",
"KTX_UNKNOWN_FILE_FORMAT",
"KTX_UNSUPPORTED_TEXTURE_TYPE",
"KTX_UNSUPPORTED_FEATURE",
"KTX_LIBRARY_NOT_LINKED",
};
std::string get_sponza_texture_filename(const std::string &short_name)
{
return vkb::fs::path::get(vkb::fs::path::Type::Assets) + "scenes/sponza/ktx2/" + short_name + "2";
}
} // namespace
#define KTX_CHECK(x) \
do \
{ \
KTX_error_code err = x; \
if (err != KTX_SUCCESS) \
{ \
auto index = static_cast<uint32_t>(err); \
LOGE("Detected KTX error: {}", index < error_codes.size() ? error_codes[index] : ""); \
abort(); \
} \
} while (0)
bool TextureCompressionComparison::prepare(const vkb::ApplicationOptions &options)
{
if (!VulkanSample::prepare(options))
{
return false;
}
load_assets();
auto &camera_node = vkb::add_free_camera(get_scene(), "main_camera", get_render_context().get_surface_extent());
camera = &camera_node.get_component<vkb::sg::Camera>();
create_subpass();
get_stats().request_stats({vkb::StatIndex::frame_times, vkb::StatIndex::gpu_ext_read_bytes});
create_gui(*window, &get_stats());
return true;
}
void TextureCompressionComparison::update(float delta_time)
{
if (require_redraw)
{
const auto &formats = get_texture_formats();
require_redraw = false;
assert(current_format >= 0 && static_cast<size_t>(current_format) < formats.size());
current_benchmark = update_textures(formats[current_format]);
}
VulkanSample::update(delta_time);
}
void TextureCompressionComparison::draw_gui()
{
if (gui_texture_names.empty())
{
const auto &formats = get_texture_formats();
gui_texture_names.resize(formats.size());
std::transform(formats.cbegin(), formats.cend(), gui_texture_names.begin(), [this](const CompressedTexture_t &format) -> std::string {
return fmt::format(FMT_STRING("{:s} {:s}"), format.short_name, is_texture_format_supported(format) ? "" : "(not supported)");
});
}
std::vector<const char *> name_pointers(gui_texture_names.size());
std::transform(gui_texture_names.cbegin(), gui_texture_names.cend(), name_pointers.begin(), [](const std::string &in) {
return in.c_str();
});
get_gui().show_options_window([this, &name_pointers]() {
if (ImGui::Combo("Compressed Format", &current_gui_format, name_pointers.data(), static_cast<int>(name_pointers.size())))
{
require_redraw = true;
const auto &format = get_texture_formats()[current_gui_format];
if (is_texture_format_supported(format))
{
current_format = current_gui_format;
}
}
const auto &format = get_texture_formats()[current_gui_format];
if (is_texture_format_supported(format))
{
ImGui::Text("Format name: %s", format.format_name);
ImGui::Text("Bytes: %f MB", static_cast<float>(current_benchmark.total_bytes) / 1024.f / 1024.f);
ImGui::Text("Compression Time: %f (ms)", current_benchmark.compress_time_ms);
}
else
{
ImGui::Text("%s not supported on this GPU.", format.short_name);
}
});
}
const std::vector<TextureCompressionComparison::CompressedTexture_t> &TextureCompressionComparison::get_texture_formats()
{
static std::vector<TextureCompressionComparison::CompressedTexture_t> formats = {
CompressedTexture_t{nullptr,
"",
VK_FORMAT_R8G8B8A8_SRGB,
KTX_TTF_RGBA32,
"KTX_TTF_RGBA32",
"RGBA 32",
true},
CompressedTexture_t{&VkPhysicalDeviceFeatures::textureCompressionBC,
"",
VK_FORMAT_BC7_SRGB_BLOCK,
KTX_TTF_BC7_RGBA,
"KTX_TTF_BC7_RGBA",
"BC7"},
CompressedTexture_t{&VkPhysicalDeviceFeatures::textureCompressionBC,
"",
VK_FORMAT_BC3_SRGB_BLOCK,
KTX_TTF_BC3_RGBA,
"KTX_TTF_BC3_RGBA",
"BC3"},
CompressedTexture_t{&VkPhysicalDeviceFeatures::textureCompressionASTC_LDR,
"",
VK_FORMAT_ASTC_4x4_SRGB_BLOCK,
KTX_TTF_ASTC_4x4_RGBA,
"KTX_TTF_ASTC_4x4_RGBA",
"ASTC 4x4"},
CompressedTexture_t{&VkPhysicalDeviceFeatures::textureCompressionETC2,
"",
VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK,
KTX_TTF_ETC2_RGBA,
"KTX_TTF_ETC2_RGBA",
"ETC2"}};
return formats;
}
bool TextureCompressionComparison::is_texture_format_supported(const TextureCompressionComparison::CompressedTexture_t &format)
{
const auto device_features = get_device().get_gpu().get_features();
const bool supported_by_feature = format.feature_ptr && device_features.*format.feature_ptr;
const bool supported_by_extension = strlen(format.extension_name) && get_device().get_gpu().is_extension_supported(format.extension_name);
const bool supported_by_default = format.always_supported;
return supported_by_default || supported_by_feature || supported_by_extension;
}
void TextureCompressionComparison::load_assets()
{
load_scene("scenes/sponza/Sponza01.gltf");
if (!has_scene())
{
throw std::runtime_error("Unable to load Sponza scene");
}
for (auto &&mesh : get_scene().get_components<vkb::sg::Mesh>())
{
for (auto &&sub_mesh : mesh->get_submeshes())
{
auto material = sub_mesh->get_material();
for (auto &name_texture : material->textures)
{
vkb::sg::Texture *texture = name_texture.second;
auto image = texture->get_image();
textures.emplace_back(texture, image->get_name());
}
}
}
}
void TextureCompressionComparison::create_subpass()
{
vkb::ShaderSource vert_shader("base.vert.spv");
vkb::ShaderSource frag_shader("base.frag.spv");
auto scene_sub_pass = std::make_unique<vkb::ForwardSubpass>(get_render_context(), std::move(vert_shader), std::move(frag_shader), get_scene(), *camera);
auto render_pipeline = std::make_unique<vkb::RenderPipeline>();
render_pipeline->add_subpass(std::move(scene_sub_pass));
set_render_pipeline(std::move(render_pipeline));
}
TextureCompressionComparison::TextureBenchmark TextureCompressionComparison::update_textures(const TextureCompressionComparison::CompressedTexture_t &new_format)
{
TextureBenchmark benchmark;
std::unordered_set<std::string> visited;
for (auto &&texture_filename : textures)
{
vkb::sg::Texture *texture = texture_filename.first;
assert(!!texture);
auto &internal_name = texture_filename.second;
if (!visited.count(internal_name))
{
auto filename = get_sponza_texture_filename(internal_name);
auto new_image = compress(filename, new_format, "");
texture_raw_data[internal_name].image = std::move(new_image.first);
texture_raw_data[internal_name].benchmark = new_image.second;
benchmark += new_image.second;
}
vkb::sg::Image *image = texture_raw_data[internal_name].image.get();
assert(image);
texture->set_image(*image);
visited.insert(internal_name);
}
// update the forward subpass to use the new textures
create_subpass();
return benchmark;
}
namespace
{
class CompressedImage : public vkb::sg::Image
{
public:
CompressedImage(vkb::core::DeviceC &device, const std::string &name, std::vector<vkb::sg::Mipmap> &&mipmaps, VkFormat format) :
vkb::sg::Image(name, std::vector<uint8_t>{}, std::move(mipmaps))
{
vkb::sg::Image::set_format(format);
vkb::sg::Image::create_vk_image(device);
}
};
} // namespace
std::unique_ptr<vkb::sg::Image> TextureCompressionComparison::create_image(ktxTexture2 *ktx_texture, const std::string &name)
{
std::unique_ptr<vkb::core::BufferC> staging_buffer = std::make_unique<vkb::core::BufferC>(get_device(), ktx_texture->dataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
memcpy(staging_buffer->map(), ktx_texture->pData, ktx_texture->dataSize);
const auto vk_format = static_cast<VkFormat>(ktx_texture->vkFormat);
VkExtent3D extent{ktx_texture->baseWidth, ktx_texture->baseHeight, 1};
std::vector<VkBufferImageCopy> buffer_copies;
std::unique_ptr<vkb::sg::Image> image_out;
{
std::vector<vkb::sg::Mipmap> mip_maps;
for (uint32_t mip_level = 0; mip_level < ktx_texture->numLevels; ++mip_level)
{
VkExtent3D mip_extent = VkExtent3D{extent.width >> mip_level, extent.height >> mip_level, 1};
if (!mip_extent.width || !mip_extent.height)
{
break;
}
ktx_size_t offset{0};
KTX_CHECK(ktxTexture_GetImageOffset((ktxTexture *) ktx_texture, mip_level, 0, 0, &offset));
VkBufferImageCopy buffer_image_copy = {};
buffer_image_copy.imageSubresource = VkImageSubresourceLayers{VK_IMAGE_ASPECT_COLOR_BIT, mip_level, 0, 1};
buffer_image_copy.imageExtent = mip_extent;
buffer_image_copy.bufferOffset = static_cast<uint32_t>(offset);
buffer_copies.push_back(buffer_image_copy);
vkb::sg::Mipmap mip_map;
mip_map.extent = buffer_image_copy.imageExtent;
mip_map.level = mip_level;
mip_map.offset = static_cast<uint32_t>(offset);
mip_maps.push_back(mip_map);
}
image_out = std::make_unique<CompressedImage>(get_device(), name, std::move(mip_maps), vk_format);
}
auto &vkb_image = image_out->get_vk_image();
auto image = vkb_image.get_handle();
VkImageSubresourceRange subresource_range{VK_IMAGE_ASPECT_COLOR_BIT, 0, static_cast<uint32_t>(buffer_copies.size()), 0, 1};
VkCommandBuffer command_buffer = get_device().create_command_buffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
vkb::image_layout_transition(command_buffer, image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, subresource_range);
vkCmdCopyBufferToImage(command_buffer, staging_buffer->get_handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<uint32_t>(buffer_copies.size()), buffer_copies.data());
vkb::image_layout_transition(command_buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, subresource_range);
get_device().flush_command_buffer(command_buffer, get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0).get_handle(), true);
return image_out;
}
std::pair<std::unique_ptr<vkb::sg::Image>, TextureCompressionComparison::TextureBenchmark> TextureCompressionComparison::compress(const std::string &filename, TextureCompressionComparison::CompressedTexture_t texture_format, const std::string &name)
{
ktxTexture2 *ktx_texture{nullptr};
KTX_CHECK(ktxTexture2_CreateFromNamedFile(filename.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktx_texture));
TextureBenchmark benchmark;
{
const auto start = std::chrono::high_resolution_clock::now();
KTX_CHECK(ktxTexture2_TranscodeBasis(ktx_texture, texture_format.ktx_format, 0));
const auto end = std::chrono::high_resolution_clock::now();
benchmark.compress_time_ms = static_cast<float>(std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()) / 1000.f;
}
benchmark.total_bytes = ktx_texture->dataSize;
auto image = create_image(ktx_texture, name);
ktxTexture_Destroy((ktxTexture *) ktx_texture);
return {std::move(image), benchmark};
}
std::unique_ptr<TextureCompressionComparison> create_texture_compression_comparison()
{
return std::make_unique<TextureCompressionComparison>();
}