This commit is contained in:
xsl
2025-09-04 10:54:47 +08:00
commit 6bc8f61b18
1808 changed files with 208268 additions and 0 deletions
@@ -0,0 +1,365 @@
/* Copyright (c) 2020-2025, Arm Limited and Contributors
*
* 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 "16bit_arithmetic.h"
#include "gui.h"
#include "stats/stats.h"
#include <random>
#include <scene_graph/components/camera.h>
static constexpr unsigned Width = 1024;
static constexpr unsigned Height = 1024;
static constexpr unsigned NumBlobs = 16;
KHR16BitArithmeticSample::KHR16BitArithmeticSample()
{
// Enables required extensions to use 16-bit storage.
// For this sample, this is not optional.
// This sample also serves as a tutorial on how to use 16-bit storage
// for SSBOs and push constants.
add_instance_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, false);
add_device_extension(VK_KHR_STORAGE_BUFFER_STORAGE_CLASS_EXTENSION_NAME, false);
add_device_extension(VK_KHR_16BIT_STORAGE_EXTENSION_NAME, false);
// Enables the extension which allows shaders to use 16-bit float and 8-bit integer arithmetic.
// This sample will only make use of 16-bit floats.
add_device_extension(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME, true);
auto &config = get_configuration();
config.insert<vkb::BoolSetting>(0, khr_16bit_arith_enabled, false);
config.insert<vkb::BoolSetting>(1, khr_16bit_arith_enabled, true);
}
bool KHR16BitArithmeticSample::prepare(const vkb::ApplicationOptions &options)
{
if (!VulkanSample::prepare(options))
{
return false;
}
// Normally, we should see the immediate effect on frame times,
// but if we're somehow hitting 60 FPS, GPU cycles / s should go down while hitting vsync.
get_stats().request_stats({vkb::StatIndex::gpu_cycles, vkb::StatIndex::frame_times});
create_gui(*window, &get_stats());
// Set up some structs for the (color, depth) attachments in the default render pass.
load_store_infos.resize(2);
load_store_infos[0].load_op = VK_ATTACHMENT_LOAD_OP_CLEAR;
load_store_infos[0].store_op = VK_ATTACHMENT_STORE_OP_STORE;
load_store_infos[1].load_op = VK_ATTACHMENT_LOAD_OP_CLEAR;
load_store_infos[1].store_op = VK_ATTACHMENT_STORE_OP_DONT_CARE;
VkClearValue clear_value;
clear_value.color.float32[0] = 0.0f;
clear_value.color.float32[1] = 0.0f;
clear_value.color.float32[2] = 0.0f;
clear_value.color.float32[3] = 1.0f;
clear_values.push_back(clear_value);
clear_value.depthStencil.depth = 1.0f;
clear_value.depthStencil.stencil = 0;
clear_values.push_back(clear_value);
// Generate some random blobs to render and place them in a 4xfp16 data structure.
std::default_random_engine rng(42);
std::normal_distribution<float> position_dist(0.0f, 0.1f);
std::uniform_real_distribution<float> intensity_dist(0.4f, 0.8f);
std::uniform_real_distribution<float> falloff_dist(50.0f, 100.0f);
glm::vec4 initial_data_fp32[NumBlobs];
for (unsigned i = 0; i < NumBlobs; i++)
{
initial_data_fp32[i].x = position_dist(rng);
initial_data_fp32[i].y = position_dist(rng);
initial_data_fp32[i].z = intensity_dist(rng);
initial_data_fp32[i].w = falloff_dist(rng);
}
// Convert FP32 to FP16.
glm::uvec2 initial_data_fp16[NumBlobs];
for (unsigned i = 0; i < NumBlobs; i++)
{
initial_data_fp16[i].x = glm::packHalf2x16(initial_data_fp32[i].xy);
initial_data_fp16[i].y = glm::packHalf2x16(initial_data_fp32[i].zw);
}
// Upload the blob buffer.
auto &device = get_render_context().get_device();
blob_buffer = std::make_unique<vkb::core::BufferC>(device, sizeof(initial_data_fp16),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VMA_MEMORY_USAGE_GPU_ONLY);
auto staging_buffer = vkb::core::BufferC::create_staging_buffer(device, initial_data_fp16);
auto cmd = device.get_command_pool().request_command_buffer();
cmd->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, VK_NULL_HANDLE);
cmd->copy_buffer(staging_buffer, *blob_buffer, sizeof(initial_data_fp16));
vkb::BufferMemoryBarrier barrier;
barrier.src_stage_mask = VK_PIPELINE_STAGE_TRANSFER_BIT;
barrier.dst_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
barrier.src_access_mask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dst_access_mask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
cmd->buffer_memory_barrier(*blob_buffer, 0, VK_WHOLE_SIZE, barrier);
cmd->end();
auto &queue = device.get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
queue.submit(*cmd, device.get_fence_pool().request_fence());
device.get_fence_pool().wait();
// Create the target image we render into in the main compute shader.
image = std::make_unique<vkb::core::Image>(device, VkExtent3D{Width, Height, 1},
VK_FORMAT_R16G16B16A16_SFLOAT,
VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VMA_MEMORY_USAGE_GPU_ONLY);
image_view = std::make_unique<vkb::core::ImageView>(*image, VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_R16G16B16A16_SFLOAT,
0, 0, 1, 1);
// Calculate valid filter
VkFilter filter = VK_FILTER_LINEAR;
vkb::make_filters_valid(get_device().get_gpu().get_handle(), image->get_format(), &filter);
VkSamplerCreateInfo sampler_create_info = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler_create_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_create_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_create_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_create_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
sampler_create_info.magFilter = filter;
sampler_create_info.minFilter = filter;
sampler_create_info.maxLod = VK_LOD_CLAMP_NONE;
sampler = std::make_unique<vkb::core::Sampler>(device, sampler_create_info);
// Load shader modules.
auto &module =
device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource{"16bit_arithmetic/compute_buffer.comp.spv"});
compute_layout = &device.get_resource_cache().request_pipeline_layout({&module});
if (supported_extensions)
{
vkb::ShaderVariant variant;
if (supports_push_constant16)
{
auto &module_fp16 =
device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource{"16bit_arithmetic/compute_buffer_fp16.comp.spv"}, variant);
compute_layout_fp16 = &device.get_resource_cache().request_pipeline_layout({&module_fp16});
}
else
{
auto &module_fp16 =
device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource{"16bit_arithmetic/compute_buffer_fp16_fallback.comp.spv"}, variant);
compute_layout_fp16 = &device.get_resource_cache().request_pipeline_layout({&module_fp16});
}
}
else
{
compute_layout_fp16 = compute_layout;
}
// Setup the visualization subpass which is there to blit the final result to screen.
vkb::ShaderSource vertex_source{"16bit_arithmetic/visualize.vert.spv"};
vkb::ShaderSource fragment_source{"16bit_arithmetic/visualize.frag.spv"};
auto subpass = std::make_unique<VisualizationSubpass>(get_render_context(),
std::move(vertex_source),
std::move(fragment_source));
subpass->view = image_view.get();
subpass->sampler = sampler.get();
subpasses.emplace_back(std::move(subpass));
for (auto &subpass : subpasses)
{
subpass->prepare();
}
return true;
}
KHR16BitArithmeticSample::VisualizationSubpass::VisualizationSubpass(vkb::RenderContext &context,
vkb::ShaderSource &&vertex_source,
vkb::ShaderSource &&fragment_source) :
vkb::rendering::SubpassC(context, std::move(vertex_source), std::move(fragment_source))
{
set_output_attachments({0});
}
void KHR16BitArithmeticSample::VisualizationSubpass::draw(vkb::core::CommandBufferC &command_buffer)
{
command_buffer.bind_pipeline_layout(*layout);
// A depth-stencil attachment exists in the default render pass, make sure we ignore it.
vkb::DepthStencilState ds_state = {};
ds_state.depth_test_enable = VK_FALSE;
ds_state.stencil_test_enable = VK_FALSE;
ds_state.depth_write_enable = VK_FALSE;
ds_state.depth_compare_op = VK_COMPARE_OP_ALWAYS;
command_buffer.set_depth_stencil_state(ds_state);
command_buffer.bind_image(*view, *sampler, 0, 0, 0);
command_buffer.draw(3, 1, 0, 0);
}
void KHR16BitArithmeticSample::VisualizationSubpass::prepare()
{
auto &device = get_render_context().get_device();
auto &vert_shader_module = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_VERTEX_BIT, get_vertex_shader());
auto &frag_shader_module = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_FRAGMENT_BIT, get_fragment_shader());
std::vector<vkb::ShaderModule *> shader_modules{&vert_shader_module, &frag_shader_module};
layout = &device.get_resource_cache().request_pipeline_layout(shader_modules);
}
void KHR16BitArithmeticSample::request_gpu_features(vkb::PhysicalDevice &gpu)
{
// Required features.
REQUEST_REQUIRED_FEATURE(gpu,
VkPhysicalDevice16BitStorageFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES,
storageBuffer16BitAccess);
REQUEST_REQUIRED_FEATURE(gpu,
VkPhysicalDevice16BitStorageFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES,
uniformAndStorageBuffer16BitAccess);
// Optional features.
supported_extensions = REQUEST_OPTIONAL_FEATURE(gpu,
VkPhysicalDeviceFloat16Int8FeaturesKHR,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FLOAT16_INT8_FEATURES_KHR,
shaderFloat16);
supports_push_constant16 =
REQUEST_OPTIONAL_FEATURE(gpu, VkPhysicalDevice16BitStorageFeatures, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES, storagePushConstant16);
}
void KHR16BitArithmeticSample::draw_renderpass(vkb::core::CommandBufferC &command_buffer, vkb::RenderTarget &render_target)
{
if (khr_16bit_arith_enabled)
{
command_buffer.bind_pipeline_layout(*compute_layout_fp16);
}
else
{
command_buffer.bind_pipeline_layout(*compute_layout);
}
command_buffer.bind_buffer(*blob_buffer, 0, NumBlobs * sizeof(glm::uvec2), 0, 0, 0);
command_buffer.bind_image(*image_view, 0, 1, 0);
// Wait for fragment shader is done reading before we can write in compute.
vkb::ImageMemoryBarrier write_after_read_hazard;
write_after_read_hazard.src_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
write_after_read_hazard.dst_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
write_after_read_hazard.src_access_mask = 0;
write_after_read_hazard.dst_access_mask = VK_ACCESS_SHADER_WRITE_BIT;
write_after_read_hazard.old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
write_after_read_hazard.new_layout = VK_IMAGE_LAYOUT_GENERAL;
command_buffer.image_memory_barrier(*image_view, write_after_read_hazard);
// 16-bit push constants are supported by VK_KHR_16bit_storage, which is handy for conserving space without
// using many "unpack" instructions in the shader.
struct Push16
{
uint16_t num_blobs;
uint16_t fp16_seed;
int16_t range_x, range_y;
} push16 = {};
struct Push32
{
uint32_t num_blobs;
float fp32_seed;
int32_t range_x, range_y;
} push32 = {};
frame_count = (frame_count + 1u) & 511u;
float seed_value = 0.5f * glm::sin(glm::two_pi<float>() * (static_cast<float>(frame_count) / 512.0f));
push32.num_blobs = NumBlobs;
push32.fp32_seed = seed_value;
push32.range_x = 2;
push32.range_y = 1;
if (khr_16bit_arith_enabled && supports_push_constant16)
{
push16.num_blobs = push32.num_blobs;
push16.fp16_seed = static_cast<uint16_t>(glm::packHalf2x16(glm::vec2(push32.fp32_seed)));
push16.range_x = push32.range_x;
push16.range_y = push32.range_y;
command_buffer.push_constants(push16);
}
else
{
command_buffer.push_constants(push32);
}
command_buffer.set_specialization_constant(0, Width);
command_buffer.set_specialization_constant(1, Height);
// Workgroup size is (8, 8)
command_buffer.dispatch(Width / 8, Height / 8, 1);
vkb::ImageMemoryBarrier to_fragment_barrier;
to_fragment_barrier.old_layout = VK_IMAGE_LAYOUT_GENERAL;
to_fragment_barrier.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
to_fragment_barrier.src_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
to_fragment_barrier.dst_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
to_fragment_barrier.src_access_mask = VK_ACCESS_SHADER_WRITE_BIT;
to_fragment_barrier.dst_access_mask = VK_ACCESS_SHADER_READ_BIT;
command_buffer.image_memory_barrier(*image_view, to_fragment_barrier);
// Blit result to screen and render UI.
command_buffer.begin_render_pass(render_target, load_store_infos, clear_values, subpasses);
command_buffer.set_viewport(0, {{0.0f, 0.0f, static_cast<float>(render_target.get_extent().width), static_cast<float>(render_target.get_extent().height), 0.0f, 1.0f}});
command_buffer.set_scissor(0, {{{0, 0}, render_target.get_extent()}});
subpasses.front()->draw(command_buffer);
get_gui().draw(command_buffer);
command_buffer.end_render_pass();
}
void KHR16BitArithmeticSample::draw_gui()
{
const char *label;
if (supported_extensions)
{
label = "Enable 16-bit arithmetic";
}
else
{
label = "16-bit arithmetic (unsupported features)";
}
get_gui().show_options_window(
/* body = */ [this, label]() {
if (!supported_extensions)
{
ImGui::Text("%s", label);
}
else
{
ImGui::Checkbox(label, &khr_16bit_arith_enabled);
}
},
/* lines = */ 1);
}
std::unique_ptr<vkb::VulkanSampleC> create_16bit_arithmetic()
{
return std::make_unique<KHR16BitArithmeticSample>();
}
@@ -0,0 +1,72 @@
/* Copyright (c) 2020-2025, Arm Limited and Contributors
*
* 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.
*/
#pragma once
#include "vulkan_sample.h"
#include <memory>
/**
* @brief Using 16-bit arithmetic extension to improve arithmetic throughput
*/
class KHR16BitArithmeticSample : public vkb::VulkanSampleC
{
public:
KHR16BitArithmeticSample();
virtual ~KHR16BitArithmeticSample() = default;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
virtual void draw_renderpass(vkb::core::CommandBufferC &cmd, vkb::RenderTarget &render_target) override;
private:
virtual void draw_gui() override;
bool khr_16bit_arith_enabled{false};
bool supported_extensions{false};
bool supports_push_constant16{false};
std::vector<vkb::LoadStoreInfo> load_store_infos;
std::vector<std::unique_ptr<vkb::rendering::SubpassC>> subpasses;
std::vector<VkClearValue> clear_values;
std::unique_ptr<vkb::core::BufferC> blob_buffer;
std::unique_ptr<vkb::core::Image> image;
std::unique_ptr<vkb::core::ImageView> image_view;
std::unique_ptr<vkb::core::Sampler> sampler;
vkb::PipelineLayout *compute_layout{nullptr};
vkb::PipelineLayout *compute_layout_fp16{nullptr};
unsigned frame_count{0};
struct VisualizationSubpass : vkb::rendering::SubpassC
{
VisualizationSubpass(vkb::RenderContext &context, vkb::ShaderSource &&vertex_source, vkb::ShaderSource &&fragment_source);
virtual void prepare() override;
virtual void draw(vkb::core::CommandBufferC &command_buffer) override;
vkb::PipelineLayout *layout{nullptr};
const vkb::core::ImageView *view{nullptr};
const vkb::core::Sampler *sampler{nullptr};
};
};
std::unique_ptr<vkb::VulkanSampleC> create_16bit_arithmetic();
@@ -0,0 +1,34 @@
# Copyright (c) 2020-2025, Arm Limited and Contributors
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample_with_tags(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Hans-Kristian Arntzen"
NAME "16-bit arithmetic"
DESCRIPTION "Using VK_KHR_shader_float16_int8 to improve arithmetic throughput."
SHADER_FILES_GLSL
"16bit_arithmetic/visualize.vert"
"16bit_arithmetic/visualize.frag"
"16bit_arithmetic/compute_buffer.comp"
"16bit_arithmetic/compute_buffer_fp16.comp"
"16bit_arithmetic/compute_buffer_fp16_fallback.comp"
)
@@ -0,0 +1,225 @@
////
- Copyright (c) 2020-2023, Arm Limited and Contributors
-
- 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 explicit 16-bit arithmetic in applications
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/16bit_arithmetic[Khronos Vulkan samples github repository].
endif::[]
== Overview
In the world of mobile GPUs, `mediump` has long been used as a critical optimization for performance and bandwidth.
The desktop GPUs and APIs have not had much support for native 16-bit operations, but in recent architectures, this feature is becoming widespread, and FP16 in particular is becoming more common.
In this sample, we demonstrate `VK_KHR_shader_float16_int8`, which adds standardized support for FP16 arithmetic (and INT8 arithmetic).
== Enable 16-bit floating-point arithmetic support
To add FP16 arithmetic support, enable the `VK_KHR_shader_float16_int8` extension.
You will also need to query `vkGetPhysicalDeviceFeatures2` with the `VkPhysicalDeviceShaderFloat16Int8Features` struct.
Here, we can query and enable two separate features:
* `shaderFloat16`
* `shaderInt8`
With these features enabled, we can create SPIR-V modules with the `Float16` and `Int8` capabilities enabled, respectively.
Note that this feature does not include support for 16-bit or 8-bit storage.
For 16-bit storage, see `VK_KHR_16bit_storage`.
=== 8-bit arithmetic?
While this extension also adds supports for using 8-bit integer arithmetic, this feature is not exercised by this sample.
=== 16-bit integers?
Vulkan 1.0 already had a `shaderInt16` feature.
== Enable 16-bit storage support
When using 16-bit arithmetic, it's very likely that you would also use 16-bit values in buffers.
To that end, this sample also shows how to make use of 16-bit storage in SSBOs and push constants.
For this case, we need enable the `VK_KHR_16bit_storage` extension, as well as `VK_KHR_storage_buffer_storage_class`, which is required for `VK_KHR_16bit_storage`.
From `vkGetPhysicalDeviceFeatures2`, we check `VkPhysicalDevice16BitStorageFeatures`, and enable:
* `storageBuffer16BitAccess`
* `storagePushConstant16`
== The 16-bit arithmetic sample
This sample aims to hammer the GPU with 16-bit floating-point arithmetic to observe a significant uplift in arithmetic throughput.
The sample is completely brute force and computes some procedural color rings.
For animation purposes, these rings move around on screen and change their appearance over time.
This is not intended to be an efficient way of rendering this kind of effect, quite the contrary.
Every pixel tests every ring unconditionally, and the math expended to compute the final color is heavily exaggerated to make sure that we completely isolate arithmetic throughput as the main bottleneck.
image::./images/blobs_result_fp32.jpg[32-bit arithmetic]
Here, the critical arithmetic overhead is:
[,glsl]
----
// This is very arbitrary. Expends a ton of arithmetic to compute
// something that looks similar to a lens flare.
vec4 compute_blob(vec2 pos, vec4 blob, float seed)
{
vec2 offset = pos - blob.xy;
vec2 s_offset = offset * (1.1 + seed);
vec2 r_offset = offset * 0.95;
vec2 g_offset = offset * 1.0;
vec2 b_offset = offset * 1.05;
float r_dot = dot(r_offset, r_offset);
float g_dot = dot(g_offset, g_offset);
float b_dot = dot(b_offset, b_offset);
float s_dot = dot(s_offset, s_offset);
vec4 dots = vec4(r_dot, g_dot, b_dot, s_dot) * blob.w;
// Now we have square distances to blob center.
// Gotta have some FMAs, right? :D
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
vec4 parabolas = max(vec4(1.0, 1.0, 1.0, 0.9) - dots, vec4(0.0));
parabolas -= parabolas.w;
parabolas = max(parabolas, vec4(0.0));
return parabolas;
}
----
image::./images/blobs_result_fp16.jpg[16-bit arithmetic]
In this version, we rewrite `compute_blob` and the rest of the shader to be as pure FP16 as we can:
[,glsl]
----
// Allows us to use float16_t for arithmetic purposes.
#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require
// Allows us to use int16_t, uint16_t and float16_t for buffers.
#extension GL_EXT_shader_16bit_storage : require
----
[,glsl]
----
// This is very arbitrary. Expends a ton of arithmetic to compute
// something that looks similar to a lens flare.
f16vec4 compute_blob(f16vec2 pos, f16vec4 blob, float16_t seed)
{
f16vec2 offset = pos - blob.xy;
f16vec4 rg_offset = offset.xxyy * f16vec4(0.95hf, 1.0hf, 0.95hf, 1.0hf);
f16vec4 bs_offset = offset.xxyy * f16vec4(1.05hf, 1.1hf + seed, 1.05hf, 1.1hf + seed);
f16vec4 rg_dot = rg_offset * rg_offset;
f16vec4 bs_dot = bs_offset * bs_offset;
// Dot products can be somewhat awkward in FP16, since the result is a scalar 16-bit value, and we don't want that.
// To that end, we compute at least two dot products side by side, and rg_offset and bs_offset are swizzled
// such that we avoid swizzling across a 32-bit boundary.
f16vec4 dots = f16vec4(rg_dot.xy + rg_dot.zw, bs_dot.xy + bs_dot.zw) * blob.w;
// Now we have square distances to blob center.
// Gotta have some FMAs, right? :D
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
f16vec4 parabolas = max(f16vec4(1.0hf, 1.0hf, 1.0hf, 0.9hf) - dots, f16vec4(0.0hf));
parabolas -= parabolas.w;
parabolas = max(parabolas, f16vec4(0.0hf));
return parabolas;
}
----
== Explicit 16-bit arithmetic vs. `mediump` / `RelaxedPrecision`
Explicit, standardized 16-bit arithmetic support is quite recent in the graphics API world, but `mediump` will be familiar to many mobile (OpenGL ES) developers.
In SPIR-V, this translates to the `RelaxedPrecision` decoration.
The main problem with `mediump` has always been that you're just not quite sure if the driver actually makes use of the precision qualifier.
`mediump` simply signals the intent that "it's okay to use FP16 here, but compiler is free to ignore it and just use FP32".
This causes many headaches for developers (and users!), as developers might add `mediump`, observe that everything renders correctly on their implementation, but then try on a different implementation and see that rendering is broken.
If you use explicit FP16, you're guaranteed that the device in question is actually using FP16, and there is no guesswork involved.
`mediump` is supported in Vulkan GLSL even in desktop profile, and some desktop drivers and GPUs actually do make use of the resulting `RelaxedPrecision` qualifier.
It is a legitimate strategy to use `mediump` in Vulkan.
The main benefit of going that route is that you do not need to implement shader variants to handle FP16 vs.
FP32, as not all devices support explicit FP16 arithmetic yet.
Especially for fragment shaders rendering normal graphics, it can cause a headache to have to add more shader variants just for this case.
`mediump` can be a useful tool here since it works everywhere, but you have to accept different rendering results on different devices.
Explicit FP16 shines in compute workloads, where the consideration for shader variants is less of a concern, and you can implement and tune FP16 kernels.
== The hidden benefit of FP16, reducing register pressure
A somewhat hidden benefit of using smaller arithmetic types is not just a higher throughput potential, but reduction in register use.
GPU performance is in large part dictated by how many registers are required to run shaders.
As more registers are used, fewer threads can run concurrently, and thus, it is worse at hiding instruction latency.
Memory operations such as loads and stores, as well as texture operations tend to have high latencies, and if register use is too high, the shader cores are not able to effectively "hide" this latency.
This directly results in worse performance as the shader cores spend cycles doing nothing useful.
In compute shaders, you can also use shared memory with small arithmetic types, which is very nice as well.
Demonstrating these effects in a sample is quite difficult since it depends on so many unknown factors, but these effects are possible to study by using vendor tools or the `VK_KHR_pipeline_executable_properties` extension, which typically reports register usage/occupancy.
== Best practice summary
*Do*
* Consider using FP16 if you're struggling with arithmetic throughput or register pressure.
* Carefully benchmark your algorithmic improvements.
It is very hard to guarantee uplift when using FP16.
The more complicated the code is, the harder it is to successfully make good use of FP16.
If the problem can be expressed almost entirely with FMA, it is very easy to see uplift however.
* Consider using `mediump` / `RelaxedPrecision` if you don't want to explicitly use FP16, or you would need to use a lot of shader variants to select between FP32 and FP16.
The most common case here being typical graphics fragment shaders, which can easily have a combinatorial explosion of variants.
Using specially optimized compute shaders is a more plausible scenario for explicit FP16.
* If using `mediump`, make sure you test on a wide number of implementations to actually observe precision losses when using it.
* If using FP16, make sure you carefully vectorize the code by using `f16vec2` or `f16vec4`.
Modern GPU architectures rely on "packed" f16x2 instructions to achieve improved arithmetic performance.
Scalar `float16_t` won't have much, if any, benefit.
*Don't*
* Cast between FP16 and FP32 too much.
Most GPUs need to spend cycles when converting between FP16 and FP32.
* Rely on `mediump` without testing it on a wide range of implementations.
*Impact*
* Not taking advantage of FP16 could leave some optimization potential on the table.
* Not taking advantage of FP16 could lead to poor shader occupancy, i.e.
too many registers are used.
This in turn would lead to execution bubbles on a shader core, where cycles are wasted.
*Debugging*
* The only reasonable way to debug arithmetic throughput is with a profiler that can give you stats about this.
* To debug shader occupancy, an offline compiler, vendor tools or the standard `VK_KHR_pipeline_executable_properties` extension could help to obtain this kind of information.
Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

@@ -0,0 +1,251 @@
/* Copyright (c) 2020-2025, Arm Limited and Contributors
*
* 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 "16bit_storage_input_output.h"
#include "gui.h"
#include "rendering/subpasses/forward_subpass.h"
#include "scene_graph/components/mesh.h"
#include "scene_graph/components/pbr_material.h"
#include <random>
KHR16BitStorageInputOutputSample::KHR16BitStorageInputOutputSample()
{
// For enabling 16-bit storage device extensions.
add_instance_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, true);
// Will be used in vertex and fragment shaders to declare varying data as FP16 rather than FP32.
// This significantly reduces bandwidth as varyings are stored in main memory on TBDR architectures.
// On Vulkan 1.1, this extension is in core, but just enable the extension in case we are running on a Vulkan 1.0 implementation.
add_device_extension(VK_KHR_16BIT_STORAGE_EXTENSION_NAME, true);
// 16-bit storage depends on this extension as well.
add_device_extension(VK_KHR_STORAGE_BUFFER_STORAGE_CLASS_EXTENSION_NAME, true);
auto &config = get_configuration();
config.insert<vkb::BoolSetting>(0, khr_16bit_storage_input_output_enabled, false);
config.insert<vkb::BoolSetting>(1, khr_16bit_storage_input_output_enabled, true);
}
void KHR16BitStorageInputOutputSample::setup_scene()
{
load_scene("scenes/teapot.gltf");
// Setup the scene so we have many teapots.
vkb::sg::Mesh *teapot_mesh = nullptr;
// Override the default material so it's not rendering all black.
auto materials = get_scene().get_components<vkb::sg::PBRMaterial>();
for (auto *material : materials)
{
material->base_color_factor = glm::vec4(0.8f, 0.6f, 0.5f, 1.0f);
material->roughness_factor = 1.0f;
material->metallic_factor = 0.0f;
}
std::default_random_engine rng(42); // Use a fixed seed, makes rendering deterministic from run to run.
std::uniform_real_distribution<float> float_distribution{-1.0f, 1.0f};
const auto get_random_axis = [&]() -> glm::vec3 {
glm::vec3 axis;
for (unsigned i = 0; i < 3; i++)
{
axis[i] = float_distribution(rng);
}
if (glm::all(glm::equal(axis, glm::vec3(0.0f))))
{
axis = glm::vec3(1.0f, 0.0f, 0.0f);
}
else
{
axis = glm::normalize(axis);
}
return axis;
};
const auto get_random_angular_freq = [&]() -> float {
return 1.0f + 0.2f * float_distribution(rng);
};
auto &root_node = get_scene().get_root_node();
for (auto *child : root_node.get_children())
{
if (child->get_name() == "Teapot")
{
teapot_mesh = &child->get_component<vkb::sg::Mesh>();
auto &transform = child->get_component<vkb::sg::Transform>();
transform.set_scale(glm::vec3(1.0f));
transform.set_translation(glm::vec3(-40.0f, -20.0f, 0.0f));
transform.set_rotation(glm::quat(1.0f, 0.0f, 0.0f, 0.0f));
Transform teapot;
teapot.transform = &transform;
teapot.axis = get_random_axis();
teapot.angular_frequency = get_random_angular_freq();
teapot_transforms.push_back(teapot);
}
}
if (!teapot_mesh)
{
throw std::runtime_error("Teapot mesh does not exist in teapot.gltf?");
}
// Duplicate out a lot of unique nodes so that we can render the teapot many times.
for (int y = -20; y <= 20; y += 5)
{
for (int x = -40; x <= 40; x += 5)
{
// We already have this teapot.
if (x == -40 && y == -20)
{
continue;
}
auto node = std::make_unique<vkb::sg::Node>(-1, "Teapot");
node->set_component(*teapot_mesh);
teapot_mesh->add_node(*node);
auto &transform = node->get_component<vkb::sg::Transform>();
transform.set_scale(glm::vec3(1.0f));
transform.set_translation(glm::vec3(x, y, 0.0f));
transform.set_rotation(glm::quat(1.0f, 0.0f, 0.0f, 0.0f));
Transform teapot;
teapot.transform = &transform;
teapot.axis = get_random_axis();
teapot.angular_frequency = get_random_angular_freq();
teapot_transforms.push_back(teapot);
root_node.add_child(*node);
get_scene().add_node(std::move(node));
}
}
}
void KHR16BitStorageInputOutputSample::update_pipeline()
{
const char *vertex_path;
const char *fragment_path;
const std::string base_path = "16bit_storage_input_output/";
if (khr_16bit_storage_input_output_enabled && supports_16bit_storage)
{
vertex_path = "16bit_storage_input_output_enabled.vert.spv";
fragment_path = "16bit_storage_input_output_enabled.frag.spv";
}
else
{
vertex_path = "16bit_storage_input_output_disabled.vert.spv";
fragment_path = "16bit_storage_input_output_disabled.frag.spv";
}
vkb::ShaderSource vert_shader(base_path + vertex_path);
vkb::ShaderSource frag_shader(base_path + fragment_path);
auto scene_subpass = 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_subpass));
set_render_pipeline(std::move(render_pipeline));
}
bool KHR16BitStorageInputOutputSample::prepare(const vkb::ApplicationOptions &options)
{
if (!VulkanSample::prepare(options))
{
return false;
}
setup_scene();
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>();
auto &camera_transform = camera->get_node()->get_component<vkb::sg::Transform>();
camera_transform.set_translation(glm::vec3(0.0f, 0.0f, 60.0f));
camera_transform.set_rotation(glm::quat(1.0f, 0.0f, 0.0f, 0.0f));
update_pipeline();
get_stats().request_stats({vkb::StatIndex::gpu_ext_read_bytes, vkb::StatIndex::gpu_ext_write_bytes});
create_gui(*window, &get_stats());
return true;
}
void KHR16BitStorageInputOutputSample::request_gpu_features(vkb::PhysicalDevice &gpu)
{
REQUEST_REQUIRED_FEATURE(gpu, VkPhysicalDevice16BitStorageFeatures, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES, storageInputOutput16);
}
void KHR16BitStorageInputOutputSample::update(float delta_time)
{
if (khr_16bit_storage_input_output_enabled != khr_16bit_storage_input_output_last_enabled)
{
update_pipeline();
khr_16bit_storage_input_output_last_enabled = khr_16bit_storage_input_output_enabled;
}
float angular_freq = 1.0f;
for (auto &teapot : teapot_transforms)
{
glm::quat rotation = teapot.transform->get_rotation();
rotation = glm::normalize(glm::angleAxis(teapot.angular_frequency * delta_time, teapot.axis) * rotation);
teapot.transform->set_rotation(rotation);
angular_freq += 0.05f;
}
VulkanSample::update(delta_time);
}
void KHR16BitStorageInputOutputSample::draw_gui()
{
const char *label;
if (supports_16bit_storage)
{
label = "Enable 16-bit InputOutput";
}
else
{
label = "Enable 16-bit InputOutput (noop - unsupported by device)";
}
get_gui().show_options_window(
/* body = */ [this, label]() {
ImGui::Checkbox(label, &khr_16bit_storage_input_output_enabled);
},
/* lines = */ 1);
}
void KHR16BitStorageInputOutputSample::recreate_swapchain()
{
std::set<VkImageUsageFlagBits> image_usage_flags = {VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT};
get_device().wait_idle();
get_render_context().update_swapchain(image_usage_flags);
}
std::unique_ptr<vkb::VulkanSampleC> create_16bit_storage_input_output()
{
return std::make_unique<KHR16BitStorageInputOutputSample>();
}
@@ -0,0 +1,63 @@
/* Copyright (c) 2020-2024, Arm Limited and Contributors
*
* 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.
*/
#pragma once
#include "scene_graph/components/camera.h"
#include "vulkan_sample.h"
/**
* @brief Using 16-bit storage features to reduce bandwidth for input-output data between vertex and fragment shaders.
*/
class KHR16BitStorageInputOutputSample : public vkb::VulkanSampleC
{
public:
KHR16BitStorageInputOutputSample();
virtual ~KHR16BitStorageInputOutputSample() = default;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void update(float delta_time) override;
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
private:
vkb::sg::Camera *camera{nullptr};
virtual void draw_gui() override;
void recreate_swapchain();
bool khr_16bit_storage_input_output_last_enabled{false};
bool khr_16bit_storage_input_output_enabled{false};
void setup_scene();
struct Transform
{
vkb::sg::Transform *transform;
glm::vec3 axis;
float angular_frequency;
};
std::vector<Transform> teapot_transforms;
void update_pipeline();
bool supports_16bit_storage{false};
};
std::unique_ptr<vkb::VulkanSampleC> create_16bit_storage_input_output();
@@ -0,0 +1,34 @@
# Copyright (c) 2020-2021, Arm Limited and Contributors
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample_with_tags(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Hans-Kristian Arntzen"
NAME "16-bit Storage InputOutput"
DESCRIPTION "Using VK_KHR_16bit_storage InputOutput feature to reduce bandwidth for varying data."
TAGS
"arm"
SHADER_FILES_GLSL
"16bit_storage_input_output/16bit_storage_input_output_enabled.vert"
"16bit_storage_input_output/16bit_storage_input_output_enabled.frag"
"16bit_storage_input_output/16bit_storage_input_output_disabled.vert"
"16bit_storage_input_output/16bit_storage_input_output_disabled.frag")
@@ -0,0 +1,214 @@
////
- Copyright (c) 2020-2023, Arm Limited and Contributors
-
- 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 16-bit storage InputOutput feature to reduce bandwidth on tile-based architectures
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/16bit_storage_input_output[Khronos Vulkan samples github repository].
endif::[]
== Overview
On some tile-based deferred renderers (TBDR), such as Arm Mali GPUs, vertex shading and fragment shading is split into two distinct stages.
Vertex shaders and the general geometry pipeline will write to memory for each visible vertex:
* Position (`gl_Position`)
* Vertex output data (`layout(location = N) out`).
* GPU specific data structures to accelerate rasterization later.
Then, in fragment stage, the positions and GPU specific data structures are read to rasterize primitives, and for each fragment we invoke, the vertex output data can be read by the fragment shader as input variables.
All of this consumes bandwidth.
As vertex output data is something the application controls (and tends to consume many bytes per vertex), we should attempt to minimize the bandwidth we consume.
This samples demonstrates real bandwidth saving achieved with a simple sample.
== Enabling the 16-bit storage extension
`VK_KHR_16bit_storage` is an extension which supports many different features, but this sample in particular will focus on the `storageInputOutput16` feature.
The `VK_KHR_16bit_storage` is core in Vulkan 1.1, so an extension is not required, but the feature you plan to use must still be enabled.
First, add `VkPhysicalDevice16BitStorageFeatures` as a `pNext` to `vkGetPhysicalDeviceFeatures2`.
Now, you can check which features are supported:
* `storageBuffer16BitAccess`: Allows you to use true 16-bit values in SSBOs.
* `uniformAndStorageBuffer16BitAccess`: Allows you to use true 16-bit values in UBOs as well.
* `storagePushConstant16`: Allows you to use true 16-bit values in push constant blocks (quite handy since push constant space is so tight!)
* `storageInputOutput16`: A TBDR friendly feature, allows you to use true 16-bit values in shader input and output interfaces.
To use the feature, make sure `VkPhysicalDevice16BitStorageFeatures` is passed down to ``vkCreateDevice``'s `pNext` chain.
=== Difference between storage and arithmetic support
There's two aspects to 16-bit support, storage and arithmetic.
With storage support, the main goal is to reduce bandwidth and memory requirements.
With 16-bit arithmetic, we will be able to improve arithmetic throughput.
This sample focuses on storage only, so the sample takes care not to enable any 16-bit arithmetic features.
If a device only supports 16-bit storage, but not arithmetic, we need to be somewhat careful when writing shaders.
The native 16-bit types can only be used when storing to the variable, or when loading from the variable.
[,glsl]
----
// GL_EXT_16bit_storage would normally be used here, but that extension does not support input/output.
// glslang enables SPIR-V capabilities as required.
#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require
layout(location = 0) out f16vec4 Foo;
vec4 arithmetic = blah(); // Arithmetic happens in FP32.
Foo = f16vec4(arithmetic); // Narrowing store.
----
[,glsl]
----
// GL_EXT_16bit_storage would normally be used here, but that extension does not support input/output.
// glslang enables SPIR-V capabilities as required.
#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require
layout(location = 0) in f16vec4 Foo;
vec4 arithmetic = vec4(Foo); // Expand fp16 -> fp32.
arithmetic += blah();
----
In SPIR-V, you can verify what is going on by looking at the enabled capabilities:
----
OpCapability Shader
OpCapability StorageInputOutput16 ; <-- Only needs VK_KHR_16bit_storage
%51 = OpLoad %v4float %color
%52 = OpFConvert %v4half %51 <-- OpFConvert followed by OpStore just requires storage.
; Similarly, OpLoad followed by OpFConvert just requires Storage feature.
OpStore %o_color %52
----
For example, if you use 16-bit arithmetic, you will see:
----
OpCapability Shader
OpCapability Float16 ; <-- Arithmetic support, needs a different extension.
OpCapability StorageInputOutput16
%55 = OpFAdd %v4half %52 %54 <-- This is arithmetic in native FP16
OpStore %o_color %55
----
The validation layers will catch this if you forget to enable the extensions and features.
== The 16-bit InputOutput storage sample
In the sample, we render a large number of teapot meshes (17 * 9 = 153), each with 9128 triangles and 4689 unique vertices.
The teapots are not textured, and there is no post-processing happening, meaning that geometry bandwidth is the key contributor to overall bandwidth.
As mentioned above, a significant contributor to geometry bandwidth on TBDR is the storage for shader input and output data.
Vertex shaders which write to `layout(location = N) out T variable;` consume bandwidth.
Similarly, fragment shaders which read `layout(location = N) in T variable;` also consume bandwidth.
This sample demonstrates that there can be significant savings in global device bandwidth by ensuring that the types used in vertex output and fragment input interfaces are as narrow as possible.
In the first scenario, the vertex shader has 3 vec3 outputs, which is not a lot when considering modern rendering engines.
[,glsl]
----
layout (location = 0) out vec3 o_vertex_color;
layout (location = 1) out vec3 o_normal;
layout (location = 2) out vec3 o_delta_pos;
----
For each unique vertex that is visible on screen, we must assume 52 bytes of memory bandwidth is consumed, 16 bytes for `gl_Position` and 36 bytes for vertex outputs.
The shading itself is somewhat contrived, it is mostly there to show something being rendered.
image::./images/fp16_input_output_disable.jpg[16bit-storage InputOutput off]
In the unoptimized case, we observe ~1.67 GB/s write bandwidth and ~1.16 GB/s read bandwidth on the specific device we tested (see screenshot).
We can estimate what is going on here using intuition.
We are rendering 153 teapots with 4689 vertices.
717417 unique vertices will be vertex shaded.
On average, half the vertices are actually visible due to back-face culling (358k).
VSync is approximately 60 FPS on the test device in the screenshots, so ~21.5M unique vertices / s will need to be written to memory.
For each vertex, we have 16 bytes for `gl_Position` and 36 bytes for output variables, meaning this data accounts for at least ~1.11 GB/s here.
That's about 2/3 of the total write bandwidth we are measuring here.
image::./images/fp16_input_output_enable.jpg[16bit-storage InputOutput on]
Here, we simply replace the vertex outputs and fragment inputs to be fp16.
This should theoretically save 18 bytes per vertex, or ~21.5 M/s * 18 B = 387 MB / s of pure write bandwidth, and that seems to be almost spot-on with observed results.
Similarly, read bandwidth is significantly reduced as well.
== Alternative implementation: `mediump`
Marking vertex output variables as `mediump` will generally allow us to achieve the same bandwidth savings as explicit FP16 would, but the caveat is that you cannot be sure unless you know the driver implementation details.
The snippet below will work on any core Vulkan 1.0 implementation, but it may or may not give you true FP16 vertex outputs:
[,glsl]
----
// Vertex
layout(location = 0) out mediump vec3 o_normal;
// Fragment
layout(location = 0) in mediump vec3 in_normal;
----
== Considering precision
FP16 is not very accurate, and you most likely cannot use FP16 for every vertex output in your application.
Things which work well with FP16 precision:
* Normals / Tangent / Bi-tangent
* Vertex colors (if you're still using those)
* Any auxillary data which is centered around 0 and doesn't need exceptionally high precision.
Use cases which may, or may not work well with FP16 precision:
* Local world position.
Precision can be significantly improved if you use `delta_pos = f16vec3(world_position - camera_position);`.
This centers the value around 0, and has the nice property that the closer you get to the camera, the better precision you get.
Far away from the camera precision is less of a concern anyways.
On a mobile screen, the precision errors might not be perceptible.
* Texture coordinates with smaller texture resolutions and constrained UV range.
If UVs can be kept between `[-1, 1]`, we have reasonable resolution in FP16, but might not be enough.
Things which almost certainly won't work:
* Global world position.
* UI texture coordinates.
== Best practice summary
*Do*
* Use FP16 vertex outputs when possible and meets quality requirements.
* If you cannot rely on VK_KHR_16bit_storage being available, at the very least use `mediump` in lieu of true FP16.
*Don't*
* Ignore bandwidth benefits of FP16 vertex outputs.
Even if 60 FPS is met, battery life can be extended by saving bandwidth.
*Impact*
* Not using FP16 vertex outputs where you can will waste bandwidth on TBDR renderers, leading to increased power consumption.
*Debugging*
* To observe the impact of any change to vertex output precision, use a profiler such as Streamline to observe external write or read bandwidth.
Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

+163
View File
@@ -0,0 +1,163 @@
////
- Copyright (c) 2021-2024, The Khronos Group
-
- 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.
-
////
ifndef::performance_samplespath[:performance_samplespath:]
== Performance samples
The goal of these samples is to demonstrate how to use certain features and functions to achieve optimal performance.
To visualize this, they also include real-time profiling information.
=== xref:./{performance_samplespath}afbc/README.adoc[AFBC]
AFBC (Arm Frame Buffer Compression) is a real-time lossless compression algorithm found in Arm Mali GPUs, designed to tackle the ever-growing demand for higher resolution graphics.
This format is applied to the framebuffers that are to be written to the GPU.
This technology can offer bandwidth reductions of https://developer.arm.com/Architectures/Arm%20Frame%20Buffer%20Compression[up to 50%].
=== xref:./{performance_samplespath}command_buffer_usage/README.adoc[Command buffer usage]
This sample demonstrates how to use and manage secondary command buffers, and how to record them concurrently.
Implementing multi-threaded recording of draw calls can help reduce CPU frame time.
=== xref:./{performance_samplespath}constant_data/README.adoc[Constant data]
The Vulkan API exposes a few different ways in which we can send uniform data into our shaders.
There are enough methods that it raises the question "Which one is fastest?", and more often than not the answer is "It depends".
The main issue for developers is that the fastest methods may differ between the various vendors, so often there is no "one size fits all" solution.
This sample aims to highlight this issue, and help move the Vulkan ecosystem to a point where we are better equipped to solve this for developers.
This is done by having an interactive way to toggle different constant data methods that the Vulkan API expose to us.
This can then be run on a platform of the developers choice to see the performance implications that each of them bring.
=== xref:./{performance_samplespath}descriptor_management/README.adoc[Descriptor management]
An application using Vulkan will have to implement a system to manage descriptor pools and sets.
The most straightforward and flexible approach is to re-create them for each frame, but doing so might be very inefficient, especially on mobile platforms.
The problem of descriptor management is intertwined with that of buffer management, that is choosing how to pack data in `VkBuffer` objects.
This sample will explore a few options to improve both descriptor and buffer management.
=== xref:./{performance_samplespath}hpp_pipeline_cache/README.adoc[HPP Pipeline cache]
A transcoded version of the Performance sample xref:./{performance_samplespath}pipeline_cache/README.adoc[Pipeline cache] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp.
=== xref:./{performance_samplespath}hpp_swapchain_images/README.adoc[HPP Swapchain images]
A transcoded version of the Performance samplexref:./{performance_samplespath}swapchain_images/README.adoc[Swapchain images] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp.
=== xref:./{performance_samplespath}hpp_texture_compression_comparison/README.adoc[HPP Texture compression comparison]
A transcoded version of the Performance sample xref:./{performance_samplespath}texture_compression_comparison/README.adoc[Texture compression comparison] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp.
=== xref:./{performance_samplespath}image_compression_control/README.adoc[Image compression control]
This sample shows how to use the extensions https://docs.vulkan.org/spec/latest/appendices/extensions.html#VK_EXT_image_compression_control[`VK_EXT_image_compression_control`] and https://docs.vulkan.org/spec/latest/appendices/extensions.html#VK_EXT_image_compression_control_swapchain[`VK_EXT_image_compression_control_swapchain`] to select between different levels of image compression.
The UI shows the impact compression has on image size and bandwidth, illustrating the benefits of fixed-rate (visually lossless) compression.
=== xref:./{performance_samplespath}layout_transitions/README.adoc[Layout transitions]
Vulkan requires the application to manage image layouts, so that all render pass attachments are in the correct layout when the render pass begins.
This is usually done using pipeline barriers or the `initialLayout` and `finalLayout` parameters of the render pass.
If the rendering pipeline is complex, transitioning each image to its correct layout is not trivial, as it requires some sort of state tracking.
If previous image contents are not needed, there is an easy way out, that is setting `oldLayout`/`initialLayout` to `VK_IMAGE_LAYOUT_UNDEFINED`.
While this is functionally correct, it can have performance implications as it may prevent the GPU from performing some optimizations.
This sample will cover an example of such optimizations and how to avoid the performance overhead from using sub-optimal layouts.
=== xref:./{performance_samplespath}msaa/README.adoc[MSAA]
Aliasing is the result of under-sampling a signal.
In graphics this means computing the color of a pixel at a resolution that results in artifacts, commonly jaggies at model edges.
Multisample anti-aliasing (MSAA) is an efficient technique that reduces pixel sampling error.
=== xref:./{performance_samplespath}multithreading_render_passes/README.adoc[Multi-threaded recording with multiple render passes]
Ideally you render all stages of your frame in a single render pass.
However, in some cases different stages can't be performed in the same render pass.
This sample shows how multi-threading can help to boost performance when using multiple render passes to render a single frame.
=== xref:./{performance_samplespath}pipeline_barriers/README.adoc[Pipeline barriers]
Vulkan gives the application significant control over memory access for resources.
Pipeline barriers are particularly convenient for synchronizing memory accesses between render passes.
Having barriers is required whenever there is a memory dependency - the application should not assume that render passes are executed in order.
However, having too many or too strict barriers can affect the application's performance.
This sample will cover how to set up pipeline barriers efficiently, with a focus on pipeline stages.
=== xref:./{performance_samplespath}pipeline_cache/README.adoc[Pipeline cache]
Vulkan gives applications the ability to save internal representation of a pipeline (graphics or compute) to enable recreating the same pipeline later.
This sample will look in detail at the implementation and performance implications of the pipeline creation, caching and management.
=== xref:./{performance_samplespath}render_passes/README.adoc[Render passes]
Vulkan render-passes use attachments to describe input and output render targets.
This sample shows how loading and storing attachments might affect performance on mobile.
During the creation of a render-pass, you can specify various color attachments and a depth-stencil attachment.
Each of those is described by a https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkAttachmentDescription.html[`VkAttachmentDescription`] struct, which contains attributes to specify the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkAttachmentLoadOp.html[load operation] (`loadOp`) and the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkAttachmentStoreOp.html[store operation] (`storeOp`).
This sample lets you choose between different combinations of these operations at runtime.
=== xref:./{performance_samplespath}specialization_constants/README.adoc[Specialization constants]
Vulkan exposes a number of methods for setting values within shader code during run-time, this includes UBOs and Specialization Constants.
This sample compares these two methods and the performance impact of them.
=== xref:./{performance_samplespath}subpasses/README.adoc[Sub passes]
Vulkan introduces the concept of _subpasses_ to subdivide a single xref:./{performance_samplespath}render_passes/README.adoc[render pass] into separate logical phases.
The benefit of using subpasses over multiple render passes is that a GPU is able to perform various optimizations.
Tile-based renderers, for example, can take advantage of tile memory, which being on chip is decisively faster than external memory, potentially saving a considerable amount of bandwidth.
=== xref:./{performance_samplespath}surface_rotation/README.adoc[Surface rotation]
Mobile devices can be rotated, therefore the logical orientation of the application window and the physical orientation of the display may not match.
Applications then need to be able to operate in two modes: portrait and landscape.
The difference between these two modes can be simplified to just a change in resolution.
However, some display subsystems always work on the "native" (or "physical") orientation of the display panel.
Since the device has been rotated, to achieve the desired effect the application output must also rotate.
In this sample we focus on the rotation step, and analyze the performance implications of implementing it correctly with Vulkan.
=== xref:./{performance_samplespath}swapchain_images/README.adoc[Swapchain images]
Vulkan gives the application some significant control over the number of swapchain images to be created.
This sample analyzes the available options and their performance implications.
=== xref:./{performance_samplespath}wait_idle/README.adoc[Wait idle]
This sample compares two methods for synchronizing between the CPU and GPU, `WaitIdle` and `Fences` demonstrating which one is the best option in order to avoid stalling.
=== xref:./{performance_samplespath}16bit_storage_input_output/README.adoc[16-bit storage InputOutput]
This sample compares bandwidth consumption when using FP32 varyings compared to using FP16 varyings with `VK_KHR_16bit_storage`.
=== xref:./{performance_samplespath}16bit_arithmetic/README.adoc[16-bit arithmetic]
This sample compares arithmetic throughput for 32-bit arithmetic operations and 16-bit arithmetic.
The sample also shows how to enable 16-bit storage for SSBOs and push constants.
=== xref:./{performance_samplespath}async_compute/README.adoc[Async compute]
This sample demonstrates using multiple Vulkan queues to get better hardware utilization with compute post-processing workloads.
=== xref:./{performance_samplespath}texture_compression_basisu/README.adoc[Basis Universal supercompressed GPU textures]
This sample demonstrates how to use Basis universal supercompressed GPU textures in a Vulkan application.
=== xref:./{performance_samplespath}multi_draw_indirect/README.adoc[GPU Rendering and Multi-Draw Indirect]
This sample demonstrates how to reduce CPU usage by offloading draw call generation and frustum culling to the GPU.
=== xref:./{performance_samplespath}texture_compression_comparison/README.adoc[Texture compression comparison]
This sample demonstrates how to use different types of compressed GPU textures in a Vulkan application, and shows the timing benefits of each.
+32
View File
@@ -0,0 +1,32 @@
# Copyright (c) 2019-2021, Arm Limited and Contributors
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample_with_tags(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Arm"
NAME "AFBC"
DESCRIPTION "Using framebuffer compression to reduce bandwidth."
TAGS
"arm"
SHADER_FILES_GLSL
"base.vert"
"base.frag")
+163
View File
@@ -0,0 +1,163 @@
////
- Copyright (c) 2019-2024, Arm Limited and Contributors
-
- 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.
-
////
= Enabling AFBC in your Vulkan Application
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/afbc[Khronos Vulkan samples github repository].
endif::[]
== Overview
AFBC (Arm Frame Buffer Compression) is a real-time lossless compression algorithm found in Arm Mali GPUs, designed to tackle the ever-growing demand for higher resolution graphics.
This format is applied to the framebuffers that are to be written to the GPU.
This technology can offer bandwidth reductions of https://developer.arm.com/technologies/graphics-technologies/arm-frame-buffer-compression[up to 50%].
The sample is geared towards demonstrating the bandwidth that you can save by toggling AFBC on and off and displaying a real-time graph of the external bandwidth.
In this case we will be focusing on the swapchain images.
The Vulkan API allows the developer a level of control around how the `VkSwapchainKHR` is created and formatted.
It is here where we want to ensure that it is created and formatted in the right way so that the subsequent ``VkImage``'s that we query from it have AFBC appropriately applied.
It is important to note that from a device perspective to have AFBC enabled on Vulkan, you will need at least driver version `r16p0` and a `Mali G-51` or higher.
To find out your GPU and driver version, open the link:../../../docs/misc.adoc#Debug-Window[debug window] or follow the steps in this link:../../../docs/misc.adoc#Driver-Version[article].
____
Tested on: Samsung Galaxy S10, Huawei P30
____
== Enabling AFBC
AFBC is functionally transparent to the application and will be automatically applied on a per `VkImage` basis (provided multiple checks pass on various properties of your device and your images).
The driver will check the applications state along with the `VkImage` properties to determine if it will enable AFBC or just continue without it.
This section will detail the requirements.
`VkImage` requirements:
* `VkSampleCountFlagBits` must be `VK_SAMPLE_COUNT_1_BIT`
* `VkImageType` must be `VK_IMAGE_TYPE_2D`
* `VkImageTiling` must be `VK_IMAGE_TILING_OPTIMAL`
* `VkFormat` <<format-support,supported list>>
In addition to this, your `VkImage` needs to adhere to the following flags:
* `VkImageUsageFlags` must not contain:
** `VK_IMAGE_USAGE_STORAGE_BIT`
** `VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT`
** (Only for some devices with driver version `r16p0`) `VK_IMAGE_USAGE_TRANSFER_DST_BIT`
* `VkImageCreateFlags` must not contain:
** `VK_IMAGE_CREATE_ALIAS_BIT`
** `VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT`
== The AFBC Sample
The sample presents the user with Sponza, with a graph displaying bandwidth at the top and a configuration window at the bottom.
There is a back-and-forth oscillating camera to disable any GPU optimisations for frames that are identical.
The configuration itself is simple.
There is one checkbox (labelled "Enable AFBC") that will reload the swapchain when its value is changed.
image::./images/afbc_disabled.jpg[Sponza AFBC off]
Here the sample is in its default state: AFBC off.
At the top of the screen there is a graph displaying the external write bandwidth (measured from `L2_EXT_WRITE_BEATS`).
It is currently setting the `VK_IMAGE_USAGE_STORAGE_BIT` flag in the `VkImageUsageFlags` for the swapchain images, causing the driver to skip over applying AFBC to the swapchain images.
When we enable the check box, the sample will reload the swapchain with the right usage flags to have the driver enable AFBC.
image::./images/afbc_enabled.jpg[Sponza AFBC on]
Here is the same scene as before, except AFBC is now enabled.
The `VK_IMAGE_USAGE_STORAGE_BIT` flag is not being set and the swapchain is being created properly.
The bandwidth has dropped from 788.0 MiB/s to 528.6 MiB/s, this is approximately a 33% reduction.
You can also confirm these numbers in Streamline.
Here are some screenshots:
image:./images/streamline_disabled.png[Streamline AFBC Off] image:./images/streamline_enabled.png[Streamline AFBC On]
== Format Support
GPUs from Mali-G77 onwards support formats up to and including 32 bits per pixel regardless of color channel arrangement or sRBG.
Previous generations that support AFBC only support a subset of formats:
[cols=^]
|===
| Formats
| VK_FORMAT_R4G4B4A4_UNORM_PACK16
| VK_FORMAT_B4G4R4A4_UNORM_PACK16
| VK_FORMAT_R5G6B5_UNORM_PACK16
| VK_FORMAT_R5G5B5A1_UNORM_PACK16
| VK_FORMAT_B5G5R5A1_UNORM_PACK16
| VK_FORMAT_A1R5G5B5_UNORM_PACK16
| VK_FORMAT_B8G8R8_UNORM
| VK_FORMAT_B8G8R8A8_UNORM
| VK_FORMAT_B8G8R8A8_SRGB
| VK_FORMAT_A8B8G8R8_UNORM
| VK_FORMAT_A8B8G8R8_SRGB
| VK_FORMAT_A8R8G8B8_SRGB
| VK_FORMAT_B10G10R10A2_UNORM
| VK_FORMAT_R4G4B4A4_UNORM
| VK_FORMAT_R5G6B5_UNORM
| VK_FORMAT_R5G5B5A1_UNORM
| VK_FORMAT_R8_UNORM
| VK_FORMAT_R8G8_UNORM
| VK_FORMAT_R8G8B8_UNORM
| VK_FORMAT_R8G8B8A8_UNORM
| VK_FORMAT_R8G8B8A8_SRGB
| VK_FORMAT_A8R8G8B8_UNORM
| VK_FORMAT_R10G10B10A2_UNORM
| VK_FORMAT_D24_UNORM_S8_UINT
| VK_FORMAT_D16_UNORM
| VK_FORMAT_D32_SFLOAT
|===
== Further Reading
* https://www.arm.com/why-arm/technologies/graphics-technologies/arm-frame-buffer-compression[Arm Frame Buffer Compression] - developer.arm.com
== Best practice summary
*Do*
* Ensure that your swapchain is created correctly as per the requirements of AFBC.
* AFBC is not a replacement for (it is independent of) texture compression like ASTC, so always compress all of the things when possible.
* Avoid changing your image configuration at run-time (using `vkCmdCopyImage` with an invalid AFBC requirement) as it will trigger a decompression.
* Make sure you are resolving your images using `pResolveAttachments` when it comes to multisampling.
Any `VkImage` with `SAMPLE_COUNT > 1` will not have AFBC applied to it.
*Don't*
* Use image usage flags, such as `VK_IMAGE_USAGE_STORAGE_BIT`, unless you really need it (i.e.
for compute on a specific image).
*Impact*
* Having an incorrect configuration of your images will cause all your surface ``VkImage``'s to be uncompressed, losing out on considerable system wide bandwidth reductions.
*Debugging*
* To test if AFBC is enabled or disabled, you can use a profiler such as Streamline and record the bandwidth values when AFBC is enabled or when AFBC is disabled.
* You may also use the extension https://docs.vulkan.org/spec/latest/appendices/extensions.html#VK_EXT_image_compression_control[`VK_EXT_image_compression_control`] to query if AFBC is enabled.
See the xref:/samples/performance/image_compression_control/README.adoc[Image Compression Control sample] for more details.
+136
View File
@@ -0,0 +1,136 @@
/* Copyright (c) 2019-2025, Arm Limited and Contributors
*
* 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 "afbc.h"
#include "common/vk_common.h"
#include "filesystem/legacy.h"
#include "gltf_loader.h"
#include "gui.h"
#include "rendering/subpasses/forward_subpass.h"
#include "stats/stats.h"
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
# include "platform/android/android_platform.h"
#endif
#include "scene_graph/components/camera.h"
#include "scene_graph/components/transform.h"
#include "scene_graph/node.h"
AFBCSample::AFBCSample()
{
// Extension that may be used to query if AFBC is enabled
add_device_extension(VK_EXT_IMAGE_COMPRESSION_CONTROL_EXTENSION_NAME, true);
auto &config = get_configuration();
config.insert<vkb::BoolSetting>(0, afbc_enabled, false);
config.insert<vkb::BoolSetting>(1, afbc_enabled, true);
}
bool AFBCSample::prepare(const vkb::ApplicationOptions &options)
{
if (!VulkanSample::prepare(options))
{
return false;
}
// We want AFBC disabled at start-up
afbc_enabled = false;
recreate_swapchain();
load_scene("scenes/sponza/Sponza01.gltf");
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>();
vkb::ShaderSource vert_shader("base.vert.spv");
vkb::ShaderSource frag_shader("base.frag.spv");
auto scene_subpass = 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_subpass));
set_render_pipeline(std::move(render_pipeline));
get_stats().request_stats({vkb::StatIndex::gpu_ext_write_bytes});
create_gui(*window, &get_stats());
// Store the start time to calculate rotation
start_time = std::chrono::system_clock::now();
return true;
}
void AFBCSample::update(float delta_time)
{
if (afbc_enabled != afbc_enabled_last_value)
{
recreate_swapchain();
afbc_enabled_last_value = afbc_enabled;
}
/* Pan the camera back and forth. */
auto &camera_transform = camera->get_node()->get_component<vkb::sg::Transform>();
float rotation_factor = std::chrono::duration<float>(std::chrono::system_clock::now() - start_time).count();
glm::quat qy = glm::angleAxis(0.003f * sin(rotation_factor * 0.7f), glm::vec3(0.0f, 1.0f, 0.0f));
glm::quat orientation = glm::normalize(qy * camera_transform.get_rotation() * glm::angleAxis(0.0f, glm::vec3(1.0f, 0.0f, 0.0f)));
camera_transform.set_rotation(orientation);
VulkanSample::update(delta_time);
}
void AFBCSample::recreate_swapchain()
{
std::set<VkImageUsageFlagBits> image_usage_flags = {VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT};
if (!afbc_enabled)
{
// To force-disable AFBC, set an invalid image usage flag
image_usage_flags.insert(VK_IMAGE_USAGE_STORAGE_BIT);
}
get_device().wait_idle();
get_render_context().update_swapchain(image_usage_flags);
}
void AFBCSample::draw_gui()
{
get_gui().show_options_window(
/* body = */ [this]() {
ImGui::Checkbox("Enable AFBC", &afbc_enabled);
if (get_device().is_extension_enabled(VK_EXT_IMAGE_COMPRESSION_CONTROL_EXTENSION_NAME))
{
ImGui::SameLine();
ImGui::Text("(%s)", vkb::image_compression_flags_to_string(get_render_context().get_swapchain().get_applied_compression()).c_str());
}
},
/* lines = */ 1);
}
std::unique_ptr<vkb::VulkanSampleC> create_afbc()
{
return std::make_unique<AFBCSample>();
}
+53
View File
@@ -0,0 +1,53 @@
/* Copyright (c) 2019-2024, Arm Limited and Contributors
*
* 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.
*/
#pragma once
#include "rendering/render_pipeline.h"
#include "scene_graph/components/camera.h"
#include "timer.h"
#include "vulkan_sample.h"
/**
* @brief Using framebuffer compression to reduce bandwidth
*/
class AFBCSample : public vkb::VulkanSampleC
{
public:
AFBCSample();
virtual ~AFBCSample() = default;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void update(float delta_time) override;
private:
vkb::sg::Camera *camera{nullptr};
virtual void draw_gui() override;
void recreate_swapchain();
bool afbc_enabled_last_value{false};
bool afbc_enabled{false};
std::chrono::system_clock::time_point start_time;
};
std::unique_ptr<vkb::VulkanSampleC> create_afbc();
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@@ -0,0 +1,37 @@
# Copyright (c) 2021-2025, Arm Limited and Contributors
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample_with_tags(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Hans-Kristian Arntzen"
NAME "Async compute"
DESCRIPTION "Using multiple queues to achieve more parallelism on the GPU."
SHADER_FILES_GLSL
"async_compute/blur_down.comp"
"async_compute/blur_up.comp"
"async_compute/forward.vert"
"async_compute/forward.frag"
"async_compute/shadow.vert"
"async_compute/shadow.frag"
"async_compute/composite.vert"
"async_compute/composite.frag"
"async_compute/threshold.comp")
@@ -0,0 +1,167 @@
////
- Copyright (c) 2021-2023, Arm Limited and Contributors
-
- 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 async compute to saturate GPU
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/async_compute[Khronos Vulkan samples github repository].
endif::[]
== Overview
Most Vulkan implementations expose multiple Vulkan queues which the application can make use of at any one time.
The main motivation for hardware to expose multiple queues is that we can express parallelism at a higher level than threads.
== Compute all the things - a post processing case study
Compute shaders are increasingly being employed to do "everything" except for main pass rasterization in modern game engines.
This sample aims to demonstrate some techniques we can use to get optimal behavior on tile based renderers in particular.
As we will discuss later, the strategy for immediate mode renderers are somewhat different due to architectural differences.
== The challenge of compute shader post processing on tile-based deferred renderers (TBDR)
The TBDR architecture splits vertex shading and fragment shading in two.
First, vertices and shaded and binned, and once all of that is done, fragment shading happens.
A critical performance win is that vertex shading in render pass N + {1, 2, ...} can overlap fragment shading in render pass N.
When targeting optimal performance on these GPUs, we must ensure to never stall fragment shading.
Due to this kind of rendering architecture, there should be at least two hardware queues on these GPUs.
At least on Arm Mali GPUs, compute workloads run in the same queue as vertex shading and binning.
This is intuitive since vertex shading is basically the same as compute shading with some extra fixed function magic.
Compute shader post processing becomes problematic in this kind of frame:
* Rasterize pass
* FRAGMENT \-> COMPUTE semaphore
* Compute pass
* COMPUTE \-> FRAGMENT semaphore *(Perf cliff!
:<)*
* Render UI + post output
* Present in graphics
It is natural to end a frame in the graphics queue for two reasons:
* We really want to render UI in the same render pass where we render to swapchain.
Bandwidth is very important, and this way we avoid a writeback and readback of the native-resolution UI image which can be rather large.
* Rendering UI inline in one compute thread is theoretically possible (VK_EXT_descriptor_indexing can certainly help!) but an extremely complex thing to hack in.
The real problem is the COMPUTE \-> FRAGMENT semaphore here.
Due to the FRAGMENT \-> COMPUTE and COMPUTE \-> FRAGMENT barriers we have effectively blocked FRAGMENT from doing any work while COMPUTE is running.
As mentioned earlier, this is a performance problem on TBDR.
=== A note on compute post-processing on TBDR vs immediate mode (IMR) desktop GPUs
A more desktop-style approach here is to present from async compute, i.e., do *everything* in compute, and try to go with this approach instead:
* Rasterize pass
* FRAGMENT \-> COMPUTE semaphore
* Compute post
* Render UI in graphics queue (bandwidth hit, but we don't really care here)
* FRAGMENT \-> COMPUTE semaphore
* Composite final result in a compute shader
* Present in compute
Presenting from async compute is a different topic and is not covered by this sample, but it's something to keep in mind.
=== Using multiple graphics queues to pop the bubble
Some GPUs expose multiple graphics queues in Vulkan.
This is very handy since we can fix the barrier problem this way.
Assume that we have 2 VkQueues which support everything, and we can render the frame like this instead:
* Rasterize pass (Queue #1)
* FRAGMENT (Queue #1) \-> COMPUTE (Queue #0) semaphore
* Compute pass (Queue #0)
* COMPUTE (Queue #0) \-> FRAGMENT (Queue #0) semaphore *(No perf OOF!
:>)*
* Render UI + post output (Queue #0)
* Present in graphics (Queue #0)
* ...
* Rasterize pass (Queue #1) is not blocked on compute, overlap achieved :>
== Queue priorities
A final cherry on top is to fiddle with queue priorities.
How queue priorities behave is implementation independent, but the intention is that it allows drivers to prioritize work in one queue over another.
In our case, we should make Queue #0 high priority and #1 low priority, since work late in the frame is more important than work happening early in next frame.
From a latency point of view, it would be ideal if Queue #0 can interrupt Queue #1.
== Reordering passes manually?
An alternative to the approach in this sample which sidesteps the issue is to defer submitting the UI + present work, and start submitting graphics work for the next frame before blocking on compute work.
This is problematic because:
* It adds complexity to juggle multiple in-flight frames.
* It adds needless input latency.
When we add more overlap between frames we also reduce responsiveness, which is very important for interactive content.
== The sample
image::./images/image.jpg[HDR sample]
The sample implements a very bare bones rendering pipeline which demonstrates a plausible rendering scenario consisting of:
* Render directional shadowmap at 8K
* Render HDR image at 4K with very basic lighting
* Very naive and simple HDR + (a very bloomy) Bloom pipeline in async compute
* Tonemap + UI in swapchain pass
The goal here is to exploit the shadow mapping pass, which is extremely bound on fixed function rasterization performance.
If we can do useful compute work in parallel, we should get a win in performance.
image::./images/noasync.jpg[No async queue]
Here we see that fragment cycles is much lower than GPU cycles.
This means the fragment queue is starved for work.
This is due to our bad barriers mentioned above.
Vertex + Fragment cycles is still > GPU cycles, which means there is some overlap, but this is only vertex shading that overlaps.
Post-process compute is starving the GPU.
image::./images/async.jpg[With async queue]
Here we can see a nice perf win (21.8 ms vs.
22.9 ms), and fragment cycles is very close to GPU cycles now, which means no starvation is happening.
Note that performance does not scale immensely here, and we shouldn't expect that either.
While vertex cycles and fragment cycles both increase, they are still competing for resources on the same shader core.
The work we do to get good overlap means the GPU always has something to do in the lull periods between barriers which drain a hardware queue for work.
=== Options
* *Enable async queues*: Uses multiple queues to avoid stalling the fragment queue.
* *Double buffer HDR*: Aims to exploit more overlap opportunities.
* *Rotate shadows*: Disables the animated light, it is hard to study performance differences when it is on since performance fluctuates a bit with it on.
== Best practice summary
These tips are somewhat TBDR specific.
*Do*
* Use multiple Vulkan queues if there is any FRAGMENT \-> COMPUTE workload happening.
* Any COMPUTE work which depends on FRAGMENT should be done in a different queue to avoid stalling FRAGMENT.
* Use higher priority on the queue which presents the final image.
*Don't*
* Introduce a FRAGMENT \-> COMPUTE barrier unless you have a plan on how to avoid the inevitable COMPUTE \-> FRAGMENT barrier.
*Debugging*
* IHV profiling tools can visualize how different hardware queues are saturated.
@@ -0,0 +1,914 @@
/* Copyright (c) 2021-2025, Arm Limited and Contributors
*
* 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 "async_compute.h"
#include "api_vulkan_sample.h"
#include "common/vk_common.h"
#include "filesystem/legacy.h"
#include "gltf_loader.h"
#include "gui.h"
#include "scene_graph/components/orthographic_camera.h"
#include "stats/stats.h"
AsyncComputeSample::AsyncComputeSample()
{
auto &config = get_configuration();
config.insert<vkb::BoolSetting>(0, async_enabled, false);
config.insert<vkb::BoolSetting>(1, async_enabled, true);
config.insert<vkb::BoolSetting>(0, rotate_shadows, false);
config.insert<vkb::BoolSetting>(1, rotate_shadows, true);
config.insert<vkb::BoolSetting>(0, double_buffer_hdr_frames, false);
config.insert<vkb::BoolSetting>(1, double_buffer_hdr_frames, true);
}
void AsyncComputeSample::request_gpu_features(vkb::PhysicalDevice &gpu)
{
#ifdef VKB_ENABLE_PORTABILITY
// Since sampler_info.compareEnable = VK_TRUE, must enable the mutableComparisonSamplers feature of VK_KHR_portability_subset
REQUEST_REQUIRED_FEATURE(
gpu, VkPhysicalDevicePortabilitySubsetFeaturesKHR, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PORTABILITY_SUBSET_FEATURES_KHR, mutableComparisonSamplers);
#endif
}
void AsyncComputeSample::draw_gui()
{
get_gui().show_options_window(
/* body = */ [this]() {
ImGui::Checkbox("Enable async queues", &async_enabled);
ImGui::Checkbox("Double buffer HDR", &double_buffer_hdr_frames);
ImGui::Checkbox("Rotate shadows", &rotate_shadows);
},
/* lines = */ 3);
}
static VkExtent3D downsample_extent(const VkExtent3D &extent, uint32_t level)
{
return {
std::max(1u, extent.width >> level),
std::max(1u, extent.height >> level),
std::max(1u, extent.depth >> level)};
}
void AsyncComputeSample::prepare_render_targets()
{
// To make this sample demanding enough to saturate the tested mobile devices, use 4K.
// Could base this off the swapchain extent, but comparing cross-device performance
// could get awkward.
VkExtent3D size = {3840, 2160, 1};
// Support double-buffered HDR.
vkb::core::Image color_targets[2]{
{get_device(), size, VK_FORMAT_R16G16B16A16_SFLOAT,
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY},
{get_device(), size, VK_FORMAT_R16G16B16A16_SFLOAT,
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY},
};
color_targets[0].set_debug_name("color_targets[0]");
color_targets[1].set_debug_name("color_targets[1]");
// Should only really need one depth target, but vkb::RenderTarget needs to own the resource.
vkb::core::Image depth_targets[2]{
{get_device(), size, VK_FORMAT_D32_SFLOAT,
VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY},
{get_device(), size, VK_FORMAT_D32_SFLOAT,
VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY},
};
depth_targets[0].set_debug_name("depth_targets[0]");
depth_targets[1].set_debug_name("depth_targets[1]");
// 8K shadow-map overkill to stress devices.
// Min-spec is 4K however, so clamp to that if required.
VkExtent3D shadow_resolution{8 * 1024, 8 * 1024, 1};
VkImageFormatProperties depth_properties{};
vkGetPhysicalDeviceImageFormatProperties(get_device().get_gpu().get_handle(), VK_FORMAT_D16_UNORM, VK_IMAGE_TYPE_2D,
VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
0, &depth_properties);
shadow_resolution.width = std::min(depth_properties.maxExtent.width, shadow_resolution.width);
shadow_resolution.height = std::min(depth_properties.maxExtent.height, shadow_resolution.height);
shadow_resolution.width = std::min(get_device().get_gpu().get_properties().limits.maxFramebufferWidth, shadow_resolution.width);
shadow_resolution.height = std::min(get_device().get_gpu().get_properties().limits.maxFramebufferHeight, shadow_resolution.height);
vkb::core::Image shadow_target{get_device(), shadow_resolution, VK_FORMAT_D16_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY};
shadow_target.set_debug_name("shadow_target");
// Create a simple mip-chain used for bloom blur.
// Could technically mip-map the HDR target,
// but there's no real reason to do it like that.
for (uint32_t level = 1; level < 7; level++)
{
blur_chain.push_back(std::make_unique<vkb::core::Image>(
get_device(), downsample_extent(size, level),
VK_FORMAT_R16G16B16A16_SFLOAT,
VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VMA_MEMORY_USAGE_GPU_ONLY));
blur_chain_views.push_back(std::make_unique<vkb::core::ImageView>(
*blur_chain.back(), VK_IMAGE_VIEW_TYPE_2D));
}
// Calculate valid filter
VkFilter filter = VK_FILTER_LINEAR;
vkb::make_filters_valid(get_device().get_gpu().get_handle(), depth_targets[0].get_format(), &filter);
auto sampler_info = vkb::initializers::sampler_create_info();
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.minFilter = filter;
sampler_info.magFilter = filter;
sampler_info.maxLod = VK_LOD_CLAMP_NONE;
linear_sampler = std::make_unique<vkb::core::Sampler>(get_device(), sampler_info);
// Inverse Z, so use GEQ test.
sampler_info.compareOp = VK_COMPARE_OP_GREATER_OR_EQUAL;
sampler_info.compareEnable = VK_TRUE;
comparison_sampler = std::make_unique<vkb::core::Sampler>(get_device(), sampler_info);
for (unsigned i = 0; i < 2; i++)
{
std::vector<vkb::core::Image> color_attachments;
color_attachments.push_back(std::move(color_targets[i]));
color_attachments.push_back(std::move(depth_targets[i]));
forward_render_targets[i] = std::make_unique<vkb::RenderTarget>(std::move(color_attachments));
}
std::vector<vkb::core::Image> shadow_attachments;
shadow_attachments.push_back(std::move(shadow_target));
shadow_render_target = std::make_unique<vkb::RenderTarget>(std::move(shadow_attachments));
}
void AsyncComputeSample::setup_queues()
{
present_graphics_queue = &get_device().get_queue_by_present(0);
last_async_enabled = async_enabled;
// Need to be careful about sync if we're going to suddenly switch to async compute.
get_device().wait_idle();
// The way we set things up here somewhat heavily favors devices where we have 2 or more graphics queues.
// The pipeline we ideally want is:
// - Low priority graphics queue renders the HDR frames
// - Async compute queue does post
// - High priority queue does (HDR + Bloom) tonemap + UI in one graphics pass and presents.
//
// We want to present in the high priority graphics queue since on at least Arm devices,
// we can get pre-emption behavior
// where we can start rendering the next frame in parallel with async compute post,
// but the next frame will not block presentation. This keeps latency low, and
// is important to achieve full utilization of the fragment queue.
// Getting the async queue idle as fast as possible unblocks vertex shading work for the next frame.
// On desktop, in particular on architectures with just one graphics queue, this setup isn't very appealing
// since we cannot have a low and high priority graphics queue.
// We would ideally change the entire pipeline to be geared towards presenting in the async compute queue where
// tonemap + UI happens in compute instead.
// This complicates things since we would have to render UI in a fragment pass, which compute just composites.
// The hardcore alternative is to render the UI entirely in compute, but all of these consideration
// are outside the scope of this sample.
if (async_enabled)
{
const auto &queue_family_properties = get_device().get_gpu().get_queue_family_properties();
uint32_t graphics_family_index = vkb::get_queue_family_index(queue_family_properties, VK_QUEUE_GRAPHICS_BIT);
uint32_t compute_family_index = vkb::get_queue_family_index(queue_family_properties, VK_QUEUE_COMPUTE_BIT);
if (queue_family_properties[graphics_family_index].queueCount >= 2)
{
LOGI("Device has 2 or more graphics queues.");
early_graphics_queue = &get_device().get_queue(graphics_family_index, 1);
}
else
{
LOGI("Device has just 1 graphics queue.");
early_graphics_queue = present_graphics_queue;
}
if (graphics_family_index == compute_family_index)
{
LOGI("Device does not have a dedicated compute queue family.");
post_compute_queue = early_graphics_queue;
}
else
{
LOGI("Device has async compute queue.");
post_compute_queue = &get_device().get_queue(compute_family_index, 0);
}
}
else
{
// Force everything through the same queue.
early_graphics_queue = present_graphics_queue;
post_compute_queue = present_graphics_queue;
}
}
bool AsyncComputeSample::prepare(const vkb::ApplicationOptions &options)
{
// Set setup_queues() for details.
set_high_priority_graphics_queue_enable(true);
if (!VulkanSample::prepare(options))
{
return false;
}
load_scene("scenes/bonza/Bonza.gltf");
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>();
// Attach a shadow camera to the directional light.
auto lights = get_scene().get_components<vkb::sg::Light>();
for (auto &light : lights)
{
if (light->get_light_type() == vkb::sg::LightType::Directional)
{
vkb::sg::LightProperties props{};
props.color = glm::vec3(50.0f, 40.0f, 30.0f);
light->set_properties(props);
auto *node = light->get_node();
// Hardcoded to fit to the scene.
auto ortho_camera = std::make_unique<vkb::sg::OrthographicCamera>("shadow_camera",
-2000.0f, 3000.0f,
-2500.0f, 1500.0f,
-2000.0f, 2000.0f);
ortho_camera->set_node(*node);
get_scene().add_component(std::move(ortho_camera), *node);
shadow_camera = &node->get_component<vkb::sg::Camera>();
break;
}
}
prepare_render_targets();
vkb::ShaderSource vert_shader("async_compute/forward.vert.spv");
vkb::ShaderSource frag_shader("async_compute/forward.frag.spv");
auto scene_subpass =
std::make_unique<ShadowMapForwardSubpass>(get_render_context(), std::move(vert_shader), std::move(frag_shader), get_scene(), *camera, *shadow_camera);
vkb::ShaderSource shadow_vert_shader("async_compute/shadow.vert.spv");
vkb::ShaderSource shadow_frag_shader("async_compute/shadow.frag.spv");
auto shadow_scene_subpass =
std::make_unique<DepthMapSubpass>(get_render_context(), std::move(shadow_vert_shader), std::move(shadow_frag_shader), get_scene(), *shadow_camera);
shadow_render_pipeline.add_subpass(std::move(shadow_scene_subpass));
shadow_render_pipeline.set_load_store({{VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE}});
vkb::ShaderSource composite_vert_shader("async_compute/composite.vert.spv");
vkb::ShaderSource composite_frag_shader("async_compute/composite.frag.spv");
auto composite_scene_subpass =
std::make_unique<CompositeSubpass>(get_render_context(), std::move(composite_vert_shader), std::move(composite_frag_shader));
forward_render_pipeline.add_subpass(std::move(scene_subpass));
forward_render_pipeline.set_load_store({{VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE},
{VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_DONT_CARE}});
auto blit_render_pipeline = std::make_unique<vkb::RenderPipeline>();
blit_render_pipeline->add_subpass(std::move(composite_scene_subpass));
blit_render_pipeline->set_load_store({{VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE},
{VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_DONT_CARE}});
set_render_pipeline(std::move(blit_render_pipeline));
vkb::CounterSamplingConfig config;
config.mode = vkb::CounterSamplingMode::Continuous;
get_stats().request_stats({
vkb::StatIndex::frame_times,
vkb::StatIndex::gpu_cycles,
vkb::StatIndex::gpu_vertex_cycles,
vkb::StatIndex::gpu_fragment_cycles,
},
config);
create_gui(*window, &get_stats());
// Store the start time to calculate rotation
start_time = std::chrono::system_clock::now();
auto &threshold_module = get_device().get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource("async_compute/threshold.comp.spv"));
auto &blur_up_module = get_device().get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource("async_compute/blur_up.comp.spv"));
auto &blur_down_module = get_device().get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource("async_compute/blur_down.comp.spv"));
threshold_pipeline = &get_device().get_resource_cache().request_pipeline_layout({&threshold_module});
blur_up_pipeline = &get_device().get_resource_cache().request_pipeline_layout({&blur_up_module});
blur_down_pipeline = &get_device().get_resource_cache().request_pipeline_layout({&blur_down_module});
setup_queues();
return true;
}
void AsyncComputeSample::render_shadow_pass()
{
auto &queue = *early_graphics_queue;
auto command_buffer = get_render_context().get_active_frame().get_command_pool(queue).request_command_buffer();
command_buffer->set_debug_name("shadow_pass");
command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
auto &views = shadow_render_target->get_views();
assert(!views.empty());
{
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
memory_barrier.src_access_mask = 0;
memory_barrier.dst_access_mask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
command_buffer->image_memory_barrier(views[0], memory_barrier);
}
set_viewport_and_scissor(*command_buffer, shadow_render_target->get_extent());
shadow_render_pipeline.draw(*command_buffer, *shadow_render_target, VK_SUBPASS_CONTENTS_INLINE);
command_buffer->end_render_pass();
{
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
memory_barrier.src_access_mask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
memory_barrier.dst_access_mask = VK_ACCESS_SHADER_READ_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
command_buffer->image_memory_barrier(views[0], memory_barrier);
}
command_buffer->end();
get_render_context().submit(queue, {command_buffer});
}
vkb::RenderTarget &AsyncComputeSample::get_current_forward_render_target()
{
return *forward_render_targets[forward_render_target_index];
}
VkSemaphore AsyncComputeSample::render_forward_offscreen_pass(VkSemaphore hdr_wait_semaphore)
{
auto &queue = *early_graphics_queue;
auto command_buffer = get_render_context().get_active_frame().get_command_pool(queue).request_command_buffer();
command_buffer->set_debug_name("forward_offscreen_pass");
command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
auto &views = get_current_forward_render_target().get_views();
assert(1 < views.size());
{
// If maintenance9 is not enabled, resources with VK_SHARING_MODE_EXCLUSIVE must only be accessed by queues in the queue family that has ownership of the resource.
// Upon creation resources with VK_SHARING_MODE_EXCLUSIVE are not owned by any queue, ownership is implicitly acquired upon first use.
// The application must perform a queue family ownership transfer if it wishes to make the memory contents of the resource accessible to a different queue family.
// A queue family can take ownership of a resource without an ownership transfer, in the same way as for a resource that was just created, but the content will be undefined.
// We do not need to acquire color_targets[0] from present_graphics to early_graphics
// A queue transfer barrier is not necessary for the resource first access.
// Moreover, in our sample we do not care about the content at this point so we can skip the queue transfer barrier.
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
memory_barrier.src_access_mask = 0;
memory_barrier.dst_access_mask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
command_buffer->image_memory_barrier(views[0], memory_barrier);
}
{
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
memory_barrier.src_access_mask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
memory_barrier.dst_access_mask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
command_buffer->image_memory_barrier(views[1], memory_barrier);
}
set_viewport_and_scissor(*command_buffer, get_current_forward_render_target().get_extent());
forward_render_pipeline.draw(*command_buffer, get_current_forward_render_target(), VK_SUBPASS_CONTENTS_INLINE);
command_buffer->end_render_pass();
const bool queue_family_transfer = early_graphics_queue->get_family_index() != post_compute_queue->get_family_index();
{
// When doing async compute this barrier is used to do a queue family ownership transfer
// release_barrier_0: Releasing color_targets[0] from early_graphics to post_compute
// This release barrier is replicated by the corresponding acquire_barrier_0 in the post_compute queue
// The application must ensure the release operation happens before the acquire operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier{
.src_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.dst_stage_mask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, // Ignored for the release barrier.
// Release barriers ignore dst_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.src_access_mask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
.dst_access_mask = 0, // dst_access_mask is ignored for release barriers, without affecting its validity
.old_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, // We want a layout transition, so the old_layout and new_layout values need to be replicated in the acquire barrier
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = queue_family_transfer ?
early_graphics_queue->get_family_index() :
VK_QUEUE_FAMILY_IGNORED, // Release barriers are executed from a queue of the source queue family
.dst_queue_family = queue_family_transfer ? post_compute_queue->get_family_index() : VK_QUEUE_FAMILY_IGNORED,
};
command_buffer->image_memory_barrier(views[0], memory_barrier);
}
command_buffer->end();
// Conditionally waits on hdr_wait_semaphore.
// This resolves the write-after-read hazard where previous frame tonemap read from HDR buffer.
// We are not using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
// so VK_PIPELINE_STAGE_ALL_COMMANDS_BIT is the only valid stage to wait for queue transfer operations.
const VkPipelineStageFlags wait_stage = queue_family_transfer ? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
auto signal_semaphore = get_render_context().submit(queue, {command_buffer}, hdr_wait_semaphore, wait_stage);
if (hdr_wait_semaphore)
{
get_render_context().release_owned_semaphore(hdr_wait_semaphore);
}
return signal_semaphore;
}
VkSemaphore AsyncComputeSample::render_swapchain(VkSemaphore post_semaphore)
{
auto &queue = *present_graphics_queue;
auto command_buffer = get_render_context().get_active_frame().get_command_pool(queue).request_command_buffer();
command_buffer->set_debug_name("swapchain");
command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
if (post_compute_queue->get_family_index() != present_graphics_queue->get_family_index())
{
// acquire_barrier_1: Acquiring color_targets[0] from post_compute to present_graphics
// This acquire barrier is replicated by the corresponding release_barrier_1 in the post_compute queue
// The application must ensure the acquire operation happens after the release operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier{
.src_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, // Ignored for the acquire barrier.
// Acquire barriers ignore src_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.dst_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
.src_access_mask = 0, // src_access_mask is ignored for acquire barriers, without affecting its validity
.dst_access_mask = VK_ACCESS_SHADER_READ_BIT,
.old_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, // Purely ownership transfer. We do not need a layout transition.
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = post_compute_queue->get_family_index(),
.dst_queue_family = present_graphics_queue->get_family_index(), // Acquire barriers are executed from a queue of the destination queue family
};
command_buffer->image_memory_barrier(get_current_forward_render_target().get_views()[0], memory_barrier);
// acquire_barrier_2: Acquiring blur_chain_views[1] from post_compute to present_graphics
// This acquire barrier is replicated by the corresponding release_barrier_2 in the post_compute queue
// The application must ensure the acquire operation happens after the release operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier_2{
.src_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, // Ignored for the acquire barrier.
// Acquire barriers ignore src_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.dst_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
.src_access_mask = 0, // src_access_mask is ignored for acquire barriers, without affecting its validity
.dst_access_mask = VK_ACCESS_SHADER_READ_BIT,
.old_layout = VK_IMAGE_LAYOUT_GENERAL, // We want a layout transition, so the old_layout and new_layout values need to be replicated in the acquire barrier
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = post_compute_queue->get_family_index(),
.dst_queue_family = present_graphics_queue->get_family_index(), // Acquire barriers are executed from a queue of the destination queue family
};
command_buffer->image_memory_barrier(*blur_chain_views[1], memory_barrier_2);
}
draw(*command_buffer, get_render_context().get_active_frame().get_render_target());
// If maintenance9 is not enabled, resources with VK_SHARING_MODE_EXCLUSIVE must only be accessed by queues in the queue family that has ownership of the resource.
// Upon creation resources with VK_SHARING_MODE_EXCLUSIVE are not owned by any queue, ownership is implicitly acquired upon first use.
// The application must perform a queue family ownership transfer if it wishes to make the memory contents of the resource accessible to a different queue family.
// A queue family can take ownership of a resource without an ownership transfer, in the same way as for a resource that was just created, but the content will be undefined.
// We do not need to release blur_chain_views[1] and color_targets[0] from present_graphics
// A queue transfer barrier is not necessary for the resource first access.
// Moreover, in our sample we do not care about the content after presenting so we can skip the queue transfer barrier.
command_buffer->end();
// We're going to wait on this semaphore in different frame,
// so we need to hold ownership of the semaphore until we complete the wait.
hdr_wait_semaphores[forward_render_target_index] = get_render_context().request_semaphore_with_ownership();
// We've read the post buffer outputs, so we need to consider write-after-read
// next frame. This is only meaningful if we're doing double buffered HDR since it's
// theoretically possible to complete HDR rendering for frame N + 1 while we're doing presentation.
// In that case, the async compute post pipeline can start writing blur results *before* we're done reading.
compute_post_semaphore = get_render_context().request_semaphore_with_ownership();
const VkSemaphore signal_semaphores[] = {
get_render_context().request_semaphore(),
hdr_wait_semaphores[forward_render_target_index],
compute_post_semaphore,
};
const VkSemaphore wait_semaphores[] = {
post_semaphore,
get_render_context().consume_acquired_semaphore(),
};
const VkPipelineStageFlags wait_stages[] = {
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
};
auto info = vkb::initializers::submit_info();
info.pSignalSemaphores = signal_semaphores;
info.signalSemaphoreCount = 3;
info.pWaitSemaphores = wait_semaphores;
info.waitSemaphoreCount = 2;
info.pWaitDstStageMask = wait_stages;
info.commandBufferCount = 1;
info.pCommandBuffers = &command_buffer->get_handle();
queue.submit({info}, get_render_context().get_active_frame().get_fence_pool().request_fence());
get_render_context().release_owned_semaphore(wait_semaphores[1]);
return signal_semaphores[0];
}
VkSemaphore AsyncComputeSample::render_compute_post(VkSemaphore wait_graphics_semaphore, VkSemaphore wait_present_semaphore)
{
auto &queue = *post_compute_queue;
auto command_buffer = get_render_context().get_active_frame().get_command_pool(queue).request_command_buffer();
command_buffer->set_debug_name("compute_post");
command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
if (early_graphics_queue->get_family_index() != post_compute_queue->get_family_index())
{
// acquire_barrier_0: Acquiring color_targets[0] from early_graphics to post_compute
// This acquire barrier is replicated by the corresponding release_barrier_0 in the early_graphics queue
// The application must ensure the acquire operation happens after the release operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier{
.src_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, // Ignored for the acquire barrier.
// Acquire barriers ignore src_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.dst_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
.src_access_mask = 0, // src_access_mask is ignored for acquire barriers, without affecting its validity
.dst_access_mask = VK_ACCESS_SHADER_READ_BIT,
.old_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, // We want a layout transition, so the old_layout and new_layout values need to be replicated in the release barrier
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = early_graphics_queue->get_family_index(),
.dst_queue_family = post_compute_queue->get_family_index(), // Acquire barriers are executed from a queue of the destination queue family
};
command_buffer->image_memory_barrier(get_current_forward_render_target().get_views()[0], memory_barrier);
}
const auto discard_blur_view = [&](const vkb::core::ImageView &view) {
// If maintenance9 is not enabled, resources with VK_SHARING_MODE_EXCLUSIVE must only be accessed by queues in the queue family that has ownership of the resource.
// Upon creation resources with VK_SHARING_MODE_EXCLUSIVE are not owned by any queue, ownership is implicitly acquired upon first use.
// The application must perform a queue family ownership transfer if it wishes to make the memory contents of the resource accessible to a different queue family.
// A queue family can take ownership of a resource without an ownership transfer, in the same way as for a resource that was just created, but the content will be undefined.
// We do not need to acquire blur_chain_views[1] from present_graphics to post_compute
// A queue transfer barrier is not necessary for the resource first access.
// Moreover, in our sample we do not care about the content at this point so we can skip the queue transfer barrier.
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_GENERAL;
memory_barrier.src_access_mask = 0;
memory_barrier.dst_access_mask = VK_ACCESS_SHADER_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
command_buffer->image_memory_barrier(view, memory_barrier);
};
const auto read_only_blur_view = [&](const vkb::core::ImageView &view, bool is_final) {
const bool queue_family_transfer = is_final && post_compute_queue->get_family_index() != present_graphics_queue->get_family_index();
// release_barrier_2: Releasing blur_chain_views[1] from post_compute to present_graphics
// This release barrier is replicated by the corresponding acquire_barrier_2 in the present_graphics queue
// The application must ensure the release operation happens before the acquire operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier{
.src_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
.dst_stage_mask = is_final ? VkPipelineStageFlags(VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT) : VkPipelineStageFlags(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT), // Ignored for the release barrier.
// Release barriers ignore dst_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.src_access_mask = VK_ACCESS_SHADER_WRITE_BIT,
.dst_access_mask = is_final ? VkAccessFlags(0) : VkAccessFlags(VK_ACCESS_SHADER_READ_BIT), // dst_access_mask is ignored for release barriers, without affecting its validity
.old_layout = VK_IMAGE_LAYOUT_GENERAL, // We want a layout transition, so the old_layout and new_layout values need to be replicated in the acquire barrier
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = queue_family_transfer ? post_compute_queue->get_family_index() : VK_QUEUE_FAMILY_IGNORED, // Release barriers are executed from a queue of the source queue family
.dst_queue_family = queue_family_transfer ? present_graphics_queue->get_family_index() : VK_QUEUE_FAMILY_IGNORED, // Release barriers are executed from a queue of the source queue family
};
command_buffer->image_memory_barrier(view, memory_barrier);
};
struct Push
{
uint32_t width, height;
float inv_width, inv_height;
float inv_input_width, inv_input_height;
};
const auto dispatch_pass = [&](const vkb::core::ImageView &dst, const vkb::core::ImageView &src, bool is_final = false) {
discard_blur_view(dst);
auto dst_extent = downsample_extent(dst.get_image().get_extent(), dst.get_subresource_range().baseMipLevel);
auto src_extent = downsample_extent(src.get_image().get_extent(), src.get_subresource_range().baseMipLevel);
Push push{};
push.width = dst_extent.width;
push.height = dst_extent.height;
push.inv_width = 1.0f / static_cast<float>(push.width);
push.inv_height = 1.0f / static_cast<float>(push.height);
push.inv_input_width = 1.0f / static_cast<float>(src_extent.width);
push.inv_input_height = 1.0f / static_cast<float>(src_extent.height);
command_buffer->push_constants(push);
command_buffer->bind_image(src, *linear_sampler, 0, 0, 0);
command_buffer->bind_image(dst, 0, 1, 0);
command_buffer->dispatch((push.width + 7) / 8, (push.height + 7) / 8, 1);
read_only_blur_view(dst, is_final);
};
// A very basic and dumb HDR Bloom pipeline. Don't consider this a particularly good or efficient implementation.
// It's here to represent a plausible compute post workload.
// - Threshold pass
// - Blur down
// - Blur up
command_buffer->bind_pipeline_layout(*threshold_pipeline);
dispatch_pass(*blur_chain_views[0], get_current_forward_render_target().get_views()[0]);
command_buffer->bind_pipeline_layout(*blur_down_pipeline);
for (uint32_t index = 1; index < blur_chain_views.size(); index++)
{
dispatch_pass(*blur_chain_views[index], *blur_chain_views[index - 1]);
}
command_buffer->bind_pipeline_layout(*blur_up_pipeline);
for (uint32_t index = static_cast<uint32_t>(blur_chain_views.size() - 2); index >= 1; index--)
{
dispatch_pass(*blur_chain_views[index], *blur_chain_views[index + 1], index == 1);
}
if (post_compute_queue->get_family_index() != present_graphics_queue->get_family_index())
{
// release_barrier_1: Releasing color_targets[0] from post_compute to present_graphics
// This release barrier is replicated by the corresponding acquire_barrier_1 in the present_graphics queue
// The application must ensure the release operation happens before the acquire operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier{
.src_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
.dst_stage_mask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, // Ignored for the release barrier.
// Release barriers ignore dst_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.src_access_mask = VK_ACCESS_SHADER_READ_BIT,
.dst_access_mask = 0, // dst_access_mask is ignored for release barriers, without affecting its validity
.old_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, // Purely ownership transfer. We do not need a layout transition.
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = post_compute_queue->get_family_index(), // Release barriers are executed from a queue of the source queue family
.dst_queue_family = present_graphics_queue->get_family_index(),
};
command_buffer->image_memory_barrier(get_current_forward_render_target().get_views()[0], memory_barrier);
}
command_buffer->end();
VkPipelineStageFlags wait_stages[] = {VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT};
VkSemaphore wait_semaphores[] = {wait_graphics_semaphore, wait_present_semaphore};
VkSemaphore signal_semaphore = get_render_context().request_semaphore();
auto info = vkb::initializers::submit_info();
info.pSignalSemaphores = &signal_semaphore;
info.signalSemaphoreCount = 1;
info.pWaitSemaphores = wait_semaphores;
info.waitSemaphoreCount = wait_present_semaphore != VK_NULL_HANDLE ? 2 : 1;
info.pWaitDstStageMask = wait_stages;
info.commandBufferCount = 1;
info.pCommandBuffers = &command_buffer->get_handle();
if (wait_present_semaphore != VK_NULL_HANDLE)
{
get_render_context().release_owned_semaphore(wait_present_semaphore);
}
queue.submit({info}, VK_NULL_HANDLE);
return signal_semaphore;
}
void AsyncComputeSample::update(float delta_time)
{
// don't call the parent's update, because it's done differently here... but call the grandparent's update for fps logging
vkb::Application::update(delta_time);
if (last_async_enabled != async_enabled)
{
setup_queues();
}
// We can potentially get more overlap if we double buffer the HDR render target.
// In this scenario, the next frame can run ahead a little further before it needs to block.
if (double_buffer_hdr_frames)
{
forward_render_target_index = 1 - forward_render_target_index;
}
else
{
forward_render_target_index = 0;
}
auto *forward_subpass = static_cast<ShadowMapForwardSubpass *>(forward_render_pipeline.get_subpasses()[0].get());
auto *composite_subpass = static_cast<CompositeSubpass *>(get_render_pipeline().get_subpasses()[0].get());
forward_subpass->set_shadow_map(&shadow_render_target->get_views()[0], comparison_sampler.get());
composite_subpass->set_texture(&get_current_forward_render_target().get_views()[0], blur_chain_views[1].get(), linear_sampler.get()); // blur_chain[1] and color_targets[0] will be used by the present queue
float rotation_factor = std::chrono::duration<float>(std::chrono::system_clock::now() - start_time).count();
glm::quat orientation;
// Lots of random jank to get a desired orientation quaternion for the directional light.
if (rotate_shadows)
{
// Move shadows and directional light slightly.
orientation = glm::normalize(
glm::angleAxis(glm::pi<float>(), glm::vec3(0.0f, -1.0f, 0.0f)) *
glm::angleAxis(-0.2f * glm::half_pi<float>(), glm::vec3(1.0f, 0.0f, 0.0f)) *
glm::angleAxis(glm::two_pi<float>() * glm::fract(rotation_factor * 0.05f), glm::vec3(0.0f, 0.0f, -1.0f)) *
glm::angleAxis(-0.05f * glm::half_pi<float>(), glm::vec3(1.0f, 0.0f, 0.0f)));
}
else
{
orientation = glm::normalize(
glm::angleAxis(glm::pi<float>(), glm::vec3(0.0f, -1.0f, 0.0f)) *
glm::angleAxis(-0.2f * glm::half_pi<float>(), glm::vec3(1.0f, 0.0f, 0.0f)));
}
auto &shadow_camera_transform = shadow_camera->get_node()->get_component<vkb::sg::Transform>();
shadow_camera_transform.set_rotation(orientation);
// Explicit begin_frame and end_frame since we're doing async compute, many submissions and custom semaphores ...
get_render_context().begin_frame();
update_scene(delta_time);
update_gui(delta_time);
// Collect the performance data for the sample graphs
update_stats(delta_time);
// Setup render pipeline:
// - Shadow pass
// - HDR
// - Async compute post
// - Composite
render_shadow_pass();
VkSemaphore graphics_semaphore = render_forward_offscreen_pass(hdr_wait_semaphores[forward_render_target_index]);
hdr_wait_semaphores[forward_render_target_index] = VK_NULL_HANDLE;
VkSemaphore post_semaphore = render_compute_post(graphics_semaphore, compute_post_semaphore);
compute_post_semaphore = VK_NULL_HANDLE;
VkSemaphore present_semaphore = render_swapchain(post_semaphore);
get_render_context().end_frame(present_semaphore);
}
void AsyncComputeSample::finish()
{
if (has_device())
{
for (auto &sem : hdr_wait_semaphores)
{
// We're outside a frame context, so free the semaphore manually.
get_device().wait_idle();
vkDestroySemaphore(get_device().get_handle(), sem, nullptr);
}
if (compute_post_semaphore)
{
// We're outside a frame context, so free the semaphore manually.
get_device().wait_idle();
vkDestroySemaphore(get_device().get_handle(), compute_post_semaphore, nullptr);
}
}
}
std::unique_ptr<vkb::VulkanSampleC> create_async_compute()
{
return std::make_unique<AsyncComputeSample>();
}
AsyncComputeSample::DepthMapSubpass::DepthMapSubpass(vkb::RenderContext &render_context,
vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader,
vkb::sg::Scene &scene, vkb::sg::Camera &camera) :
vkb::ForwardSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera)
{
// PCF, so need depth bias to avoid (most) shadow acne.
base_rasterization_state.depth_bias_enable = VK_TRUE;
}
void AsyncComputeSample::DepthMapSubpass::draw(vkb::core::CommandBufferC &command_buffer)
{
// Negative bias since we're using inverted Z.
command_buffer.set_depth_bias(-1.0f, 0.0f, -2.0f);
vkb::ForwardSubpass::draw(command_buffer);
}
AsyncComputeSample::ShadowMapForwardSubpass::ShadowMapForwardSubpass(vkb::RenderContext &render_context,
vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader,
vkb::sg::Scene &scene, vkb::sg::Camera &camera, vkb::sg::Camera &shadow_camera_) :
vkb::ForwardSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera),
shadow_camera(shadow_camera_)
{
}
void AsyncComputeSample::ShadowMapForwardSubpass::set_shadow_map(const vkb::core::ImageView *view, const vkb::core::Sampler *sampler)
{
shadow_view = view;
shadow_sampler = sampler;
}
void AsyncComputeSample::ShadowMapForwardSubpass::draw(vkb::core::CommandBufferC &command_buffer)
{
auto shadow_matrix = vkb::rendering::vulkan_style_projection(shadow_camera.get_projection()) * shadow_camera.get_view();
shadow_matrix = glm::translate(glm::vec3(0.5f, 0.5f, 0.0f)) * glm::scale(glm::vec3(0.5f, 0.5f, 1.0f)) * shadow_matrix;
auto &render_frame = get_render_context().get_active_frame();
auto allocation = render_frame.allocate_buffer(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, sizeof(glm::mat4), thread_index);
allocation.update(shadow_matrix);
// Custom part, bind shadow map to the fragment shader.
command_buffer.bind_buffer(allocation.get_buffer(), allocation.get_offset(), allocation.get_size(), 0, 5, 0);
command_buffer.bind_image(*shadow_view, *shadow_sampler, 0, 6, 0);
vkb::ForwardSubpass::draw(command_buffer);
}
AsyncComputeSample::CompositeSubpass::CompositeSubpass(vkb::RenderContext &render_context, vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader) :
vkb::rendering::SubpassC(render_context, std::move(vertex_shader), std::move(fragment_shader))
{
}
void AsyncComputeSample::CompositeSubpass::set_texture(const vkb::core::ImageView *hdr_view_, const vkb::core::ImageView *bloom_view_,
const vkb::core::Sampler *sampler_)
{
hdr_view = hdr_view_;
bloom_view = bloom_view_;
sampler = sampler_;
}
void AsyncComputeSample::CompositeSubpass::prepare()
{
auto &device = get_render_context().get_device();
auto &vertex = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_VERTEX_BIT, get_vertex_shader());
auto &fragment = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_FRAGMENT_BIT, get_fragment_shader());
layout = &device.get_resource_cache().request_pipeline_layout({&vertex, &fragment});
}
void AsyncComputeSample::CompositeSubpass::draw(vkb::core::CommandBufferC &command_buffer)
{
command_buffer.bind_image(*hdr_view, *sampler, 0, 0, 0);
command_buffer.bind_image(*bloom_view, *sampler, 0, 1, 0);
command_buffer.bind_pipeline_layout(*layout);
// A depth-stencil attachment exists in the default render pass, make sure we ignore it.
vkb::DepthStencilState ds_state = {};
ds_state.depth_test_enable = VK_FALSE;
ds_state.stencil_test_enable = VK_FALSE;
ds_state.depth_write_enable = VK_FALSE;
ds_state.depth_compare_op = VK_COMPARE_OP_ALWAYS;
command_buffer.set_depth_stencil_state(ds_state);
command_buffer.draw(3, 1, 0, 0);
}
@@ -0,0 +1,123 @@
/* Copyright (c) 2021-2025, Arm Limited and Contributors
*
* 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.
*/
#pragma once
#include "rendering/render_pipeline.h"
#include "rendering/subpasses/forward_subpass.h"
#include "scene_graph/components/camera.h"
#include "timer.h"
#include "vulkan_sample.h"
/**
* @brief Using multiple queues to achieve more parallelism on the GPU
*/
class AsyncComputeSample : public vkb::VulkanSampleC
{
public:
AsyncComputeSample();
virtual ~AsyncComputeSample() = default;
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void update(float delta_time) override;
virtual void finish() override;
private:
vkb::sg::Camera *camera{nullptr};
vkb::sg::Camera *shadow_camera{nullptr};
virtual void draw_gui() override;
std::chrono::system_clock::time_point start_time;
void render_shadow_pass();
VkSemaphore render_forward_offscreen_pass(VkSemaphore hdr_wait_semaphore);
VkSemaphore render_compute_post(VkSemaphore wait_graphics_semaphore, VkSemaphore wait_present_semaphore);
VkSemaphore render_swapchain(VkSemaphore post_semaphore);
void setup_queues();
void prepare_render_targets();
std::unique_ptr<vkb::RenderTarget> forward_render_targets[2];
std::unique_ptr<vkb::RenderTarget> shadow_render_target;
vkb::RenderPipeline shadow_render_pipeline;
vkb::RenderPipeline forward_render_pipeline;
std::unique_ptr<vkb::core::Sampler> comparison_sampler;
std::unique_ptr<vkb::core::Sampler> linear_sampler;
std::vector<std::unique_ptr<vkb::core::Image>> blur_chain;
std::vector<std::unique_ptr<vkb::core::ImageView>> blur_chain_views;
vkb::PipelineLayout *threshold_pipeline{nullptr};
vkb::PipelineLayout *blur_down_pipeline{nullptr};
vkb::PipelineLayout *blur_up_pipeline{nullptr};
const vkb::Queue *early_graphics_queue{nullptr};
const vkb::Queue *present_graphics_queue{nullptr};
const vkb::Queue *post_compute_queue{nullptr};
VkSemaphore hdr_wait_semaphores[2]{};
VkSemaphore compute_post_semaphore{};
bool async_enabled{false};
bool rotate_shadows{true};
bool last_async_enabled{false};
bool double_buffer_hdr_frames{false};
unsigned forward_render_target_index{};
struct DepthMapSubpass : vkb::ForwardSubpass
{
DepthMapSubpass(vkb::RenderContext &render_context,
vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader,
vkb::sg::Scene &scene, vkb::sg::Camera &camera);
virtual void draw(vkb::core::CommandBufferC &command_buffer) override;
};
struct ShadowMapForwardSubpass : vkb::ForwardSubpass
{
ShadowMapForwardSubpass(vkb::RenderContext &render_context,
vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader,
vkb::sg::Scene &scene, vkb::sg::Camera &camera, vkb::sg::Camera &shadow_camera);
void set_shadow_map(const vkb::core::ImageView *view, const vkb::core::Sampler *sampler);
virtual void draw(vkb::core::CommandBufferC &command_buffer) override;
const vkb::core::ImageView *shadow_view{nullptr};
const vkb::core::Sampler *shadow_sampler{nullptr};
vkb::sg::Camera &shadow_camera;
};
struct CompositeSubpass : vkb::rendering::SubpassC
{
CompositeSubpass(vkb::RenderContext &render_context,
vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader);
void set_texture(const vkb::core::ImageView *hdr_view, const vkb::core::ImageView *bloom_view,
const vkb::core::Sampler *sampler);
virtual void draw(vkb::core::CommandBufferC &command_buffer) override;
virtual void prepare() override;
const vkb::core::ImageView *hdr_view{nullptr};
const vkb::core::ImageView *bloom_view{nullptr};
const vkb::core::Sampler *sampler{nullptr};
vkb::PipelineLayout *layout{nullptr};
};
vkb::RenderTarget &get_current_forward_render_target();
};
std::unique_ptr<vkb::VulkanSampleC> create_async_compute();
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

@@ -0,0 +1,30 @@
# Copyright (c) 2019-2021, Arm Limited and Contributors
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Arm"
NAME "Command Buffer Usage"
DESCRIPTION "Primary and secondary command buffers, allocation and recycling."
SHADER_FILES_GLSL
"base.vert"
"base.frag")
@@ -0,0 +1,210 @@
////
- Copyright (c) 2019-2024, Arm Limited and Contributors
-
- 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.
-
////
= Command buffer usage and multi-threaded recording
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/command_buffer_usage[Khronos Vulkan samples github repository].
endif::[]
:pp: {plus}{plus}
== Overview
This sample demonstrates how to use and manage secondary command buffers, and how to record them concurrently.
Implementing multi-threaded recording of draw calls can help reduce CPU frame time.
In tile-based renderers, the best approach to split the draw calls is to record them in secondary command buffers.
This way they can all be submitted to the same render pass, and can take advantage of tile local memory.
== Secondary command buffers
Secondary command buffers can inherit the render pass state from a primary command buffer using a https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferInheritanceInfo.html[VkCommandBufferInheritanceInfo] structure which is passed to https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkBeginCommandBuffer.html[vkBeginCommandBuffer] as part of https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferBeginInfo.html[VkCommandBufferBeginInfo], along with the flag `VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT`.
Secondary command buffers may then be recorded concurrently.
The primary command buffer must have used the flag `VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS` in https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdBeginRenderPass.html[vkCmdBeginRenderPass].
Finally, the primary command buffer records https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdExecuteCommands.html[vkCmdExecuteCommands] (before https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdEndRenderPass.html[vkCmdEndRenderPass]) with an array of recorded secondary command buffers to execute.
The sample divides the draw calls for opaque objects based on the slider value.
It then submits a separate buffer for transparent objects if any, and finally one for the GUI elements if visible.
== Multi-threaded recording
To record command buffers concurrently, the framework needs to manage resource pools per frame and per thread.
According to the Vulkan Spec:
* https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandPool.html[_A command pool must not be used concurrently in multiple threads._]
* https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkDescriptorPool.html[_The application must not allocate and/or free descriptor sets from the same pool in multiple threads simultaneously._]
In the framework, each frame in the queue (e.g.
three frames in case of triple buffering) manages a collection of pools so that each thread can own:
* A command pool
* A descriptor pool cache
* A descriptor set cache
* A buffer pool
This sample then uses a thread pool to push work to multiple threads.
When splitting the draw calls, it is advisable to keep the loads balanced.
The sample allows to change the number of buffers, but if the number of calls is not divisible, the remaining will be evenly spread through other buffers.
The average number of draws per buffer is shown on the screen.
Note that since state is not reused across command buffers, a reasonable number of draw calls should be submitted per command buffer, to avoid having the GPU going idle while processing commands.
Therefore having many secondary command buffers with few draw calls can negatively affect performance.
In any case there is no advantage in exceeding the CPU parallelism level i.e.
using more command buffers than threads.
Similarly having more threads than buffers may have a performance impact.
To keep all threads busy, the sample resizes the thread pool for low number of buffers.
The sample slider can help illustrate these trade-offs and their impact on performance, as shown by the performance graphs.
NOTE: Since the time of writing this tutorial, the CPU counter provider, HWCPipe, has been updated and it no longer provides CPU cycles. These may still be measured using external tools, as shown later.
In this case, a scene with a high number of draw calls (~1800, this number may be found in the link:../../../docs/misc.adoc#debug-window[debug window]) shows a 15% improvement in performance when dividing the workload among 8 buffers across 8 threads:
image::./images/single_threading.png[Single-threaded]
image::./images/multi_threading.png[Multi-threaded]
To test the sample, make sure to build it in release mode and without validation layers.
Both these factors can significantly affect the results.
== Recycling strategies
Vulkan provides different ways to manage and allocate command buffers.
This sample compares them and demonstrates the best approach.
* <<allocate-and-free,Allocate and Free>>
* <<resetting-individual-command-buffers,Resetting individual command buffers>>
* <<resetting-the-command-pool,Resetting the command pool>>
=== Allocate and free
Command buffers are allocated from a command pool with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkAllocateCommandBuffers.html[vkAllocateCommandBuffers].
They can then be recorded and submitted to a queue for the Vulkan device to execute them.
A possible approach to managing the command buffers for each frame in our application would be to free them once they are executed, using https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkFreeCommandBuffers.html[vkFreeCommandBuffers].
The command pool will not automatically recycle memory from deleted command buffers if the command pool was created without the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandPoolCreateFlagBits.html[RESET_COMMAND_BUFFER_BIT] flag.
This flag however will force separate internal allocators to be used for each command buffer in the pool, which can increase CPU overhead compared to a single pool reset.
This is the worst-performing method of managing command buffers as it involves a significant CPU overhead for allocating and freeing memory frequently.
The sample shows how to use the framework to follow this approach and profile its performance.
image::./images/allocate_and_free.jpg[Allocate and Free]
Rather than freeing and re-allocating the memory used by a command buffer, it is more efficient to recycle it for recording new commands.
There are two ways of resetting a command buffer: individually, with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandBuffer.html[vkResetCommandBuffer], or indirectly by resetting the command pool with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandPool.html[vkResetCommandPool].
=== Resetting individual command buffers
In order to reset command buffers individually with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandBuffer.html[vkResetCommandBuffer], the pool must have been created with the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandPoolCreateFlagBits.html[RESET_COMMAND_BUFFER_BIT] flag set.
The buffer will then return to a recordable state and the command pool can reuse the memory it allocated for it.
However frequent calls to https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandBuffer.html[vkResetCommandBuffer] are more expensive than a command pool reset.
image::./images/reset_buffers.jpg[Reset Buffers]
=== Resetting the command pool
Resetting the pool with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandPool.html[vkResetCommandPool] automatically resets all the command buffers allocated by it.
Doing this periodically will allow the pool to reuse the memory allocated for command buffers with lower CPU overhead.
To reset the pool the flag https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandPoolCreateFlagBits.html[RESET_COMMAND_BUFFER_BIT] is _not_ required, and it is actually better to avoid it since it prevents it from using a single large allocator for all buffers in the pool thus increasing memory overhead.
image::./images/reset_pool.jpg[Reset Pool]
=== Relative performance
The sample offers the option of recording all drawing operations on a single command buffer, or to divide the opaque object draw calls among a given number of secondary command buffers.
The second method allows multi-threaded command buffer construction.
However the number of secondary command buffers should be kept low since their invocations are expensive.
This sample lets the user adjust the number of command buffers.
Using a high number of secondary command buffers causes the application to become CPU bound and makes the differences between the described memory allocation approaches more pronounced.
All command buffers in this sample are initialized with the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferUsageFlagBits.html[ONE_TIME_SUBMIT_BIT] flag set.
This indicates to the driver that the buffer will not be re-submitted after execution, and allows it to optimize accordingly.
Performance may be reduced if the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferUsageFlagBits.html[SIMULTANEOUS_USE_BIT] flag is set instead.
This sample provides options to try the three different approaches to command buffer management described above and monitor their efficiency.
This is relatively obvious directly on the device by monitoring frame time.
Since the application is CPU bound, the https://developer.android.com/studio/profile/android-profiler[Android Profiler] is a helpful tool to analyze the differences in performance.
As expected, most of the time goes into the https://github.com/ARM-software/vulkan_best_practice_for_mobile_developers/blob/master/framework/core/command_pool.cpp[command pool framework functions]: `request_command_buffer` and `reset`.
These handle the different modes of operation exposed by this sample.
image:./images/android_profiler_allocate_and_free.png[Android Profiler: Allocate and Free] _Android Profiler capture: use the Flame Chart to visualize which functions take the most time in the capture.
Filter for command buffer functions, and use the tooltips to find out how much time out of the overall capture was used by each function._
Capturing the C{pp} calls this way allows us to determine how much each function contributed to the overall running time.
The results are captured in the table below.
|===
| Mode | Request + Reset command buffers (ms) | Total capture (ms) | Contribution
| Reset pool
| 53.3
| 11 877
| 0.45 %
| Reset buffers
| 140.29
| 12 087
| 1.16 %
| Allocate and free
| 3 319.25
| 11 513
| 28.8 %
|===
In this application the differences between individual reset and pool reset are more subtle, but allocating and freeing buffers are clearly the bottleneck in the worst performing case.
== Further reading
* xref:samples/performance/multithreading_render_passes/README.adoc[Multi-threaded recording with multiple render passes]
* https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap6.html#commandbuffer-allocation[Command Buffer Allocation and Management]
* https://www.khronos.org/registry/vulkan/specs/1.0-wsi_extensions/html/chap5.html#commandbuffers-lifecycle[Command Buffer Lifecycle]
* _"Writing an efficient Vulkan renderer"_ by Arseny Kapoulkine (from "GPU Zen 2: Advanced Rendering Techniques")
== Best-practice summary
*Do*
* Use secondary command buffers to allow multi-threaded render pass construction.
* Minimize the number of secondary command buffer invocations used per frame.
* Set https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferUsageFlagBits.html[ONE_TIME_SUBMIT_BIT] if you are not going to reuse the command buffer.
* Periodically call https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandPool.html[vkResetCommandPool()] to release the memory if you are not reusing command buffers.
*Don't*
* Set https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandPoolCreateFlagBits.html[RESET_COMMAND_BUFFER_BIT] if you only need to free the whole pool.
If the bit is not set, some implementations might use a single large allocator for the pool, reducing memory management overhead.
* Call https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandBuffer.html[vkResetCommandBuffer()] on a high frequency call path.
*Impact*
* Increased CPU load will be incurred with secondary command buffers.
* Increased CPU load will be incurred if command pool creation and command buffer begin flags are not used appropriately.
* Increased CPU overhead if command buffer resets are too frequent.
* Increased memory usage until a manual command pool reset is triggered.
*Debugging*
* Evaluate every use of any command buffer flag other than https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferUsageFlagBits.html[ONE_TIME_SUBMIT_BIT], and review whether it's a necessary use of the flag combination.
* Evaluate every use of https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandBuffer.html[vkResetCommandBuffer()] and see if it could be replaced with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandPool.html[vkResetCommandPool()] instead.
@@ -0,0 +1,453 @@
/* Copyright (c) 2019-2025, Arm Limited and Contributors
*
* 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 "command_buffer_usage.h"
#include <algorithm>
#include <numeric>
#include "core/device.h"
#include "core/pipeline_layout.h"
#include "core/shader_module.h"
#include "filesystem/legacy.h"
#include "gltf_loader.h"
#include "gui.h"
#include "stats/stats.h"
CommandBufferUsage::CommandBufferUsage()
{
auto &config = get_configuration();
config.insert<vkb::IntSetting>(0, gui_secondary_cmd_buf_count, 0);
config.insert<vkb::BoolSetting>(0, gui_multi_threading, false);
config.insert<vkb::IntSetting>(0, gui_command_buffer_reset_mode, 0);
config.insert<vkb::IntSetting>(1, gui_secondary_cmd_buf_count, 8);
config.insert<vkb::BoolSetting>(1, gui_multi_threading, true);
config.insert<vkb::IntSetting>(1, gui_command_buffer_reset_mode, 0);
config.insert<vkb::IntSetting>(2, gui_secondary_cmd_buf_count, 16);
config.insert<vkb::BoolSetting>(2, gui_multi_threading, true);
config.insert<vkb::IntSetting>(2, gui_command_buffer_reset_mode, 1);
config.insert<vkb::IntSetting>(3, gui_secondary_cmd_buf_count, 32);
config.insert<vkb::BoolSetting>(3, gui_multi_threading, true);
config.insert<vkb::IntSetting>(3, gui_command_buffer_reset_mode, 2);
}
bool CommandBufferUsage::prepare(const vkb::ApplicationOptions &options)
{
if (!VulkanSample::prepare(options))
{
return false;
}
load_scene("scenes/bonza/Bonza4X.gltf");
auto &camera_node = vkb::add_free_camera(get_scene(), "main_camera", get_render_context().get_surface_extent());
camera = dynamic_cast<vkb::sg::PerspectiveCamera *>(&camera_node.get_component<vkb::sg::Camera>());
vkb::ShaderSource vert_shader("base.vert.spv");
vkb::ShaderSource frag_shader("base.frag.spv");
auto scene_subpass =
std::make_unique<ForwardSubpassSecondary>(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_subpass));
set_render_pipeline(std::move(render_pipeline));
get_stats().request_stats({vkb::StatIndex::frame_times, vkb::StatIndex::cpu_cycles});
create_gui(*window, &get_stats());
// Adjust the maximum number of secondary command buffers
// In this sample, only the recording of opaque meshes will be multi-threaded
auto is_opaque = [](vkb::sg::SubMesh *sub_mesh) {
return sub_mesh->get_material()->alpha_mode != vkb::sg::AlphaMode::Blend;
};
auto count_opaque_submeshes = [is_opaque](uint32_t accumulated, const vkb::sg::Mesh *mesh) -> uint32_t {
return accumulated + vkb::to_u32(mesh->get_nodes().size() * std::count_if(mesh->get_submeshes().begin(), mesh->get_submeshes().end(), is_opaque));
};
const auto &mesh_components = get_scene().get_components<vkb::sg::Mesh>();
const uint32_t opaque_mesh_count = std::accumulate(mesh_components.begin(), mesh_components.end(), 0, count_opaque_submeshes);
max_secondary_command_buffer_count = std::min(opaque_mesh_count, max_secondary_command_buffer_count);
// Show number of opaque meshes in debug window
get_debug_info().insert<vkb::field::Static, uint32_t>("opaque_mesh_count", opaque_mesh_count);
return true;
}
void CommandBufferUsage::prepare_render_context()
{
max_thread_count = std::max(std::thread::hardware_concurrency(), MIN_THREAD_COUNT);
get_render_context().prepare(max_thread_count);
}
void CommandBufferUsage::update(float delta_time)
{
// don't call the parent's update, because it's done differently here... but call the grandparent's update for fps logging
vkb::Application::update(delta_time);
auto &subpass_state = static_cast<ForwardSubpassSecondary *>(get_render_pipeline().get_active_subpass().get())->get_state();
// Process GUI input
subpass_state.secondary_cmd_buf_count = vkb::to_u32(gui_secondary_cmd_buf_count);
use_secondary_command_buffers = subpass_state.secondary_cmd_buf_count > 0;
// If there are not enough command buffers to keep all threads busy, use fewer threads
subpass_state.thread_count = std::min(subpass_state.secondary_cmd_buf_count, max_thread_count);
subpass_state.command_buffer_reset_mode = static_cast<vkb::CommandBufferResetMode>(gui_command_buffer_reset_mode);
subpass_state.multi_threading = gui_multi_threading;
auto &render_context = get_render_context();
update_scene(delta_time);
update_gui(delta_time);
auto primary_command_buffer = render_context.begin(subpass_state.command_buffer_reset_mode);
update_stats(delta_time);
primary_command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
get_stats().begin_sampling(*primary_command_buffer);
draw(*primary_command_buffer, render_context.get_active_frame().get_render_target());
get_stats().end_sampling(*primary_command_buffer);
primary_command_buffer->end();
render_context.submit(primary_command_buffer);
}
void CommandBufferUsage::draw_gui()
{
const bool landscape = camera->get_aspect_ratio() > 1.0f;
uint32_t lines = landscape ? 3 : 5;
const auto &subpass = static_cast<ForwardSubpassSecondary *>(get_render_pipeline().get_active_subpass().get());
get_gui().show_options_window(
/* body = */
[&]() {
// Secondary command buffer count
ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.55f);
ImGui::SliderInt("##secCmdBufs", &gui_secondary_cmd_buf_count, 0, max_secondary_command_buffer_count, "Secondary CmdBuffs: %d");
ImGui::SameLine();
ImGui::Text("Draws/buf: %.1f", subpass->get_avg_draws_per_buffer());
// Multi-threading (no effect if 0 secondary command buffers)
ImGui::Checkbox("Multi-threading", &gui_multi_threading);
ImGui::SameLine();
ImGui::Text("(%d threads)", subpass->get_state().thread_count);
// Buffer management options
ImGui::RadioButton(
"Allocate and free", &gui_command_buffer_reset_mode, static_cast<int>(vkb::CommandBufferResetMode::AlwaysAllocate));
if (landscape)
{
ImGui::SameLine();
}
ImGui::RadioButton(
"Reset buffer", &gui_command_buffer_reset_mode, static_cast<int>(vkb::CommandBufferResetMode::ResetIndividually));
if (landscape)
{
ImGui::SameLine();
}
ImGui::RadioButton(
"Reset pool", &gui_command_buffer_reset_mode, static_cast<int>(vkb::CommandBufferResetMode::ResetPool));
},
/* lines = */ lines);
}
void CommandBufferUsage::render(vkb::core::CommandBufferC &primary_command_buffer)
{
if (has_render_pipeline())
{
if (use_secondary_command_buffers)
{
// The user will set the number of secondary command buffers used for opaque meshes
// There will be additional buffers for transparent meshes and for the GUI
get_render_pipeline().draw(
primary_command_buffer, get_render_context().get_active_frame().get_render_target(), VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS);
}
else
{
get_render_pipeline().draw(primary_command_buffer, get_render_context().get_active_frame().get_render_target(), VK_SUBPASS_CONTENTS_INLINE);
}
}
}
void CommandBufferUsage::draw_renderpass(vkb::core::CommandBufferC &primary_command_buffer, vkb::RenderTarget &render_target)
{
const auto &subpass = static_cast<ForwardSubpassSecondary *>(get_render_pipeline().get_active_subpass().get());
auto &extent = render_target.get_extent();
VkViewport viewport{};
viewport.width = static_cast<float>(extent.width);
viewport.height = static_cast<float>(extent.height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
primary_command_buffer.set_viewport(0, {viewport});
subpass->set_viewport(viewport);
VkRect2D scissor{};
scissor.extent = extent;
primary_command_buffer.set_scissor(0, {scissor});
subpass->set_scissor(scissor);
render(primary_command_buffer);
// Draw gui
if (has_gui())
{
if (use_secondary_command_buffers)
{
const auto &queue = get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
auto secondary_command_buffer = get_render_context()
.get_active_frame()
.get_command_pool(queue, subpass->get_state().command_buffer_reset_mode)
.request_command_buffer(VK_COMMAND_BUFFER_LEVEL_SECONDARY);
secondary_command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT | VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT, &primary_command_buffer);
secondary_command_buffer->set_viewport(0, {viewport});
secondary_command_buffer->set_scissor(0, {scissor});
get_gui().draw(*secondary_command_buffer);
secondary_command_buffer->end();
primary_command_buffer.execute_commands(*secondary_command_buffer);
}
else
{
get_gui().draw(primary_command_buffer);
}
}
primary_command_buffer.end_render_pass();
}
CommandBufferUsage::ForwardSubpassSecondary::ForwardSubpassSecondary(vkb::RenderContext &render_context,
vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader, vkb::sg::Scene &scene_, vkb::sg::Camera &camera) :
vkb::ForwardSubpass{render_context, std::move(vertex_shader), std::move(fragment_shader), scene_, camera}
{
}
void CommandBufferUsage::ForwardSubpassSecondary::record_draw(vkb::core::CommandBufferC &command_buffer,
const std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> &nodes,
uint32_t mesh_start,
uint32_t mesh_end,
size_t thread_index)
{
command_buffer.set_color_blend_state(color_blend_state);
command_buffer.set_depth_stencil_state(get_depth_stencil_state());
command_buffer.bind_lighting(get_lighting_state(), 0, 4);
assert(mesh_end <= nodes.size());
for (uint32_t i = mesh_start; i < mesh_end; i++)
{
update_uniform(command_buffer, *nodes[i].first, thread_index);
draw_submesh(command_buffer, *nodes[i].second);
}
}
std::shared_ptr<vkb::core::CommandBufferC>
CommandBufferUsage::ForwardSubpassSecondary::record_draw_secondary(vkb::core::CommandBufferC &primary_command_buffer,
const std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> &nodes,
uint32_t mesh_start,
uint32_t mesh_end,
size_t thread_index)
{
const auto &queue = get_render_context().get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
auto secondary_command_buffer = get_render_context()
.get_active_frame()
.get_command_pool(queue, state.command_buffer_reset_mode, thread_index)
.request_command_buffer(VK_COMMAND_BUFFER_LEVEL_SECONDARY);
secondary_command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT | VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT, &primary_command_buffer);
secondary_command_buffer->set_viewport(0, {viewport});
secondary_command_buffer->set_scissor(0, {scissor});
record_draw(*secondary_command_buffer, nodes, mesh_start, mesh_end, thread_index);
secondary_command_buffer->end();
return secondary_command_buffer;
}
void CommandBufferUsage::ForwardSubpassSecondary::draw(vkb::core::CommandBufferC &primary_command_buffer)
{
std::multimap<float, std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> opaque_nodes;
std::multimap<float, std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> transparent_nodes;
get_sorted_nodes(opaque_nodes, transparent_nodes);
// Sort opaque objects in front-to-back order
// Note: sorting objects does not help on PowerVR, so it can be avoided to save CPU cycles
std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> sorted_opaque_nodes;
for (auto node_it = opaque_nodes.begin(); node_it != opaque_nodes.end(); node_it++)
{
sorted_opaque_nodes.push_back(node_it->second);
}
const auto opaque_submeshes = vkb::to_u32(sorted_opaque_nodes.size());
// Sort transparent objects in back-to-front order
std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> sorted_transparent_nodes;
for (auto node_it = transparent_nodes.rbegin(); node_it != transparent_nodes.rend(); node_it++)
{
sorted_transparent_nodes.push_back(node_it->second);
}
const auto transparent_submeshes = vkb::to_u32(sorted_transparent_nodes.size());
allocate_lights<vkb::ForwardLights>(scene.get_components<vkb::sg::Light>(), MAX_FORWARD_LIGHT_COUNT);
color_blend_attachment.blend_enable = VK_FALSE;
color_blend_state.attachments.resize(get_output_attachments().size());
color_blend_state.attachments[0] = color_blend_attachment;
// Draw opaque objects. Depending on the subpass state, use one or multiple
// command buffers, and one or multiple threads
const bool use_secondary_command_buffers = state.secondary_cmd_buf_count > 0;
std::vector<std::shared_ptr<vkb::core::CommandBufferC>> secondary_command_buffers;
avg_draws_per_buffer = (state.secondary_cmd_buf_count > 0) ? static_cast<float>(opaque_submeshes) / state.secondary_cmd_buf_count : 0;
if (state.thread_count != thread_pool.size())
{
thread_pool.resize(state.thread_count);
}
if (use_secondary_command_buffers)
{
std::vector<std::future<std::shared_ptr<vkb::core::CommandBufferC>>> secondary_cmd_buf_futures;
// Save the number of draws left over, these will be distributed among the first buffers
uint32_t draws_per_buffer = vkb::to_u32(std::floor(avg_draws_per_buffer));
uint32_t remainder_draws = opaque_submeshes % state.secondary_cmd_buf_count;
uint32_t mesh_start = 0;
for (uint32_t cb_count = 0; cb_count < state.secondary_cmd_buf_count; cb_count++)
{
// Latter command buffers may contain fewer draws
uint32_t mesh_end = std::min(opaque_submeshes, mesh_start + draws_per_buffer);
if (remainder_draws > 0)
{
mesh_end++;
remainder_draws--;
}
if (state.multi_threading)
{
auto fut = thread_pool.push(std::bind(&CommandBufferUsage::ForwardSubpassSecondary::record_draw_secondary,
this,
std::ref(primary_command_buffer),
std::cref(sorted_opaque_nodes),
mesh_start,
mesh_end,
std::placeholders::_1));
secondary_cmd_buf_futures.push_back(std::move(fut));
}
else
{
secondary_command_buffers.push_back(record_draw_secondary(primary_command_buffer, sorted_opaque_nodes, mesh_start, mesh_end));
}
mesh_start = mesh_end;
}
if (state.multi_threading)
{
for (auto &fut : secondary_cmd_buf_futures)
{
secondary_command_buffers.push_back(fut.get());
}
}
}
else
{
record_draw(primary_command_buffer, sorted_opaque_nodes, 0, opaque_submeshes);
}
// Enable alpha blending
color_blend_attachment.blend_enable = VK_TRUE;
color_blend_attachment.src_color_blend_factor = VK_BLEND_FACTOR_SRC_ALPHA;
color_blend_attachment.dst_color_blend_factor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
color_blend_attachment.src_alpha_blend_factor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
color_blend_state.attachments[0] = color_blend_attachment;
// Draw transparent objects
if (transparent_submeshes > 0)
{
if (use_secondary_command_buffers)
{
secondary_command_buffers.push_back(record_draw_secondary(primary_command_buffer, sorted_transparent_nodes, 0, transparent_submeshes));
}
else
{
record_draw(primary_command_buffer, sorted_transparent_nodes, 0, transparent_submeshes);
}
}
if (use_secondary_command_buffers)
{
primary_command_buffer.execute_commands(secondary_command_buffers);
}
}
void CommandBufferUsage::ForwardSubpassSecondary::set_viewport(VkViewport &viewport)
{
this->viewport = viewport;
}
void CommandBufferUsage::ForwardSubpassSecondary::set_scissor(VkRect2D &scissor)
{
this->scissor = scissor;
}
float CommandBufferUsage::ForwardSubpassSecondary::get_avg_draws_per_buffer() const
{
return avg_draws_per_buffer;
}
CommandBufferUsage::ForwardSubpassSecondaryState &CommandBufferUsage::ForwardSubpassSecondary::get_state()
{
return state;
}
std::unique_ptr<vkb::VulkanSampleC> create_command_buffer_usage()
{
return std::make_unique<CommandBufferUsage>();
}
@@ -0,0 +1,234 @@
/* Copyright (c) 2019-2025, Arm Limited and Contributors
*
* 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.
*/
#pragma once
#include "buffer_pool.h"
#include "common/utils.h"
#include "rendering/render_pipeline.h"
#include "rendering/subpasses/forward_subpass.h"
#include "scene_graph/components/material.h"
#include "scene_graph/components/mesh.h"
#include "scene_graph/components/perspective_camera.h"
#include "vulkan_sample.h"
class ThreadPool
{
public:
explicit ThreadPool() :
stop_flag(false)
{}
~ThreadPool()
{
shutdown();
}
template <class F, class... Args>
auto push(F &&f, Args &&...args) -> std::future<std::invoke_result_t<F, Args..., size_t>>
{
using return_type = std::invoke_result_t<F, Args..., size_t>;
auto task_ptr = std::make_shared<std::packaged_task<return_type(size_t)>>(std::bind(std::forward<F>(f), std::forward<Args>(args)..., std::placeholders::_1));
std::future<return_type> res = task_ptr->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace([task_ptr](size_t thread_index) { (*task_ptr)(thread_index); });
}
condition.notify_one();
return res;
}
void resize(size_t thread_count)
{
if (thread_count != workers.size())
{
shutdown();
for (size_t i = 0; i < thread_count; ++i)
{
workers.emplace_back([this, i] {
size_t thread_index = i;
while (true)
{
std::function<void(size_t)> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this] { return stop_flag || !tasks.empty(); });
if (stop_flag && tasks.empty())
return;
task = std::move(tasks.front());
tasks.pop();
}
task(thread_index);
}
});
}
}
}
void shutdown()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop_flag = true;
}
condition.notify_all();
for (auto &worker : workers)
worker.join();
workers.clear();
stop_flag = false;
}
size_t size() const
{
return workers.size();
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void(size_t)>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
std::atomic<bool> stop_flag;
};
/**
* @brief Sample showing the use of secondary command buffers for
* multi-threaded recording, as well as the different
* strategies for recycling command buffers every frame
*/
class CommandBufferUsage : public vkb::VulkanSampleC
{
public:
CommandBufferUsage();
virtual ~CommandBufferUsage() = default;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void update(float delta_time) override;
/**
* @brief Helper structure used to set subpass state
*/
struct ForwardSubpassSecondaryState
{
uint32_t secondary_cmd_buf_count = 0;
vkb::CommandBufferResetMode command_buffer_reset_mode = vkb::CommandBufferResetMode::ResetPool;
bool multi_threading = false;
uint32_t thread_count = 0;
};
/**
* @brief Overrides the draw method to allow for dividing draw calls
* into multiple secondary command buffers, optionally
* in different threads
*/
class ForwardSubpassSecondary : public vkb::ForwardSubpass
{
public:
ForwardSubpassSecondary(vkb::RenderContext &render_context,
vkb::ShaderSource &&vertex_source, vkb::ShaderSource &&fragment_source,
vkb::sg::Scene &scene, vkb::sg::Camera &camera);
void draw(vkb::core::CommandBufferC &primary_command_buffer) override;
void set_viewport(VkViewport &viewport);
void set_scissor(VkRect2D &scissor);
float get_avg_draws_per_buffer() const;
ForwardSubpassSecondaryState &get_state();
private:
/**
* @brief Records the necessary commands to draw the specified range of scene meshes
* @param command_buffer The primary command buffer to record
* @param nodes The meshes to draw
* @param mesh_start Index to the first mesh to draw
* @param mesh_end Index to the mesh where recording will stop (not included)
* @param thread_index Identifies the resources allocated for this thread
*/
void record_draw(vkb::core::CommandBufferC &command_buffer,
const std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> &nodes,
uint32_t mesh_start,
uint32_t mesh_end,
size_t thread_index = 0);
/**
* @brief Records the necessary commands to draw the specified range of scene meshes
* The primary command buffer provided is used to initialize, record, end and return a
* pointer to a new secondary command buffer.
* @param primary_command_buffer The primary command buffer used to inherit a secondary
* @param nodes The meshes to draw
* @param mesh_start Index to the first mesh to draw
* @param mesh_end Index to the mesh where recording will stop (not included)
* @param thread_index Identifies the resources allocated for this thread
* @return a pointer to the recorded secondary command buffer
*/
std::shared_ptr<vkb::core::CommandBufferC> record_draw_secondary(vkb::core::CommandBufferC &primary_command_buffer,
const std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> &nodes,
uint32_t mesh_start,
uint32_t mesh_end,
size_t thread_index = 0);
VkViewport viewport{};
VkRect2D scissor{};
vkb::ColorBlendAttachmentState color_blend_attachment{};
vkb::ColorBlendState color_blend_state{};
ForwardSubpassSecondaryState state{};
float avg_draws_per_buffer{0};
ThreadPool thread_pool;
};
private:
virtual void prepare_render_context() override;
vkb::sg::PerspectiveCamera *camera{nullptr};
void render(vkb::core::CommandBufferC &command_buffer) override;
void draw_renderpass(vkb::core::CommandBufferC &primary_command_buffer, vkb::RenderTarget &render_target) override;
void draw_gui() override;
int gui_secondary_cmd_buf_count{0};
uint32_t max_secondary_command_buffer_count{100};
bool use_secondary_command_buffers{false};
int gui_command_buffer_reset_mode{0};
bool gui_multi_threading{false};
const uint32_t MIN_THREAD_COUNT{4};
uint32_t max_thread_count{0};
};
std::unique_ptr<vkb::VulkanSampleC> create_command_buffer_usage();
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

@@ -0,0 +1,36 @@
# Copyright (c) 2019-2025, Arm Limited and Contributors
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Arm"
NAME "Constant Data"
DESCRIPTION "Comparing the various ways of specifying shader constants."
SHADER_FILES_GLSL
"constant_data/push_constant_small.vert"
"constant_data/push_constant_large.vert"
"constant_data/push_constant.frag"
"constant_data/ubo_small.vert"
"constant_data/ubo_large.vert"
"constant_data/ubo.frag"
"constant_data/buffer_array.vert"
"constant_data/buffer_array.frag")
@@ -0,0 +1,455 @@
////
- Copyright (c) 2021-2024, Arm Limited and Contributors
-
- 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.
-
////
= Constant data in 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/constant_data[Khronos Vulkan samples github repository].
endif::[]
// omit in toc
:pp: {plus}{plus}
ifndef::site-gen-antora[]
== Contents
:toc:
endif::[]
== Overview
The Vulkan API exposes a few different ways in which we can send uniform data into our shaders.
There are enough methods that it raises the question "Which one is fastest?", and more often than not the answer is "It depends".
The main issue for developers is that the fastest methods may differ between the various vendors, so often there is no "one size fits all" solution.
This sample aims to highlight this issue, and help move the Vulkan ecosystem to a point where we are better equipped to solve this for developers.
This is done by having an interactive way to toggle different constant data methods that the Vulkan API expose to us.
This can then be run on a platform of the developers choice to see the performance implications that each of them bring.
== What is constant data?
=== Introduction
Constant data is a form of information that is supplied to the pipeline to help with shader computations.
In theory, this data can be anything we want it to be, for instance it can be used for things such as calculating where an object should be placed inside our world, or computing the overall brightness of an object based on the lights in the scene.
It differs from other data (e.g.
input vertex data) in the sense that it remains _constant_ across every shader invocation of a draw call.
This is important as because of this assumption, the data can be *shared* between shader stages, as we know it isn't going to be changed throughout the runtime of a single draw call in a render pipeline.
The next section aims to cover the constant data theory, starting at the shader level before moving to the basics of how to plug in your data using Vulkan.
=== Constant data in Vulkan shaders
Constant data is implemented in shader code by using global variables.
*Global variables* have the following format: `<layout> <storage> <type> <variable_name>`.
Take this vertex shader for example:
[,glsl]
----
layout(location = 0) in vec4 position;
layout(set = 0, binding = 0) uniform ConstantData
{
mat4 model;
} constant_data;
layout(location = 0) out vec4 o_pos;
----
We can see three global variables, each with a different *storage type*:
* Inputs (`in`)
* Uniforms (`uniform`)
* Outputs (`out`)
==== Varying Types
The global variables that use inputs (`in`) and outputs (`out`) are values that _may vary_ from one shader invocation to the next, therefore they *shouldn't* be used for constant data.
They require a `layout location` which is used to identify a particular input/output.
They have slightly different rules for what they do depending on the shader stage, and have slightly different restrictions on the types of data it can represent.
However, generally their use is to feed values from one stage to the next (e.g.
from vertex shader to fragment shader).
[NOTE]
You can read more about shader stage inputs and outputs link:++https://www.khronos.org/opengl/wiki/Type_Qualifier_(GLSL)#Shader_stage_inputs_and_outputs++[here].
==== Uniform Types
Uniform types are global variables that have either the `uniform` or `buffer` storage type, these are _uniform buffer objects_ and _shader storage buffer objects_ respectively.
They describe data which remains constant across an entire draw call, meaning that the values stay the same across the different shader stages and shader invocations.
These values use a `layout binding` and, when working with multiple ``VkDescriptorSet``s, we will also give it a `layout set`.
*Uniform buffer objects (UBOs)* are the more commonly used of the two.
They are _read-only_ buffers, so trying to edit them in shader code will result in a compile-time error.
*Shader storage buffer objects (SSBOs)* are like special types of uniform buffer objects, denoted by the storage type `buffer`.
Unlike UBOs they can be written to, meaning the values _can_ be changed in the shaders so therefore they don't always represent data that is constant.
Having said this, depending on the implementation, they generally can hold a lot more data as opposed to UBOs.
[NOTE]
To check how much data we can store in uniform buffers and storage buffers, you can query the physical device for its `VkPhysicalDeviceLimits` and check the values `maxUniformBufferRange` and `maxStorageBufferRange` respectively._
==== Interface Blocks
To implement our constant data we have to use an interface block.
Interface blocks in shader code are used to group multiple global variables of the same `<storage>` type, so in theory they aren't necessarily solely for constant data.
For example:
[,glsl]
----
layout(set = 0, binding = 0) uniform PerMeshData
{
vec4 camera_position;
mat4 model_matrix;
vec3 mesh_color;
}
per_mesh_data;
----
Interface blocks are still global variables, and technically still follow the global variable format that was mentioned at the start of this chapter.
However, the difference is that they have to be given a user-defined type.
They work exactly the same way as a `struct` in GLSL/C{pp}.
For example, to access the model matrix in this interface block, you'd use `per_mesh_data.model_matrix`.
You can read more about interface blocks link:https://www.khronos.org/opengl/wiki/Type_Qualifier_(GLSL)#Shader_stage_inputs_and_outputs[here].
=== Vulkan API
We've covered how constant data is implemented in the shader, however to push the data from the application to the shader we need to use Vulkan.
We do this mainly with the use of ``VkBuffer``s, which is Vulkan's implementation of buffer memory.
Buffers in Vulkan are just chunks of memory used for storing data, which can be read by the GPU.
They need to be created and have their memory _manually_ allocated, and then we can copy our constant data into the allocated memory.
This data can then be plugged into the draw calls, so that it can finally be used in our shader computations.
[NOTE]
The library https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator[Vulkan Memory Allocator (VMA)] is extremely good for handling a lot of the common pitfalls that come with managing your Vulkan memory, without removing the control that you would otherwise have with native Vulkan.
The following links are useful for learning how to create a Vulkan buffer in your application:
* https://docs.vulkan.org/tutorial/latest/04_Vertex_buffers/01_Vertex_buffer_creation.html[Vulkan Tutorial vertex buffer creation]
* https://docs.vulkan.org/tutorial/latest/04_Vertex_buffers/02_Staging_buffer.html#_abstracting_buffer_creation[Vulkan tutorial abstracting buffer creation]
* https://docs.vulkan.org/tutorial/latest/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.html#_uniform_buffer[Vulkan tutorial uniform buffer]
==== The Methods
There are various ways to push your constant data, where this tutorial will cover a subset of these methods.
However the Vulkan API gives a lot of flexibility about how to handle *descriptor sets*, offering many different types and different ways to bind and use them (especially when we factor in extensions).
This can puzzle developers about which is best, and for which scenarios.
This tutorial aims to ease some of the confusion and uncertainty around this subject.
When we break this down, we have the following methods:
* Push Constants
* Descriptor Sets
* Dynamic Descriptor Sets
* Update-after-bind Descriptor Sets
* Buffer array with dynamic indexing
* https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VK_EXT_inline_uniform_block.html[Inline uniform buffer objects] (click to read more)
* https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VK_KHR_push_descriptor.html[Push descriptors] (click to read more)
*Inline uniform buffer objects* and *push descriptors* are not covered by this tutorial, please use the links above to learn more about them.
== Sample Overview
=== Introduction
The sample uses a mesh heavy scene which has 1856 meshes (475 KB of mesh data).
This is to demonstrate a use case where many different calls to pushing constant data will occur during a single frame.
This is to artificially exaggerate the performance delta.
The constant data that is being sent is the per-mesh model matrix, the camera view projection matrix, a scale matrix and some extra padding.
If the GPU doesn't support at least 256 bytes of push constants, it will instead push 128 bytes (it won't include the scale matrix and the extra padding).
A performance graph is displayed at the top with two charts, one showing frame time, and one showing the load/store cycles.
image::./images/graph_data.jpg[graph]
These two counters will show the CPU and GPU cost respectively, so when you go to toggle the different method you can see how it changes.
=== Controls
The options presented to the user lets them change the method by which we push the MVP data.
image::./images/controls.png[sample]
It is important to note that the configuration adapts to the device and GPU.
This is so if an extension isn't supported, the related option will no longer show.
You can check the console log to see a warning message detailing what features were disabled and why.
When an option is changed, the descriptor sets are flushed and recreated with their new setup, and the respective render pipeline/subpass.
== Push Constants
=== Introduction
Push constants are usually the first method newer Vulkan programmers will stumble upon when beginning to work with constant data.
They are straightforward to use and integrate nicely into any codebase, making them a great option to send simple data to your shaders.
A downside to push constants is that on some platforms they have strict limitations on how much data can be sent.
The Vulkan spec guarantees that drivers will support at least 128 bytes of push constants.
Many modern implementations of Vulkan will commonly support 256 bytes and sometimes much more.
[NOTE]
To determine how many bytes your system supports, you can query the physical device for its `VkPhysicalDeviceLimits` and check the value `maxPushConstantsSize`.
Having said this, 128/256 bytes is still a useful amount of data, even if it isn't exactly scalable to a full game scenario.
In the case of 128 bytes, we can at least send two float 4x4 matrices (2 * 4 * 16 = 128).
This, for example, can hold our world matrix and our view-projection matrix.
So that the shader can understand where this data will be sent, we specify a special push constants `<layout>` in our shader code.
For example:
[,glsl]
----
layout(push_constant) uniform MeshData
{
mat4 model;
} mesh_data;
----
To then send the push constant data to the shader we use the https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#vkCmdPushConstants[`vkCmdPushConstants`] function:
----
void vkCmdPushConstants(
VkCommandBuffer commandBuffer,
VkPipelineLayout layout,
VkShaderStageFlags stageFlags,
uint32_t offset,
uint32_t size,
const void* pValues);
----
=== Performance
image::./images/push_constants_performance.jpg[push constants]
In early implementations of Vulkan on Arm Mali, this was usually the fastest way of pushing data to your shaders.
In more recent times, we have observed on Mali devices that _overall_ they can be slower.
If performance is something you are trying to maximise on Mali devices, descriptor sets may be the way to go.
However, other devices may still favour push constants.
Having said this, descriptor sets are one of the more complex features of Vulkan, making the convenience of push constants still worth considering as a go-to method, especially if working with trivial data.
Scroll down for a comparison with static descriptor sets.
== Descriptor Sets
=== Introduction
In Vulkan, resources are exposed to shaders by the use of *resource descriptors*.
A *resource descriptor* (or *descriptor* for short) is a way for a shader to access a resource such as a buffer or an image.
These *descriptors* are simple structures holding a pointer to the resource it is "describing", along with an associated _resource binding_ so that when we execute a draw call the shader knows where to look for the resource.
A collection of **descriptor**s are called a *descriptor set*, which itself will have an associated _set binding_.
For example, if we take this line of shader code:
----
layout(set = 0, binding = 0) uniform ConstantData
{
mat4 model;
} constant_data;
----
The `set` value maps to the _set binding_, and the `binding` value maps to the _resource binding_.
So therefore we can deduce that for this shader we'd need a pipeline that has one descriptor set with one binding (0 and 0 respectively).
To create a *descriptor set*, we need to allocate it from a *descriptor set pool* and give it a specific *descriptor set layout*.
After a *descriptor set* is allocated, it needs to be updated with the *descriptors*.
The update process requires us to specify a list of *write operations*, where a write operation is a https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkWriteDescriptorSet.html[`VkWriteDescriptorSet`] struct.
Then the valid *descriptor set* is bound to a command buffer so that when `vkCmdDraw*()` commands are run, the right resources are made available in the GPU.
==== Buffer Object
For all the descriptor set sections below, we will use one such resource known as a *buffer object*, as this will be what we use to store our MVP data.
A *buffer object* in Vulkan is a type of `VkBuffer`, created with the respective buffer usage flag.
For uniform buffer objects we use the `VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT` flag, and for shader storage buffer objects we use the `VK_BUFFER_USAGE_STORAGE_BUFFER_BIT` flag.
These map to the shader `<storage>` type ``uniform``s and ``buffer``s respectively.
=== Performance
image::./images/descriptor_set_performance.jpg[single ubo]
While it is not straightforward to perform a 1:1 comparison between push constants and descriptor sets, the sample does show static descriptor sets outperforming push constants.
When comparing with <<push-constants,push constants>> on an Arm Mali GPU, we can see the frametime remains the same (16.7ms), however it is the load/store cycles we want to look at.
They drop from 266 k/s to 123 k/s, showing that the GPU is worked more in the case of push constants to achieve the same visual results.
== Dynamic Descriptor Sets
=== Introduction
Dynamic descriptor sets differ to the regular descriptor sets because they allow an offset to be specified when we are _binding_ (`vkCmdBindDescriptorSets`) the descriptor set.
This dynamic offset can be used in addition to the base offset used at the time of updating the descriptor set.
One case in which this can be useful is:
. Allocating one giant *uniform buffer object* containing all the world matrices of the meshes in your scene.
. Allocating a *descriptor set* with a binding containing the `VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC` flag that then points to the *UBO* you just created.
. In our draw call, for each mesh, _dynamically_ offset into the giant *uniform buffer object*.
=== Performance
image::./images/dynamic_descriptor_set_performance.jpg[dynamic ubo]
In the screenshot above (taken on an S10 with a Mali G76 GPU) we can see the load/store cycles stay roughly the same compared to <<uniform-buffer-objects,static uniform buffer objects>>.
However, the frame time goes up from 16.7 ms to 20.9 ms.
This is due to the extra time you need to spend every frame determining the dynamic offsets, that you need to send in the bind call (`vkCmdBindDescriptorSets`).
== Update-after-bind Descriptor Sets
=== Introduction
Traditionally, *descriptor sets* require updating before they are bound to a command buffer - any further updates after it is bound will invalidate the command buffer it is bound to.
However, this can be considered an "overly cautious" restriction when we realise that the command buffer isn't actually executed until it's submitted on a queue.
This is where newer versions of Vulkan have introduced the concept of "update after bind".
Essentially it adds in a binding flag to descriptor set layouts which allows the contents of the *descriptor set* to be updated up until the command buffer _is submitted to the queue_, rather than when the descriptor set is _bound to the command buffer_.
[NOTE]
Update-after-bind bindings cannot be used with dynamic descriptor sets.
=== Performance
This should come with zero performance costs, and as a result this method is designed purely for offering flexibility to your codebase.
== Buffer Object Arrays
=== Introduction
Another approach, which can be likened to a dynamic descriptor set, is a buffer object array.
This is the concept of allocating all of your constant data _upfront_ in a large buffer, and writing the entire buffer to a descriptor set.
This means in any one shader invocation we have access to all of the model data for the entire scene, at the benefit of only needing to bind one descriptor set per entire draw call.
You can use either a `uniform` or a `buffer` storage type in your shader code to achieve this.
However, since ``buffer``s can generally hold bigger amounts of data, this tutorial will use them.
_*Note:* If deciding to use a `uniform`, then the size of the array needs to be defined at compile time.
This can be achieved with a shader variant definition._
Here is an example of using a `buffer` in shader code:
[,glsl]
----
layout(set = 0, binding = 1) buffer MeshArray
{
mat4 model_matrices[];
} mesh_array;
----
Before you draw the scene, you create a `VkBuffer` with the `VK_BUFFER_USAGE_STORAGE_BUFFER_BIT` usage flag, and fill it with all the model matrices of each mesh in the scene.
Then to get the correct matrix inside our shader, we can pass a *dynamic index* to our draw call.
We do this by using the `gl_InstanceIndex` value.
For example, your shader code will look something like this:
----
mat4 model_matrix = mesh_array.model_matrices[gl_InstanceIndex];
out_pos = model_matrix * vec4(in_pos, 1.0);
----
To control the value of `gl_InstanceIndex` we use the `uint32_t firstInstance` parameter of the `vkCmdDraw*()` commands.
It's important to note that we can use other mechanisms to push this index to the shader, such as push constants.
For example, this `vkCmdDrawIndexed` is taken from the https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCmdDrawIndexed.html[Vulkan spec]:
----
void vkCmdDrawIndexed(
VkCommandBuffer commandBuffer,
uint32_t indexCount,
uint32_t instanceCount,
uint32_t firstIndex,
int32_t vertexOffset,
uint32_t firstInstance);
----
Here is some pseudo-code to show how the `vkCmdDrawIndexed` function is used, and also to describe how a generic scene render function will look:
----
uint32_t instance_index = 0;
vkCmdBindDescriptorSet(command_buffer, buffer_array_descriptor_set);
for(auto &mesh : meshes)
{
vkCmdBindVertexBuffer(command_buffer, mesh.vertex_buffer);
vkCmdBindIndexBuffer(command_buffer, mesh.index_buffer);
// This line does our drawing
vkCmdDrawIndexed(command_buffer, mesh.index_buffer.size(), 1, 0, 0, instance_index++);
}
----
In the code snippet above we can see that we bind our descriptor set once, and for each mesh bind its vertex and index buffers and then execute a draw call with an incrementing value for `uint32_t firstInstance`.
This `uint32_t` will be substituted in wherever `gl_InstanceIndex` exists in the shader code, which will pull out the required model matrix to position the mesh inside our world.
=== Performance
While this could be a fast method for some devices, on Mali it is not a recommend practice as it disables a compiler optimisation technique known as *pilot shaders*.
Pilot shaders are a technique that allows us to determine what calculations can be "piloted" into your GPU's register so that when the data needs to be read it doesn't take a full read cycle from the GPU RAM.
To show this here is a Streamline capture of a Mali G76, showing the read cycles for using a single descriptor set per mesh against the pre allocated buffer array:
image::./images/loadcycles.png[loadcycles]
A few different stats are affected in the Mali GPU by using this, but the main thing is the *full read* in the *Mali Core Load/Store Cycles*.
== Further reading
* The https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html[Vulkan 1.2 spec]
* "Writing an efficient Vulkan renderer" by Arseny Kapoulkine https://zeux.io/2020/02/27/writing-an-efficient-vulkan-renderer/
* Alexander Overvoorde's https://vulkan-tutorial.com/Uniform_buffers/Descriptor_layout_and_buffer[Vulkan Tutorial on Descriptors] guide
* Vulkan Fast Paths https://gpuopen.com/wp-content/uploads/2016/03/VulkanFastPaths.pdf
== Best practice summary
*Do*
* Do keep constant data small, where 128 bytes is a good rule of thumb.
* Do use push constants if you do not want to set up a descriptor set/UBO system.
* Do make constant data directly available in the shader if it is pre-determinable, such as with the use of specialization constants.
*Avoid*
* Avoid indexing in the shader if possible, such as dynamically indexing into `buffer` or `uniform` arrays, as this can disable shader optimisations in some platforms.
*Impact*
* Failing to use the correct method of constant data will negatively impact performance, causing either reduced FPS and/or increased BW and load/store activity.
* On Mali, register mapped uniforms are effectively free.
Any spilling to buffers in memory will increase load/store cache accesses to the per thread uniform fetches.
@@ -0,0 +1,496 @@
/* Copyright (c) 2019-2025, Arm Limited and Contributors
*
* SPDX-License-Identifier: MIT
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "constant_data.h"
#include "common/vk_common.h"
#include "filesystem/legacy.h"
#include "gltf_loader.h"
#include "gui.h"
#include "rendering/pipeline_state.h"
#include "rendering/render_context.h"
#include "rendering/render_pipeline.h"
#include "rendering/subpasses/forward_subpass.h"
#include "rendering/subpasses/geometry_subpass.h"
#include "rendering/subpasses/lighting_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"
#include "scene_graph/components/pbr_material.h"
#include "scene_graph/components/texture.h"
#include "scene_graph/components/transform.h"
#include "scene_graph/node.h"
#include "scene_graph/scene.h"
#include "stats/stats.h"
namespace
{
/**
* @brief Helper function to fill the contents of the MVPUniform struct with the transform of the node and the camera view-projection matrix.
*/
inline MVPUniform fill_mvp(vkb::sg::Node &node, vkb::sg::Camera &camera)
{
MVPUniform mvp;
auto &transform = node.get_transform();
mvp.model = transform.get_world_matrix();
mvp.camera_view_proj = vkb::rendering::vulkan_style_projection(camera.get_projection()) * camera.get_view();
mvp.scale = glm::mat4(1.0f);
mvp.padding = glm::mat4(1.0f);
return mvp;
}
} // namespace
ConstantData::ConstantData()
{
auto &config = get_configuration();
// Set all types as a configuration, the sample should no-op on the types that aren't supported (i.e. update after binds)
for (size_t i = 0; i < methods.size(); ++i)
{
config.insert<vkb::IntSetting>(vkb::to_u32(i), gui_method_value, vkb::to_u32(i));
}
// Request sample-specific extensions as optional
add_instance_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, true);
add_device_extension(VK_KHR_MAINTENANCE3_EXTENSION_NAME, true);
add_device_extension(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME, true);
}
bool ConstantData::prepare(const vkb::ApplicationOptions &options)
{
if (!VulkanSample::prepare(options))
{
return false;
}
// If descriptor indexing and its dependencies were enabled, then we can mark the update after bind method as supported
if (get_instance().is_enabled(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME) &&
get_device().is_extension_enabled(VK_KHR_MAINTENANCE3_EXTENSION_NAME) &&
get_device().is_extension_enabled(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME))
{
methods[Method::UpdateAfterBindDescriptorSets].supported = true;
}
else
{
LOGW("Update-after-bind descriptor sets are not supported by your device, this sample option will be disabled.");
}
// Load a scene from the assets folder
load_scene("scenes/bonza/Bonza4X.gltf");
// Attach a move script to the camera component in the scene
auto &camera_node = vkb::add_free_camera(get_scene(), "main_camera", get_render_context().get_surface_extent());
camera = dynamic_cast<vkb::sg::PerspectiveCamera *>(&camera_node.get_component<vkb::sg::Camera>());
// Create the render pipelines
// Shader data passing depends on max. push constant size of the implementation
auto push_constant_limit = get_device().get_gpu().get_properties().limits.maxPushConstantsSize;
push_constant_render_pipeline = create_render_pipeline<PushConstantSubpass>(push_constant_limit >= 256 ? "constant_data/push_constant_large.vert.spv" : "constant_data/push_constant_small.vert.spv", "constant_data/push_constant.frag.spv");
descriptor_set_render_pipeline = create_render_pipeline<DescriptorSetSubpass>(push_constant_limit >= 256 ? "constant_data/ubo_large.vert.spv" : "constant_data/ubo_small.vert.spv", "constant_data/ubo.frag.spv");
buffer_array_render_pipeline = create_render_pipeline<BufferArraySubpass>("constant_data/buffer_array.vert.spv", "constant_data/buffer_array.frag.spv");
// Add a GUI with the stats you want to monitor
get_stats().request_stats(std::set<vkb::StatIndex>{vkb::StatIndex::frame_times, vkb::StatIndex::gpu_load_store_cycles});
create_gui(*window, &get_stats());
return true;
}
void ConstantData::request_gpu_features(vkb::PhysicalDevice &gpu)
{
if (gpu.get_features().vertexPipelineStoresAndAtomics)
{
gpu.get_mutable_requested_features().vertexPipelineStoresAndAtomics = VK_TRUE;
}
else
{
throw std::runtime_error("Requested required feature <VkPhysicalDeviceFeatures::vertexPipelineStoresAndAtomics> is not supported");
}
}
void ConstantData::draw_renderpass(vkb::core::CommandBufferC &command_buffer, vkb::RenderTarget &render_target)
{
auto &extent = render_target.get_extent();
VkViewport viewport{};
viewport.width = static_cast<float>(extent.width);
viewport.height = static_cast<float>(extent.height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
command_buffer.set_viewport(0, {viewport});
VkRect2D scissor{};
scissor.extent = extent;
command_buffer.set_scissor(0, {scissor});
// Get the selected method from the GUI - ensuring that it is also supported
auto selected_method = get_active_method();
// Only draw if using a defined method
if (selected_method != Undefined)
{
// If the GUI dropdown value is changed by the user, then handle updating the subpasses and sample state
if (gui_method_value != last_gui_method_value)
{
// Clear the descriptor sets for all render frames so that they recreate properly
get_device().wait_idle();
for (auto &render_frame : get_render_context().get_render_frames())
{
render_frame->clear_descriptors();
}
// If we are using a descriptor set method, we need to pass the method to the descriptor set pipeline
if (selected_method != Method::PushConstants && selected_method != Method::BufferArray)
{
auto &subpasses = descriptor_set_render_pipeline->get_subpasses();
for (auto &subpass : subpasses)
{
if (auto ubo_subpass = dynamic_cast<DescriptorSetSubpass *>(subpass.get()))
{
// We store the method so the subpass can apply the right resource tags
ubo_subpass->method = selected_method;
}
}
// Prepare all the subpasses again
descriptor_set_render_pipeline->prepare();
}
// Set the command buffer to enable updating update-after-bind bindings if we are using update-after-binds
command_buffer.set_update_after_bind(selected_method == Method::UpdateAfterBindDescriptorSets);
last_gui_method_value = gui_method_value;
}
// Choose the correct dedicated pipeline to draw to the render target
if (selected_method == Method::PushConstants)
{
push_constant_render_pipeline->draw(command_buffer, render_target);
}
else if (selected_method == Method::BufferArray)
{
buffer_array_render_pipeline->draw(command_buffer, render_target);
}
else
{
// The descriptor set pipeline has the active method stored for later
descriptor_set_render_pipeline->draw(command_buffer, render_target);
}
if (has_gui())
{
get_gui().draw(command_buffer);
}
// Update the remaining bindings on all the descriptor sets
if (selected_method == Method::UpdateAfterBindDescriptorSets)
{
get_render_context().get_active_frame().update_descriptor_sets();
}
command_buffer.end_render_pass();
}
}
inline ConstantData::Method ConstantData::get_active_method()
{
auto selected_method_it = methods.find(static_cast<Method>(gui_method_value));
// If method couldn't be found, or it isn't supported we set iterator to the start
if (selected_method_it == methods.end() || !selected_method_it->second.supported)
{
return Method::Undefined;
}
return selected_method_it->first;
}
void ConstantData::draw_gui()
{
auto lines = 1;
if (camera->get_aspect_ratio() < 1.0f)
{
// In portrait, show buttons below heading
lines = lines * 2;
}
get_gui().show_options_window(
/* body = */ [this]() {
// Create a line for every config
ImGui::Text("Method of pushing MVP to shader:");
if (camera->get_aspect_ratio() > 1.0f)
{
// In landscape, show all options following the heading
ImGui::SameLine();
}
auto active_method = get_active_method();
// Create a radio button for every option
if (ImGui::BeginCombo("##constant-data-method", methods[active_method].description))
{
for (size_t i = 0; i < methods.size(); ++i)
{
auto &method = methods[static_cast<Method>(i)];
if (method.supported)
{
bool is_selected = active_method == static_cast<Method>(i);
if (ImGui::Selectable(method.description, is_selected))
{
gui_method_value = static_cast<int>(i);
}
if (is_selected)
{
ImGui::SetItemDefaultFocus();
}
}
}
ImGui::EndCombo();
}
},
/* lines = */ vkb::to_u32(lines));
}
std::unique_ptr<vkb::VulkanSampleC> create_constant_data()
{
return std::make_unique<ConstantData>();
}
void ConstantData::ConstantDataSubpass::prepare()
{
// Build all shader variance upfront
auto &device = get_render_context().get_device();
for (auto &mesh : meshes)
{
for (auto &sub_mesh : mesh->get_submeshes())
{
auto &variant = sub_mesh->get_mut_shader_variant();
auto &vert_module = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_VERTEX_BIT, get_vertex_shader(), variant);
auto &frag_module = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_FRAGMENT_BIT, get_fragment_shader(), variant);
}
}
}
void ConstantData::PushConstantSubpass::update_uniform(vkb::core::CommandBufferC &command_buffer,
vkb::sg::Node &node,
size_t thread_index)
{
mvp_uniform = fill_mvp(node, camera);
}
vkb::PipelineLayout &ConstantData::PushConstantSubpass::prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
const std::vector<vkb::ShaderModule *> &shader_modules)
{
/**
* POI
* Since this pipeline doesn't use any custom descriptor set layouts, we just request a pipeline layout without modifying the modules
*/
return command_buffer.get_device().get_resource_cache().request_pipeline_layout(shader_modules);
}
void ConstantData::PushConstantSubpass::prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh)
{
/**
* POI
* The mvp_uniform variable contains the scene graph node mvp data.
* Here we just simply record the vkCmdPushConstants command
*/
// Push 128 bytes of data
command_buffer.push_constants(mvp_uniform.model); // 64 bytes
command_buffer.push_constants(mvp_uniform.camera_view_proj); // 64 bytes
// If we can push another 128 bytes, push more as this will make the delta more prominent
if (struct_size == 256)
{
command_buffer.push_constants(mvp_uniform.scale); // 64 bytes
command_buffer.push_constants(mvp_uniform.padding); // 64 bytes
}
}
void ConstantData::DescriptorSetSubpass::update_uniform(vkb::core::CommandBufferC &command_buffer,
vkb::sg::Node &node,
size_t thread_index)
{
MVPUniform mvp;
auto &render_frame = get_render_context().get_active_frame();
auto &transform = node.get_transform();
auto allocation = render_frame.allocate_buffer(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, sizeof(MVPUniform), thread_index);
mvp = fill_mvp(node, camera);
// Ensure the container doesn't hold more bytes than are needed
auto data = vkb::to_bytes(mvp);
data.resize(struct_size);
allocation.update(data);
command_buffer.bind_buffer(allocation.get_buffer(), allocation.get_offset(), allocation.get_size(), 0, 1, 0);
}
vkb::PipelineLayout &ConstantData::DescriptorSetSubpass::prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
const std::vector<vkb::ShaderModule *> &shader_modules)
{
/**
* POI
* Based on the UBO setting enabled by the sample, we mark the MVPUniform with that particular mode
* so when the descriptor state is flushed the corresponding API method pushes the data to the shaders
*/
for (auto &shader_module : shader_modules)
{
if (method == Method::DescriptorSets)
{
shader_module->set_resource_mode("MVPUniform", vkb::ShaderResourceMode::Static);
}
else if (method == Method::DynamicDescriptorSets)
{
shader_module->set_resource_mode("MVPUniform", vkb::ShaderResourceMode::Dynamic);
}
else if (method == Method::UpdateAfterBindDescriptorSets)
{
shader_module->set_resource_mode("MVPUniform", vkb::ShaderResourceMode::UpdateAfterBind);
}
}
return command_buffer.get_device().get_resource_cache().request_pipeline_layout(shader_modules);
}
void ConstantData::DescriptorSetSubpass::prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh)
{
/**
* POI
* We want to disable push constants, so we override this function and intentionally do nothing (no-op)
*/
return;
}
void ConstantData::BufferArraySubpass::draw(vkb::core::CommandBufferC &command_buffer)
{
auto &render_frame = get_render_context().get_active_frame();
std::vector<MVPUniform> uniforms;
// Update with all mvp scene data
for (auto &mesh : meshes)
{
for (auto &node : mesh->get_nodes())
{
for (auto &submesh : mesh->get_submeshes())
{
uniforms.push_back(fill_mvp(*node, camera));
}
}
}
auto allocation = render_frame.allocate_buffer(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, sizeof(MVPUniform) * uniforms.size());
uint32_t offset = 0;
for (size_t i = 0; i < uniforms.size(); ++i)
{
// Push 128 bytes of data
allocation.update(uniforms[i].model, offset + 0); // Update bytes 0 - 63
allocation.update(uniforms[i].camera_view_proj, offset + 64); // Update bytes 64 - 127
allocation.update(uniforms[i].scale, offset + 128); // Update bytes 128 - 191
allocation.update(uniforms[i].padding, offset + 192); // Update bytes 192 - 255
offset += 256;
}
command_buffer.bind_buffer(allocation.get_buffer(), allocation.get_offset(), allocation.get_size(), 0, 1, 0);
// Reset the instance index back to 0 for each draw call
instance_index = 0;
allocate_lights<vkb::ForwardLights>(scene.get_components<vkb::sg::Light>(), MAX_FORWARD_LIGHT_COUNT);
command_buffer.bind_lighting(get_lighting_state(), 0, 4);
GeometrySubpass::draw(command_buffer);
}
void ConstantData::BufferArraySubpass::update_uniform(vkb::core::CommandBufferC &command_buffer,
vkb::sg::Node &node,
size_t thread_index)
{
/**
* POI
* We fill all uniform data before the draw, so we want this function to do nothing (no-op).
*/
return;
}
vkb::PipelineLayout &ConstantData::BufferArraySubpass::prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
const std::vector<vkb::ShaderModule *> &shader_modules)
{
/**
* POI
* Since this pipeline doesn't use any custom descriptor set layouts, we just request a pipeline layout without modifying the modules
*/
return command_buffer.get_device().get_resource_cache().request_pipeline_layout(shader_modules);
}
void ConstantData::BufferArraySubpass::prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh)
{
/**
* POI
* We want to disable push constants, so we override this function and intentionally do nothing (no-op)
*/
return;
}
void ConstantData::BufferArraySubpass::draw_submesh_command(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh)
{
/**
* POI
* We control the shader `gl_InstanceIndex` value with the last argument of the draw commands.
* The BufferArraySubpass stores a value `instance_index` which is cleared to 0 before each
* pass, and is incremented for each mesh that we draw with this function.
*
* We bind a storage buffer object containing all the uniform data we require for the entire scene
* in the right order, so the index's have to match that order of how the individual uniform data
* structs are packed in the buffer.
*/
if (sub_mesh.vertex_indices != 0)
{
// Bind index buffer of submesh
command_buffer.bind_index_buffer(*sub_mesh.index_buffer, sub_mesh.index_offset, sub_mesh.index_type);
command_buffer.draw_indexed(sub_mesh.vertex_indices, 1, 0, 0, instance_index++);
}
else
{
command_buffer.draw(sub_mesh.vertices_count, 1, 0, instance_index++);
}
}
@@ -0,0 +1,282 @@
/* Copyright (c) 2019-2025, Arm Limited and Contributors
*
* SPDX-License-Identifier: MIT
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "rendering/render_pipeline.h"
#include "rendering/subpasses/forward_subpass.h"
#include "scene_graph/components/camera.h"
#include "scene_graph/components/perspective_camera.h"
#include "vulkan_sample.h"
/**
* @brief This structure will be pushed in its entirety if 256 bytes of push constants
* are supported by the physical device, otherwise it will be trimmed to 128 bytes
* (i.e. only "model" and "camera_view_proj" will be pushed)
*
* The shaders will be compiled with a define to handle this difference.
*/
struct alignas(16) MVPUniform
{
glm::mat4 model;
glm::mat4 camera_view_proj;
glm::mat4 scale;
// This value is ignored by the shader and is just to increase bandwidth
glm::mat4 padding;
};
/**
* @brief Constant Data sample
*
* This sample is designed to show the different ways in which Vulkan can push constant data to the
* shaders.
*
* The current ways that are supported are:
* - Push Constants
* - Descriptor Sets
* - Dynamic Descriptor Sets
* - Update-after-bind Descriptor Sets
* - Pre-allocated buffer array
*
* The sample also shows the performance implications that these different methods would have on your
* application or game. These performance deltas may differ between platforms and vendors.
*
* The data structure used will be pushed in its entirety if 256 bytes of push constants
* are supported by the physical device, otherwise it will be trimmed to 128 bytes
* (i.e. only "model" and "camera_view_proj" will be pushed)
*
* The shaders will be compiled with a definition to handle this difference.
*/
class ConstantData : public vkb::VulkanSampleC
{
public:
/**
* @brief The sample supported methods of using constant data in shaders
*/
enum Method
{
PushConstants,
DescriptorSets,
DynamicDescriptorSets,
UpdateAfterBindDescriptorSets, // May be disabled if the device doesn't support
BufferArray,
Undefined
};
/**
* @brief Describes the properties of a method
*/
struct MethodProperties
{
const char *description;
bool supported{true};
};
ConstantData();
virtual ~ConstantData() = default;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
/**
* @brief The base subpass to help prepare the shader variants and store the push constant limit
*/
class ConstantDataSubpass : public vkb::ForwardSubpass
{
public:
ConstantDataSubpass(vkb::RenderContext &render_context, vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader, vkb::sg::Scene &scene, vkb::sg::Camera &camera) :
vkb::ForwardSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera)
{}
virtual void prepare() override;
uint32_t struct_size{128};
};
/**
* @brief A custom forward subpass to isolate just the use of push constants
*
* This subpass is intentionally set up with custom shaders that possess just a single push constant structure
*/
class PushConstantSubpass : public ConstantDataSubpass
{
public:
PushConstantSubpass(vkb::RenderContext &render_context, vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader, vkb::sg::Scene &scene, vkb::sg::Camera &camera) :
ConstantDataSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera)
{}
/**
* @brief Updates the MVP uniform member variable to then be pushed into the shader
*/
virtual void update_uniform(vkb::core::CommandBufferC &command_buffer, vkb::sg::Node &node, size_t thread_index) override;
/**
* @brief Overridden to intentionally disable any dynamic shader module updates
*/
virtual vkb::PipelineLayout &prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
const std::vector<vkb::ShaderModule *> &shader_modules) override;
/**
* @brief Overridden to push a custom data structure to the shader
*/
virtual void prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh) override;
// The MVP uniform data structure
MVPUniform mvp_uniform;
};
/**
* @brief A custom forward subpass to isolate just the use of uniform buffer objects
*
* This subpass is intentionally set up with custom shaders that possess just a single UBO binding
* The subpass will use the right UBO method (Static, Dynamic or Update-after-bind) based on its setting as set by the sample
*/
class DescriptorSetSubpass : public ConstantDataSubpass
{
public:
DescriptorSetSubpass(vkb::RenderContext &render_context, vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader, vkb::sg::Scene &scene, vkb::sg::Camera &camera) :
ConstantDataSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera)
{}
/**
* @brief Creates a buffer filled with the mvp data and binds it
*/
virtual void update_uniform(vkb::core::CommandBufferC &command_buffer, vkb::sg::Node &node, size_t thread_index) override;
/**
* @brief Dynamically retrieves the correct pipeline layout depending on the method of UBO
*/
virtual vkb::PipelineLayout &prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
const std::vector<vkb::ShaderModule *> &shader_modules) override;
/**
* @brief Overridden to intentionally disable any push constants
*/
virtual void prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh) override;
// The method by which the UBO subpass will operate
Method method;
};
/**
* @brief A custom forward subpass to isolate the use of a shader storage buffer object
*
* This subpass is intentionally set up with custom shaders that own just a buffer binding holding an array of mvp data
* The subpass will use instancing to index into the UBO array
*/
class BufferArraySubpass : public ConstantDataSubpass
{
public:
BufferArraySubpass(vkb::RenderContext &render_context, vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader, vkb::sg::Scene &scene, vkb::sg::Camera &camera) :
ConstantDataSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera)
{}
virtual void draw(vkb::core::CommandBufferC &command_buffer) override;
/**
* @brief No-op, uniform data is sent upfront before the draw call
*/
virtual void update_uniform(vkb::core::CommandBufferC &command_buffer, vkb::sg::Node &node, size_t thread_index) override;
/**
* @brief Returns a default pipeline layout
*/
virtual vkb::PipelineLayout &prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
const std::vector<vkb::ShaderModule *> &shader_modules) override;
/**
* @brief Overridden to intentionally disable any push constants
*/
virtual void prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh) override;
/**
* @brief Overridden to send an index
*/
virtual void draw_submesh_command(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh) override;
uint32_t instance_index{0};
};
template <typename T>
std::unique_ptr<vkb::RenderPipeline> create_render_pipeline(const std::string &vertex_shader, const std::string &fragment_shader)
{
static_assert(std::is_base_of<ConstantDataSubpass, T>::value, "T is an invalid type. Must be a derived class from ConstantDataSubpass");
vkb::ShaderSource vert_shader(vertex_shader);
vkb::ShaderSource frag_shader(fragment_shader);
auto subpass = std::make_unique<T>(get_render_context(), std::move(vert_shader), std::move(frag_shader), get_scene(), *camera);
// We want to check if the push constants limit can support the full 256 bytes
auto push_constant_limit = get_device().get_gpu().get_properties().limits.maxPushConstantsSize;
if (push_constant_limit >= 256)
{
subpass->struct_size = 256;
}
std::vector<std::unique_ptr<vkb::rendering::SubpassC>> subpasses{};
subpasses.push_back(std::move(subpass));
return std::make_unique<vkb::RenderPipeline>(std::move(subpasses));
}
private:
virtual void draw_gui() override;
virtual void draw_renderpass(vkb::core::CommandBufferC &command_buffer, vkb::RenderTarget &render_target) override;
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
/**
* @brief Helper function to determine the constant data method that is selected and supported by the sample
* @returns The method that is selected, otherwise push constants
*/
inline Method get_active_method();
vkb::sg::PerspectiveCamera *camera{};
// The render pipeline designed for using push constants
std::unique_ptr<vkb::RenderPipeline> push_constant_render_pipeline{nullptr};
// The render pipeline designed for using Descriptor Sets, Dynamic Descriptor Sets and Update-after-bind Descriptor Sets
std::unique_ptr<vkb::RenderPipeline> descriptor_set_render_pipeline{nullptr};
// The render pipeline designed for using a large shader storage buffer object that is instanced into to get the relevant MVP data
std::unique_ptr<vkb::RenderPipeline> buffer_array_render_pipeline{nullptr};
uint32_t max_push_constant_size{128};
// The samples constant data methods and their properties
std::unordered_map<Method, MethodProperties> methods = {
{Method::PushConstants, {"Push Constants"}},
{Method::DescriptorSets, {"Descriptor Sets"}},
{Method::DynamicDescriptorSets, {"Dynamic Descriptor Sets"}},
{Method::UpdateAfterBindDescriptorSets, {"Update-after-bind Descriptor Sets", false}},
{Method::BufferArray, {"Single Pre-allocated Buffer Array"}}};
int gui_method_value{static_cast<int>(Method::PushConstants)};
int last_gui_method_value{static_cast<int>(Method::PushConstants)};
};
std::unique_ptr<vkb::VulkanSampleC> create_constant_data();
Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

@@ -0,0 +1,30 @@
# Copyright (c) 2019-2021, Arm Limited and Contributors
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Arm"
NAME "Descriptor Management"
DESCRIPTION "Descriptor set management and buffer allocation strategies."
SHADER_FILES_GLSL
"base.vert"
"base.frag")
@@ -0,0 +1,156 @@
////
- Copyright (c) 2019-2024, Arm Limited and Contributors
-
- 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.
-
////
= Descriptor and buffer management
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/descriptor_management[Khronos Vulkan samples github repository].
endif::[]
== Overview
An application using Vulkan will have to implement a system to manage descriptor pools and sets.
The most straightforward and flexible approach is to re-create them for each frame, but doing so might be very inefficient, especially on mobile platforms.
The problem of descriptor management is intertwined with that of buffer management, that is choosing how to pack data in `VkBuffer` objects.
This tutorial will explore a few options to improve both descriptor and buffer management.
== The problem
When rendering dynamic objects the application will need to push some amount of per-object data to the GPU, such as the MVP matrix.
This data may not fit into the push constant limit for the device, so it becomes necessary to send it to the GPU by putting it into a `VkBuffer` and binding a descriptor set that points to it.
Materials also need their own descriptor sets, which point to the textures they use.
We can either bind per-material and per-object descriptor sets separately or collate them into a single set.
Either way, complex applications will have a large amount of descriptor sets that may need to change on the fly, for example due to textures being streamed in or out.
The simplest approach to circumvent the issue is to have one or more ``VkDescriptorPool``s per frame, reset them at the beginning of the frame and allocate the required descriptor sets from it.
This approach will consist of a https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetDescriptorPool.html[vkResetDescriptorPool()] call at the beginning, followed by a series of https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkAllocateDescriptorSets.html[vkAllocateDescriptorSets()] and https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkUpdateDescriptorSets.html[vkUpdateDescriptorSets()] to fill them with data.
The issue is that these calls can add a significant overhead to the CPU frame time, especially on mobile.
In the worst cases, for example calling https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkUpdateDescriptorSets.html[vkUpdateDescriptorSets()] for each draw call, the time it takes to update descriptors can be longer than the time of the draws themselves.
image::./images/bonza_no_caching_multiple_buf.jpg[Basic descriptor set management]
The sample highlights the issue with a draw-call intensive scene.
Frame time is around 44 ms (on a 2019 high-end mobile phone), corresponding to 23 FPS, with the simplest descriptor management scheme.
If you want to test the sample, make sure to set it in release mode and without validation layers.
Both these factors can significantly affect the results.
== Caching descriptor sets
A major way to reduce descriptor set updates is to re-use them as much as possible.
Instead of calling https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetDescriptorPool.html[vkResetDescriptorPool()] every frame, the app will keep the `VkDescriptorSet` handles stored with some caching mechanism to access them.
The cache could be a hashmap with the contents of the descriptor set (images, buffers) as key.
This approach is used in our framework by default.
It is possible to remove another level of indirection by storing descriptor sets handles directly in the materials and/or meshes.
Caching descriptor sets has a dramatic effect on frame time for our CPU-heavy scene:
image::./images/bonza_caching_multiple_buf.jpg[Descriptor set caching]
The frame time is now around 27 ms, corresponding to 37 FPS.
This is a 38% decrease in frame time.
We can confirm this behavior using https://developer.arm.com/tools-and-software/graphics-and-gaming/arm-mobile-studio/components/streamline-performance-analyzer[Streamline Performance Analyzer].
image::./images/streamline_desc_caching.png[Streamline analysis]
The first part of the trace until the marker is without descriptor set caching.
We can see that the app is CPU bound, since the GPU is idling between frames while the CPU is fully utilized.
After the marker we enable descriptor set caching and we can see that frames are processed faster.
GPU frame time does not change much and the app is still CPU bound, so the speedup is related to CPU-side improvements.
This system is reasonably easy to implement for a static scene, but it becomes harder when you need to delete descriptor sets.
Complex engines may implement techniques to figure out which descriptor sets have not been accessed for a certain number of frames, so they can be removed from the map.
This may correspond to calling https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkFreeDescriptorSets.html[vkFreeDescriptorSets()], but this solution poses another issue: in order to free individual descriptor sets the pool has to be created with the `VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT` flag.
Mobile implementations may use a simpler allocator if that flag is not set, relying on the fact that pool memory will only be recycled in block.
It is possible to avoid using that flag by updating descriptor sets instead of deleting them.
The application can keep track of recycled descriptor sets and re-use one of them when a new one is requested.
The xref:samples/performance/subpasses/README.adoc[subpasses sample] uses this approach when it re-creates the G-buffer images.
== Buffer management
Going back to the initial case, we will now explore an alternative approach, that is complementary to descriptor caching in some way.
Especially for applications in which descriptor caching is not quite feasible, buffer management is another lever for optimizing performance.
As discussed at the beginning, each rendered object will typically need some uniform data along with it, that needs to be pushed to the GPU somehow.
A straightforward approach is to store a `VkBuffer` per object and update that data for each frame.
This already poses an interesting question: is one buffer enough?
The problem is that this data will change dynamically and will be in use by the GPU while the frame is in flight.
Since we do not want to flush the GPU pipeline between each frame, we will need to keep several copies of each buffer, one for each frame in flight.
Another similar option is to use just one buffer per object, but with a size equal to `num_frames * buffer_size`, then offset it dynamically based on the frame index.
A similar approach is used in the default configuration of the sample.
For each frame, one buffer per object is created and filled with data.
This means that we will have many descriptor sets to create, since every object will need one that points to its `VkBuffer`.
Furthermore, we will have to update many buffers separately, meaning we cannot control their memory layout and we might lose some optimization opportunities with caching.
We can address both problems by reverting the approach: instead of having a `VkBuffer` per object containing per-frame data, we will have a `VkBuffer` per frame containing per-object data.
The buffer will be cleared at the beginning of the frame, then each object will record its data and will receive a dynamic offset to be used at https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdBindDescriptorSets.html[vkCmdBindDescriptorSets()] time.
With this approach we will need less descriptor sets, as more objects can share the same one: they will all reference the same `VkBuffer`, but at different dynamic offsets.
Furthermore, we can control the memory layout within the buffer.
image::./images/bonza_no_caching_single_buf.jpg[Using one large VkBuffer]
Using a single large `VkBuffer` in this case shows a performance improvement similar to descriptor set caching.
For this relatively simple scene stacking the two approaches does not provide a further performance boost, but for a more complex case they do stack nicely:
* Descriptor caching is necessary when the number of descriptors sets is not just due to ``VkBuffer``s with uniform data, for example if the scene uses a large amount of materials/textures.
* Buffer management will help reduce the overall number of descriptor sets, thus cache pressure will be reduced and the cache itself will be smaller.
== Further resources
* The "DescriptorSet cache" section from https://youtu.be/XCUfk5vRblo?t=2057[Bringing Fortnite to Mobile with Vulkan and OpenGL ES - GDC 2019]
* "Writing an efficient Vulkan renderer" by Arseny Kapoulkine (from "GPU Zen 2: Advanced Rendering Techniques")
== Best practice summary
*Do*
* Update already allocated but no longer referenced descriptor sets, instead of resetting descriptor pools and reallocating new descriptor sets.
* Prefer reusing already allocated descriptor sets, and not updating them with same information every time.
* Consider caching your descriptor sets when feasible.
* Consider using a single (or few) `VkBuffer` per frame with dynamic offsets.
*Don't*
* Allocate descriptor sets from descriptor pools on performance critical code paths.
* Allocate, free or update descriptor sets every frame, unless it is necessary.
* Set `VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT` if you do not need to free individual descriptor sets.
*Impact*
* Increased CPU load for draw calls.
* Setting `VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT` may prevent the implementation from using a simpler (and faster) allocator.
*Debugging*
* The time spent in https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkUpdateDescriptorSets.html[vkUpdateDescriptorSets()] can be checked with a CPU profiler.
In the worst cases it may be comparable or higher than the time spent performing the actual draw calls.
* Monitor if there is contention on https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkAllocateDescriptorSets.html[vkAllocateDescriptorSets()], which will probably be a performance problem if it occurs.
@@ -0,0 +1,153 @@
/* Copyright (c) 2019-2025, Arm Limited and Contributors
*
* 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 "descriptor_management.h"
#include "common/vk_common.h"
#include "filesystem/legacy.h"
#include "gltf_loader.h"
#include "gui.h"
#include "rendering/subpasses/forward_subpass.h"
#include "stats/stats.h"
DescriptorManagement::DescriptorManagement()
{
auto &config = get_configuration();
config.insert<vkb::IntSetting>(0, descriptor_caching.value, 0);
config.insert<vkb::IntSetting>(0, buffer_allocation.value, 0);
config.insert<vkb::IntSetting>(1, descriptor_caching.value, 1);
config.insert<vkb::IntSetting>(1, buffer_allocation.value, 1);
}
bool DescriptorManagement::prepare(const vkb::ApplicationOptions &options)
{
if (!VulkanSample::prepare(options))
{
return false;
}
// Load a scene from the assets folder
load_scene("scenes/bonza/Bonza4X.gltf");
// Attach a move script to the camera component in the scene
auto &camera_node = vkb::add_free_camera(get_scene(), "main_camera", get_render_context().get_surface_extent());
camera = dynamic_cast<vkb::sg::PerspectiveCamera *>(&camera_node.get_component<vkb::sg::Camera>());
vkb::ShaderSource vert_shader("base.vert.spv");
vkb::ShaderSource frag_shader("base.frag.spv");
auto scene_subpass = 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_subpass));
set_render_pipeline(std::move(render_pipeline));
// Add a GUI with the stats you want to monitor
get_stats().request_stats({vkb::StatIndex::frame_times});
create_gui(*window, &get_stats());
return true;
}
void DescriptorManagement::update(float delta_time)
{
// don't call the parent's update, because it's done differently here... but call the grandparent's update for fps logging
vkb::Application::update(delta_time);
update_scene(delta_time);
update_gui(delta_time);
auto &render_context = get_render_context();
auto command_buffer = render_context.begin();
update_stats(delta_time);
// Process GUI input
auto buffer_alloc_strategy = (buffer_allocation.value == 0) ?
vkb::rendering::BufferAllocationStrategy::OneAllocationPerBuffer :
vkb::rendering::BufferAllocationStrategy::MultipleAllocationsPerBuffer;
render_context.get_active_frame().set_buffer_allocation_strategy(buffer_alloc_strategy);
auto descriptor_management_strategy = (descriptor_caching.value == 0) ?
vkb::rendering::DescriptorManagementStrategy::CreateDirectly :
vkb::rendering::DescriptorManagementStrategy::StoreInCache;
render_context.get_active_frame().set_descriptor_management_strategy(descriptor_management_strategy);
command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
get_stats().begin_sampling(*command_buffer);
draw(*command_buffer, render_context.get_active_frame().get_render_target());
get_stats().end_sampling(*command_buffer);
command_buffer->end();
render_context.submit(command_buffer);
}
void DescriptorManagement::draw_gui()
{
auto lines = radio_buttons.size();
if (camera->get_aspect_ratio() < 1.0f)
{
// In portrait, show buttons below heading
lines = lines * 2;
}
get_gui().show_options_window(
/* body = */ [this, lines]() {
// For every option set
for (size_t i = 0; i < radio_buttons.size(); ++i)
{
// Avoid conflicts between buttons with identical labels
ImGui::PushID(vkb::to_u32(i));
auto &radio_button = radio_buttons[i];
ImGui::Text("%s: ", radio_button->description);
if (camera->get_aspect_ratio() > 1.0f)
{
// In landscape, show all options following the heading
ImGui::SameLine();
}
// For every option
for (size_t j = 0; j < radio_button->options.size(); ++j)
{
ImGui::RadioButton(radio_button->options[j], &radio_button->value, vkb::to_u32(j));
if (j < radio_button->options.size() - 1)
{
ImGui::SameLine();
}
}
ImGui::PopID();
}
},
/* lines = */ vkb::to_u32(lines));
}
std::unique_ptr<vkb::VulkanSampleC> create_descriptor_management()
{
return std::make_unique<DescriptorManagement>();
}
@@ -0,0 +1,64 @@
/* Copyright (c) 2019-2024, Arm Limited and Contributors
*
* 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.
*/
#pragma once
#include "rendering/render_pipeline.h"
#include "scene_graph/components/perspective_camera.h"
#include "vulkan_sample.h"
class DescriptorManagement : public vkb::VulkanSampleC
{
public:
DescriptorManagement();
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual ~DescriptorManagement() = default;
virtual void update(float delta_time) override;
private:
/**
* @brief Struct that contains radio button labeling and the value
* which is selected
*/
struct RadioButtonGroup
{
const char *description;
std::vector<const char *> options;
int value;
};
RadioButtonGroup descriptor_caching{
"Descriptor set caching",
{"Disabled", "Enabled"},
0};
RadioButtonGroup buffer_allocation{
"Single large VkBuffer",
{"Disabled", "Enabled"},
0};
std::vector<RadioButtonGroup *> radio_buttons = {&descriptor_caching, &buffer_allocation};
vkb::sg::PerspectiveCamera *camera{nullptr};
virtual void draw_gui() override;
};
std::unique_ptr<vkb::VulkanSampleC> create_descriptor_management();
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@@ -0,0 +1,30 @@
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Arm"
NAME "HPP Pipeline Management"
DESCRIPTION "Pipeline creation and caching, using Vulkan-Hpp."
SHADER_FILES_GLSL
"base.vert"
"base.frag")
@@ -0,0 +1,136 @@
////
- Copyright (c) 2022-2024, The Khronos Group
-
- 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.
-
////
= Pipeline Management with Vulkan-Hpp
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/hpp_pipeline_cache[Khronos Vulkan samples github repository].
endif::[]
:pp: {plus}{plus}
NOTE: A transcoded version of the performance sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/performance/pipeline_cache[Pipeline Cache] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp.
== Overview
Vulkan gives applications the ability to save internal representation of a pipeline (graphics or compute) to enable recreating the same pipeline later.
This sample will look in detail at the implementation and performance implications of the pipeline creation, caching and management.
== Vulkan Pipeline
To create a graphics pipeline in Vulkan, using Vulkan-Hpp, the following objects are required:
* vk::ShaderModule (Vertex and Fragment stages)
* vk::RenderPass
* Subpass Index
* vk::PipelineLayout
** Push Constants
** Descriptor Set Layouts
* Pipeline States
** Vertex Input
** Input Assembly
** Rasterizer
** Depth Stencil
** Color Blend
image::samples/performance/pipeline_cache/images/graphics_pipeline_dependencies.png[Graphics Pipeline Dependencies]
Alternatively for a compute pipeline in Vulkan, using Vulkan-Hpp, you need:
* vk::ShaderModule (Compute stage)
* vk::PipelineLayout
** Push Constants
** Descriptor Set Layouts
image::samples/performance/pipeline_cache/images/compute_pipeline_dependencies.png[Compute Pipeline Dependencies]
== Vulkan Pipeline Cache
Creating a Vulkan pipeline requires compiling `vk::ShaderModule` internally.
This will have a significant increase in frame time if performed at runtime.
To reduce this time, you can provide a previously initialised `vk::PipelineCache` object when calling the `vk::Device::createGraphicsPipeline[s]` or `vk::Device::createComputePipeline[s]` functions.
This object behaves like a cache container which stores the pipeline internal representation for reuse.
In order to benefit from using a `vk::PipelineCache` object, the data recorded during pipeline creation needs to be saved to disk and reused between application runs.
Vulkan allows an application to obtain the binary data of a `vk::PipelineCache` object and save it to a file on disk before terminating the application.
This operation can be achieved using two calls to the `vk::Device::getPipelineCacheData` function to obtain the size and `vk::PipelineCache` object's binary data.
In the next application run, the `vk::PipelineCache` can be initialised with the previous run's data.
This will allow the `vkDevice::createGraphicsPipeline[s]` or `vkDevice::createComputePipeline[s]` functions to reuse the baked state and avoid repeating costly operations such as shader compilation.
== Resource Cache Warmup
A graphics pipeline needs information from the render pass, render state, mesh data and shaders.
This makes it harder for a game engine to prepare the Vulkan pipeline upfront because rendering is controlled by game logic.
Vulkan tutorials typically show pipelines being built upfront because their state is known.
This can also be achieved in a game engine by recording the pipelines created during a game run and then using the information to warmup the internal resource cache in subsequent runs of the game.
In order for this system to work, resource management must be done to track the state of all the Vulkan objects required for pipeline creation and cache them for later reuse.
This process is usually done by hashing the input data (`CreateInfo` structure members) used to create the Vulkan objects.
This enables a future similar request to return immediately with the cached object.
The mapping between input data and the Vulkan object can also alternatively be done by creating the hash using the bitfield hash of the structure members.
While the application is loading, the Vulkan resources can be prepared so that the rendering for the first frames will have minimal CPU impact as all the data necessary has been pre-computed.
For example, when the level changes or the game exits, the recorded Vulkan objects can be serialised and written to a file on disk.
In the next run the file can be read and deserialised to warmup the internal resource cache.
== The sample
The `hpp_pipeline_cache` sample demonstrates this behaviour, by allowing you to enable or disable the use of pipeline cache objects.
Destroying the existing pipelines will trigger re-caching, which is a process that will slow down the application.
In this case there are only 2 pipelines, and the effect is noticeable, therefore we can expect it to have a much greater impact in a real game.
____
On the first run of the sample on a device, the first frames will have a slightly bigger execution time because the pipelines are created for the first time - this is expected behaviour.
In the next runs of the sample, the `vk::PipelineCache` is created with the data saved from the previous run and the internal resource cache.
____
Below is a screenshot of the sample on a phone with Mali G76 GPU:
image::samples/performance/pipeline_cache/images/pipeline_cache_enable.jpg[Pipeline Cache Enable]
Pipeline cache is enabled and Sponza is rendered at 60 FPS when the existing pipelines are destroyed.
Pipeline re-creation takes 24.4 ms thanks to the pipeline cache.
image::samples/performance/pipeline_cache/images/pipeline_cache_disable.jpg[Pipeline Cache Disable]
If we disable the pipeline cache, re-creating the pipelines takes 50.4 ms, more than double the previous time.
Building pipelines dynamically without a pipeline cache can result in a sudden framerate drop.
== Best practices summary
*Do*
* Create known pipelines early in the application execution (use data between application runs).
* Use pipeline cache to reduce pipeline creation cost.
*Don't*
* Create pipelines at draw time without a pipeline cache (introduces performance stutters).
*Impact*
* Increased frame time execution if a pipeline baked state is not reused.
The driver then needs to rebuild the pipeline which includes shader compilation, an expensive operation.
*Debugging*
* A frame capture would show if there are any calls to `vk::Device::createGraphicsPipeline[s]` or `vk::Device::createComputePipeline[s]` with an empty `vk::PipelineCache` object.
____
Due to how `RenderDoc` captures and replays a frame, the field for `vk::PipelineCache` is always empty in the report for the 'create pipeline' functions.
____
@@ -0,0 +1,162 @@
/* Copyright (c) 2022-2025, NVIDIA CORPORATION. All rights reserved.
*
* 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 "hpp_pipeline_cache.h"
#include "common/hpp_utils.h"
#include "rendering/subpasses/hpp_forward_subpass.h"
HPPPipelineCache::HPPPipelineCache()
{
auto &config = get_configuration();
config.insert<vkb::BoolSetting>(0, enable_pipeline_cache, true);
config.insert<vkb::BoolSetting>(1, enable_pipeline_cache, false);
}
HPPPipelineCache::~HPPPipelineCache()
{
if (pipeline_cache)
{
/* Get data of pipeline cache */
std::vector<uint8_t> data = get_device().get_handle().getPipelineCacheData(pipeline_cache);
/* Write pipeline cache data to a file in binary format */
vkb::fs::write_temp(data, "pipeline_cache.data");
/* Destroy Vulkan pipeline cache */
get_device().get_handle().destroyPipelineCache(pipeline_cache);
}
vkb::fs::write_temp(get_device().get_resource_cache().serialize(), "hpp_cache.data");
}
bool HPPPipelineCache::prepare(const vkb::ApplicationOptions &options)
{
if (!vkb::VulkanSampleCpp::prepare(options))
{
return false;
}
/* Try to read pipeline cache file if exists */
std::vector<uint8_t> pipeline_data;
try
{
pipeline_data = vkb::fs::read_temp("pipeline_cache.data");
}
catch (std::runtime_error &ex)
{
LOGW("No pipeline cache found. {}", ex.what());
}
/* Add initial pipeline cache data from the cached file */
vk::PipelineCacheCreateInfo pipeline_cache_create_info{.initialDataSize = static_cast<uint32_t>(pipeline_data.size()),
.pInitialData = pipeline_data.data()};
/* Create Vulkan pipeline cache */
pipeline_cache = get_device().get_handle().createPipelineCache(pipeline_cache_create_info);
vkb::HPPResourceCache &resource_cache = get_device().get_resource_cache();
/* Use pipeline cache to store pipelines */
resource_cache.set_pipeline_cache(pipeline_cache);
std::vector<uint8_t> data_cache;
try
{
data_cache = vkb::fs::read_temp("hpp_cache.data");
}
catch (std::runtime_error &ex)
{
LOGW("No data cache found. {}", ex.what());
}
/* Build all pipelines from a previous run */
resource_cache.warmup(data_cache);
get_stats().request_stats({vkb::StatIndex::frame_times});
float dpi_factor = window->get_dpi_factor();
button_size.x = button_size.x * dpi_factor;
button_size.y = button_size.y * dpi_factor;
create_gui(*window, &get_stats());
load_scene("scenes/sponza/Sponza01.gltf");
auto &camera_node = vkb::common::add_free_camera(get_scene(), "main_camera", get_render_context().get_surface_extent());
camera = &camera_node.get_component<vkb::sg::Camera>();
vkb::ShaderSource vert_shader("base.vert.spv");
vkb::ShaderSource frag_shader("base.frag.spv");
auto scene_subpass = std::make_unique<vkb::rendering::subpasses::HPPForwardSubpass>(
get_render_context(), std::move(vert_shader), std::move(frag_shader), get_scene(), *camera);
auto render_pipeline = std::make_unique<vkb::rendering::HPPRenderPipeline>();
render_pipeline->add_subpass(std::move(scene_subpass));
set_render_pipeline(std::move(render_pipeline));
return true;
}
void HPPPipelineCache::draw_gui()
{
get_gui().show_options_window(
/* body = */ [this]() {
if (ImGui::Checkbox("Pipeline cache", &enable_pipeline_cache))
{
get_device().get_resource_cache().set_pipeline_cache(enable_pipeline_cache ? pipeline_cache : nullptr);
}
ImGui::SameLine();
if (ImGui::Button("Destroy Pipelines", button_size))
{
get_device().get_handle().waitIdle();
get_device().get_resource_cache().clear_pipelines();
record_frame_time_next_frame = true;
}
if (rebuild_pipelines_frame_time_ms > 0.0f)
{
ImGui::Text("Pipeline rebuild frame time: %.1f ms", rebuild_pipelines_frame_time_ms);
}
else
{
ImGui::Text("Pipeline rebuild frame time: N/A");
}
},
/* lines = */ 2);
}
void HPPPipelineCache::update(float delta_time)
{
if (record_frame_time_next_frame)
{
rebuild_pipelines_frame_time_ms = delta_time * 1000.0f;
record_frame_time_next_frame = false;
}
vkb::VulkanSampleCpp::update(delta_time);
}
std::unique_ptr<vkb::VulkanSampleCpp> create_hpp_pipeline_cache()
{
return std::make_unique<HPPPipelineCache>();
}
@@ -0,0 +1,48 @@
/* Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved.
*
* 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.
*/
#pragma once
#include "imgui.h"
#include "scene_graph/components/camera.h"
#include "vulkan_sample.h"
/**
* @brief Pipeline creation and caching
*/
class HPPPipelineCache : public vkb::VulkanSampleCpp
{
public:
HPPPipelineCache();
virtual ~HPPPipelineCache();
private:
// from vkb::VulkanSample
virtual void draw_gui() override;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void update(float delta_time) override;
private:
ImVec2 button_size = ImVec2(150, 30);
vkb::sg::Camera *camera = nullptr;
bool enable_pipeline_cache = true;
vk::PipelineCache pipeline_cache;
float rebuild_pipelines_frame_time_ms = 0.0f;
bool record_frame_time_next_frame = false;
};
std::unique_ptr<vkb::VulkanSampleCpp> create_hpp_pipeline_cache();
@@ -0,0 +1,30 @@
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Arm"
NAME "HPP Swapchain Images"
DESCRIPTION "Using triple buffering over double buffering, using Vulkan-Hpp."
SHADER_FILES_GLSL
"base.vert"
"base.frag")
@@ -0,0 +1,152 @@
////
- Copyright (c) 2022-2024, The Khronos Group
-
- 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.
-
////
:doctype: book
:pp: {plus}{plus}
= Choosing the right number of swapchain images Vulkan-Hpp
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/hpp_swapchain_images[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the performance sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/performance/swapchain_images[Swapchain Images] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp.
== Overview
This is the readme as written in https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/performance/swapchain_images[Swapchain Images], with code transcoded to functions and classes from vulkan.hpp.
Vulkan gives the application some significant control over the number of swapchain images to be created.
This sample analyzes the available options and their performance implications.
== Choosing a number of images
The control over the number of swapchain images is shared between the application and the platform.
The application can ask for a minimum number of images by setting the `minImageCount` parameter in https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCreateSwapchainKHR.html[vk::Device::createSwapchainKHR].
The exact number of images created can then be polled via https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkGetSwapchainImagesKHR.html[vk::Device::getSwapchainImagesKHR].
In order to properly set the `minImageCount` parameter, the application should get the surface capabilities of the physical device via https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkGetPhysicalDeviceSurfaceCapabilitiesKHR.html[vk::PhysicalDevice::getSurfaceCapabilitiesKHR].
The https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkSurfaceCapabilitiesKHR.html[vk::SurfaceCapabilitiesKHR] structure has the `minImageCount` and `maxImageCount` parameters, which set the boundaries for the image count that can be safely requested.
As a rule of thumb on mobile, `+pSurfaceCapabilities->minImageCount+` is usually 2, while `+pSurfaceCapabilities->maxImageCount+` is large enough to not pose any problem with common applications (though it is still good practice to check its value).
The most common values that an application may ask for are:
* `2` for double buffering
* `3` for triple buffering
The swapchain will then create a number of images based on both `minImageCount` and the requested present mode.
We will discuss present modes in the next section.
== Choosing a present mode
The available present modes can be queried via https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkGetPhysicalDeviceSurfacePresentModesKHR.html[vk::PhysicalDevice::getSurfacePresentModesKHR].
There are several presentation modes in Vulkan, but mobile GPUs do not support the ones in which the image is directly presented to the screen (immediate mode).
The only ones which satisfy Android's VSync requirement are `vk::PresentModeKHR::eFifo` and `vk::PresentModeKHR::eMailbox`.
In `vk::PresentModeKHR::eFifo` mode the presentation requests are stored in a queue.
If the queue is full the application will have to wait until an image is ready to be acquired again.
This is a normal operating mode for mobile, which automatically locks the framerate to 60 FPS.
In `vk::PresentModeKHR::eMailbox` mode a single presentation request is stored by the presentation engine.
In case of a new request the previous image will be replaced and will be available for acquisition once again.
As a rule of thumb, if we ask for `vk::PresentModeKHR::eMailbox` we may get more images than `minImageCount`, typically 4.
The application can keep submitting new frames for presentation, without stalling.
This is useful in some cases, e.g.
for reducing input latency, but it is not optimal for mobile because it keeps the CPU and GPU active while not strictly necessary.
Unless the application really needs `vk::PresentModeKHR::eMailbox`, it is better to go for `vk::PresentModeKHR::eFifo` to reduce CPU and GPU load.
In `vk::PresentModeKHR::eFifo` mode the number of swapchain images created typically corresponds to `minImageCount`.
We will now discuss how many images the application should ask for, which is a critical point performance-wise.
== Double buffering or triple buffering?
Android has a VSync signal running at 60 FPS (i.e.
every 16 ms), which is the only chance for an image to be presented.
Double buffering works well if frames can be processed within 16 ms, so at each VSync signal the processed image is presented on screen and the previously presented one becomes available to the application again.
This behavior is demonstrated in the figure below:
image::samples/performance/swapchain_images/images/swapchain_double_buffering.png[Double buffering at 60 FPS]
This behavior breaks when frames take more than 16 ms to be processed.
Let us suppose that a frame is ready after 20 ms.
The following figure illustrates what happens in this case:
image::samples/performance/swapchain_images/images/swapchain_double_buffering_slow.png[Double buffering at <60 FPS]
The orange dashed line highlights a point in which the whole system is idle.
FB1 was not yet ready for presentation for the previous VSync signal, so the presentation engine keeps presenting FB0, which in turn cannot be used to start processing the next frame.
This idling behavior caps framerate at 30 fps, while the application could achieve ~50 fps.
With triple buffering there will always be an image already processed and ready for presentation, so the GPU can start processing a new image without stalling.
image::samples/performance/swapchain_images/images/swapchain_triple_buffering.png[Triple buffering at 60 FPS]
== The sample
The `hpp_swapchain_images` Vulkan sample highlights this behavior, by allowing to switch between double buffering and triple buffering.
This is a screenshot of the sample on a phone with a Mali G72 GPU:
image::samples/performance/swapchain_images/images/sponza_triple_buffering.jpg[Sponza with triple buffering]
Triple buffering is enabled and Sponza is comfortably rendered at 60 FPS.
When we switch to double buffering the framerate drops to 30 FPS:
image::samples/performance/swapchain_images/images/sponza_double_buffering.jpg[Sponza with double buffering]
In this case the actual CPU and GPU frame time is close to 16 ms, so it is possible that the framerate remains at 60 FPS for a few seconds even after switching to double buffering.
Thermal effects or other running processes may cause a small increase in frame time, resulting in the app missing VSync.
As previously discussed, with double buffering a missed VSync causes a sudden drop in framerate.
In order for an application to achieve its potential framerate without being VSync-bound, triple buffering is the preferred option.
We can confirm this behavior using https://developer.arm.com/products/software-development-tools/arm-development-studio/components/streamline-performance-analyzer[Streamline Performance Analyzer].
image::samples/performance/swapchain_images/images/streamline_swapchain_marker.png[Streamline analysis]
The first part of the trace until the marker is with triple buffering.
As we can see the CPU and GPU show a good utilization, with not much idling between frames.
After the marker we switch to double buffering and we confirm what we predicted earlier: there are longer periods of time in which both the CPU and GPU are idle because the presentation system needs to wait for VSync before providing a new image.
== Best practice summary
*Do*
* Ensure that the value of `minImageCount` is within the valid range from https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkGetPhysicalDeviceSurfaceCapabilitiesKHR.html[vk::PhysicalDevice::getSurfaceCapabilitiesKHR] (between `minImageCount` and `maxImageCount`).
* Use `vk::PresentModeKHR::eFifo` to avoid unnecessary CPU and GPU load.
* Use triple buffering to maximize performance.
*Don't*
* Use `vk::PresentModeKHR::eMailbox` unless you specifically need that behavior, e.g.
for lower input latency.
* Use double buffering, unless you are happy with the drop in framerate.
If you want to cap framerate to 30 FPS to save power, this can be achieved on the CPU side while still using triple buffering.
*Impact*
* Double buffering will limit framerate if VSync is missed, as the system will need to stall until the next VSync signal.
*Debugging*
* It is possible to check how many images are created via https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkGetSwapchainImagesKHR.html[vk::Device::getSwapchainImagesKHR].
If only 2 images are being created, `minImageCount` should be increased to 3, if the physical device allows for it (it normally does).
@@ -0,0 +1,91 @@
/* Copyright (c) 2022-2025, NVIDIA CORPORATION. All rights reserved.
*
* 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 "hpp_swapchain_images.h"
#include <common/hpp_utils.h>
#include <rendering/subpasses/hpp_forward_subpass.h>
HPPSwapchainImages::HPPSwapchainImages()
{
auto &config = get_configuration();
config.insert<vkb::IntSetting>(0, swapchain_image_count, 3);
config.insert<vkb::IntSetting>(1, swapchain_image_count, 2);
}
bool HPPSwapchainImages::prepare(const vkb::ApplicationOptions &options)
{
if (!vkb::VulkanSampleCpp::prepare(options))
{
return false;
}
load_scene("scenes/sponza/Sponza01.gltf");
auto &camera_node = vkb::common::add_free_camera(get_scene(), "main_camera", get_render_context().get_surface_extent());
camera = &camera_node.get_component<vkb::sg::Camera>();
vkb::ShaderSource vert_shader("base.vert.spv");
vkb::ShaderSource frag_shader("base.frag.spv");
auto scene_subpass = std::make_unique<vkb::rendering::subpasses::HPPForwardSubpass>(
get_render_context(), std::move(vert_shader), std::move(frag_shader), get_scene(), *camera);
auto render_pipeline = std::make_unique<vkb::rendering::HPPRenderPipeline>();
render_pipeline->add_subpass(std::move(scene_subpass));
set_render_pipeline(std::move(render_pipeline));
get_stats().request_stats({vkb::StatIndex::frame_times});
create_gui(*window, &get_stats());
return true;
}
void HPPSwapchainImages::update(float delta_time)
{
// Process GUI input
if (swapchain_image_count != last_swapchain_image_count)
{
get_device().get_handle().waitIdle();
// Create a new swapchain with a new swapchain image count
get_render_context().update_swapchain(swapchain_image_count);
last_swapchain_image_count = swapchain_image_count;
}
vkb::VulkanSampleCpp::update(delta_time);
}
void HPPSwapchainImages::draw_gui()
{
get_gui().show_options_window(
/* body = */
[this]() {
ImGui::RadioButton("Double buffering", &swapchain_image_count, 2);
ImGui::SameLine();
ImGui::RadioButton("Triple buffering", &swapchain_image_count, 3);
ImGui::SameLine();
},
/* lines = */ 1);
}
std::unique_ptr<vkb::Application> create_hpp_swapchain_images()
{
return std::make_unique<HPPSwapchainImages>();
}
@@ -0,0 +1,43 @@
/* Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved.
*
* 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.
*/
#pragma once
#include "vulkan_sample.h"
#include <scene_graph/components/camera.h>
/**
* @brief Using triple buffering over double buffering, using Vulkan-Hpp
*/
class HPPSwapchainImages : public vkb::VulkanSampleCpp
{
public:
HPPSwapchainImages();
private:
// from vkb::VulkanSample
virtual void draw_gui() override;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void update(float delta_time) override;
private:
vkb::sg::Camera *camera{nullptr};
int swapchain_image_count{3};
int last_swapchain_image_count{3};
};
std::unique_ptr<vkb::Application> create_hpp_swapchain_images();
@@ -0,0 +1,28 @@
# Copyright (c) 2021-2024, Holochip
# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample_with_tags(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Holochip"
NAME "HPP Texture compression comparison"
DESCRIPTION "Compare the performance of different texture formats (ASTC, BC[x], ETC2, and PVRTC), using Vulkan-Hpp")
@@ -0,0 +1,27 @@
////
- Copyright (c) 2024, The Khronos Group
-
- 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.
-
////
= Texture compression comparison using Vulkan-Hpp
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/hpp_texture_compression_comparison[Khronos Vulkan samples github repository].
endif::[]
This sample demonstrates how to use different types of compressed GPU textures in a Vulkan application, and shows the timing benefits of each, using Vulkan-Hpp.
@@ -0,0 +1,319 @@
/* Copyright (c) 2021-2025, Holochip
* Copyright (c) 2024-2025, NVIDIA CORPORATION. All rights reserved.
*
* 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 "hpp_texture_compression_comparison.h"
#include "core/hpp_queue.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";
}
class HPPCompressedImage : public vkb::scene_graph::components::HPPImage
{
public:
HPPCompressedImage(vkb::core::DeviceCpp &device,
const std::string &name,
std::vector<vkb::scene_graph::components::HPPMipmap> &&mipmaps,
vk::Format format) :
vkb::scene_graph::components::HPPImage(name, std::vector<uint8_t>{}, std::move(mipmaps))
{
vkb::scene_graph::components::HPPImage::set_format(format);
vkb::scene_graph::components::HPPImage::create_vk_image(device);
}
};
} // 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)
HPPTextureCompressionComparison::HPPTextureCompressionComparison()
{
texture_compression_data = {
{nullptr, "", vk::Format::eR8G8B8A8Srgb, KTX_TTF_RGBA32, "KTX_TTF_RGBA32", "RGBA 32", true},
{&vk::PhysicalDeviceFeatures::textureCompressionBC, "", vk::Format::eBc7SrgbBlock, KTX_TTF_BC7_RGBA, "KTX_TTF_BC7_RGBA", "BC7"},
{&vk::PhysicalDeviceFeatures::textureCompressionBC, "", vk::Format::eBc3SrgbBlock, KTX_TTF_BC3_RGBA, "KTX_TTF_BC3_RGBA", "BC3"},
{&vk::PhysicalDeviceFeatures::textureCompressionASTC_LDR, "", vk::Format::eAstc4x4SrgbBlock, KTX_TTF_ASTC_4x4_RGBA, "KTX_TTF_ASTC_4x4_RGBA", "ASTC 4x4"},
{&vk::PhysicalDeviceFeatures::textureCompressionETC2, "", vk::Format::eEtc2R8G8B8A8SrgbBlock, KTX_TTF_ETC2_RGBA, "KTX_TTF_ETC2_RGBA", "ETC2"}};
}
void HPPTextureCompressionComparison::draw_gui()
{
get_gui().show_options_window(
[this]() {
if (ImGui::Combo(
"Compressed Format",
&current_gui_format,
[](void *user_data, int idx) -> char const * { return reinterpret_cast<HPPTextureCompressionData *>(user_data)[idx].gui_name.c_str(); },
texture_compression_data.data(),
static_cast<int>(texture_compression_data.size())))
{
require_redraw = true;
if (texture_compression_data[current_gui_format].is_supported)
{
current_format = current_gui_format;
}
}
const auto &current_gui_tc = texture_compression_data[current_gui_format];
if (current_gui_tc.is_supported)
{
ImGui::Text("Format name: %s", current_gui_tc.format_name.c_str());
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.", current_gui_tc.short_name.c_str());
}
});
}
bool HPPTextureCompressionComparison::prepare(const vkb::ApplicationOptions &options)
{
if (vkb::VulkanSample<vkb::BindingType::Cpp>::prepare(options))
{
load_assets();
auto &camera_node = vkb::common::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());
prepare_gui();
return true;
}
return false;
}
void HPPTextureCompressionComparison::update(float delta_time)
{
if (require_redraw)
{
require_redraw = false;
assert(current_format >= 0 && static_cast<size_t>(current_format) < texture_compression_data.size());
current_benchmark = update_textures(texture_compression_data[current_format]);
}
VulkanSample<vkb::BindingType::Cpp>::update(delta_time);
}
std::pair<std::unique_ptr<vkb::scene_graph::components::HPPImage>, HPPTextureCompressionComparison::HPPTextureBenchmark>
HPPTextureCompressionComparison::compress(const std::string &filename,
HPPTextureCompressionComparison::HPPTextureCompressionData 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));
HPPTextureBenchmark 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<vkb::scene_graph::components::HPPImage> HPPTextureCompressionComparison::create_image(ktxTexture2 *ktx_texture, const std::string &name)
{
std::unique_ptr<vkb::core::BufferCpp> staging_buffer =
std::make_unique<vkb::core::BufferCpp>(get_device(), ktx_texture->dataSize, vk::BufferUsageFlagBits::eTransferSrc, VMA_MEMORY_USAGE_CPU_TO_GPU);
memcpy(staging_buffer->map(), ktx_texture->pData, ktx_texture->dataSize);
const auto vk_format = static_cast<vk::Format>(ktx_texture->vkFormat);
vk::Extent3D extent{ktx_texture->baseWidth, ktx_texture->baseHeight, 1};
std::vector<vk::BufferImageCopy> buffer_copies;
std::unique_ptr<vkb::scene_graph::components::HPPImage> image_out;
{
std::vector<vkb::scene_graph::components::HPPMipmap> mip_maps;
for (uint32_t mip_level = 0; mip_level < ktx_texture->numLevels; ++mip_level)
{
vk::Extent3D mip_extent{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));
vk::BufferImageCopy buffer_image_copy = {};
buffer_image_copy.imageSubresource = vk::ImageSubresourceLayers{vk::ImageAspectFlagBits::eColor, 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::scene_graph::components::HPPMipmap 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<HPPCompressedImage>(get_device(), name, std::move(mip_maps), vk_format);
}
auto &vkb_image = image_out->get_vk_image();
auto image = vkb_image.get_handle();
vk::ImageSubresourceRange subresource_range{vk::ImageAspectFlagBits::eColor, 0, static_cast<uint32_t>(buffer_copies.size()), 0, 1};
vk::CommandBuffer command_buffer = get_device().create_command_buffer(vk::CommandBufferLevel::ePrimary, true);
vkb::common::image_layout_transition(command_buffer, image, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, subresource_range);
command_buffer.copyBufferToImage(staging_buffer->get_handle(), image, vk::ImageLayout::eTransferDstOptimal, buffer_copies);
vkb::common::image_layout_transition(
command_buffer, image, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, subresource_range);
get_device().flush_command_buffer(command_buffer, get_device().get_queue_by_flags(vk::QueueFlagBits::eGraphics, 0).get_handle(), true);
return image_out;
}
void HPPTextureCompressionComparison::create_subpass()
{
vkb::ShaderSource vert_shader("base.vert.spv");
vkb::ShaderSource frag_shader("base.frag.spv");
auto scene_sub_pass = std::make_unique<vkb::rendering::subpasses::HPPForwardSubpass>(
get_render_context(), std::move(vert_shader), std::move(frag_shader), get_scene(), *camera);
auto render_pipeline = std::make_unique<vkb::rendering::HPPRenderPipeline>();
render_pipeline->add_subpass(std::move(scene_sub_pass));
set_render_pipeline(std::move(render_pipeline));
}
bool HPPTextureCompressionComparison::is_texture_format_supported(const HPPTextureCompressionData &tcd, vk::PhysicalDeviceFeatures const &device_features)
{
const bool supported_by_feature = tcd.feature_ptr && device_features.*tcd.feature_ptr;
const bool supported_by_extension = tcd.extension_name.length() && get_device().get_gpu().is_extension_supported(tcd.extension_name);
const bool supported_by_default = tcd.always_supported;
return supported_by_default || supported_by_feature || supported_by_extension;
}
void HPPTextureCompressionComparison::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::scene_graph::components::HPPMesh>())
{
for (auto &&sub_mesh : mesh->get_submeshes())
{
auto material = sub_mesh->get_material();
for (auto &name_texture : material->get_textures())
{
vkb::scene_graph::components::HPPTexture *texture = name_texture.second;
auto image = texture->get_image();
textures.emplace_back(texture, image->get_name());
}
}
}
}
void HPPTextureCompressionComparison::prepare_gui()
{
const auto device_features = get_device().get_gpu().get_features();
for (auto &tc : texture_compression_data)
{
tc.is_supported = is_texture_format_supported(tc, device_features);
tc.gui_name = fmt::format(FMT_STRING("{:s} {:s}"), tc.short_name, tc.is_supported ? "" : "(not supported)");
}
}
HPPTextureCompressionComparison::HPPTextureBenchmark HPPTextureCompressionComparison::update_textures(const HPPTextureCompressionData &new_format)
{
HPPTextureBenchmark benchmark;
std::unordered_set<std::string> visited;
for (auto &&texture_filename : textures)
{
vkb::scene_graph::components::HPPTexture *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::scene_graph::components::HPPImage *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;
}
std::unique_ptr<HPPTextureCompressionComparison> create_hpp_texture_compression_comparison()
{
return std::make_unique<HPPTextureCompressionComparison>();
}
@@ -0,0 +1,91 @@
/* Copyright (c) 2021-2024, Holochip
* Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
*
* 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.
*/
#pragma once
#include "ktx.h"
#include "vulkan_sample.h"
class HPPTextureCompressionComparison : public vkb::VulkanSample<vkb::BindingType::Cpp>
{
public:
HPPTextureCompressionComparison();
private:
// from vkb::VulkanSample
void draw_gui() override;
bool prepare(const vkb::ApplicationOptions &options) override;
void update(float delta_time) override;
private:
struct HPPTextureCompressionData
{
vk::Bool32 vk::PhysicalDeviceFeatures::*feature_ptr = nullptr;
std::string extension_name = {};
vk::Format format = {};
ktx_transcode_fmt_e ktx_format = KTX_TTF_NOSELECTION;
std::string format_name = {};
std::string short_name = {};
bool always_supported = false;
std::string gui_name = {};
bool is_supported = {};
};
struct HPPTextureBenchmark
{
HPPTextureBenchmark &operator+=(const HPPTextureBenchmark &other)
{
total_bytes += other.total_bytes;
compress_time_ms += other.compress_time_ms;
frame_time_ms += other.frame_time_ms;
return *this;
}
vk::DeviceSize total_bytes = 0;
float compress_time_ms = 0.f;
float frame_time_ms = 0.f;
};
struct HPPSampleTexture
{
std::vector<uint8_t> raw_bytes;
std::unique_ptr<vkb::scene_graph::components::HPPImage> image;
HPPTextureBenchmark benchmark;
};
private:
std::pair<std::unique_ptr<vkb::scene_graph::components::HPPImage>, HPPTextureBenchmark> compress(
const std::string &filename, HPPTextureCompressionData texture_format, const std::string &name);
std::unique_ptr<vkb::scene_graph::components::HPPImage> create_image(ktxTexture2 *ktx_texture, const std::string &name);
void create_subpass();
bool is_texture_format_supported(const HPPTextureCompressionData &tcd, vk::PhysicalDeviceFeatures const &device_features);
void load_assets();
void prepare_gui();
HPPTextureBenchmark update_textures(const HPPTextureCompressionData &new_format);
private:
vkb::sg::Camera *camera = nullptr;
HPPTextureBenchmark current_benchmark = {};
int current_gui_format = 0;
int current_format = 0;
bool require_redraw = true;
std::vector<HPPTextureCompressionData> texture_compression_data;
std::unordered_map<std::string, HPPSampleTexture> texture_raw_data;
std::vector<std::pair<vkb::scene_graph::components::HPPTexture *, std::string>> textures;
};
std::unique_ptr<HPPTextureCompressionComparison> create_hpp_texture_compression_comparison();
@@ -0,0 +1,30 @@
# Copyright (c) 2024-2025, Arm Limited and Contributors
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Arm"
NAME "Image Compression Control"
DESCRIPTION "Save memory footprint and bandwidth with visually lossless compression"
SHADER_FILES_GLSL
"postprocessing/postprocessing.vert"
"postprocessing/chromatic_aberration.frag")
@@ -0,0 +1,291 @@
////
- Copyright (c) 2024, The Khronos Group
- Copyright (c) 2024, Arm Limited and Contributors
-
- 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.
-
////
= Image Compression Control
////
The following block adds linkage to this repo in the Vulkan docs site project. It's only visible if the file is viewed via the Antora framework.
////
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/image_compression_control[Khronos Vulkan samples github repository].
endif::[]
== Overview
This sample shows how a Vulkan application can control the compression of https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImage.html[`VkImage`] elements, in particular a framebuffer attachment and the swapchain.
This requires enabling and using the extensions https://docs.vulkan.org/spec/latest/appendices/extensions.html#VK_EXT_image_compression_control[`VK_EXT_image_compression_control`] and https://docs.vulkan.org/spec/latest/appendices/extensions.html#VK_EXT_image_compression_control_swapchain[`VK_EXT_image_compression_control_swapchain`], respectively.
Applications that use compression generally perform better thanks to the reduced memory footprint and bandwidth.
In this sample, the use-case is a simple post-processing effect (https://en.wikipedia.org/wiki/Chromatic_aberration[chromatic aberration]) applied to the output color of the forward-rendering pass.
Since the color output needs to be saved to main memory and read again by the post-processing pass, this an opportunity to improve performance by using image compression.
The sample allows toggling between:
* "Default": lossless compression (e.g. xref:/samples/performance/afbc/README.adoc[AFBC]).
* "Fixed-rate": visually lossless compression (e.g. https://learn.arm.com/learning-paths/smartphones-and-mobile/afrc/[AFRC]).
A low/high level of compression can be specified in this case.
* "None": disable all compression, which is usually not recommended except for debugging purposes.
The compression settings will be applied to both the color attachment and the swapchain, if the extensions are supported.
The on-screen hardware counters show the impact each option has on bandwidth and on the memory footprint.
image::./images/image_compression_control.png[Image Compression Control sample, 900, align="center"]
[.text-center]
__Sponza scene with default (lossless) compression__
=== Default compression [[default_compression]]
This is lossless compression that devices can enable transparently to save performance where possible.
Vulkan applications do not need to explicitly enable this sort of compression.
Devices with Arm GPUs implement Arm Frame Buffer Compression (AFBC), which uses variable bitrate encoding: the image is compressed in blocks (e.g. 4x4 pixels) and, depending on their composition, a different bitrate will be used.
This means that the bandwidth savings depend on the image being compressed, and the compression ratio applied to each block.
On average, high compression ratios are often obtained, as shown in the xref:/samples/performance/afbc/README.adoc[AFBC sample].
=== Fixed-rate compression [[fixed_rate_compression]]
On the other hand, a compression scheme may use a constant bitrate for all blocks, and in this case the compression is defined as fixed-rate.
This means that in some cases a block will lose some information, and thus the compression is lossy.
Therefore the device cannot enable it automatically, and the developer must opt-in using the Vulkan image compression control extensions.
Recent devices with Arm GPUs support Arm Fixed Rate Compression (AFRC), which achieve high quality results even with the highest compression ratios.
For instance, see below for images saved from a Pixel 8 device:
[.center, cols="a,a,a"]
|===
| Default compression
| 2BPC Fixed-rate compression
| Pixel difference
| image::./images/default.png[Default compression, 400]
| image::./images/fixed_rate_2BPC.png[Fixed-rate compression, 400]
| image::./images/compare.png[Pixel difference, 400]
|===
[.text-center]
__Space Module scene compression comparison__
Since the difference is not noticeable with the naked eye, this is sometimes referred to as "visually lossless" compression.
Software like https://imagemagick.org/script/compare.php[imagemagick allows to compare] the images and obtain a https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio[PSNR] quality estimation, in this case with the high value of 49.8dB:
[,shell]
----
$ magick compare -metric PSNR default.png fixed_rate_2BPC.png compare.png
49.8487 (0.498487)
----
There are some performance benefits associated with fixed-rate compression, as described below.
=== Memory footprint savings
Images compressed with a fixed-rate will always consume less memory.
In this case, an image compressed with a 2BPC bitrate results in a 65% reduction compared to uncompressed.
image::./images/footprint.png[Memory footprint savings, 700, align="center"]
In this case, the slightly larger size of images compressed with AFBC is expected, as variable bitrates require enough space for the worse case (uncompressed) as well as some extra storage for compression-related metadata.
=== Bandwidth savings
The sample allows to observe an estimate of bytes being written out to main memory.
On this device the write bandwidth difference between uncompressed and fixed-rate compression is approximately 38%:
image::./images/bandwidth.png[Bandwidth savings, 700, align="center"]
Bandwidth savings coming from image compression depend on the pixels being compressed.
Moving the camera and showing different distribution of colors in the frame changes the results.
Be sure to profile your application and verify which compression scheme is optimal in each case.
For instance, images with a high proportion of solid color (e.g. normals or material properties) may be more optimally compressed with variable bitrates than with fixed-rate.
This is the case for the Space Module scene shown above.
== VK_EXT_image_compression_control
This sample enables the https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_image_compression_control.html[`VK_EXT_image_compression_control`] extension and requests the relevant device feature, https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceImageCompressionControlFeaturesEXT.html[`imageCompressionControl`]
This extension abstracts how applications choose a fixed compression rate, in terms of "minimum number of bits per component (BPC)".
=== Query for image compression support
To query if a particular image supports fixed-rate compression, add a https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionPropertiesEXT.html[`VkImageCompressionPropertiesEXT`] to the `pNext` chain of https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageFormatProperties2KHR.html[`VkImageFormatProperties2`], and call https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkGetPhysicalDeviceImageFormatProperties2KHR.html[`vkGetPhysicalDeviceImageFormatProperties2KHR`]:
[,cpp]
----
VkImageCompressionPropertiesEXT supported_compression_properties{VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_PROPERTIES_EXT};
VkImageCompressionControlEXT compression_control{VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_CONTROL_EXT};
compression_control.flags = VK_IMAGE_COMPRESSION_FIXED_RATE_DEFAULT_EXT;
VkPhysicalDeviceImageFormatInfo2 image_format_info{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2};
image_format_info.format = VK_FORMAT_R8G8B8_UNORM;
image_format_info.type = VK_IMAGE_TYPE_2D;
image_format_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_format_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
image_format_info.pNext = &compression_control;
VkImageFormatProperties2 image_format_properties{VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2};
image_format_properties.pNext = &supported_compression_properties;
vkGetPhysicalDeviceImageFormatProperties2KHR(device.get_gpu().get_handle(), &image_format_info, &image_format_properties);
----
In the Vulkan Samples framework, this happens in the https://github.com/KhronosGroup/Vulkan-Samples/blob/main/framework/common/vk_common.cpp[`vkb::query_supported_fixed_rate_compression`] function.
Then inspect the values written to the `imageCompressionFixedRateFlags` component of https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionPropertiesEXT.html[`VkImageCompressionPropertiesEXT`].
If fixed-rate compression is supported, the flags will indicate which levels may be selected for this image, for instance https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionFixedRateFlagBitsEXT.html[`VK_IMAGE_COMPRESSION_FIXED_RATE_2BPC_BIT_EXT`] or https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionFixedRateFlagBitsEXT.html[`VK_IMAGE_COMPRESSION_FIXED_RATE_5BPC_BIT_EXT`].
The sample will use the minimum BPC available for its high compression setting, and the maximum BPC available for its low compression setting.
image::./images/fixed_rate_levels.png[Image Compression Control sample, 900, align="center"]
[.text-center]
__Fixed-rate options__
=== Request image compression
To request fixed-rate compression, provide a https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionControlEXT.html[`VkImageCompressionControlEXT`] to the `pNext` chain of https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCreateInfo.html[`VkImageCreateInfo`]:
[,cpp]
----
VkImageCompressionFixedRateFlagsEXT fixed_rate_flags_array[1] = {VK_IMAGE_COMPRESSION_FIXED_RATE_2BPC_BIT_EXT};
VkImageCompressionControlEXT compression_control{VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_CONTROL_EXT};
compression_control.flags = VK_IMAGE_COMPRESSION_FIXED_RATE_EXPLICIT_EXT;
compression_control.compressionControlPlaneCount = 1;
compression_control.pFixedRateFlags = &fixed_rate_flags_array[0];
VkImageCreateInfo image_info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
image_info.format = VK_FORMAT_R8G8B8_UNORM;
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
image_info.pNext = &compression_control;
vkCreateImage(device, &image_info, nullptr, &new_image);
----
Note that, instead of using https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionFlagBitsEXT.html[`VK_IMAGE_COMPRESSION_FIXED_RATE_EXPLICIT_EXT`], one may use https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionFlagBitsEXT.html[`VK_IMAGE_COMPRESSION_FIXED_RATE_DEFAULT_EXT`], and in that case it would not be necessary to provide a specific set of `pFixedRateFlags`.
In the Vulkan Samples framework, this happens in the https://github.com/KhronosGroup/Vulkan-Samples/blob/main/framework/core/image.cpp[`core::Image`] constructor.
=== Verify image compression [[verify_image_compression]]
To query which compression was applied, if any, once a https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImage.html[`VkImage`] has been created, add a https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionPropertiesEXT.html[`VkImageCompressionPropertiesEXT`] to the `pNext` chain of https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageSubresource2EXT.html[`VkImageSubresource2EXT`], and call https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkGetImageSubresourceLayout2EXT.html[`vkGetImageSubresourceLayout2EXT`]:
[,cpp]
----
VkImageCompressionPropertiesEXT compression_properties{VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_PROPERTIES_EXT};
VkSubresourceLayout2EXT subresource_layout{VK_STRUCTURE_TYPE_SUBRESOURCE_LAYOUT_2_KHR};
subresource_layout.pNext = &compression_properties;
VkImageSubresource2EXT image_subresource{VK_STRUCTURE_TYPE_IMAGE_SUBRESOURCE_2_KHR};
image_subresource.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
image_subresource.imageSubresource.mipLevel = 0;
image_subresource.imageSubresource.arrayLayer = 0;
vkGetImageSubresourceLayout2EXT(device, image, &image_subresource, &subresource_layout);
----
Then inspect the values written to the `imageCompressionFlags` and `imageCompressionFixedRateFlags` components of https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionPropertiesEXT.html[`VkImageCompressionPropertiesEXT`].
In the Vulkan Samples framework, this happens in the https://github.com/KhronosGroup/Vulkan-Samples/blob/main/framework/core/image.cpp[`core::Image::query_applied_compression`] function.
== VK_EXT_image_compression_control_swapchain
Compression control for swapchain images is similar, but it requires the https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_image_compression_control_swapchain.html[`VK_EXT_image_compression_control_swapchain`] extension and the https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceImageCompressionControlSwapchainFeaturesEXT.html[`imageCompressionControlSwapchain`] device feature to be enabled.
These depend on the https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_image_compression_control.html[`VK_EXT_image_compression_control`] being available and enabled too.
=== Query for surface compression support
To query if the surface supports fixed-rate compression, add a https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionPropertiesEXT.html[`VkImageCompressionPropertiesEXT`] to the `pNext` chain of https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageFormatProperties2KHR.html[`VkImageFormatProperties2`], and call https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkGetPhysicalDeviceImageFormatProperties2KHR.html[`vkGetPhysicalDeviceImageFormatProperties2KHR`]:
[,cpp]
----
VkPhysicalDeviceSurfaceInfo2KHR surface_info{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SURFACE_INFO_2_KHR};
surface_info.surface = surface;
uint32_t surface_format_count{0U};
vkGetPhysicalDeviceSurfaceFormats2KHR(device, &surface_info, &surface_format_count, nullptr);
std::vector<VkSurfaceFormat2KHR> surface_formats;
surface_formats.resize(surface_format_count, {VK_STRUCTURE_TYPE_SURFACE_FORMAT_2_KHR});
std::vector<VkImageCompressionPropertiesEXT> compression_properties;
compression_properties.resize(surface_format_count, {VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_PROPERTIES_EXT});
for (uint32_t i = 0; i < surface_format_count; i++)
{
surface_formats[i].pNext = &compression_properties[i];
}
vkGetPhysicalDeviceSurfaceFormats2KHR(device, &surface_info, &surface_format_count, surface_formats.data());
----
Then inspect the values written to the `imageCompressionFixedRateFlags` component of https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionPropertiesEXT.html[`VkImageCompressionPropertiesEXT`], associated to a particular https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkSurfaceFormat2KHR.html[`VkSurfaceFormat2KHR`].
In the Vulkan Samples framework, this happens in the https://github.com/KhronosGroup/Vulkan-Samples/blob/main/framework/core/swapchain.cpp[`Swapchain::query_supported_fixed_rate_compression`] function.
=== Request surface compression
To request fixed-rate compression, provide a https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionControlEXT.html[`VkImageCompressionControlEXT`] to the `pNext` chain of https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkSwapchainCreateInfoKHR.html[`VkSwapchainCreateInfoKHR`]:
[,cpp]
----
VkImageCompressionFixedRateFlagsEXT fixed_rate_flags_array[1] = {VK_IMAGE_COMPRESSION_FIXED_RATE_2BPC_BIT_EXT};
VkImageCompressionControlEXT compression_control{VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_CONTROL_EXT};
compression_control.flags = VK_IMAGE_COMPRESSION_FIXED_RATE_EXPLICIT_EXT;
compression_control.compressionControlPlaneCount = 1;
compression_control.pFixedRateFlags = &fixed_rate_flags_array[0];
VkSwapchainCreateInfoKHR create_info{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
create_info.surface = surface;
create_info.pNext = &compression_control;
vkCreateSwapchainKHR(device, &create_info, nullptr, &new_swapchain);
----
Similarly to regular images, https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionFlagBitsEXT.html[`VK_IMAGE_COMPRESSION_FIXED_RATE_DEFAULT_EXT`] may be used instead.
In the Vulkan Samples framework, this happens in the https://github.com/KhronosGroup/Vulkan-Samples/blob/main/framework/core/swapchain.cpp[`Swapchain`] constructor.
=== Verify surface compression
To verify that compression was applied to the swapchain images, use the same method as described for a regular https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImage.html[`VkImage`] in <<verify_image_compression>>.
No need to enable https://docs.vulkan.org/spec/latest/appendices/extensions.html#VK_EXT_image_compression_control_swapchain[`VK_EXT_image_compression_control_swapchain`] for this.
In the Vulkan Samples framework, this happens in the https://github.com/KhronosGroup/Vulkan-Samples/blob/main/framework/core/swapchain.cpp[`Swapchain::get_applied_compression`] function.
Note that even if the surface supports fixed-rate compression and the extensions are enabled, the surface might not be compressed.
The most likely reason is that, even though the GPU supports it, other IP components in the system (e.g. the Display) do not support it, and therefore images are not compressed.
== Disabling fixed-rate compression
As explained above, the `flags` in https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionControlEXT.html[`VkImageCompressionControlEXT`] control the compression scheme selection for images.
Take care not to accidentally disable <<default_compression>> when disabling <<fixed_rate_compression>>.
That is, ensure that https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionFlagBitsEXT.html[`VK_IMAGE_COMPRESSION_DEFAULT_EXT`] is used by default, rather than https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImageCompressionFlagBitsEXT.html[`VK_IMAGE_COMPRESSION_DISABLED_EXT`], which disables all compression, negatively impacting performance.
== Conclusion
https://docs.vulkan.org/spec/latest/appendices/extensions.html#VK_EXT_image_compression_control[`VK_EXT_image_compression_control`] allows applications to check if default compression is enabled.
It also provides the mechanism to request lossy (fixed-rate) compression where appropriate (https://docs.vulkan.org/spec/latest/appendices/extensions.html#VK_EXT_image_compression_control_swapchain[`VK_EXT_image_compression_control_swapchain`] is required for swapchain images).
Fixed-rate compression guarantees the most efficient memory footprint and can result in substantially reduced memory bandwidth, without sacrificing image quality.
Bandwidth reductions can in turn result in performance improvements and power savings.
@@ -0,0 +1,584 @@
/* Copyright (c) 2024-2025, Arm Limited and Contributors
*
* 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 "image_compression_control.h"
#include "common/vk_common.h"
#include "gltf_loader.h"
#include "gui.h"
#include "platform/platform.h"
#include "rendering/postprocessing_renderpass.h"
#include "rendering/subpasses/forward_subpass.h"
#include "stats/stats.h"
ImageCompressionControlSample::ImageCompressionControlSample()
{
// Extensions of interest in this sample (optional)
add_device_extension(VK_EXT_IMAGE_COMPRESSION_CONTROL_EXTENSION_NAME, true);
add_device_extension(VK_EXT_IMAGE_COMPRESSION_CONTROL_SWAPCHAIN_EXTENSION_NAME, true);
// Extension dependency requirements (given that instance API version is 1.0.0)
add_instance_extension(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME, true);
auto &config = get_configuration();
// Batch mode will test the toggle between different compression modes
config.insert<vkb::IntSetting>(0, static_cast<int>(gui_target_compression), 0);
config.insert<vkb::IntSetting>(1, static_cast<int>(gui_target_compression), 1);
config.insert<vkb::IntSetting>(2, static_cast<int>(gui_target_compression), 2);
}
void ImageCompressionControlSample::request_gpu_features(vkb::PhysicalDevice &gpu)
{
if (gpu.is_extension_supported(VK_EXT_IMAGE_COMPRESSION_CONTROL_EXTENSION_NAME))
{
REQUEST_REQUIRED_FEATURE(gpu,
VkPhysicalDeviceImageCompressionControlFeaturesEXT,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_COMPRESSION_CONTROL_FEATURES_EXT,
imageCompressionControl);
}
if (gpu.is_extension_supported(VK_EXT_IMAGE_COMPRESSION_CONTROL_SWAPCHAIN_EXTENSION_NAME))
{
REQUEST_OPTIONAL_FEATURE(gpu,
VkPhysicalDeviceImageCompressionControlSwapchainFeaturesEXT,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_COMPRESSION_CONTROL_SWAPCHAIN_FEATURES_EXT,
imageCompressionControlSwapchain);
}
}
bool ImageCompressionControlSample::prepare(const vkb::ApplicationOptions &options)
{
if (!VulkanSample::prepare(options))
{
return false;
}
load_scene("scenes/sponza/Sponza01.gltf");
auto &camera_node = vkb::add_free_camera(get_scene(), "main_camera", get_render_context().get_surface_extent());
camera = dynamic_cast<vkb::sg::PerspectiveCamera *>(&camera_node.get_component<vkb::sg::Camera>());
vkb::ShaderSource scene_vs("base.vert.spv");
vkb::ShaderSource scene_fs("base.frag.spv");
auto scene_subpass = std::make_unique<vkb::ForwardSubpass>(get_render_context(), std::move(scene_vs), std::move(scene_fs), get_scene(), *camera);
scene_subpass->set_output_attachments({static_cast<int>(Attachments::Color)});
// Forward rendering pass
auto render_pipeline = std::make_unique<vkb::RenderPipeline>();
render_pipeline->add_subpass(std::move(scene_subpass));
render_pipeline->set_load_store(scene_load_store);
set_render_pipeline(std::move(render_pipeline));
// Post-processing pass (chromatic aberration)
vkb::ShaderSource postprocessing_vs("postprocessing/postprocessing.vert.spv");
postprocessing_pipeline = std::make_unique<vkb::PostProcessingPipeline>(get_render_context(), std::move(postprocessing_vs));
postprocessing_pipeline->add_pass().add_subpass(vkb::ShaderSource("postprocessing/chromatic_aberration.frag.spv"));
// Trigger recreation of Swapchain and render targets, with initial compression parameters
update_render_targets();
get_stats().request_stats({vkb::StatIndex::frame_times,
vkb::StatIndex::gpu_ext_write_bytes});
create_gui(*window, &get_stats());
// Hide GUI compression options other than default if the required extension is not supported
if (!get_device().is_extension_enabled(VK_EXT_IMAGE_COMPRESSION_CONTROL_EXTENSION_NAME))
{
for (int i = 0; i < static_cast<int>(TargetCompression::Count); i++)
{
if (static_cast<TargetCompression>(i) != TargetCompression::Default)
{
gui_skip_compression_values.insert(static_cast<TargetCompression>(i));
}
}
}
return true;
}
void ImageCompressionControlSample::create_render_context()
{
/**
* The framework expects a prioritized list of surface formats. For this sample, include
* only those that can be compressed.
*/
std::vector<VkSurfaceFormatKHR> surface_formats_that_support_compression;
/**
* To query for compression support, VK_EXT_image_compression_control_swapchain allows to
* add a VkImageCompressionPropertiesEXT to the pNext chain of VkSurfaceFormat2KHR when
* calling vkGetPhysicalDeviceSurfaceFormats2KHR.
* See the implementation of this helper function in vkb::Swapchain.
*/
auto surface_compression_properties_list = vkb::Swapchain::query_supported_fixed_rate_compression(get_device(), get_surface());
LOGI("The following surface formats support compression:");
for (auto &surface_compression_properties : surface_compression_properties_list)
{
if (surface_compression_properties.compression_properties.imageCompressionFixedRateFlags != VK_IMAGE_COMPRESSION_FIXED_RATE_NONE_EXT)
{
VkSurfaceFormatKHR surface_format = {surface_compression_properties.surface_format.surfaceFormat.format,
surface_compression_properties.surface_format.surfaceFormat.colorSpace};
LOGI(" \t{}:\t{}",
vkb::to_string(surface_format),
vkb::image_compression_fixed_rate_flags_to_string(surface_compression_properties.compression_properties.imageCompressionFixedRateFlags));
surface_formats_that_support_compression.push_back(surface_format);
}
}
if (surface_formats_that_support_compression.empty())
{
LOGI(" \tNo surface formats support fixed-rate compression");
// Fall-back to default surface format priority list
VulkanSample::create_render_context();
}
else
{
// Filter default list to those formats that support compression
std::vector<VkSurfaceFormatKHR> new_surface_priority_list;
for (auto const &surface_priority : get_surface_priority_list())
{
auto it = std::ranges::find_if(surface_formats_that_support_compression,
[&](VkSurfaceFormatKHR &sf) { return surface_priority.format == sf.format &&
surface_priority.colorSpace == sf.colorSpace; });
if (it != surface_formats_that_support_compression.end())
{
new_surface_priority_list.push_back(*it);
surface_formats_that_support_compression.erase(it);
}
}
// In case there is no overlap, append any formats that support compression but were not in the default list
for (auto &remaining_format : surface_formats_that_support_compression)
{
new_surface_priority_list.push_back(remaining_format);
}
vkb::VulkanSampleC::create_render_context(new_surface_priority_list);
}
/**
* At this point, a Swapchain has been created using the first supported format in the list above. Save a list of
* its corresponding supported compression rates (if any).
*/
const auto &selected_surface_format = get_render_context().get_swapchain().get_surface_format();
for (size_t i = 0; i < surface_compression_properties_list.size(); i++)
{
if (selected_surface_format.format == surface_compression_properties_list[i].surface_format.surfaceFormat.format &&
selected_surface_format.colorSpace == surface_compression_properties_list[i].surface_format.surfaceFormat.colorSpace)
{
supported_fixed_rate_flags_swapchain = vkb::fixed_rate_compression_flags_to_vector(
surface_compression_properties_list[i].compression_properties.imageCompressionFixedRateFlags);
break;
}
}
}
void ImageCompressionControlSample::prepare_render_context()
{
get_render_context().prepare(1, std::bind(&ImageCompressionControlSample::create_render_target, this, std::placeholders::_1));
}
std::unique_ptr<vkb::RenderTarget> ImageCompressionControlSample::create_render_target(vkb::core::Image &&swapchain_image)
{
/**
* The render passes will use 3 attachments: Color, Depth and Swapchain.
* This sample allows to control the compression of the color and Swapchain attachments.
* The Swapchain has already been created by the RenderContext. Here, create color and depth images.
*/
color_image_info.imageType = VK_IMAGE_TYPE_2D;
color_image_info.extent = swapchain_image.get_extent();
color_image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
color_image_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
// The first time this function is called, choose a compressible format for the color attachment.
if (VK_FORMAT_UNDEFINED == color_image_info.format)
{
// Query fixed-rate compression support for a few candidate formats
std::vector<VkFormat> format_list{VK_FORMAT_R8G8B8_UNORM, VK_FORMAT_R8G8B8_SNORM, VK_FORMAT_R8G8B8_SRGB,
VK_FORMAT_B8G8R8_UNORM, VK_FORMAT_B8G8R8_SNORM, VK_FORMAT_B8G8R8_SRGB,
VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SNORM, VK_FORMAT_R8G8B8A8_SRGB,
VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_SNORM, VK_FORMAT_B8G8R8A8_SRGB};
VkFormat chosen_format{VK_FORMAT_UNDEFINED};
if (get_device().is_extension_enabled(VK_EXT_IMAGE_COMPRESSION_CONTROL_EXTENSION_NAME))
{
for (auto &candidate_format : format_list)
{
color_image_info.format = candidate_format;
/**
* To query for compression support, VK_EXT_image_compression_control allows to
* add a VkImageCompressionPropertiesEXT to the pNext chain of VkImageFormatProperties2 when
* calling vkGetPhysicalDeviceImageFormatProperties2KHR.
* See the implementation of this helper function in vkb::core::Image.
*/
VkImageCompressionPropertiesEXT supported_compression_properties = vkb::query_supported_fixed_rate_compression(get_device().get_gpu().get_handle(), color_image_info);
auto fixed_rate_flags = vkb::fixed_rate_compression_flags_to_vector(supported_compression_properties.imageCompressionFixedRateFlags);
VkImageFormatProperties format_properties;
auto result = vkGetPhysicalDeviceImageFormatProperties(get_device().get_gpu().get_handle(),
color_image_info.format,
color_image_info.imageType,
color_image_info.tiling,
color_image_info.usage,
0, // no create flags
&format_properties);
// Pick the first format that supports at least 2 or more levels of fixed-rate compression, otherwise
// pick the first format that supports at least 1 level
if (result != VK_ERROR_FORMAT_NOT_SUPPORTED &&
fixed_rate_flags.size() > 0)
{
if ((VK_FORMAT_UNDEFINED == chosen_format) || (fixed_rate_flags.size() > 1))
{
chosen_format = candidate_format;
supported_fixed_rate_flags_color = fixed_rate_flags;
}
if (fixed_rate_flags.size() > 1)
{
break;
}
}
}
}
// Fallback if no fixed-rate compressible format was found
color_image_info.format = (VK_FORMAT_UNDEFINED != chosen_format) ? chosen_format : swapchain_image.get_format();
LOGI("Chosen color format: {}", vkb::to_string(color_image_info.format));
// Hide GUI fixed-rate compression option if chosen format does not support it
if (supported_fixed_rate_flags_color.empty())
{
gui_skip_compression_values.insert(TargetCompression::FixedRate);
LOGW("Color image does not support fixed-rate compression. Possible reasons:");
LOGW("\t- Its format may not be supported (format = {})", vkb::to_string(color_image_info.format));
if (color_image_info.usage & VK_IMAGE_USAGE_STORAGE_BIT)
{
LOGW("\t- It is a storage image (usage = {})", vkb::image_usage_to_string(color_image_info.usage));
}
if (color_image_info.samples > 1)
{
LOGW("\t- It is a multi-sampled image (sample count = {})", vkb::to_string(color_image_info.samples));
}
}
}
vkb::core::Image depth_image{get_device(),
vkb::core::ImageBuilder(color_image_info.extent)
.with_format(vkb::get_suitable_depth_format(get_device().get_gpu().get_handle()))
.with_usage(VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT)
.with_vma_usage(VMA_MEMORY_USAGE_GPU_ONLY)};
vkb::core::ImageBuilder color_image_builder(color_image_info.extent);
color_image_builder.with_format(color_image_info.format)
.with_usage(color_image_info.usage)
.with_tiling(color_image_info.tiling)
.with_vma_usage(VMA_MEMORY_USAGE_GPU_ONLY);
// Prepare compression control structure
VkImageCompressionControlEXT color_compression_control{VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_CONTROL_EXT};
if (VK_IMAGE_COMPRESSION_DEFAULT_EXT != compression_flag)
{
color_compression_control.flags = compression_flag;
if (VK_IMAGE_COMPRESSION_FIXED_RATE_EXPLICIT_EXT == compression_flag &&
VK_IMAGE_COMPRESSION_FIXED_RATE_NONE_EXT != compression_fixed_rate_flag_color)
{
color_compression_control.compressionControlPlaneCount = 1;
color_compression_control.pFixedRateFlags = &compression_fixed_rate_flag_color;
}
color_image_builder.with_extension<VkImageCompressionControlEXT>(color_compression_control);
}
vkb::core::Image color_image{get_device(), color_image_builder};
if (VK_IMAGE_COMPRESSION_FIXED_RATE_EXPLICIT_EXT == compression_flag)
{
/**
* To verify that the requested compression was indeed applied, the framework core::Image class
* provides a helper function that uses the new structures introduced by VK_EXT_image_compression_control.
* It calls vkGetImageSubresourceLayout2EXT with a VkImageSubresource2EXT that includes a
* VkImageCompressionPropertiesEXT structure in its pNext chain.
* This structure is then filled with the applied compression flags.
*/
LOGI("Applied fixed-rate compression for color ({}): {}", vkb::to_string(color_image_info.format),
vkb::image_compression_fixed_rate_flags_to_string(color_image.get_applied_compression().imageCompressionFixedRateFlags));
}
// Update memory footprint values in GUI (displayed in MB)
const float bytes_in_mb = 1024 * 1024;
footprint_swapchain = swapchain_image.get_image_required_size() / bytes_in_mb;
footprint_color = color_image.get_image_required_size() / bytes_in_mb;
scene_load_store.clear();
std::vector<vkb::core::Image> images;
// Attachment 0 - Swapchain - Not used in the scene render pass, output of postprocessing
images.push_back(std::move(swapchain_image));
scene_load_store.push_back({VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_DONT_CARE});
// Attachment 1 - Attachments::Depth - Transient, used only in the scene render pass
images.push_back(std::move(depth_image));
scene_load_store.push_back({VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_DONT_CARE});
// Attachment 2 - Attachments::Color - Output of the scene render pass, input of postprocessing
images.push_back(std::move(color_image));
scene_load_store.push_back({VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE});
return std::make_unique<vkb::RenderTarget>(std::move(images));
}
void ImageCompressionControlSample::update(float delta_time)
{
elapsed_time += delta_time;
if ((gui_target_compression != last_gui_target_compression) ||
(gui_fixed_rate_compression_level != last_gui_fixed_rate_compression_level))
{
update_render_targets();
last_gui_target_compression = gui_target_compression;
last_gui_fixed_rate_compression_level = gui_fixed_rate_compression_level;
}
VulkanSample::update(delta_time);
}
void ImageCompressionControlSample::update_render_targets()
{
/**
* Define the compression flags that will be used to select the compression
* level of the color and Swapchain images. In the framework, this will be
* handled by the vkb::core::Image and vkb::Swapchain constructors.
* The extensions introduce a new VkImageCompressionControlEXT struct, which
* collects compression settings, and that can be provided to the pNext
* list of VkImageCreateInfo and VkSwapchainCreateInfoKHR.
*/
switch (gui_target_compression)
{
case TargetCompression::FixedRate:
{
compression_flag = VK_IMAGE_COMPRESSION_FIXED_RATE_EXPLICIT_EXT;
break;
}
case TargetCompression::None:
{
compression_flag = VK_IMAGE_COMPRESSION_DISABLED_EXT;
break;
}
case TargetCompression::Default:
default:
{
compression_flag = VK_IMAGE_COMPRESSION_DEFAULT_EXT;
break;
}
}
/**
* Select the minimum (higher compression) or maximum (lower compression) bitrate supported,
* which might be different for the color and the Swapchain.
*/
auto fixed_rate_compression_level = static_cast<FixedRateCompressionLevel>(gui_fixed_rate_compression_level);
compression_fixed_rate_flag_color = select_fixed_rate_compression_flag(supported_fixed_rate_flags_color, fixed_rate_compression_level);
compression_fixed_rate_flag_swapchain = select_fixed_rate_compression_flag(supported_fixed_rate_flags_swapchain, fixed_rate_compression_level);
// Recreate the Swapchain, which also triggers recreation of render targets
get_device().wait_idle();
get_render_context().update_swapchain(compression_flag, compression_fixed_rate_flag_swapchain);
}
VkImageCompressionFixedRateFlagBitsEXT ImageCompressionControlSample::select_fixed_rate_compression_flag(
std::vector<VkImageCompressionFixedRateFlagBitsEXT> &supported_fixed_rate_flags,
FixedRateCompressionLevel compression_level)
{
if (!supported_fixed_rate_flags.empty())
{
switch (compression_level)
{
case FixedRateCompressionLevel::High:
{
return supported_fixed_rate_flags.front();
}
case FixedRateCompressionLevel::Low:
{
return supported_fixed_rate_flags.back();
}
default:
{
LOGW("Unknown fixed-rate compression level {}", static_cast<int>(compression_level));
break;
}
}
}
return VK_IMAGE_COMPRESSION_FIXED_RATE_NONE_EXT;
}
void ImageCompressionControlSample::render(vkb::core::CommandBufferC &command_buffer)
{
// Scene (forward rendering) pass
VulkanSample::render(command_buffer);
command_buffer.end_render_pass();
/**
* Post processing pass, which applies a simple chromatic aberration effect.
* The effect is animated, using elapsed time, for two reasons:
* 1. Allows to visualize the scene with and without the effect.
* 2. Reduces the effect of transaction elimination, a useful feature that
* reduces bandwidth but may hide the bandwidth benefits of compression,
* the focus of this sample.
*/
auto &postprocessing_pass = postprocessing_pipeline->get_pass(0);
postprocessing_pass.set_uniform_data(sin(elapsed_time));
auto &postprocessing_subpass = postprocessing_pass.get_subpass(0);
postprocessing_subpass.bind_sampled_image("color_sampler", static_cast<int>(Attachments::Color));
postprocessing_pipeline->draw(command_buffer, get_render_context().get_active_frame().get_render_target());
}
namespace
{
/**
* @brief Helper function to generate a GUI drop-down options menu
*/
template <typename T>
inline T generate_combo(T current_value, const char *combo_label, const std::unordered_map<T, std::string> &enum_to_string, float item_width, const std::set<T> *skip_values = nullptr)
{
ImGui::PushItemWidth(item_width);
T new_enum_value = current_value;
const auto &search_it = enum_to_string.find(current_value);
const char *selected_value = search_it != enum_to_string.end() ? search_it->second.c_str() : "";
if (ImGui::BeginCombo(combo_label, selected_value))
{
for (const auto &it : enum_to_string)
{
if (skip_values && std::ranges::find(*skip_values, it.first) != skip_values->end())
{
continue;
}
const bool is_selected = it.first == current_value;
if (ImGui::Selectable(it.second.c_str(), is_selected))
{
new_enum_value = it.first;
}
if (is_selected)
{
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
return new_enum_value;
}
} // namespace
void ImageCompressionControlSample::draw_gui()
{
const bool landscape = camera->get_aspect_ratio() > 1.0f;
uint32_t lines = 3;
if (landscape)
{
lines--;
}
get_gui().show_options_window(
[&]() {
const auto window_width = ImGui::GetWindowWidth();
/**
* Select compression scheme from those available. Some options may be hidden if the extension(s) are not supported,
* or if the chosen color format does not support fixed-rate compression.
*/
ImGui::Text("Compression:");
ImGui::SameLine();
const TargetCompression compression = generate_combo(gui_target_compression, "##compression",
{{TargetCompression::FixedRate, "Fixed-rate"}, {TargetCompression::None, "None"}, {TargetCompression::Default, "Default"}},
window_width * 0.2f,
&gui_skip_compression_values);
gui_target_compression = compression;
if (compression == TargetCompression::FixedRate && supported_fixed_rate_flags_color.size() > 1)
{
/**
* Select level of fixed-rate compression from those available. It is assumed that the Swapchain can only be compressed
* if the color attachment can be compressed too, given that we select similar formats and the Swapchain compression control
* extension depends on the general image compression control extension.
*/
ImGui::SameLine();
ImGui::Text("Level:");
ImGui::SameLine();
const FixedRateCompressionLevel compression_level = generate_combo(gui_fixed_rate_compression_level, "##compression-level",
{{FixedRateCompressionLevel::High, "High"}, {FixedRateCompressionLevel::Low, "Low"}},
window_width * 0.2f);
gui_fixed_rate_compression_level = compression_level;
}
if (landscape)
{
ImGui::SameLine();
}
if (gui_skip_compression_values.size() >= static_cast<int>(TargetCompression::Count) - 1)
{
// Single or no compression options available on this device
ImGui::Text("(Extensions are not supported)");
}
else
{
// Indicate if the Swapchain compression matches that of the color attachment
ImGui::Text("(Swapchain is %s affected)", get_render_context().get_swapchain().get_applied_compression() == compression_flag ? "also" : "not");
}
/**
* Display the memory footprint of the configurable targets, which will be lower if fixed-rate compression is selected.
*/
ImGui::Text("Color attachment (%.1f MB), Swapchain (%.1f MB)", footprint_color, footprint_swapchain);
},
lines);
}
std::unique_ptr<vkb::VulkanSampleC> create_image_compression_control()
{
return std::make_unique<ImageCompressionControlSample>();
}
@@ -0,0 +1,189 @@
/* Copyright (c) 2024-2025, Arm Limited and Contributors
*
* 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.
*/
#pragma once
#include "rendering/postprocessing_pipeline.h"
#include "rendering/render_pipeline.h"
#include "scene_graph/components/camera.h"
#include "scene_graph/components/perspective_camera.h"
#include "vulkan_sample.h"
/**
* @brief Image Compression Control Sample
*
* This sample shows how to use the extensions VK_EXT_image_compression_control
* and VK_EXT_image_compression_control_swapchain to select between
* different levels of image compression.
*
* The UI shows the impact compression has on image size and bandwidth,
* illustrating the benefits of fixed-rate (visually lossless) compression.
*/
class ImageCompressionControlSample : public vkb::VulkanSampleC
{
public:
ImageCompressionControlSample();
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
virtual ~ImageCompressionControlSample() = default;
virtual void update(float delta_time) override;
virtual void render(vkb::core::CommandBufferC &command_buffer) override;
void draw_gui() override;
private:
vkb::sg::PerspectiveCamera *camera{nullptr};
virtual void prepare_render_context() override;
virtual void create_render_context() override;
std::unique_ptr<vkb::RenderTarget> create_render_target(vkb::core::Image &&swapchain_image);
enum class Attachments : int
{
Swapchain = 0,
Depth = 1,
Color = 2,
};
/**
* @brief Postprocessing pipeline
* In this sample, a postprocessing pass applies a simple chromatic aberration effect
* to the output of the forward rendering pass. Since this color output needs to
* be saved to main memory, it is a simple use case for compressing the image and
* saving bandwidth and memory footprint, as illustrated in the sample.
*/
std::unique_ptr<vkb::PostProcessingPipeline> postprocessing_pipeline{};
/**
* @brief Load/store operations of the forward rendering pass attachments
* Used to specify that the color output must be stored to main memory.
*/
std::vector<vkb::LoadStoreInfo> scene_load_store{};
/**
* @brief Color image parameters
* These are the properties of the color output image.
*/
VkImageCreateInfo color_image_info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
/**
* @brief Compression scheme
* By default, many implementations support lossless compression.
* Some also support fixed-rate compression, which is generally visually lossless.
* This flag can also be used to disable all compression, which is not recommended.
*/
VkImageCompressionFlagsEXT compression_flag{VK_IMAGE_COMPRESSION_DEFAULT_EXT};
/**
* @brief Possible compression schemes
*/
enum class TargetCompression : int
{
Default = 0,
FixedRate = 1,
None = 2,
// Enum value count
Count = 3
};
/**
* @brief Compression rate for the color image
* If fixed-rate compression is selected, this specifies the bitrate per component.
*/
VkImageCompressionFixedRateFlagsEXT compression_fixed_rate_flag_color{};
/**
* @brief Compression rate for the Swapchain image
*/
VkImageCompressionFixedRateFlagsEXT compression_fixed_rate_flag_swapchain{};
/**
* @brief Supported values for compression rate
* These depend on the properties (format and usage) of the color image.
*/
std::vector<VkImageCompressionFixedRateFlagBitsEXT> supported_fixed_rate_flags_color{};
/**
* @brief Supported values for compression rate
* These depend on the properties (format and usage) of the Swapchain image.
*/
std::vector<VkImageCompressionFixedRateFlagBitsEXT> supported_fixed_rate_flags_swapchain{};
/**
* @brief Possible fixed-rate compression levels
*/
enum FixedRateCompressionLevel : int
{
High = 0, // Highest compression available (smallest bitrate)
Low = 1, // Lowest compression available (largest bitrate)
};
/**
* @brief Selects the compression bitrate given a list of supported values and a compression level
*/
VkImageCompressionFixedRateFlagBitsEXT select_fixed_rate_compression_flag(std::vector<VkImageCompressionFixedRateFlagBitsEXT> &supported_fixed_rate_flags,
FixedRateCompressionLevel compression_level);
/**
* @brief Size (in MB) of the color image (output of the forward rendering pass)
*/
float footprint_color{};
/**
* @brief Size (in MB) of the Swapchain image (output of the postprocessing pass)
*/
float footprint_swapchain{};
/**
* @brief Time
* Used to animate the chromatic aberration effect.
*/
float elapsed_time{};
/**
* @brief Updates compression settings based on user GUI input.
* Triggers recreation of the Swapchain and other render targets.
*/
void update_render_targets();
/**
* @brief Unsupported compression schemes
* Used to reduce the GUI controls if, for instance, the required
* extensions are not supported by the device.
*/
std::set<TargetCompression> gui_skip_compression_values;
/* Helpers for managing GUI input */
TargetCompression gui_target_compression{TargetCompression::Default};
TargetCompression last_gui_target_compression{gui_target_compression};
FixedRateCompressionLevel gui_fixed_rate_compression_level{FixedRateCompressionLevel::High};
FixedRateCompressionLevel last_gui_fixed_rate_compression_level{gui_fixed_rate_compression_level};
};
std::unique_ptr<vkb::VulkanSampleC> create_image_compression_control();
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

@@ -0,0 +1,32 @@
# Copyright (c) 2019-2021, Arm Limited and Contributors
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Arm"
NAME "Layout Transitions"
DESCRIPTION "Choosing the correct layout when transitioning images."
SHADER_FILES_GLSL
"deferred/geometry.vert"
"deferred/geometry.frag"
"deferred/lighting.vert"
"deferred/lighting.frag")
@@ -0,0 +1,108 @@
////
- Copyright (c) 2019-2024, Arm Limited and Contributors
-
- 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.
-
////
= Layout transitions
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/layout_transitions[Khronos Vulkan samples github repository].
endif::[]
Vulkan requires the application to manage image layouts, so that all render pass attachments are in the correct layout when the render pass begins.
This is usually done using pipeline barriers or the `initialLayout` and `finalLayout` parameters of the render pass.
If the rendering pipeline is complex, transitioning each image to its correct layout is not trivial, as it requires some sort of state tracking.
If previous image contents are not needed, there is an easy way out, that is setting `oldLayout`/`initialLayout` to `VK_IMAGE_LAYOUT_UNDEFINED`.
While this is functionally correct, it can have performance implications as it may prevent the GPU from performing some optimizations.
This tutorial will cover an example of such optimizations and how to avoid the performance overhead from using sub-optimal layouts.
== Transaction elimination on Mali GPUs
Mali GPUs employ something called transaction elimination, which is a technology used to avoid frame buffer write bandwidth for static regions of the framebuffer.
This is especially beneficial for games that contain many static opaque overlays.
Transaction elimination is used for an image under the following conditions:
* The sample count is 1.
* The mipmap level is 1.
* The image uses `COLOR_ATTACHMENT_BIT`.
* The image does not use `TRANSIENT_ATTACHMENT_BIT`.
* A single color attachment is being used.
Does not apply to the Mali G51 GPU, or later.
* The effective tile size is 16x16 pixels.
Pixel data storage determines the effective tile size.
The driver keeps a signature buffer for the image to check for redundant frame buffer writes.
The signature buffer must always be in sync with the actual contents of the image, which is the case when an image is only used within the tile write path.
In practice, this corresponds to only using layouts that are either read-only or can only be written to by fragment shading.
These "safe" layouts are:
* `COLOR_ATTACHMENT_OPTIMAL`
* `SHADER_READ_ONLY_OPTIMAL`
* `TRANSFER_SRC_OPTIMAL`
* `PRESENT_SRC_KHR`
All other layouts, including `UNDEFINED` layout, are considered "unsafe" as they allow writes to an image outside the tile write path.
When an image is transitioned via an "unsafe" layout, the signature buffer must be invalidated to prevent the signature and the data from becoming desynchronized.
Note that the swapchain image is a slightly special case, as it is considered "safe" even when transitioned from `UNDEFINED`.
In addition signature invalidation could happen as part of a `VkImageMemoryBarrier`, `vkCmdPipelineBarrier()`, `vkCmdWaitEvents()`, or as part of a `VkRenderPass` if the color attachment reference layout is different from the final layout.
The `vkCmdBlitImage()` framebuffer transfer stage operation will also always invalidate the signature buffer, so shader-based blits will likely be more efficient.
== The sample
The sample sets up deferred rendering using two render passes, to show the effect of transitioning G-buffer images from `UNDEFINED` rather than their last known layout.
Note that a deferred rendering implementation using subpasses might be more efficient overall;
see xref:samples/performance/subpasses/README.adoc[the subpasses tutorial] for more detail.
The base case is with all color images being transitioned from `UNDEFINED`, as shown in the image below.
image::./images/undefined_layout.jpg[Undefined layout transitions]
When we switch to using the last known layout as `oldLayout` in the pipeline barriers, transaction elimination can take place.
This is highlighted in the counters showing about double the amount of tiles killed by CRC match, along with ~10% reduction in write bandwidth.
image::./images/last_layout.jpg[Last layout transitions]
A reduction in memory bandwidth will reduce the power consumption of the device, resulting in less overheating and longer battery life.
Additionally, this may improve performance on games that are bandwidth limited.
== Best practice summary
*Do*
* Use `COLOR_ATTACHMENT_OPTIMAL` image layout for color attachments.
* Keep an image in a "safe" image layout to avoid unnecessary signature invalidation, including avoiding unnecessary transitions via `UNDEFINED`.
* Use `storeOp = DONT_CARE` rather than `UNDEFINED` layouts to skip unneeded render target writes.
*Don't*
* Transition color attachments from "safe" to "unsafe" unless required by the algorithm.
* Use `vkCmdBlitImage()` to copy constant data between two images;
shader-based blits are likely to be more efficient as they will preserve the signature integrity.
*Impact*
* Loss of transaction elimination will increase external memory bandwidth for scenes with static regions across frames.
This may reduce performance on systems which are memory bandwidth limited, as well as cause a general increase in power consumption.
*Debugging*
* The GPU performance counters can count the number of tile writes killed by transaction elimination, so you can determine if it is being triggered at all.
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

@@ -0,0 +1,278 @@
/* Copyright (c) 2019-2025, Arm Limited and Contributors
*
* 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 "layout_transitions.h"
#include "core/device.h"
#include "core/pipeline_layout.h"
#include "core/shader_module.h"
#include "filesystem/legacy.h"
#include "gltf_loader.h"
#include "gui.h"
#include "rendering/subpasses/forward_subpass.h"
#include "rendering/subpasses/lighting_subpass.h"
#include "scene_graph/components/material.h"
#include "scene_graph/components/pbr_material.h"
#include "stats/stats.h"
LayoutTransitions::LayoutTransitions()
{
auto &config = get_configuration();
config.insert<vkb::IntSetting>(0, reinterpret_cast<int &>(layout_transition_type), LayoutTransitionType::UNDEFINED);
config.insert<vkb::IntSetting>(1, reinterpret_cast<int &>(layout_transition_type), LayoutTransitionType::LAST_LAYOUT);
#if defined(PLATFORM__MACOS) && TARGET_OS_IOS && TARGET_OS_SIMULATOR
// On iOS Simulator use layer setting to disable MoltenVK's Metal argument buffers - otherwise blank display
add_instance_extension(VK_EXT_LAYER_SETTINGS_EXTENSION_NAME, /*optional*/ true);
VkLayerSettingEXT layerSetting;
layerSetting.pLayerName = "MoltenVK";
layerSetting.pSettingName = "MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS";
layerSetting.type = VK_LAYER_SETTING_TYPE_INT32_EXT;
layerSetting.valueCount = 1;
// Make this static so layer setting reference remains valid after leaving constructor scope
static const int32_t useMetalArgumentBuffers = 0;
layerSetting.pValues = &useMetalArgumentBuffers;
add_layer_setting(layerSetting);
#endif
}
bool LayoutTransitions::prepare(const vkb::ApplicationOptions &options)
{
if (!VulkanSample::prepare(options))
{
return false;
}
load_scene("scenes/sponza/Sponza01.gltf");
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>();
auto geometry_vs = vkb::ShaderSource{"deferred/geometry.vert.spv"};
auto geometry_fs = vkb::ShaderSource{"deferred/geometry.frag.spv"};
std::unique_ptr<vkb::rendering::SubpassC> gbuffer_pass =
std::make_unique<vkb::GeometrySubpass>(get_render_context(), std::move(geometry_vs), std::move(geometry_fs), get_scene(), *camera);
gbuffer_pass->set_output_attachments({1, 2, 3});
gbuffer_pipeline.add_subpass(std::move(gbuffer_pass));
gbuffer_pipeline.set_load_store(vkb::gbuffer::get_clear_store_all());
auto lighting_vs = vkb::ShaderSource{"deferred/lighting.vert.spv"};
auto lighting_fs = vkb::ShaderSource{"deferred/lighting.frag.spv"};
std::unique_ptr<vkb::rendering::SubpassC> lighting_subpass =
std::make_unique<vkb::LightingSubpass>(get_render_context(), std::move(lighting_vs), std::move(lighting_fs), *camera, get_scene());
lighting_subpass->set_input_attachments({1, 2, 3});
lighting_pipeline.add_subpass(std::move(lighting_subpass));
lighting_pipeline.set_load_store(vkb::gbuffer::get_load_all_store_swapchain());
get_stats().request_stats({vkb::StatIndex::gpu_killed_tiles,
vkb::StatIndex::gpu_ext_write_bytes});
create_gui(*window, &get_stats());
return true;
}
void LayoutTransitions::prepare_render_context()
{
get_render_context().prepare(1, [this](vkb::core::Image &&swapchain_image) { return create_render_target(std::move(swapchain_image)); });
}
std::unique_ptr<vkb::RenderTarget> LayoutTransitions::create_render_target(vkb::core::Image &&swapchain_image)
{
auto &device = swapchain_image.get_device();
auto &extent = swapchain_image.get_extent();
vkb::core::Image depth_image{device,
extent,
vkb::get_suitable_depth_format(swapchain_image.get_device().get_gpu().get_handle()),
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY};
vkb::core::Image albedo_image{device,
extent,
VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY};
vkb::core::Image normal_image{device,
extent,
VK_FORMAT_A2B10G10R10_UNORM_PACK32,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY};
std::vector<vkb::core::Image> images;
// Attachment 0
images.push_back(std::move(swapchain_image));
// Attachment 1
images.push_back(std::move(depth_image));
// Attachment 2
images.push_back(std::move(albedo_image));
// Attachment 3
images.push_back(std::move(normal_image));
return std::make_unique<vkb::RenderTarget>(std::move(images));
}
VkImageLayout LayoutTransitions::pick_old_layout(VkImageLayout last_layout)
{
return (layout_transition_type == LayoutTransitionType::UNDEFINED) ?
VK_IMAGE_LAYOUT_UNDEFINED :
last_layout;
}
void LayoutTransitions::draw(vkb::core::CommandBufferC &command_buffer, vkb::RenderTarget &render_target)
{
// POI
//
// The old_layout for each memory barrier is picked based on the sample's setting.
// We either use the last valid layout for the image or UNDEFINED.
//
// Both approaches are functionally correct, as we are clearing the images anyway,
// but using the last valid layout can give the driver more optimization opportunities.
//
auto &views = render_target.get_views();
assert(1 < views.size());
{
// Image 0 is the swapchain
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = pick_old_layout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
memory_barrier.new_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
memory_barrier.src_access_mask = 0;
memory_barrier.dst_access_mask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
command_buffer.image_memory_barrier(views[0], memory_barrier);
// Skip 1 as it is handled later as a depth-stencil attachment
for (size_t i = 2; i < views.size(); ++i)
{
memory_barrier.old_layout = pick_old_layout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
command_buffer.image_memory_barrier(views[i], memory_barrier);
}
}
{
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = pick_old_layout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL);
memory_barrier.new_layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
memory_barrier.src_access_mask = 0;
memory_barrier.dst_access_mask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
command_buffer.image_memory_barrier(views[1], memory_barrier);
}
auto &extent = render_target.get_extent();
VkViewport viewport{};
viewport.width = static_cast<float>(extent.width);
viewport.height = static_cast<float>(extent.height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
command_buffer.set_viewport(0, {viewport});
VkRect2D scissor{};
scissor.extent = extent;
command_buffer.set_scissor(0, {scissor});
gbuffer_pipeline.draw(command_buffer, get_render_context().get_active_frame().get_render_target());
command_buffer.end_render_pass();
// Memory barriers needed
for (size_t i = 1; i < render_target.get_views().size(); ++i)
{
auto &view = render_target.get_views()[i];
vkb::ImageMemoryBarrier barrier;
if (i == 1)
{
barrier.old_layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
barrier.new_layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
barrier.src_stage_mask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
barrier.src_access_mask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
}
else
{
barrier.old_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.src_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
barrier.src_access_mask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
}
barrier.dst_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
barrier.dst_access_mask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT;
command_buffer.image_memory_barrier(view, barrier);
}
lighting_pipeline.draw(command_buffer, get_render_context().get_active_frame().get_render_target());
if (has_gui())
{
get_gui().draw(command_buffer);
}
command_buffer.end_render_pass();
{
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
memory_barrier.src_access_mask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
command_buffer.image_memory_barrier(views[0], memory_barrier);
}
}
void LayoutTransitions::draw_gui()
{
get_gui().show_options_window(
/* body = */ [this]() {
ImGui::Text("Transition images from:");
ImGui::RadioButton("Undefined layout", reinterpret_cast<int *>(&layout_transition_type), LayoutTransitionType::UNDEFINED);
ImGui::SameLine();
ImGui::RadioButton("Current layout", reinterpret_cast<int *>(&layout_transition_type), LayoutTransitionType::LAST_LAYOUT);
ImGui::SameLine();
},
/* lines = */ 2);
}
std::unique_ptr<vkb::VulkanSampleC> create_layout_transitions()
{
return std::make_unique<LayoutTransitions>();
}
@@ -0,0 +1,63 @@
/* Copyright (c) 2019-2025, Arm Limited and Contributors
*
* 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.
*/
#pragma once
#include "common/utils.h"
#include "rendering/render_pipeline.h"
#include "scene_graph/components/camera.h"
#include "vulkan_sample.h"
/**
* @brief Transitioning images from UNDEFINED vs last known layout
*/
class LayoutTransitions : public vkb::VulkanSampleC
{
public:
LayoutTransitions();
virtual ~LayoutTransitions() = default;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
private:
enum LayoutTransitionType : int
{
UNDEFINED,
LAST_LAYOUT
};
vkb::sg::Camera *camera{nullptr};
std::unique_ptr<vkb::RenderTarget> create_render_target(vkb::core::Image &&swapchain_image);
virtual void prepare_render_context() override;
void draw(vkb::core::CommandBufferC &command_buffer, vkb::RenderTarget &render_target) override;
virtual void draw_gui() override;
VkImageLayout pick_old_layout(VkImageLayout last_layout);
vkb::RenderPipeline gbuffer_pipeline;
vkb::RenderPipeline lighting_pipeline;
LayoutTransitionType layout_transition_type{LayoutTransitionType::UNDEFINED};
};
std::unique_ptr<vkb::VulkanSampleC> create_layout_transitions();
+32
View File
@@ -0,0 +1,32 @@
# Copyright (c) 2020-2021, Arm Limited and Contributors
#
# 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.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Arm"
NAME "MSAA"
DESCRIPTION "How to efficiently use multisampling for MSAA."
SHADER_FILES_GLSL
"base.vert"
"base.frag"
"postprocessing/postprocessing.vert"
"postprocessing/outline.frag")
+217
View File
@@ -0,0 +1,217 @@
////
- Copyright (c) 2021-2024, Arm Limited and Contributors
-
- 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.
-
////
= MSAA (Multisample anti-aliasing)
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/msaa[Khronos Vulkan samples github repository].
endif::[]
Aliasing is the result of under-sampling a signal.
In graphics this means computing the color of a pixel at a resolution that results in artifacts, commonly jaggies at model edges.
Multisample anti-aliasing (MSAA) is an efficient technique that reduces pixel sampling error.
In the figure below, the frame on the left was rendered with no anti-aliasing, whereas the same scene on the right uses 4X MSAA.
image::./images/example_comparison.png[No anti-aliasing (left) vs 4X MSAA (right)]
When computing the color of a pixel, the GPU will evaluate the color of a given primitive if it covers the centre coordinate of the pixel (and passes the depth test).
As shown in the figure below, with no anti-aliasing the fragment shader is evaluated for those pixels that pass this test, and they are shaded accordingly.
Depending on the pixel density, this single-sampled procedure may result in aliasing.
image::./images/steps_no_msaa.png[No anti-aliasing]
With multisample anti-aliasing, more than one location is tested within a pixel.
In the figure below there are four samples, so it is denoted 4X MSAA.
This effectively increases the resolution of each pixel, by storing a color value for each sample.
The fragment shader is still evaluated only once (using the centre coordinate) and the color result is stored as the value of those samples that lie within the primitive (and pass the depth test, which means the depth buffer also needs to be larger to accommodate multiple values per pixel).
In other words, the fragment shader value will be blended to all samples with coverage.
The final value for the pixel is calculated as the average of all samples.
This is known as the resolving step.
This results in different shades of primitive color at the edges, which reduces the aliasing effect.
image::./images/steps_msaa.png[4X MSAA]
In the figure above the samples within the pixel are positioned in a rotated grid.
Sampling coordinates are defined by the https://www.khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap24.html#primsrast-multisampling[spec].
Irregular patterns achieve https://pdfs.semanticscholar.org/ebd9/ddb08c4244fc7df00672cacb420212cdde54.pdf[better results] in horizontal and vertical edges.
Note that MSAA has no effect for pixels within the primitive, where all samples store the same color value.
MSAA is different from (and more efficient than) super-sampling anti-aliasing (SSAA) where the fragment shader is evaluated for each sample.
This would help reduce aliasing within primitives, but usually xref:samples/api/texture_mipmap_generation/README.adoc[mip-maps] mitigate this already.
To enable MSAA, first query http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap32.html#VkPhysicalDeviceLimits[`vkPhysicalDeviceLimits`] to select a supported level of MSAA e.g.
http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap32.html#VkSampleCountFlagBits[`VK_SAMPLE_COUNT_4_BIT`], and use this when creating the multisampled attachment, as well as when setting the http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap24.html#VkPipelineMultisampleStateCreateInfo[`rasterizationSamples`] member of http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap9.html#VkGraphicsPipelineCreateInfo[`pMultisampleState`] in the graphics pipeline.
As stated earlier for MSAA we do _not_ want to set http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap24.html#primsrast-sampleshading[sample shading] as this will enable the more expensive SSAA.
== Color resolve
4x MSAA can be particularly efficient in link:../pipeline_barriers/README.adoc#tile-based-rendering[tiler architectures], where the multi-sampled attachment is resolved in tile memory and can therefore be transient.
This is typically the case of the link:../render_passes/README.adoc#depth-attachment-store-operation[depth buffer] as shown below:
image:./images/no_msaa.png[No MSAA diagram] image:./images/screenshot_no_msaa.jpg[No MSAA sample]
It is important to avoid writing multisampled attachments back to main memory if they are not going to be needed after rendering the scene.
This means that the multisampled attachment must use `storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE` and `usage |= VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT`, and allocate the image with the `LAZILY_ALLOCATED` memory property, as explained in the link:../render_passes/README.adoc#depth-attachment-store-operation[Render Passes tutorial].
----
// Multisampled attachment is transient
// This allows tilers to completely avoid writing out the multisampled attachment to memory,
// a considerable performance and bandwidth improvement
load_store[i_color_ms].store_op = VK_ATTACHMENT_STORE_OP_DONT_CARE;
----
To resolve color on write-back as shown below, configure the subpass so that http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap7.html#VkSubpassDescription[`pResolveAttachments`] points to the single-sampled attachment that we want the multisampled color to be resolved to, in this case the swapchain image.
----
// Good practice
// Enable write-back resolve to single-sampled attachment
subpass->set_color_resolve_attachments({i_swapchain});
----
image:./images/msaa_good.png[MSAA with write-back color resolve] image:./images/screenshot_color_writeback.jpg[MSAA with write-back color resolve sample]
With 4X MSAA enabled, we are rendering to a larger color attachment storing 4 color values for each pixel.
If this attachment remains in tile memory, the impact on performance remains minimal (3% bandwidth increase shown in the screenshots above) while the aliasing is considerably reduced at the edges.
As mentioned earlier this is due to the fact that the hardware can resolve (average the samples of) the multisampled attachment as the image is written back to main memory.
Vulkan offers an alternative way to explicitly define a separate resolve pass for the color attachment, using http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap18.html#vkCmdResolveImage[`vkCmdResolveImage`]:
----
// Bad practice
// Resolve multisampled attachment to destination, extremely expensive
vkCmdResolveImage(cmd_buf.get_handle(),
multisampled_img.get_handle(),
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
swapchain_img.get_handle(),
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
to_u32(regions.size()), regions.data());
----
However this path requires storing the multisampled attachment (which in this case is 4 times larger than the framebuffer) at the end of the subpass and then read it back to the GPU in order to resolve it:
image:./images/msaa_bad.png[MSAA with color resolve in separate pass] image:./images/screenshot_color_separate.jpg[MSAA with separate color resolve sample]
This consumes much more bandwidth and is therefore not recommended if the same result can be achieved by using http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap7.html#VkSubpassDescription[`pResolveAttachments`] to resolve color on write-back.
To illustrate this the sample allows to toggle between resolving on write-back as opposed to in a separate pass, and to monitor the impact on bandwidth as a result.
On a high-end smartphone with a Mali-G76 as shown in the screenshots above, the difference in bandwidth could be explained as follows.
The sample is rendering 2168 x 1080 pixels which require 32 bits each (RGBA8, 4 bytes) at 60 FPS:
----
1X attachment: 2168 * 1080 * 4 * 60 = 562 bytes/s
----
This is multiplied by 4 if when we need to store 4 sample values per pixel:
----
4X attachment: 2168 * 1080 * 4 * 4 * 60 = 2247 bytes/s
----
Comparing the counter numbers shown in the screenshots above, both the read and write bandwidth increase by roughly the size of a 4X attachment, since the multisampled attachment needs to be written out at the end of the scene renderpass and then re-read to resolve the final color.
This means that a separate resolve pass is resulting in 5 GB/s increase in bandwidth.
Considering that in a mobile device such as this the external DDR bandwidth costs around 100 mW per GB/s, this overhead uses 500 mW (20%) out of an approximate 2.5 W device power budget, which is prohibitively expensive.
These counters can also be recorded with a profiler such as https://developer.arm.com/tools-and-software/graphics-and-gaming/arm-mobile-studio/components/streamline-performance-analyzer[Streamline], showing color resolve on write-back followed by separate resolve pass:
image::./images/streamline_writeback_separate.png[Streamline write-back resolve followed by separate resolve]
== Depth resolve
In all of the cases shown above the depth buffer has remained transient, regardless of MSAA.
This is because once the color is calculated and written out to the swapchain for presentation to the display, the depth can be discarded and therefore we recommend to link:../render_passes/README.adoc#depth-attachment-store-operation[configure load/store operations to avoid writing it out].
There are cases where we might need to save the depth attachment.
Consider a simple post-processing pass that samples both color and depth (bound as textures) in order to compute a screen-based effect such as https://en.wikipedia.org/wiki/Screen_space_ambient_occlusion[SSAO]:
image:./images/no_msaa_postprocessing.png[Postprocessing] image:./images/screenshot_no_msaa_postprocessing.jpg[Postprocessing sample]
In this case the increase in bandwidth corresponds to that of writing out 2 full-screen attachments, as expected.
With 4X MSAA the cost once again remains almost the same, as long as we remember to resolve both color and depth on write-back:
image:./images/msaa_good_postprocessing.png[Postprocessing with MSAA and write-back resolve of color and depth] image:./images/screenshot_color_depth_writeback.jpg[Postprocessing with MSAA and write-back resolve sample]
To resolve depth on write-back, http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap40.html#VK_KHR_depth_stencil_resolve[`VK_KHR_depth_stencil_resolve`] (promoted in http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap39.html#versions-1.2[Vulkan 1.2]) is required.
To configure the subpass, we must use a http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap7.html#VkSubpassDescription2[`VkSubpassDescription2`] and make `pNext` to point to a http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap7.html#VkSubpassDescriptionDepthStencilResolve[`VkSubpassDescriptionDepthStencilResolve`] structure.
This structure defines the single-sampled attachment that will be used to resolve depth:
----
// Good practice
// Multisampled attachment is transient
// This allows tilers to completely avoid writing out the multisampled attachment to memory,
// a considerable performance and bandwidth improvement
load_store[i_depth].store_op = VK_ATTACHMENT_STORE_OP_DONT_CARE;
// Enable write-back resolve to single-sampled attachment
subpass->set_depth_stencil_resolve_attachment(i_depth_resolve);
subpass->set_depth_stencil_resolve_mode(depth_resolve_mode);
----
Here we may also select how to resolve depth, by setting http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap7.html#VkResolveModeFlagBits[`depthResolveMode`] to one of the http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap32.html#VkPhysicalDeviceDepthStencilResolvePropertiesKHR[supported] options (the sample queries the device for supported modes and presents a drop-down selection list):
----
typedef enum VkResolveModeFlagBits {
VK_RESOLVE_MODE_NONE,
VK_RESOLVE_MODE_SAMPLE_ZERO_BIT,
VK_RESOLVE_MODE_AVERAGE_BIT,
VK_RESOLVE_MODE_MIN_BIT,
VK_RESOLVE_MODE_MAX_BIT
} VkResolveModeFlagBits;
----
In contrast to color, Vulkan does not offer an alternative way to resolve depth attachments (http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap18.html#vkCmdResolveImage[`vkCmdResolveImage`] does not support depth).
Therefore if http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap40.html#VK_KHR_depth_stencil_resolve[`VK_KHR_depth_stencil_resolve`] is not supported or properly configured, this pipeline will require an additional read-back of the multisampled depth attachment to carry out the post-processing effect:
image:./images/msaa_bad_postprocessing.png[Postprocessing with MSAA and color resolve in separate pass, no depth resolve] image:./images/screenshot_color_depth_separate.jpg[Postprocessing with MSAA no write-back resolve sample]
In the worst possible scenario shown above, where both multisampled depth and color are written out to main memory, the read bandwidth increases 2366 MiB/s (close to the bandwidth of a 4X attachment as calculated above) due to the color re-read required for separate resolve.
The write bandwidth increases 3951 MiB/s, which roughly corresponds to the difference between a 4X (2247 MiB/s) and a 1X (562 MiB/s) depth attachment (in this case depth is also 32bpp) i.e.
1685 MiB/s, plus the bandwidth required to write out an additional 4X color attachment i.e.
2247 MiB/s.
In total the read/write bandwidth increase is 6.3GB/s, a 302% increase with respect to the write-back resolve best practice and 630 mW of power (25% of budget) that could be saved to preserve battery life, achieve sustainable performance and an overall better user experience.
== Best practice summary
For most uses of multisampling it is possible to keep all of the data for the additional samples in the tile memory inside of the GPU, and resolve the value to a single pixel color as part of tile write-back.
This means that the additional bandwidth of those additional samples never hits external memory, which makes it exceptionally efficient.
MSAA can be integrated fully with Vulkan render passes, allowing a multisampled resolve to be explicitly specified at the end of a subpass.
*Do*
* Use 4x MSAA if possible;
it's not expensive and provides good image quality improvements.
* Use `loadOp = LOAD_OP_CLEAR` or `loadOp = LOAD_OP_DONT_CARE` for multisampled images.
* Use `storeOp = STORE_OP_DONT_CARE` for multisampled images.
* Use `LAZILY_ALLOCATED` memory to back the allocated multisampled images;
they do not need to be persisted into main memory and therefore do not need physical backing storage.
* Use `pResolveAttachments` in a subpass to automatically resolve a multisampled color buffer into a single-sampled color buffer.
* Use http://khronos.org/registry/vulkan/specs/1.2-khr-extensions/html/chap40.html#VK_KHR_depth_stencil_resolve[`VK_KHR_depth_stencil_resolve`] in a subpass to automatically resolve a multisampled depth buffer into a single-sampled depth buffer.
Typically this is only useful if the depth buffer is going to be used further, in most cases it is transient and does not need to be resolved.
*Avoid*
* Avoid using `vkCmdResolveImage()`;
this has a significant negative impact on bandwidth and performance.
* Avoid using `loadOp = LOAD_OP_LOAD` for multisampled image attachments.
* Avoid using `storeOp = STORE_OP_STORE` for multisampled image attachments.
* Avoid using more than 4x MSAA without checking performance.
*Impact*
* Failing to get an inline resolve can result in substantially higher memory bandwidth and reduced performance;
manually writing and resolving a 4x MSAA 1080p surface at 60 FPS requires 3.9GB/s of memory bandwidth compared to just 500MB/s when using an inline resolve.
Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Some files were not shown because too many files have changed in this diff Show More