remove other sample and change apk to inewme

This commit is contained in:
xsl
2025-09-21 17:18:31 +08:00
parent a43bd12970
commit 8c39579e1c
1321 changed files with 6 additions and 122951 deletions
-37
View File
@@ -1,37 +0,0 @@
# Copyright (c) 2019-2024, Sascha Willems
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 the "License";
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
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 "Sascha Willems"
NAME "Compute N-Body simulation"
DESCRIPTION "Multi-pass compute dispatch N-Body particle simulation"
SHADER_FILES_GLSL
"compute_nbody/glsl/particle.vert"
"compute_nbody/glsl/particle.frag"
"compute_nbody/glsl/particle_calculate.comp"
"compute_nbody/glsl/particle_integrate.comp"
SHADER_FILES_HLSL
"compute_nbody/hlsl/particle.vert.hlsl"
"compute_nbody/hlsl/particle.frag.hlsl"
"compute_nbody/hlsl/particle_calculate.comp.hlsl"
"compute_nbody/hlsl/particle_integrate.comp.hlsl")
-27
View File
@@ -1,27 +0,0 @@
////
- Copyright (c) 2019-2023, 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.
-
////
= Compute shader N-Body simulation
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/compute_nbody[Khronos Vulkan samples github repository].
endif::[]
Compute shader example that uses two passes and shared compute shader memory for simulating a N-Body particle system.
-880
View File
@@ -1,880 +0,0 @@
/* Copyright (c) 2019-2025, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Compute shader N-body simulation using two passes and shared compute shader memory
*/
#include "compute_nbody.h"
#include "benchmark_mode/benchmark_mode.h"
ComputeNBody::ComputeNBody()
{
title = "Compute shader N-body system";
camera.type = vkb::CameraType::LookAt;
// Note: Using reversed depth-buffer for increased precision, so Z-Near and Z-Far are flipped
camera.set_perspective(60.0f, static_cast<float>(width) / static_cast<float>(height), 512.0f, 0.1f);
camera.set_rotation(glm::vec3(-26.0f, 75.0f, 0.0f));
camera.set_translation(glm::vec3(0.0f, 0.0f, -14.0f));
camera.translation_speed = 2.5f;
}
ComputeNBody::~ComputeNBody()
{
if (has_device())
{
// Graphics
graphics.uniform_buffer.reset();
vkDestroyPipeline(get_device().get_handle(), graphics.pipeline, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), graphics.pipeline_layout, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), graphics.descriptor_set_layout, nullptr);
vkDestroySemaphore(get_device().get_handle(), graphics.semaphore, nullptr);
// Compute
compute.storage_buffer.reset();
compute.uniform_buffer.reset();
vkDestroyPipelineLayout(get_device().get_handle(), compute.pipeline_layout, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), compute.descriptor_set_layout, nullptr);
vkDestroyPipeline(get_device().get_handle(), compute.pipeline_calculate, nullptr);
vkDestroyPipeline(get_device().get_handle(), compute.pipeline_integrate, nullptr);
vkDestroySemaphore(get_device().get_handle(), compute.semaphore, nullptr);
vkDestroyCommandPool(get_device().get_handle(), compute.command_pool, nullptr);
vkDestroySampler(get_device().get_handle(), textures.particle.sampler, nullptr);
vkDestroySampler(get_device().get_handle(), textures.gradient.sampler, nullptr);
}
}
void ComputeNBody::request_gpu_features(vkb::PhysicalDevice &gpu)
{
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = VK_TRUE;
}
}
void ComputeNBody::load_assets()
{
textures.particle = load_texture("textures/particle_rgba.ktx", vkb::sg::Image::Color);
textures.gradient = load_texture("textures/particle_gradient_rgba.ktx", vkb::sg::Image::Color);
}
void ComputeNBody::build_command_buffers()
{
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VkClearValue clear_values[2];
clear_values[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
clear_values[1].depthStencil = {0.0f, 0};
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.renderPass = render_pass;
render_pass_begin_info.renderArea.offset.x = 0;
render_pass_begin_info.renderArea.offset.y = 0;
render_pass_begin_info.renderArea.extent.width = width;
render_pass_begin_info.renderArea.extent.height = height;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.pClearValues = clear_values;
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
// Set target frame buffer
render_pass_begin_info.framebuffer = framebuffers[i];
VK_CHECK(vkBeginCommandBuffer(draw_cmd_buffers[i], &command_buffer_begin_info));
// Acquire
if (graphics.queue_family_index != compute.queue_family_index)
{
VkBufferMemoryBarrier buffer_barrier =
{
VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
nullptr,
0,
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT,
compute.queue_family_index,
graphics.queue_family_index,
compute.storage_buffer->get_handle(),
0,
compute.storage_buffer->get_size()};
vkCmdPipelineBarrier(
draw_cmd_buffers[i],
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_VERTEX_INPUT_BIT,
0,
0, nullptr,
1, &buffer_barrier,
0, nullptr);
}
// Draw the particle system using the update vertex buffer
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(width, height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphics.pipeline);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphics.pipeline_layout, 0, 1, &graphics.descriptor_set, 0, NULL);
VkDeviceSize offsets[1] = {0};
vkCmdBindVertexBuffers(draw_cmd_buffers[i], 0, 1, compute.storage_buffer->get(), offsets);
vkCmdDraw(draw_cmd_buffers[i], num_particles, 1, 0, 0);
draw_ui(draw_cmd_buffers[i]);
vkCmdEndRenderPass(draw_cmd_buffers[i]);
// Release barrier
if (graphics.queue_family_index != compute.queue_family_index)
{
VkBufferMemoryBarrier buffer_barrier =
{
VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
nullptr,
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT,
0,
graphics.queue_family_index,
compute.queue_family_index,
compute.storage_buffer->get_handle(),
0,
compute.storage_buffer->get_size()};
vkCmdPipelineBarrier(
draw_cmd_buffers[i],
VK_PIPELINE_STAGE_VERTEX_INPUT_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0,
0, nullptr,
1, &buffer_barrier,
0, nullptr);
}
VK_CHECK(vkEndCommandBuffer(draw_cmd_buffers[i]));
}
}
void ComputeNBody::build_compute_command_buffer()
{
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VK_CHECK(vkBeginCommandBuffer(compute.command_buffer, &command_buffer_begin_info));
// Acquire
if (graphics.queue_family_index != compute.queue_family_index)
{
VkBufferMemoryBarrier buffer_barrier =
{
VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
nullptr,
0,
VK_ACCESS_SHADER_WRITE_BIT,
graphics.queue_family_index,
compute.queue_family_index,
compute.storage_buffer->get_handle(),
0,
compute.storage_buffer->get_size()};
vkCmdPipelineBarrier(
compute.command_buffer,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0,
0, nullptr,
1, &buffer_barrier,
0, nullptr);
}
// First pass: Calculate particle movement
// -------------------------------------------------------------------------------------------------------
vkCmdBindPipeline(compute.command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline_calculate);
vkCmdBindDescriptorSets(compute.command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline_layout, 0, 1, &compute.descriptor_set, 0, 0);
vkCmdDispatch(compute.command_buffer, num_particles / work_group_size, 1, 1);
// Add memory barrier to ensure that the computer shader has finished writing to the buffer
VkBufferMemoryBarrier memory_barrier = vkb::initializers::buffer_memory_barrier();
memory_barrier.buffer = compute.storage_buffer->get_handle();
memory_barrier.size = compute.storage_buffer->get_size();
memory_barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
memory_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
memory_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
memory_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
vkCmdPipelineBarrier(
compute.command_buffer,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_FLAGS_NONE,
0, nullptr,
1, &memory_barrier,
0, nullptr);
// Second pass: Integrate particles
// -------------------------------------------------------------------------------------------------------
vkCmdBindPipeline(compute.command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline_integrate);
vkCmdDispatch(compute.command_buffer, num_particles / work_group_size, 1, 1);
// Release
if (graphics.queue_family_index != compute.queue_family_index)
{
VkBufferMemoryBarrier buffer_barrier =
{
VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
nullptr,
VK_ACCESS_SHADER_WRITE_BIT,
0,
compute.queue_family_index,
graphics.queue_family_index,
compute.storage_buffer->get_handle(),
0,
compute.storage_buffer->get_size()};
vkCmdPipelineBarrier(
compute.command_buffer,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
0,
0, nullptr,
1, &buffer_barrier,
0, nullptr);
}
vkEndCommandBuffer(compute.command_buffer);
}
// Setup and fill the compute shader storage buffers containing the particles
void ComputeNBody::prepare_storage_buffers()
{
#if 0
std::vector<glm::vec3> attractors = {
glm::vec3(2.5f, 1.5f, 0.0f),
glm::vec3(-2.5f, -1.5f, 0.0f),
};
#else
std::vector<glm::vec3> attractors = {
glm::vec3(5.0f, 0.0f, 0.0f),
glm::vec3(-5.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 0.0f, 5.0f),
glm::vec3(0.0f, 0.0f, -5.0f),
glm::vec3(0.0f, 4.0f, 0.0f),
glm::vec3(0.0f, -8.0f, 0.0f),
};
#endif
num_particles = static_cast<uint32_t>(attractors.size()) * PARTICLES_PER_ATTRACTOR;
// Initial particle positions
std::vector<Particle> particle_buffer(num_particles);
std::default_random_engine rnd_engine(lock_simulation_speed ? 0 : static_cast<unsigned>(time(nullptr)));
std::normal_distribution<float> rnd_distribution(0.0f, 1.0f);
for (uint32_t i = 0; i < static_cast<uint32_t>(attractors.size()); i++)
{
for (uint32_t j = 0; j < PARTICLES_PER_ATTRACTOR; j++)
{
Particle &particle = particle_buffer[i * PARTICLES_PER_ATTRACTOR + j];
// First particle in group as heavy center of gravity
if (j == 0)
{
particle.pos = glm::vec4(attractors[i] * 1.5f, 90000.0f);
particle.vel = glm::vec4(glm::vec4(0.0f));
}
else
{
// Position
glm::vec3 position(attractors[i] + glm::vec3(rnd_distribution(rnd_engine), rnd_distribution(rnd_engine), rnd_distribution(rnd_engine)) * 0.75f);
float len = glm::length(glm::normalize(position - attractors[i]));
position.y *= 2.0f - (len * len);
// Velocity
glm::vec3 angular = glm::vec3(0.5f, 1.5f, 0.5f) * (((i % 2) == 0) ? 1.0f : -1.0f);
glm::vec3 velocity = glm::cross((position - attractors[i]), angular) + glm::vec3(rnd_distribution(rnd_engine), rnd_distribution(rnd_engine), rnd_distribution(rnd_engine) * 0.025f);
float mass = (rnd_distribution(rnd_engine) * 0.5f + 0.5f) * 75.0f;
particle.pos = glm::vec4(position, mass);
particle.vel = glm::vec4(velocity, 0.0f);
}
// Color gradient offset
particle.vel.w = static_cast<float>(i) * 1.0f / static_cast<uint32_t>(attractors.size());
}
}
compute.ubo.particle_count = num_particles;
VkDeviceSize storage_buffer_size = particle_buffer.size() * sizeof(Particle);
// Staging
// SSBO won't be changed on the host after upload so copy to device local memory
vkb::core::BufferC staging_buffer = vkb::core::BufferC::create_staging_buffer(get_device(), particle_buffer);
compute.storage_buffer = std::make_unique<vkb::core::BufferC>(get_device(),
storage_buffer_size,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VMA_MEMORY_USAGE_GPU_ONLY);
// Copy from staging buffer to storage buffer
VkCommandBuffer copy_command = get_device().create_command_buffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
VkBufferCopy copy_region = {};
copy_region.size = storage_buffer_size;
vkCmdCopyBuffer(copy_command, staging_buffer.get_handle(), compute.storage_buffer->get_handle(), 1, &copy_region);
// Execute a transfer to the compute queue, if necessary
if (graphics.queue_family_index != compute.queue_family_index)
{
VkBufferMemoryBarrier buffer_barrier =
{
VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
nullptr,
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT,
0,
graphics.queue_family_index,
compute.queue_family_index,
compute.storage_buffer->get_handle(),
0,
compute.storage_buffer->get_size()};
vkCmdPipelineBarrier(
copy_command,
VK_PIPELINE_STAGE_VERTEX_INPUT_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0,
0, nullptr,
1, &buffer_barrier,
0, nullptr);
}
get_device().flush_command_buffer(copy_command, queue, true);
}
void ComputeNBody::setup_descriptor_pool()
{
std::vector<VkDescriptorPoolSize> pool_sizes =
{
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2)};
VkDescriptorPoolCreateInfo descriptor_pool_create_info =
vkb::initializers::descriptor_pool_create_info(
static_cast<uint32_t>(pool_sizes.size()),
pool_sizes.data(),
2);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &descriptor_pool_create_info, nullptr, &descriptor_pool));
}
void ComputeNBody::setup_descriptor_set_layout()
{
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings;
set_layout_bindings = {
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 0),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 2),
};
VkDescriptorSetLayoutCreateInfo descriptor_layout =
vkb::initializers::descriptor_set_layout_create_info(
set_layout_bindings.data(),
static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout, nullptr, &graphics.descriptor_set_layout));
VkPipelineLayoutCreateInfo pipeline_layout_create_info =
vkb::initializers::pipeline_layout_create_info(
&graphics.descriptor_set_layout,
1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &graphics.pipeline_layout));
}
void ComputeNBody::setup_descriptor_set()
{
VkDescriptorSetAllocateInfo alloc_info =
vkb::initializers::descriptor_set_allocate_info(
descriptor_pool,
&graphics.descriptor_set_layout,
1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &graphics.descriptor_set));
VkDescriptorBufferInfo buffer_descriptor = create_descriptor(*graphics.uniform_buffer);
VkDescriptorImageInfo particle_image_descriptor = create_descriptor(textures.particle);
VkDescriptorImageInfo gradient_image_descriptor = create_descriptor(textures.gradient);
std::vector<VkWriteDescriptorSet> write_descriptor_sets;
write_descriptor_sets = {
vkb::initializers::write_descriptor_set(graphics.descriptor_set, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 0, &particle_image_descriptor),
vkb::initializers::write_descriptor_set(graphics.descriptor_set, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &gradient_image_descriptor),
vkb::initializers::write_descriptor_set(graphics.descriptor_set, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2, &buffer_descriptor),
};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
}
void ComputeNBody::prepare_pipelines()
{
VkPipelineInputAssemblyStateCreateInfo input_assembly_state =
vkb::initializers::pipeline_input_assembly_state_create_info(
VK_PRIMITIVE_TOPOLOGY_POINT_LIST,
0,
VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterization_state =
vkb::initializers::pipeline_rasterization_state_create_info(
VK_POLYGON_MODE_FILL,
VK_CULL_MODE_NONE,
VK_FRONT_FACE_COUNTER_CLOCKWISE,
0);
VkPipelineColorBlendAttachmentState blend_attachment_state =
vkb::initializers::pipeline_color_blend_attachment_state(
0xf,
VK_FALSE);
VkPipelineColorBlendStateCreateInfo color_blend_state =
vkb::initializers::pipeline_color_blend_state_create_info(
1,
&blend_attachment_state);
VkPipelineDepthStencilStateCreateInfo depth_stencil_state =
vkb::initializers::pipeline_depth_stencil_state_create_info(
VK_FALSE,
VK_FALSE,
VK_COMPARE_OP_ALWAYS);
VkPipelineViewportStateCreateInfo viewport_state =
vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0);
VkPipelineMultisampleStateCreateInfo multisample_state =
vkb::initializers::pipeline_multisample_state_create_info(
VK_SAMPLE_COUNT_1_BIT,
0);
std::vector<VkDynamicState> dynamic_state_enables = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamicState =
vkb::initializers::pipeline_dynamic_state_create_info(
dynamic_state_enables.data(),
static_cast<uint32_t>(dynamic_state_enables.size()),
0);
// Rendering pipeline
// Load shaders
std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages;
shader_stages[0] = load_shader("compute_nbody", "particle.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("compute_nbody", "particle.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
// Vertex bindings and attributes
const std::vector<VkVertexInputBindingDescription> vertex_input_bindings = {
vkb::initializers::vertex_input_binding_description(0, sizeof(Particle), VK_VERTEX_INPUT_RATE_VERTEX),
};
const std::vector<VkVertexInputAttributeDescription> vertex_input_attributes = {
vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Particle, pos)),
vkb::initializers::vertex_input_attribute_description(0, 1, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Particle, vel))};
VkPipelineVertexInputStateCreateInfo vertex_input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
vertex_input_state.vertexBindingDescriptionCount = static_cast<uint32_t>(vertex_input_bindings.size());
vertex_input_state.pVertexBindingDescriptions = vertex_input_bindings.data();
vertex_input_state.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size());
vertex_input_state.pVertexAttributeDescriptions = vertex_input_attributes.data();
VkGraphicsPipelineCreateInfo pipeline_create_info =
vkb::initializers::pipeline_create_info(
graphics.pipeline_layout,
render_pass,
0);
pipeline_create_info.pVertexInputState = &vertex_input_state;
pipeline_create_info.pInputAssemblyState = &input_assembly_state;
pipeline_create_info.pRasterizationState = &rasterization_state;
pipeline_create_info.pColorBlendState = &color_blend_state;
pipeline_create_info.pMultisampleState = &multisample_state;
pipeline_create_info.pViewportState = &viewport_state;
pipeline_create_info.pDepthStencilState = &depth_stencil_state;
pipeline_create_info.pDynamicState = &dynamicState;
pipeline_create_info.stageCount = static_cast<uint32_t>(shader_stages.size());
pipeline_create_info.pStages = shader_stages.data();
pipeline_create_info.renderPass = render_pass;
// Additive blending
blend_attachment_state.colorWriteMask = 0xF;
blend_attachment_state.blendEnable = VK_TRUE;
blend_attachment_state.colorBlendOp = VK_BLEND_OP_ADD;
blend_attachment_state.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
blend_attachment_state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
blend_attachment_state.alphaBlendOp = VK_BLEND_OP_ADD;
blend_attachment_state.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
blend_attachment_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_DST_ALPHA;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &graphics.pipeline));
}
void ComputeNBody::prepare_graphics()
{
prepare_storage_buffers();
prepare_uniform_buffers();
setup_descriptor_set_layout();
prepare_pipelines();
setup_descriptor_set();
// Semaphore for compute & graphics sync
VkSemaphoreCreateInfo semaphore_create_info = vkb::initializers::semaphore_create_info();
VK_CHECK(vkCreateSemaphore(get_device().get_handle(), &semaphore_create_info, nullptr, &graphics.semaphore));
}
void ComputeNBody::prepare_compute()
{
// Get compute queue
vkGetDeviceQueue(get_device().get_handle(), compute.queue_family_index, 0, &compute.queue);
// Create compute pipeline
// Compute pipelines are created separate from graphics pipelines even if they use the same queue (family index)
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings = {
// Binding 0 : Particle position storage buffer
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
VK_SHADER_STAGE_COMPUTE_BIT,
0),
// Binding 1 : Uniform buffer
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
VK_SHADER_STAGE_COMPUTE_BIT,
1),
};
VkDescriptorSetLayoutCreateInfo descriptor_layout =
vkb::initializers::descriptor_set_layout_create_info(
set_layout_bindings.data(),
static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout, nullptr, &compute.descriptor_set_layout));
VkPipelineLayoutCreateInfo pipeline_layout_create_info =
vkb::initializers::pipeline_layout_create_info(
&compute.descriptor_set_layout,
1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &compute.pipeline_layout));
VkDescriptorSetAllocateInfo alloc_info =
vkb::initializers::descriptor_set_allocate_info(
descriptor_pool,
&compute.descriptor_set_layout,
1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &compute.descriptor_set));
VkDescriptorBufferInfo storage_buffer_descriptor = create_descriptor(*compute.storage_buffer);
VkDescriptorBufferInfo uniform_buffer_descriptor = create_descriptor(*compute.uniform_buffer);
std::vector<VkWriteDescriptorSet> compute_write_descriptor_sets =
{
// Binding 0 : Particle position storage buffer
vkb::initializers::write_descriptor_set(
compute.descriptor_set,
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
0,
&storage_buffer_descriptor),
// Binding 1 : Uniform buffer
vkb::initializers::write_descriptor_set(
compute.descriptor_set,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
1,
&uniform_buffer_descriptor)};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(compute_write_descriptor_sets.size()), compute_write_descriptor_sets.data(), 0, NULL);
// Create pipelines
VkComputePipelineCreateInfo compute_pipeline_create_info = vkb::initializers::compute_pipeline_create_info(compute.pipeline_layout, 0);
// 1st pass - Particle movement calculations
compute_pipeline_create_info.stage = load_shader("compute_nbody", "particle_calculate.comp.spv", VK_SHADER_STAGE_COMPUTE_BIT);
// Set some shader parameters via specialization constants
struct SpecializationData
{
uint32_t workgroup_size;
uint32_t shared_data_size;
float gravity;
float power;
float soften;
} specialization_data;
std::vector<VkSpecializationMapEntry> specialization_map_entries;
specialization_map_entries.push_back(vkb::initializers::specialization_map_entry(0, offsetof(SpecializationData, workgroup_size), sizeof(uint32_t)));
specialization_map_entries.push_back(vkb::initializers::specialization_map_entry(1, offsetof(SpecializationData, shared_data_size), sizeof(uint32_t)));
specialization_map_entries.push_back(vkb::initializers::specialization_map_entry(2, offsetof(SpecializationData, gravity), sizeof(float)));
specialization_map_entries.push_back(vkb::initializers::specialization_map_entry(3, offsetof(SpecializationData, power), sizeof(float)));
specialization_map_entries.push_back(vkb::initializers::specialization_map_entry(4, offsetof(SpecializationData, soften), sizeof(float)));
specialization_data.workgroup_size = work_group_size;
specialization_data.shared_data_size = shared_data_size;
specialization_data.gravity = 0.002f;
specialization_data.power = 0.75f;
specialization_data.soften = 0.05f;
VkSpecializationInfo specialization_info =
vkb::initializers::specialization_info(static_cast<uint32_t>(specialization_map_entries.size()), specialization_map_entries.data(), sizeof(specialization_data), &specialization_data);
compute_pipeline_create_info.stage.pSpecializationInfo = &specialization_info;
VK_CHECK(vkCreateComputePipelines(get_device().get_handle(), pipeline_cache, 1, &compute_pipeline_create_info, nullptr, &compute.pipeline_calculate));
// 2nd pass - Particle integration
compute_pipeline_create_info.stage = load_shader("compute_nbody", "particle_integrate.comp.spv", VK_SHADER_STAGE_COMPUTE_BIT);
specialization_map_entries.clear();
specialization_map_entries.push_back(vkb::initializers::specialization_map_entry(0, 0, sizeof(uint32_t)));
specialization_info =
vkb::initializers::specialization_info(1, specialization_map_entries.data(), sizeof(work_group_size), &work_group_size);
compute_pipeline_create_info.stage.pSpecializationInfo = &specialization_info;
VK_CHECK(vkCreateComputePipelines(get_device().get_handle(), pipeline_cache, 1, &compute_pipeline_create_info, nullptr, &compute.pipeline_integrate));
// Separate command pool as queue family for compute may be different than graphics
VkCommandPoolCreateInfo command_pool_create_info = {};
command_pool_create_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
command_pool_create_info.queueFamilyIndex = vkb::get_queue_family_index(get_device().get_gpu().get_queue_family_properties(), VK_QUEUE_COMPUTE_BIT);
VK_CHECK(vkCreateCommandPool(get_device().get_handle(), &command_pool_create_info, nullptr, &compute.command_pool));
// Create a command buffer for compute operations
VkCommandBufferAllocateInfo command_buffer_allocate_info =
vkb::initializers::command_buffer_allocate_info(
compute.command_pool,
VK_COMMAND_BUFFER_LEVEL_PRIMARY,
1);
VK_CHECK(vkAllocateCommandBuffers(get_device().get_handle(), &command_buffer_allocate_info, &compute.command_buffer));
// Semaphore for compute & graphics sync
VkSemaphoreCreateInfo semaphore_create_info = vkb::initializers::semaphore_create_info();
VK_CHECK(vkCreateSemaphore(get_device().get_handle(), &semaphore_create_info, nullptr, &compute.semaphore));
// Signal the semaphore
VkSubmitInfo submit_info = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &compute.semaphore;
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE));
VK_CHECK(vkQueueWaitIdle(queue));
// Build a single command buffer containing the compute dispatch commands
build_compute_command_buffer();
// If necessary, acquire and immediately release the storage buffer, so that the initial acquire
// from the graphics command buffers are matched up properly.
if (graphics.queue_family_index != compute.queue_family_index)
{
VkCommandBuffer transfer_command;
// Create a transient command buffer for setting up the initial buffer transfer state
VkCommandBufferAllocateInfo command_buffer_allocate_info =
vkb::initializers::command_buffer_allocate_info(
compute.command_pool,
VK_COMMAND_BUFFER_LEVEL_PRIMARY,
1);
VK_CHECK(vkAllocateCommandBuffers(get_device().get_handle(), &command_buffer_allocate_info, &transfer_command));
VkCommandBufferBeginInfo command_buffer_info{};
command_buffer_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
VK_CHECK(vkBeginCommandBuffer(transfer_command, &command_buffer_info));
VkBufferMemoryBarrier acquire_buffer_barrier =
{
VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
nullptr,
0,
VK_ACCESS_SHADER_WRITE_BIT,
graphics.queue_family_index,
compute.queue_family_index,
compute.storage_buffer->get_handle(),
0,
compute.storage_buffer->get_size()};
vkCmdPipelineBarrier(
transfer_command,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0,
0, nullptr,
1, &acquire_buffer_barrier,
0, nullptr);
VkBufferMemoryBarrier release_buffer_barrier =
{
VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
nullptr,
VK_ACCESS_SHADER_WRITE_BIT,
0,
compute.queue_family_index,
graphics.queue_family_index,
compute.storage_buffer->get_handle(),
0,
compute.storage_buffer->get_size()};
vkCmdPipelineBarrier(
transfer_command,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
0,
0, nullptr,
1, &release_buffer_barrier,
0, nullptr);
// Copied from Device::flush_command_buffer, which we can't use because it would be
// working with the wrong command pool
VK_CHECK(vkEndCommandBuffer(transfer_command));
// Submit compute commands
VkSubmitInfo submit_info{};
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &transfer_command;
// Create fence to ensure that the command buffer has finished executing
VkFenceCreateInfo fence_info{};
fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fence_info.flags = VK_FLAGS_NONE;
VkFence fence;
VK_CHECK(vkCreateFence(get_device().get_handle(), &fence_info, nullptr, &fence));
// Submit to the *compute* queue
VkResult result = vkQueueSubmit(compute.queue, 1, &submit_info, fence);
// Wait for the fence to signal that command buffer has finished executing
VK_CHECK(vkWaitForFences(get_device().get_handle(), 1, &fence, VK_TRUE, DEFAULT_FENCE_TIMEOUT));
vkDestroyFence(get_device().get_handle(), fence, nullptr);
vkFreeCommandBuffers(get_device().get_handle(), compute.command_pool, 1, &transfer_command);
}
}
// Prepare and initialize uniform buffer containing shader uniforms
void ComputeNBody::prepare_uniform_buffers()
{
// Compute shader uniform buffer block
compute.uniform_buffer = std::make_unique<vkb::core::BufferC>(get_device(),
sizeof(compute.ubo),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
// Vertex shader uniform buffer block
graphics.uniform_buffer = std::make_unique<vkb::core::BufferC>(get_device(),
sizeof(graphics.ubo),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
update_compute_uniform_buffers(1.0f);
update_graphics_uniform_buffers();
}
void ComputeNBody::update_compute_uniform_buffers(float delta_time)
{
compute.ubo.delta_time = paused ? 0.0f : delta_time;
compute.uniform_buffer->convert_and_update(compute.ubo);
}
void ComputeNBody::update_graphics_uniform_buffers()
{
graphics.ubo.projection = camera.matrices.perspective;
graphics.ubo.view = camera.matrices.view;
graphics.ubo.screenDim = glm::vec2(static_cast<float>(width), static_cast<float>(height));
graphics.uniform_buffer->convert_and_update(graphics.ubo);
}
void ComputeNBody::draw()
{
ApiVulkanSample::prepare_frame();
VkPipelineStageFlags graphics_wait_stage_masks[] = {VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
VkSemaphore graphics_wait_semaphores[] = {compute.semaphore, semaphores.acquired_image_ready};
VkSemaphore graphics_signal_semaphores[] = {graphics.semaphore, semaphores.render_complete};
// Submit graphics commands
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &draw_cmd_buffers[current_buffer];
submit_info.waitSemaphoreCount = 2;
submit_info.pWaitSemaphores = graphics_wait_semaphores;
submit_info.pWaitDstStageMask = graphics_wait_stage_masks;
submit_info.signalSemaphoreCount = 2;
submit_info.pSignalSemaphores = graphics_signal_semaphores;
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE));
ApiVulkanSample::submit_frame();
// Wait for rendering finished
VkPipelineStageFlags wait_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
// Submit compute commands
VkSubmitInfo compute_submit_info = vkb::initializers::submit_info();
compute_submit_info.commandBufferCount = 1;
compute_submit_info.pCommandBuffers = &compute.command_buffer;
compute_submit_info.waitSemaphoreCount = 1;
compute_submit_info.pWaitSemaphores = &graphics.semaphore;
compute_submit_info.pWaitDstStageMask = &wait_stage_mask;
compute_submit_info.signalSemaphoreCount = 1;
compute_submit_info.pSignalSemaphores = &compute.semaphore;
VK_CHECK(vkQueueSubmit(compute.queue, 1, &compute_submit_info, VK_NULL_HANDLE));
}
bool ComputeNBody::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
auto const &queue_family_properties = get_device().get_gpu().get_queue_family_properties();
graphics.queue_family_index = vkb::get_queue_family_index(queue_family_properties, VK_QUEUE_GRAPHICS_BIT);
compute.queue_family_index = vkb::get_queue_family_index(queue_family_properties, VK_QUEUE_COMPUTE_BIT);
// Not all implementations support a work group size of 256, so we need to check with the device limits
work_group_size = std::min(static_cast<uint32_t>(256), get_device().get_gpu().get_properties().limits.maxComputeWorkGroupSize[0]);
// Same for shared data size for passing data between shader invocations
shared_data_size = std::min(static_cast<uint32_t>(1024), static_cast<uint32_t>(get_device().get_gpu().get_properties().limits.maxComputeSharedMemorySize / sizeof(glm::vec4)));
load_assets();
setup_descriptor_pool();
prepare_graphics();
prepare_compute();
build_command_buffers();
prepared = true;
return true;
}
void ComputeNBody::render(float delta_time)
{
if (!prepared)
{
return;
}
draw();
update_compute_uniform_buffers(delta_time);
if (camera.updated)
{
update_graphics_uniform_buffers();
}
}
bool ComputeNBody::resize(const uint32_t width, const uint32_t height)
{
ApiVulkanSample::resize(width, height);
update_graphics_uniform_buffers();
return true;
}
std::unique_ptr<vkb::Application> create_compute_nbody()
{
return std::make_unique<ComputeNBody>();
}
-119
View File
@@ -1,119 +0,0 @@
/* Copyright (c) 2019-2024, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Compute shader N-body simulation using two passes and shared compute shader memory
*/
#pragma once
#include "api_vulkan_sample.h"
#if defined(__ANDROID__)
// Lower particle count on Android for performance reasons
# define PARTICLES_PER_ATTRACTOR 3 * 1024
#else
# define PARTICLES_PER_ATTRACTOR 4 * 1024
#endif
class ComputeNBody : public ApiVulkanSample
{
public:
uint32_t num_particles;
uint32_t work_group_size = 128;
uint32_t shared_data_size = 1024;
struct
{
Texture particle;
Texture gradient;
} textures;
// Resources for the graphics part of the example
struct
{
std::unique_ptr<vkb::core::BufferC> uniform_buffer; // Contains scene matrices
VkDescriptorSetLayout descriptor_set_layout; // Particle system rendering shader binding layout
VkDescriptorSet descriptor_set; // Particle system rendering shader bindings
VkPipelineLayout pipeline_layout; // Layout of the graphics pipeline
VkPipeline pipeline; // Particle rendering pipeline
VkSemaphore semaphore; // Execution dependency between compute & graphic submission
uint32_t queue_family_index;
struct
{
glm::mat4 projection;
glm::mat4 view;
glm::vec2 screenDim;
} ubo;
} graphics;
// Resources for the compute part of the example
struct
{
std::unique_ptr<vkb::core::BufferC> storage_buffer; // (Shader) storage buffer object containing the particles
std::unique_ptr<vkb::core::BufferC> uniform_buffer; // Uniform buffer object containing particle system parameters
VkQueue queue; // Separate queue for compute commands (queue family may differ from the one used for graphics)
VkCommandPool command_pool; // Use a separate command pool (queue family may differ from the one used for graphics)
VkCommandBuffer command_buffer; // Command buffer storing the dispatch commands and barriers
VkSemaphore semaphore; // Execution dependency between compute & graphic submission
VkDescriptorSetLayout descriptor_set_layout; // Compute shader binding layout
VkDescriptorSet descriptor_set; // Compute shader bindings
VkPipelineLayout pipeline_layout; // Layout of the compute pipeline
VkPipeline pipeline_calculate; // Compute pipeline for N-Body velocity calculation (1st pass)
VkPipeline pipeline_integrate; // Compute pipeline for euler integration (2nd pass)
VkPipeline blur;
VkPipelineLayout pipeline_layout_blur;
VkDescriptorSetLayout descriptor_set_layout_blur;
VkDescriptorSet descriptor_set_blur;
uint32_t queue_family_index;
struct ComputeUBO
{ // Compute shader uniform block object
float delta_time; // Frame delta time
int32_t particle_count;
} ubo;
} compute;
// SSBO particle declaration
struct Particle
{
glm::vec4 pos; // xyz = position, w = mass
glm::vec4 vel; // xyz = velocity, w = gradient texture position
};
ComputeNBody();
~ComputeNBody();
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
void load_assets();
void build_command_buffers() override;
void build_compute_command_buffer();
void prepare_storage_buffers();
void setup_descriptor_pool();
void setup_descriptor_set_layout();
void setup_descriptor_set();
void prepare_pipelines();
void prepare_graphics();
void prepare_compute();
void prepare_uniform_buffers();
void update_compute_uniform_buffers(float delta_time);
void update_graphics_uniform_buffers();
void draw();
bool prepare(const vkb::ApplicationOptions &options) override;
virtual void render(float delta_time) override;
virtual bool resize(const uint32_t width, const uint32_t height) override;
};
std::unique_ptr<vkb::Application> create_compute_nbody();
@@ -1,33 +0,0 @@
# Copyright (c) 2019-2024, Sascha Willems
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 the "License";
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
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 "Sascha Willems"
NAME "Dynamic uniform buffers"
DESCRIPTION "Demonstrates the use of dynamic offsets into one single uniform buffers for rendering multiple objects"
SHADER_FILES_GLSL
"dynamic_uniform_buffers/glsl/base.vert"
"dynamic_uniform_buffers/glsl/base.frag"
SHADER_FILES_HLSL
"dynamic_uniform_buffers/hlsl/base.vert.hlsl"
"dynamic_uniform_buffers/hlsl/base.frag.hlsl")
@@ -1,27 +0,0 @@
////
- Copyright (c) 2019-2023, 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.
-
////
= Dynamic Uniform buffers
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/dynamic_uniform_buffers[Khronos Vulkan samples github repository].
endif::[]
Dynamic uniform buffers are used for rendering multiple objects with separate matrices stored in a single uniform buffer object, that are addressed dynamically.
@@ -1,541 +0,0 @@
/* Copyright (c) 2019-2025, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Demonstrates the use of dynamic uniform buffers.
*
* Instead of using one uniform buffer per-object, this example allocates one big uniform buffer
* with respect to the alignment reported by the device via minUniformBufferOffsetAlignment that
* contains all matrices for the objects in the scene.
*
* The used descriptor type VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC then allows to set a dynamic
* offset used to pass data from the single uniform buffer to the connected shader binding point.
*/
#include "dynamic_uniform_buffers.h"
#include "benchmark_mode/benchmark_mode.h"
DynamicUniformBuffers::DynamicUniformBuffers()
{
title = "Dynamic uniform buffers";
}
DynamicUniformBuffers ::~DynamicUniformBuffers()
{
if (has_device())
{
if (ubo_data_dynamic.model)
{
aligned_free(ubo_data_dynamic.model);
}
// Clean up used Vulkan resources
// Note : Inherited destructor cleans up resources stored in base class
vkDestroyPipeline(get_device().get_handle(), pipeline, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layout, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), descriptor_set_layout, nullptr);
}
}
// Wrapper functions for aligned memory allocation
// There is currently no standard for this in C++ that works across all platforms and vendors, so we abstract this
void *DynamicUniformBuffers::aligned_alloc(size_t size, size_t alignment)
{
void *data = nullptr;
#if defined(_MSC_VER) || defined(__MINGW32__)
data = _aligned_malloc(size, alignment);
#else
int res = posix_memalign(&data, alignment, size);
if (res != 0)
{
data = nullptr;
}
#endif
return data;
}
void DynamicUniformBuffers::aligned_free(void *data)
{
#if defined(_MSC_VER) || defined(__MINGW32__)
_aligned_free(data);
#else
free(data);
#endif
}
void DynamicUniformBuffers::build_command_buffers()
{
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VkClearValue clear_values[2];
clear_values[0].color = default_clear_color;
clear_values[1].depthStencil = {0.0f, 0};
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.renderPass = render_pass;
render_pass_begin_info.renderArea.offset.x = 0;
render_pass_begin_info.renderArea.offset.y = 0;
render_pass_begin_info.renderArea.extent.width = width;
render_pass_begin_info.renderArea.extent.height = height;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.pClearValues = clear_values;
for (int32_t i = 0; i < static_cast<int32_t>(draw_cmd_buffers.size()); ++i)
{
render_pass_begin_info.framebuffer = framebuffers[i];
VK_CHECK(vkBeginCommandBuffer(draw_cmd_buffers[i], &command_buffer_begin_info));
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(width, height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
VkDeviceSize offsets[1] = {0};
vkCmdBindVertexBuffers(draw_cmd_buffers[i], 0, 1, vertex_buffer->get(), offsets);
vkCmdBindIndexBuffer(draw_cmd_buffers[i], index_buffer->get_handle(), 0, VK_INDEX_TYPE_UINT32);
// Render multiple objects using different model matrices by dynamically offsetting into one uniform buffer
for (uint32_t j = 0; j < OBJECT_INSTANCES; j++)
{
// One dynamic offset per dynamic descriptor to offset into the ubo containing all model matrices
uint32_t dynamic_offset = j * static_cast<uint32_t>(dynamic_alignment);
// Bind the descriptor set for rendering a mesh using the dynamic offset
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set, 1, &dynamic_offset);
vkCmdDrawIndexed(draw_cmd_buffers[i], index_count, 1, 0, 0, 0);
}
draw_ui(draw_cmd_buffers[i]);
vkCmdEndRenderPass(draw_cmd_buffers[i]);
VK_CHECK(vkEndCommandBuffer(draw_cmd_buffers[i]));
}
}
void DynamicUniformBuffers::draw()
{
ApiVulkanSample::prepare_frame();
// Command buffer to be submitted to the queue
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &draw_cmd_buffers[current_buffer];
// Submit to queue
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE));
ApiVulkanSample::submit_frame();
}
void DynamicUniformBuffers::generate_cube()
{
// Setup vertices indices for a colored cube
std::vector<Vertex> vertices = {
{{-1.0f, -1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}},
{{1.0f, -1.0f, 1.0f}, {0.0f, 1.0f, 0.0f}},
{{1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 0.0f}},
{{-1.0f, -1.0f, -1.0f}, {1.0f, 0.0f, 0.0f}},
{{1.0f, -1.0f, -1.0f}, {0.0f, 1.0f, 0.0f}},
{{1.0f, 1.0f, -1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, 1.0f, -1.0f}, {0.0f, 0.0f, 0.0f}},
};
std::vector<uint32_t> indices = {
0,
1,
2,
2,
3,
0,
1,
5,
6,
6,
2,
1,
7,
6,
5,
5,
4,
7,
4,
0,
3,
3,
7,
4,
4,
5,
1,
1,
0,
4,
3,
2,
6,
6,
7,
3,
};
index_count = static_cast<uint32_t>(indices.size());
auto vertex_buffer_size = vertices.size() * sizeof(Vertex);
auto index_buffer_size = indices.size() * sizeof(uint32_t);
// Create buffers
// For the sake of simplicity we won't stage the vertex data to the gpu memory
// Vertex buffer
vertex_buffer = std::make_unique<vkb::core::BufferC>(get_device(),
vertex_buffer_size,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
vertex_buffer->update(vertices.data(), vertex_buffer_size);
index_buffer = std::make_unique<vkb::core::BufferC>(get_device(),
index_buffer_size,
VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
index_buffer->update(indices.data(), index_buffer_size);
}
void DynamicUniformBuffers::setup_descriptor_pool()
{
// Example uses one ubo and one image sampler
std::vector<VkDescriptorPoolSize> pool_sizes =
{
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1)};
VkDescriptorPoolCreateInfo descriptor_pool_create_info =
vkb::initializers::descriptor_pool_create_info(
static_cast<uint32_t>(pool_sizes.size()),
pool_sizes.data(),
2);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &descriptor_pool_create_info, nullptr, &descriptor_pool));
}
void DynamicUniformBuffers::setup_descriptor_set_layout()
{
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings =
{
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 0),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, VK_SHADER_STAGE_VERTEX_BIT, 1),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 2)};
VkDescriptorSetLayoutCreateInfo descriptor_layout =
vkb::initializers::descriptor_set_layout_create_info(
set_layout_bindings.data(),
static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout, nullptr, &descriptor_set_layout));
VkPipelineLayoutCreateInfo pipeline_layout_create_info =
vkb::initializers::pipeline_layout_create_info(
&descriptor_set_layout,
1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &pipeline_layout));
}
void DynamicUniformBuffers::setup_descriptor_set()
{
VkDescriptorSetAllocateInfo alloc_info =
vkb::initializers::descriptor_set_allocate_info(
descriptor_pool,
&descriptor_set_layout,
1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &descriptor_set));
VkDescriptorBufferInfo view_buffer_descriptor = create_descriptor(*uniform_buffers.view);
// Pass the actual dynamic alignment as the descriptor's size
VkDescriptorBufferInfo dynamic_buffer_descriptor = create_descriptor(*uniform_buffers.dynamic, dynamic_alignment);
std::vector<VkWriteDescriptorSet> write_descriptor_sets = {
// Binding 0 : Projection/View matrix uniform buffer
vkb::initializers::write_descriptor_set(descriptor_set, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &view_buffer_descriptor),
// Binding 1 : Instance matrix as dynamic uniform buffer
vkb::initializers::write_descriptor_set(descriptor_set, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, &dynamic_buffer_descriptor),
};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
}
void DynamicUniformBuffers::prepare_pipelines()
{
VkPipelineInputAssemblyStateCreateInfo input_assembly_state =
vkb::initializers::pipeline_input_assembly_state_create_info(
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
0,
VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterization_state =
vkb::initializers::pipeline_rasterization_state_create_info(
VK_POLYGON_MODE_FILL,
VK_CULL_MODE_NONE,
VK_FRONT_FACE_COUNTER_CLOCKWISE,
0);
VkPipelineColorBlendAttachmentState blend_attachment_state =
vkb::initializers::pipeline_color_blend_attachment_state(
0xf,
VK_FALSE);
VkPipelineColorBlendStateCreateInfo color_blend_state =
vkb::initializers::pipeline_color_blend_state_create_info(
1,
&blend_attachment_state);
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
VkPipelineDepthStencilStateCreateInfo depth_stencil_state =
vkb::initializers::pipeline_depth_stencil_state_create_info(
VK_TRUE,
VK_TRUE,
VK_COMPARE_OP_GREATER);
VkPipelineViewportStateCreateInfo viewport_state =
vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0);
VkPipelineMultisampleStateCreateInfo multisample_state =
vkb::initializers::pipeline_multisample_state_create_info(
VK_SAMPLE_COUNT_1_BIT,
0);
std::vector<VkDynamicState> dynamic_state_enables = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic_state =
vkb::initializers::pipeline_dynamic_state_create_info(
dynamic_state_enables.data(),
static_cast<uint32_t>(dynamic_state_enables.size()),
0);
// Load shaders
std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages;
shader_stages[0] = load_shader("dynamic_uniform_buffers", "base.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("dynamic_uniform_buffers", "base.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
// Vertex bindings and attributes
const std::vector<VkVertexInputBindingDescription> vertex_input_bindings = {
vkb::initializers::vertex_input_binding_description(0, sizeof(Vertex), VK_VERTEX_INPUT_RATE_VERTEX),
};
const std::vector<VkVertexInputAttributeDescription> vertex_input_attributes = {
vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, pos)), // Location 0 : Position
vkb::initializers::vertex_input_attribute_description(0, 1, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, color)), // Location 1 : Color
};
VkPipelineVertexInputStateCreateInfo vertex_input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
vertex_input_state.vertexBindingDescriptionCount = static_cast<uint32_t>(vertex_input_bindings.size());
vertex_input_state.pVertexBindingDescriptions = vertex_input_bindings.data();
vertex_input_state.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size());
vertex_input_state.pVertexAttributeDescriptions = vertex_input_attributes.data();
VkGraphicsPipelineCreateInfo pipeline_create_info =
vkb::initializers::pipeline_create_info(
pipeline_layout,
render_pass,
0);
pipeline_create_info.pVertexInputState = &vertex_input_state;
pipeline_create_info.pInputAssemblyState = &input_assembly_state;
pipeline_create_info.pRasterizationState = &rasterization_state;
pipeline_create_info.pColorBlendState = &color_blend_state;
pipeline_create_info.pMultisampleState = &multisample_state;
pipeline_create_info.pViewportState = &viewport_state;
pipeline_create_info.pDepthStencilState = &depth_stencil_state;
pipeline_create_info.pDynamicState = &dynamic_state;
pipeline_create_info.stageCount = static_cast<uint32_t>(shader_stages.size());
pipeline_create_info.pStages = shader_stages.data();
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipeline));
}
// Prepare and initialize uniform buffer containing shader uniforms
void DynamicUniformBuffers::prepare_uniform_buffers()
{
// Allocate data for the dynamic uniform buffer object
// We allocate this manually as the alignment of the offset differs between GPUs
// Calculate required alignment based on minimum device offset alignment
size_t min_ubo_alignment = static_cast<size_t>(get_device().get_gpu().get_properties().limits.minUniformBufferOffsetAlignment);
dynamic_alignment = sizeof(glm::mat4);
if (min_ubo_alignment > 0)
{
dynamic_alignment = (dynamic_alignment + min_ubo_alignment - 1) & ~(min_ubo_alignment - 1);
}
size_t buffer_size = OBJECT_INSTANCES * dynamic_alignment;
ubo_data_dynamic.model = static_cast<glm::mat4 *>(aligned_alloc(buffer_size, dynamic_alignment));
assert(ubo_data_dynamic.model);
std::cout << "minUniformBufferOffsetAlignment = " << min_ubo_alignment << std::endl;
std::cout << "dynamicAlignment = " << dynamic_alignment << std::endl;
// Vertex shader uniform buffer block
// Static shared uniform buffer object with projection and view matrix
uniform_buffers.view = std::make_unique<vkb::core::BufferC>(get_device(),
sizeof(ubo_vs),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
uniform_buffers.dynamic = std::make_unique<vkb::core::BufferC>(get_device(),
buffer_size,
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
// Prepare per-object matrices with offsets and random rotations
std::default_random_engine rnd_engine(lock_simulation_speed ? 0 : static_cast<unsigned>(time(nullptr)));
std::normal_distribution<float> rnd_dist(-1.0f, 1.0f);
for (uint32_t i = 0; i < OBJECT_INSTANCES; i++)
{
rotations[i] = glm::vec3(rnd_dist(rnd_engine), rnd_dist(rnd_engine), rnd_dist(rnd_engine)) * 2.0f * glm::pi<float>();
rotation_speeds[i] = glm::vec3(rnd_dist(rnd_engine), rnd_dist(rnd_engine), rnd_dist(rnd_engine));
}
update_uniform_buffers();
update_dynamic_uniform_buffer(0.0f, true);
}
void DynamicUniformBuffers::update_uniform_buffers()
{
// Fixed ubo with projection and view matrices
ubo_vs.projection = camera.matrices.perspective;
ubo_vs.view = camera.matrices.view;
uniform_buffers.view->convert_and_update(ubo_vs);
}
void DynamicUniformBuffers::update_dynamic_uniform_buffer(float delta_time, bool force)
{
// Update at max. 60 fps
animation_timer += delta_time;
if ((animation_timer + 0.0025 < (1.0f / 60.0f)) && (!force))
{
return;
}
// Dynamic ubo with per-object model matrices indexed by offsets in the command buffer
auto dim = static_cast<uint32_t>(pow(OBJECT_INSTANCES, (1.0f / 3.0f)));
auto fdim = static_cast<float>(dim);
glm::vec3 offset(5.0f);
for (uint32_t x = 0; x < dim; x++)
{
auto fx = static_cast<float>(x);
for (uint32_t y = 0; y < dim; y++)
{
auto fy = static_cast<float>(y);
for (uint32_t z = 0; z < dim; z++)
{
auto fz = static_cast<float>(z);
auto index = x * dim * dim + y * dim + z;
// Aligned offset
auto model_mat = (glm::mat4 *) (((uint64_t) ubo_data_dynamic.model + (index * dynamic_alignment)));
// Update rotations
rotations[index] += animation_timer * rotation_speeds[index];
// Update matrices
glm::vec3 pos(-((fdim * offset.x) / 2.0f) + offset.x / 2.0f + fx * offset.x,
-((fdim * offset.y) / 2.0f) + offset.y / 2.0f + fy * offset.y,
-((fdim * offset.z) / 2.0f) + offset.z / 2.0f + fz * offset.z);
*model_mat = glm::translate(glm::mat4(1.0f), pos);
*model_mat = glm::rotate(*model_mat, rotations[index].x, glm::vec3(1.0f, 1.0f, 0.0f));
*model_mat = glm::rotate(*model_mat, rotations[index].y, glm::vec3(0.0f, 1.0f, 0.0f));
*model_mat = glm::rotate(*model_mat, rotations[index].z, glm::vec3(0.0f, 0.0f, 1.0f));
}
}
}
animation_timer = 0.0f;
uniform_buffers.dynamic->update(ubo_data_dynamic.model, static_cast<size_t>(uniform_buffers.dynamic->get_size()));
// Flush to make changes visible to the device
uniform_buffers.dynamic->flush();
}
bool DynamicUniformBuffers::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
camera.type = vkb::CameraType::LookAt;
camera.set_position(glm::vec3(0.0f, 0.0f, -30.0f));
camera.set_rotation(glm::vec3(0.0f));
// Note: Using reversed depth-buffer for increased precision, so Znear and Zfar are flipped
camera.set_perspective(60.0f, static_cast<float>(width) / static_cast<float>(height), 256.0f, 0.1f);
generate_cube();
prepare_uniform_buffers();
setup_descriptor_set_layout();
prepare_pipelines();
setup_descriptor_pool();
setup_descriptor_set();
build_command_buffers();
prepared = true;
return true;
}
bool DynamicUniformBuffers::resize(const uint32_t width, const uint32_t height)
{
ApiVulkanSample::resize(width, height);
update_uniform_buffers();
return true;
}
void DynamicUniformBuffers::render(float delta_time)
{
if (!prepared)
{
return;
}
draw();
if (!paused)
{
update_dynamic_uniform_buffer(delta_time);
}
if (camera.updated)
{
update_uniform_buffers();
}
}
std::unique_ptr<vkb::Application> create_dynamic_uniform_buffers()
{
return std::make_unique<DynamicUniformBuffers>();
}
@@ -1,101 +0,0 @@
/* Copyright (c) 2019-2024, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Demonstrates the use of dynamic uniform buffers.
*
* Instead of using one uniform buffer per-object, this example allocates one big uniform buffer
* with respect to the alignment reported by the device via minUniformBufferOffsetAlignment that
* contains all matrices for the objects in the scene.
*
* The used descriptor type VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC then allows to set a dynamic
* offset used to pass data from the single uniform buffer to the connected shader binding point.
*/
#pragma once
#include "api_vulkan_sample.h"
#define OBJECT_INSTANCES 125
class DynamicUniformBuffers : public ApiVulkanSample
{
private:
void *aligned_alloc(size_t size, size_t alignment);
void aligned_free(void *data);
public:
struct Vertex
{
float pos[3];
float color[3];
};
std::unique_ptr<vkb::core::BufferC> vertex_buffer;
std::unique_ptr<vkb::core::BufferC> index_buffer;
uint32_t index_count = 0;
struct UniformBuffers
{
std::unique_ptr<vkb::core::BufferC> view;
std::unique_ptr<vkb::core::BufferC> dynamic;
} uniform_buffers;
struct UboVS
{
glm::mat4 projection;
glm::mat4 view;
} ubo_vs;
// Store random per-object rotations
glm::vec3 rotations[OBJECT_INSTANCES];
glm::vec3 rotation_speeds[OBJECT_INSTANCES];
// One big uniform buffer that contains all matrices
// Note that we need to manually allocate the data to cope for GPU-specific uniform buffer offset alignments
struct UboDataDynamic
{
glm::mat4 *model = nullptr;
} ubo_data_dynamic;
VkPipeline pipeline;
VkPipelineLayout pipeline_layout;
VkDescriptorSet descriptor_set;
VkDescriptorSetLayout descriptor_set_layout;
float animation_timer = 0.0f;
size_t dynamic_alignment = 0;
DynamicUniformBuffers();
~DynamicUniformBuffers();
void build_command_buffers() override;
void generate_cube();
void setup_descriptor_pool();
void setup_descriptor_set_layout();
void setup_descriptor_set();
void prepare_pipelines();
void prepare_uniform_buffers();
void update_uniform_buffers();
void update_dynamic_uniform_buffer(float delta_time, bool force = false);
void draw();
bool prepare(const vkb::ApplicationOptions &options) override;
virtual void render(float delta_time) override;
virtual bool resize(const uint32_t width, const uint32_t height) override;
};
std::unique_ptr<vkb::Application> create_dynamic_uniform_buffers();
-41
View File
@@ -1,41 +0,0 @@
# Copyright (c) 2019-2024, Sascha Willems
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 the "License";
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
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 "Sascha Willems"
NAME "HDR"
DESCRIPTION "High-dynamic-range rendering"
SHADER_FILES_GLSL
"hdr/glsl/composition.vert"
"hdr/glsl/composition.frag"
"hdr/glsl/bloom.vert"
"hdr/glsl/bloom.frag"
"hdr/glsl/gbuffer.vert"
"hdr/glsl/gbuffer.frag"
SHADER_FILES_HLSL
"hdr/hlsl/composition.vert.hlsl"
"hdr/hlsl/composition.frag.hlsl"
"hdr/hlsl/bloom.vert.hlsl"
"hdr/hlsl/bloom.frag.hlsl"
"hdr/hlsl/gbuffer.vert.hlsl"
"hdr/hlsl/gbuffer.frag.hlsl")
-27
View File
@@ -1,27 +0,0 @@
////
- Copyright (c) 2019-2023, 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.
-
////
= High dynamic range
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hdr[Khronos Vulkan samples github repository].
endif::[]
Implements a high dynamic range rendering pipeline using 16/32 bit floating point precision for all calculations.
-961
View File
@@ -1,961 +0,0 @@
/* Copyright (c) 2019-2025, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* High dynamic range rendering
*/
#include "hdr.h"
#include "scene_graph/components/sub_mesh.h"
HDR::HDR()
{
title = "High dynamic range rendering";
}
HDR::~HDR()
{
if (has_device())
{
vkDestroyPipeline(get_device().get_handle(), pipelines.skybox, nullptr);
vkDestroyPipeline(get_device().get_handle(), pipelines.reflect, nullptr);
vkDestroyPipeline(get_device().get_handle(), pipelines.composition, nullptr);
vkDestroyPipeline(get_device().get_handle(), pipelines.bloom[0], nullptr);
vkDestroyPipeline(get_device().get_handle(), pipelines.bloom[1], nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layouts.models, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layouts.composition, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layouts.bloom_filter, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), descriptor_set_layouts.models, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), descriptor_set_layouts.composition, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), descriptor_set_layouts.bloom_filter, nullptr);
vkDestroyRenderPass(get_device().get_handle(), offscreen.render_pass, nullptr);
vkDestroyRenderPass(get_device().get_handle(), filter_pass.render_pass, nullptr);
vkDestroyFramebuffer(get_device().get_handle(), offscreen.framebuffer, nullptr);
vkDestroyFramebuffer(get_device().get_handle(), filter_pass.framebuffer, nullptr);
vkDestroySampler(get_device().get_handle(), offscreen.sampler, nullptr);
vkDestroySampler(get_device().get_handle(), filter_pass.sampler, nullptr);
offscreen.depth.destroy(get_device().get_handle());
offscreen.color[0].destroy(get_device().get_handle());
offscreen.color[1].destroy(get_device().get_handle());
filter_pass.color[0].destroy(get_device().get_handle());
vkDestroySampler(get_device().get_handle(), textures.envmap.sampler, nullptr);
}
}
void HDR::request_gpu_features(vkb::PhysicalDevice &gpu)
{
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = VK_TRUE;
}
}
void HDR::build_command_buffers()
{
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VkClearValue clear_values[2];
clear_values[0].color = {{0.0f, 0.0f, 0.0f, 0.0f}};
clear_values[1].depthStencil = {0.0f, 0};
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.renderPass = render_pass;
render_pass_begin_info.renderArea.offset.x = 0;
render_pass_begin_info.renderArea.offset.y = 0;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.pClearValues = clear_values;
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
VK_CHECK(vkBeginCommandBuffer(draw_cmd_buffers[i], &command_buffer_begin_info));
{
/*
First pass: Render scene to offscreen framebuffer
*/
std::array<VkClearValue, 3> clear_values;
clear_values[0].color = {{0.0f, 0.0f, 0.0f, 0.0f}};
clear_values[1].color = {{0.0f, 0.0f, 0.0f, 0.0f}};
clear_values[2].depthStencil = {0.0f, 0};
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.renderPass = offscreen.render_pass;
render_pass_begin_info.framebuffer = offscreen.framebuffer;
render_pass_begin_info.renderArea.extent.width = offscreen.width;
render_pass_begin_info.renderArea.extent.height = offscreen.height;
render_pass_begin_info.clearValueCount = 3;
render_pass_begin_info.pClearValues = clear_values.data();
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(offscreen.width), static_cast<float>(offscreen.height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(offscreen.width, offscreen.height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
VkDeviceSize offsets[1] = {0};
// Skybox
if (display_skybox)
{
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.skybox);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layouts.models, 0, 1, &descriptor_sets.skybox, 0, NULL);
draw_model(models.skybox, draw_cmd_buffers[i]);
}
// 3D object
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.reflect);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layouts.models, 0, 1, &descriptor_sets.object, 0, NULL);
draw_model(models.objects[models.object_index], draw_cmd_buffers[i]);
vkCmdEndRenderPass(draw_cmd_buffers[i]);
}
/*
Second render pass: First bloom pass
*/
if (bloom)
{
VkClearValue clear_values[2];
clear_values[0].color = {{0.0f, 0.0f, 0.0f, 0.0f}};
clear_values[1].depthStencil = {0.0f, 0};
// Bloom filter
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.framebuffer = filter_pass.framebuffer;
render_pass_begin_info.renderPass = filter_pass.render_pass;
render_pass_begin_info.clearValueCount = 1;
render_pass_begin_info.renderArea.extent.width = filter_pass.width;
render_pass_begin_info.renderArea.extent.height = filter_pass.height;
render_pass_begin_info.pClearValues = clear_values;
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(filter_pass.width), static_cast<float>(filter_pass.height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(filter_pass.width, filter_pass.height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layouts.bloom_filter, 0, 1, &descriptor_sets.bloom_filter, 0, NULL);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.bloom[1]);
vkCmdDraw(draw_cmd_buffers[i], 3, 1, 0, 0);
vkCmdEndRenderPass(draw_cmd_buffers[i]);
}
/*
Note: Explicit synchronization is not required between the render pass, as this is done implicit via sub pass dependencies
*/
/*
Third render pass: Scene rendering with applied second bloom pass (when enabled)
*/
{
VkClearValue clear_values[2];
clear_values[0].color = {{0.0f, 0.0f, 0.0f, 0.0f}};
clear_values[1].depthStencil = {0.0f, 0};
// Final composition
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.framebuffer = framebuffers[i];
render_pass_begin_info.renderPass = render_pass;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.renderArea.extent.width = width;
render_pass_begin_info.renderArea.extent.height = height;
render_pass_begin_info.pClearValues = clear_values;
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(width, height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layouts.composition, 0, 1, &descriptor_sets.composition, 0, NULL);
// Scene
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.composition);
vkCmdDraw(draw_cmd_buffers[i], 3, 1, 0, 0);
// Bloom
if (bloom)
{
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.bloom[0]);
vkCmdDraw(draw_cmd_buffers[i], 3, 1, 0, 0);
}
draw_ui(draw_cmd_buffers[i]);
vkCmdEndRenderPass(draw_cmd_buffers[i]);
}
VK_CHECK(vkEndCommandBuffer(draw_cmd_buffers[i]));
}
}
void HDR::create_attachment(VkFormat format, VkImageUsageFlagBits usage, FrameBufferAttachment *attachment)
{
VkImageAspectFlags aspect_mask = 0;
VkImageLayout image_layout;
attachment->format = format;
if (usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
{
aspect_mask = VK_IMAGE_ASPECT_COLOR_BIT;
image_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
}
if (usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)
{
aspect_mask = VK_IMAGE_ASPECT_DEPTH_BIT;
// Stencil aspect should only be set on depth + stencil formats (VK_FORMAT_D16_UNORM_S8_UINT..VK_FORMAT_D32_SFLOAT_S8_UINT
if (format >= VK_FORMAT_D16_UNORM_S8_UINT)
{
aspect_mask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
image_layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
}
assert(aspect_mask > 0);
VkImageCreateInfo image = vkb::initializers::image_create_info();
image.imageType = VK_IMAGE_TYPE_2D;
image.format = format;
image.extent.width = offscreen.width;
image.extent.height = offscreen.height;
image.extent.depth = 1;
image.mipLevels = 1;
image.arrayLayers = 1;
image.samples = VK_SAMPLE_COUNT_1_BIT;
image.tiling = VK_IMAGE_TILING_OPTIMAL;
image.usage = usage | VK_IMAGE_USAGE_SAMPLED_BIT;
VkMemoryAllocateInfo memory_allocate_info = vkb::initializers::memory_allocate_info();
VkMemoryRequirements memory_requirements;
VK_CHECK(vkCreateImage(get_device().get_handle(), &image, nullptr, &attachment->image));
vkGetImageMemoryRequirements(get_device().get_handle(), attachment->image, &memory_requirements);
memory_allocate_info.allocationSize = memory_requirements.size;
memory_allocate_info.memoryTypeIndex = get_device().get_gpu().get_memory_type(memory_requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_CHECK(vkAllocateMemory(get_device().get_handle(), &memory_allocate_info, nullptr, &attachment->mem));
VK_CHECK(vkBindImageMemory(get_device().get_handle(), attachment->image, attachment->mem, 0));
VkImageViewCreateInfo image_view_create_info = vkb::initializers::image_view_create_info();
image_view_create_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
image_view_create_info.format = format;
image_view_create_info.subresourceRange = {};
image_view_create_info.subresourceRange.aspectMask = aspect_mask;
image_view_create_info.subresourceRange.baseMipLevel = 0;
image_view_create_info.subresourceRange.levelCount = 1;
image_view_create_info.subresourceRange.baseArrayLayer = 0;
image_view_create_info.subresourceRange.layerCount = 1;
image_view_create_info.image = attachment->image;
VK_CHECK(vkCreateImageView(get_device().get_handle(), &image_view_create_info, nullptr, &attachment->view));
}
// Prepare a new framebuffer and attachments for offscreen rendering (G-Buffer)
void HDR::prepare_offscreen_buffer()
{
// We need to select a format that supports the color attachment blending flag, so we iterate over multiple formats to find one that supports this flag
const std::vector<VkFormat> float_format_priority_list = {
VK_FORMAT_R32G32B32A32_SFLOAT,
VK_FORMAT_R16G16B16A16_SFLOAT // Guaranteed blend support for this
};
VkFormat color_format = vkb::choose_blendable_format(get_device().get_gpu().get_handle(), float_format_priority_list);
{
offscreen.width = width;
offscreen.height = height;
// Color attachments
// We are using two 128-Bit RGBA floating point color buffers for this sample
// In a performance or bandwidth-limited scenario you should consider using a format with lower precision
create_attachment(color_format, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, &offscreen.color[0]);
create_attachment(color_format, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, &offscreen.color[1]);
// Depth attachment
create_attachment(depth_format, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, &offscreen.depth);
// Set up separate renderpass with references to the color and depth attachments
std::array<VkAttachmentDescription, 3> attachment_descriptions = {};
// Init attachment properties
for (uint32_t i = 0; i < 3; ++i)
{
attachment_descriptions[i].samples = VK_SAMPLE_COUNT_1_BIT;
attachment_descriptions[i].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachment_descriptions[i].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment_descriptions[i].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment_descriptions[i].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
if (i == 2)
{
attachment_descriptions[i].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachment_descriptions[i].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
}
else
{
attachment_descriptions[i].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachment_descriptions[i].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}
}
// Formats
attachment_descriptions[0].format = offscreen.color[0].format;
attachment_descriptions[1].format = offscreen.color[1].format;
attachment_descriptions[2].format = offscreen.depth.format;
std::vector<VkAttachmentReference> color_references;
color_references.push_back({0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL});
color_references.push_back({1, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL});
VkAttachmentReference depth_reference = {};
depth_reference.attachment = 2;
depth_reference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.pColorAttachments = color_references.data();
subpass.colorAttachmentCount = 2;
subpass.pDepthStencilAttachment = &depth_reference;
// Use subpass dependencies for attachment layout transitions
std::array<VkSubpassDependency, 2> dependencies;
dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
dependencies[0].dstSubpass = 0;
dependencies[0].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
// End of previous commands
dependencies[0].srcStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
dependencies[0].srcAccessMask = 0;
// Read/write from/to depth
dependencies[0].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependencies[0].dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
// Write to attachment
dependencies[0].dstStageMask |= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependencies[0].dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
dependencies[1].srcSubpass = 0;
dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
dependencies[1].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
// End of write to attachment
dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
// Attachment later read using sampler in 'composition' pipeline
dependencies[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
dependencies[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
VkRenderPassCreateInfo render_pass_create_info = {};
render_pass_create_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
render_pass_create_info.pAttachments = attachment_descriptions.data();
render_pass_create_info.attachmentCount = static_cast<uint32_t>(attachment_descriptions.size());
render_pass_create_info.subpassCount = 1;
render_pass_create_info.pSubpasses = &subpass;
render_pass_create_info.dependencyCount = 2;
render_pass_create_info.pDependencies = dependencies.data();
VK_CHECK(vkCreateRenderPass(get_device().get_handle(), &render_pass_create_info, nullptr, &offscreen.render_pass));
std::array<VkImageView, 3> attachments;
attachments[0] = offscreen.color[0].view;
attachments[1] = offscreen.color[1].view;
attachments[2] = offscreen.depth.view;
VkFramebufferCreateInfo framebuffer_create_info = {};
framebuffer_create_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebuffer_create_info.pNext = NULL;
framebuffer_create_info.renderPass = offscreen.render_pass;
framebuffer_create_info.pAttachments = attachments.data();
framebuffer_create_info.attachmentCount = static_cast<uint32_t>(attachments.size());
framebuffer_create_info.width = offscreen.width;
framebuffer_create_info.height = offscreen.height;
framebuffer_create_info.layers = 1;
VK_CHECK(vkCreateFramebuffer(get_device().get_handle(), &framebuffer_create_info, nullptr, &offscreen.framebuffer));
// Calculate valid filter and mipmap modes
VkFilter filter = VK_FILTER_NEAREST;
VkSamplerMipmapMode mipmap_mode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
vkb::make_filters_valid(get_device().get_gpu().get_handle(), color_format, &filter, &mipmap_mode);
// Create sampler to sample from the color attachments
VkSamplerCreateInfo sampler = vkb::initializers::sampler_create_info();
sampler.magFilter = filter;
sampler.minFilter = filter;
sampler.mipmapMode = mipmap_mode;
sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler.addressModeV = sampler.addressModeU;
sampler.addressModeW = sampler.addressModeU;
sampler.mipLodBias = 0.0f;
sampler.maxAnisotropy = 1.0f;
sampler.minLod = 0.0f;
sampler.maxLod = 1.0f;
sampler.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
VK_CHECK(vkCreateSampler(get_device().get_handle(), &sampler, nullptr, &offscreen.sampler));
}
// Bloom separable filter pass
{
filter_pass.width = width;
filter_pass.height = height;
// Floating point color attachment
create_attachment(color_format, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, &filter_pass.color[0]);
// Set up separate renderpass with references to the color and depth attachments
std::array<VkAttachmentDescription, 1> attachment_descriptions = {};
// Init attachment properties
attachment_descriptions[0].samples = VK_SAMPLE_COUNT_1_BIT;
attachment_descriptions[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachment_descriptions[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment_descriptions[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment_descriptions[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment_descriptions[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachment_descriptions[0].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
attachment_descriptions[0].format = filter_pass.color[0].format;
std::vector<VkAttachmentReference> color_references;
color_references.push_back({0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL});
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.pColorAttachments = color_references.data();
subpass.colorAttachmentCount = 1;
// Use subpass dependencies for attachment layout transitions
std::array<VkSubpassDependency, 2> dependencies;
dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
dependencies[0].dstSubpass = 0;
dependencies[0].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
// End of previous commands
dependencies[0].srcStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
dependencies[0].srcAccessMask = 0;
// Read from image in fragment shader
dependencies[0].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
dependencies[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
// Write to attachment
dependencies[0].dstStageMask |= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependencies[0].dstAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
dependencies[1].srcSubpass = 0;
dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
dependencies[1].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
// End of write to attachment
dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
// Attachment later read using sampler in 'bloom[0]' pipeline
dependencies[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
dependencies[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
VkRenderPassCreateInfo render_pass_create_info = {};
render_pass_create_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
render_pass_create_info.pAttachments = attachment_descriptions.data();
render_pass_create_info.attachmentCount = static_cast<uint32_t>(attachment_descriptions.size());
render_pass_create_info.subpassCount = 1;
render_pass_create_info.pSubpasses = &subpass;
render_pass_create_info.dependencyCount = 2;
render_pass_create_info.pDependencies = dependencies.data();
VK_CHECK(vkCreateRenderPass(get_device().get_handle(), &render_pass_create_info, nullptr, &filter_pass.render_pass));
std::array<VkImageView, 1> attachments;
attachments[0] = filter_pass.color[0].view;
VkFramebufferCreateInfo framebuffer_create_info = {};
framebuffer_create_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebuffer_create_info.pNext = NULL;
framebuffer_create_info.renderPass = filter_pass.render_pass;
framebuffer_create_info.pAttachments = attachments.data();
framebuffer_create_info.attachmentCount = static_cast<uint32_t>(attachments.size());
framebuffer_create_info.width = filter_pass.width;
framebuffer_create_info.height = filter_pass.height;
framebuffer_create_info.layers = 1;
VK_CHECK(vkCreateFramebuffer(get_device().get_handle(), &framebuffer_create_info, nullptr, &filter_pass.framebuffer));
// Calculate valid filter and mipmap modes
VkFilter filter = VK_FILTER_NEAREST;
VkSamplerMipmapMode mipmap_mode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
vkb::make_filters_valid(get_device().get_gpu().get_handle(), color_format, &filter, &mipmap_mode);
// Create sampler to sample from the color attachments
VkSamplerCreateInfo sampler = vkb::initializers::sampler_create_info();
sampler.magFilter = filter;
sampler.minFilter = filter;
sampler.mipmapMode = mipmap_mode;
sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler.addressModeV = sampler.addressModeU;
sampler.addressModeW = sampler.addressModeU;
sampler.mipLodBias = 0.0f;
sampler.maxAnisotropy = 1.0f;
sampler.minLod = 0.0f;
sampler.maxLod = 1.0f;
sampler.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
VK_CHECK(vkCreateSampler(get_device().get_handle(), &sampler, nullptr, &filter_pass.sampler));
}
}
void HDR::load_assets()
{
// Models
models.skybox = load_model("scenes/cube.gltf");
std::vector<std::string> filenames = {"geosphere.gltf", "teapot.gltf", "torusknot.gltf"};
object_names = {"Sphere", "Teapot", "Torusknot"};
for (auto file : filenames)
{
auto object = load_model("scenes/" + file);
models.objects.emplace_back(std::move(object));
}
// Transforms
auto geosphere_matrix = glm::mat4(1.0f);
auto teapot_matrix = glm::mat4(1.0f);
teapot_matrix = glm::scale(teapot_matrix, glm::vec3(10.0f, 10.0f, 10.0f));
teapot_matrix = glm::rotate(teapot_matrix, glm::radians(180.0f), glm::vec3(1.0f, 0.0f, 0.0f));
auto torus_matrix = glm::mat4(1.0f);
models.transforms.push_back(geosphere_matrix);
models.transforms.push_back(teapot_matrix);
models.transforms.push_back(torus_matrix);
// Load HDR cube map
textures.envmap = load_texture_cubemap("textures/uffizi_rgba16f_cube.ktx", vkb::sg::Image::Color);
}
void HDR::setup_descriptor_pool()
{
std::vector<VkDescriptorPoolSize> pool_sizes = {
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6)};
uint32_t num_descriptor_sets = 4;
VkDescriptorPoolCreateInfo descriptor_pool_create_info =
vkb::initializers::descriptor_pool_create_info(static_cast<uint32_t>(pool_sizes.size()), pool_sizes.data(), num_descriptor_sets);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &descriptor_pool_create_info, nullptr, &descriptor_pool));
}
void HDR::setup_descriptor_set_layout()
{
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings = {
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT, 2),
};
VkDescriptorSetLayoutCreateInfo descriptor_layout_create_info =
vkb::initializers::descriptor_set_layout_create_info(set_layout_bindings.data(), static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout_create_info, nullptr, &descriptor_set_layouts.models));
VkPipelineLayoutCreateInfo pipeline_layout_create_info =
vkb::initializers::pipeline_layout_create_info(
&descriptor_set_layouts.models,
1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &pipeline_layouts.models));
// Bloom filter
set_layout_bindings = {
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 0),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1),
};
descriptor_layout_create_info = vkb::initializers::descriptor_set_layout_create_info(set_layout_bindings.data(), static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout_create_info, nullptr, &descriptor_set_layouts.bloom_filter));
pipeline_layout_create_info = vkb::initializers::pipeline_layout_create_info(&descriptor_set_layouts.bloom_filter, 1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &pipeline_layouts.bloom_filter));
// G-Buffer composition
set_layout_bindings = {
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 0),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1),
};
descriptor_layout_create_info = vkb::initializers::descriptor_set_layout_create_info(set_layout_bindings.data(), static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout_create_info, nullptr, &descriptor_set_layouts.composition));
pipeline_layout_create_info = vkb::initializers::pipeline_layout_create_info(&descriptor_set_layouts.composition, 1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &pipeline_layouts.composition));
}
void HDR::setup_descriptor_sets()
{
VkDescriptorSetAllocateInfo alloc_info =
vkb::initializers::descriptor_set_allocate_info(
descriptor_pool,
&descriptor_set_layouts.models,
1);
// 3D object descriptor set
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &descriptor_sets.object));
VkDescriptorBufferInfo matrix_buffer_descriptor = create_descriptor(*uniform_buffers.matrices);
VkDescriptorImageInfo environment_image_descriptor = create_descriptor(textures.envmap);
VkDescriptorBufferInfo params_buffer_descriptor = create_descriptor(*uniform_buffers.params);
std::vector<VkWriteDescriptorSet> write_descriptor_sets = {
vkb::initializers::write_descriptor_set(descriptor_sets.object, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &matrix_buffer_descriptor),
vkb::initializers::write_descriptor_set(descriptor_sets.object, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &environment_image_descriptor),
vkb::initializers::write_descriptor_set(descriptor_sets.object, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2, &params_buffer_descriptor),
};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
// Sky box descriptor set
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &descriptor_sets.skybox));
matrix_buffer_descriptor = create_descriptor(*uniform_buffers.matrices);
environment_image_descriptor = create_descriptor(textures.envmap);
params_buffer_descriptor = create_descriptor(*uniform_buffers.params);
write_descriptor_sets = {
vkb::initializers::write_descriptor_set(descriptor_sets.skybox, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &matrix_buffer_descriptor),
vkb::initializers::write_descriptor_set(descriptor_sets.skybox, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &environment_image_descriptor),
vkb::initializers::write_descriptor_set(descriptor_sets.skybox, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2, &params_buffer_descriptor),
};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
// Bloom filter
alloc_info = vkb::initializers::descriptor_set_allocate_info(descriptor_pool, &descriptor_set_layouts.bloom_filter, 1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &descriptor_sets.bloom_filter));
std::vector<VkDescriptorImageInfo> color_descriptors = {
vkb::initializers::descriptor_image_info(offscreen.sampler, offscreen.color[0].view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL),
vkb::initializers::descriptor_image_info(offscreen.sampler, offscreen.color[1].view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL),
};
write_descriptor_sets = {
vkb::initializers::write_descriptor_set(descriptor_sets.bloom_filter, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 0, &color_descriptors[0]),
vkb::initializers::write_descriptor_set(descriptor_sets.bloom_filter, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &color_descriptors[1]),
};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
// Composition descriptor set
alloc_info = vkb::initializers::descriptor_set_allocate_info(descriptor_pool, &descriptor_set_layouts.composition, 1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &descriptor_sets.composition));
color_descriptors = {
vkb::initializers::descriptor_image_info(offscreen.sampler, offscreen.color[0].view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL),
vkb::initializers::descriptor_image_info(offscreen.sampler, filter_pass.color[0].view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL),
};
write_descriptor_sets = {
vkb::initializers::write_descriptor_set(descriptor_sets.composition, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 0, &color_descriptors[0]),
vkb::initializers::write_descriptor_set(descriptor_sets.composition, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &color_descriptors[1]),
};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
}
void HDR::prepare_pipelines()
{
VkPipelineInputAssemblyStateCreateInfo input_assembly_state =
vkb::initializers::pipeline_input_assembly_state_create_info(
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
0,
VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterization_state =
vkb::initializers::pipeline_rasterization_state_create_info(
VK_POLYGON_MODE_FILL,
VK_CULL_MODE_BACK_BIT,
VK_FRONT_FACE_COUNTER_CLOCKWISE,
0);
VkPipelineColorBlendAttachmentState blend_attachment_state =
vkb::initializers::pipeline_color_blend_attachment_state(
0xf,
VK_FALSE);
VkPipelineColorBlendStateCreateInfo color_blend_state =
vkb::initializers::pipeline_color_blend_state_create_info(
1,
&blend_attachment_state);
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
VkPipelineDepthStencilStateCreateInfo depth_stencil_state =
vkb::initializers::pipeline_depth_stencil_state_create_info(
VK_FALSE,
VK_FALSE,
VK_COMPARE_OP_GREATER);
VkPipelineViewportStateCreateInfo viewport_state =
vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0);
VkPipelineMultisampleStateCreateInfo multisample_state =
vkb::initializers::pipeline_multisample_state_create_info(
VK_SAMPLE_COUNT_1_BIT,
0);
std::vector<VkDynamicState> dynamic_state_enables = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic_state =
vkb::initializers::pipeline_dynamic_state_create_info(
dynamic_state_enables.data(),
static_cast<uint32_t>(dynamic_state_enables.size()),
0);
VkGraphicsPipelineCreateInfo pipeline_create_info =
vkb::initializers::pipeline_create_info(
pipeline_layouts.models,
render_pass,
0);
std::vector<VkPipelineColorBlendAttachmentState> blend_attachment_states = {
vkb::initializers::pipeline_color_blend_attachment_state(0xf, VK_FALSE),
vkb::initializers::pipeline_color_blend_attachment_state(0xf, VK_FALSE),
};
pipeline_create_info.pInputAssemblyState = &input_assembly_state;
pipeline_create_info.pRasterizationState = &rasterization_state;
pipeline_create_info.pColorBlendState = &color_blend_state;
pipeline_create_info.pMultisampleState = &multisample_state;
pipeline_create_info.pViewportState = &viewport_state;
pipeline_create_info.pDepthStencilState = &depth_stencil_state;
pipeline_create_info.pDynamicState = &dynamic_state;
std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages;
pipeline_create_info.stageCount = static_cast<uint32_t>(shader_stages.size());
pipeline_create_info.pStages = shader_stages.data();
VkSpecializationInfo specialization_info;
std::array<VkSpecializationMapEntry, 1> specialization_map_entries;
// Full screen pipelines
// Empty vertex input state, full screen triangles are generated by the vertex shader
VkPipelineVertexInputStateCreateInfo empty_input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
pipeline_create_info.pVertexInputState = &empty_input_state;
// Final fullscreen composition pass pipeline
shader_stages[0] = load_shader("hdr", "composition.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("hdr", "composition.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
pipeline_create_info.layout = pipeline_layouts.composition;
pipeline_create_info.renderPass = render_pass;
rasterization_state.cullMode = VK_CULL_MODE_FRONT_BIT;
color_blend_state.attachmentCount = 1;
color_blend_state.pAttachments = blend_attachment_states.data();
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.composition));
// Bloom pass
shader_stages[0] = load_shader("hdr", "bloom.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("hdr", "bloom.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
color_blend_state.pAttachments = &blend_attachment_state;
blend_attachment_state.colorWriteMask = 0xF;
blend_attachment_state.blendEnable = VK_TRUE;
blend_attachment_state.colorBlendOp = VK_BLEND_OP_ADD;
blend_attachment_state.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
blend_attachment_state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
blend_attachment_state.alphaBlendOp = VK_BLEND_OP_ADD;
blend_attachment_state.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
blend_attachment_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_DST_ALPHA;
// Set constant parameters via specialization constants
specialization_map_entries[0] = vkb::initializers::specialization_map_entry(0, 0, sizeof(uint32_t));
uint32_t dir = 1;
specialization_info = vkb::initializers::specialization_info(1, specialization_map_entries.data(), sizeof(dir), &dir);
shader_stages[1].pSpecializationInfo = &specialization_info;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.bloom[0]));
// Second blur pass (into separate framebuffer)
pipeline_create_info.renderPass = filter_pass.render_pass;
dir = 0;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.bloom[1]));
// Object rendering pipelines
rasterization_state.cullMode = VK_CULL_MODE_BACK_BIT;
// Vertex bindings an attributes for model rendering
// Binding description
std::vector<VkVertexInputBindingDescription> vertex_input_bindings = {
vkb::initializers::vertex_input_binding_description(0, sizeof(Vertex), VK_VERTEX_INPUT_RATE_VERTEX),
};
// Attribute descriptions
std::vector<VkVertexInputAttributeDescription> vertex_input_attributes = {
vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0), // Position
vkb::initializers::vertex_input_attribute_description(0, 1, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3) // Normal
};
VkPipelineVertexInputStateCreateInfo vertex_input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
vertex_input_state.vertexBindingDescriptionCount = static_cast<uint32_t>(vertex_input_bindings.size());
vertex_input_state.pVertexBindingDescriptions = vertex_input_bindings.data();
vertex_input_state.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size());
vertex_input_state.pVertexAttributeDescriptions = vertex_input_attributes.data();
pipeline_create_info.pVertexInputState = &vertex_input_state;
// Skybox pipeline (background cube)
blend_attachment_state.blendEnable = VK_FALSE;
pipeline_create_info.layout = pipeline_layouts.models;
pipeline_create_info.renderPass = offscreen.render_pass;
color_blend_state.attachmentCount = 2;
color_blend_state.pAttachments = blend_attachment_states.data();
shader_stages[0] = load_shader("hdr", "gbuffer.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("hdr", "gbuffer.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
// Set constant parameters via specialization constants
specialization_map_entries[0] = vkb::initializers::specialization_map_entry(0, 0, sizeof(uint32_t));
uint32_t shadertype = 0;
specialization_info = vkb::initializers::specialization_info(1, specialization_map_entries.data(), sizeof(shadertype), &shadertype);
shader_stages[0].pSpecializationInfo = &specialization_info;
shader_stages[1].pSpecializationInfo = &specialization_info;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.skybox));
// Object rendering pipeline
shadertype = 1;
// Enable depth test and write
depth_stencil_state.depthWriteEnable = VK_TRUE;
depth_stencil_state.depthTestEnable = VK_TRUE;
// Flip cull mode
rasterization_state.cullMode = VK_CULL_MODE_FRONT_BIT;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.reflect));
}
// Prepare and initialize uniform buffer containing shader uniforms
void HDR::prepare_uniform_buffers()
{
// Matrices vertex shader uniform buffer
uniform_buffers.matrices = std::make_unique<vkb::core::BufferC>(get_device(),
sizeof(ubo_vs),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
// Params
uniform_buffers.params = std::make_unique<vkb::core::BufferC>(get_device(),
sizeof(ubo_params),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
update_params();
}
void HDR::update_uniform_buffers()
{
ubo_vs.projection = camera.matrices.perspective;
ubo_vs.modelview = camera.matrices.view * models.transforms[models.object_index];
ubo_vs.skybox_modelview = camera.matrices.view;
ubo_vs.inverse_modelview = glm::inverse(camera.matrices.view);
uniform_buffers.matrices->convert_and_update(ubo_vs);
}
void HDR::update_params()
{
uniform_buffers.params->convert_and_update(ubo_params);
}
void HDR::draw()
{
ApiVulkanSample::prepare_frame();
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &draw_cmd_buffers[current_buffer];
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE));
ApiVulkanSample::submit_frame();
}
bool HDR::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
camera.type = vkb::CameraType::LookAt;
camera.set_position(glm::vec3(0.0f, 0.0f, -4.0f));
camera.set_rotation(glm::vec3(0.0f, 180.0f, 0.0f));
// Note: Using reversed depth-buffer for increased precision, so Znear and Zfar are flipped
camera.set_perspective(60.0f, static_cast<float>(width) / static_cast<float>(height), 256.0f, 0.1f);
load_assets();
prepare_uniform_buffers();
prepare_offscreen_buffer();
setup_descriptor_set_layout();
prepare_pipelines();
setup_descriptor_pool();
setup_descriptor_sets();
build_command_buffers();
prepared = true;
return true;
}
void HDR::render(float delta_time)
{
if (!prepared)
{
return;
}
draw();
if (camera.updated)
{
update_uniform_buffers();
}
}
void HDR::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Settings"))
{
if (drawer.combo_box("Object type", &models.object_index, object_names))
{
update_uniform_buffers();
rebuild_command_buffers();
}
if (drawer.input_float("Exposure", &ubo_params.exposure, 0.025f, "%.3f"))
{
update_params();
}
if (drawer.checkbox("Bloom", &bloom))
{
rebuild_command_buffers();
}
if (drawer.checkbox("Skybox", &display_skybox))
{
rebuild_command_buffers();
}
}
}
bool HDR::resize(const uint32_t width, const uint32_t height)
{
ApiVulkanSample::resize(width, height);
update_uniform_buffers();
return true;
}
std::unique_ptr<vkb::Application> create_hdr()
{
return std::make_unique<HDR>();
}
-151
View File
@@ -1,151 +0,0 @@
/* Copyright (c) 2019-2024, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* High dynamic range rendering
*/
#pragma once
#include "api_vulkan_sample.h"
class HDR : public ApiVulkanSample
{
public:
bool bloom = true;
bool display_skybox = true;
struct
{
Texture envmap;
} textures;
struct Models
{
std::unique_ptr<vkb::sg::SubMesh> skybox;
std::vector<std::unique_ptr<vkb::sg::SubMesh>> objects;
std::vector<glm::mat4> transforms;
int32_t object_index = 0;
} models;
struct
{
std::unique_ptr<vkb::core::BufferC> matrices;
std::unique_ptr<vkb::core::BufferC> params;
} uniform_buffers;
struct UBOVS
{
glm::mat4 projection;
glm::mat4 modelview;
glm::mat4 skybox_modelview;
glm::mat4 inverse_modelview;
float modelscale = 0.05f;
} ubo_vs;
struct UBOParams
{
float exposure = 1.0f;
} ubo_params;
struct
{
VkPipeline skybox;
VkPipeline reflect;
VkPipeline composition;
VkPipeline bloom[2];
} pipelines;
struct
{
VkPipelineLayout models;
VkPipelineLayout composition;
VkPipelineLayout bloom_filter;
} pipeline_layouts;
struct
{
VkDescriptorSet object;
VkDescriptorSet skybox;
VkDescriptorSet composition;
VkDescriptorSet bloom_filter;
} descriptor_sets;
struct
{
VkDescriptorSetLayout models;
VkDescriptorSetLayout composition;
VkDescriptorSetLayout bloom_filter;
} descriptor_set_layouts;
// Framebuffer for offscreen rendering
struct FrameBufferAttachment
{
VkImage image;
VkDeviceMemory mem;
VkImageView view;
VkFormat format;
void destroy(VkDevice device)
{
vkDestroyImageView(device, view, nullptr);
vkDestroyImage(device, image, nullptr);
vkFreeMemory(device, mem, nullptr);
}
};
struct FrameBuffer
{
int32_t width, height;
VkFramebuffer framebuffer;
FrameBufferAttachment color[2];
FrameBufferAttachment depth;
VkRenderPass render_pass;
VkSampler sampler;
} offscreen;
struct
{
int32_t width, height;
VkFramebuffer framebuffer;
FrameBufferAttachment color[1];
VkRenderPass render_pass;
VkSampler sampler;
} filter_pass;
std::vector<std::string> object_names;
HDR();
~HDR();
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
void build_command_buffers() override;
void create_attachment(VkFormat format, VkImageUsageFlagBits usage, FrameBufferAttachment *attachment);
void prepare_offscreen_buffer();
void load_assets();
void setup_descriptor_pool();
void setup_descriptor_set_layout();
void setup_descriptor_sets();
void prepare_pipelines();
void prepare_uniform_buffers();
void update_uniform_buffers();
void update_params();
void draw();
bool prepare(const vkb::ApplicationOptions &options) override;
virtual void render(float delta_time) override;
virtual void on_update_ui_overlay(vkb::Drawer &drawer) override;
virtual bool resize(const uint32_t width, const uint32_t height) override;
};
std::unique_ptr<vkb::Application> create_hdr();
-37
View File
@@ -1,37 +0,0 @@
# Copyright (c) 2019-2025, Arm Limited and Contributors
# Copyright (c) 2025, Sascha Willems
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 the "License";
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
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 "Hello Triangle"
DESCRIPTION "An introduction into Vulkan and its respective objects."
SHADER_FILES_GLSL
"hello_triangle/glsl/triangle.vert"
"hello_triangle/glsl/triangle.frag"
SHADER_FILES_HLSL
"hello_triangle/hlsl/triangle.vert.hlsl"
"hello_triangle/hlsl/triangle.frag.hlsl"
SHADER_FILES_SLANG
"hello_triangle/slang/triangle.vert.slang"
"hello_triangle/slang/triangle.frag.slang")
-27
View File
@@ -1,27 +0,0 @@
////
- Copyright (c) 2019-2023, 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.
-
////
= Hello Triangle
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hello_triangle[Khronos Vulkan samples github repository].
endif::[]
A self-contained (minimal use of framework) sample that illustrates the rendering of a triangle.
File diff suppressed because it is too large Load Diff
-179
View File
@@ -1,179 +0,0 @@
/* Copyright (c) 2018-2025, Arm Limited and Contributors
* Copyright (c) 2025, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "common/vk_common.h"
#include "core/instance.h"
#include "platform/application.h"
/**
* @brief A self-contained (minimal use of framework) sample that illustrates
* the rendering of a triangle
*/
class HelloTriangle : public vkb::Application
{
/**
* @brief Swapchain state
*/
struct SwapchainDimensions
{
/// Width of the swapchain.
uint32_t width = 0;
/// Height of the swapchain.
uint32_t height = 0;
/// Pixel format of the swapchain.
VkFormat format = VK_FORMAT_UNDEFINED;
};
/**
* @brief Per-frame data
*/
struct PerFrame
{
VkFence queue_submit_fence = VK_NULL_HANDLE;
VkCommandPool primary_command_pool = VK_NULL_HANDLE;
VkCommandBuffer primary_command_buffer = VK_NULL_HANDLE;
VkSemaphore swapchain_acquire_semaphore = VK_NULL_HANDLE;
VkSemaphore swapchain_release_semaphore = VK_NULL_HANDLE;
};
/**
* @brief Vulkan objects and global state
*/
struct Context
{
/// The Vulkan instance.
VkInstance instance = VK_NULL_HANDLE;
/// The Vulkan physical device.
VkPhysicalDevice gpu = VK_NULL_HANDLE;
/// The Vulkan device.
VkDevice device = VK_NULL_HANDLE;
/// The Vulkan device queue.
VkQueue queue = VK_NULL_HANDLE;
/// The swapchain.
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
/// The swapchain dimensions.
SwapchainDimensions swapchain_dimensions;
/// The surface we will render to.
VkSurfaceKHR surface = VK_NULL_HANDLE;
/// The queue family index where graphics work will be submitted.
int32_t graphics_queue_index = -1;
/// The image view for each swapchain image.
std::vector<VkImageView> swapchain_image_views;
/// The framebuffer for each swapchain image view.
std::vector<VkFramebuffer> swapchain_framebuffers;
/// The renderpass description.
VkRenderPass render_pass = VK_NULL_HANDLE;
/// The graphics pipeline.
VkPipeline pipeline = VK_NULL_HANDLE;
/**
* The pipeline layout for resources.
* Not used in this sample, but we still need to provide a dummy one.
*/
VkPipelineLayout pipeline_layout = VK_NULL_HANDLE;
/// The debug utility callback.
VkDebugUtilsMessengerEXT debug_callback = VK_NULL_HANDLE;
/// A set of semaphores that can be reused.
std::vector<VkSemaphore> recycled_semaphores;
/// A set of per-frame data.
std::vector<PerFrame> per_frame;
VmaAllocator vma_allocator = VK_NULL_HANDLE;
};
/// Properties of the vertices used in this sample.
struct Vertex
{
glm::vec3 position;
glm::vec3 color;
};
/// The Vulkan buffer object that holds the vertex data for the triangle.
VkBuffer vertex_buffer = VK_NULL_HANDLE;
/// The device memory allocated for the vertex buffer.
VkDeviceMemory vertex_buffer_memory = VK_NULL_HANDLE;
/// Vulkan Memory Allocator (VMA) allocation info for the vertex buffer.
VmaAllocation vertex_buffer_allocation = VK_NULL_HANDLE;
public:
HelloTriangle();
virtual ~HelloTriangle();
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void update(float delta_time) override;
virtual bool resize(const uint32_t width, const uint32_t height) override;
bool validate_extensions(const std::vector<const char *> &required,
const std::vector<VkExtensionProperties> &available);
void init_instance();
void init_device();
void init_vertex_buffer();
void init_per_frame(PerFrame &per_frame);
void teardown_per_frame(PerFrame &per_frame);
void init_swapchain();
void init_render_pass();
VkShaderModule load_shader_module(const std::string &path);
void init_pipeline();
VkResult acquire_next_image(uint32_t *image);
void render_triangle(uint32_t swapchain_index);
VkResult present_image(uint32_t index);
void init_framebuffers();
private:
Context context;
std::unique_ptr<vkb::core::InstanceC> vk_instance;
};
std::unique_ptr<vkb::Application> create_hello_triangle();
@@ -1,37 +0,0 @@
# Copyright (c) 2024-2025, Huawei Technologies Co., Ltd.
#
# 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.
set(CMAKE_CXX_EXTENSIONS OFF)
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 "Huawei Technologies Co., Ltd."
NAME "Vulkan 1.3 Hello Triangle"
DESCRIPTION "An introduction into Vulkan using Vulkan 1.3"
SHADER_FILES_GLSL
"hello_triangle_1_3/glsl/triangle.vert"
"hello_triangle_1_3/glsl/triangle.frag"
SHADER_FILES_HLSL
"hello_triangle_1_3/hlsl/triangle.vert.hlsl"
"hello_triangle_1_3/hlsl/triangle.frag.hlsl"
SHADER_FILES_SLANG
"hello_triangle_1_3/slang/triangle.vert.slang"
"hello_triangle_1_3/slang/triangle.frag.slang")
-213
View File
@@ -1,213 +0,0 @@
////
* Copyright (c) 2024, Huawei Technologies Co., Ltd.
*
* 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.
////
= Hello Triangle with Vulkan 1.3 Features
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hello_triangle_1_3[Khronos Vulkan samples github repository].
endif::[]
This sample demonstrates how to render a simple triangle using Vulkan 1.3 core features. It modernizes the traditional "Hello Triangle" Vulkan sample by incorporating:
- **Dynamic Rendering**
- **Synchronization2**
- **Extended Dynamic State**
- **Vertex Buffers**
## Overview
The sample renders a colored triangle to the screen using Vulkan 1.3. It showcases how to:
- Initialize Vulkan with Vulkan 1.3 features enabled.
- Use dynamic rendering to simplify the rendering pipeline.
- Employ the Synchronization2 API for improved synchronization.
- Utilize extended dynamic states to reduce pipeline complexity.
- Manage vertex data using vertex buffers instead of hard-coded vertices.
## Key Features
### 1. Dynamic Rendering
**What is Dynamic Rendering?**
Dynamic Rendering is a feature introduced in Vulkan 1.3 that allows rendering without pre-defined render passes and framebuffers. It simplifies the rendering process by enabling you to specify rendering states directly during command buffer recording.
**How It's Used in the Sample:**
- **No Render Passes or Framebuffers:** The sample does not create `VkRenderPass` or `VkFramebuffer` objects.
- **`vkCmdBeginRendering()` and `vkCmdEndRendering()`:** These functions are used to begin and end rendering operations dynamically.
- **Pipeline Creation:** Uses `VkPipelineRenderingCreateInfo` during pipeline creation to specify rendering details.
**Benefits:**
- Simplifies code by reducing boilerplate associated with render passes and framebuffers.
- Increases flexibility by allowing rendering to different attachments without recreating render passes.
### 2. Synchronization2
**What is Synchronization2?**
Synchronization2 is an improved synchronization API introduced in Vulkan 1.3. It provides more granular control over synchronization primitives and simplifies the synchronization process.
**How It's Used in the Sample:**
- **`vkCmdPipelineBarrier2()`:** Replaces the older `vkCmdPipelineBarrier()` for more detailed synchronization.
- **`VkDependencyInfo` and `VkImageMemoryBarrier2`:** Used to specify precise memory dependencies and image layout transitions.
**Example Usage:**
```cpp
VkImageMemoryBarrier2 image_barrier = {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
// ... other members ...
};
VkDependencyInfo dependency_info = {
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
.imageMemoryBarrierCount = 1,
.pImageMemoryBarriers = &image_barrier,
};
vkCmdPipelineBarrier2(cmd, &dependency_info);
```
**Benefits:**
- Provides more expressive and flexible synchronization.
- Reduces the potential for synchronization errors.
- Simplifies the specification of pipeline stages and access masks.
### 3. Extended Dynamic State
**What is Extended Dynamic State?**
Extended Dynamic State allows more pipeline states to be set dynamically at command buffer recording time rather than during pipeline creation. This reduces the number of pipeline objects needed.
**How It's Used in the Sample:**
- **Dynamic States Enabled:** The sample enables dynamic states like `VK_DYNAMIC_STATE_CULL_MODE`, `VK_DYNAMIC_STATE_FRONT_FACE`, and `VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY`.
- **Dynamic State Commands:** Uses `vkCmdSetCullMode()`, `vkCmdSetFrontFace()`, and `vkCmdSetPrimitiveTopology()` to set these states dynamically.
**Example Usage:**
```cpp
vkCmdSetCullMode(cmd, VK_CULL_MODE_NONE);
vkCmdSetFrontFace(cmd, VK_FRONT_FACE_CLOCKWISE);
vkCmdSetPrimitiveTopology(cmd, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
```
**Benefits:**
- Reduces the need to create multiple pipelines for different state configurations.
- Enhances flexibility by allowing state changes without pipeline recreation.
### 4. Vertex Buffers
**What Changed?**
Unlike the original sample, which used hard-coded vertices in the shader, this sample uses a vertex buffer to store vertex data.
**How It's Used in the Sample:**
- **Vertex Structure Defined:**
```cpp
struct Vertex {
glm::vec2 position;
glm::vec3 color;
};
```
- **Vertex Data Stored in a Buffer:**
```cpp
std::vector<Vertex> vertices = {
{{0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}}, // Red Vertex
// ... other vertices ...
};
```
- **Buffer Creation and Memory Allocation:**
```cpp
VkBufferCreateInfo buffer_info = { /* ... */ };
vkCreateBuffer(device, &buffer_info, nullptr, &vertex_buffer);
VkMemoryAllocateInfo alloc_info = { /* ... */ };
vkAllocateMemory(device, &alloc_info, nullptr, &vertex_buffer_memory);
```
- **Binding the Vertex Buffer:**
```cpp
vkCmdBindVertexBuffers(cmd, 0, 1, &vertex_buffer, &offset);
```
**Benefits:**
- **Flexibility:** Easier to modify vertex data without changing shaders.
- **Performance:** Potentially better performance due to efficient memory usage.
- **Scalability:** Simplifies rendering more complex geometries.
## How the Sample Works
1. **Initialization:**
- **Instance Creation:** Initializes a Vulkan instance with Vulkan 1.3 API version and required extensions.
- **Device Selection:** Chooses a physical device that supports Vulkan 1.3 and required features.
- **Logical Device Creation:** Creates a logical device with enabled Vulkan 1.3 features like dynamic rendering, synchronization2, and extended dynamic state.
- **Surface and Swapchain Creation:** Sets up the window surface and initializes the swapchain for presenting images.
2. **Vertex Buffer Setup:**
- **Vertex Data Definition:** Defines vertices with positions and colors.
- **Buffer Creation:** Creates a buffer to store vertex data.
- **Memory Allocation:** Allocates memory for the buffer and maps the vertex data into it.
3. **Pipeline Setup:**
- **Shader Modules:** Loads and compiles vertex and fragment shaders.
- **Pipeline Layout:** Creates a pipeline layout (empty in this case as no descriptors are used).
- **Dynamic States Specification:** Specifies which states will be dynamic.
- **Graphics Pipeline Creation:** Creates the graphics pipeline with dynamic rendering info and dynamic states enabled.
4. **Rendering Loop:**
- **Acquire Swapchain Image:** Gets the next available image from the swapchain.
- **Command Buffer Recording:**
- **Begin Rendering:** Uses `vkCmdBeginRendering()` with dynamic rendering info.
- **Set Dynamic States:** Sets viewport, scissor, cull mode, front face, and primitive topology dynamically.
- **Bind Pipeline and Vertex Buffer:** Binds the graphics pipeline and the vertex buffer.
- **Draw Call:** Issues a draw call to render the triangle.
- **End Rendering:** Uses `vkCmdEndRendering()` to finish rendering.
- **Image Layout Transition:** Transitions the swapchain image layout for presentation using `vkCmdPipelineBarrier2()`.
- **Queue Submission:** Submits the command buffer to the graphics queue.
- **Present Image:** Presents the rendered image to the screen.
5. **Cleanup:**
- **Resource Destruction:** Cleans up Vulkan resources like pipelines, buffers, and swapchain images upon application exit.
## Dependencies and Requirements
- **Vulkan SDK 1.3 or Later:** Ensure you have the Vulkan SDK that supports Vulkan 1.3.
- **Hardware Support:** A GPU that supports Vulkan 1.3 features, including dynamic rendering, synchronization2, and extended dynamic state.
- **GLM Library:** Used for vector and matrix operations.
- **Shader Compiler:** GLSL shaders are compiled at runtime using a GLSL compiler.
File diff suppressed because it is too large Load Diff
@@ -1,177 +0,0 @@
/* Copyright (c) 2024-2025, Huawei Technologies Co., Ltd.
*
* 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/vk_common.h"
#include "core/instance.h"
#include "platform/application.h"
/**
* @brief A self-contained (minimal use of framework) sample that illustrates
* the rendering of a triangle
*/
class HelloTriangleV13 : public vkb::Application
{
// Define the Vertex structure
struct Vertex
{
glm::vec2 position;
glm::vec3 color;
};
// Define the vertex data
const std::vector<Vertex> vertices = {
{{0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}}, // Vertex 1: Red
{{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, // Vertex 2: Green
{{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} // Vertex 3: Blue
};
/**
* @brief Swapchain state
*/
struct SwapchainDimensions
{
/// Width of the swapchain.
uint32_t width = 0;
/// Height of the swapchain.
uint32_t height = 0;
/// Pixel format of the swapchain.
VkFormat format = VK_FORMAT_UNDEFINED;
};
/**
* @brief Per-frame data
*/
struct PerFrame
{
VkFence queue_submit_fence = VK_NULL_HANDLE;
VkCommandPool primary_command_pool = VK_NULL_HANDLE;
VkCommandBuffer primary_command_buffer = VK_NULL_HANDLE;
VkSemaphore swapchain_acquire_semaphore = VK_NULL_HANDLE;
VkSemaphore swapchain_release_semaphore = VK_NULL_HANDLE;
};
/**
* @brief Vulkan objects and global state
*/
struct Context
{
/// The Vulkan instance.
VkInstance instance = VK_NULL_HANDLE;
/// The Vulkan physical device.
VkPhysicalDevice gpu = VK_NULL_HANDLE;
/// The Vulkan device.
VkDevice device = VK_NULL_HANDLE;
/// The Vulkan device queue.
VkQueue queue = VK_NULL_HANDLE;
/// The swapchain.
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
/// The swapchain dimensions.
SwapchainDimensions swapchain_dimensions;
/// The surface we will render to.
VkSurfaceKHR surface = VK_NULL_HANDLE;
/// The queue family index where graphics work will be submitted.
int32_t graphics_queue_index = -1;
/// The image view for each swapchain image.
std::vector<VkImageView> swapchain_image_views;
/// The handles to the images in the swapchain.
std::vector<VkImage> swapchain_images;
/// The graphics pipeline.
VkPipeline pipeline = VK_NULL_HANDLE;
/**
* The pipeline layout for resources.
* Not used in this sample, but we still need to provide a dummy one.
*/
VkPipelineLayout pipeline_layout = VK_NULL_HANDLE;
/// The debug utility messenger callback.
VkDebugUtilsMessengerEXT debug_callback = VK_NULL_HANDLE;
/// A set of semaphores that can be reused.
std::vector<VkSemaphore> recycled_semaphores;
/// A set of per-frame data.
std::vector<PerFrame> per_frame;
/// The Vulkan buffer object that holds the vertex data for the triangle.
VkBuffer vertex_buffer = VK_NULL_HANDLE;
/// The device memory allocated for the vertex buffer.
VkDeviceMemory vertex_buffer_memory = VK_NULL_HANDLE;
};
public:
HelloTriangleV13() = default;
virtual ~HelloTriangleV13();
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void update(float delta_time) override;
virtual bool resize(const uint32_t width, const uint32_t height) override;
bool validate_extensions(const std::vector<const char *> &required,
const std::vector<VkExtensionProperties> &available);
void init_instance();
void init_device();
void init_vertex_buffer();
void init_per_frame(PerFrame &per_frame);
void teardown_per_frame(PerFrame &per_frame);
void init_swapchain();
VkShaderModule load_shader_module(const std::string &path, VkShaderStageFlagBits shader_stage);
void init_pipeline();
VkResult acquire_next_swapchain_image(uint32_t *image);
void render_triangle(uint32_t swapchain_index);
VkResult present_image(uint32_t index);
void transition_image_layout(VkCommandBuffer cmd, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkAccessFlags2 srcAccessMask, VkAccessFlags2 dstAccessMask, VkPipelineStageFlags2 srcStage, VkPipelineStageFlags2 dstStage);
uint32_t find_memory_type(VkPhysicalDevice physical_device, uint32_t type_filter, VkMemoryPropertyFlags properties);
private:
Context context;
std::unique_ptr<vkb::core::InstanceC> vk_instance;
};
std::unique_ptr<vkb::Application> create_hello_triangle_1_3();
@@ -1,37 +0,0 @@
# 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.
#
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 "Sascha Willems"
NAME "HPP Compute N-Body simulation"
DESCRIPTION "Multi-pass compute dispatch N-Body particle simulation, using vulkan.hpp"
SHADER_FILES_GLSL
"compute_nbody/glsl/particle.vert"
"compute_nbody/glsl/particle.frag"
"compute_nbody/glsl/particle_calculate.comp"
"compute_nbody/glsl/particle_integrate.comp"
SHADER_FILES_HLSL
"compute_nbody/hlsl/particle.vert.hlsl"
"compute_nbody/hlsl/particle.frag.hlsl"
"compute_nbody/hlsl/particle_calculate.comp.hlsl"
"compute_nbody/hlsl/particle_integrate.comp.hlsl")
-27
View File
@@ -1,27 +0,0 @@
////
- Copyright (c) 2023, 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.
-
////
:pp: {plus}{plus}
= HPP Compute shader N-Body simulation
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hpp_compute_nbody[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/compute_nbody[Compute N-Body] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp.
@@ -1,666 +0,0 @@
/* 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.
*/
/*
* Compute shader N-body simulation using two passes and shared compute shader memory, using vulkan.hpp
*/
#include "hpp_compute_nbody.h"
#include "benchmark_mode/benchmark_mode.h"
#include "core/command_pool.h"
#include <random>
HPPComputeNBody::HPPComputeNBody()
{
title = "Compute shader N-body system";
initializeCamera();
}
HPPComputeNBody::~HPPComputeNBody()
{
if (has_device() && get_device().get_handle())
{
vk::Device device = get_device().get_handle();
compute.destroy(device);
graphics.destroy(device);
textures.destroy(device);
}
}
bool HPPComputeNBody::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
load_assets();
descriptor_pool = create_descriptor_pool();
prepare_graphics();
prepare_compute();
build_command_buffers();
prepared = true;
}
return prepared;
}
bool HPPComputeNBody::resize(const uint32_t width, const uint32_t height)
{
HPPApiVulkanSample::resize(width, height);
update_graphics_uniform_buffers();
return true;
}
void HPPComputeNBody::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = VK_TRUE;
}
}
void HPPComputeNBody::build_command_buffers()
{
std::array<vk::ClearValue, 2> clear_values = {{vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 1.0f}})),
vk::ClearDepthStencilValue{0.0f, 0}}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = render_pass,
.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
// Set target frame buffer
render_pass_begin_info.framebuffer = framebuffers[i];
vk::CommandBuffer command_buffer = draw_cmd_buffers[i];
command_buffer.begin(vk::CommandBufferBeginInfo());
// Acquire
if (graphics.queue_family_index != compute.queue_family_index)
{
vk::BufferMemoryBarrier buffer_barrier{.dstAccessMask = vk::AccessFlagBits::eVertexAttributeRead,
.srcQueueFamilyIndex = compute.queue_family_index,
.dstQueueFamilyIndex = graphics.queue_family_index,
.buffer = compute.storage_buffer->get_handle(),
.size = compute.storage_buffer->get_size()};
command_buffer.pipelineBarrier(
vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eVertexInput, {}, nullptr, buffer_barrier, nullptr);
}
// Draw the particle system using the update vertex buffer
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
command_buffer.setViewport(0, {{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f}});
command_buffer.setScissor(0, {{{0, 0}, extent}});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, graphics.pipeline);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, graphics.pipeline_layout, 0, graphics.descriptor_set, nullptr);
command_buffer.bindVertexBuffers(0, compute.storage_buffer->get_handle(), {0});
command_buffer.draw(compute.ubo.particle_count, 1, 0, 0);
draw_ui(command_buffer);
command_buffer.endRenderPass();
// Release barrier
if (graphics.queue_family_index != compute.queue_family_index)
{
vk::BufferMemoryBarrier buffer_barrier{.srcAccessMask = vk::AccessFlagBits::eVertexAttributeRead,
.srcQueueFamilyIndex = graphics.queue_family_index,
.dstQueueFamilyIndex = compute.queue_family_index,
.buffer = compute.storage_buffer->get_handle(),
.size = compute.storage_buffer->get_size()};
command_buffer.pipelineBarrier(
vk::PipelineStageFlagBits::eVertexInput, vk::PipelineStageFlagBits::eComputeShader, {}, nullptr, buffer_barrier, nullptr);
}
command_buffer.end();
}
}
void HPPComputeNBody::render(float delta_time)
{
if (prepared)
{
draw();
update_compute_uniform_buffers(delta_time);
if (camera.updated)
{
update_graphics_uniform_buffers();
}
}
}
void HPPComputeNBody::build_compute_command_buffer()
{
compute.command_buffer.begin(vk::CommandBufferBeginInfo());
// Acquire
if (graphics.queue_family_index != compute.queue_family_index)
{
vk::BufferMemoryBarrier buffer_barrier{.dstAccessMask = vk::AccessFlagBits::eShaderWrite,
.srcQueueFamilyIndex = graphics.queue_family_index,
.dstQueueFamilyIndex = compute.queue_family_index,
.buffer = compute.storage_buffer->get_handle(),
.size = compute.storage_buffer->get_size()};
compute.command_buffer.pipelineBarrier(
vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eComputeShader, {}, nullptr, buffer_barrier, nullptr);
}
// First pass: Calculate particle movement
// -------------------------------------------------------------------------------------------------------
compute.command_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, compute.pipeline_calculate);
compute.command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, compute.pipeline_layout, 0, compute.descriptor_set, nullptr);
compute.command_buffer.dispatch(compute.ubo.particle_count / compute.work_group_size, 1, 1);
// Add memory barrier to ensure that the computer shader has finished writing to the buffer
vk::BufferMemoryBarrier memory_barrier{.srcAccessMask = vk::AccessFlagBits::eShaderWrite,
.dstAccessMask = vk::AccessFlagBits::eShaderRead,
.srcQueueFamilyIndex = vk::QueueFamilyIgnored,
.dstQueueFamilyIndex = vk::QueueFamilyIgnored,
.buffer = compute.storage_buffer->get_handle(),
.size = compute.storage_buffer->get_size()};
compute.command_buffer.pipelineBarrier(
vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eComputeShader, {}, nullptr, memory_barrier, nullptr);
// Second pass: Integrate particles
// -------------------------------------------------------------------------------------------------------
compute.command_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, compute.pipeline_integrate);
compute.command_buffer.dispatch(compute.ubo.particle_count / compute.work_group_size, 1, 1);
// Release
if (graphics.queue_family_index != compute.queue_family_index)
{
vk::BufferMemoryBarrier buffer_barrier{.srcAccessMask = vk::AccessFlagBits::eShaderWrite,
.srcQueueFamilyIndex = compute.queue_family_index,
.dstQueueFamilyIndex = graphics.queue_family_index,
.buffer = compute.storage_buffer->get_handle(),
.size = compute.storage_buffer->get_size()};
compute.command_buffer.pipelineBarrier(
vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eTransfer, {}, nullptr, buffer_barrier, nullptr);
}
compute.command_buffer.end();
}
void HPPComputeNBody::build_compute_transfer_command_buffer(vk::CommandBuffer command_buffer) const
{
command_buffer.begin(vk::CommandBufferBeginInfo());
vk::BufferMemoryBarrier acquire_buffer_barrier{.dstAccessMask = vk::AccessFlagBits::eShaderWrite,
.srcQueueFamilyIndex = graphics.queue_family_index,
.dstQueueFamilyIndex = compute.queue_family_index,
.buffer = compute.storage_buffer->get_handle(),
.size = compute.storage_buffer->get_size()};
command_buffer.pipelineBarrier(
vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eComputeShader, {}, nullptr, acquire_buffer_barrier, nullptr);
vk::BufferMemoryBarrier release_buffer_barrier{.srcAccessMask = vk::AccessFlagBits::eShaderWrite,
.srcQueueFamilyIndex = compute.queue_family_index,
.dstQueueFamilyIndex = graphics.queue_family_index,
.buffer = compute.storage_buffer->get_handle(),
.size = compute.storage_buffer->get_size()};
command_buffer.pipelineBarrier(
vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eTransfer, {}, nullptr, release_buffer_barrier, nullptr);
// Copied from Device::flush_command_buffer, which we can't use because it would be
// working with the wrong command pool
command_buffer.end();
}
void HPPComputeNBody::build_copy_command_buffer(vk::CommandBuffer command_buffer, vk::Buffer staging_buffer, vk::DeviceSize buffer_size) const
{
command_buffer.begin(vk::CommandBufferBeginInfo());
command_buffer.copyBuffer(staging_buffer, compute.storage_buffer->get_handle(), {{0, 0, buffer_size}});
// Execute a transfer to the compute queue, if necessary
if (graphics.queue_family_index != compute.queue_family_index)
{
vk::BufferMemoryBarrier buffer_barrier{.srcAccessMask = vk::AccessFlagBits::eVertexAttributeRead,
.srcQueueFamilyIndex = graphics.queue_family_index,
.dstQueueFamilyIndex = compute.queue_family_index,
.buffer = compute.storage_buffer->get_handle(),
.size = compute.storage_buffer->get_size()};
command_buffer.pipelineBarrier(vk::PipelineStageFlagBits::eVertexInput, vk::PipelineStageFlagBits::eComputeShader, {}, nullptr, buffer_barrier, nullptr);
}
command_buffer.end();
}
vk::DescriptorSetLayout HPPComputeNBody::create_compute_descriptor_set_layout()
{
std::array<vk::DescriptorSetLayoutBinding, 2> bindings = {{{0, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eCompute},
{1, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eCompute}}};
return get_device().get_handle().createDescriptorSetLayout({.bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data()});
}
vk::Pipeline HPPComputeNBody::create_compute_pipeline(vk::PipelineShaderStageCreateInfo const &stage)
{
vk::ComputePipelineCreateInfo compute_pipeline_create_info{.stage = stage, .layout = compute.pipeline_layout};
vk::Result result;
vk::Pipeline pipeline;
std::tie(result, pipeline) = get_device().get_handle().createComputePipeline(pipeline_cache, compute_pipeline_create_info);
assert(result == vk::Result::eSuccess);
return pipeline;
}
vk::DescriptorPool HPPComputeNBody::create_descriptor_pool()
{
std::array<vk::DescriptorPoolSize, 3> pool_sizes = {{{vk::DescriptorType::eUniformBuffer, 2},
{vk::DescriptorType::eStorageBuffer, 1},
{vk::DescriptorType::eCombinedImageSampler, 2}}};
return get_device().get_handle().createDescriptorPool(
{.maxSets = 2, .poolSizeCount = static_cast<uint32_t>(pool_sizes.size()), .pPoolSizes = pool_sizes.data()});
}
vk::DescriptorSetLayout HPPComputeNBody::create_graphics_descriptor_set_layout()
{
std::array<vk::DescriptorSetLayoutBinding, 3> bindings = {{{0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{2, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex}}};
return get_device().get_handle().createDescriptorSetLayout({.bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data()});
}
vk::Pipeline HPPComputeNBody::create_graphics_pipeline()
{
// Load shaders
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {load_shader("compute_nbody", "particle.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("compute_nbody", "particle.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Vertex bindings and attributes
vk::VertexInputBindingDescription vertex_input_bindings{0, sizeof(Particle), vk::VertexInputRate::eVertex};
std::array<vk::VertexInputAttributeDescription, 2> vertex_input_attributes = {
{{0, 0, vk::Format::eR32G32B32A32Sfloat, offsetof(Particle, pos)}, // Location 0 : Position
{1, 0, vk::Format::eR32G32B32A32Sfloat, offsetof(Particle, vel)}}}; // Location 1 : Velocity
vk::PipelineVertexInputStateCreateInfo vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &vertex_input_bindings,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size()),
.pVertexAttributeDescriptions = vertex_input_attributes.data()};
// Additive blending
vk::PipelineColorBlendAttachmentState blend_attachment_state{.blendEnable = true,
.srcColorBlendFactor = vk::BlendFactor::eOne,
.dstColorBlendFactor = vk::BlendFactor::eOne,
.colorBlendOp = vk::BlendOp::eAdd,
.srcAlphaBlendFactor = vk::BlendFactor::eSrcAlpha,
.dstAlphaBlendFactor = vk::BlendFactor::eDstAlpha,
.alphaBlendOp = vk::BlendOp::eAdd,
.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthTestEnable = false;
depth_stencil_state.depthWriteEnable = false;
depth_stencil_state.depthCompareOp = vk::CompareOp::eAlways;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
vertex_input_state,
vk::PrimitiveTopology::ePointList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
graphics.pipeline_layout,
render_pass);
}
void HPPComputeNBody::draw()
{
HPPApiVulkanSample::prepare_frame();
std::array<vk::PipelineStageFlags, 2> graphics_wait_stage_masks = {vk::PipelineStageFlagBits::eVertexInput,
vk::PipelineStageFlagBits::eColorAttachmentOutput};
std::array<vk::Semaphore, 2> graphics_wait_semaphores = {compute.semaphore, semaphores.acquired_image_ready};
std::array<vk::Semaphore, 2> graphics_signal_semaphores = {graphics.semaphore, semaphores.render_complete};
// Submit graphics commands
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
submit_info.setWaitSemaphores(graphics_wait_semaphores);
submit_info.setWaitDstStageMask(graphics_wait_stage_masks);
submit_info.setSignalSemaphores(graphics_signal_semaphores);
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
// Submit compute commands, waiting for rendering finished
vk::PipelineStageFlags wait_stage_mask = vk::PipelineStageFlagBits::eComputeShader;
vk::SubmitInfo compute_submit_info{.waitSemaphoreCount = 1,
.pWaitSemaphores = &graphics.semaphore,
.pWaitDstStageMask = &wait_stage_mask,
.commandBufferCount = 1,
.pCommandBuffers = &compute.command_buffer,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &compute.semaphore};
compute.queue.submit(compute_submit_info);
}
void HPPComputeNBody::initializeCamera()
{
camera.type = vkb::CameraType::LookAt;
// Note: Using reversed depth-buffer for increased precision, so Z-Near and Z-Far are flipped
camera.set_perspective(60.0f, static_cast<float>(extent.width) / static_cast<float>(extent.height), 512.0f, 0.1f);
camera.set_rotation(glm::vec3(-26.0f, 75.0f, 0.0f));
camera.set_translation(glm::vec3(0.0f, 0.0f, -14.0f));
camera.translation_speed = 2.5f;
}
void HPPComputeNBody::load_assets()
{
textures.particle = load_texture("textures/particle_rgba.ktx", vkb::scene_graph::components::HPPImage::Color);
textures.gradient = load_texture("textures/particle_gradient_rgba.ktx", vkb::scene_graph::components::HPPImage::Color);
}
void HPPComputeNBody::prepare_compute()
{
vk::Device device = get_device().get_handle();
compute.queue_family_index = vkb::common::get_queue_family_index(get_device().get_gpu().get_queue_family_properties(), vk::QueueFlagBits::eCompute);
vk::PhysicalDeviceLimits const &limits = get_device().get_gpu().get_properties().limits;
// Not all implementations support a work group size of 256, so we need to check with the device limits
compute.work_group_size = std::min<uint32_t>(256, limits.maxComputeWorkGroupSize[0]);
// Same for shared data size for passing data between shader invocations
compute.shared_data_size = std::min<uint32_t>(1024, limits.maxComputeSharedMemorySize / sizeof(glm::vec4));
prepare_compute_storage_buffers();
// Compute shader uniform buffer block
compute.uniform_buffer =
std::make_unique<vkb::core::BufferCpp>(get_device(), sizeof(compute.ubo), vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
update_compute_uniform_buffers(1.0f);
// Get compute queue
// Compute pipelines are created separate from graphics pipelines even if they use the same queue (family index)
compute.queue = device.getQueue(compute.queue_family_index, 0);
compute.descriptor_set_layout = create_compute_descriptor_set_layout();
compute.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, compute.descriptor_set_layout);
update_compute_descriptor_set();
compute.pipeline_layout = device.createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &compute.descriptor_set_layout});
// create the compute pipelines
// 1st pass - Particle movement calculations
{
vk::PipelineShaderStageCreateInfo stage = load_shader("compute_nbody", "particle_calculate.comp.spv", vk::ShaderStageFlagBits::eCompute);
// Set some shader parameters via specialization constants
struct MovementSpecializationData
{
uint32_t workgroup_size;
uint32_t shared_data_size;
float gravity;
float power;
float soften;
};
std::array<vk::SpecializationMapEntry, 5> movement_specialization_map_entries = {
{{0, offsetof(MovementSpecializationData, workgroup_size), sizeof(uint32_t)},
{1, offsetof(MovementSpecializationData, shared_data_size), sizeof(uint32_t)},
{2, offsetof(MovementSpecializationData, gravity), sizeof(float)},
{3, offsetof(MovementSpecializationData, power), sizeof(float)},
{4, offsetof(MovementSpecializationData, soften), sizeof(float)}}};
MovementSpecializationData movement_specialization_data{compute.work_group_size, compute.shared_data_size, 0.002f, 0.75f, 0.05f};
vk::SpecializationInfo specialization_info{static_cast<uint32_t>(movement_specialization_map_entries.size()),
movement_specialization_map_entries.data(),
sizeof(movement_specialization_data),
&movement_specialization_data};
stage.pSpecializationInfo = &specialization_info;
compute.pipeline_calculate = create_compute_pipeline(stage);
}
// 2nd pass - Particle integration
{
vk::PipelineShaderStageCreateInfo stage = load_shader("compute_nbody", "particle_integrate.comp.spv", vk::ShaderStageFlagBits::eCompute);
vk::SpecializationMapEntry integration_specialization_entry{0, 0, sizeof(compute.work_group_size)};
vk::SpecializationInfo specialization_info{1, &integration_specialization_entry, sizeof(compute.work_group_size), &compute.work_group_size};
stage.pSpecializationInfo = &specialization_info;
compute.pipeline_integrate = create_compute_pipeline(stage);
}
// Separate command pool as queue family for compute may be different than graphics
compute.command_pool = device.createCommandPool({.queueFamilyIndex = compute.queue_family_index});
// Create a command buffer for compute operations
compute.command_buffer = vkb::common::allocate_command_buffer(device, compute.command_pool);
// Semaphore for compute & graphics sync
compute.semaphore = device.createSemaphore({});
// Signal the semaphore
vkb::common::submit_and_wait(device, queue, {}, {compute.semaphore});
// Build a single command buffer containing the compute dispatch commands
build_compute_command_buffer();
// If necessary, acquire and immediately release the storage buffer, so that the initial acquire
// from the graphics command buffers are matched up properly.
if (graphics.queue_family_index != compute.queue_family_index)
{
// Create a transient command buffer for setting up the initial buffer transfer state
vk::CommandBuffer transfer_command = vkb::common::allocate_command_buffer(device, compute.command_pool);
build_compute_transfer_command_buffer(transfer_command);
// Submit and wait for compute commands
vkb::common::submit_and_wait(device, compute.queue, {transfer_command});
// free the transfer command buffer
device.freeCommandBuffers(compute.command_pool, transfer_command);
}
}
// Setup and fill the compute shader storage buffers containing the particles
void HPPComputeNBody::prepare_compute_storage_buffers()
{
#if 0
std::vector<glm::vec3> attractors = {
glm::vec3(2.5f, 1.5f, 0.0f),
glm::vec3(-2.5f, -1.5f, 0.0f),
};
#else
std::vector<glm::vec3> attractors = {
glm::vec3(5.0f, 0.0f, 0.0f),
glm::vec3(-5.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 0.0f, 5.0f),
glm::vec3(0.0f, 0.0f, -5.0f),
glm::vec3(0.0f, 4.0f, 0.0f),
glm::vec3(0.0f, -8.0f, 0.0f),
};
#endif
compute.ubo.particle_count = static_cast<uint32_t>(attractors.size()) * PARTICLES_PER_ATTRACTOR;
// Initial particle positions
std::vector<Particle> particle_buffer(compute.ubo.particle_count);
std::default_random_engine rnd_engine(lock_simulation_speed ? 0 : static_cast<unsigned>(time(nullptr)));
std::normal_distribution<float> rnd_distribution(0.0f, 1.0f);
for (uint32_t i = 0; i < static_cast<uint32_t>(attractors.size()); i++)
{
for (uint32_t j = 0; j < PARTICLES_PER_ATTRACTOR; j++)
{
Particle &particle = particle_buffer[i * PARTICLES_PER_ATTRACTOR + j];
// First particle in group as heavy center of gravity
if (j == 0)
{
particle.pos = glm::vec4(attractors[i] * 1.5f, 90000.0f);
particle.vel = glm::vec4(glm::vec4(0.0f));
}
else
{
// Position
glm::vec3 position(attractors[i] +
glm::vec3(rnd_distribution(rnd_engine), rnd_distribution(rnd_engine), rnd_distribution(rnd_engine)) * 0.75f);
float len = glm::length(glm::normalize(position - attractors[i]));
position.y *= 2.0f - (len * len);
// Velocity
glm::vec3 angular = glm::vec3(0.5f, 1.5f, 0.5f) * (((i % 2) == 0) ? 1.0f : -1.0f);
glm::vec3 velocity = glm::cross((position - attractors[i]), angular) +
glm::vec3(rnd_distribution(rnd_engine), rnd_distribution(rnd_engine), rnd_distribution(rnd_engine) * 0.025f);
float mass = (rnd_distribution(rnd_engine) * 0.5f + 0.5f) * 75.0f;
particle.pos = glm::vec4(position, mass);
particle.vel = glm::vec4(velocity, 0.0f);
}
// Color gradient offset
particle.vel.w = static_cast<float>(i) * 1.0f / static_cast<uint32_t>(attractors.size());
}
}
vk::DeviceSize storage_buffer_size = particle_buffer.size() * sizeof(Particle);
// Staging
// SSBO won't be changed on the host after upload so copy to device local memory
vkb::core::BufferCpp staging_buffer = vkb::core::BufferCpp::create_staging_buffer(get_device(), particle_buffer);
compute.storage_buffer = std::make_unique<vkb::core::BufferCpp>(get_device(),
storage_buffer_size,
vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eStorageBuffer |
vk::BufferUsageFlagBits::eTransferDst,
VMA_MEMORY_USAGE_GPU_ONLY);
// Copy from staging buffer to storage buffer
vk::Device device = get_device().get_handle();
vk::CommandBuffer copy_command = vkb::common::allocate_command_buffer(get_device().get_handle(), get_device().get_command_pool().get_handle());
build_copy_command_buffer(copy_command, staging_buffer.get_handle(), storage_buffer_size);
vkb::common::submit_and_wait(device, queue, {copy_command});
device.freeCommandBuffers(get_device().get_command_pool().get_handle(), copy_command);
}
void HPPComputeNBody::prepare_graphics()
{
vk::Device device = get_device().get_handle();
graphics.queue_family_index = vkb::common::get_queue_family_index(get_device().get_gpu().get_queue_family_properties(), vk::QueueFlagBits::eGraphics);
// Vertex shader uniform buffer block
graphics.uniform_buffer =
std::make_unique<vkb::core::BufferCpp>(get_device(), sizeof(graphics.ubo), vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
update_graphics_uniform_buffers();
graphics.descriptor_set_layout = create_graphics_descriptor_set_layout();
graphics.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, graphics.descriptor_set_layout);
update_graphics_descriptor_set();
graphics.pipeline_layout = device.createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &graphics.descriptor_set_layout});
graphics.pipeline = create_graphics_pipeline();
// Semaphore for compute & graphics sync
graphics.semaphore = device.createSemaphore({});
}
void HPPComputeNBody::update_compute_descriptor_set()
{
vk::DescriptorBufferInfo storage_buffer_descriptor{compute.storage_buffer->get_handle(), 0, vk::WholeSize};
vk::DescriptorBufferInfo uniform_buffer_descriptor{compute.uniform_buffer->get_handle(), 0, vk::WholeSize};
std::array<vk::WriteDescriptorSet, 2> compute_write_descriptor_sets = {{// Binding 0 : Particle position storage buffer
{.dstSet = compute.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageBuffer,
.pBufferInfo = &storage_buffer_descriptor},
// Binding 1 : Uniform buffer
{.dstSet = compute.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &uniform_buffer_descriptor}}};
get_device().get_handle().updateDescriptorSets(compute_write_descriptor_sets, nullptr);
}
void HPPComputeNBody::update_compute_uniform_buffers(float delta_time)
{
compute.ubo.delta_time = paused ? 0.0f : delta_time;
compute.uniform_buffer->convert_and_update(compute.ubo);
}
void HPPComputeNBody::update_graphics_descriptor_set()
{
vk::DescriptorBufferInfo buffer_descriptor{graphics.uniform_buffer->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo particle_image_descriptor{textures.particle.sampler,
textures.particle.image->get_vk_image_view().get_handle(),
descriptor_type_to_image_layout(vk::DescriptorType::eCombinedImageSampler,
textures.particle.image->get_vk_image_view().get_format())};
vk::DescriptorImageInfo gradient_image_descriptor{textures.gradient.sampler,
textures.gradient.image->get_vk_image_view().get_handle(),
descriptor_type_to_image_layout(vk::DescriptorType::eCombinedImageSampler,
textures.gradient.image->get_vk_image_view().get_format())};
std::array<vk::WriteDescriptorSet, 3> write_descriptor_sets = {{{.dstSet = graphics.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &particle_image_descriptor},
{.dstSet = graphics.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &gradient_image_descriptor},
{.dstSet = graphics.descriptor_set,
.dstBinding = 2,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &buffer_descriptor}}};
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, nullptr);
}
void HPPComputeNBody::update_graphics_uniform_buffers()
{
graphics.ubo.projection = camera.matrices.perspective;
graphics.ubo.view = camera.matrices.view;
graphics.ubo.screenDim = glm::vec2(static_cast<float>(extent.width), static_cast<float>(extent.height));
graphics.uniform_buffer->convert_and_update(graphics.ubo);
}
std::unique_ptr<vkb::Application> create_hpp_compute_nbody()
{
return std::make_unique<HPPComputeNBody>();
}
@@ -1,166 +0,0 @@
/* 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.
*/
/*
* Compute shader N-body simulation using two passes and shared compute shader memory, using vulkan.hpp
*/
#pragma once
#include <hpp_api_vulkan_sample.h>
#if defined(__ANDROID__)
// Lower particle count on Android for performance reasons
# define PARTICLES_PER_ATTRACTOR 3 * 1024
#else
# define PARTICLES_PER_ATTRACTOR 4 * 1024
#endif
class HPPComputeNBody : public HPPApiVulkanSample
{
public:
HPPComputeNBody();
~HPPComputeNBody();
private:
// Resources for the compute part of the example
struct ComputeUBO
{ // Compute shader uniform block object
float delta_time; // Frame delta time
int32_t particle_count;
};
struct Compute
{
vk::CommandBuffer command_buffer; // Command buffer storing the dispatch commands and barriers
vk::CommandPool command_pool; // Use a separate command pool (queue family may differ from the one used for graphics)
vk::DescriptorSet descriptor_set; // Compute shader bindings
vk::DescriptorSetLayout descriptor_set_layout; // Compute shader binding layout
vk::Pipeline pipeline_calculate; // Compute pipeline for N-Body velocity calculation (1st pass)
vk::Pipeline pipeline_integrate; // Compute pipeline for euler integration (2nd pass)
vk::PipelineLayout pipeline_layout; // Layout of the compute pipeline
vk::Queue queue; // Separate queue for compute commands (queue family may differ from the one used for graphics)
uint32_t queue_family_index = ~0;
vk::Semaphore semaphore; // Execution dependency between compute & graphic submission
uint32_t shared_data_size = 1024;
std::unique_ptr<vkb::core::BufferCpp> storage_buffer; // (Shader) storage buffer object containing the particles
ComputeUBO ubo;
std::unique_ptr<vkb::core::BufferCpp> uniform_buffer; // Uniform buffer object containing particle system parameters
uint32_t work_group_size = 128;
void destroy(vk::Device device)
{
storage_buffer.reset();
uniform_buffer.reset();
device.destroyPipeline(pipeline_calculate);
device.destroyPipeline(pipeline_integrate);
device.destroyPipelineLayout(pipeline_layout);
// no need to free the descriptor_set, as it's implicitly free'd with the descriptor_pool
device.destroyDescriptorSetLayout(descriptor_set_layout);
device.destroySemaphore(semaphore);
device.freeCommandBuffers(command_pool, command_buffer);
device.destroyCommandPool(command_pool);
}
};
// Resources for the graphics part of the example
struct GraphicsUBO
{
glm::mat4 projection;
glm::mat4 view;
glm::vec2 screenDim;
};
struct Graphics
{
vk::DescriptorSet descriptor_set; // Particle system rendering shader bindings
vk::DescriptorSetLayout descriptor_set_layout; // Particle system rendering shader binding layout
vk::Pipeline pipeline; // Particle rendering pipeline
vk::PipelineLayout pipeline_layout; // Layout of the graphics pipeline
uint32_t queue_family_index = ~0;
vk::Semaphore semaphore; // Execution dependency between compute & graphic submission
GraphicsUBO ubo;
std::unique_ptr<vkb::core::BufferCpp> uniform_buffer; // Contains scene matrices
void destroy(vk::Device device)
{
uniform_buffer.reset();
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
// no need to free the descriptor_set, as it's implicitly free'd with the descriptor_pool
device.destroyDescriptorSetLayout(descriptor_set_layout);
device.destroySemaphore(semaphore);
}
};
// SSBO particle declaration
struct Particle
{
glm::vec4 pos; // xyz = position, w = mass
glm::vec4 vel; // xyz = velocity, w = gradient texture position
};
struct Textures
{
HPPTexture gradient;
HPPTexture particle;
void destroy(vk::Device device)
{
device.destroySampler(particle.sampler);
device.destroySampler(gradient.sampler);
}
};
private:
// from vkb::Application
bool prepare(const vkb::ApplicationOptions &options) override;
bool resize(const uint32_t width, const uint32_t height) override;
// from vkb::VulkanSample
void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
void build_command_buffers() override;
void render(float delta_time) override;
void build_compute_command_buffer();
void build_compute_transfer_command_buffer(vk::CommandBuffer command_buffer) const;
void build_copy_command_buffer(vk::CommandBuffer command_buffer, vk::Buffer staging_buffer, vk::DeviceSize buffer_size) const;
vk::DescriptorSetLayout create_compute_descriptor_set_layout();
vk::Pipeline create_compute_pipeline(vk::PipelineShaderStageCreateInfo const &stage);
vk::DescriptorPool create_descriptor_pool();
vk::DescriptorSetLayout create_graphics_descriptor_set_layout();
vk::Pipeline create_graphics_pipeline();
void draw();
void initializeCamera();
void load_assets();
void prepare_compute();
void prepare_compute_storage_buffers();
void prepare_graphics();
void update_compute_descriptor_set();
void update_compute_uniform_buffers(float delta_time);
void update_graphics_descriptor_set();
void update_graphics_uniform_buffers();
private:
Compute compute;
Graphics graphics;
Textures textures;
};
std::unique_ptr<vkb::Application> create_hpp_compute_nbody();
@@ -1,33 +0,0 @@
# Copyright (c) 2021-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 "Sascha Willems"
NAME "HPP Dynamic uniform buffers"
DESCRIPTION "Demonstrates the use of dynamic offsets into one single uniform buffers for rendering multiple objects, using vulkan.hpp"
SHADER_FILES_GLSL
"dynamic_uniform_buffers/glsl/base.vert"
"dynamic_uniform_buffers/glsl/base.frag"
SHADER_FILES_HLSL
"dynamic_uniform_buffers/hlsl/base.vert.hlsl"
"dynamic_uniform_buffers/hlsl/base.frag.hlsl")
@@ -1,27 +0,0 @@
////
- Copyright (c) 2019-2023, 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.
-
////
:pp: {plus}{plus}
= HPP Dynamic Uniform Buffers
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hpp_dynamic_uniform_buffers[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/dynamic_uniform_buffers[Dynamic Uniform buffers] that illustrates the usage of the C{pp} bindings of Vulkan provided by vulkan.hpp.
@@ -1,414 +0,0 @@
/* Copyright (c) 2021-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.
*/
/*
* Demonstrates the use of dynamic uniform buffers, using vulkan.hpp
*
* Instead of using one uniform buffer per-object, this example allocates one big uniform buffer
* with respect to the alignment reported by the device via minUniformBufferOffsetAlignment that
* contains all matrices for the objects in the scene.
*
* The used descriptor type vk::DescriptorType::eUniformBufferDynamic then allows to set a dynamic
* offset used to pass data from the single uniform buffer to the connected shader binding point.
*/
#include "hpp_dynamic_uniform_buffers.h"
#include <benchmark_mode/benchmark_mode.h>
#include <iostream>
#include <random>
HPPDynamicUniformBuffers::HPPDynamicUniformBuffers()
{
title = "HPP Dynamic uniform buffers";
}
HPPDynamicUniformBuffers ::~HPPDynamicUniformBuffers()
{
if (has_device() && get_device().get_handle())
{
if (ubo_data_dynamic.model)
{
aligned_free(ubo_data_dynamic.model);
}
vk::Device device = get_device().get_handle();
// Clean up used Vulkan resources
// Note : Inherited destructor cleans up resources stored in base class
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
}
}
// Wrapper functions for aligned memory allocation
// There is currently no standard for this in C++ that works across all platforms and vendors, so we abstract this
void *HPPDynamicUniformBuffers::aligned_alloc(size_t size, size_t alignment)
{
void *data = nullptr;
#if defined(_MSC_VER) || defined(__MINGW32__)
data = _aligned_malloc(size, alignment);
#else
int res = posix_memalign(&data, alignment, size);
if (res != 0)
{
data = nullptr;
}
#endif
return data;
}
void HPPDynamicUniformBuffers::aligned_free(void *data)
{
#if defined(_MSC_VER) || defined(__MINGW32__)
_aligned_free(data);
#else
free(data);
#endif
}
bool HPPDynamicUniformBuffers::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
prepare_camera();
generate_cube();
prepare_uniform_buffers();
descriptor_set_layout = create_descriptor_set_layout();
pipeline_layout = get_device().get_handle().createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &descriptor_set_layout});
pipeline = create_pipeline();
descriptor_pool = create_descriptor_pool();
descriptor_set = vkb::common::allocate_descriptor_set(get_device().get_handle(), descriptor_pool, descriptor_set_layout);
update_descriptor_set();
build_command_buffers();
prepared = true;
}
return prepared;
}
bool HPPDynamicUniformBuffers::resize(const uint32_t width, const uint32_t height)
{
HPPApiVulkanSample::resize(width, height);
update_uniform_buffers();
return true;
}
void HPPDynamicUniformBuffers::build_command_buffers()
{
vk::DeviceSize offset = 0;
std::array<vk::ClearValue, 2> clear_values = {default_clear_color, vk::ClearDepthStencilValue{0.0f, 0}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = render_pass,
.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
for (size_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
render_pass_begin_info.framebuffer = framebuffers[i];
vk::CommandBuffer command_buffer = draw_cmd_buffers[i];
command_buffer.begin(vk::CommandBufferBeginInfo());
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
command_buffer.setViewport(0, {{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f}});
command_buffer.setScissor(0, {{{0, 0}, extent}});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
command_buffer.bindVertexBuffers(0, static_cast<vk::Buffer>(vertex_buffer->get_handle()), offset);
command_buffer.bindIndexBuffer(index_buffer->get_handle(), 0, vk::IndexType::eUint32);
// Render multiple objects using different model matrices by dynamically offsetting into one uniform buffer
for (uint32_t j = 0; j < OBJECT_INSTANCES; j++)
{
// One dynamic offset per dynamic descriptor to offset into the ubo containing all model matrices
uint32_t dynamic_offset = j * static_cast<uint32_t>(dynamic_alignment);
// Bind the descriptor set for rendering a mesh using the dynamic offset
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, descriptor_set, dynamic_offset);
command_buffer.drawIndexed(index_count, 1, 0, 0, 0);
}
draw_ui(command_buffer);
command_buffer.endRenderPass();
command_buffer.end();
}
}
void HPPDynamicUniformBuffers::render(float delta_time)
{
if (prepared)
{
draw();
if (!paused)
{
update_dynamic_uniform_buffer(delta_time);
}
if (camera.updated)
{
update_uniform_buffers();
}
}
}
vk::DescriptorPool HPPDynamicUniformBuffers::create_descriptor_pool()
{
// Example uses one ubo, on dynamic ubo, and one combined image sampler
std::array<vk::DescriptorPoolSize, 3> pool_sizes = {{{vk::DescriptorType::eUniformBuffer, 1},
{vk::DescriptorType::eUniformBufferDynamic, 1},
{vk::DescriptorType::eCombinedImageSampler, 1}}};
return get_device().get_handle().createDescriptorPool(
{.maxSets = 2, .poolSizeCount = static_cast<uint32_t>(pool_sizes.size()), .pPoolSizes = pool_sizes.data()});
}
vk::DescriptorSetLayout HPPDynamicUniformBuffers::create_descriptor_set_layout()
{
std::array<vk::DescriptorSetLayoutBinding, 3> bindings = {{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex},
{1, vk::DescriptorType::eUniformBufferDynamic, 1, vk::ShaderStageFlagBits::eVertex},
{2, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}};
return get_device().get_handle().createDescriptorSetLayout({.bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data()});
}
vk::Pipeline HPPDynamicUniformBuffers::create_pipeline()
{
// Load shaders
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {load_shader("dynamic_uniform_buffers", "base.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("dynamic_uniform_buffers", "base.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Vertex bindings and attributes
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(Vertex), vk::VertexInputRate::eVertex};
std::array<vk::VertexInputAttributeDescription, 2> vertex_input_attributes = {
{{0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos)}, // Location 0 : Position
{1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)}}}; // Location 1 : Color
vk::PipelineVertexInputStateCreateInfo vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &vertex_input_binding,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size()),
.pVertexAttributeDescriptions = vertex_input_attributes.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthTestEnable = true;
depth_stencil_state.depthWriteEnable = true;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.back.compareOp = vk::CompareOp::eGreater;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
pipeline_layout,
render_pass);
}
void HPPDynamicUniformBuffers::draw()
{
HPPApiVulkanSample::prepare_frame();
// Submit to queue
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
}
void HPPDynamicUniformBuffers::generate_cube()
{
// Setup vertices indices for a colored cube
std::vector<Vertex> vertices = {
{{-1.0f, -1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}},
{{1.0f, -1.0f, 1.0f}, {0.0f, 1.0f, 0.0f}},
{{1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 0.0f}},
{{-1.0f, -1.0f, -1.0f}, {1.0f, 0.0f, 0.0f}},
{{1.0f, -1.0f, -1.0f}, {0.0f, 1.0f, 0.0f}},
{{1.0f, 1.0f, -1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, 1.0f, -1.0f}, {0.0f, 0.0f, 0.0f}},
};
// clang-format off
std::vector<uint32_t> indices = { 0, 1, 2, 2, 3, 0, 1, 5, 6, 6, 2, 1, 7, 6, 5, 5, 4, 7,
4, 0, 3, 3, 7, 4, 4, 5, 1, 1, 0, 4, 3, 2, 6, 6, 7, 3 };
// clang-format on
index_count = static_cast<uint32_t>(indices.size());
auto vertex_buffer_size = vertices.size() * sizeof(Vertex);
auto index_buffer_size = indices.size() * sizeof(uint32_t);
// Create buffers
// For the sake of simplicity we won't stage the vertex data to the gpu memory
// Vertex buffer
vertex_buffer =
std::make_unique<vkb::core::BufferCpp>(get_device(), vertex_buffer_size, vk::BufferUsageFlagBits::eVertexBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
vertex_buffer->update(vertices.data(), vertex_buffer_size);
index_buffer = std::make_unique<vkb::core::BufferCpp>(get_device(), index_buffer_size, vk::BufferUsageFlagBits::eIndexBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
index_buffer->update(indices.data(), index_buffer_size);
}
void HPPDynamicUniformBuffers::prepare_camera()
{
camera.type = vkb::CameraType::LookAt;
camera.set_position(glm::vec3(0.0f, 0.0f, -30.0f));
camera.set_rotation(glm::vec3(0.0f));
// Note: Using reversed depth-buffer for increased precision, so Znear and Zfar are flipped
camera.set_perspective(60.0f, static_cast<float>(extent.width) / static_cast<float>(extent.height), 256.0f, 0.1f);
}
// Prepare and initialize uniform buffer containing shader uniforms
void HPPDynamicUniformBuffers::prepare_uniform_buffers()
{
// Allocate data for the dynamic uniform buffer object
// We allocate this manually as the alignment of the offset differs between GPUs
// Calculate required alignment based on minimum device offset alignment
vk::DeviceSize min_ubo_alignment = get_device().get_gpu().get_handle().getProperties().limits.minUniformBufferOffsetAlignment;
dynamic_alignment = sizeof(glm::mat4);
if (min_ubo_alignment > 0)
{
dynamic_alignment = (dynamic_alignment + min_ubo_alignment - 1) & ~(min_ubo_alignment - 1);
}
size_t buffer_size = OBJECT_INSTANCES * dynamic_alignment;
ubo_data_dynamic.model = static_cast<glm::mat4 *>(aligned_alloc(buffer_size, dynamic_alignment));
assert(ubo_data_dynamic.model);
std::cout << "minUniformBufferOffsetAlignment = " << min_ubo_alignment << std::endl;
std::cout << "dynamicAlignment = " << dynamic_alignment << std::endl;
// Vertex shader uniform buffer block
// Static shared uniform buffer object with projection and view matrix
uniform_buffers.view =
std::make_unique<vkb::core::BufferCpp>(get_device(), sizeof(ubo_vs), vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
uniform_buffers.dynamic =
std::make_unique<vkb::core::BufferCpp>(get_device(), buffer_size, vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
// Prepare per-object matrices with offsets and random rotations
std::default_random_engine rnd_engine(lock_simulation_speed ? 0 : static_cast<unsigned>(time(nullptr)));
std::normal_distribution<float> rnd_dist(-1.0f, 1.0f);
for (uint32_t i = 0; i < OBJECT_INSTANCES; i++)
{
rotations[i] = glm::vec3(rnd_dist(rnd_engine), rnd_dist(rnd_engine), rnd_dist(rnd_engine)) * 2.0f * glm::pi<float>();
rotation_speeds[i] = glm::vec3(rnd_dist(rnd_engine), rnd_dist(rnd_engine), rnd_dist(rnd_engine));
}
update_uniform_buffers();
update_dynamic_uniform_buffer(0.0f, true);
}
void HPPDynamicUniformBuffers::update_descriptor_set()
{
vk::DescriptorBufferInfo view_buffer_descriptor{uniform_buffers.view->get_handle(), 0, vk::WholeSize};
vk::DescriptorBufferInfo dynamic_buffer_descriptor{uniform_buffers.dynamic->get_handle(), 0, dynamic_alignment};
std::array<vk::WriteDescriptorSet, 2> write_descriptor_sets = {{// Binding 0 : Projection/View matrix uniform buffer
{.dstSet = descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &view_buffer_descriptor},
// Binding 1 : Instance matrix as dynamic uniform buffer
{.dstSet = descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBufferDynamic,
.pBufferInfo = &dynamic_buffer_descriptor}}};
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, {});
}
void HPPDynamicUniformBuffers::update_dynamic_uniform_buffer(float delta_time, bool force)
{
// Update at max. 60 fps
animation_timer += delta_time;
if ((animation_timer + 0.0025 < (1.0f / 60.0f)) && (!force))
{
return;
}
// Dynamic ubo with per-object model matrices indexed by offsets in the command buffer
auto dim = static_cast<uint32_t>(pow(OBJECT_INSTANCES, (1.0f / 3.0f)));
auto fdim = static_cast<float>(dim);
glm::vec3 offset(5.0f);
for (uint32_t x = 0; x < dim; x++)
{
auto fx = static_cast<float>(x);
for (uint32_t y = 0; y < dim; y++)
{
auto fy = static_cast<float>(y);
for (uint32_t z = 0; z < dim; z++)
{
auto fz = static_cast<float>(z);
auto index = x * dim * dim + y * dim + z;
// Aligned offset
auto model_mat = reinterpret_cast<glm::mat4 *>((reinterpret_cast<uint64_t>(ubo_data_dynamic.model) + (index * dynamic_alignment)));
// Update rotations
rotations[index] += animation_timer * rotation_speeds[index];
// Update matrices
glm::vec3 pos(-((fdim * offset.x) / 2.0f) + offset.x / 2.0f + fx * offset.x,
-((fdim * offset.y) / 2.0f) + offset.y / 2.0f + fy * offset.y,
-((fdim * offset.z) / 2.0f) + offset.z / 2.0f + fz * offset.z);
*model_mat = glm::translate(glm::mat4(1.0f), pos);
*model_mat = glm::rotate(*model_mat, rotations[index].x, glm::vec3(1.0f, 1.0f, 0.0f));
*model_mat = glm::rotate(*model_mat, rotations[index].y, glm::vec3(0.0f, 1.0f, 0.0f));
*model_mat = glm::rotate(*model_mat, rotations[index].z, glm::vec3(0.0f, 0.0f, 1.0f));
}
}
}
animation_timer = 0.0f;
uniform_buffers.dynamic->update(ubo_data_dynamic.model, static_cast<size_t>(uniform_buffers.dynamic->get_size()));
// Flush to make changes visible to the device
uniform_buffers.dynamic->flush();
}
void HPPDynamicUniformBuffers::update_uniform_buffers()
{
// Fixed ubo with projection and view matrices
ubo_vs.projection = camera.matrices.perspective;
ubo_vs.view = camera.matrices.view;
uniform_buffers.view->convert_and_update(ubo_vs);
}
std::unique_ptr<vkb::Application> create_hpp_dynamic_uniform_buffers()
{
return std::make_unique<HPPDynamicUniformBuffers>();
}
@@ -1,108 +0,0 @@
/* Copyright (c) 2021-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.
*/
/*
* Demonstrates the use of dynamic uniform buffers, using vulkan.hpp
*
* Instead of using one uniform buffer per-object, this example allocates one big uniform buffer
* with respect to the alignment reported by the device via minUniformBufferOffsetAlignment that
* contains all matrices for the objects in the scene.
*
* The used descriptor type vk::DescriptorType::eUniformBufferDynamic then allows to set a dynamic
* offset used to pass data from the single uniform buffer to the connected shader binding point.
*/
#pragma once
#include <hpp_api_vulkan_sample.h>
#define OBJECT_INSTANCES 125
class HPPDynamicUniformBuffers : public HPPApiVulkanSample
{
public:
HPPDynamicUniformBuffers();
~HPPDynamicUniformBuffers();
private:
// One big uniform buffer that contains all matrices
// Note that we need to manually allocate the data to cope for GPU-specific uniform buffer offset alignments
struct UboDataDynamic
{
glm::mat4 *model = nullptr;
};
struct UboVS
{
glm::mat4 projection;
glm::mat4 view;
};
struct UniformBuffers
{
std::unique_ptr<vkb::core::BufferCpp> view;
std::unique_ptr<vkb::core::BufferCpp> dynamic;
};
struct Vertex
{
float pos[3];
float color[3];
};
private:
static void *aligned_alloc(size_t size, size_t alignment);
static void aligned_free(void *data);
private:
// from vkb::Application
bool prepare(const vkb::ApplicationOptions &options) override;
bool resize(const uint32_t width, const uint32_t height) override;
// from HPPApiVulkanSample
void render(float delta_time) override;
void build_command_buffers() override;
vk::DescriptorPool create_descriptor_pool();
vk::DescriptorSetLayout create_descriptor_set_layout();
vk::Pipeline create_pipeline();
void draw();
void generate_cube();
void prepare_camera();
void prepare_uniform_buffers();
void update_descriptor_set();
void update_dynamic_uniform_buffer(float delta_time, bool force = false);
void update_uniform_buffers();
private:
float animation_timer = 0.0f;
vk::DescriptorSet descriptor_set;
vk::DescriptorSetLayout descriptor_set_layout;
size_t dynamic_alignment = 0;
std::unique_ptr<vkb::core::BufferCpp> index_buffer;
uint32_t index_count = 0;
vk::Pipeline pipeline;
vk::PipelineLayout pipeline_layout;
glm::vec3 rotations[OBJECT_INSTANCES]; // Store random per-object rotations
glm::vec3 rotation_speeds[OBJECT_INSTANCES];
UboDataDynamic ubo_data_dynamic;
UboVS ubo_vs;
UniformBuffers uniform_buffers;
std::unique_ptr<vkb::core::BufferCpp> vertex_buffer;
};
std::unique_ptr<vkb::Application> create_hpp_dynamic_uniform_buffers();
-41
View File
@@ -1,41 +0,0 @@
# 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.
#
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 "Sascha Willems"
NAME "HPP HDR"
DESCRIPTION "High-dynamic-range rendering, using vulkan.hpp"
SHADER_FILES_GLSL
"hdr/glsl/composition.vert"
"hdr/glsl/composition.frag"
"hdr/glsl/bloom.vert"
"hdr/glsl/bloom.frag"
"hdr/glsl/gbuffer.vert"
"hdr/glsl/gbuffer.frag"
SHADER_FILES_HLSL
"hdr/hlsl/composition.vert.hlsl"
"hdr/hlsl/composition.frag.hlsl"
"hdr/hlsl/bloom.vert.hlsl"
"hdr/hlsl/bloom.frag.hlsl"
"hdr/hlsl/gbuffer.vert.hlsl"
"hdr/hlsl/gbuffer.frag.hlsl")
-27
View File
@@ -1,27 +0,0 @@
////
- Copyright (c) 2023, 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.
-
////
:pp: {plus}{plus}
= HPP High dynamic range
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hpp_hdr[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hdr[High dynamic range] that illustrates the usage of the C{pp} bindings of Vulkan provided by vulkan.hpp.
-741
View File
@@ -1,741 +0,0 @@
/* 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.
*/
/*
* High dynamic range rendering, using vulkan.hpp
*/
#include "hpp_hdr.h"
HPPHDR::HPPHDR()
{
title = "HPP High dynamic range rendering";
}
HPPHDR::~HPPHDR()
{
if (has_device() && get_device().get_handle())
{
vk::Device device = get_device().get_handle();
bloom.destroy(device);
composition.destroy(device);
filter_pass.destroy(device);
models.destroy(device, descriptor_pool);
offscreen.destroy(device);
textures.destroy(device);
}
}
bool HPPHDR::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
prepare_camera();
load_assets();
prepare_uniform_buffers();
prepare_offscreen_buffer();
descriptor_pool = create_descriptor_pool();
prepare_bloom();
prepare_composition();
prepare_models();
build_command_buffers();
prepared = true;
}
return prepared;
}
bool HPPHDR::resize(const uint32_t width, const uint32_t height)
{
HPPApiVulkanSample::resize(width, height);
update_uniform_buffers();
return true;
}
void HPPHDR::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = true;
}
}
void HPPHDR::build_command_buffers()
{
vk::CommandBufferBeginInfo command_buffer_begin_info;
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
vk::CommandBuffer command_buffer = draw_cmd_buffers[i];
command_buffer.begin(command_buffer_begin_info);
{
/*
First pass: Render scene to offscreen framebuffer
*/
std::array<vk::ClearValue, 3> clear_values = {{vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 0.0f}})),
vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 0.0f}})),
vk::ClearDepthStencilValue{0.0f, 0}}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = offscreen.render_pass,
.framebuffer = offscreen.framebuffer,
.renderArea = {{0, 0}, offscreen.extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(offscreen.extent.width), static_cast<float>(offscreen.extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, offscreen.extent};
command_buffer.setScissor(0, scissor);
// Skybox
if (display_skybox)
{
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, models.skybox.pipeline);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, models.pipeline_layout, 0, models.skybox.descriptor_set, {});
draw_model(models.skybox.meshes[0], command_buffer);
}
// 3D object
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, models.objects.pipeline);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, models.pipeline_layout, 0, models.objects.descriptor_set, {});
draw_model(models.objects.meshes[models.object_index], command_buffer);
command_buffer.endRenderPass();
}
/*
Second render pass: First bloom pass
*/
if (bloom.enabled)
{
// Bloom filter
vk::ClearValue clear_value(vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 0.0f}})));
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = filter_pass.render_pass,
.framebuffer = filter_pass.framebuffer,
.renderArea = {{0, 0}, filter_pass.extent},
.clearValueCount = 1,
.pClearValues = &clear_value};
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(filter_pass.extent.width), static_cast<float>(filter_pass.extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, filter_pass.extent};
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, bloom.pipeline_layout, 0, bloom.descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, bloom.pipelines[1]);
command_buffer.draw(3, 1, 0, 0);
command_buffer.endRenderPass();
}
/*
Note: Explicit synchronization is not required between the render pass, as this is done implicit via sub pass dependencies
*/
/*
Third render pass: Scene rendering with applied second bloom pass (when enabled)
*/
{
// Final composition
std::array<vk::ClearValue, 2> clear_values = {{vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 0.0f}})),
vk::ClearDepthStencilValue{0.0f, 0}}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = render_pass,
.framebuffer = framebuffers[i],
.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, extent};
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, composition.pipeline_layout, 0, composition.descriptor_set, {});
// Scene
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, composition.pipeline);
command_buffer.draw(3, 1, 0, 0);
// Bloom
if (bloom.enabled)
{
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, bloom.pipelines[0]);
command_buffer.draw(3, 1, 0, 0);
}
draw_ui(command_buffer);
command_buffer.endRenderPass();
}
command_buffer.end();
}
}
void HPPHDR::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Settings"))
{
if (drawer.combo_box("Object type", &models.object_index, object_names))
{
update_uniform_buffers();
rebuild_command_buffers();
}
if (drawer.input_float("Exposure", &ubo_params.exposure, 0.025f, "%.3f"))
{
update_params();
}
if (drawer.checkbox("Bloom", &bloom.enabled))
{
rebuild_command_buffers();
}
if (drawer.checkbox("Skybox", &display_skybox))
{
rebuild_command_buffers();
}
}
}
void HPPHDR::render(float delta_time)
{
if (prepared)
{
draw();
if (camera.updated)
{
update_uniform_buffers();
}
}
}
vk::DeviceMemory HPPHDR::allocate_memory(vk::Image image)
{
vk::MemoryRequirements memory_requirements = get_device().get_handle().getImageMemoryRequirements(image);
vk::MemoryAllocateInfo memory_allocate_info{.allocationSize = memory_requirements.size,
.memoryTypeIndex = get_device().get_gpu().get_memory_type(memory_requirements.memoryTypeBits,
vk::MemoryPropertyFlagBits::eDeviceLocal)};
return get_device().get_handle().allocateMemory(memory_allocate_info);
}
HPPHDR::FramebufferAttachment HPPHDR::create_attachment(vk::Format format, vk::ImageUsageFlagBits usage)
{
vk::Image image = create_image(format, usage);
vk::DeviceMemory memory = allocate_memory(image);
get_device().get_handle().bindImageMemory(image, memory, 0);
vk::ImageView view =
vkb::common::create_image_view(get_device().get_handle(), image, vk::ImageViewType::e2D, format, vkb::common::get_image_aspect_flags(usage, format));
return {format, image, memory, view};
}
vk::DescriptorPool HPPHDR::create_descriptor_pool()
{
std::array<vk::DescriptorPoolSize, 2> pool_sizes = {{{vk::DescriptorType::eUniformBuffer, 4}, {vk::DescriptorType::eCombinedImageSampler, 6}}};
return get_device().get_handle().createDescriptorPool(
{.maxSets = 4, .poolSizeCount = static_cast<uint32_t>(pool_sizes.size()), .pPoolSizes = pool_sizes.data()});
}
vk::Pipeline HPPHDR::create_bloom_pipeline(uint32_t direction)
{
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages{load_shader("hdr", "bloom.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("hdr", "bloom.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Set constant parameters via specialization constants
vk::SpecializationMapEntry specialization_map_entry{0, 0, sizeof(uint32_t)};
vk::SpecializationInfo specialization_info{1, &specialization_map_entry, sizeof(uint32_t), &direction};
shader_stages[1].pSpecializationInfo = &specialization_info;
vk::PipelineColorBlendAttachmentState blend_attachment_state{.blendEnable = true,
.srcColorBlendFactor = vk::BlendFactor::eOne,
.dstColorBlendFactor = vk::BlendFactor::eOne,
.colorBlendOp = vk::BlendOp::eAdd,
.srcAlphaBlendFactor = vk::BlendFactor::eSrcAlpha,
.dstAlphaBlendFactor = vk::BlendFactor::eDstAlpha,
.alphaBlendOp = vk::BlendOp::eAdd,
.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
// Empty vertex input state, full screen triangles are generated by the vertex shader
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
{},
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eFront,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
bloom.pipeline_layout,
direction == 1 ? render_pass : filter_pass.render_pass);
}
vk::Pipeline HPPHDR::create_composition_pipeline()
{
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages{load_shader("hdr", "composition.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("hdr", "composition.frag.spv", vk::ShaderStageFlagBits::eFragment)};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
// Empty vertex input state, full screen triangles are generated by the vertex shader
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
{},
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eFront,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
composition.pipeline_layout,
render_pass);
}
vk::RenderPass HPPHDR::create_filter_render_pass()
{
// Set up separate renderpass with references to the color and depth attachments
vk::AttachmentDescription attachment_description{.format = filter_pass.color.format,
.samples = vk::SampleCountFlagBits::e1,
.loadOp = vk::AttachmentLoadOp::eClear,
.storeOp = vk::AttachmentStoreOp::eStore,
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
.initialLayout = vk::ImageLayout::eUndefined,
.finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal};
vk::AttachmentReference color_reference{0, vk::ImageLayout::eColorAttachmentOptimal};
vk::SubpassDescription subpass{.pipelineBindPoint = vk::PipelineBindPoint::eGraphics, .colorAttachmentCount = 1, .pColorAttachments = &color_reference};
return create_render_pass({attachment_description}, subpass);
}
vk::Image HPPHDR::create_image(vk::Format format, vk::ImageUsageFlagBits usage)
{
vk::ImageCreateInfo image_create_info{.imageType = vk::ImageType::e2D,
.format = format,
.extent = {offscreen.extent.width, offscreen.extent.height, 1},
.mipLevels = 1,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.tiling = vk::ImageTiling::eOptimal,
.usage = usage | vk::ImageUsageFlagBits::eSampled};
return get_device().get_handle().createImage(image_create_info);
}
vk::Pipeline HPPHDR::create_models_pipeline(uint32_t shaderType, vk::CullModeFlagBits cullMode, bool depthTestAndWrite)
{
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages{load_shader("hdr", "gbuffer.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("hdr", "gbuffer.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Set constant parameters via specialization constants
vk::SpecializationMapEntry specialization_map_entry{0, 0, sizeof(uint32_t)};
// Set constant parameters via specialization constants
vk::SpecializationInfo specialization_info{1, &specialization_map_entry, sizeof(uint32_t), &shaderType};
shader_stages[0].pSpecializationInfo = &specialization_info;
shader_stages[1].pSpecializationInfo = &specialization_info;
// Vertex bindings an attributes for model rendering
// Binding description
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
// Attribute descriptions
std::vector<vk::VertexInputAttributeDescription> vertex_input_attributes = {{0, 0, vk::Format::eR32G32B32Sfloat, 0},
{1, 0, vk::Format::eR32G32B32Sfloat, 3 * sizeof(float)}};
vk::PipelineVertexInputStateCreateInfo vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &vertex_input_binding,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size()),
.pVertexAttributeDescriptions = vertex_input_attributes.data()};
std::vector<vk::PipelineColorBlendAttachmentState> blend_attachment_states(2);
blend_attachment_states[0].colorWriteMask =
vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA;
blend_attachment_states[1].colorWriteMask = blend_attachment_states[0].colorWriteMask;
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.depthWriteEnable = depthTestAndWrite;
depth_stencil_state.depthTestEnable = depthTestAndWrite;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
cullMode,
vk::FrontFace::eCounterClockwise,
blend_attachment_states,
depth_stencil_state,
models.pipeline_layout,
offscreen.render_pass);
}
vk::RenderPass HPPHDR::create_offscreen_render_pass()
{
// Set up separate renderpass with references to the color and depth attachments
std::vector<vk::AttachmentDescription> attachment_descriptions(3);
// Init attachment properties
for (uint32_t i = 0; i < 3; ++i)
{
attachment_descriptions[i].samples = vk::SampleCountFlagBits::e1;
attachment_descriptions[i].loadOp = vk::AttachmentLoadOp::eClear;
attachment_descriptions[i].storeOp = vk::AttachmentStoreOp::eStore;
attachment_descriptions[i].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
attachment_descriptions[i].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
attachment_descriptions[i].initialLayout = vk::ImageLayout::eUndefined;
attachment_descriptions[i].finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
}
attachment_descriptions[2].finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal;
// Formats
attachment_descriptions[0].format = offscreen.color[0].format;
attachment_descriptions[1].format = offscreen.color[1].format;
attachment_descriptions[2].format = offscreen.depth.format;
std::array<vk::AttachmentReference, 2> color_references{{{0, vk::ImageLayout::eColorAttachmentOptimal},
{1, vk::ImageLayout::eColorAttachmentOptimal}}};
vk::AttachmentReference depth_reference{2, vk::ImageLayout::eDepthStencilAttachmentOptimal};
vk::SubpassDescription subpass{.pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
.colorAttachmentCount = static_cast<uint32_t>(color_references.size()),
.pColorAttachments = color_references.data(),
.pDepthStencilAttachment = &depth_reference};
return create_render_pass(attachment_descriptions, subpass);
}
vk::RenderPass HPPHDR::create_render_pass(std::vector<vk::AttachmentDescription> const &attachment_descriptions, vk::SubpassDescription const &subpass_description)
{
// Use subpass dependencies for attachment layout transitions
std::array<vk::SubpassDependency, 2> subpass_dependencies;
subpass_dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
subpass_dependencies[0].dstSubpass = 0;
// End of previous commands
subpass_dependencies[0].srcStageMask = vk::PipelineStageFlagBits::eBottomOfPipe;
subpass_dependencies[0].srcAccessMask = vk::AccessFlagBits::eNoneKHR;
// Read/write from/to depth
subpass_dependencies[0].dstStageMask = vk::PipelineStageFlagBits::eEarlyFragmentTests;
subpass_dependencies[0].dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;
// Write to attachment
subpass_dependencies[0].dstStageMask |= vk::PipelineStageFlagBits::eColorAttachmentOutput;
subpass_dependencies[0].dstAccessMask |= vk::AccessFlagBits::eColorAttachmentWrite;
subpass_dependencies[1].srcSubpass = 0;
subpass_dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
// End of write to attachment
subpass_dependencies[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
subpass_dependencies[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
// Attachment later read using sampler in 'bloom[0]' pipeline
subpass_dependencies[1].dstStageMask = vk::PipelineStageFlagBits::eFragmentShader;
subpass_dependencies[1].dstAccessMask = vk::AccessFlagBits::eShaderRead;
vk::RenderPassCreateInfo render_pass_create_info{.attachmentCount = static_cast<uint32_t>(attachment_descriptions.size()),
.pAttachments = attachment_descriptions.data(),
.subpassCount = 1,
.pSubpasses = &subpass_description,
.dependencyCount = static_cast<uint32_t>(subpass_dependencies.size()),
.pDependencies = subpass_dependencies.data()};
return get_device().get_handle().createRenderPass(render_pass_create_info);
}
void HPPHDR::draw()
{
HPPApiVulkanSample::prepare_frame();
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
}
void HPPHDR::load_assets()
{
// Models
models.skybox.meshes.emplace_back(load_model("scenes/cube.gltf"));
std::vector<std::string> filenames = {"geosphere.gltf", "teapot.gltf", "torusknot.gltf"};
object_names = {"Sphere", "Teapot", "Torusknot"};
for (auto file : filenames)
{
models.objects.meshes.emplace_back(load_model("scenes/" + file));
}
// Transforms
auto geosphere_matrix = glm::mat4(1.0f);
models.transforms.push_back(geosphere_matrix);
auto teapot_matrix = glm::mat4(1.0f);
teapot_matrix = glm::scale(teapot_matrix, glm::vec3(10.0f, 10.0f, 10.0f));
teapot_matrix = glm::rotate(teapot_matrix, glm::radians(180.0f), glm::vec3(1.0f, 0.0f, 0.0f));
models.transforms.push_back(teapot_matrix);
auto torus_matrix = glm::mat4(1.0f);
models.transforms.push_back(torus_matrix);
// Load HDR cube map
textures.envmap = load_texture_cubemap("textures/uffizi_rgba16f_cube.ktx", vkb::scene_graph::components::HPPImage::Color);
}
void HPPHDR::prepare_bloom()
{
std::array<vk::DescriptorSetLayoutBinding, 2> bindings = {{{0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}};
vk::Device device = get_device().get_handle();
bloom.descriptor_set_layout = device.createDescriptorSetLayout({.bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data()});
bloom.pipeline_layout = device.createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &bloom.descriptor_set_layout});
bloom.pipelines[0] = create_bloom_pipeline(1);
bloom.pipelines[1] = create_bloom_pipeline(0);
bloom.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, bloom.descriptor_set_layout);
update_bloom_descriptor_set();
}
void HPPHDR::prepare_camera()
{
camera.type = vkb::CameraType::LookAt;
camera.set_position(glm::vec3(0.0f, 0.0f, -4.0f));
camera.set_rotation(glm::vec3(0.0f, 180.0f, 0.0f));
// Note: Using reversed depth-buffer for increased precision, so Znear and Zfar are flipped
camera.set_perspective(60.0f, static_cast<float>(extent.width) / static_cast<float>(extent.height), 256.0f, 0.1f);
}
void HPPHDR::prepare_composition()
{
std::array<vk::DescriptorSetLayoutBinding, 2> bindings = {{{0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}};
vk::Device device = get_device().get_handle();
composition.descriptor_set_layout =
device.createDescriptorSetLayout({.bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data()});
composition.pipeline_layout = device.createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &composition.descriptor_set_layout});
composition.pipeline = create_composition_pipeline();
composition.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, composition.descriptor_set_layout);
update_composition_descriptor_set();
}
void HPPHDR::prepare_models()
{
std::array<vk::DescriptorSetLayoutBinding, 3> bindings = {{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{2, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eFragment}}};
vk::Device device = get_device().get_handle();
models.descriptor_set_layout = device.createDescriptorSetLayout({.bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data()});
models.pipeline_layout = device.createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &models.descriptor_set_layout});
models.objects.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, models.descriptor_set_layout);
update_model_descriptor_set(models.objects.descriptor_set);
models.objects.pipeline = create_models_pipeline(1, vk::CullModeFlagBits::eFront, true);
models.skybox.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, models.descriptor_set_layout);
update_model_descriptor_set(models.skybox.descriptor_set);
models.skybox.pipeline = create_models_pipeline(0, vk::CullModeFlagBits::eBack, false);
}
// Prepare a new framebuffer and attachments for offscreen rendering (G-Buffer)
void HPPHDR::prepare_offscreen_buffer()
{
// We need to select a format that supports the color attachment blending flag, so we iterate over multiple formats to find one that supports this flag
const std::vector<vk::Format> float_format_priority_list = {
vk::Format::eR32G32B32A32Sfloat,
vk::Format::eR16G16B16A16Sfloat // Guaranteed blend support for this
};
vk::Format color_format = vkb::common::choose_blendable_format(get_device().get_gpu().get_handle(), float_format_priority_list);
{
offscreen.extent = extent;
// Color attachments
// We are using two 128-Bit RGBA floating point color buffers for this sample
// In a performance or bandwidth-limited scenario you should consider using a format with lower precision
offscreen.color[0] = create_attachment(color_format, vk::ImageUsageFlagBits::eColorAttachment);
offscreen.color[1] = create_attachment(color_format, vk::ImageUsageFlagBits::eColorAttachment);
// Depth attachment
offscreen.depth = create_attachment(depth_format, vk::ImageUsageFlagBits::eDepthStencilAttachment);
offscreen.render_pass = create_offscreen_render_pass();
offscreen.framebuffer = vkb::common::create_framebuffer(
get_device().get_handle(), offscreen.render_pass, {offscreen.color[0].view, offscreen.color[1].view, offscreen.depth.view}, offscreen.extent);
// Create sampler to sample from the color attachments
offscreen.sampler = vkb::common::create_sampler(get_device().get_gpu().get_handle(), get_device().get_handle(), color_format,
vk::Filter::eNearest, vk::SamplerAddressMode::eClampToEdge, 1.0f, 1.0f);
}
// Bloom separable filter pass
{
filter_pass.extent = extent;
// Color attachments
// Floating point color attachment
filter_pass.color = create_attachment(color_format, vk::ImageUsageFlagBits::eColorAttachment);
filter_pass.render_pass = create_filter_render_pass();
filter_pass.framebuffer = vkb::common::create_framebuffer(get_device().get_handle(), filter_pass.render_pass, {filter_pass.color.view}, filter_pass.extent);
filter_pass.sampler = vkb::common::create_sampler(get_device().get_gpu().get_handle(), get_device().get_handle(),
color_format, vk::Filter::eNearest, vk::SamplerAddressMode::eClampToEdge, 1.0f, 1.0f);
}
}
// Prepare and initialize uniform buffer containing shader uniforms
void HPPHDR::prepare_uniform_buffers()
{
// Matrices vertex shader uniform buffer
uniform_buffers.matrices = std::make_unique<vkb::core::BufferCpp>(get_device(),
sizeof(ubo_matrices),
vk::BufferUsageFlagBits::eUniformBuffer,
VMA_MEMORY_USAGE_CPU_TO_GPU);
// Params
uniform_buffers.params = std::make_unique<vkb::core::BufferCpp>(get_device(),
sizeof(ubo_params),
vk::BufferUsageFlagBits::eUniformBuffer,
VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
update_params();
}
void HPPHDR::update_composition_descriptor_set()
{
std::array<vk::DescriptorImageInfo, 2> color_descriptors = {{{offscreen.sampler, offscreen.color[0].view, vk::ImageLayout::eShaderReadOnlyOptimal},
{offscreen.sampler, filter_pass.color.view, vk::ImageLayout::eShaderReadOnlyOptimal}}};
std::array<vk::WriteDescriptorSet, 2> sampler_write_descriptor_sets = {{{.dstSet = composition.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &color_descriptors[0]},
{.dstSet = composition.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &color_descriptors[1]}}};
get_device().get_handle().updateDescriptorSets(sampler_write_descriptor_sets, {});
}
void HPPHDR::update_bloom_descriptor_set()
{
std::array<vk::DescriptorImageInfo, 2> color_descriptors = {{{offscreen.sampler, offscreen.color[0].view, vk::ImageLayout::eShaderReadOnlyOptimal},
{offscreen.sampler, offscreen.color[1].view, vk::ImageLayout::eShaderReadOnlyOptimal}}};
std::array<vk::WriteDescriptorSet, 2> sampler_write_descriptor_sets = {{{.dstSet = bloom.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &color_descriptors[0]},
{.dstSet = bloom.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &color_descriptors[1]}}};
get_device().get_handle().updateDescriptorSets(sampler_write_descriptor_sets, {});
}
void HPPHDR::update_model_descriptor_set(vk::DescriptorSet descriptor_set)
{
vk::DescriptorBufferInfo matrix_buffer_descriptor{uniform_buffers.matrices->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo environment_image_descriptor{textures.envmap.sampler,
textures.envmap.image->get_vk_image_view().get_handle(),
descriptor_type_to_image_layout(vk::DescriptorType::eCombinedImageSampler,
textures.envmap.image->get_vk_image_view().get_format())};
vk::DescriptorBufferInfo params_buffer_descriptor{uniform_buffers.params->get_handle(), 0, vk::WholeSize};
std::array<vk::WriteDescriptorSet, 3> write_descriptor_sets = {{{.dstSet = descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &matrix_buffer_descriptor},
{.dstSet = descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &environment_image_descriptor},
{.dstSet = descriptor_set,
.dstBinding = 2,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &params_buffer_descriptor}}};
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, {});
}
void HPPHDR::update_params()
{
uniform_buffers.params->convert_and_update(ubo_params);
}
void HPPHDR::update_uniform_buffers()
{
ubo_matrices.projection = camera.matrices.perspective;
ubo_matrices.modelview = camera.matrices.view * models.transforms[models.object_index];
ubo_matrices.skybox_modelview = camera.matrices.view;
ubo_matrices.inverse_modelview = glm::inverse(camera.matrices.view);
uniform_buffers.matrices->convert_and_update(ubo_matrices);
}
std::unique_ptr<vkb::Application> create_hpp_hdr()
{
return std::make_unique<HPPHDR>();
}
-232
View File
@@ -1,232 +0,0 @@
/* 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.
*/
/*
* High dynamic range rendering, using vulkan.hpp
*/
#pragma once
#include <hpp_api_vulkan_sample.h>
class HPPHDR : public HPPApiVulkanSample
{
public:
HPPHDR();
~HPPHDR();
private:
struct Bloom
{
bool enabled = true;
vk::DescriptorSetLayout descriptor_set_layout = {};
vk::DescriptorSet descriptor_set;
vk::PipelineLayout pipeline_layout;
vk::Pipeline pipelines[2];
void destroy(vk::Device device)
{
device.destroyPipeline(pipelines[0]);
device.destroyPipeline(pipelines[1]);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
// no need to free the descriptor_set, as it's implicitly free'd with the descriptor_pool
}
};
struct Composition
{
vk::DescriptorSetLayout descriptor_set_layout = {};
vk::DescriptorSet descriptor_set = {};
vk::PipelineLayout pipeline_layout = {};
vk::Pipeline pipeline = {};
void destroy(vk::Device device)
{
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
// no need to free the descriptor_set, as it's implicitly free'd with the descriptor_pool
}
};
// Framebuffer for offscreen rendering
struct FramebufferAttachment
{
vk::Format format = {};
vk::Image image = {};
vk::DeviceMemory mem = {};
vk::ImageView view = {};
void destroy(vk::Device device)
{
device.destroyImageView(view);
device.destroyImage(image);
device.freeMemory(mem);
}
};
struct FilterPass
{
vk::Extent2D extent = {};
vk::Framebuffer framebuffer = {};
FramebufferAttachment color = {};
vk::RenderPass render_pass = {};
vk::Sampler sampler = {};
void destroy(vk::Device device)
{
device.destroySampler(sampler);
device.destroyFramebuffer(framebuffer);
device.destroyRenderPass(render_pass);
color.destroy(device);
}
};
struct Geometry
{
vk::DescriptorSet descriptor_set = {};
vk::Pipeline pipeline = {};
std::vector<std::unique_ptr<vkb::scene_graph::components::HPPSubMesh>> meshes = {};
void destroy(vk::Device device, vk::DescriptorPool descriptor_pool)
{
// no need to free the descriptor_set, as it's implicitly free'd with the descriptor_pool
device.destroyPipeline(pipeline);
}
};
struct Models
{
vk::DescriptorSetLayout descriptor_set_layout = {};
vk::PipelineLayout pipeline_layout = {};
Geometry objects = {};
Geometry skybox = {};
std::vector<glm::mat4> transforms = {};
int32_t object_index = 0;
void destroy(vk::Device device, vk::DescriptorPool descriptor_pool)
{
objects.destroy(device, descriptor_pool);
skybox.destroy(device, descriptor_pool);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
}
};
struct Offscreen
{
vk::Extent2D extent = {};
vk::Framebuffer framebuffer = {};
FramebufferAttachment color[2] = {};
FramebufferAttachment depth = {};
vk::RenderPass render_pass = {};
vk::Sampler sampler = {};
void destroy(vk::Device device)
{
device.destroySampler(sampler);
device.destroyFramebuffer(framebuffer);
device.destroyRenderPass(render_pass);
color[0].destroy(device);
color[1].destroy(device);
depth.destroy(device);
}
};
struct Textures
{
HPPTexture envmap;
void destroy(vk::Device device)
{
device.destroySampler(envmap.sampler);
}
};
struct UBOMatrices
{
glm::mat4 projection;
glm::mat4 modelview;
glm::mat4 skybox_modelview;
glm::mat4 inverse_modelview;
float modelscale = 0.05f;
};
struct UBOParams
{
float exposure = 1.0f;
};
struct UniformBuffers
{
std::unique_ptr<vkb::core::BufferCpp> matrices;
std::unique_ptr<vkb::core::BufferCpp> params;
};
private:
// from vkb::Application
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual bool resize(const uint32_t width, const uint32_t height) override;
// from vkb::VulkanSample
virtual void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
virtual void build_command_buffers() override;
virtual void on_update_ui_overlay(vkb::Drawer &drawer) override;
virtual void render(float delta_time) override;
vk::DeviceMemory allocate_memory(vk::Image image);
FramebufferAttachment create_attachment(vk::Format format, vk::ImageUsageFlagBits usage);
vk::DescriptorPool create_descriptor_pool();
vk::Pipeline create_bloom_pipeline(uint32_t direction);
vk::Pipeline create_composition_pipeline();
vk::RenderPass create_filter_render_pass();
vk::Image create_image(vk::Format format, vk::ImageUsageFlagBits usage);
vk::Pipeline create_models_pipeline(uint32_t shaderType, vk::CullModeFlagBits cullMode, bool depthTestAndWrite);
vk::RenderPass create_offscreen_render_pass();
vk::RenderPass create_render_pass(std::vector<vk::AttachmentDescription> const &attachment_descriptions, vk::SubpassDescription const &subpass_description);
void draw();
void load_assets();
void prepare_bloom();
void prepare_camera();
void prepare_composition();
void prepare_models();
void prepare_offscreen_buffer();
void prepare_uniform_buffers();
void update_composition_descriptor_set();
void update_bloom_descriptor_set();
void update_model_descriptor_set(vk::DescriptorSet descriptor_set);
void update_params();
void update_uniform_buffers();
private:
Bloom bloom;
Composition composition;
bool display_skybox = true;
FilterPass filter_pass;
Models models;
std::vector<std::string> object_names;
Offscreen offscreen;
Textures textures;
UBOMatrices ubo_matrices;
UBOParams ubo_params;
UniformBuffers uniform_buffers;
};
std::unique_ptr<vkb::Application> create_hpp_hdr();
@@ -1,40 +0,0 @@
# Copyright (c) 2021-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.
#
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 Hello Triangle"
DESCRIPTION "An introduction into Vulkan and its respective objects using vulkan.hpp."
SHADER_FILES_GLSL
"hello_triangle/glsl/triangle.vert"
"hello_triangle/glsl/triangle.frag"
SHADER_FILES_HLSL
"hello_triangle/hlsl/triangle.vert.hlsl"
"hello_triangle/hlsl/triangle.frag.hlsl"
SHADER_FILES_SLANG
"hello_triangle/slang/triangle.vert.slang"
"hello_triangle/slang/triangle.frag.slang")
if(${VKB_${FOLDER_NAME}})
target_compile_definitions(${FOLDER_NAME} PUBLIC VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
endif()
@@ -1,27 +0,0 @@
////
- Copyright (c) 2019-2023, 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.
-
////
:pp: {plus}{plus}
= HPP Hello Triangle
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hpp_hello_triangle[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hello_triangle[Hello Triangle] that illustrates the usage of the C{pp} bindings of Vulkan provided by vulkan.hpp.
@@ -1,968 +0,0 @@
/* Copyright (c) 2021-2025, NVIDIA CORPORATION. All rights reserved.
* 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 "hpp_hello_triangle.h"
#include <common/hpp_error.h>
#include <common/hpp_vk_common.h>
#include <core/util/logging.hpp>
#include <filesystem/legacy.h>
#include <platform/window.h>
// Note: the default dispatcher is instantiated in hpp_api_vulkan_sample.cpp.
// Even though, that file is not part of this sample, it's part of the sample-project!
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
/// @brief A debug callback called from Vulkan validation layers.
VKAPI_ATTR vk::Bool32 VKAPI_CALL debug_utils_messenger_callback(vk::DebugUtilsMessageSeverityFlagBitsEXT message_severity,
vk::DebugUtilsMessageTypeFlagsEXT message_type,
const vk::DebugUtilsMessengerCallbackDataEXT *callback_data,
void *user_data)
{
// Log debug message
if (message_severity & vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning)
{
LOGW("{} - {}: {}", callback_data->messageIdNumber, callback_data->pMessageIdName, callback_data->pMessage);
}
else if (message_severity & vk::DebugUtilsMessageSeverityFlagBitsEXT::eError)
{
LOGE("{} - {}: {}", callback_data->messageIdNumber, callback_data->pMessageIdName, callback_data->pMessage);
}
return VK_FALSE;
}
#endif
/**
* @brief Validates a list of required extensions, comparing it with the available ones.
*
* @param required A vector containing required extension names.
* @param available A vk::ExtensionProperties object containing available extensions.
* @return true if all required extensions are available
* @return false otherwise
*/
bool validate_extensions(const std::vector<const char *> &required,
const std::vector<vk::ExtensionProperties> &available)
{
// inner find_if gives true if the extension was not found
// outer find_if gives true if none of the extensions were not found, that is if all extensions were found
return std::ranges::find_if(required,
[&available](auto extension) {
return std::ranges::find_if(available,
[&extension](auto const &ep) {
return strcmp(ep.extensionName, extension) == 0;
}) == available.end();
}) == required.end();
}
HPPHelloTriangle::HPPHelloTriangle()
{
}
HPPHelloTriangle::~HPPHelloTriangle()
{
// Don't release anything until the GPU is completely idle.
device.waitIdle();
teardown_framebuffers();
for (auto &pfd : per_frame_data)
{
teardown_per_frame(pfd);
}
per_frame_data.clear();
for (auto semaphore : recycled_semaphores)
{
device.destroySemaphore(semaphore);
}
if (pipeline)
{
device.destroyPipeline(pipeline);
}
if (pipeline_layout)
{
device.destroyPipelineLayout(pipeline_layout);
}
if (render_pass)
{
device.destroyRenderPass(render_pass);
}
for (auto image_view : swapchain_data.image_views)
{
device.destroyImageView(image_view);
}
if (swapchain_data.swapchain)
{
device.destroySwapchainKHR(swapchain_data.swapchain);
}
if (surface)
{
instance.destroySurfaceKHR(surface);
}
if (vertex_buffer_allocation != VK_NULL_HANDLE)
{
vmaDestroyBuffer(vma_allocator, vertex_buffer, vertex_buffer_allocation);
}
if (vma_allocator != VK_NULL_HANDLE)
{
vmaDestroyAllocator(vma_allocator);
}
if (device)
{
device.destroy();
}
if (debug_utils_messenger)
{
instance.destroyDebugUtilsMessengerEXT(debug_utils_messenger);
}
instance.destroy();
}
bool HPPHelloTriangle::prepare(const vkb::ApplicationOptions &options)
{
// Headless is not supported to keep this sample as simple as possible
assert(options.window != nullptr);
assert(options.window->get_window_mode() != vkb::Window::Mode::Headless);
if (Application::prepare(options))
{
instance = create_instance({VK_KHR_SURFACE_EXTENSION_NAME}, {});
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
debug_utils_messenger = instance.createDebugUtilsMessengerEXT(debug_utils_create_info);
#endif
select_physical_device_and_surface();
const vkb::Window::Extent &extent = options.window->get_extent();
swapchain_data.extent.width = extent.width;
swapchain_data.extent.height = extent.height;
// create a device
device = create_device({VK_KHR_SWAPCHAIN_EXTENSION_NAME});
// get the (graphics) queue
queue = device.getQueue(graphics_queue_index, 0);
vma_allocator = create_vma_allocator();
std::tie(vertex_buffer, vertex_buffer_allocation) = create_vertex_buffer();
init_swapchain();
// Create the necessary objects for rendering.
render_pass = create_render_pass();
// Create a blank pipeline layout.
// We are not binding any resources to the pipeline in this first sample.
pipeline_layout = device.createPipelineLayout({});
pipeline = create_graphics_pipeline();
init_framebuffers();
}
return true;
}
void HPPHelloTriangle::update(float delta_time)
{
vk::Result res;
uint32_t index;
std::tie(res, index) = acquire_next_image();
// Handle outdated error in acquire.
if (res == vk::Result::eSuboptimalKHR || res == vk::Result::eErrorOutOfDateKHR)
{
resize(swapchain_data.extent.width, swapchain_data.extent.height);
std::tie(res, index) = acquire_next_image();
}
if (res != vk::Result::eSuccess)
{
queue.waitIdle();
return;
}
render_triangle(index);
// Present swapchain image
vk::PresentInfoKHR present_info{.waitSemaphoreCount = 1,
.pWaitSemaphores = &per_frame_data[index].swapchain_release_semaphore,
.swapchainCount = 1,
.pSwapchains = &swapchain_data.swapchain,
.pImageIndices = &index};
res = queue.presentKHR(present_info);
// Handle Outdated error in present.
if (res == vk::Result::eSuboptimalKHR || res == vk::Result::eErrorOutOfDateKHR)
{
resize(swapchain_data.extent.width, swapchain_data.extent.height);
}
else if (res != vk::Result::eSuccess)
{
LOGE("Failed to present swapchain image.");
}
}
bool HPPHelloTriangle::resize(const uint32_t, const uint32_t)
{
if (!device)
{
return false;
}
vk::SurfaceCapabilitiesKHR surface_properties = gpu.getSurfaceCapabilitiesKHR(surface);
// Only rebuild the swapchain if the dimensions have changed
if (surface_properties.currentExtent == swapchain_data.extent)
{
return false;
}
device.waitIdle();
teardown_framebuffers();
init_swapchain();
init_framebuffers();
return true;
}
/**
* @brief Acquires an image from the swapchain.
* @param[out] image The swapchain index for the acquired image.
* @returns Vulkan result code
*/
std::pair<vk::Result, uint32_t> HPPHelloTriangle::acquire_next_image()
{
vk::Semaphore acquire_semaphore;
if (recycled_semaphores.empty())
{
acquire_semaphore = device.createSemaphore({});
}
else
{
acquire_semaphore = recycled_semaphores.back();
recycled_semaphores.pop_back();
}
vk::Result res;
uint32_t image;
std::tie(res, image) = device.acquireNextImageKHR(swapchain_data.swapchain, UINT64_MAX, acquire_semaphore);
if (res != vk::Result::eSuccess)
{
recycled_semaphores.push_back(acquire_semaphore);
return {res, image};
}
// If we have outstanding fences for this swapchain image, wait for them to complete first.
// After begin frame returns, it is safe to reuse or delete resources which
// were used previously.
//
// We wait for fences which completes N frames earlier, so we do not stall,
// waiting for all GPU work to complete before this returns.
// Normally, this doesn't really block at all,
// since we're waiting for old frames to have been completed, but just in case.
if (per_frame_data[image].queue_submit_fence)
{
(void) device.waitForFences(per_frame_data[image].queue_submit_fence, true, UINT64_MAX);
device.resetFences(per_frame_data[image].queue_submit_fence);
}
if (per_frame_data[image].primary_command_pool)
{
device.resetCommandPool(per_frame_data[image].primary_command_pool);
}
// Recycle the old semaphore back into the semaphore manager.
vk::Semaphore old_semaphore = per_frame_data[image].swapchain_acquire_semaphore;
if (old_semaphore)
{
recycled_semaphores.push_back(old_semaphore);
}
per_frame_data[image].swapchain_acquire_semaphore = acquire_semaphore;
return {vk::Result::eSuccess, image};
}
vk::Device HPPHelloTriangle::create_device(const std::vector<const char *> &required_device_extensions)
{
std::vector<vk::ExtensionProperties> device_extensions = gpu.enumerateDeviceExtensionProperties();
if (!validate_extensions(required_device_extensions, device_extensions))
{
throw std::runtime_error("Required device extensions are missing, will try without.");
}
std::vector<const char *> active_device_extensions(required_device_extensions);
#if (defined(VKB_ENABLE_PORTABILITY))
// VK_KHR_portability_subset must be enabled if present in the implementation (e.g on macOS/iOS with beta extensions enabled)
if (std::ranges::any_of(device_extensions,
[](vk::ExtensionProperties const &extension) { return strcmp(extension.extensionName, VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME) == 0; }))
{
active_device_extensions.push_back(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME);
}
#endif
// Create a device with one queue
float queue_priority = 1.0f;
vk::DeviceQueueCreateInfo queue_info{.queueFamilyIndex = graphics_queue_index, .queueCount = 1, .pQueuePriorities = &queue_priority};
vk::DeviceCreateInfo device_info{.queueCreateInfoCount = 1,
.pQueueCreateInfos = &queue_info,
.enabledExtensionCount = static_cast<uint32_t>(active_device_extensions.size()),
.ppEnabledExtensionNames = active_device_extensions.data()};
vk::Device device = gpu.createDevice(device_info);
// initialize function pointers for device
VULKAN_HPP_DEFAULT_DISPATCHER.init(device);
return device;
}
vk::Pipeline HPPHelloTriangle::create_graphics_pipeline()
{
// Load our SPIR-V shaders.
// Samples support different shading languages, all of which are offline compiled to SPIR-V, the shader format that Vulkan uses.
// The shading language to load for can be selected via command line
std::string shader_folder{""};
switch (get_shading_language())
{
case vkb::ShadingLanguage::HLSL:
shader_folder = "hlsl";
break;
case vkb::ShadingLanguage::SLANG:
shader_folder = "slang";
break;
default:
shader_folder = "glsl";
}
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages{
{.stage = vk::ShaderStageFlagBits::eVertex, .module = create_shader_module("hello_triangle/" + shader_folder + "/triangle.vert.spv"), .pName = "main"},
{.stage = vk::ShaderStageFlagBits::eFragment, .module = create_shader_module("hello_triangle/" + shader_folder + "/triangle.frag.spv"), .pName = "main"}};
// Define the vertex input binding.
vk::VertexInputBindingDescription binding_description{.binding = 0, .stride = sizeof(Vertex), .inputRate = vk::VertexInputRate::eVertex};
// Define the vertex input attribute.
std::array<vk::VertexInputAttributeDescription, 2> attribute_descriptions{
{{.location = 0, .binding = 0, .format = vk::Format::eR32G32B32Sfloat, .offset = offsetof(Vertex, position)},
{.location = 1, .binding = 0, .format = vk::Format::eR32G32B32Sfloat, .offset = offsetof(Vertex, color)}}};
// Define the pipeline vertex input.
vk::PipelineVertexInputStateCreateInfo vertex_input{
.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &binding_description,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(attribute_descriptions.size()),
.pVertexAttributeDescriptions = attribute_descriptions.data()};
// Our attachment will write to all color channels, but no blending is enabled.
vk::PipelineColorBlendAttachmentState blend_attachment{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Disable all depth testing.
vk::PipelineDepthStencilStateCreateInfo depth_stencil;
vk::Pipeline pipeline = vkb::common::create_graphics_pipeline(device,
nullptr,
shader_stages,
vertex_input,
vk::PrimitiveTopology::eTriangleList, // We will use triangle lists to draw geometry.
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eBack,
vk::FrontFace::eClockwise,
{blend_attachment},
depth_stencil,
pipeline_layout, // We need to specify the pipeline layout
render_pass); // and the render pass up front as well
// Pipeline is baked, we can delete the shader modules now.
device.destroyShaderModule(shader_stages[0].module);
device.destroyShaderModule(shader_stages[1].module);
return pipeline;
}
vk::ImageView HPPHelloTriangle::create_image_view(vk::Image image)
{
vk::ImageViewCreateInfo image_view_create_info{
.image = image,
.viewType = vk::ImageViewType::e2D,
.format = swapchain_data.format,
.components = {.r = vk::ComponentSwizzle::eR, .g = vk::ComponentSwizzle::eG, .b = vk::ComponentSwizzle::eB, .a = vk::ComponentSwizzle::eA},
.subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1}};
return device.createImageView(image_view_create_info);
}
vk::Instance HPPHelloTriangle::create_instance(std::vector<const char *> const &required_instance_extensions, std::vector<const char *> const &required_validation_layers)
{
#if defined(_HPP_VULKAN_LIBRARY)
static vk::detail::DynamicLoader dl(_HPP_VULKAN_LIBRARY);
#else
static vk::detail::DynamicLoader dl;
#endif
PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = dl.getProcAddress<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr");
VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr);
std::vector<vk::ExtensionProperties> available_instance_extensions = vk::enumerateInstanceExtensionProperties();
std::vector<const char *> active_instance_extensions(required_instance_extensions);
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
active_instance_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
#endif
#if (defined(VKB_ENABLE_PORTABILITY))
active_instance_extensions.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
bool portability_enumeration_available = false;
if (std::ranges::any_of(available_instance_extensions,
[](vk::ExtensionProperties const &extension) { return strcmp(extension.extensionName, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) == 0; }))
{
active_instance_extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
portability_enumeration_available = true;
}
#endif
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
active_instance_extensions.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_WIN32_KHR)
active_instance_extensions.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_METAL_EXT)
active_instance_extensions.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_XCB_KHR)
active_instance_extensions.push_back(VK_KHR_XCB_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_XLIB_KHR)
active_instance_extensions.push_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_WAYLAND_KHR)
active_instance_extensions.push_back(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_DISPLAY_KHR)
active_instance_extensions.push_back(VK_KHR_DISPLAY_EXTENSION_NAME);
#else
# pragma error Platform not supported
#endif
if (!validate_extensions(active_instance_extensions, available_instance_extensions))
{
throw std::runtime_error("Required instance extensions are missing.");
}
std::vector<const char *> requested_instance_layers(required_validation_layers);
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
char const *validationLayer = "VK_LAYER_KHRONOS_validation";
std::vector<vk::LayerProperties> supported_instance_layers = vk::enumerateInstanceLayerProperties();
if (std::ranges::any_of(supported_instance_layers, [&validationLayer](auto const &lp) { return strcmp(lp.layerName, validationLayer) == 0; }))
{
requested_instance_layers.push_back(validationLayer);
LOGI("Enabled Validation Layer {}", validationLayer);
}
else
{
LOGW("Validation Layer {} is not available", validationLayer);
}
#endif
vk::ApplicationInfo app{.pApplicationName = "HPP Hello Triangle", .pEngineName = "Vulkan Samples", .apiVersion = VK_API_VERSION_1_1};
vk::InstanceCreateInfo instance_info{.pApplicationInfo = &app,
.enabledLayerCount = static_cast<uint32_t>(requested_instance_layers.size()),
.ppEnabledLayerNames = requested_instance_layers.data(),
.enabledExtensionCount = static_cast<uint32_t>(active_instance_extensions.size()),
.ppEnabledExtensionNames = active_instance_extensions.data()};
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
debug_utils_create_info =
vk::DebugUtilsMessengerCreateInfoEXT{.messageSeverity =
vk::DebugUtilsMessageSeverityFlagBitsEXT::eError | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning,
.messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance,
.pfnUserCallback = debug_utils_messenger_callback};
instance_info.pNext = &debug_utils_create_info;
#endif
#if (defined(VKB_ENABLE_PORTABILITY))
if (portability_enumeration_available)
{
instance_info.flags |= vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR;
}
#endif
// Create the Vulkan instance
vk::Instance instance = vk::createInstance(instance_info);
// initialize function pointers for instance
VULKAN_HPP_DEFAULT_DISPATCHER.init(instance);
#if defined(VK_USE_PLATFORM_DISPLAY_KHR) || defined(VK_USE_PLATFORM_ANDROID_KHR) || defined(VK_USE_PLATFORM_METAL_EXT)
// we need some additional initializing for this platform!
if (volkInitialize())
{
throw std::runtime_error("Failed to initialize volk.");
}
volkLoadInstance(instance);
#endif
return instance;
}
vk::RenderPass HPPHelloTriangle::create_render_pass()
{
vk::AttachmentDescription attachment{
.format = swapchain_data.format, // Backbuffer format.
.samples = vk::SampleCountFlagBits::e1, // Not multisampled.
.loadOp = vk::AttachmentLoadOp::eClear, // When starting the frame, we want tiles to be cleared.
.storeOp = vk::AttachmentStoreOp::eStore, // When ending the frame, we want tiles to be written out.
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare, // Don't care about stencil since we're not using it.
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare, // Don't care about stencil since we're not using it.
.initialLayout = vk::ImageLayout::eUndefined, // The image layout will be undefined when the render pass begins.
.finalLayout = vk::ImageLayout::ePresentSrcKHR // After the render pass is complete, we will transition to PRESENT_SRC_KHR layout.
};
// We have one subpass. This subpass has one color attachment.
// While executing this subpass, the attachment will be in attachment optimal layout.
vk::AttachmentReference color_ref{.attachment = 0, .layout = vk::ImageLayout::eColorAttachmentOptimal};
// We will end up with two transitions.
// The first one happens right before we start subpass #0, where
// eUndefined is transitioned into eColorAttachmentOptimal.
// The final layout in the render pass attachment states ePresentSrcKHR, so we
// will get a final transition from eColorAttachmentOptimal to ePresetSrcKHR.
vk::SubpassDescription subpass{.pipelineBindPoint = vk::PipelineBindPoint::eGraphics, .colorAttachmentCount = 1, .pColorAttachments = &color_ref};
// Create a dependency to external events.
// We need to wait for the WSI semaphore to signal.
// Only pipeline stages which depend on eColorAttachmentOutput will
// actually wait for the semaphore, so we must also wait for that pipeline stage.
vk::SubpassDependency dependency{.srcSubpass = vk::SubpassExternal,
.dstSubpass = 0,
.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
// Since we changed the image layout, we need to make the memory visible to color attachment to modify.
.srcAccessMask = {},
.dstAccessMask = vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite};
// Finally, create the renderpass.
vk::RenderPassCreateInfo rp_info{
.attachmentCount = 1, .pAttachments = &attachment, .subpassCount = 1, .pSubpasses = &subpass, .dependencyCount = 1, .pDependencies = &dependency};
return device.createRenderPass(rp_info);
}
/**
* @brief Helper function to load a shader module from an offline-compiled SPIR-V file.
* @param path The path for the shader (relative to the assets directory).
* @returns A vk::ShaderModule handle. Aborts execution if shader creation fails.
*/
vk::ShaderModule HPPHelloTriangle::create_shader_module(std::string const &path)
{
auto spirv = vkb::fs::read_shader_binary_u32(path);
vk::ShaderModuleCreateInfo shader_module_create_info{.codeSize = spirv.size() * sizeof(uint32_t), .pCode = spirv.data()};
return device.createShaderModule(shader_module_create_info);
}
vk::SwapchainKHR
HPPHelloTriangle::create_swapchain(vk::Extent2D const &swapchain_extent, vk::SurfaceFormatKHR surface_format, vk::SwapchainKHR old_swapchain)
{
vk::SurfaceCapabilitiesKHR surface_properties = gpu.getSurfaceCapabilitiesKHR(surface);
// Determine the number of vk::Image's to use in the swapchain.
// Ideally, we desire to own 1 image at a time, the rest of the images can
// either be rendered to and/or being queued up for display.
uint32_t desired_swapchain_images = surface_properties.minImageCount + 1;
if ((surface_properties.maxImageCount > 0) && (desired_swapchain_images > surface_properties.maxImageCount))
{
// Application must settle for fewer images than desired.
desired_swapchain_images = surface_properties.maxImageCount;
}
// Figure out a suitable surface transform.
vk::SurfaceTransformFlagBitsKHR pre_transform =
(surface_properties.supportedTransforms & vk::SurfaceTransformFlagBitsKHR::eIdentity) ? vk::SurfaceTransformFlagBitsKHR::eIdentity : surface_properties.currentTransform;
// Find a supported composite type.
vk::CompositeAlphaFlagBitsKHR composite = vk::CompositeAlphaFlagBitsKHR::eOpaque;
if (surface_properties.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::eOpaque)
{
composite = vk::CompositeAlphaFlagBitsKHR::eOpaque;
}
else if (surface_properties.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::eInherit)
{
composite = vk::CompositeAlphaFlagBitsKHR::eInherit;
}
else if (surface_properties.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::ePreMultiplied)
{
composite = vk::CompositeAlphaFlagBitsKHR::ePreMultiplied;
}
else if (surface_properties.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::ePostMultiplied)
{
composite = vk::CompositeAlphaFlagBitsKHR::ePostMultiplied;
}
// FIFO must be supported by all implementations.
vk::PresentModeKHR swapchain_present_mode = vk::PresentModeKHR::eFifo;
vk::SwapchainCreateInfoKHR swapchain_create_info{
.surface = surface,
.minImageCount = desired_swapchain_images,
.imageFormat = surface_format.format,
.imageColorSpace = surface_format.colorSpace,
.imageExtent = swapchain_extent,
.imageArrayLayers = 1,
.imageUsage = vk::ImageUsageFlagBits::eColorAttachment,
.imageSharingMode = vk::SharingMode::eExclusive,
.preTransform = pre_transform,
.compositeAlpha = composite,
.presentMode = swapchain_present_mode,
.clipped = true,
.oldSwapchain = old_swapchain};
return device.createSwapchainKHR(swapchain_create_info);
}
std::pair<vk::Buffer, VmaAllocation> HPPHelloTriangle::create_vertex_buffer()
{
// Vertex data for a single colored triangle
const std::vector<Vertex> vertices = {
{{0.5f, -0.5f, 0.5f}, {1.0f, 0.0f, 0.0f}},
{{0.5f, 0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}},
{{-0.5f, 0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}};
const vk::DeviceSize buffer_size = sizeof(vertices[0]) * vertices.size();
// Copy Vertex data to a buffer accessible by the device
vk::BufferCreateInfo buffer_create_info{.size = buffer_size, .usage = vk::BufferUsageFlagBits::eVertexBuffer};
// We use the Vulkan Memory Allocator to find a memory type that can be written and mapped from the host
// On most setups this will return a memory type that resides in VRAM and is accessible from the host
VmaAllocationCreateInfo allocation_create_info{
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,
.usage = VMA_MEMORY_USAGE_AUTO,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT};
vk::Buffer vertex_buffer;
VmaAllocation vertex_buffer_allocation;
VmaAllocationInfo allocation_info{};
vmaCreateBuffer(vma_allocator, reinterpret_cast<VkBufferCreateInfo *>(&buffer_create_info), &allocation_create_info, reinterpret_cast<VkBuffer *>(&vertex_buffer), &vertex_buffer_allocation, &allocation_info);
if (allocation_info.pMappedData)
{
memcpy(allocation_info.pMappedData, vertices.data(), buffer_size);
}
else
{
throw std::runtime_error("Could not map vertex buffer.");
}
return {vertex_buffer, vertex_buffer_allocation};
}
VmaAllocator HPPHelloTriangle::create_vma_allocator()
{
// This sample uses the Vulkan Memory Alloctor (VMA), which needs to be set up
VmaVulkanFunctions vma_vulkan_functions{
.vkGetInstanceProcAddr = VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr,
.vkGetDeviceProcAddr = VULKAN_HPP_DEFAULT_DISPATCHER.vkGetDeviceProcAddr};
VmaAllocatorCreateInfo allocator_info{.physicalDevice = gpu, .device = device, .pVulkanFunctions = &vma_vulkan_functions, .instance = instance};
VmaAllocator allocator;
VkResult result = vmaCreateAllocator(&allocator_info, &allocator);
if (result != VK_SUCCESS)
{
throw std::runtime_error("Could not create allocator for VMA allocator");
}
return allocator;
}
/**
* @brief Initializes the Vulkan framebuffers.
*/
void HPPHelloTriangle::init_framebuffers()
{
assert(swapchain_data.framebuffers.empty());
// Create framebuffer for each swapchain image view
for (auto &image_view : swapchain_data.image_views)
{
// create the framebuffer.
swapchain_data.framebuffers.push_back(vkb::common::create_framebuffer(device, render_pass, {image_view}, swapchain_data.extent));
}
}
/**
* @brief Initializes the Vulkan swapchain.
*/
void HPPHelloTriangle::init_swapchain()
{
vk::SurfaceCapabilitiesKHR surface_properties = gpu.getSurfaceCapabilitiesKHR(surface);
vk::Extent2D swapchain_extent = (surface_properties.currentExtent.width == 0xFFFFFFFF) ? swapchain_data.extent : surface_properties.currentExtent;
vk::SurfaceFormatKHR surface_format = vkb::common::select_surface_format(gpu, surface);
vk::SwapchainKHR old_swapchain = swapchain_data.swapchain;
swapchain_data.swapchain = create_swapchain(swapchain_extent, surface_format, old_swapchain);
if (old_swapchain)
{
for (vk::ImageView image_view : swapchain_data.image_views)
{
device.destroyImageView(image_view);
}
size_t image_count = device.getSwapchainImagesKHR(old_swapchain).size();
for (size_t i = 0; i < image_count; i++)
{
teardown_per_frame(per_frame_data[i]);
}
swapchain_data.image_views.clear();
device.destroySwapchainKHR(old_swapchain);
}
swapchain_data.extent = swapchain_extent;
swapchain_data.format = surface_format.format;
/// The swapchain images.
std::vector<vk::Image> swapchain_images = device.getSwapchainImagesKHR(swapchain_data.swapchain);
size_t image_count = swapchain_images.size();
// Initialize per-frame resources.
// Every swapchain image has its own command pool and fence manager.
// This makes it very easy to keep track of when we can reset command buffers and such.
per_frame_data.clear();
per_frame_data.resize(image_count);
for (size_t frame = 0; frame < image_count; frame++)
{
auto &pfd = per_frame_data[frame];
pfd.queue_submit_fence = device.createFence({.flags = vk::FenceCreateFlagBits::eSignaled});
pfd.primary_command_pool = device.createCommandPool({.flags = vk::CommandPoolCreateFlagBits::eTransient, .queueFamilyIndex = graphics_queue_index});
pfd.primary_command_buffer = vkb::common::allocate_command_buffer(device, pfd.primary_command_pool);
}
for (size_t i = 0; i < image_count; i++)
{
// Create an image view which we can render into.
swapchain_data.image_views.push_back(create_image_view(swapchain_images[i]));
}
}
/**
* @brief Renders a triangle to the specified swapchain image.
* @param swapchain_index The swapchain index for the image being rendered.
*/
void HPPHelloTriangle::render_triangle(uint32_t swapchain_index)
{
// Render to this framebuffer.
vk::Framebuffer framebuffer = swapchain_data.framebuffers[swapchain_index];
// Allocate or re-use a primary command buffer.
vk::CommandBuffer cmd = per_frame_data[swapchain_index].primary_command_buffer;
// We will only submit this once before it's recycled.
vk::CommandBufferBeginInfo begin_info{.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit};
// Begin command recording
cmd.begin(begin_info);
// Set clear color values.
vk::ClearValue clear_value;
clear_value.color = vk::ClearColorValue(std::array<float, 4>({{0.01f, 0.01f, 0.033f, 1.0f}}));
// Begin the render pass.
vk::RenderPassBeginInfo rp_begin{.renderPass = render_pass,
.framebuffer = framebuffer,
.renderArea = {{0, 0}, {swapchain_data.extent.width, swapchain_data.extent.height}},
.clearValueCount = 1,
.pClearValues = &clear_value};
// We will add draw commands in the same command buffer.
cmd.beginRenderPass(rp_begin, vk::SubpassContents::eInline);
// Bind the graphics pipeline.
cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
vk::Viewport vp{0.0f, 0.0f, static_cast<float>(swapchain_data.extent.width), static_cast<float>(swapchain_data.extent.height), 0.0f, 1.0f};
// Set viewport dynamically
cmd.setViewport(0, vp);
vk::Rect2D scissor{{0, 0}, {swapchain_data.extent.width, swapchain_data.extent.height}};
// Set scissor dynamically
cmd.setScissor(0, scissor);
// Bind the vertex buffer to source the draw calls from.
vk::DeviceSize offset = {0};
cmd.bindVertexBuffers(0, vertex_buffer, offset);
// Draw three vertices with one instance.
cmd.draw(3, 1, 0, 0);
// Complete render pass.
cmd.endRenderPass();
// Complete the command buffer.
cmd.end();
// Submit it to the queue with a release semaphore.
if (!per_frame_data[swapchain_index].swapchain_release_semaphore)
{
per_frame_data[swapchain_index].swapchain_release_semaphore = device.createSemaphore({});
}
vk::PipelineStageFlags wait_stage{VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
vk::SubmitInfo info{.waitSemaphoreCount = 1,
.pWaitSemaphores = &per_frame_data[swapchain_index].swapchain_acquire_semaphore,
.pWaitDstStageMask = &wait_stage,
.commandBufferCount = 1,
.pCommandBuffers = &cmd,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &per_frame_data[swapchain_index].swapchain_release_semaphore};
// Submit command buffer to graphics queue
queue.submit(info, per_frame_data[swapchain_index].queue_submit_fence);
}
/**
* @brief Select a physical device.
*/
void HPPHelloTriangle::select_physical_device_and_surface()
{
std::vector<vk::PhysicalDevice> gpus = instance.enumeratePhysicalDevices();
bool found_graphics_queue_index = false;
for (size_t i = 0; i < gpus.size() && !found_graphics_queue_index; i++)
{
gpu = gpus[i];
std::vector<vk::QueueFamilyProperties> queue_family_properties = gpu.getQueueFamilyProperties();
if (queue_family_properties.empty())
{
throw std::runtime_error("No queue family found.");
}
if (surface)
{
instance.destroySurfaceKHR(surface);
}
surface = static_cast<vk::SurfaceKHR>(window->create_surface(static_cast<VkInstance>(instance), static_cast<VkPhysicalDevice>(gpu)));
if (!surface)
{
throw std::runtime_error("Failed to create window surface.");
}
for (uint32_t j = 0; j < vkb::to_u32(queue_family_properties.size()); j++)
{
vk::Bool32 supports_present = gpu.getSurfaceSupportKHR(j, surface);
// Find a queue family which supports graphics and presentation.
if ((queue_family_properties[j].queueFlags & vk::QueueFlagBits::eGraphics) && supports_present)
{
graphics_queue_index = j;
found_graphics_queue_index = true;
break;
}
}
}
if (!found_graphics_queue_index)
{
LOGE("Did not find suitable queue which supports graphics and presentation.");
}
}
/**
* @brief Tears down the framebuffers. If our swapchain changes, we will call this, and create a new swapchain.
*/
void HPPHelloTriangle::teardown_framebuffers()
{
// Wait until device is idle before teardown.
queue.waitIdle();
for (auto &framebuffer : swapchain_data.framebuffers)
{
device.destroyFramebuffer(framebuffer);
}
swapchain_data.framebuffers.clear();
}
/**
* @brief Tears down the frame data.
* @param per_frame_data The data of a frame.
*/
void HPPHelloTriangle::teardown_per_frame(FrameData &per_frame_data)
{
if (per_frame_data.queue_submit_fence)
{
device.destroyFence(per_frame_data.queue_submit_fence);
per_frame_data.queue_submit_fence = nullptr;
}
if (per_frame_data.primary_command_buffer)
{
device.freeCommandBuffers(per_frame_data.primary_command_pool, per_frame_data.primary_command_buffer);
per_frame_data.primary_command_buffer = nullptr;
}
if (per_frame_data.primary_command_pool)
{
device.destroyCommandPool(per_frame_data.primary_command_pool);
per_frame_data.primary_command_pool = nullptr;
}
if (per_frame_data.swapchain_acquire_semaphore)
{
device.destroySemaphore(per_frame_data.swapchain_acquire_semaphore);
per_frame_data.swapchain_acquire_semaphore = nullptr;
}
if (per_frame_data.swapchain_release_semaphore)
{
device.destroySemaphore(per_frame_data.swapchain_release_semaphore);
per_frame_data.swapchain_release_semaphore = nullptr;
}
}
std::unique_ptr<vkb::Application> create_hpp_hello_triangle()
{
return std::make_unique<HPPHelloTriangle>();
}
@@ -1,111 +0,0 @@
/* Copyright (c) 2021-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.
*/
#pragma once
#include <vulkan/vulkan.hpp>
#include <platform/application.h>
/**
* @brief A self-contained (minimal use of framework) sample that illustrates
* the rendering of a triangle
*/
class HPPHelloTriangle : public vkb::Application
{
/**
* @brief Swapchain data
*/
struct SwapchainData
{
vk::Extent2D extent; // The swapchain extent
vk::Format format = vk::Format::eUndefined; // Pixel format of the swapchain.
vk::SwapchainKHR swapchain; // The swapchain.
std::vector<vk::ImageView> image_views; // The image view for each swapchain image.
std::vector<vk::Framebuffer> framebuffers; // The framebuffer for each swapchain image view.
};
/**
* @brief Per-frame data
*/
struct FrameData
{
vk::Fence queue_submit_fence;
vk::CommandPool primary_command_pool;
vk::CommandBuffer primary_command_buffer;
vk::Semaphore swapchain_acquire_semaphore;
vk::Semaphore swapchain_release_semaphore;
};
/// Properties of the vertices used in this sample.
struct Vertex
{
glm::vec3 position;
glm::vec3 color;
};
public:
HPPHelloTriangle();
virtual ~HPPHelloTriangle();
private:
// from vkb::Application
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual bool resize(const uint32_t width, const uint32_t height) override;
virtual void update(float delta_time) override;
std::pair<vk::Result, uint32_t> acquire_next_image();
vk::Device create_device(const std::vector<const char *> &required_device_extensions);
vk::Pipeline create_graphics_pipeline();
vk::ImageView create_image_view(vk::Image image);
vk::Instance create_instance(std::vector<const char *> const &required_instance_extensions, std::vector<const char *> const &required_validation_layers);
vk::RenderPass create_render_pass();
vk::ShaderModule create_shader_module(std::string const &path);
vk::SwapchainKHR create_swapchain(vk::Extent2D const &swapchain_extent, vk::SurfaceFormatKHR surface_format, vk::SwapchainKHR old_swapchain);
std::pair<vk::Buffer, VmaAllocation> create_vertex_buffer();
VmaAllocator create_vma_allocator();
void init_framebuffers();
void init_swapchain();
void render_triangle(uint32_t swapchain_index);
void select_physical_device_and_surface();
void teardown_framebuffers();
void teardown_per_frame(FrameData &per_frame_data);
private:
vk::Instance instance; // The Vulkan instance.
vk::PhysicalDevice gpu; // The Vulkan physical device.
vk::Device device; // The Vulkan device.
vk::Queue queue; // The Vulkan device queue.
SwapchainData swapchain_data; // The swapchain state.
vk::SurfaceKHR surface; // The surface we will render to.
uint32_t graphics_queue_index; // The queue family index where graphics work will be submitted.
vk::RenderPass render_pass; // The renderpass description.
vk::PipelineLayout pipeline_layout; // The pipeline layout for resources.
vk::Pipeline pipeline; // The graphics pipeline.
vk::DebugUtilsMessengerEXT debug_utils_messenger; // The debug utils messenger.
std::vector<vk::Semaphore> recycled_semaphores; // A set of semaphores that can be reused.
std::vector<FrameData> per_frame_data; // A set of per-frame data.
vk::Buffer vertex_buffer; // The Vulkan buffer object that holds the vertex data for the triangle.
VmaAllocation vertex_buffer_allocation = VK_NULL_HANDLE; // Vulkan Memory Allocator (VMA) allocation info for the vertex buffer.
VmaAllocator vma_allocator; // The VMA allocator for memory management.
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
vk::DebugUtilsMessengerCreateInfoEXT debug_utils_create_info;
#endif
};
std::unique_ptr<vkb::Application> create_hpp_hello_triangle();
@@ -1,41 +0,0 @@
# Copyright (c) 2024-2025, Huawei Technologies Co., Ltd.
#
# 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.
set(CMAKE_CXX_EXTENSIONS OFF)
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 "Huawei Technologies Co., Ltd."
NAME "Vulkan 1.3 HPP Hello Triangle"
DESCRIPTION "An introduction into Vulkan using Vulkan 1.3 using Vulkan-Hpp"
SHADER_FILES_GLSL
"hello_triangle_1_3/glsl/triangle.vert"
"hello_triangle_1_3/glsl/triangle.frag"
SHADER_FILES_HLSL
"hello_triangle_1_3/hlsl/triangle.vert.hlsl"
"hello_triangle_1_3/hlsl/triangle.frag.hlsl"
SHADER_FILES_SLANG
"hello_triangle_1_3/slang/triangle.vert.slang"
"hello_triangle_1_3/slang/triangle.frag.slang")
if(${VKB_${FOLDER_NAME}})
target_compile_definitions(${FOLDER_NAME} PUBLIC VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
endif()
@@ -1,212 +0,0 @@
////
* Copyright (c) 2025, 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.
////
= Hello Triangle with Vulkan 1.3 Features 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/api/hpp_hello_triangle_1_3[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hello_triangle_1_3[Hello Triangle 1.3] that illustrates the usage of the C{pp} bindings of Vulkan provided by Vulkan-Hpp.
This sample demonstrates how to render a simple triangle using Vulkan 1.3 core features. It modernizes the traditional "Hello Triangle" Vulkan sample by incorporating:
- **Dynamic Rendering**
- **Synchronization2**
- **Extended Dynamic State**
- **Vertex Buffers**
## Overview
The sample renders a colored triangle to the screen using Vulkan 1.3. It showcases how to:
- Initialize Vulkan with Vulkan 1.3 features enabled.
- Use dynamic rendering to simplify the rendering pipeline.
- Employ the Synchronization2 API for improved synchronization.
- Utilize extended dynamic states to reduce pipeline complexity.
- Manage vertex data using vertex buffers instead of hard-coded vertices.
## Key Features
### 1. Dynamic Rendering
**What is Dynamic Rendering?**
Dynamic Rendering is a feature introduced in Vulkan 1.3 that allows rendering without pre-defined render passes and framebuffers. It simplifies the rendering process by enabling you to specify rendering states directly during command buffer recording.
**How It's Used in the Sample:**
- **No Render Passes or Framebuffers:** The sample does not create `vk::RenderPass` or `vk::Framebuffer` objects.
- **`vk::CommandBuffer::beginRendering()` and `vk::CommandBuffer::endRendering()`:** These functions are used to begin and end rendering operations dynamically.
- **Pipeline Creation:** Uses `vk::PipelineRenderingCreateInfo` during pipeline creation to specify rendering details.
**Benefits:**
- Simplifies code by reducing boilerplate associated with render passes and framebuffers.
- Increases flexibility by allowing rendering to different attachments without recreating render passes.
### 2. Synchronization2
**What is Synchronization2?**
Synchronization2 is an improved synchronization API introduced in Vulkan 1.3. It provides more granular control over synchronization primitives and simplifies the synchronization process.
**How It's Used in the Sample:**
- **`vk::CommandBuffer::pipelineBarrier2()`:** Replaces the older `vk::CommandBuffer::pipelineBarrier()` for more detailed synchronization.
- **`vk::DependencyInfo` and `vk::ImageMemoryBarrier2`:** Used to specify precise memory dependencies and image layout transitions.
**Example Usage:**
```cpp
vk::ImageMemoryBarrier2 image_barrier = {
// ... members ...
};
vk::DependencyInfo dependency_info = {
.imageMemoryBarrierCount = 1,
.pImageMemoryBarriers = &image_barrier,
};
cmd.pipelineBarrier2(dependency_info);
```
**Benefits:**
- Provides more expressive and flexible synchronization.
- Reduces the potential for synchronization errors.
- Simplifies the specification of pipeline stages and access masks.
### 3. Extended Dynamic State
**What is Extended Dynamic State?**
Extended Dynamic State allows more pipeline states to be set dynamically at command buffer recording time rather than during pipeline creation. This reduces the number of pipeline objects needed.
**How It's Used in the Sample:**
- **Dynamic States Enabled:** The sample enables dynamic states like `vk::DynamicState::eCullMode`, `vk::DynamicState::eFrontFace`, and `vk::DynamicState::ePrimitiveTopology`.
- **Dynamic State Commands:** Uses `vk::CommandBuffer::setCullMode()`, `vk::CommandBuffer::setFrontFace()`, and `vk::CommandBuffer::setPrimitiveTopology()` to set these states dynamically.
**Example Usage:**
```cpp
cmd.setCullMode(vk::DynamicState::eCullMode);
cmd.setFrontFace(vk::FrontFace::eClockwise);
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
```
**Benefits:**
- Reduces the need to create multiple pipelines for different state configurations.
- Enhances flexibility by allowing state changes without pipeline recreation.
### 4. Vertex Buffers
**What Changed?**
Unlike the original sample, which used hard-coded vertices in the shader, this sample uses a vertex buffer to store vertex data.
**How It's Used in the Sample:**
- **Vertex Structure Defined:**
```cpp
struct Vertex {
glm::vec2 position;
glm::vec3 color;
};
```
- **Vertex Data Stored in a Buffer:**
```cpp
std::vector<Vertex> vertices = {
{{0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}}, // Red Vertex
// ... other vertices ...
};
```
- **Buffer Creation and Memory Allocation:**
```cpp
vk::BufferCreateInfo buffer_info = { /* ... */ };
vertex_buffer = device.createBuffer(buffer_info);
vk::MemoryAllocateInfo alloc_info = { /* ... */ };
vertex_buffer_memory = device.allocateMemory(alloc_info);
```
- **Binding the Vertex Buffer:**
```cpp
cmd.bindVertexBuffers(0, vertex_buffer, offset);
```
**Benefits:**
- **Flexibility:** Easier to modify vertex data without changing shaders.
- **Performance:** Potentially better performance due to efficient memory usage.
- **Scalability:** Simplifies rendering more complex geometries.
## How the Sample Works
1. **Initialization:**
- **Instance Creation:** Initializes a Vulkan instance with Vulkan 1.3 API version and required extensions.
- **Device Selection:** Chooses a physical device that supports Vulkan 1.3 and required features.
- **Logical Device Creation:** Creates a logical device with enabled Vulkan 1.3 features like dynamic rendering, synchronization2, and extended dynamic state.
- **Surface and Swapchain Creation:** Sets up the window surface and initializes the swapchain for presenting images.
2. **Vertex Buffer Setup:**
- **Vertex Data Definition:** Defines vertices with positions and colors.
- **Buffer Creation:** Creates a buffer to store vertex data.
- **Memory Allocation:** Allocates memory for the buffer and maps the vertex data into it.
3. **Pipeline Setup:**
- **Shader Modules:** Loads and compiles vertex and fragment shaders.
- **Pipeline Layout:** Creates a pipeline layout (empty in this case as no descriptors are used).
- **Dynamic States Specification:** Specifies which states will be dynamic.
- **Graphics Pipeline Creation:** Creates the graphics pipeline with dynamic rendering info and dynamic states enabled.
4. **Rendering Loop:**
- **Acquire Swapchain Image:** Gets the next available image from the swapchain.
- **Command Buffer Recording:**
- **Begin Rendering:** Uses `vk::CommandBuffer::beginRendering()` with dynamic rendering info.
- **Set Dynamic States:** Sets viewport, scissor, cull mode, front face, and primitive topology dynamically.
- **Bind Pipeline and Vertex Buffer:** Binds the graphics pipeline and the vertex buffer.
- **Draw Call:** Issues a draw call to render the triangle.
- **End Rendering:** Uses `vk::CommandBuffer::endRendering()` to finish rendering.
- **Image Layout Transition:** Transitions the swapchain image layout for presentation using `vk::CommandBuffer::pipelineBarrier2()`.
- **Queue Submission:** Submits the command buffer to the graphics queue.
- **Present Image:** Presents the rendered image to the screen.
5. **Cleanup:**
- **Resource Destruction:** Cleans up Vulkan resources like pipelines, buffers, and swapchain images upon application exit.
## Dependencies and Requirements
- **Vulkan SDK 1.3 or Later:** Ensure you have the Vulkan SDK that supports Vulkan 1.3.
- **Hardware Support:** A GPU that supports Vulkan 1.3 features, including dynamic rendering, synchronization2, and extended dynamic state.
- **GLM Library:** Used for vector and matrix operations.
- **Shader Compiler:** GLSL shaders are compiled at runtime using a GLSL compiler.
File diff suppressed because it is too large Load Diff
@@ -1,170 +0,0 @@
/* Copyright (c) 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.
*/
#pragma once
#include "platform/application.h"
#include "platform/window.h"
#include <vulkan/vulkan.hpp>
/**
* @brief A self-contained (minimal use of framework) sample that illustrates
* the rendering of a triangle
*/
class HPPHelloTriangleV13 : public vkb::Application
{
// Define the Vertex structure
struct Vertex
{
glm::vec2 position;
glm::vec3 color;
};
// Define the vertex data
const std::vector<Vertex> vertices = {
{{0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}}, // Vertex 1: Red
{{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, // Vertex 2: Green
{{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} // Vertex 3: Blue
};
/**
* @brief Swapchain state
*/
struct SwapchainDimensions
{
/// Width of the swapchain.
uint32_t width = 0;
/// Height of the swapchain.
uint32_t height = 0;
/// Pixel format of the swapchain.
vk::Format format = vk::Format::eUndefined;
};
/**
* @brief Per-frame data
*/
struct PerFrame
{
vk::Fence queue_submit_fence = nullptr;
vk::CommandPool primary_command_pool = nullptr;
vk::CommandBuffer primary_command_buffer = nullptr;
vk::Semaphore swapchain_acquire_semaphore = nullptr;
vk::Semaphore swapchain_release_semaphore = nullptr;
};
/**
* @brief Vulkan objects and global state
*/
struct Context
{
/// The Vulkan instance.
vk::Instance instance = nullptr;
/// The Vulkan physical device.
vk::PhysicalDevice gpu = nullptr;
/// The Vulkan device.
vk::Device device = nullptr;
/// The Vulkan device queue.
vk::Queue queue = nullptr;
/// The swapchain.
vk::SwapchainKHR swapchain = nullptr;
/// The swapchain dimensions.
SwapchainDimensions swapchain_dimensions;
/// The surface we will render to.
vk::SurfaceKHR surface = nullptr;
/// The queue family index where graphics work will be submitted.
int32_t graphics_queue_index = -1;
/// The image view for each swapchain image.
std::vector<vk::ImageView> swapchain_image_views;
/// The handles to the images in the swapchain.
std::vector<vk::Image> swapchain_images;
/// The graphics pipeline.
vk::Pipeline pipeline = nullptr;
/**
* The pipeline layout for resources.
* Not used in this sample, but we still need to provide a dummy one.
*/
vk::PipelineLayout pipeline_layout = nullptr;
/// The debug utility messenger callback.
vk::DebugUtilsMessengerEXT debug_callback = nullptr;
/// A set of semaphores that can be reused.
std::vector<vk::Semaphore> recycled_semaphores;
/// A set of per-frame data.
std::vector<PerFrame> per_frame;
/// The Vulkan buffer object that holds the vertex data for the triangle.
vk::Buffer vertex_buffer = nullptr;
/// The device memory allocated for the vertex buffer.
vk::DeviceMemory vertex_buffer_memory = nullptr;
};
public:
HPPHelloTriangleV13() = default;
virtual ~HPPHelloTriangleV13();
private:
// from vkb::Application
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual bool resize(const uint32_t width, const uint32_t height) override;
virtual void update(float delta_time) override;
vk::Result acquire_next_swapchain_image(uint32_t *image);
uint32_t find_memory_type(vk::PhysicalDevice physical_device, uint32_t type_filter, vk::MemoryPropertyFlags properties);
void init_device();
void init_instance();
void init_per_frame(PerFrame &per_frame);
void init_pipeline();
void init_swapchain();
void init_vertex_buffer();
vk::ShaderModule load_shader_module(const std::string &path, vk::ShaderStageFlagBits shader_stage);
vk::Result present_image(uint32_t index);
void render_triangle(uint32_t swapchain_index);
void select_physical_device_and_surface(vkb::Window *window);
void teardown_per_frame(PerFrame &per_frame);
void transition_image_layout(vk::CommandBuffer cmd,
vk::Image image,
vk::ImageLayout oldLayout,
vk::ImageLayout newLayout,
vk::AccessFlags2 srcAccessMask,
vk::AccessFlags2 dstAccessMask,
vk::PipelineStageFlags2 srcStage,
vk::PipelineStageFlags2 dstStage);
bool validate_extensions(const std::vector<const char *> &required, const std::vector<vk::ExtensionProperties> &available);
private:
Context context;
};
std::unique_ptr<vkb::Application> create_hpp_hello_triangle_1_3();
-41
View File
@@ -1,41 +0,0 @@
# 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.
#
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 "Sascha Willems"
NAME "HPP Instancing"
DESCRIPTION "Instanced mesh rendering, uses a separate vertex buffer for instanced data, using vulkan.hpp"
SHADER_FILES_GLSL
"instancing/glsl/instancing.vert"
"instancing/glsl/instancing.frag"
"instancing/glsl/planet.vert"
"instancing/glsl/planet.frag"
"instancing/glsl/starfield.vert"
"instancing/glsl/starfield.frag"
SHADER_FILES_HLSL
"instancing/hlsl/instancing.vert.hlsl"
"instancing/hlsl/instancing.frag.hlsl"
"instancing/hlsl/planet.vert.hlsl"
"instancing/hlsl/planet.frag.hlsl"
"instancing/hlsl/starfield.vert.hlsl"
"instancing/hlsl/starfield.frag.hlsl")
-27
View File
@@ -1,27 +0,0 @@
////
- Copyright (c) 2023, 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.
-
////
:pp: {plus}{plus}
= HPP Instancing
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hpp_instancing[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/instancing[Instancing] that illustrates the usage of the C{pp} bindings of Vulkan provided by vulkan.hpp.
@@ -1,538 +0,0 @@
/* 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.
*/
/*
* Instanced mesh rendering, uses a separate vertex buffer for instanced data, using vulkan.hpp
*/
#include "hpp_instancing.h"
#include <benchmark_mode/benchmark_mode.h>
#include <random>
HPPInstancing::HPPInstancing()
{
title = "HPP instanced mesh rendering";
}
HPPInstancing::~HPPInstancing()
{
if (has_device() && get_device().get_handle())
{
vk::Device device = get_device().get_handle();
planet.destroy(device);
rocks.destroy(device);
device.destroyPipeline(starfield_pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
}
}
bool HPPInstancing::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
initialize_camera();
load_assets();
prepare_instance_data();
prepare_uniform_buffers();
vk::Device device = get_device().get_handle();
descriptor_set_layout = create_descriptor_set_layout();
pipeline_layout = device.createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &descriptor_set_layout});
descriptor_pool = create_descriptor_pool();
// setup planet
planet.pipeline = create_planet_pipeline();
planet.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, descriptor_set_layout);
update_planet_descriptor_set();
// setup rocks
rocks.pipeline = create_rocks_pipeline();
rocks.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, descriptor_set_layout);
update_rocks_descriptor_set();
// setup starfield
starfield_pipeline = create_starfield_pipeline();
build_command_buffers();
prepared = true;
}
return prepared;
}
bool HPPInstancing::resize(const uint32_t width, const uint32_t height)
{
HPPApiVulkanSample::resize(width, height);
rebuild_command_buffers();
return true;
}
void HPPInstancing::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
auto &requested_features = gpu.get_mutable_requested_features();
auto const &features = gpu.get_features();
// Enable anisotropic filtering if supported
if (features.samplerAnisotropy)
{
requested_features.samplerAnisotropy = true;
}
// Enable texture compression
if (features.textureCompressionBC)
{
requested_features.textureCompressionBC = true;
}
else if (features.textureCompressionASTC_LDR)
{
requested_features.textureCompressionASTC_LDR = true;
}
else if (features.textureCompressionETC2)
{
requested_features.textureCompressionETC2 = true;
}
};
void HPPInstancing::build_command_buffers()
{
vk::CommandBufferBeginInfo command_buffer_begin_info;
std::array<vk::ClearValue, 2> clear_values =
{{vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.033f, 0.0f}})),
vk::ClearDepthStencilValue{0.0f, 0}}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = render_pass,
.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
// Set target frame buffer
render_pass_begin_info.framebuffer = framebuffers[i];
auto command_buffer = draw_cmd_buffers[i];
command_buffer.begin(command_buffer_begin_info);
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, extent};
command_buffer.setScissor(0, scissor);
vk::DeviceSize offset = 0;
// Star field
// the star field uses the same descriptor_set as planet !
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, planet.descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, starfield_pipeline);
command_buffer.draw(4, 1, 0, 0);
// Planet
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, planet.descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, planet.pipeline);
command_buffer.bindVertexBuffers(0, planet.mesh->get_vertex_buffer("vertex_buffer").get_handle(), offset);
command_buffer.bindIndexBuffer(planet.mesh->get_index_buffer().get_handle(), 0, vk::IndexType::eUint32);
command_buffer.drawIndexed(planet.mesh->vertex_indices, 1, 0, 0, 0);
// Instanced rocks
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, rocks.descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, rocks.pipeline);
// Binding point 0 : Mesh vertex buffer
command_buffer.bindVertexBuffers(0, rocks.mesh->get_vertex_buffer("vertex_buffer").get_handle(), offset);
// Binding point 1 : Instance data buffer
command_buffer.bindVertexBuffers(1, instance_buffer.buffer->get_handle(), offset);
command_buffer.bindIndexBuffer(rocks.mesh->get_index_buffer().get_handle(), 0, vk::IndexType::eUint32);
// Render instances
command_buffer.drawIndexed(rocks.mesh->vertex_indices, INSTANCE_COUNT, 0, 0, 0);
draw_ui(command_buffer);
command_buffer.endRenderPass();
command_buffer.end();
}
}
void HPPInstancing::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Statistics"))
{
drawer.text("Instances: %d", INSTANCE_COUNT);
}
}
void HPPInstancing::render(float delta_time)
{
if (prepared)
{
draw();
if (!paused || camera.updated)
{
update_uniform_buffer(delta_time);
}
}
}
vk::DescriptorPool HPPInstancing::create_descriptor_pool()
{
// Example uses one ubo
std::array<vk::DescriptorPoolSize, 2> pool_sizes = {{{vk::DescriptorType::eUniformBuffer, 2}, {vk::DescriptorType::eCombinedImageSampler, 2}}};
vk::DescriptorPoolCreateInfo descriptor_pool_create_info{.maxSets = 2,
.poolSizeCount = static_cast<uint32_t>(pool_sizes.size()),
.pPoolSizes = pool_sizes.data()};
return get_device().get_handle().createDescriptorPool(descriptor_pool_create_info);
}
vk::DescriptorSetLayout HPPInstancing::create_descriptor_set_layout()
{
std::array<vk::DescriptorSetLayoutBinding, 2> set_layout_bindings = {
{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex}, // Binding 0 : Vertex shader uniform buffer
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}}; // Binding 1 : Fragment shader combined sampler
return get_device().get_handle().createDescriptorSetLayout(
{.bindingCount = static_cast<uint32_t>(set_layout_bindings.size()), .pBindings = set_layout_bindings.data()});
}
vk::Pipeline HPPInstancing::create_planet_pipeline()
{
// Planet rendering pipeline
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {load_shader("instancing", "planet.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("instancing", "planet.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Vertex input bindings
vk::VertexInputBindingDescription binding_description{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
// Vertex attribute bindings
std::array<vk::VertexInputAttributeDescription, 3> attribute_descriptions = {
{ // Per-vertex attributes
// These are advanced for each vertex fetched by the vertex shader
{0, 0, vk::Format::eR32G32B32Sfloat, 0}, // Location 0: Position
{1, 0, vk::Format::eR32G32B32Sfloat, 3 * sizeof(float)}, // Location 1: Normal
{2, 0, vk::Format::eR32G32Sfloat, 6 * sizeof(float)}}}; // Location 2: Texture coordinates
// Use all input bindings and attribute descriptions
vk::PipelineVertexInputStateCreateInfo input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &binding_description,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(attribute_descriptions.size()),
.pVertexAttributeDescriptions = attribute_descriptions.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.depthTestEnable = true;
depth_stencil_state.depthWriteEnable = true;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eBack,
vk::FrontFace::eClockwise,
{blend_attachment_state},
depth_stencil_state,
pipeline_layout,
render_pass);
}
vk::Pipeline HPPInstancing::create_rocks_pipeline()
{
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages{load_shader("instancing", "instancing.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("instancing", "instancing.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Vertex input bindings
// The instancing pipeline uses a vertex input state with two bindings
std::array<vk::VertexInputBindingDescription, 2> binding_descriptions = {
{{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex}, // Binding point 0: Mesh vertex layout description at per-vertex rate
{1, sizeof(InstanceData), vk::VertexInputRate::eInstance}}}; // Binding point 1: Instanced data at per-instance rate
// Vertex attribute bindings
// Note that the shader declaration for per-vertex and per-instance attributes is the same, the different input rates are only stored in the bindings:
// instanced.vert:
// layout (location = 0) in vec3 inPos; Per-Vertex
// ...
// layout (location = 4) in vec3 instancePos; Per-Instance
std::array<vk::VertexInputAttributeDescription, 7> attribute_descriptions = {
{ // Per-vertex attributes
// These are advanced for each vertex fetched by the vertex shader
{0, 0, vk::Format::eR32G32B32Sfloat, 0}, // Location 0: Position
{1, 0, vk::Format::eR32G32B32Sfloat, 3 * sizeof(float)}, // Location 1: Normal
{2, 0, vk::Format::eR32G32Sfloat, 6 * sizeof(float)}, // Location 2: Texture coordinates
// Per-Instance attributes
// These are fetched for each instance rendered
{3, 1, vk::Format::eR32G32B32Sfloat, 0}, // Location 3: Position
{4, 1, vk::Format::eR32G32B32Sfloat, 3 * sizeof(float)}, // Location 4: Rotation
{5, 1, vk::Format::eR32Sfloat, 6 * sizeof(float)}, // Location 5: Scale
{6, 1, vk::Format::eR32Sint, 7 * sizeof(float)}}}; // Location 6: Texture array layer index
// Use all input bindings and attribute descriptions
vk::PipelineVertexInputStateCreateInfo input_state{.vertexBindingDescriptionCount = static_cast<uint32_t>(binding_descriptions.size()),
.pVertexBindingDescriptions = binding_descriptions.data(),
.vertexAttributeDescriptionCount = static_cast<uint32_t>(attribute_descriptions.size()),
.pVertexAttributeDescriptions = attribute_descriptions.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.depthTestEnable = true;
depth_stencil_state.depthWriteEnable = true;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eBack,
vk::FrontFace::eClockwise,
{blend_attachment_state},
depth_stencil_state,
pipeline_layout,
render_pass);
}
vk::Pipeline HPPInstancing::create_starfield_pipeline()
{
// Starfield rendering pipeline
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {load_shader("instancing", "starfield.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("instancing", "starfield.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Vertex input bindings
vk::VertexInputBindingDescription binding_description{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
// Vertex attribute bindings
std::array<vk::VertexInputAttributeDescription, 3> attribute_descriptions = {
{ // Per-vertex attributes
// These are advanced for each vertex fetched by the vertex shader
{0, 0, vk::Format::eR32G32B32Sfloat, 0}, // Location 0: Position
{1, 0, vk::Format::eR32G32B32Sfloat, 3 * sizeof(float)}, // Location 1: Normal
{2, 0, vk::Format::eR32G32Sfloat, 6 * sizeof(float)}}}; // Location 2: Texture coordinates
// Use all input bindings and attribute descriptions
vk::PipelineVertexInputStateCreateInfo input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &binding_description,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(attribute_descriptions.size()),
.pVertexAttributeDescriptions = attribute_descriptions.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.depthTestEnable = false;
depth_stencil_state.depthWriteEnable = false;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
{}, // Vertices are generated in the vertex shader
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eClockwise,
{blend_attachment_state},
depth_stencil_state,
pipeline_layout,
render_pass);
}
void HPPInstancing::draw()
{
HPPApiVulkanSample::prepare_frame();
// Command buffer to be submitted to the queue
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
// Submit to queue
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
}
void HPPInstancing::load_assets()
{
rocks.mesh = load_model("scenes/rock.gltf");
planet.mesh = load_model("scenes/planet.gltf");
rocks.texture = load_texture_array("textures/texturearray_rocks_color_rgba.ktx", vkb::scene_graph::components::HPPImage::Color);
planet.texture = load_texture("textures/lavaplanet_color_rgba.ktx", vkb::scene_graph::components::HPPImage::Color);
}
void HPPInstancing::initialize_camera()
{
camera.type = vkb::CameraType::LookAt;
camera.set_rotation(glm::vec3(-17.2f, -4.7f, 0.0f));
camera.set_translation(glm::vec3(5.5f, -1.85f, -18.5f));
// Note: Using reversed depth-buffer for increased precision, so Znear and Zfar are flipped
camera.set_perspective(60.0f, static_cast<float>(extent.width) / static_cast<float>(extent.height), 256.0f, 0.1f);
}
void HPPInstancing::prepare_instance_data()
{
std::vector<InstanceData> instance_data;
instance_data.resize(INSTANCE_COUNT);
std::default_random_engine rnd_generator(lock_simulation_speed ? 0 : static_cast<unsigned>(time(nullptr)));
std::uniform_real_distribution<float> uniform_dist(0.0, 1.0);
std::uniform_int_distribution<uint32_t> rnd_texture_index(0, rocks.texture.image->get_vk_image().get_array_layer_count());
// Distribute rocks randomly on two different rings
glm::vec2 ring0{7.0f, 11.0f};
glm::vec2 ring1{14.0f, 18.0f};
for (auto i = 0, j = INSTANCE_COUNT / 2; i < INSTANCE_COUNT / 2; i++, j++)
{
float rho, theta;
// Inner ring
rho = sqrt((pow(ring0[1], 2.0f) - pow(ring0[0], 2.0f)) * uniform_dist(rnd_generator) + pow(ring0[0], 2.0f));
theta = 2.0f * glm::pi<float>() * uniform_dist(rnd_generator);
instance_data[i].pos = glm::vec3(rho * cos(theta), uniform_dist(rnd_generator) * 0.5f - 0.25f, rho * sin(theta));
instance_data[i].rot = glm::vec3(glm::pi<float>() * uniform_dist(rnd_generator), glm::pi<float>() * uniform_dist(rnd_generator), glm::pi<float>() * uniform_dist(rnd_generator));
instance_data[i].scale = 1.5f + uniform_dist(rnd_generator) - uniform_dist(rnd_generator);
instance_data[i].texIndex = rnd_texture_index(rnd_generator);
instance_data[i].scale *= 0.75f;
// Outer ring
rho = sqrt((pow(ring1[1], 2.0f) - pow(ring1[0], 2.0f)) * uniform_dist(rnd_generator) + pow(ring1[0], 2.0f));
theta = 2.0f * glm::pi<float>() * uniform_dist(rnd_generator);
instance_data[j].pos = glm::vec3(rho * cos(theta), uniform_dist(rnd_generator) * 0.5f - 0.25f, rho * sin(theta));
instance_data[j].rot = glm::vec3(glm::pi<float>() * uniform_dist(rnd_generator), glm::pi<float>() * uniform_dist(rnd_generator), glm::pi<float>() * uniform_dist(rnd_generator));
instance_data[j].scale = 1.5f + uniform_dist(rnd_generator) - uniform_dist(rnd_generator);
instance_data[j].texIndex = rnd_texture_index(rnd_generator);
instance_data[j].scale *= 0.75f;
}
instance_buffer.size = instance_data.size() * sizeof(InstanceData);
// Staging
// Instanced data is static, copy to device local memory
// On devices with separate memory types for host visible and device local memory this will result in better performance
// On devices with unified memory types (DEVICE_LOCAL_BIT and HOST_VISIBLE_BIT supported at once) this isn't necessary and you could skip the staging
auto const &device = get_device();
vkb::core::BufferCpp staging_buffer(get_device(), instance_buffer.size, vk::BufferUsageFlagBits::eTransferSrc, VMA_MEMORY_USAGE_CPU_TO_GPU);
staging_buffer.update(instance_data.data(), instance_buffer.size);
instance_buffer.buffer = std::make_unique<vkb::core::BufferCpp>(
get_device(), instance_buffer.size, vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst, VMA_MEMORY_USAGE_GPU_ONLY);
// Copy to staging buffer
vk::CommandBuffer copy_command = get_device().create_command_buffer(vk::CommandBufferLevel::ePrimary, true);
vk::BufferCopy copy_region{.size = instance_buffer.size};
copy_command.copyBuffer(staging_buffer.get_handle(), instance_buffer.buffer->get_handle(), copy_region);
get_device().flush_command_buffer(copy_command, queue, true);
instance_buffer.descriptor.range = instance_buffer.size;
instance_buffer.descriptor.buffer = instance_buffer.buffer->get_handle();
instance_buffer.descriptor.offset = 0;
}
void HPPInstancing::prepare_uniform_buffers()
{
uniform_buffers.scene =
std::make_unique<vkb::core::BufferCpp>(get_device(), sizeof(ubo_vs), vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffer(0.0f);
}
void HPPInstancing::update_uniform_buffer(float delta_time)
{
ubo_vs.projection = camera.matrices.perspective;
ubo_vs.view = camera.matrices.view;
if (!paused)
{
ubo_vs.loc_speed += delta_time * 0.35f;
ubo_vs.glob_speed += delta_time * 0.01f;
}
uniform_buffers.scene->convert_and_update(ubo_vs);
}
void HPPInstancing::update_planet_descriptor_set()
{
vk::DescriptorBufferInfo buffer_descriptor{uniform_buffers.scene->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo image_descriptor{planet.texture.sampler,
planet.texture.image->get_vk_image_view().get_handle(),
descriptor_type_to_image_layout(vk::DescriptorType::eCombinedImageSampler,
planet.texture.image->get_vk_image_view().get_format())};
std::array<vk::WriteDescriptorSet, 2> write_descriptor_sets = {{{.dstSet = planet.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &buffer_descriptor}, // Binding 0 : Vertex shader uniform buffer
{.dstSet = planet.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &image_descriptor}}}; // Binding 1 : Color map
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, {});
}
void HPPInstancing::update_rocks_descriptor_set()
{
vk::DescriptorBufferInfo buffer_descriptor{uniform_buffers.scene->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo image_descriptor{rocks.texture.sampler,
rocks.texture.image->get_vk_image_view().get_handle(),
descriptor_type_to_image_layout(vk::DescriptorType::eCombinedImageSampler,
rocks.texture.image->get_vk_image_view().get_format())};
std::array<vk::WriteDescriptorSet, 2> write_descriptor_sets = {{{.dstSet = rocks.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &buffer_descriptor}, // Binding 0 : Vertex shader uniform buffer
{.dstSet = rocks.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &image_descriptor}}}; // Binding 1 : Color map
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, {});
}
std::unique_ptr<vkb::Application> create_hpp_instancing()
{
return std::make_unique<HPPInstancing>();
}
-123
View File
@@ -1,123 +0,0 @@
/* 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.
*/
/*
* Instanced mesh rendering, uses a separate vertex buffer for instanced data, using vulkan.hpp
*/
#pragma once
#include <hpp_api_vulkan_sample.h>
#if defined(__ANDROID__)
# define INSTANCE_COUNT 4096
#else
# define INSTANCE_COUNT 8192
#endif
class HPPInstancing : public HPPApiVulkanSample
{
public:
HPPInstancing();
~HPPInstancing();
private:
// Contains the instanced data
struct InstanceBuffer
{
std::unique_ptr<vkb::core::BufferCpp> buffer;
vk::DescriptorBufferInfo descriptor;
size_t size = 0;
};
// Per-instance data block
struct InstanceData
{
glm::vec3 pos;
glm::vec3 rot;
float scale;
uint32_t texIndex;
};
struct Model
{
vk::DescriptorSet descriptor_set;
std::unique_ptr<vkb::scene_graph::components::HPPSubMesh> mesh;
vk::Pipeline pipeline;
HPPTexture texture;
void destroy(vk::Device device)
{
mesh.reset();
device.destroyPipeline(pipeline);
device.destroySampler(texture.sampler);
}
};
struct UBOVS
{
glm::mat4 projection;
glm::mat4 view;
glm::vec4 light_pos = glm::vec4(0.0f, -5.0f, 0.0f, 1.0f);
float loc_speed = 0.0f;
float glob_speed = 0.0f;
};
struct UniformBuffers
{
std::unique_ptr<vkb::core::BufferCpp> scene;
};
private:
// from vkb::Application
bool prepare(const vkb::ApplicationOptions &options) override;
bool resize(const uint32_t width, const uint32_t height) override;
// from vkb::VulkanSample
void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
void build_command_buffers() override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
void render(float delta_time) override;
vk::DescriptorPool create_descriptor_pool();
vk::DescriptorSetLayout create_descriptor_set_layout();
vk::Pipeline create_planet_pipeline();
vk::Pipeline create_rocks_pipeline();
vk::Pipeline create_starfield_pipeline();
void draw();
void load_assets();
void initialize_camera();
void prepare_instance_data();
void prepare_uniform_buffers();
void update_uniform_buffer(float delta_time);
void update_planet_descriptor_set();
void update_rocks_descriptor_set();
private:
vk::DescriptorSetLayout descriptor_set_layout;
InstanceBuffer instance_buffer;
Model planet;
Model rocks;
vk::PipelineLayout pipeline_layout;
vk::Pipeline starfield_pipeline;
UBOVS ubo_vs;
UniformBuffers uniform_buffers;
};
std::unique_ptr<vkb::Application> create_hpp_instancing();
@@ -1,35 +0,0 @@
# Copyright (c) 2024, Google
#
# 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 "Google"
NAME "HPP Order-independent transparency (depth peeling)"
DESCRIPTION "Order-independent transparency using depth peeling using Vulkan-Hpp"
SHADER_FILES_GLSL
"oit_depth_peeling/background.frag"
"oit_depth_peeling/combine.frag"
"oit_depth_peeling/fullscreen.vert"
"oit_depth_peeling/gather.frag"
"oit_depth_peeling/gather.vert"
"oit_depth_peeling/gather_first.frag")
@@ -1,80 +0,0 @@
////
- Copyright (c) 2024, Google
-
- 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.
-
////
= Order-independent transparency with depth peeling 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/api/hpp_oit_depth_peeling[Khronos Vulkan samples github repository].
endif::[]
NOTE: This is a transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/oit_depth_peeling[OIT depth peeling] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp. Please see there for the documentation on this sample.
:pp: {plus}{plus}
image::samples/api/oit_depth_peeling/images/sample.png[Sample]
== Overview
This sample implements an order-independent transparency (OIT) algorithm using depth peeling.
It renders a single torus whose opacity can be controlled via the UI.
It produces pixel-perfect results.
It is based on the https://developer.download.nvidia.com/assets/gamedev/docs/OrderIndependentTransparency.pdf[original paper] from Cass Everitt.
== Algorithm
The OIT algorithm consists of several _gather_ passes followed by one _combine_ pass.
Each _gather_ pass renders one layer of transparent geometry.
The first pass renders the first layer, the second pass the second layer, etc.
The N^th^ layer consists of all the N^th^ fragments of each pixel when the fragments are ordered from front to back.
The _combine_ pass is a screen-space operation.
It merges the layer images from back to front to produce the final result.
The algorithm can produce pixel-perfect results, even with intersecting geometry.
When there are more geometry layers than gather passes, the backmost layers get skipped, but the visual results stay stable (i.e. no flickering pixels).
== Options
[cols="2,4,4"]
|===
| Option | Description | Comments
| Camera auto-rotation
| Enable the automatic rotation of the camera
|
| Background grayscale
| Specify the grayscale value by which the background color is multiplied (0.0 to 1.0)
|
| Object alpha
| Specify the opacity of the transparent object (0.0 to 1.0)
|
| Front layer index
| The first layer to be rendered (0 to 7).
|
| Back layer index
| The last layer to be rendered (0 to 7).
| This cannot be less that the front layer index.
|===
@@ -1,595 +0,0 @@
/* Copyright (c) 2024-2025, Google
* Copyright (c) 2024-2025, NVIDIA
*
* 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_oit_depth_peeling.h"
HPPOITDepthPeeling::HPPOITDepthPeeling()
{}
HPPOITDepthPeeling::~HPPOITDepthPeeling()
{
if (has_device())
{
vk::Device device = get_device().get_handle();
background.destroy(device);
combinePass.destroy(device);
for (auto &d : depths)
{
d.destroy();
}
device.destroyDescriptorPool(descriptor_pool);
gatherPass.destroy(device);
for (auto &l : layers)
{
l.destroy(device);
}
model.reset();
device.destroySampler(point_sampler);
scene_constants.reset();
}
}
bool HPPOITDepthPeeling::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
prepare_camera();
load_assets();
create_point_sampler();
create_scene_constants_buffer();
create_descriptor_pool();
create_combine_pass();
create_images(extent.width, extent.height);
create_gather_pass();
create_background_pipeline();
update_scene_constants();
update_descriptors();
build_command_buffers();
prepared = true;
}
return prepared;
}
bool HPPOITDepthPeeling::resize(const uint32_t width, const uint32_t height)
{
create_images(width, height);
create_gather_pass_framebuffers(width, height);
update_descriptors();
return HPPApiVulkanSample::resize(width, height);
}
void HPPOITDepthPeeling::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = VK_TRUE;
}
else
{
throw std::runtime_error("This sample requires support for anisotropic sampling");
}
}
void HPPOITDepthPeeling::build_command_buffers()
{
vk::CommandBufferBeginInfo command_buffer_begin_info;
std::array<vk::ClearValue, 2> clear_values = {{vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 0.0f}})),
vk::ClearDepthStencilValue{0.0f, 0}}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
vk::Rect2D scissor{{0, 0}, extent};
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
auto const &command_buffer = draw_cmd_buffers[i];
command_buffer.begin(command_buffer_begin_info);
{
// Gather passes
// Each pass renders a single transparent layer into a layer texture.
for (uint32_t l = 0; l <= gui.layer_index_back; ++l)
{
// Two depth textures are used.
// Their roles alternates for each pass.
// The first depth texture is used for fixed-function depth test.
// The second one is the result of the depth test from the previous gatherPass pass.
// It is bound as texture and read in the shader to discard fragments from the
// previous layers.
vk::ImageSubresourceRange depth_subresource_range = {vk::ImageAspectFlagBits::eDepth, 0, 1, 0, 1};
vkb::common::image_layout_transition(command_buffer,
depths[l % kDepthCount].image->get_handle(),
vk::PipelineStageFlagBits::eFragmentShader,
vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests,
vk::AccessFlagBits::eShaderRead,
vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite,
l <= 1 ? vk::ImageLayout::eUndefined : vk::ImageLayout::eDepthStencilReadOnlyOptimal,
vk::ImageLayout::eDepthStencilAttachmentOptimal,
depth_subresource_range);
if (l > 0)
{
vkb::common::image_layout_transition(command_buffer,
depths[(l + 1) % kDepthCount].image->get_handle(),
vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests,
vk::PipelineStageFlagBits::eFragmentShader,
vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite,
vk::AccessFlagBits::eShaderRead,
vk::ImageLayout::eDepthStencilAttachmentOptimal,
vk::ImageLayout::eDepthStencilReadOnlyOptimal,
depth_subresource_range);
}
// Set one of the layer textures as color attachment, as the gatherPass pass will render to it.
vk::ImageSubresourceRange layer_subresource_range = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
vkb::common::image_layout_transition(command_buffer,
layers[l].image->get_handle(),
vk::PipelineStageFlagBits::eFragmentShader,
vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::AccessFlagBits::eShaderRead,
vk::AccessFlagBits::eColorAttachmentWrite,
vk::ImageLayout::eUndefined,
vk::ImageLayout::eColorAttachmentOptimal,
layer_subresource_range);
render_pass_begin_info.framebuffer = layers[l].gather_framebuffer;
render_pass_begin_info.renderPass = gatherPass.render_pass;
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
{
// Render the geometry into the layer texture.
command_buffer.setViewport(0, viewport);
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, gatherPass.pipeline_layout, 0, depths[l % kDepthCount].gather_descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, l == 0 ? gatherPass.first_pipeline : gatherPass.pipeline);
draw_model(model, command_buffer);
}
command_buffer.endRenderPass();
// Get the layer texture ready to be read by the combinePass pass.
vkb::common::image_layout_transition(command_buffer,
layers[l].image->get_handle(),
vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::PipelineStageFlagBits::eFragmentShader,
vk::AccessFlagBits::eColorAttachmentWrite,
vk::AccessFlagBits::eShaderRead,
vk::ImageLayout::eColorAttachmentOptimal,
vk::ImageLayout::eShaderReadOnlyOptimal,
layer_subresource_range);
}
// Combine pass
// This pass blends all the layers into the final transparent color.
// The final color is then alpha blended into the background.
render_pass_begin_info.framebuffer = framebuffers[i];
render_pass_begin_info.renderPass = render_pass;
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
{
command_buffer.setViewport(0, viewport);
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, combinePass.pipeline_layout, 0, combinePass.descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, background.pipeline);
command_buffer.draw(3, 1, 0, 0);
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, combinePass.pipeline);
command_buffer.draw(3, 1, 0, 0);
draw_ui(command_buffer);
}
command_buffer.endRenderPass();
}
command_buffer.end();
}
}
void HPPOITDepthPeeling::on_update_ui_overlay(vkb::Drawer &drawer)
{
drawer.checkbox("Camera auto-rotation", &gui.camera_auto_rotation);
drawer.slider_float("Background grayscale", &gui.background_grayscale, kBackgroundGrayscaleMin, kBackgroundGrayscaleMax);
drawer.slider_float("Object opacity", &gui.object_opacity, kObjectAlphaMin, kObjectAlphaMax);
drawer.slider_int("Front layer index", &gui.layer_index_front, 0, gui.layer_index_back);
drawer.slider_int("Back layer index", &gui.layer_index_back, gui.layer_index_front, kLayerMaxCount - 1);
}
void HPPOITDepthPeeling::render(float delta_time)
{
if (prepared)
{
HPPApiVulkanSample::prepare_frame();
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
if (gui.camera_auto_rotation)
{
camera.rotate({delta_time * 5.0f, delta_time * 5.0f, 0.0f});
}
update_scene_constants();
}
}
////////////////////////////////////////////////////////////////////////////////
void HPPOITDepthPeeling::create_background_pipeline()
{
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {load_shader("oit_depth_peeling/fullscreen.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("oit_depth_peeling/background.frag.spv", vk::ShaderStageFlagBits::eFragment)};
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
std::array<vk::VertexInputAttributeDescription, 2> vertex_input_attributes = {{{0, 0, vk::Format::eR32G32B32Sfloat, offsetof(HPPVertex, pos)},
{1, 0, vk::Format::eR32G32Sfloat, offsetof(HPPVertex, uv)}}};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::FlagTraits<vk::ColorComponentFlagBits>::allFlags};
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state{.depthCompareOp = vk::CompareOp::eGreater, .back = {.compareOp = vk::CompareOp::eAlways}};
background.pipeline = vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
{},
vk::PrimitiveTopology::eTriangleList,
{},
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
combinePass.pipeline_layout,
render_pass);
}
void HPPOITDepthPeeling::create_combine_pass()
{
vk::Device device = get_device().get_handle();
std::array<vk::DescriptorSetLayoutBinding, 3> set_layout_bindings = {
{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{2, vk::DescriptorType::eCombinedImageSampler, kLayerMaxCount, vk::ShaderStageFlagBits::eFragment}}};
combinePass.descriptor_set_layout = device.createDescriptorSetLayout({.bindingCount = static_cast<uint32_t>(set_layout_bindings.size()), .pBindings = set_layout_bindings.data()});
combinePass.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, combinePass.descriptor_set_layout);
combinePass.pipeline_layout = device.createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &combinePass.descriptor_set_layout});
create_combine_pass_pipeline();
}
void HPPOITDepthPeeling::create_combine_pass_pipeline()
{
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {load_shader("oit_depth_peeling/fullscreen.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("oit_depth_peeling/combine.frag.spv", vk::ShaderStageFlagBits::eFragment)};
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
std::array<vk::VertexInputAttributeDescription, 2> vertex_input_attributes = {{{0, 0, vk::Format::eR32G32B32Sfloat, offsetof(HPPVertex, pos)},
{1, 0, vk::Format::eR32G32Sfloat, offsetof(HPPVertex, uv)}}};
vk::PipelineColorBlendAttachmentState blend_attachment_state{true,
vk::BlendFactor::eSrcAlpha,
vk::BlendFactor::eOneMinusSrcColor,
vk::BlendOp::eAdd,
vk::BlendFactor::eOne,
vk::BlendFactor::eZero,
vk::BlendOp::eAdd,
vk::FlagTraits<vk::ColorComponentFlagBits>::allFlags};
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state{.depthCompareOp = vk::CompareOp::eGreater, .back = {.compareOp = vk::CompareOp::eAlways}};
combinePass.pipeline = vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
{},
vk::PrimitiveTopology::eTriangleList,
{},
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
combinePass.pipeline_layout,
render_pass);
}
void HPPOITDepthPeeling::create_descriptor_pool()
{
const uint32_t num_gather_pass_combined_image_sampler = kDepthCount;
const uint32_t num_gather_pass_uniform_buffer = kDepthCount;
const uint32_t num_combine_pass_combined_image_sampler = kLayerMaxCount + 1;
const uint32_t num_combine_pass_uniform_buffer = 1;
const uint32_t num_uniform_buffer_descriptors = num_gather_pass_uniform_buffer + num_combine_pass_uniform_buffer;
const uint32_t num_combined_image_sampler_descriptors = num_gather_pass_combined_image_sampler + num_combine_pass_combined_image_sampler;
std::array<vk::DescriptorPoolSize, 2> pool_sizes = {{{vk::DescriptorType::eUniformBuffer, num_uniform_buffer_descriptors},
{vk::DescriptorType::eCombinedImageSampler, num_combined_image_sampler_descriptors}}};
const uint32_t num_gather_descriptor_sets = 2;
const uint32_t num_combine_descriptor_sets = 1;
const uint32_t num_descriptor_sets = num_gather_descriptor_sets + num_combine_descriptor_sets;
vk::DescriptorPoolCreateInfo descriptor_pool_create_info{.maxSets = num_descriptor_sets,
.poolSizeCount = static_cast<uint32_t>(pool_sizes.size()),
.pPoolSizes = pool_sizes.data()};
descriptor_pool = get_device().get_handle().createDescriptorPool(descriptor_pool_create_info);
}
void HPPOITDepthPeeling::create_gather_pass()
{
create_gather_pass_descriptor_set_layout();
create_gather_pass_render_pass();
create_gather_pass_depth_descriptor_sets();
create_gather_pass_framebuffers(extent.width, extent.height);
gatherPass.pipeline_layout = get_device().get_handle().createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &gatherPass.descriptor_set_layout});
create_gather_pass_pipelines();
}
void HPPOITDepthPeeling::create_gather_pass_depth_descriptor_sets()
{
vk::Device device = get_device().get_handle();
for (auto &d : depths)
{
d.gather_descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, gatherPass.descriptor_set_layout);
}
}
void HPPOITDepthPeeling::create_gather_pass_descriptor_set_layout()
{
std::array<vk::DescriptorSetLayoutBinding, 2> set_layout_bindings = {
{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}};
gatherPass.descriptor_set_layout = get_device().get_handle().createDescriptorSetLayout(
{.bindingCount = static_cast<uint32_t>(set_layout_bindings.size()), .pBindings = set_layout_bindings.data()});
}
void HPPOITDepthPeeling::create_gather_pass_framebuffers(const uint32_t width, const uint32_t height)
{
vk::Device device = get_device().get_handle();
vk::FramebufferCreateInfo framebuffer_create_info{.width = width, .height = height, .layers = 1};
for (uint32_t i = 0; i < kLayerMaxCount; ++i)
{
if (layers[i].gather_framebuffer)
{
device.destroyFramebuffer(layers[i].gather_framebuffer);
}
framebuffer_create_info.renderPass = gatherPass.render_pass;
const std::array<vk::ImageView, 2> attachments{layers[i].image_view->get_handle(), depths[i % kDepthCount].image_view->get_handle()};
framebuffer_create_info.setAttachments(attachments);
layers[i].gather_framebuffer = device.createFramebuffer(framebuffer_create_info);
}
}
void HPPOITDepthPeeling::create_gather_pass_pipelines()
{
vk::Device device = get_device().get_handle();
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {load_shader("oit_depth_peeling/gather.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("oit_depth_peeling/gather_first.frag.spv", vk::ShaderStageFlagBits::eFragment)};
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
std::array<vk::VertexInputAttributeDescription, 2> vertex_input_attributes = {{{0, 0, vk::Format::eR32G32B32Sfloat, offsetof(HPPVertex, pos)},
{1, 0, vk::Format::eR32G32Sfloat, offsetof(HPPVertex, uv)}}};
vk::PipelineVertexInputStateCreateInfo vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &vertex_input_binding,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size()),
.pVertexAttributeDescriptions = vertex_input_attributes.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::FlagTraits<vk::ColorComponentFlagBits>::allFlags};
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state{
.depthTestEnable = true, .depthWriteEnable = true, .depthCompareOp = vk::CompareOp::eGreater, .back = {.compareOp = vk::CompareOp::eAlways}};
gatherPass.first_pipeline = vkb::common::create_graphics_pipeline(device,
pipeline_cache,
shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
{},
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
gatherPass.pipeline_layout,
gatherPass.render_pass);
shader_stages[1] = load_shader("oit_depth_peeling/gather.frag.spv", vk::ShaderStageFlagBits::eFragment);
gatherPass.pipeline = vkb::common::create_graphics_pipeline(device,
pipeline_cache,
shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
{},
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
gatherPass.pipeline_layout,
gatherPass.render_pass);
}
void HPPOITDepthPeeling::create_gather_pass_render_pass()
{
std::array<vk::AttachmentDescription, 2> attachment_descriptions = {{{{},
vk::Format::eR8G8B8A8Unorm,
vk::SampleCountFlagBits::e1,
vk::AttachmentLoadOp::eClear,
vk::AttachmentStoreOp::eStore,
vk::AttachmentLoadOp::eDontCare,
vk::AttachmentStoreOp::eDontCare,
vk::ImageLayout::eUndefined,
vk::ImageLayout::eColorAttachmentOptimal},
{{},
vk::Format::eD32Sfloat,
vk::SampleCountFlagBits::e1,
vk::AttachmentLoadOp::eClear,
vk::AttachmentStoreOp::eStore,
vk::AttachmentLoadOp::eDontCare,
vk::AttachmentStoreOp::eDontCare,
vk::ImageLayout::eUndefined,
vk::ImageLayout::eDepthStencilAttachmentOptimal}}};
vk::AttachmentReference color_attachment_reference{0, vk::ImageLayout::eColorAttachmentOptimal};
vk::AttachmentReference depth_attachment_reference{1, vk::ImageLayout::eDepthStencilAttachmentOptimal};
vk::SubpassDescription subpass{.pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
.colorAttachmentCount = 1,
.pColorAttachments = &color_attachment_reference,
.pDepthStencilAttachment = &depth_attachment_reference};
vk::RenderPassCreateInfo render_pass_create_info{.attachmentCount = static_cast<uint32_t>(attachment_descriptions.size()),
.pAttachments = attachment_descriptions.data(),
.subpassCount = 1,
.pSubpasses = &subpass};
gatherPass.render_pass = get_device().get_handle().createRenderPass(render_pass_create_info);
}
void HPPOITDepthPeeling::create_images(const uint32_t width, const uint32_t height)
{
const vk::Extent3D image_extent = {width, height, 1};
for (uint32_t i = 0; i < kLayerMaxCount; ++i)
{
layers[i].image = std::make_unique<vkb::core::HPPImage>(get_device(),
image_extent,
vk::Format::eR8G8B8A8Unorm,
vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eColorAttachment,
VMA_MEMORY_USAGE_GPU_ONLY,
vk::SampleCountFlagBits::e1);
layers[i].image_view = std::make_unique<vkb::core::HPPImageView>(*layers[i].image, vk::ImageViewType::e2D, vk::Format::eR8G8B8A8Unorm);
}
for (uint32_t i = 0; i < kDepthCount; ++i)
{
depths[i].image = std::make_unique<vkb::core::HPPImage>(get_device(),
image_extent,
vk::Format::eD32Sfloat,
vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eDepthStencilAttachment,
VMA_MEMORY_USAGE_GPU_ONLY,
vk::SampleCountFlagBits::e1);
depths[i].image_view = std::make_unique<vkb::core::HPPImageView>(*depths[i].image, vk::ImageViewType::e2D, vk::Format::eD32Sfloat);
}
}
void HPPOITDepthPeeling::create_point_sampler()
{
point_sampler = vkb::common::create_sampler(
get_device().get_handle(), vk::Filter::eNearest, vk::Filter::eNearest, vk::SamplerMipmapMode::eNearest, vk::SamplerAddressMode::eClampToEdge, 1.0f, 1.0f);
}
void HPPOITDepthPeeling::create_scene_constants_buffer()
{
scene_constants =
std::make_unique<vkb::core::BufferCpp>(get_device(), sizeof(SceneConstants), vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
}
void HPPOITDepthPeeling::load_assets()
{
model = load_model("scenes/torusknot.gltf");
background.texture = load_texture("textures/vulkan_logo_full.ktx", vkb::scene_graph::components::HPPImage::Color);
}
void HPPOITDepthPeeling::prepare_camera()
{
camera.type = vkb::CameraType::LookAt;
camera.set_position({0.0f, 0.0f, -4.0f});
camera.set_rotation({0.0f, 0.0f, 0.0f});
camera.set_perspective(60.0f, static_cast<float>(extent.width) / static_cast<float>(extent.height), 16.0f, 0.1f);
}
void HPPOITDepthPeeling::update_descriptors()
{
vk::Device device = get_device().get_handle();
vk::DescriptorBufferInfo scene_constants_descriptor{scene_constants->get_handle(), 0, vk::WholeSize};
for (uint32_t i = 0; i < kDepthCount; ++i)
{
vk::DescriptorImageInfo depth_texture_descriptor{point_sampler,
depths[(i + 1) % kDepthCount].image_view->get_handle(),
vk::ImageLayout::eDepthStencilReadOnlyOptimal};
std::array<vk::WriteDescriptorSet, 2> write_descriptor_sets = {{{.dstSet = depths[i].gather_descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &scene_constants_descriptor},
{.dstSet = depths[i].gather_descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &depth_texture_descriptor}}};
device.updateDescriptorSets(write_descriptor_sets, {});
}
vk::DescriptorImageInfo background_texture_descriptor{background.texture.sampler,
background.texture.image->get_vk_image_view().get_handle(),
vk::ImageLayout::eShaderReadOnlyOptimal};
std::array<vk::DescriptorImageInfo, kLayerMaxCount> layer_texture_descriptor;
for (uint32_t i = 0; i < kLayerMaxCount; ++i)
{
layer_texture_descriptor[i].sampler = point_sampler;
layer_texture_descriptor[i].imageView = layers[i].image_view->get_handle();
layer_texture_descriptor[i].imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
}
std::array<vk::WriteDescriptorSet, 3> write_descriptor_sets = {{{.dstSet = combinePass.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &scene_constants_descriptor},
{.dstSet = combinePass.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &background_texture_descriptor},
{.dstSet = combinePass.descriptor_set,
.dstBinding = 2,
.descriptorCount = static_cast<uint32_t>(layer_texture_descriptor.size()),
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = layer_texture_descriptor.data()}}};
device.updateDescriptorSets(write_descriptor_sets, {});
}
void HPPOITDepthPeeling::update_scene_constants()
{
SceneConstants constants = {};
constants.model_view_projection = camera.matrices.perspective * camera.matrices.view * glm::scale(glm::mat4(1.0f), glm::vec3(0.08));
constants.background_grayscale = gui.background_grayscale;
constants.object_opacity = gui.object_opacity;
constants.front_layer_index = gui.layer_index_front;
constants.back_layer_index = gui.layer_index_back;
scene_constants->convert_and_update(constants);
}
////////////////////////////////////////////////////////////////////////////////
std::unique_ptr<vkb::VulkanSampleCpp> create_hpp_oit_depth_peeling()
{
return std::make_unique<HPPOITDepthPeeling>();
}
@@ -1,187 +0,0 @@
/* Copyright (c) 2024, Google
* Copyright (c) 2024, NVIDIA
*
* 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 "hpp_api_vulkan_sample.h"
class HPPOITDepthPeeling : public HPPApiVulkanSample
{
public:
HPPOITDepthPeeling();
~HPPOITDepthPeeling();
private:
// from vkb::Application
bool prepare(const vkb::ApplicationOptions &options) override;
bool resize(const uint32_t width, const uint32_t height) override;
// from vkb::VulkanSample
void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
void build_command_buffers() override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
void render(float delta_time) override;
void create_background_pipeline();
void create_combine_pass();
void create_combine_pass_pipeline();
void create_descriptor_pool();
void create_gather_pass();
void create_gather_pass_depth_descriptor_sets();
void create_gather_pass_descriptor_set_layout();
void create_gather_pass_framebuffers(const uint32_t width, const uint32_t height);
void create_gather_pass_pipelines();
void create_gather_pass_render_pass();
void create_images(const uint32_t width, const uint32_t height);
void create_point_sampler();
void create_scene_constants_buffer();
void load_assets();
void prepare_camera();
void update_descriptors();
void update_scene_constants();
private:
static constexpr uint32_t kLayerMaxCount = 8;
static constexpr uint32_t kDepthCount = 2;
static constexpr float kBackgroundGrayscaleMin = 0.0f;
static constexpr float kBackgroundGrayscaleMax = 1.0f;
static constexpr float kObjectAlphaMin = 0.0f;
static constexpr float kObjectAlphaMax = 1.0f;
struct Background
{
vk::Pipeline pipeline = {};
HPPTexture texture = {};
void destroy(vk::Device device)
{
device.destroyPipeline(pipeline);
device.destroySampler(texture.sampler);
}
};
struct CombinePass
{
vk::DescriptorSet descriptor_set = {};
vk::DescriptorSetLayout descriptor_set_layout = {};
vk::Pipeline pipeline = {};
vk::PipelineLayout pipeline_layout = {};
void destroy(vk::Device device)
{
// descriptor_set is implicitly destroyed when the managing descriptor_pool is destroyed
device.destroyDescriptorSetLayout(descriptor_set_layout);
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
descriptor_set_layout = nullptr;
pipeline = nullptr;
pipeline_layout = nullptr;
}
};
struct Depth
{
vk::DescriptorSet gather_descriptor_set = {};
std::unique_ptr<vkb::core::HPPImage> image = {};
std::unique_ptr<vkb::core::HPPImageView> image_view = {};
void destroy()
{
image_view.reset();
image.reset();
}
};
struct GatherPass
{
vk::DescriptorSetLayout descriptor_set_layout = {};
vk::Pipeline first_pipeline = {};
vk::Pipeline pipeline = {};
vk::PipelineLayout pipeline_layout = {};
vk::RenderPass render_pass = {};
void destroy(vk::Device device)
{
device.destroyDescriptorSetLayout(descriptor_set_layout);
device.destroyPipeline(first_pipeline);
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyRenderPass(render_pass);
descriptor_set_layout = nullptr;
first_pipeline = nullptr;
pipeline = nullptr;
pipeline_layout = nullptr;
render_pass = nullptr;
}
};
struct GUI
{
float background_grayscale = 0.3f;
int32_t camera_auto_rotation = false;
int32_t layer_index_back = kLayerMaxCount - 1;
int32_t layer_index_front = 0;
float object_opacity = 0.5f;
};
struct Layer
{
vk::Framebuffer gather_framebuffer = {};
std::unique_ptr<vkb::core::HPPImage> image = {};
std::unique_ptr<vkb::core::HPPImageView> image_view = {};
void destroy(vk::Device device)
{
device.destroyFramebuffer(gather_framebuffer);
image_view.reset();
image.reset();
gather_framebuffer = nullptr;
}
};
struct SceneConstants
{
glm::mat4 model_view_projection = {};
glm::f32 background_grayscale = {};
glm::f32 object_opacity = {};
glm::int32 front_layer_index = {};
glm::int32 back_layer_index = {};
};
private:
Background background = {};
CombinePass combinePass = {};
std::array<Depth, kDepthCount> depths = {};
vk::DescriptorPool descriptor_pool = {};
GatherPass gatherPass = {};
GUI gui = {};
std::array<Layer, kLayerMaxCount> layers = {};
std::unique_ptr<vkb::scene_graph::components::HPPSubMesh> model = {};
vk::Sampler point_sampler = {};
std::unique_ptr<vkb::core::BufferCpp> scene_constants = {};
};
std::unique_ptr<vkb::VulkanSampleCpp> create_hpp_oit_depth_peeling();
@@ -1,35 +0,0 @@
# 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.
#
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 "Google"
NAME "HPP Order-independent transparency (ordered linked lists)"
DESCRIPTION "Order-independent transparency using per-pixel ordered linked lists, using vulkan.hpp"
SHADER_FILES_GLSL
"oit_linked_lists/background.frag"
"oit_linked_lists/combine.frag"
"oit_linked_lists/combine.vert"
"oit_linked_lists/fullscreen.vert"
"oit_linked_lists/gather.frag"
"oit_linked_lists/gather.vert")
@@ -1,26 +0,0 @@
////
- Copyright (c) 2023, NVIDIA
-
- 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.
-
////
= Order-independent transparency with per-pixel ordered linked lists 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/api/hpp_oit_linked_lists[Khronos Vulkan samples github repository].
endif::[]
NOTE: This is a transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/oit_linked_lists[OIT linked lists] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp. Please see there for the documentation on this sample.
@@ -1,505 +0,0 @@
/* Copyright (c) 2023-2025, NVIDIA
*
* 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_oit_linked_lists.h"
HPPOITLinkedLists::HPPOITLinkedLists()
{
title = "HPP OIT linked lists";
}
HPPOITLinkedLists::~HPPOITLinkedLists()
{
if (has_device() && get_device().get_handle())
{
vk::Device device = get_device().get_handle();
device.destroyPipeline(combine_pipeline);
device.destroyPipeline(background_pipeline);
device.destroyPipeline(gather_pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorPool(descriptor_pool);
device.destroyDescriptorSetLayout(descriptor_set_layout);
destroy_sized_objects();
device.destroySampler(background_texture.sampler);
}
}
bool HPPOITLinkedLists::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
initialize_camera();
load_assets();
create_constant_buffers();
create_descriptors();
create_sized_objects(extent);
create_pipelines();
update_scene_constants();
fill_instance_data();
update_descriptors();
build_command_buffers();
prepared = true;
}
return prepared;
}
bool HPPOITLinkedLists::resize(const uint32_t width, const uint32_t height)
{
if ((width != extent.width) || (height != extent.height))
{
destroy_sized_objects();
create_sized_objects({width, height});
update_descriptors();
}
return HPPApiVulkanSample::resize(width, height);
}
void HPPOITLinkedLists::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
auto &requested_features = gpu.get_mutable_requested_features();
auto const &features = gpu.get_features();
if (features.fragmentStoresAndAtomics)
{
requested_features.fragmentStoresAndAtomics = true;
}
else
{
throw std::runtime_error("This sample requires support for buffers and images stores and atomic operations in the fragment shader stage");
}
// Enable anisotropic filtering if supported
if (features.samplerAnisotropy)
{
requested_features.samplerAnisotropy = true;
}
}
void HPPOITLinkedLists::build_command_buffers()
{
vk::CommandBufferBeginInfo command_buffer_begin_info;
vk::RenderPassBeginInfo gather_render_pass_begin_info{.renderPass = gather_render_pass,
.framebuffer = gather_framebuffer,
.renderArea = {{0, 0}, extent}};
std::array<vk::ClearValue, 2> combine_clear_values =
{{vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 0.0f}})),
vk::ClearDepthStencilValue{0.0f, 0}}};
vk::RenderPassBeginInfo combine_render_pass_begin_info{.renderPass = render_pass,
.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(combine_clear_values.size()),
.pClearValues = combine_clear_values.data()};
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
vk::Rect2D scissor{{0, 0}, extent};
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
auto const &command_buffer = draw_cmd_buffers[i];
command_buffer.begin(command_buffer_begin_info);
{
// Gather pass
command_buffer.beginRenderPass(gather_render_pass_begin_info, vk::SubpassContents::eInline);
{
command_buffer.setViewport(0, viewport);
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, gather_pipeline);
draw_model(object, command_buffer, kInstanceCount);
}
command_buffer.endRenderPass();
vkb::common::image_layout_transition(command_buffer,
linked_list_head_image->get_handle(),
vk::PipelineStageFlagBits::eFragmentShader,
vk::PipelineStageFlagBits::eFragmentShader,
vk::AccessFlagBits::eShaderWrite,
vk::AccessFlagBits::eShaderRead,
vk::ImageLayout::eGeneral,
vk::ImageLayout::eGeneral,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1});
// Combine pass
combine_render_pass_begin_info.framebuffer = framebuffers[i];
command_buffer.beginRenderPass(combine_render_pass_begin_info, vk::SubpassContents::eInline);
{
command_buffer.setViewport(0, viewport);
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, background_pipeline);
command_buffer.draw(3, 1, 0, 0);
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, combine_pipeline);
command_buffer.draw(3, 1, 0, 0);
draw_ui(command_buffer);
}
command_buffer.endRenderPass();
}
command_buffer.end();
}
}
void HPPOITLinkedLists::on_update_ui_overlay(vkb::Drawer &drawer)
{
drawer.checkbox("Sort fragments", &sort_fragments);
drawer.checkbox("Camera auto-rotation", &camera_auto_rotation);
drawer.slider_int("Sorted fragments per pixel", &sorted_fragment_count, kSortedFragmentMinCount, kSortedFragmentMaxCount);
drawer.slider_float("Background grayscale", &background_grayscale, kBackgroundGrayscaleMin, kBackgroundGrayscaleMax);
}
void HPPOITLinkedLists::render(float delta_time)
{
if (prepared)
{
HPPApiVulkanSample::prepare_frame();
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
if (camera_auto_rotation)
{
camera.rotate({delta_time * 5.0f, delta_time * 5.0f, 0.0f});
}
update_scene_constants();
}
}
////////////////////////////////////////////////////////////////////////////////
void HPPOITLinkedLists::clear_sized_resources()
{
vk::CommandBuffer command_buffer = vkb::common::allocate_command_buffer(get_device().get_handle(), cmd_pool);
vk::CommandBufferBeginInfo command_buffer_begin_info;
command_buffer.begin(command_buffer_begin_info);
{
command_buffer.fillBuffer(fragment_counter->get_handle(), 0, sizeof(glm::uint), 0);
vk::ImageSubresourceRange subresource_range{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
vkb::common::image_layout_transition(command_buffer,
linked_list_head_image->get_handle(),
vk::PipelineStageFlagBits::eBottomOfPipe,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eMemoryWrite,
vk::AccessFlagBits::eTransferWrite,
vk::ImageLayout::eUndefined,
vk::ImageLayout::eGeneral,
subresource_range);
vk::ClearColorValue linked_lists_clear_value(kLinkedListEndSentinel, kLinkedListEndSentinel, kLinkedListEndSentinel, kLinkedListEndSentinel);
command_buffer.clearColorImage(linked_list_head_image->get_handle(), vk::ImageLayout::eGeneral, linked_lists_clear_value, subresource_range);
}
command_buffer.end();
{
vk::SubmitInfo submit_info{.commandBufferCount = 1, .pCommandBuffers = &command_buffer};
queue.submit(submit_info);
queue.waitIdle();
}
get_device().get_handle().freeCommandBuffers(cmd_pool, command_buffer);
}
void HPPOITLinkedLists::create_constant_buffers()
{
scene_constants =
std::make_unique<vkb::core::BufferCpp>(get_device(), sizeof(SceneConstants), vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
instance_data = std::make_unique<vkb::core::BufferCpp>(
get_device(), sizeof(Instance) * kInstanceCount, vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
}
vk::DescriptorPool HPPOITLinkedLists::create_descriptor_pool()
{
std::array<vk::DescriptorPoolSize, 4> pool_sizes = {{{vk::DescriptorType::eUniformBuffer, 2},
{vk::DescriptorType::eStorageImage, 1},
{vk::DescriptorType::eStorageBuffer, 2},
{vk::DescriptorType::eCombinedImageSampler, 1}}};
vk::DescriptorPoolCreateInfo descriptor_pool_create_info{.maxSets = 1,
.poolSizeCount = static_cast<uint32_t>(pool_sizes.size()),
.pPoolSizes = pool_sizes.data()};
return get_device().get_handle().createDescriptorPool(descriptor_pool_create_info);
}
vk::DescriptorSetLayout HPPOITLinkedLists::create_descriptor_set_layout()
{
std::array<vk::DescriptorSetLayoutBinding, 6> set_layout_bindings = {
{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex},
{2, vk::DescriptorType::eStorageImage, 1, vk::ShaderStageFlagBits::eFragment},
{3, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eFragment},
{4, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eFragment},
{5, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}};
return get_device().get_handle().createDescriptorSetLayout(
{.bindingCount = static_cast<uint32_t>(set_layout_bindings.size()), .pBindings = set_layout_bindings.data()});
}
void HPPOITLinkedLists::create_descriptors()
{
descriptor_set_layout = create_descriptor_set_layout();
descriptor_pool = create_descriptor_pool();
descriptor_set = vkb::common::allocate_descriptor_set(get_device().get_handle(), descriptor_pool, descriptor_set_layout);
}
void HPPOITLinkedLists::create_fragment_resources(vk::Extent2D const &extent)
{
const vk::Extent3D image_extent{extent.width, extent.height, 1};
const vk::Format image_format{vk::Format::eR32Uint};
linked_list_head_image = std::make_unique<vkb::core::HPPImage>(get_device(),
image_extent,
image_format,
vk::ImageUsageFlagBits::eStorage | vk::ImageUsageFlagBits::eTransferDst,
VMA_MEMORY_USAGE_GPU_ONLY,
vk::SampleCountFlagBits::e1);
linked_list_head_image_view = std::make_unique<vkb::core::HPPImageView>(*linked_list_head_image, vk::ImageViewType::e2D, image_format);
fragment_max_count = extent.width * extent.height * kFragmentsPerPixelAverage;
const uint32_t fragment_buffer_size = sizeof(glm::uvec3) * fragment_max_count;
fragment_buffer =
std::make_unique<vkb::core::BufferCpp>(get_device(), fragment_buffer_size, vk::BufferUsageFlagBits::eStorageBuffer, VMA_MEMORY_USAGE_GPU_ONLY);
fragment_counter = std::make_unique<vkb::core::BufferCpp>(
get_device(), sizeof(glm::uint), vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst, VMA_MEMORY_USAGE_GPU_ONLY);
}
void HPPOITLinkedLists::create_gather_pass_objects(vk::Extent2D const &extent)
{
vk::SubpassDescription subpass{.pipelineBindPoint = vk::PipelineBindPoint::eGraphics};
gather_render_pass = get_device().get_handle().createRenderPass({.subpassCount = 1, .pSubpasses = &subpass});
gather_framebuffer = vkb::common::create_framebuffer(get_device().get_handle(), gather_render_pass, {}, extent);
}
void HPPOITLinkedLists::create_pipelines()
{
pipeline_layout = get_device().get_handle().createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &descriptor_set_layout});
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.depthTestEnable = false;
depth_stencil_state.depthWriteEnable = false;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
std::vector<vk::PipelineShaderStageCreateInfo> gather_shader_stages = {load_shader("oit_linked_lists/gather.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("oit_linked_lists/gather.frag.spv", vk::ShaderStageFlagBits::eFragment)};
vk::VertexInputBindingDescription gather_vertex_input_binding{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
vk::VertexInputAttributeDescription gather_vertex_input_attribute{0, 0, vk::Format::eR32G32B32Sfloat, offsetof(HPPVertex, pos)};
vk::PipelineVertexInputStateCreateInfo gather_vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &gather_vertex_input_binding,
.vertexAttributeDescriptionCount = 1,
.pVertexAttributeDescriptions = &gather_vertex_input_attribute};
gather_pipeline = vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
gather_shader_stages,
gather_vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
pipeline_layout,
gather_render_pass);
std::vector<vk::PipelineShaderStageCreateInfo> background_shader_stages = {load_shader("oit_linked_lists/fullscreen.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("oit_linked_lists/background.frag.spv", vk::ShaderStageFlagBits::eFragment)};
vk::PipelineVertexInputStateCreateInfo vertex_input_state;
background_pipeline = vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
background_shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
pipeline_layout,
render_pass);
std::vector<vk::PipelineShaderStageCreateInfo> combine_shader_stages = {load_shader("oit_linked_lists/combine.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("oit_linked_lists/combine.frag.spv", vk::ShaderStageFlagBits::eFragment)};
vk::PipelineColorBlendAttachmentState combine_blend_attachment_state{true,
vk::BlendFactor::eSrcAlpha,
vk::BlendFactor::eOneMinusSrcAlpha,
vk::BlendOp::eAdd,
vk::BlendFactor::eOne,
vk::BlendFactor::eZero,
vk::BlendOp::eAdd,
vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
combine_pipeline = vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
combine_shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{combine_blend_attachment_state},
depth_stencil_state,
pipeline_layout,
render_pass);
}
void HPPOITLinkedLists::create_sized_objects(vk::Extent2D const &extent)
{
create_gather_pass_objects(extent);
create_fragment_resources(extent);
clear_sized_resources();
}
void HPPOITLinkedLists::destroy_sized_objects()
{
get_device().get_handle().destroyFramebuffer(gather_framebuffer);
get_device().get_handle().destroyRenderPass(gather_render_pass);
fragment_counter.reset();
fragment_buffer.reset();
fragment_max_count = 0;
linked_list_head_image_view.reset();
linked_list_head_image.reset();
}
void HPPOITLinkedLists::fill_instance_data()
{
Instance instances[kInstanceCount] = {};
auto get_random_float = []() { return static_cast<float>(rand()) / (RAND_MAX); };
for (uint32_t l = 0, instance_index = 0; l < kInstanceLayerCount; ++l)
{
for (uint32_t c = 0; c < kInstanceColumnCount; ++c)
{
for (uint32_t r = 0; r < kInstanceRowCount; ++r, ++instance_index)
{
const float x = static_cast<float>(r) - ((kInstanceRowCount - 1) * 0.5f);
const float y = static_cast<float>(c) - ((kInstanceColumnCount - 1) * 0.5f);
const float z = static_cast<float>(l) - ((kInstanceLayerCount - 1) * 0.5f);
const float scale = 0.02f;
instances[instance_index].model = glm::scale(glm::translate(glm::mat4(1.0f), glm::vec3(x, y, z)), glm::vec3(scale));
instances[instance_index].color.r = get_random_float();
instances[instance_index].color.g = get_random_float();
instances[instance_index].color.b = get_random_float();
instances[instance_index].color.a = get_random_float() * 0.8f + 0.2f;
}
}
}
instance_data->convert_and_update(instances);
}
void HPPOITLinkedLists::initialize_camera()
{
camera.type = vkb::CameraType::LookAt;
camera.set_position({0.0f, 0.0f, -4.0f});
camera.set_rotation({0.0f, 0.0f, 0.0f});
camera.set_perspective(60.0f, static_cast<float>(extent.width) / static_cast<float>(extent.height), 256.0f, 0.1f);
}
void HPPOITLinkedLists::load_assets()
{
object = load_model("scenes/geosphere.gltf");
background_texture = load_texture("textures/vulkan_logo_full.ktx", vkb::scene_graph::components::HPPImage::Color);
}
void HPPOITLinkedLists::update_descriptors()
{
vk::DescriptorBufferInfo scene_constants_descriptor{scene_constants->get_handle(), 0, vk::WholeSize};
vk::DescriptorBufferInfo instance_data_descriptor{instance_data->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo linked_list_head_image_view_descriptor{nullptr, linked_list_head_image_view->get_handle(), vk::ImageLayout::eGeneral};
vk::DescriptorBufferInfo fragment_buffer_descriptor{fragment_buffer->get_handle(), 0, vk::WholeSize};
vk::DescriptorBufferInfo fragment_counter_descriptor{fragment_counter->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo background_texture_descriptor{background_texture.sampler,
background_texture.image->get_vk_image_view().get_handle(),
descriptor_type_to_image_layout(vk::DescriptorType::eCombinedImageSampler,
background_texture.image->get_vk_image_view().get_format())};
std::array<vk::WriteDescriptorSet, 6> write_descriptor_sets = {{{.dstSet = descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &scene_constants_descriptor},
{.dstSet = descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &instance_data_descriptor},
{.dstSet = descriptor_set,
.dstBinding = 2,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageImage,
.pImageInfo = &linked_list_head_image_view_descriptor},
{.dstSet = descriptor_set,
.dstBinding = 3,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageBuffer,
.pBufferInfo = &fragment_buffer_descriptor},
{.dstSet = descriptor_set,
.dstBinding = 4,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageBuffer,
.pBufferInfo = &fragment_counter_descriptor},
{.dstSet = descriptor_set,
.dstBinding = 5,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &background_texture_descriptor}}};
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, {});
}
void HPPOITLinkedLists::update_scene_constants()
{
SceneConstants constants = {};
constants.projection = camera.matrices.perspective;
constants.view = camera.matrices.view;
constants.background_grayscale = background_grayscale;
constants.sort_fragments = sort_fragments ? 1U : 0U;
constants.fragment_max_count = fragment_max_count;
constants.sorted_fragment_count = sorted_fragment_count;
scene_constants->convert_and_update(constants);
}
////////////////////////////////////////////////////////////////////////////////
std::unique_ptr<vkb::Application> create_hpp_oit_linked_lists()
{
return std::make_unique<HPPOITLinkedLists>();
}
@@ -1,120 +0,0 @@
/* Copyright (c) 2023-2024, NVIDIA
*
* 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 <hpp_api_vulkan_sample.h>
class HPPOITLinkedLists : public HPPApiVulkanSample
{
public:
HPPOITLinkedLists();
~HPPOITLinkedLists();
private:
// from vkb::Application
bool prepare(const vkb::ApplicationOptions &options) override;
bool resize(const uint32_t width, const uint32_t height) override;
// from vkb::VulkanSample
void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
void build_command_buffers() override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
void render(float delta_time) override;
void clear_sized_resources();
void create_constant_buffers();
vk::DescriptorPool create_descriptor_pool();
vk::DescriptorSetLayout create_descriptor_set_layout();
void create_descriptors();
void create_fragment_resources(vk::Extent2D const &extent);
void create_gather_pass_objects(vk::Extent2D const &extent);
void create_pipelines();
void create_sized_objects(vk::Extent2D const &extent);
void destroy_sized_objects();
void fill_instance_data();
void initialize_camera();
void load_assets();
void update_descriptors();
void update_scene_constants();
private:
static constexpr uint32_t kInstanceRowCount = 4;
static constexpr uint32_t kInstanceColumnCount = 4;
static constexpr uint32_t kInstanceLayerCount = 4;
static constexpr uint32_t kInstanceCount = kInstanceRowCount * kInstanceColumnCount * kInstanceLayerCount;
static constexpr uint32_t kFragmentsPerPixelAverage = 8;
static constexpr uint32_t kSortedFragmentMinCount = 1;
static constexpr uint32_t kSortedFragmentMaxCount = 16;
static constexpr float kBackgroundGrayscaleMin = 0.0f;
static constexpr float kBackgroundGrayscaleMax = 1.0f;
static constexpr uint32_t kLinkedListEndSentinel = 0xFFFFFFFFU;
struct SceneConstants
{
glm::mat4 projection;
glm::mat4 view;
glm::f32 background_grayscale;
glm::uint sort_fragments;
glm::uint fragment_max_count;
glm::uint sorted_fragment_count;
};
struct Instance
{
glm::mat4 model;
glm::vec4 color;
};
private:
std::unique_ptr<vkb::scene_graph::components::HPPSubMesh> object;
HPPTexture background_texture;
std::unique_ptr<vkb::core::BufferCpp> scene_constants;
std::unique_ptr<vkb::core::BufferCpp> instance_data;
std::unique_ptr<vkb::core::HPPImage> linked_list_head_image;
std::unique_ptr<vkb::core::HPPImageView> linked_list_head_image_view;
std::unique_ptr<vkb::core::BufferCpp> fragment_buffer;
std::unique_ptr<vkb::core::BufferCpp> fragment_counter;
glm::uint fragment_max_count = 0U;
vk::RenderPass gather_render_pass = nullptr;
vk::Framebuffer gather_framebuffer = nullptr;
vk::DescriptorSetLayout descriptor_set_layout = nullptr;
vk::DescriptorPool descriptor_pool = nullptr;
vk::DescriptorSet descriptor_set = nullptr;
vk::PipelineLayout pipeline_layout = nullptr;
vk::Pipeline gather_pipeline = nullptr;
vk::Pipeline background_pipeline = nullptr;
vk::Pipeline combine_pipeline = nullptr;
bool sort_fragments = true;
bool camera_auto_rotation = false;
int32_t sorted_fragment_count = kSortedFragmentMaxCount;
float background_grayscale = 0.3f;
};
std::unique_ptr<vkb::Application> create_hpp_oit_linked_lists();
@@ -1,33 +0,0 @@
# 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.
#
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 "Sascha Willems"
NAME "HPP Separate image sampler"
DESCRIPTION "Displays a texture with a separated image and sampler"
SHADER_FILES_GLSL
"separate_image_sampler/glsl/separate_image_sampler.vert"
"separate_image_sampler/glsl/separate_image_sampler.frag"
SHADER_FILES_HLSL
"separate_image_sampler/hlsl/separate_image_sampler.vert.hlsl"
"separate_image_sampler/hlsl/separate_image_sampler.frag.hlsl")
@@ -1,143 +0,0 @@
////
- Copyright (c) 2022-2023, 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}
= Separating samplers and images 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/api/hpp_separate_image_sampler[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/separate_image_sampler[Separate image sampler] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp.
This tutorial, along with the accompanying example code, shows how to separate samplers and images in a Vulkan application.
Opposite to combined image and samplers, this allows the application to freely mix an arbitrary set of samplers and images in the shader.
In the sample code, a single image and multiple samplers with different options will be created.
The sampler to be used for sampling the image can then be selected at runtime.
As image and sampler objects are separated, this only requires selecting a different descriptor at runtime.
== In the application
From the application's point of view, images and samplers are always created separately.
Access to the image is done via the image's `vk::ImageView`.
Samplers are created using a `vk::Sampler` object, specifying how an image will be sampled.
The difference between separating and combining them starts at the descriptor level, which defines how the shader accesses the samplers and images.
A separate setup uses a descriptor of type `vk::DescriptorType::eSampledImage` for the sampled image, and a `vk::DescriptorType::eSampler` for the sampler, separating the image and sampler object:
// {% raw %}
[,cpp]
----
// Image info only references the image
vk::DescriptorImageInfo image_info({}, texture.image->get_vk_image_view().get_handle(), vk::ImageLayout::eShaderReadOnlyOptimal);
// Sampled image descriptor
vk::WriteDescriptorSet image_write_descriptor_set(base_descriptor_set, 1, 0, vk::DescriptorType::eSampledImage, image_info);
// One set for the sampled image
std::array<vk::WriteDescriptorSet, 2> write_descriptor_sets = {{
{base_descriptor_set, 0, 0, vk::DescriptorType::eUniformBuffer, {}, buffer_descriptor}, // Binding 0 : Vertex shader uniform buffer
image_write_descriptor_set // Binding 1 : Fragment shader sampled image
}};
get_device()->get_handle().updateDescriptorSets(write_descriptor_sets, {});
----
// {% endraw %}
For this sample, we then create two samplers with different filtering options:
[,cpp]
----
// Sets for each of the sampler
descriptor_set_alloc_info.pSetLayouts = &sampler_descriptor_set_layout;
for (size_t i = 0; i < sampler_descriptor_sets.size(); i++)
{
sampler_descriptor_sets[i] = get_device()->get_handle().allocateDescriptorSets(descriptor_set_alloc_info).front();
// Descriptor info only references the sampler
vk::DescriptorImageInfo sampler_info(samplers[i]);
vk::WriteDescriptorSet sampler_write_descriptor_set(sampler_descriptor_sets[i], 0, 0, vk::DescriptorType::eSampler, sampler_info);
get_device()->get_handle().updateDescriptorSets(sampler_write_descriptor_set, {});
}
----
At draw-time, the descriptor containing the sampled image is bound to set 0 and the descriptor for the currently selected sampler is bound to set 1:
[,cpp]
----
// Bind the uniform buffer and sampled image to set 0
draw_cmd_buffers[i].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, base_descriptor_set, {});
// Bind the selected sampler to set 1
draw_cmd_buffers[i].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 1, sampler_descriptor_sets[selected_sampler], {});
...
draw_cmd_buffers[i].drawIndexed(index_count, 1, 0, 0, 0);
----
== In the shader
There are no changes in the shader code to get it working with vulkan.hpp.
With the above setup, the shader interface for the fragment shader also separates the sampler and image as two distinct uniforms:
[,glsl]
----
layout (set = 0, binding = 1) uniform texture2D _texture;
layout (set = 1, binding = 0) uniform sampler _sampler;
----
To sample from the image referenced by `_texture`, with the currently set sampler in '_sampler', we create a sampled image in the fragment shader at runtime using the `sampler2D` function.
[,glsl]
----
void main()
{
vec4 color = texture(sampler2D(_texture, _sampler), inUV);
}
----
== Comparison with combined image samplers
For reference, a combined image and sampler setup would differ for both the application and the shader.
The app would use a single descriptor of type `VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER`, and set both image and sampler related values in the descriptor:
[,cpp]
----
// Descriptor info references image and sampler
vk::DescriptorImageInfo image_info(texture.sampler, texture.view, texture.image_layout);
vk::WriteDescriptorSet image_write_descriptor_set(descriptor_set, 1, {}, vk::DescriptorType::eCombinedImageSampler, image_info);
----
The shader interface only uses one uniform for accessing the combined image and sampler and also doesn't construct a `sampler2D` at runtime:
[,glsl]
----
layout (binding = 1) uniform sampler2D _combined_image;
void main()
{
vec4 color = texture(_combined_image, inUV);
}
----
Compared to the separated setup, changing a sampler in this setup would either require creating multiple descriptors with each image/sampler combination or rebuilding the descriptor.
@@ -1,393 +0,0 @@
/* 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.
*/
/*
* Separate samplers and image to draw a single image with different sampling options, using vulkan.hpp
*/
#include "hpp_separate_image_sampler.h"
HPPSeparateImageSampler::HPPSeparateImageSampler()
{
title = "HPP Separate sampler and image";
zoom = -0.5f;
rotation = {45.0f, 0.0f, 0.0f};
}
HPPSeparateImageSampler::~HPPSeparateImageSampler()
{
if (has_device() && get_device().get_handle())
{
vk::Device device = get_device().get_handle();
// Clean up used Vulkan resources
// Note : Inherited destructor cleans up resources stored in base class
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(base_descriptor_set_layout);
device.destroyDescriptorSetLayout(sampler_descriptor_set_layout);
for (vk::Sampler sampler : samplers)
{
device.destroySampler(sampler);
}
// Delete the implicitly created sampler for the texture loaded via the framework
device.destroySampler(texture.sampler);
}
}
bool HPPSeparateImageSampler::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
load_assets();
generate_quad();
prepare_uniform_buffers();
// Create two samplers with different options, first one with linear filtering, the second one with nearest filtering
samplers = {{create_sampler(vk::Filter::eLinear), create_sampler(vk::Filter::eNearest)}};
descriptor_pool = create_descriptor_pool();
// We separate the descriptor sets for the uniform buffer + image and samplers, so we don't need to duplicate the descriptors for the former
// Descriptors set for the uniform buffer and the image
base_descriptor_set_layout = create_base_descriptor_set_layout();
base_descriptor_set = vkb::common::allocate_descriptor_set(get_device().get_handle(), descriptor_pool, base_descriptor_set_layout);
update_base_descriptor_set();
// Sets for each of the sampler
sampler_descriptor_set_layout = create_sampler_descriptor_set_layout();
for (size_t i = 0; i < sampler_descriptor_sets.size(); i++)
{
sampler_descriptor_sets[i] = vkb::common::allocate_descriptor_set(get_device().get_handle(), descriptor_pool, sampler_descriptor_set_layout);
update_sampler_descriptor_set(i);
}
// Pipeline layout
// Set layout for the base descriptors in set 0 and set layout for the sampler descriptors in set 1
pipeline_layout = create_pipeline_layout({base_descriptor_set_layout, sampler_descriptor_set_layout});
pipeline = create_graphics_pipeline();
build_command_buffers();
prepared = true;
}
return prepared;
}
// Enable physical device features required for this example
void HPPSeparateImageSampler::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = true;
}
}
void HPPSeparateImageSampler::build_command_buffers()
{
vk::CommandBufferBeginInfo command_buffer_begin_info;
std::array<vk::ClearValue, 2> clear_values = {{default_clear_color, vk::ClearDepthStencilValue{0.0f, 0}}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = render_pass,
.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
// Set target frame buffer
render_pass_begin_info.framebuffer = framebuffers[i];
auto command_buffer = draw_cmd_buffers[i];
command_buffer.begin(command_buffer_begin_info);
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, extent};
command_buffer.setScissor(0, scissor);
// Bind the uniform buffer and sampled image to set 0
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, base_descriptor_set, {});
// Bind the selected sampler to set 1
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 1, sampler_descriptor_sets[selected_sampler], {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
vk::DeviceSize offset = 0;
command_buffer.bindVertexBuffers(0, vertex_buffer->get_handle(), offset);
command_buffer.bindIndexBuffer(index_buffer->get_handle(), 0, vk::IndexType::eUint32);
command_buffer.drawIndexed(index_count, 1, 0, 0, 0);
draw_ui(command_buffer);
command_buffer.endRenderPass();
command_buffer.end();
}
}
void HPPSeparateImageSampler::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Settings"))
{
const std::vector<std::string> sampler_names = {"Linear filtering", "Nearest filtering"};
if (drawer.combo_box("Sampler", &selected_sampler, sampler_names))
{
update_uniform_buffers();
}
}
}
void HPPSeparateImageSampler::render(float delta_time)
{
if (prepared)
{
draw();
}
}
void HPPSeparateImageSampler::view_changed()
{
update_uniform_buffers();
}
vk::DescriptorSetLayout HPPSeparateImageSampler::create_base_descriptor_set_layout()
{
// Set layout for the uniform buffer and the image
std::array<vk::DescriptorSetLayoutBinding, 2> set_layout_bindings_buffer_and_image = {{
{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex}, // Binding 0 : Vertex shader uniform buffer
{1, vk::DescriptorType::eSampledImage, 1, vk::ShaderStageFlagBits::eFragment} // Binding 1 : Fragment shader sampled image
}};
vk::DescriptorSetLayoutCreateInfo descriptor_layout_create_info{.bindingCount = static_cast<uint32_t>(set_layout_bindings_buffer_and_image.size()),
.pBindings = set_layout_bindings_buffer_and_image.data()};
return get_device().get_handle().createDescriptorSetLayout(descriptor_layout_create_info);
}
vk::DescriptorPool HPPSeparateImageSampler::create_descriptor_pool()
{
std::array<vk::DescriptorPoolSize, 3> pool_sizes = {
{{vk::DescriptorType::eUniformBuffer, 1}, {vk::DescriptorType::eSampledImage, 1}, {vk::DescriptorType::eSampler, 2}}};
vk::DescriptorPoolCreateInfo descriptor_pool_create_info{.maxSets = 3,
.poolSizeCount = static_cast<uint32_t>(pool_sizes.size()),
.pPoolSizes = pool_sizes.data()};
return get_device().get_handle().createDescriptorPool(descriptor_pool_create_info);
}
vk::Pipeline HPPSeparateImageSampler::create_graphics_pipeline()
{
// Load shaders
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {
load_shader("separate_image_sampler", "separate_image_sampler.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("separate_image_sampler", "separate_image_sampler.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Vertex bindings and attributes
vk::VertexInputBindingDescription input_binding{0, sizeof(VertexStructure), vk::VertexInputRate::eVertex};
std::array<vk::VertexInputAttributeDescription, 3> input_attributes = {
{{0, 0, vk::Format::eR32G32B32Sfloat, offsetof(VertexStructure, pos)}, // Location 0 : Position
{1, 0, vk::Format::eR32G32Sfloat, offsetof(VertexStructure, uv)}, // Location 1 : Texture Coordinates
{2, 0, vk::Format::eR32G32B32Sfloat, offsetof(VertexStructure, normal)}}}; // Location 2 : Normal
vk::PipelineVertexInputStateCreateInfo input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &input_binding,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(input_attributes.size()),
.pVertexAttributeDescriptions = input_attributes.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthTestEnable = true;
depth_stencil_state.depthWriteEnable = true;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.back.compareOp = vk::CompareOp::eGreater;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
pipeline_layout,
render_pass);
}
vk::PipelineLayout HPPSeparateImageSampler::create_pipeline_layout(std::vector<vk::DescriptorSetLayout> const &descriptor_set_layouts)
{
vk::PipelineLayoutCreateInfo pipeline_layout_create_info{.setLayoutCount = static_cast<uint32_t>(descriptor_set_layouts.size()),
.pSetLayouts = descriptor_set_layouts.data()};
return get_device().get_handle().createPipelineLayout(pipeline_layout_create_info);
}
vk::Sampler HPPSeparateImageSampler::create_sampler(vk::Filter filter)
{
return vkb::common::create_sampler(
get_device().get_gpu().get_handle(),
get_device().get_handle(),
texture.image->get_format(),
filter,
vk::SamplerAddressMode::eRepeat,
get_device().get_gpu().get_features().samplerAnisotropy ? (get_device().get_gpu().get_properties().limits.maxSamplerAnisotropy) : 1.0f,
static_cast<float>(texture.image->get_mipmaps().size()));
}
vk::DescriptorSetLayout HPPSeparateImageSampler::create_sampler_descriptor_set_layout()
{
// Set layout for the samplers
vk::DescriptorSetLayoutBinding set_layout_binding_sampler{0, vk::DescriptorType::eSampler, 1, vk::ShaderStageFlagBits::eFragment};
vk::DescriptorSetLayoutCreateInfo descriptor_layout_create_info{.bindingCount = 1, .pBindings = &set_layout_binding_sampler};
return get_device().get_handle().createDescriptorSetLayout(descriptor_layout_create_info);
}
void HPPSeparateImageSampler::draw()
{
HPPApiVulkanSample::prepare_frame();
// Command buffer to be submitted to the queue
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
// Submit to queue
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
}
void HPPSeparateImageSampler::generate_quad()
{
// Setup vertices for a single uv-mapped quad made from two triangles
std::vector<VertexStructure> vertices =
{
{{1.0f, 1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, 1.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, -1.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}},
{{1.0f, -1.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}}};
// Setup indices
std::vector<uint32_t> indices = {0, 1, 2, 2, 3, 0};
index_count = static_cast<uint32_t>(indices.size());
auto vertex_buffer_size = vkb::to_u32(vertices.size() * sizeof(VertexStructure));
auto index_buffer_size = vkb::to_u32(indices.size() * sizeof(uint32_t));
// Create buffers
// For the sake of simplicity we won't stage the vertex data to the gpu memory
// Vertex buffer
vertex_buffer = std::make_unique<vkb::core::BufferCpp>(get_device(),
vertex_buffer_size,
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer,
VMA_MEMORY_USAGE_CPU_TO_GPU);
vertex_buffer->update(vertices.data(), vertex_buffer_size);
index_buffer = std::make_unique<vkb::core::BufferCpp>(get_device(),
index_buffer_size,
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer,
VMA_MEMORY_USAGE_CPU_TO_GPU);
index_buffer->update(indices.data(), index_buffer_size);
}
void HPPSeparateImageSampler::load_assets()
{
texture = load_texture("textures/metalplate01_rgba.ktx", vkb::scene_graph::components::HPPImage::Color);
}
// Prepare and initialize uniform buffer containing shader uniforms
void HPPSeparateImageSampler::prepare_uniform_buffers()
{
// Vertex shader uniform buffer block
uniform_buffer_vs =
std::make_unique<vkb::core::BufferCpp>(get_device(), sizeof(ubo_vs), vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
}
void HPPSeparateImageSampler::update_base_descriptor_set()
{
vk::DescriptorBufferInfo buffer_descriptor{uniform_buffer_vs->get_handle(), 0, vk::WholeSize};
// Image info only references the image
vk::DescriptorImageInfo image_info{{}, texture.image->get_vk_image_view().get_handle(), vk::ImageLayout::eShaderReadOnlyOptimal};
// Sampled image descriptor
std::array<vk::WriteDescriptorSet, 2> write_descriptor_sets = {
{
{.dstSet = base_descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &buffer_descriptor}, // Binding 0 : Vertex shader uniform buffer
{.dstSet = base_descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eSampledImage,
.pImageInfo = &image_info} // Binding 1 : Fragment shader sampled image
}};
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, {});
}
void HPPSeparateImageSampler::update_sampler_descriptor_set(size_t index)
{
assert((index < samplers.size()) && (index < sampler_descriptor_sets.size()));
// Descriptor info only references the sampler
vk::DescriptorImageInfo sampler_info{samplers[index]};
vk::WriteDescriptorSet sampler_write_descriptor_set{
.dstSet = sampler_descriptor_sets[index], .dstBinding = 0, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eSampler, .pImageInfo = &sampler_info};
get_device().get_handle().updateDescriptorSets(sampler_write_descriptor_set, {});
}
void HPPSeparateImageSampler::update_uniform_buffers()
{
// Vertex shader
ubo_vs.projection = glm::perspective(glm::radians(60.0f), static_cast<float>(extent.width) / static_cast<float>(extent.height), 0.001f, 256.0f);
glm::mat4 view_matrix = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, zoom));
ubo_vs.model = view_matrix * glm::translate(glm::mat4(1.0f), camera_pos);
ubo_vs.model = glm::rotate(ubo_vs.model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
ubo_vs.model = glm::rotate(ubo_vs.model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
ubo_vs.model = glm::rotate(ubo_vs.model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
ubo_vs.view_pos = glm::vec4(0.0f, 0.0f, -zoom, 0.0f);
uniform_buffer_vs->convert_and_update(ubo_vs);
}
std::unique_ptr<vkb::Application> create_hpp_separate_image_sampler()
{
return std::make_unique<HPPSeparateImageSampler>();
}
@@ -1,92 +0,0 @@
/* Copyright (c) 2023-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.
*/
/*
* Separate samplers and image to draw a single image with different sampling options, using vulkan.hpp
*/
#pragma once
#include <hpp_api_vulkan_sample.h>
class HPPSeparateImageSampler : public HPPApiVulkanSample
{
public:
HPPSeparateImageSampler();
~HPPSeparateImageSampler() override;
private:
// Vertex layout for this example
struct VertexStructure
{
float pos[3];
float uv[2];
float normal[3];
};
struct UBO
{
glm::mat4 projection;
glm::mat4 model;
glm::vec4 view_pos;
};
private:
// from vkb::Application
bool prepare(const vkb::ApplicationOptions &options) override;
// from vkb::VulkanSample
void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
void build_command_buffers() override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
void render(float delta_time) override;
void view_changed() override;
vk::DescriptorSetLayout create_base_descriptor_set_layout();
vk::DescriptorPool create_descriptor_pool();
vk::Pipeline create_graphics_pipeline();
vk::PipelineLayout create_pipeline_layout(std::vector<vk::DescriptorSetLayout> const &descriptor_set_layouts);
vk::Sampler create_sampler(vk::Filter filter);
vk::DescriptorSetLayout create_sampler_descriptor_set_layout();
void draw();
void generate_quad();
void load_assets();
void prepare_uniform_buffers();
void update_base_descriptor_set();
void update_sampler_descriptor_set(size_t index);
void update_uniform_buffers();
private:
vk::DescriptorSet base_descriptor_set;
vk::DescriptorSetLayout base_descriptor_set_layout;
std::unique_ptr<vkb::core::BufferCpp> index_buffer;
uint32_t index_count = 0;
vk::Pipeline pipeline;
vk::PipelineLayout pipeline_layout;
vk::DescriptorSetLayout sampler_descriptor_set_layout;
std::array<vk::DescriptorSet, 2> sampler_descriptor_sets;
std::array<vk::Sampler, 2> samplers;
int32_t selected_sampler = 0;
HPPTexture texture;
UBO ubo_vs;
std::unique_ptr<vkb::core::BufferCpp> uniform_buffer_vs;
std::unique_ptr<vkb::core::BufferCpp> vertex_buffer;
};
std::unique_ptr<vkb::Application> create_hpp_separate_image_sampler();
@@ -1,41 +0,0 @@
# 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.
#
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 "Sascha Willems"
NAME "HPP Terrain tessellation"
DESCRIPTION "Using tessellation shaders for dynamic terrain level-of-detail, using vulkan.hpp"
SHADER_FILES_GLSL
"terrain_tessellation/glsl/terrain.vert"
"terrain_tessellation/glsl/terrain.frag"
"terrain_tessellation/glsl/terrain.tesc"
"terrain_tessellation/glsl/terrain.tese"
"terrain_tessellation/glsl/skysphere.vert"
"terrain_tessellation/glsl/skysphere.frag"
SHADER_FILES_HLSL
"terrain_tessellation/hlsl/terrain.vert.hlsl"
"terrain_tessellation/hlsl/terrain.frag.hlsl"
"terrain_tessellation/hlsl/terrain.tesc.hlsl"
"terrain_tessellation/hlsl/terrain.tese.hlsl"
"terrain_tessellation/hlsl/skysphere.vert.hlsl"
"terrain_tessellation/hlsl/skysphere.frag.hlsl")
@@ -1,27 +0,0 @@
////
- Copyright (c) 2023, 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.
-
////
:pp: {plus}{plus}
= Terrain Tessellation 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/api/hpp_terrain_tessellation[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/terrain_tessellation[Terrain Tessellation] that illustrates the usage of the C{pp} bindings of Vulkan provided by vulkan.hpp.
@@ -1,615 +0,0 @@
/* 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.
*/
/*
* Dynamic terrain tessellation, using vulkan.hpp
*/
#include "hpp_terrain_tessellation.h"
#include "core/command_pool.h"
#include "heightmap.h"
HPPTerrainTessellation::HPPTerrainTessellation()
{
title = "HPP Dynamic terrain tessellation";
}
HPPTerrainTessellation::~HPPTerrainTessellation()
{
if (has_device() && get_device().get_handle())
{
vk::Device device = get_device().get_handle();
// Clean up used Vulkan resources
// Note : Inherited destructor cleans up resources stored in base class
sky_sphere.destroy(device);
terrain.destroy(device);
wireframe.destroy(device);
statistics.destroy(device);
}
}
bool HPPTerrainTessellation::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
prepare_camera();
load_assets();
generate_terrain();
prepare_uniform_buffers();
descriptor_pool = create_descriptor_pool();
prepare_sky_sphere();
prepare_terrain();
prepare_wireframe();
prepare_statistics();
build_command_buffers();
prepared = true;
}
return prepared;
}
void HPPTerrainTessellation::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
// Tessellation shader support is required for this example
auto &available_features = gpu.get_features();
if (!available_features.tessellationShader)
{
throw vkb::VulkanException(VK_ERROR_FEATURE_NOT_PRESENT, "Selected GPU does not support tessellation shaders!");
}
auto &requested_features = gpu.get_mutable_requested_features();
requested_features.tessellationShader = true;
// Fill mode non solid is required for wireframe display
requested_features.fillModeNonSolid = available_features.fillModeNonSolid;
wireframe.supported = available_features.fillModeNonSolid;
// Pipeline statistics
requested_features.pipelineStatisticsQuery = available_features.pipelineStatisticsQuery;
statistics.query_supported = available_features.pipelineStatisticsQuery;
// Enable anisotropic filtering if supported
requested_features.samplerAnisotropy = available_features.samplerAnisotropy;
terrain.sampler_anisotropy_supported = available_features.samplerAnisotropy;
}
void HPPTerrainTessellation::build_command_buffers()
{
vk::CommandBufferBeginInfo command_buffer_begin_info;
std::array<vk::ClearValue, 2> clear_values = {{default_clear_color, vk::ClearDepthStencilValue{0.0f, 0}}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = render_pass,
.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
auto command_buffer = draw_cmd_buffers[i];
command_buffer.begin(command_buffer_begin_info);
if (statistics.query_supported)
{
command_buffer.resetQueryPool(statistics.query_pool, 0, 2);
}
render_pass_begin_info.framebuffer = framebuffers[i];
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, extent};
command_buffer.setScissor(0, scissor);
vk::DeviceSize offset = 0;
// Skysphere
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, sky_sphere.pipeline);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, sky_sphere.pipeline_layout, 0, sky_sphere.descriptor_set, {});
draw_model(sky_sphere.geometry, command_buffer);
// Terrain
if (statistics.query_supported)
{
// Begin pipeline statistics query
command_buffer.beginQuery(statistics.query_pool, 0, {});
}
// Render
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, wireframe.enabled ? wireframe.pipeline : terrain.pipeline);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, terrain.pipeline_layout, 0, terrain.descriptor_set, {});
command_buffer.bindVertexBuffers(0, terrain.vertices->get_handle(), offset);
command_buffer.bindIndexBuffer(terrain.indices->get_handle(), 0, vk::IndexType::eUint32);
command_buffer.drawIndexed(terrain.index_count, 1, 0, 0, 0);
if (statistics.query_supported)
{
// End pipeline statistics query
command_buffer.endQuery(statistics.query_pool, 0);
}
draw_ui(command_buffer);
command_buffer.endRenderPass();
command_buffer.end();
}
}
void HPPTerrainTessellation::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Settings"))
{
if (drawer.checkbox("Tessellation", &terrain.tessellation_enabled))
{
update_uniform_buffers();
}
if (drawer.input_float("Factor", &terrain.tessellation.tessellation_factor, 0.05f, "%.2f"))
{
update_uniform_buffers();
}
if (wireframe.supported)
{
if (drawer.checkbox("Wireframe", &wireframe.enabled))
{
rebuild_command_buffers();
}
}
}
if (statistics.query_supported)
{
if (drawer.header("Pipeline statistics"))
{
drawer.text("VS invocations: %d", statistics.results[0]);
drawer.text("TE invocations: %d", statistics.results[1]);
}
}
}
void HPPTerrainTessellation::render(float delta_time)
{
if (prepared)
{
draw();
}
}
void HPPTerrainTessellation::view_changed()
{
update_uniform_buffers();
}
vk::DescriptorPool HPPTerrainTessellation::create_descriptor_pool()
{
std::array<vk::DescriptorPoolSize, 2> pool_sizes = {{{vk::DescriptorType::eUniformBuffer, 3}, {vk::DescriptorType::eCombinedImageSampler, 3}}};
return get_device().get_handle().createDescriptorPool(
{.maxSets = 2, .poolSizeCount = static_cast<uint32_t>(pool_sizes.size()), .pPoolSizes = pool_sizes.data()});
}
vk::DescriptorSetLayout HPPTerrainTessellation::create_sky_sphere_descriptor_set_layout()
{
std::array<vk::DescriptorSetLayoutBinding, 2> layout_bindings = {
{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}};
vk::DescriptorSetLayoutCreateInfo skysphere_descriptor_layout{.bindingCount = static_cast<uint32_t>(layout_bindings.size()),
.pBindings = layout_bindings.data()};
return get_device().get_handle().createDescriptorSetLayout(skysphere_descriptor_layout);
}
vk::Pipeline HPPTerrainTessellation::create_sky_sphere_pipeline()
{
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {
load_shader("terrain_tessellation", "skysphere.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("terrain_tessellation", "skysphere.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Vertex bindings an attributes
// Binding description
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
// Attribute descriptions
std::array<vk::VertexInputAttributeDescription, 3> vertex_input_attributes = {{{0, 0, vk::Format::eR32G32B32Sfloat, 0}, // Position
{1, 0, vk::Format::eR32G32B32Sfloat, sizeof(float) * 3}, // Normal
{2, 0, vk::Format::eR32G32Sfloat, sizeof(float) * 6}}}; // UV
vk::PipelineVertexInputStateCreateInfo vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &vertex_input_binding,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size()),
.pVertexAttributeDescriptions = vertex_input_attributes.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.depthTestEnable = true;
depth_stencil_state.depthWriteEnable = false;
depth_stencil_state.back.compareOp = vk::CompareOp::eGreater;
depth_stencil_state.front = depth_stencil_state.back;
// For the sky_sphere use triangle list topology
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eBack,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
sky_sphere.pipeline_layout,
render_pass);
}
vk::DescriptorSetLayout HPPTerrainTessellation::create_terrain_descriptor_set_layout()
{
// Terrain
std::array<vk::DescriptorSetLayoutBinding, 3> layout_bindings = {
{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eTessellationControl | vk::ShaderStageFlagBits::eTessellationEvaluation},
{1,
vk::DescriptorType::eCombinedImageSampler,
1,
vk::ShaderStageFlagBits::eTessellationControl | vk::ShaderStageFlagBits::eTessellationEvaluation | vk::ShaderStageFlagBits::eFragment},
{2, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}};
vk::DescriptorSetLayoutCreateInfo terrain_descriptor_layout{.bindingCount = static_cast<uint32_t>(layout_bindings.size()),
.pBindings = layout_bindings.data()};
return get_device().get_handle().createDescriptorSetLayout(terrain_descriptor_layout);
}
vk::Pipeline HPPTerrainTessellation::create_terrain_pipeline(vk::PolygonMode polygon_mode)
{
// Vertex bindings an attributes
// Binding description
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(HPPTerrainTessellation::Vertex), vk::VertexInputRate::eVertex};
// Attribute descriptions
std::array<vk::VertexInputAttributeDescription, 3> vertex_input_attributes = {{{0, 0, vk::Format::eR32G32B32Sfloat, 0}, // Position
{1, 0, vk::Format::eR32G32B32Sfloat, sizeof(float) * 3}, // Normal
{2, 0, vk::Format::eR32G32Sfloat, sizeof(float) * 6}}}; // UV
vk::PipelineVertexInputStateCreateInfo vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &vertex_input_binding,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size()),
.pVertexAttributeDescriptions = vertex_input_attributes.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.depthTestEnable = true;
depth_stencil_state.depthWriteEnable = true;
depth_stencil_state.back.compareOp = vk::CompareOp::eGreater;
depth_stencil_state.front = depth_stencil_state.back;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
terrain.shader_stages,
vertex_input_state,
vk::PrimitiveTopology::ePatchList,
4, // we render the terrain as a grid of quad patches
polygon_mode,
vk::CullModeFlagBits::eBack,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
terrain.pipeline_layout,
render_pass);
}
void HPPTerrainTessellation::draw()
{
HPPApiVulkanSample::prepare_frame();
// Command buffer to be submitted to the queue
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
// Submit to queue
queue.submit(submit_info);
if (statistics.query_supported)
{
// Read query results for displaying in next frame
auto result = get_device()
.get_handle()
.getQueryPoolResult<std::array<uint64_t, 2>>(statistics.query_pool, 0, 1, sizeof(statistics.results), vk::QueryResultFlagBits::e64);
if (result.result == vk::Result::eSuccess)
{
statistics.results = result.value;
}
}
HPPApiVulkanSample::submit_frame();
}
// Generate a terrain quad patch for feeding to the tessellation control shader
void HPPTerrainTessellation::generate_terrain()
{
const uint32_t patch_size = 64;
const float uv_scale = 1.0f;
const uint32_t vertex_count = patch_size * patch_size;
std::vector<Vertex> vertices;
vertices.resize(vertex_count);
for (auto x = 0; x < patch_size; x++)
{
for (auto y = 0; y < patch_size; y++)
{
uint32_t index = (x + y * patch_size);
vertices[index].pos[0] = 2.0f * x + 1.0f - patch_size;
vertices[index].pos[1] = 0.0f;
vertices[index].pos[2] = 2.0f * y * 1.0f - patch_size;
vertices[index].uv = glm::vec2(static_cast<float>(x) / patch_size, static_cast<float>(y) / patch_size) * uv_scale;
}
}
// Calculate normals from height map using a sobel filter
vkb::HeightMap height_map("textures/terrain_heightmap_r16.ktx", patch_size);
for (auto x = 0; x < patch_size; x++)
{
for (auto y = 0; y < patch_size; y++)
{
// Get height samples centered around current position
float heights[3][3];
for (auto hx = -1; hx <= 1; hx++)
{
for (auto hy = -1; hy <= 1; hy++)
{
heights[hx + 1][hy + 1] = height_map.get_height(x + hx, y + hy);
}
}
// Calculate the normal
glm::vec3 normal;
// Gx sobel filter
normal.x = heights[0][0] - heights[2][0] + 2.0f * heights[0][1] - 2.0f * heights[2][1] + heights[0][2] - heights[2][2];
// Gy sobel filter
normal.z = heights[0][0] + 2.0f * heights[1][0] + heights[2][0] - heights[0][2] - 2.0f * heights[1][2] - heights[2][2];
// Calculate missing up component of the normal using the filtered x and y axis
// The first value controls the bump strength
normal.y = 0.25f * sqrt(1.0f - normal.x * normal.x - normal.z * normal.z);
vertices[x + y * patch_size].normal = glm::normalize(normal * glm::vec3(2.0f, 1.0f, 2.0f));
}
}
// Indices
const uint32_t w = (patch_size - 1);
const uint32_t index_count = w * w * 4;
std::vector<uint32_t> indices;
indices.resize(index_count);
for (auto x = 0; x < w; x++)
{
for (auto y = 0; y < w; y++)
{
uint32_t index = (x + y * w) * 4;
indices[index] = (x + y * patch_size);
indices[index + 1] = indices[index] + patch_size;
indices[index + 2] = indices[index + 1] + 1;
indices[index + 3] = indices[index] + 1;
}
}
terrain.index_count = index_count;
uint32_t vertex_buffer_size = vertex_count * sizeof(Vertex);
uint32_t index_buffer_size = index_count * sizeof(uint32_t);
// Create staging buffers
vkb::core::BufferCpp vertex_staging(get_device(), vertex_buffer_size, vk::BufferUsageFlagBits::eTransferSrc, VMA_MEMORY_USAGE_CPU_TO_GPU);
vertex_staging.update(vertices);
vkb::core::BufferCpp index_staging(get_device(), index_buffer_size, vk::BufferUsageFlagBits::eTransferSrc, VMA_MEMORY_USAGE_CPU_TO_GPU);
index_staging.update(indices);
terrain.vertices = std::make_unique<vkb::core::BufferCpp>(
get_device(), vertex_buffer_size, vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst, VMA_MEMORY_USAGE_GPU_ONLY);
terrain.indices = std::make_unique<vkb::core::BufferCpp>(
get_device(), index_buffer_size, vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eTransferDst, VMA_MEMORY_USAGE_GPU_ONLY);
// Copy from staging buffers
vk::CommandBuffer copy_command = vkb::common::allocate_command_buffer(get_device().get_handle(), get_device().get_command_pool().get_handle());
copy_command.begin(vk::CommandBufferBeginInfo());
copy_command.copyBuffer(vertex_staging.get_handle(), terrain.vertices->get_handle(), {{0, 0, vertex_buffer_size}});
copy_command.copyBuffer(index_staging.get_handle(), terrain.indices->get_handle(), {{0, 0, index_buffer_size}});
get_device().flush_command_buffer(copy_command, queue, true);
}
void HPPTerrainTessellation::load_assets()
{
sky_sphere.geometry = load_model("scenes/geosphere.gltf");
sky_sphere.texture = load_texture("textures/skysphere_rgba.ktx", vkb::scene_graph::components::HPPImage::Color);
// Terrain textures are stored in a texture array with layers corresponding to terrain height; create a repeating sampler
terrain.terrain_array = load_texture_array("textures/terrain_texturearray_rgba.ktx", vkb::scene_graph::components::HPPImage::Color, vk::SamplerAddressMode::eRepeat);
// Height data is stored in a one-channel texture; create a mirroring sampler
terrain.height_map = load_texture("textures/terrain_heightmap_r16.ktx", vkb::scene_graph::components::HPPImage::Other, vk::SamplerAddressMode::eMirroredRepeat);
}
void HPPTerrainTessellation::prepare_camera()
{
// Note: Using reversed depth-buffer for increased precision, so Znear and Zfar are flipped
camera.type = vkb::CameraType::FirstPerson;
camera.set_perspective(60.0f, static_cast<float>(extent.width) / static_cast<float>(extent.height), 512.0f, 0.1f);
camera.set_rotation(glm::vec3(-12.0f, 159.0f, 0.0f));
camera.set_translation(glm::vec3(18.0f, 22.5f, 57.5f));
camera.translation_speed = 7.5f;
}
void HPPTerrainTessellation::prepare_sky_sphere()
{
sky_sphere.descriptor_set_layout = create_sky_sphere_descriptor_set_layout();
sky_sphere.pipeline_layout = get_device().get_handle().createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &sky_sphere.descriptor_set_layout});
sky_sphere.pipeline = create_sky_sphere_pipeline();
sky_sphere.descriptor_set = vkb::common::allocate_descriptor_set(get_device().get_handle(), descriptor_pool, {sky_sphere.descriptor_set_layout});
update_sky_sphere_descriptor_set();
}
void HPPTerrainTessellation::prepare_statistics()
{
if (statistics.query_supported)
{
// Create query pool
statistics.query_pool = vkb::common::create_query_pool(get_device().get_handle(),
vk::QueryType::ePipelineStatistics,
2,
vk::QueryPipelineStatisticFlagBits::eVertexShaderInvocations |
vk::QueryPipelineStatisticFlagBits::eTessellationEvaluationShaderInvocations);
}
}
void HPPTerrainTessellation::prepare_terrain()
{
terrain.shader_stages = {load_shader("terrain_tessellation", "terrain.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("terrain_tessellation", "terrain.frag.spv", vk::ShaderStageFlagBits::eFragment),
load_shader("terrain_tessellation", "terrain.tesc.spv", vk::ShaderStageFlagBits::eTessellationControl),
load_shader("terrain_tessellation", "terrain.tese.spv", vk::ShaderStageFlagBits::eTessellationEvaluation)};
terrain.descriptor_set_layout = create_terrain_descriptor_set_layout();
terrain.pipeline_layout = get_device().get_handle().createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &terrain.descriptor_set_layout});
terrain.pipeline = create_terrain_pipeline(vk::PolygonMode::eFill);
terrain.descriptor_set = vkb::common::allocate_descriptor_set(get_device().get_handle(), descriptor_pool, {terrain.descriptor_set_layout});
update_terrain_descriptor_set();
}
// Prepare and initialize uniform buffer containing shader uniforms
void HPPTerrainTessellation::prepare_uniform_buffers()
{
// Shared tessellation shader stages uniform buffer
terrain.tessellation_buffer =
std::make_unique<vkb::core::BufferCpp>(get_device(), sizeof(terrain.tessellation), vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
// Skysphere vertex shader uniform buffer
sky_sphere.transform_buffer =
std::make_unique<vkb::core::BufferCpp>(get_device(), sizeof(sky_sphere.transform), vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
}
void HPPTerrainTessellation::prepare_wireframe()
{
if (wireframe.supported)
{
// wireframe mode uses nearly the same settings as the terrain mode... just vk::PolygonMode::eLine, instead of vk::PolygonMode::eFill
wireframe.pipeline = create_terrain_pipeline(vk::PolygonMode::eLine);
};
}
void HPPTerrainTessellation::update_uniform_buffers()
{
// Tessellation
terrain.tessellation.projection = camera.matrices.perspective;
terrain.tessellation.modelview = camera.matrices.view * glm::mat4(1.0f);
terrain.tessellation.light_pos.y = -0.5f - terrain.tessellation.displacement_factor; // todo: Not used yet
terrain.tessellation.viewport_dim = glm::vec2(static_cast<float>(extent.width), static_cast<float>(extent.height));
frustum.update(terrain.tessellation.projection * terrain.tessellation.modelview);
memcpy(terrain.tessellation.frustum_planes, frustum.get_planes().data(), sizeof(glm::vec4) * 6);
float saved_factor = terrain.tessellation.tessellation_factor;
if (!terrain.tessellation_enabled)
{
// Setting this to zero sets all tessellation factors to 1.0 in the shader
terrain.tessellation.tessellation_factor = 0.0f;
}
terrain.tessellation_buffer->convert_and_update(terrain.tessellation);
if (!terrain.tessellation_enabled)
{
terrain.tessellation.tessellation_factor = saved_factor;
}
// Skysphere vertex shader
sky_sphere.transform = camera.matrices.perspective * glm::mat4(glm::mat3(camera.matrices.view));
sky_sphere.transform_buffer->convert_and_update(sky_sphere.transform);
}
void HPPTerrainTessellation::update_sky_sphere_descriptor_set()
{
vk::DescriptorBufferInfo skysphere_buffer_descriptor{sky_sphere.transform_buffer->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo skysphere_image_descriptor{sky_sphere.texture.sampler,
sky_sphere.texture.image->get_vk_image_view().get_handle(),
descriptor_type_to_image_layout(vk::DescriptorType::eCombinedImageSampler,
sky_sphere.texture.image->get_vk_image_view().get_format())};
std::array<vk::WriteDescriptorSet, 2> skysphere_write_descriptor_sets = {{{.dstSet = sky_sphere.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &skysphere_buffer_descriptor},
{.dstSet = sky_sphere.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &skysphere_image_descriptor}}};
get_device().get_handle().updateDescriptorSets(skysphere_write_descriptor_sets, {});
}
void HPPTerrainTessellation::update_terrain_descriptor_set()
{
vk::DescriptorBufferInfo terrain_buffer_descriptor{terrain.tessellation_buffer->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo heightmap_image_descriptor{terrain.height_map.sampler,
terrain.height_map.image->get_vk_image_view().get_handle(),
descriptor_type_to_image_layout(vk::DescriptorType::eCombinedImageSampler,
terrain.height_map.image->get_vk_image_view().get_format())};
vk::DescriptorImageInfo terrainmap_image_descriptor{terrain.terrain_array.sampler,
terrain.terrain_array.image->get_vk_image_view().get_handle(),
descriptor_type_to_image_layout(vk::DescriptorType::eCombinedImageSampler,
terrain.terrain_array.image->get_vk_image_view().get_format())};
std::array<vk::WriteDescriptorSet, 3> terrain_write_descriptor_sets = {{{.dstSet = terrain.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &terrain_buffer_descriptor},
{.dstSet = terrain.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &heightmap_image_descriptor},
{.dstSet = terrain.descriptor_set,
.dstBinding = 2,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &terrainmap_image_descriptor}}};
get_device().get_handle().updateDescriptorSets(terrain_write_descriptor_sets, {});
}
std::unique_ptr<vkb::Application> create_hpp_terrain_tessellation()
{
return std::make_unique<HPPTerrainTessellation>();
}
@@ -1,182 +0,0 @@
/* 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.
*/
/*
* Dynamic terrain tessellation, using vulkan.hpp
*/
#pragma once
#include <hpp_api_vulkan_sample.h>
#include <geometry/frustum.h>
class HPPTerrainTessellation : public HPPApiVulkanSample
{
public:
HPPTerrainTessellation();
~HPPTerrainTessellation();
private:
struct SkySphere
{
vk::DescriptorSet descriptor_set;
vk::DescriptorSetLayout descriptor_set_layout;
vk::PipelineLayout pipeline_layout;
vk::Pipeline pipeline;
std::unique_ptr<vkb::scene_graph::components::HPPSubMesh> geometry;
HPPTexture texture;
glm::mat4 transform;
std::unique_ptr<vkb::core::BufferCpp> transform_buffer;
void destroy(vk::Device device)
{
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
// the descriptor_set is implicitly freed by destroying its DescriptorPool
device.destroySampler(texture.sampler);
}
};
struct Statistics
{
bool query_supported = false;
vk::QueryPool query_pool;
std::array<uint64_t, 2> results = {{0}};
void destroy(vk::Device device)
{
if (query_supported)
{
device.destroyQueryPool(query_pool);
}
}
};
// Shared values for tessellation control and evaluation stages
struct Tessellation
{
glm::mat4 projection;
glm::mat4 modelview;
glm::vec4 light_pos = glm::vec4(-48.0f, -40.0f, 46.0f, 0.0f);
glm::vec4 frustum_planes[6];
float displacement_factor = 32.0f;
float tessellation_factor = 0.75f;
glm::vec2 viewport_dim;
// Desired size of tessellated quad patch edge
float tessellated_edge_size = 20.0f;
};
struct Terrain
{
vk::DescriptorSet descriptor_set;
vk::DescriptorSetLayout descriptor_set_layout;
vk::PipelineLayout pipeline_layout;
vk::Pipeline pipeline;
std::unique_ptr<vkb::core::BufferCpp> vertices;
std::unique_ptr<vkb::core::BufferCpp> indices;
uint32_t index_count;
HPPTexture height_map;
HPPTexture terrain_array;
bool sampler_anisotropy_supported = false;
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages;
Tessellation tessellation;
std::unique_ptr<vkb::core::BufferCpp> tessellation_buffer;
bool tessellation_enabled = true;
void destroy(vk::Device device)
{
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
// the descriptor_set is implicitly freed by destroying its DescriptorPool
device.destroySampler(height_map.sampler);
device.destroySampler(terrain_array.sampler);
}
};
struct Vertex
{
glm::vec3 pos;
glm::vec3 normal;
glm::vec2 uv;
};
struct Wireframe
{
bool supported = false;
bool enabled = false;
vk::Pipeline pipeline;
void destroy(vk::Device device)
{
device.destroyPipeline(pipeline);
}
};
private:
// from vkb::Application
bool prepare(const vkb::ApplicationOptions &options) override;
// from vkb::VulkanSample
void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
void build_command_buffers() override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
void render(float delta_time) override;
void view_changed() override;
vk::DescriptorPool create_descriptor_pool();
vk::DescriptorSetLayout create_sky_sphere_descriptor_set_layout();
vk::Pipeline create_sky_sphere_pipeline();
vk::DescriptorSetLayout create_terrain_descriptor_set_layout();
vk::Pipeline create_terrain_pipeline(vk::PolygonMode polygon_mode);
void draw();
void generate_terrain();
void load_assets();
void prepare_camera();
void prepare_sky_sphere();
void prepare_statistics();
void prepare_terrain();
void prepare_uniform_buffers();
void prepare_wireframe();
void update_uniform_buffers();
void update_sky_sphere_descriptor_set();
void update_terrain_descriptor_set();
private:
vkb::Frustum frustum; // View frustum passed to tessellation control shader for culling
SkySphere sky_sphere;
Statistics statistics;
Terrain terrain;
Wireframe wireframe;
};
std::unique_ptr<vkb::Application> create_hpp_terrain_tessellation();
@@ -1,33 +0,0 @@
# Copyright (c) 2021-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 "Sascha Willems"
NAME "HPP Texture loading and mapping"
DESCRIPTION "Loading a texture and displaying it on a simple object, using vulkan.hpp"
SHADER_FILES_GLSL
"texture_loading/glsl/texture.vert"
"texture_loading/glsl/texture.frag"
SHADER_FILES_HLSL
"texture_loading/hlsl/texture.vert.hlsl"
"texture_loading/hlsl/texture.frag.hlsl")
@@ -1,27 +0,0 @@
////
- Copyright (c) 2020-2023, 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.
-
////
:pp: {plus}{plus}
= Texture Loading 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/api/hpp_texture_loading[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/texture_loading[Texture loading] that illustrates the usage of the C{pp} bindings of Vulkan provided by vulkan.hpp.
@@ -1,591 +0,0 @@
/* Copyright (c) 2021-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.
*/
/*
* Texture loading (and display) example (including mip maps), using vulkan.hpp
*/
#include "hpp_texture_loading.h"
#include "common/ktx_common.h"
#include "core/command_pool.h"
HPPTextureLoading::HPPTextureLoading()
{
title = "HPP Texture loading";
zoom = -2.5f;
rotation = {0.0f, 15.0f, 0.0f};
}
HPPTextureLoading::~HPPTextureLoading()
{
if (has_device() && get_device().get_handle())
{
vk::Device device = get_device().get_handle();
// Clean up used Vulkan resources
// Note : Inherited destructor cleans up resources stored in base class
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
texture.destroy(device);
}
vertex_buffer.reset();
index_buffer.reset();
vertex_shader_data_buffer.reset();
}
bool HPPTextureLoading::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
load_texture();
generate_quad();
prepare_uniform_buffers();
descriptor_set_layout = create_descriptor_set_layout();
pipeline_layout = get_device().get_handle().createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &descriptor_set_layout});
pipeline = create_pipeline();
descriptor_pool = create_descriptor_pool();
descriptor_set = vkb::common::allocate_descriptor_set(get_device().get_handle(), descriptor_pool, {descriptor_set_layout});
update_descriptor_set();
build_command_buffers();
prepared = true;
}
return prepared;
}
// Enable physical device features required for this example
void HPPTextureLoading::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = true;
}
}
void HPPTextureLoading::build_command_buffers()
{
vk::CommandBufferBeginInfo command_buffer_begin_info;
vk::ClearValue clear_values[2];
clear_values[0].color = default_clear_color;
clear_values[1].depthStencil = vk::ClearDepthStencilValue{0.0f, 0};
vk::RenderPassBeginInfo render_pass_begin_info;
render_pass_begin_info.renderPass = render_pass;
render_pass_begin_info.renderArea.offset.x = 0;
render_pass_begin_info.renderArea.offset.y = 0;
render_pass_begin_info.renderArea.extent = extent;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.pClearValues = clear_values;
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
auto command_buffer = draw_cmd_buffers[i];
// Set target frame buffer
render_pass_begin_info.framebuffer = framebuffers[i];
command_buffer.begin(command_buffer_begin_info);
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, extent};
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
vk::DeviceSize offset = 0;
command_buffer.bindVertexBuffers(0, vertex_buffer->get_handle(), offset);
command_buffer.bindIndexBuffer(index_buffer->get_handle(), 0, vk::IndexType::eUint32);
command_buffer.drawIndexed(index_count, 1, 0, 0, 0);
draw_ui(command_buffer);
command_buffer.endRenderPass();
command_buffer.end();
}
}
void HPPTextureLoading::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Settings"))
{
if (drawer.slider_float("LOD bias", &vertex_shader_data.lod_bias, 0.0f, static_cast<float>(texture.mip_levels)))
{
update_uniform_buffers();
}
}
}
void HPPTextureLoading::render(float delta_time)
{
if (prepared)
{
draw();
}
}
void HPPTextureLoading::view_changed()
{
update_uniform_buffers();
}
vk::DescriptorPool HPPTextureLoading::create_descriptor_pool()
{
// Example uses one ubo and one image sampler
std::array<vk::DescriptorPoolSize, 2> pool_sizes = {{{vk::DescriptorType::eUniformBuffer, 1}, {vk::DescriptorType::eCombinedImageSampler, 1}}};
return get_device().get_handle().createDescriptorPool(
{.maxSets = 2, .poolSizeCount = static_cast<uint32_t>(pool_sizes.size()), .pPoolSizes = pool_sizes.data()});
}
vk::DescriptorSetLayout HPPTextureLoading::create_descriptor_set_layout()
{
std::array<vk::DescriptorSetLayoutBinding, 2> set_layout_bindings = {
{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}};
return get_device().get_handle().createDescriptorSetLayout(
{.bindingCount = static_cast<uint32_t>(set_layout_bindings.size()), .pBindings = set_layout_bindings.data()});
}
vk::Pipeline HPPTextureLoading::create_pipeline()
{
// Load shaders
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {{load_shader("texture_loading", "texture.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("texture_loading", "texture.frag.spv", vk::ShaderStageFlagBits::eFragment)}};
// Vertex bindings and attributes
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(Vertex), vk::VertexInputRate::eVertex};
std::array<vk::VertexInputAttributeDescription, 3> vertex_input_attributes = {
{{0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos)}, // Location 0 : Position
{1, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, uv)}, // Location 1: Texture Coordinates
{2, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, normal)}}}; // Location 2 : Normal
vk::PipelineVertexInputStateCreateInfo vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &vertex_input_binding,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size()),
.pVertexAttributeDescriptions = vertex_input_attributes.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthTestEnable = true;
depth_stencil_state.depthWriteEnable = true;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.back.compareOp = vk::CompareOp::eGreater;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
pipeline_layout,
render_pass);
}
void HPPTextureLoading::draw()
{
HPPApiVulkanSample::prepare_frame();
// Command buffer to be submitted to the queue
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
// Submit to queue
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
}
void HPPTextureLoading::generate_quad()
{
// Setup vertices for a single uv-mapped quad made from two triangles
std::vector<Vertex> vertices = {{{1.0f, 1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, 1.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, -1.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}},
{{1.0f, -1.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}}};
// Setup indices
std::vector<uint32_t> indices = {0, 1, 2, 2, 3, 0};
index_count = static_cast<uint32_t>(indices.size());
auto vertex_buffer_size = vkb::to_u32(vertices.size() * sizeof(Vertex));
auto index_buffer_size = vkb::to_u32(indices.size() * sizeof(uint32_t));
// Create buffers
// For the sake of simplicity we won't stage the vertex data to the gpu memory
// Vertex buffer
vertex_buffer = std::make_unique<vkb::core::BufferCpp>(
get_device(), vertex_buffer_size, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
vertex_buffer->update(vertices.data(), vertex_buffer_size);
// Index buffer
index_buffer = std::make_unique<vkb::core::BufferCpp>(
get_device(), index_buffer_size, vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
index_buffer->update(indices.data(), index_buffer_size);
}
/*
Upload texture image data to the GPU
Vulkan offers two types of image tiling (memory layout):
Linear tiled images:
These are stored as is and can be copied directly to. But due to the linear nature they're not a good match for GPUs and format and feature support is very limited.
It's not advised to use linear tiled images for anything else than copying from host to GPU if buffer copies are not an option.
Linear tiling is thus only implemented for learning purposes, one should always prefer optimal tiled image.
Optimal tiled images:
These are stored in an implementation specific layout matching the capability of the hardware. They usually support more formats and features and are much faster.
Optimal tiled images are stored on the device and not accessible by the host. So they can't be written directly to (like liner tiled images) and always require
some sort of data copy, either from a buffer or a linear tiled image.
In Short: Always use optimal tiled images for rendering.
*/
void HPPTextureLoading::load_texture()
{
// We use the Khronos texture format (https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/)
const std::string filename = vkb::fs::path::get(vkb::fs::path::Assets, "textures/metalplate01_rgba.ktx");
// ktx1 doesn't know whether the content is sRGB or linear, but most tools save in sRGB, so assume that.
constexpr vk::Format format = vk::Format::eR8G8B8A8Srgb;
ktxTexture *ktx_texture = vkb::ktx::load_texture(filename);
texture.extent = vk::Extent2D{ktx_texture->baseWidth, ktx_texture->baseHeight};
texture.mip_levels = ktx_texture->numLevels;
// We prefer using staging to copy the texture data to a device local optimal image
vk::Bool32 use_staging = true;
// Only use linear tiling if forced
bool force_linear_tiling = false;
if (force_linear_tiling)
{
// Don't use linear if format is not supported for (linear) shader sampling
// Get device properties for the requested texture format
vk::FormatProperties format_properties = get_device().get_gpu().get_handle().getFormatProperties(format);
use_staging = !(format_properties.linearTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage);
}
vk::Device device = get_device().get_handle();
if (use_staging)
{
// Copy data to an optimal tiled image
// This loads the texture data into a host local buffer that is copied to the optimal tiled image on the device
// Create a host-visible staging buffer that contains the raw image data
// This buffer will be the data source for copying texture data to the optimal tiled image on the device
// This buffer is used as a transfer source for the buffer copy
vk::BufferCreateInfo buffer_create_info{.size = ktx_texture->dataSize,
.usage = vk::BufferUsageFlagBits::eTransferSrc,
.sharingMode = vk::SharingMode::eExclusive};
vk::Buffer staging_buffer = device.createBuffer(buffer_create_info);
// Get memory requirements for the staging buffer (alignment, memory type bits)
vk::MemoryRequirements memory_requirements = device.getBufferMemoryRequirements(staging_buffer);
// Get memory type index for a host visible buffer
uint32_t memory_type = get_device().get_gpu().get_memory_type(memory_requirements.memoryTypeBits,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
vk::MemoryAllocateInfo memory_allocate_info{.allocationSize = memory_requirements.size, .memoryTypeIndex = memory_type};
vk::DeviceMemory staging_memory = device.allocateMemory(memory_allocate_info);
device.bindBufferMemory(staging_buffer, staging_memory, 0);
// Copy texture data into host local staging buffer
uint8_t *data = reinterpret_cast<uint8_t *>(device.mapMemory(staging_memory, 0, memory_requirements.size));
memcpy(data, ktx_texture->pData, ktx_texture->dataSize);
device.unmapMemory(staging_memory);
// Setup buffer copy regions for each mip level
std::vector<vk::BufferImageCopy> buffer_copy_regions(texture.mip_levels);
for (uint32_t i = 0; i < texture.mip_levels; i++)
{
ktx_size_t offset;
KTX_error_code result = ktxTexture_GetImageOffset(ktx_texture, i, 0, 0, &offset);
buffer_copy_regions[i].imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
buffer_copy_regions[i].imageSubresource.mipLevel = i;
buffer_copy_regions[i].imageSubresource.baseArrayLayer = 0;
buffer_copy_regions[i].imageSubresource.layerCount = 1;
buffer_copy_regions[i].imageExtent.width = ktx_texture->baseWidth >> i;
buffer_copy_regions[i].imageExtent.height = ktx_texture->baseHeight >> i;
buffer_copy_regions[i].imageExtent.depth = 1;
buffer_copy_regions[i].bufferOffset = offset;
}
// Create optimal tiled target image on the device
vk::ImageCreateInfo image_create_info{
.imageType = vk::ImageType::e2D,
.format = format,
.extent = {texture.extent.width, texture.extent.height, 1},
.mipLevels = texture.mip_levels,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.tiling = vk::ImageTiling::eOptimal,
.usage = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled,
.sharingMode = vk::SharingMode::eExclusive,
.initialLayout = vk::ImageLayout::eUndefined // Set initial layout of the image to undefined
};
texture.image = device.createImage(image_create_info);
memory_requirements = device.getImageMemoryRequirements(texture.image);
memory_type = get_device().get_gpu().get_memory_type(memory_requirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eDeviceLocal);
memory_allocate_info = vk::MemoryAllocateInfo{.allocationSize = memory_requirements.size, .memoryTypeIndex = memory_type};
texture.device_memory = device.allocateMemory(memory_allocate_info);
device.bindImageMemory(texture.image, texture.device_memory, 0);
vk::CommandBuffer copy_command = vkb::common::allocate_command_buffer(device, get_device().get_command_pool().get_handle());
copy_command.begin(vk::CommandBufferBeginInfo());
// Image memory barriers for the texture image
// The sub resource range describes the regions of the image that will be transitioned using the memory barriers below
vk::ImageSubresourceRange subresource_range{
.aspectMask = vk::ImageAspectFlagBits::eColor, // Image contains only color data
.baseMipLevel = 0, // Start at first mip level
.levelCount = texture.mip_levels, // We will transition on all mip levels
.layerCount = 1 // The 2D texture only has one layer
};
// Transition the texture image layout to transfer target, so we can safely copy our buffer data to it.
vk::ImageMemoryBarrier image_memory_barrier;
image_memory_barrier.image = texture.image;
image_memory_barrier.subresourceRange = subresource_range;
image_memory_barrier.srcAccessMask = {};
image_memory_barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
image_memory_barrier.oldLayout = vk::ImageLayout::eUndefined;
image_memory_barrier.newLayout = vk::ImageLayout::eTransferDstOptimal;
image_memory_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
image_memory_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
// Insert a memory dependency at the proper pipeline stages that will execute the image layout transition
// Source pipeline stage is host write/read execution (VK_PIPELINE_STAGE_HOST_BIT)
// Destination pipeline stage is copy command execution (VK_PIPELINE_STAGE_TRANSFER_BIT)
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eHost, vk::PipelineStageFlagBits::eTransfer, {}, nullptr, nullptr, image_memory_barrier);
// Copy mip levels from staging buffer
copy_command.copyBufferToImage(staging_buffer, texture.image, vk::ImageLayout::eTransferDstOptimal, buffer_copy_regions);
// Once the data has been uploaded we transfer the texture image to the shader read layout, so it can be sampled from
image_memory_barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
image_memory_barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
image_memory_barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
image_memory_barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
// Insert a memory dependency at the proper pipeline stages that will execute the image layout transition
// Source pipeline stage stage is copy command execution (VK_PIPELINE_STAGE_TRANSFER_BIT)
// Destination pipeline stage fragment shader access (VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT)
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, nullptr, nullptr, image_memory_barrier);
// Store current layout for later reuse
texture.image_layout = vk::ImageLayout::eShaderReadOnlyOptimal;
get_device().flush_command_buffer(copy_command, queue, true);
// Clean up staging resources
device.destroyBuffer(staging_buffer);
device.freeMemory(staging_memory);
}
else
{
// Copy data to a linear tiled image
// Load mip map level 0 to linear tiling image
vk::ImageCreateInfo image_create_info{.imageType = vk::ImageType::e2D,
.format = format,
.extent = {texture.extent.width, texture.extent.height, 1},
.mipLevels = 1,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.tiling = vk::ImageTiling::eLinear,
.usage = vk::ImageUsageFlagBits::eSampled,
.sharingMode = vk::SharingMode::eExclusive,
.initialLayout = vk::ImageLayout::ePreinitialized};
vk::Image mappable_image = device.createImage(image_create_info);
// Get memory requirements for this image like size and alignment
vk::MemoryRequirements memory_requirements = device.getImageMemoryRequirements(mappable_image);
// Get memory type that can be mapped to host memory
uint32_t memory_type = get_device().get_gpu().get_memory_type(memory_requirements.memoryTypeBits,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
// Set memory allocation size to required memory size
vk::MemoryAllocateInfo memory_allocate_info{.allocationSize = memory_requirements.size, .memoryTypeIndex = memory_type};
vk::DeviceMemory mappable_memory = device.allocateMemory(memory_allocate_info);
device.bindImageMemory(mappable_image, mappable_memory, 0);
// Map image memory
void *data = device.mapMemory(mappable_memory, 0, memory_requirements.size);
// Copy image data of the first mip level into memory
memcpy(data, ktx_texture->pData, ktxTexture_GetImageSize(ktx_texture, 0));
device.unmapMemory(mappable_memory);
// Linear tiled images don't need to be staged and can be directly used as textures
texture.image = mappable_image;
texture.device_memory = mappable_memory;
texture.image_layout = vk::ImageLayout::eShaderReadOnlyOptimal;
// Setup image memory barrier transfer image to shader read layout
vk::CommandBuffer copy_command = vkb::common::allocate_command_buffer(device, get_device().get_command_pool().get_handle());
copy_command.begin(vk::CommandBufferBeginInfo());
// The sub resource range describes the regions of the image we will be transition
vk::ImageSubresourceRange subresource_range{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
// Transition the texture image layout to shader read, so it can be sampled from
vk::ImageMemoryBarrier image_memory_barrier;
image_memory_barrier.image = texture.image;
image_memory_barrier.subresourceRange = subresource_range;
image_memory_barrier.srcAccessMask = vk::AccessFlagBits::eHostWrite;
image_memory_barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
image_memory_barrier.oldLayout = vk::ImageLayout::ePreinitialized;
image_memory_barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
image_memory_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
image_memory_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
// Insert a memory dependency at the proper pipeline stages that will execute the image layout transition
// Source pipeline stage is host write/read execution (VK_PIPELINE_STAGE_HOST_BIT)
// Destination pipeline stage fragment shader access (VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT)
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eHost, vk::PipelineStageFlagBits::eFragmentShader, {}, nullptr, nullptr, image_memory_barrier);
get_device().flush_command_buffer(copy_command, queue, true);
}
// now, the ktx_texture can be destroyed
ktxTexture_Destroy(ktx_texture);
// Create a texture sampler
// In Vulkan textures are accessed by samplers
// This separates all the sampling information from the texture data. This means you could have multiple sampler objects for the same texture with different settings
// Note: Similar to the samplers available with OpenGL 3.3
// Enable anisotropic filtering
// This feature is optional, so we must check if it's supported on the device
float maxAnisotropy = 1.0f;
if (get_device().get_gpu().get_features().samplerAnisotropy)
{
// Use max. level of anisotropy for this example
maxAnisotropy = get_device().get_gpu().get_properties().limits.maxSamplerAnisotropy;
}
texture.sampler = vkb::common::create_sampler(get_device().get_gpu().get_handle(), get_device().get_handle(),
format, vk::Filter::eLinear, vk::SamplerAddressMode::eClampToEdge,
maxAnisotropy, (use_staging) ? static_cast<float>(texture.mip_levels) : 0.0f);
// Create image view
// Textures are not directly accessed by the shaders and
// are abstracted by image views containing additional
// information and sub resource ranges
vk::ImageViewCreateInfo image_view_create_info;
image_view_create_info.viewType = vk::ImageViewType::e2D;
image_view_create_info.format = format;
image_view_create_info.components = {vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eG, vk::ComponentSwizzle::eB, vk::ComponentSwizzle::eA};
// The subresource range describes the set of mip levels (and array layers) that can be accessed through this image image_view_create_info
// It's possible to create multiple image views for a single image referring to different (and/or overlapping) ranges of the image
image_view_create_info.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
image_view_create_info.subresourceRange.baseMipLevel = 0;
image_view_create_info.subresourceRange.baseArrayLayer = 0;
image_view_create_info.subresourceRange.layerCount = 1;
// Linear tiling usually won't support mip maps
// Only set mip map count if optimal tiling is used
image_view_create_info.subresourceRange.levelCount = (use_staging) ? texture.mip_levels : 1;
// The image_view_create_info will be based on the texture's image
image_view_create_info.image = texture.image;
texture.image_view = device.createImageView(image_view_create_info);
}
// Prepare and initialize uniform buffer containing shader uniforms
void HPPTextureLoading::prepare_uniform_buffers()
{
// Vertex shader uniform buffer block
vertex_shader_data_buffer =
std::make_unique<vkb::core::BufferCpp>(get_device(), sizeof(vertex_shader_data), vk::BufferUsageFlagBits::eUniformBuffer, VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
}
void HPPTextureLoading::update_descriptor_set()
{
vk::DescriptorBufferInfo buffer_descriptor{vertex_shader_data_buffer->get_handle(), 0, vk::WholeSize};
// Setup a descriptor image info for the current texture to be used as a combined image sampler
vk::DescriptorImageInfo image_descriptor{
texture.sampler, // The sampler (the sampler describes how to sample the image, including repeat, border, etc.)
texture.image_view, // The image view (the image view describes the image and the subresources that can be accessed)
texture.image_layout // The current layout of the image (Note: Should always fit the actual use, e.g. shader read)
};
std::array<vk::WriteDescriptorSet, 2> write_descriptor_sets = {{// Binding 0 : Vertex shader uniform buffer
{.dstSet = descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &buffer_descriptor},
// Binding 1 : Fragment shader texture sampler
// Fragment shader: layout (binding = 1) uniform sampler2D samplerColor;
{.dstSet = descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &image_descriptor}}};
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, {});
}
void HPPTextureLoading::update_uniform_buffers()
{
// Vertex shader
vertex_shader_data.projection = glm::perspective(glm::radians(60.0f), static_cast<float>(extent.width) / static_cast<float>(extent.height), 0.001f, 256.0f);
glm::mat4 view_matrix = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, zoom));
vertex_shader_data.model = view_matrix * glm::translate(glm::mat4(1.0f), camera_pos);
vertex_shader_data.model = glm::rotate(vertex_shader_data.model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
vertex_shader_data.model = glm::rotate(vertex_shader_data.model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
vertex_shader_data.model = glm::rotate(vertex_shader_data.model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
vertex_shader_data.view_pos = glm::vec4(0.0f, 0.0f, -zoom, 0.0f);
vertex_shader_data_buffer->convert_and_update(vertex_shader_data);
}
std::unique_ptr<vkb::Application> create_hpp_texture_loading()
{
return std::make_unique<HPPTextureLoading>();
}
@@ -1,106 +0,0 @@
/* Copyright (c) 2021-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.
*/
/*
* Texture loading (and display) example (including mip maps), using vulkan.hpp
*/
#pragma once
#include <hpp_api_vulkan_sample.h>
class HPPTextureLoading : public HPPApiVulkanSample
{
public:
HPPTextureLoading();
~HPPTextureLoading();
private:
// Contains all Vulkan objects that are required to store and use a texture
// Note that this repository contains a texture class (vulkan_texture.h) that encapsulates texture loading functionality in a class that is used in subsequent demos
struct Texture
{
vk::DeviceMemory device_memory;
vk::Image image;
vk::ImageLayout image_layout;
vk::ImageView image_view;
vk::Sampler sampler;
vk::Extent2D extent;
uint32_t mip_levels;
void destroy(vk::Device device)
{
device.destroyImageView(image_view);
device.destroyImage(image);
device.destroySampler(sampler);
device.freeMemory(device_memory);
}
};
// Vertex layout for this example
struct Vertex
{
float pos[3];
float uv[2];
float normal[3];
};
struct VertexShaderData
{
glm::mat4 projection;
glm::mat4 model;
glm::vec4 view_pos;
float lod_bias = 0.0f;
};
private:
// from vkb::Application
bool prepare(const vkb::ApplicationOptions &options) override;
// from vkb::VulkanSample
void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
void build_command_buffers() override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
void render(float delta_time) override;
void view_changed() override;
vk::DescriptorPool create_descriptor_pool();
vk::DescriptorSetLayout create_descriptor_set_layout();
vk::Pipeline create_pipeline();
void draw();
void generate_quad();
void load_texture();
void prepare_uniform_buffers();
void update_descriptor_set();
void update_uniform_buffers();
private:
vk::DescriptorSet descriptor_set;
vk::DescriptorSetLayout descriptor_set_layout;
std::unique_ptr<vkb::core::BufferCpp> index_buffer;
uint32_t index_count;
vk::Pipeline pipeline;
vk::PipelineLayout pipeline_layout;
Texture texture;
std::unique_ptr<vkb::core::BufferCpp> vertex_buffer;
VertexShaderData vertex_shader_data;
std::unique_ptr<vkb::core::BufferCpp> vertex_shader_data_buffer;
};
std::unique_ptr<vkb::Application> create_hpp_texture_loading();
@@ -1,33 +0,0 @@
# 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.
#
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 "Sascha Willems"
NAME "HPP Texture mipmap generation"
DESCRIPTION "Generate mipmaps for a texture"
SHADER_FILES_GLSL
"texture_mipmap_generation/glsl/texture.vert"
"texture_mipmap_generation/glsl/texture.frag"
SHADER_FILES_HLSL
"texture_mipmap_generation/hlsl/texture.vert.hlsl"
"texture_mipmap_generation/hlsl/texture.frag.hlsl")
@@ -1,233 +0,0 @@
////
- 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}
= Run-time mip-map generation 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/api/hpp_texture_mipmap_generation[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/texture_mipmap_generation[Texture mipmap generation] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp.
== Overview
Generates a complete texture mip-chain at runtime from a base image using image blits and proper image barriers.
This examples demonstrates how to generate a complete texture mip-chain at runtime instead of loading offline generated mip-maps from a texture file.
While usually not applied for textures stored on the disk (that usually have the mips generated offline and stored in the file) this technique is often used for dynamic textures like cubemaps for reflections or other render-to-texture effects.
Having mip-maps for runtime generated textures offers lots of benefits, both in terms of image stability and performance.
Without mip mapping the image will become noisy, especially with high frequency textures (and texture components like specular) and using mip mapping will result in higher performance due to caching.
Though this example only generates one mip-chain for a single texture at the beginning this technique can also be used during normal frame rendering to generate mip-chains for dynamic textures.
Some GPUs also offer `asynchronous transfer queues` that may be used for doing such operations in the background.
To detect this, check for queue families with only the `vk::QueueFlagBits::eTransfer` set.
== Comparison
Without mip mapping:
image::samples/api/texture_mipmap_generation/images/mip_mapping_off.jpg[Off,512px]
Using mip mapping with a bilinear filter:
image::samples/api/texture_mipmap_generation/images/mip_mapping_bilinear.jpg[Bilinear,512px]
Using mip mapping with an anisotropic filter:
image::samples/api/texture_mipmap_generation/images/mip_mapping_anisotropic.jpg[Anisotropic,512px]
== Requirements
To downsample from one mip level to the next, we will be using https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkCmdBlitImage.html[`vk::CommandBuffer::blitImage`].
This requires the format used to support the `vk::FormatFeatureFlagBits::eBlitSrc` and the `vk::FormatFeatureFlagBits::eBlitDst` flags.
If these are not supported, the image format can't be used to blit and you'd either have to choose a different format or use a custom shader to generate mip levels.
The example uses the `vk::Format::eR8G8B8A8Srgb` that should support these flags on most implementations.
*_Note:_* Use https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkGetPhysicalDeviceFormatProperties.html[`vk::PhysicalDevice::getFormatProperties`] to check if the format supports the blit flags first.
== Points of interest
=== Image setup
Even though we'll only upload the first mip level initially, we create the image with number of desired mip levels.
The following formula is used to calculate the number of mip levels based on the max.
image extent:
[,cpp]
----
texture.mip_levels = static_cast<uint32_t>(floor(log2(std::max(texture.width, texture.height))) + 1);
----
This is then passed to the image create info:
[,cpp]
----
vk::ImageCreateInfo image_create_info({},
vk::ImageType::e2D,
format,
vk::Extent3D(texture.extent, 1),
texture.mip_levels,
...
----
Setting the number of desired mip levels is necessary as this is used for allocating the correct amount of memory required by the image (`vk::Device::allocateMemory`).
=== Upload base mip level
Before generating the mip-chain we need to copy the image data loaded from disk into the newly generated image.
This image will be the base for our mip-chain:
[,cpp]
----
vk::BufferImageCopy buffer_copy_region({}, {}, {}, {vk::ImageAspectFlagBits::eColor, 0, 0, 1}, {}, vk::Extent3D(texture.extent, 1));
copy_command.copyBufferToImage(staging_buffer, texture.image, vk::ImageLayout::eTransferDstOptimal, buffer_copy_region);
----
=== Prepare base mip level
As we are going to blit *_from_* the base mip-level just uploaded we also need to insert an image memory barrier that transitions the image layout to `vk::ImageLayout::eTransferSrcOptimal` for the base mip level:
[,cpp]
----
image_memory_barrier = vk::ImageMemoryBarrier(vk::AccessFlagBits::eTransferWrite,
vk::AccessFlagBits::eTransferRead,
vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::eTransferSrcOptimal,
VK_QUEUE_FAMILY_IGNORED,
VK_QUEUE_FAMILY_IGNORED,
texture.image,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1});
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, image_memory_barrier);
----
=== Generating the mip-chain
There are two different ways of generating the mip-chain.
The first one is to blit down the whole mip-chain from level n-1 to n, the other way would be to always use the base image and blit down from that to all levels.
This example uses the first one.
*_Note:_* Blitting (same for copying) images is done inside of a command buffer that has to be submitted and as such has to be synchronized before using the new image with e.g.
a `vk::Fence`.
We simply loop over all remaining mip levels (level 0 was loaded from disk) and prepare a `vk::ImageBlit` structure for each blit from mip level i-1 to level i.
First the source for our blit.
This is the previous mip level:
// {% raw %}
[,cpp]
----
for (int32_t i = 1; i < texture.mipLevels; i++)
{
vk::ImageBlit image_blit(// Source
{vk::ImageAspectFlagBits::eColor, i - 1, 0, 1},
{{{}, {int32_t(texture.extent.width >> (i - 1)), int32_t(texture.extent.height >> (i - 1)), int32_t(1)}}},
// Destination
{vk::ImageAspectFlagBits::eColor, i, 0, 1},
{{{}, {int32_t(texture.extent.width >> i), int32_t(texture.extent.height >> i), int32_t(1)}}});
}
----
// {% endraw %}
Before we can blit to this mip level, we need to transition it's image layout to `vk::ImageLayout::eTransferDstOptimal`:
[,cpp]
----
// Prepare current mip level as image blit destination
image_memory_barrier = vk::ImageMemoryBarrier({},
vk::AccessFlagBits::eTransferWrite,
vk::ImageLayout::eUndefined,
vk::ImageLayout::eTransferDstOptimal,
VK_QUEUE_FAMILY_IGNORED,
VK_QUEUE_FAMILY_IGNORED,
texture.image,
{vk::ImageAspectFlagBits::eColor, i, 1, 0, 1});
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, image_memory_barrier);
----
Note that we set the `baseMipLevel` of the subresource range to `i`, so the image memory barrier will only affect the one mip level we want to copy to.
Now that the mip level we want to copy from and the one we'll copy to are in the proper layout (transfer source and destination) we can issue the https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkCmdBlitImage.html[`vk::CommandBuffer::blitImage`] to copy from mip level (i-1) to mip level (i):
[,cpp]
----
blit_command.blitImage(texture.image, vk::ImageLayout::eTransferSrcOptimal, texture.image, vk::ImageLayout::eTransferDstOptimal, image_blit, vk::Filter::eLinear);
----
`vk::CommandBuffer::blitImage` does the down sampling from mip level (i-1) to mip level (i) using a linear filter, if you need better or more advanced filtering for this you need to resort to using custom shaders for generating the mip chain instead of blitting.
After the blit is done we can use this mip level as a base for the next level, so we transition the layout from `vk::ImageLayout::eTransferDstOptimal` to `vk::ImageLayout::eTransferSrcOptimal` so we can use this level as transfer source for the next level:
[,cpp]
----
image_memory_barrier = vk::ImageMemoryBarrier(vk::AccessFlagBits::eTransferWrite,
vk::AccessFlagBits::eTransferRead,
vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::eTransferSrcOptimal,
VK_QUEUE_FAMILY_IGNORED,
VK_QUEUE_FAMILY_IGNORED,
texture.image,
{vk::ImageAspectFlagBits::eColor, i, 1, 0, 1});
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, image_memory_barrier);
}
----
=== Final image layout transitions
Once the loop is done we need to transition all mip levels of the image to their actual usage layout, which is `vk::ImageLayout::eShaderReadOnlyOptimal` for this example.
Note that after the loop above all levels will be in the `vk::ImageLayout::eTransferSrcOptimal` layout allowing us to transfer the whole image with a single barrier:
[,cpp]
----
image_memory_barrier = vk::ImageMemoryBarrier(vk::AccessFlagBits::eTransferRead,
vk::AccessFlagBits::eShaderRead,
vk::ImageLayout::eTransferSrcOptimal,
vk::ImageLayout::eShaderReadOnlyOptimal,
VK_QUEUE_FAMILY_IGNORED,
VK_QUEUE_FAMILY_IGNORED,
texture.image,
{vk::ImageAspectFlagBits::eColor, 0, texture.mip_levels, 0, 1});
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, {}, {}, image_memory_barrier);
----
Submitting that command buffer will result in an image with a complete mip-chain and all mip levels being transitioned to the proper image layout for shader reads.
=== Image View creation
The Image View also requires information about how many Mip Levels are used.
This is specified in the `vk::ImageViewCreateInfo.subresourceRange.levelCount` field.
[,cpp]
----
vk::ImageViewCreateInfo image_view_create_info({},
texture.image,
vk::ImageViewType::e2D,
format,
{vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eG, vk::ComponentSwizzle::eB, vk::ComponentSwizzle::eA},
{vk::ImageAspectFlagBits::eColor, 0, texture.mip_levels, 0, 1});
texture.view = get_device()->get_handle().createImageView(image_view_create_info);
----
@@ -1,455 +0,0 @@
/* 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.
*/
/*
* Runtime mip map generation, using vulkan.hpp
*/
#include "hpp_texture_mipmap_generation.h"
#include "common/hpp_vk_common.h"
#include "common/ktx_common.h"
#include "common/vk_initializers.h"
#include "core/command_pool.h"
HPPTextureMipMapGeneration::HPPTextureMipMapGeneration()
{
title = "Texture MipMap generation";
zoom = -2.5f;
rotation = {0.0f, 15.0f, 0.0f};
}
HPPTextureMipMapGeneration::~HPPTextureMipMapGeneration()
{
if (has_device() && get_device().get_handle())
{
vk::Device device = get_device().get_handle();
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
for (auto sampler : samplers)
{
device.destroySampler(sampler);
}
device.destroyImageView(texture.view);
device.destroyImage(texture.image);
device.freeMemory(texture.device_memory);
uniform_buffer.reset();
}
}
bool HPPTextureMipMapGeneration::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
prepare_camera();
load_assets();
prepare_uniform_buffers();
descriptor_set_layout = create_descriptor_set_layout();
pipeline_layout = get_device().get_handle().createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &descriptor_set_layout});
pipeline = create_pipeline();
descriptor_pool = create_descriptor_pool();
descriptor_set = vkb::common::allocate_descriptor_set(get_device().get_handle(), descriptor_pool, descriptor_set_layout);
update_descriptor_set();
build_command_buffers();
prepared = true;
}
return prepared;
}
// Enable physical device features required for this example
void HPPTextureMipMapGeneration::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = true;
}
}
void HPPTextureMipMapGeneration::build_command_buffers()
{
vk::CommandBufferBeginInfo command_buffer_begin_info;
std::array<vk::ClearValue, 2> clear_values = {{default_clear_color, vk::ClearDepthStencilValue{1.0f, 0}}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = render_pass,
.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
auto command_buffer = draw_cmd_buffers[i];
render_pass_begin_info.framebuffer = framebuffers[i];
command_buffer.begin(command_buffer_begin_info);
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, extent};
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
draw_model(scene, command_buffer);
draw_ui(command_buffer);
command_buffer.endRenderPass();
command_buffer.end();
}
}
void HPPTextureMipMapGeneration::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Settings"))
{
drawer.checkbox("Rotate", &rotate_scene);
if (drawer.slider_float("LOD bias", &ubo.lod_bias, 0.0f, static_cast<float>(texture.mip_levels)))
{
update_uniform_buffers();
}
if (drawer.combo_box("Sampler type", &ubo.sampler_index, sampler_names))
{
update_uniform_buffers();
}
}
}
void HPPTextureMipMapGeneration::render(float delta_time)
{
if (prepared)
{
draw();
if (rotate_scene)
{
update_uniform_buffers(delta_time);
}
}
}
void HPPTextureMipMapGeneration::view_changed()
{
update_uniform_buffers();
}
void HPPTextureMipMapGeneration::check_format_features(vk::Format format) const
{
// Get device properties for the requested texture format
vk::FormatProperties format_properties = get_device().get_gpu().get_handle().getFormatProperties(format);
// Check if the selected format supports blit source and destination, which is required for generating the mip levels
vk::FormatFeatureFlags format_feature_flags = vk::FormatFeatureFlagBits::eBlitSrc | vk::FormatFeatureFlagBits::eBlitDst;
// If this is not supported you could implement a fallback via compute shader image writes and stores
if ((format_properties.optimalTilingFeatures & format_feature_flags) != format_feature_flags)
{
throw std::runtime_error("Selected image format does not support blit source and destination");
}
}
vk::DescriptorPool HPPTextureMipMapGeneration::create_descriptor_pool()
{
// Example uses one ubo and one image sampler
std::array<vk::DescriptorPoolSize, 3> pool_sizes = {
{{vk::DescriptorType::eUniformBuffer, 1}, {vk::DescriptorType::eSampledImage, 1}, {vk::DescriptorType::eSampler, 3}}};
vk::DescriptorPoolCreateInfo descriptor_pool_create_info{.maxSets = 2,
.poolSizeCount = static_cast<uint32_t>(pool_sizes.size()),
.pPoolSizes = pool_sizes.data()};
return get_device().get_handle().createDescriptorPool(descriptor_pool_create_info);
}
vk::DescriptorSetLayout HPPTextureMipMapGeneration::create_descriptor_set_layout()
{
std::array<vk::DescriptorSetLayoutBinding, 3> set_layout_bindings = {
{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment}, // Binding 0 : Parameter uniform buffer
{1, vk::DescriptorType::eSampledImage, 1, vk::ShaderStageFlagBits::eFragment}, // Binding 1 : Fragment shader image sampler
{2, vk::DescriptorType::eSampler, 3, vk::ShaderStageFlagBits::eFragment}}}; // Binding 2 : Sampler array (3 descriptors)
vk::DescriptorSetLayoutCreateInfo descriptor_layout{.bindingCount = static_cast<uint32_t>(set_layout_bindings.size()),
.pBindings = set_layout_bindings.data()};
return get_device().get_handle().createDescriptorSetLayout(descriptor_layout);
}
vk::Pipeline HPPTextureMipMapGeneration::create_pipeline()
{
// Load shaders
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {
load_shader("texture_mipmap_generation", "texture.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("texture_mipmap_generation", "texture.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Vertex bindings and attributes
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
std::array<vk::VertexInputAttributeDescription, 2> vertex_input_attributes = {{
{0, 0, vk::Format::eR32G32B32Sfloat, 0}, // Position
{1, 0, vk::Format::eR32G32Sfloat, sizeof(float) * 6}, // UV
}};
vk::PipelineVertexInputStateCreateInfo vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &vertex_input_binding,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size()),
.pVertexAttributeDescriptions = vertex_input_attributes.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eLessOrEqual;
depth_stencil_state.depthTestEnable = true;
depth_stencil_state.depthWriteEnable = true;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
pipeline_layout,
render_pass);
}
void HPPTextureMipMapGeneration::draw()
{
HPPApiVulkanSample::prepare_frame();
// Command buffer to be submitted to the queue
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
// Submit to queue
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
}
void HPPTextureMipMapGeneration::load_assets()
{
scene = load_model("scenes/tunnel_cylinder.gltf");
// Load the base texture containing only the first mip level and generate the whole mip-chain at runtime
ktxTexture *ktx_texture = vkb::ktx::load_texture(vkb::fs::path::get(vkb::fs::path::Assets, "textures/checkerboard_rgba.ktx"));
texture.extent = vk::Extent2D{ktx_texture->baseWidth, ktx_texture->baseHeight};
// Calculate number of mip levels as per Vulkan specs:
// numLevels = 1 + floor(log2(max(w, h, d)))
texture.mip_levels = static_cast<uint32_t>(floor(log2(std::max(texture.extent.width, texture.extent.height))) + 1);
// ktx1 doesn't know whether the content is sRGB or linear, but most tools save in sRGB, so assume that.
constexpr vk::Format format = vk::Format::eR8G8B8A8Srgb;
check_format_features(format);
// Create a host-visible staging buffer that contains the raw image data
vkb::core::BufferCpp staging_buffer = vkb::core::BufferCpp::create_staging_buffer(get_device(), ktx_texture->dataSize, ktx_texture->pData);
// now, the ktx_texture can be destroyed
ktxTexture_Destroy(ktx_texture);
// Create optimal tiled target image on the device
auto device = get_device().get_handle();
vk::ImageCreateInfo image_create_info{.imageType = vk::ImageType::e2D,
.format = format,
.extent = {.width = texture.extent.width, .height = texture.extent.height, .depth = 1},
.mipLevels = texture.mip_levels,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.tiling = vk::ImageTiling::eOptimal,
.usage =
vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eSampled,
.sharingMode = vk::SharingMode::eExclusive};
texture.image = device.createImage(image_create_info);
vk::MemoryRequirements memory_requirements = device.getImageMemoryRequirements(texture.image);
vk::MemoryAllocateInfo memory_allocation{.allocationSize = memory_requirements.size,
.memoryTypeIndex = get_device().get_gpu().get_memory_type(memory_requirements.memoryTypeBits,
vk::MemoryPropertyFlagBits::eDeviceLocal)};
texture.device_memory = device.allocateMemory(memory_allocation);
device.bindImageMemory(texture.image, texture.device_memory, 0);
vk::CommandBuffer copy_command = vkb::common::allocate_command_buffer(get_device().get_handle(), get_device().get_command_pool().get_handle());
copy_command.begin(vk::CommandBufferBeginInfo());
// Optimal image will be used as destination for the copy, so we must transfer from our initial undefined image layout to the transfer destination layout
vkb::common::image_layout_transition(copy_command, texture.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal);
// Copy the first mip of the chain, remaining mips will be generated
vk::BufferImageCopy buffer_copy_region{.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1},
.imageExtent = {texture.extent.width, texture.extent.height, 1}};
copy_command.copyBufferToImage(staging_buffer.get_handle(), texture.image, vk::ImageLayout::eTransferDstOptimal, buffer_copy_region);
// Transition first mip level to transfer source so we can blit(read) from it
vkb::common::image_layout_transition(copy_command, texture.image, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eTransferSrcOptimal);
get_device().flush_command_buffer(copy_command, queue, true);
// Generate the mip chain
// ---------------------------------------------------------------
// We copy down the whole mip chain doing a blit from mip-1 to mip
// An alternative way would be to always blit from the first mip level and sample that one down
vk::CommandBuffer blit_command = vkb::common::allocate_command_buffer(get_device().get_handle(), get_device().get_command_pool().get_handle());
blit_command.begin(vk::CommandBufferBeginInfo());
// Copy down mips from n-1 to n
for (uint32_t i = 1; i < texture.mip_levels; i++)
{
vk::ImageBlit image_blit{
{vk::ImageAspectFlagBits::eColor, i - 1, 0, 1},
{{{{},
{static_cast<int32_t>(texture.extent.width >> (i - 1)),
static_cast<int32_t>(texture.extent.height >> (i - 1)),
static_cast<int32_t>(1)}}}},
{vk::ImageAspectFlagBits::eColor, i, 0, 1},
{{{{}, {static_cast<int32_t>(texture.extent.width >> i), static_cast<int32_t>(texture.extent.height >> i), static_cast<int32_t>(1)}}}}};
// Prepare current mip level as image blit destination
vk::ImageSubresourceRange image_subresource_range{vk::ImageAspectFlagBits::eColor, i, 1, 0, 1};
vkb::common::image_layout_transition(
blit_command, texture.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, image_subresource_range);
// Blit from previous level
blit_command.blitImage(texture.image, vk::ImageLayout::eTransferSrcOptimal, texture.image, vk::ImageLayout::eTransferDstOptimal, image_blit, vk::Filter::eLinear);
// Prepare current mip level as image blit source for next level
vkb::common::image_layout_transition(
blit_command, texture.image, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eTransferSrcOptimal, image_subresource_range);
}
// After the loop, all mip layers are in TRANSFER_SRC layout, so transition all to SHADER_READ
vkb::common::image_layout_transition(blit_command,
texture.image,
vk::ImageLayout::eTransferSrcOptimal,
vk::ImageLayout::eShaderReadOnlyOptimal,
{vk::ImageAspectFlagBits::eColor, 0, texture.mip_levels, 0, 1});
get_device().flush_command_buffer(blit_command, queue, true);
// ---------------------------------------------------------------
// Create samplers for different mip map demonstration cases
// Without mip mapping
samplers[0] = vkb::common::create_sampler(get_device().get_gpu().get_handle(), get_device().get_handle(), format,
vk::Filter::eLinear, vk::SamplerAddressMode::eRepeat, 1.0f, 0.0f);
// With mip mapping
samplers[1] =
vkb::common::create_sampler(get_device().get_gpu().get_handle(), get_device().get_handle(), format,
vk::Filter::eLinear, vk::SamplerAddressMode::eRepeat, 1.0f, static_cast<float>(texture.mip_levels));
// With mip mapping and anisotropic filtering (when supported)
samplers[2] = vkb::common::create_sampler(
get_device().get_gpu().get_handle(),
get_device().get_handle(),
format,
vk::Filter::eLinear,
vk::SamplerAddressMode::eRepeat,
get_device().get_gpu().get_features().samplerAnisotropy ? (get_device().get_gpu().get_properties().limits.maxSamplerAnisotropy) : 1.0f,
static_cast<float>(texture.mip_levels));
// Create image view
texture.view = vkb::common::create_image_view(
get_device().get_handle(), texture.image, vk::ImageViewType::e2D, format, vk::ImageAspectFlagBits::eColor, 0, texture.mip_levels);
}
void HPPTextureMipMapGeneration::prepare_camera()
{
camera.type = vkb::CameraType::FirstPerson;
camera.set_perspective(60.0f, static_cast<float>(extent.width) / static_cast<float>(extent.height), 0.1f, 1024.0f);
camera.set_translation(glm::vec3(0.0f, 0.0f, -12.5f));
}
void HPPTextureMipMapGeneration::prepare_uniform_buffers()
{
// Shared parameter uniform buffer block
uniform_buffer = std::make_unique<vkb::core::BufferCpp>(get_device(),
sizeof(ubo),
vk::BufferUsageFlagBits::eUniformBuffer,
VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
}
void HPPTextureMipMapGeneration::update_descriptor_set()
{
vk::DescriptorBufferInfo buffer_descriptor{uniform_buffer->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo image_descriptor{nullptr, texture.view, vk::ImageLayout::eShaderReadOnlyOptimal};
std::array<vk::DescriptorImageInfo, 3> sampler_descriptors = {{{samplers[0], nullptr, vk::ImageLayout::eShaderReadOnlyOptimal},
{samplers[1], nullptr, vk::ImageLayout::eShaderReadOnlyOptimal},
{samplers[2], nullptr, vk::ImageLayout::eShaderReadOnlyOptimal}}};
assert(samplers.size() == sampler_descriptors.size());
std::array<vk::WriteDescriptorSet, 3> write_descriptor_sets = {{{.dstSet = descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &buffer_descriptor}, // Binding 0 : Vertex shader uniform buffer
{.dstSet = descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eSampledImage,
.pImageInfo = &image_descriptor}, // Binding 1 : Fragment shader texture sampler
{.dstSet = descriptor_set,
.dstBinding = 2,
.descriptorCount = static_cast<uint32_t>(sampler_descriptors.size()),
.descriptorType = vk::DescriptorType::eSampler,
.pImageInfo = sampler_descriptors.data()}}}; // Binding 2: Sampler array
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, {});
}
void HPPTextureMipMapGeneration::update_uniform_buffers(float delta_time)
{
ubo.projection = camera.matrices.perspective;
ubo.model = camera.matrices.view;
ubo.model = glm::rotate(ubo.model, glm::radians(90.0f + timer * 360.0f), glm::vec3(0.0f, 0.0f, 1.0f));
ubo.model = glm::scale(ubo.model, glm::vec3(0.5f));
timer += delta_time * 0.005f;
if (timer > 1.0f)
{
timer -= 1.0f;
}
uniform_buffer->convert_and_update(ubo);
}
std::unique_ptr<vkb::Application> create_hpp_texture_mipmap_generation()
{
return std::make_unique<HPPTextureMipMapGeneration>();
}
@@ -1,90 +0,0 @@
/* 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.
*/
/*
* Runtime mip map generation, using vulkan.hpp
*/
#pragma once
#include <hpp_api_vulkan_sample.h>
#include <ktx.h>
class HPPTextureMipMapGeneration : public HPPApiVulkanSample
{
public:
HPPTextureMipMapGeneration();
~HPPTextureMipMapGeneration();
private:
struct Texture
{
vk::Image image;
vk::DeviceMemory device_memory;
vk::ImageView view;
vk::Extent2D extent;
uint32_t mip_levels;
};
struct UBO
{
glm::mat4 projection;
glm::mat4 model;
float lod_bias = 0.0f;
int32_t sampler_index = 2;
};
private:
// from vkb::Application
bool prepare(const vkb::ApplicationOptions &options) override;
// from vkb::VulkanSample
void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
void build_command_buffers() override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
void render(float delta_time) override;
void view_changed() override;
void check_format_features(vk::Format) const;
vk::DescriptorPool create_descriptor_pool();
vk::DescriptorSetLayout create_descriptor_set_layout();
vk::Pipeline create_pipeline();
void draw();
void load_assets();
void prepare_camera();
void prepare_uniform_buffers();
void update_descriptor_set();
void update_uniform_buffers(float delta_time = 0.0f);
private:
vk::DescriptorSet descriptor_set;
vk::DescriptorSetLayout descriptor_set_layout;
vk::Pipeline pipeline;
vk::PipelineLayout pipeline_layout;
bool rotate_scene = false;
// To demonstrate mip mapping and filtering this example uses separate samplers
std::vector<std::string> sampler_names{"No mip maps", "Mip maps (bilinear)", "Mip maps (anisotropic)"};
std::array<vk::Sampler, 3> samplers;
std::unique_ptr<vkb::scene_graph::components::HPPSubMesh> scene;
Texture texture;
UBO ubo;
std::unique_ptr<vkb::core::BufferCpp> uniform_buffer;
};
std::unique_ptr<vkb::Application> create_hpp_texture_mipmap_generation();
@@ -1,41 +0,0 @@
# Copyright (c) 2023-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 "Sascha Willems"
NAME "HPP Timestamp queries"
DESCRIPTION "Using timestamp queries to fetch timing information from the GPU, using vulkan.hpp"
SHADER_FILES_GLSL
"hdr/glsl/composition.vert"
"hdr/glsl/composition.frag"
"hdr/glsl/bloom.vert"
"hdr/glsl/bloom.frag"
"hdr/glsl/gbuffer.vert"
"hdr/glsl/gbuffer.frag"
SHADER_FILES_HLSL
"hdr/hlsl/composition.vert.hlsl"
"hdr/hlsl/composition.frag.hlsl"
"hdr/hlsl/bloom.vert.hlsl"
"hdr/hlsl/bloom.frag.hlsl"
"hdr/hlsl/gbuffer.vert.hlsl"
"hdr/hlsl/gbuffer.frag.hlsl")
@@ -1,282 +0,0 @@
////
- Copyright (c) 2023-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}
= Timestamp queries 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/api/hpp_timestamp_queries[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/timestamp_queries[Timestamp queries] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp.
This tutorial, along with the accompanying example code, shows how to use timestamp queries to measure timings on the GPU.
The sample, based on the HDR one, does multiple render passes and will use timestamp queries to get GPU timings for the different render passes.
This is done by writing GPU timestamps at certain points within a command buffer.
These can then be read on the host and used for approximate profiling and to e.g.
improve performance where needed.
== Introduction
Vulkan offers several https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#queries[query types] that allow you to query different types of information from the GPU.
One such query type is the https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#queries-timestamps[timestamp query].
This provides your application with a mechanism to time the execution of commands on the GPU.
As with the other query types, a query pool is then used to either directly fetch or copy over the results to the host.
== A few important notes on timestamp queries
It's important to know that timestamp queries differ greatly from how timing can be done on the CPU with e.g.
the high performance counter.
This is mostly due to how a GPU's dispatches, overlaps and finishes work across different stages of the pipeline.
So while technically you can specify any pipeline stage at which the timestamp should be written, a lot of stage combinations and orderings won't give meaningful result.
This also means that you can't compare timestamps taken on different queues.
So while it may may sound reasonable to write timestamps for the vertex and fragment shader stage directly one after another, that will usually not return meaningful results due to how the GPU works.
And so for this example, we take the same approach as some popular CPU/GPU profilers by only using the top and bottom stages of the pipeline.
This combination is known to give proper approximate timing results on most GPUs.
== Checking for support
Not all GPUs support timestamp queries, so before using them we need to make sure that they can be used.
This differs slightly from checking other features with a simple `vk::Bool`.
Here we need to check if the `timestampPeriod` limit of the physical device is greater than zero.
If that's the case, timestamp queries are supported:
[,cpp]
----
vk::PhysicalDeviceLimits const &device_limits = device->get_gpu().get_properties().limits;
if (device_limits.timestampPeriod == 0)
{
throw std::runtime_error{"The selected device does not support timestamp queries!"};
}
----
Another limit we need to check is `timestampComputeAndGraphics`.
If this is `true`, all graphics and compute pipelines support timestamp queries and the above check is sufficient.
If not, we need to check if the queue we want to use supports timestamps:
[,cpp]
----
if (!device_limits.timestampComputeAndGraphics)
{
// Check if the graphics queue used in this sample supports time stamps
vk::QueueFamilyProperties const &graphics_queue_family_properties = device->get_suitable_graphics_queue().get_properties();
if (graphics_queue_family_properties.timestampValidBits == 0)
{
throw std::runtime_error{"The selected graphics queue family does not support timestamp queries!"};
}
}
----
== Creating the query pool
As with all query types, we first need to create a pool for the timestamp queries.
This is used to store and read back the results (see `prepare_time_stamp_queries`):
[,cpp]
----
vk::QueryPoolCreateInfo query_pool_create_info({}, vk::QueryType::eTimestamp, static_cast<uint32_t>(time_stamps.size()));
time_stamps_query_pool = get_device()->get_handle().createQueryPool(query_pool_create_info);
----
The interesting parts are the `queryType`, which we set to `vk::QueryType::eTimestamp` for using timestamp queries and the `queryCount`, which is the maximum number of the the timestamp query result this pool can store.
For this sample we'll be using 6 time points, one for the start and one for the end of three render passes.
== Resetting the query pool
Before we can start writing data to the query pool, we need to reset it.
When using Vulkan 1.0 or 1.1, this requires us to enable the `VK_EXT_host_query_reset` extension:
[,cpp]
----
add_device_extension(VK_EXT_HOST_QUERY_RESET_EXTENSION_NAME);
----
With using Vulkan 1.2 this extension has become part of the core and we won't have to manually enable it.
Independent of this, we also need to enable the `hostQueryReset` physical device feature:
[,cpp]
----
auto &requested_extension_features = gpu.request_extension_features<vk::PhysicalDeviceHostQueryResetFeaturesEXT>();
requested_extension_features.hostQueryReset = true;
----
With features and extensions properly enabled, we can now reset the pool at the start of the command buffer, before writing the first timestamp.
This is done using `vk::CommandBuffer::resetQueryPool`:
[,cpp]
----
...
command_buffer.begin(command_buffer_begin_info);
command_buffer.resetQueryPool(time_stamps_query_pool, 0, static_cast<uint32_t>(time_stamps.size()));
----
'''
== Writing time stamps
Unlike getting CPU side timing information that can be queried immediately, with GPU time stamps we need to tell the implementation inside a command buffer when/where to write timestamps instead.
The results are then fetched afterwards (see below).
This is done inside the command buffer with `vk::CommandBuffer::writeTimestamp`.
This function will request a timestamp to be written from the GPU for a certain pipeline stage and write that value to memory.
The most interesting part of calling this function is the `pipelineStage` argument.
As noted earlier, it's technically possible to use any pipeline stage in here, not all pipeline stages will yield proper results due to how GPUs overlap work.
It's also important to note that not all implementations are able to latch timers at all pipeline stages (e.g.
if they don't have hardware that maps to a given stage) and may return timers at a later pipeline stage instead.
Calling this function also defines an execution dependency similar to a barrier on all commands that were submitted before it.
[,cpp]
----
command_buffer.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe, time_stamps_query_pool, 0);
// Do some work
for (int i = 0; i < draw_call_count; i++) {
command_buffer.draw(...);
}
command_buffer.writeTimestamp(vk::PipelineStageFlagBits::eBottomOfPipe, time_stamps_query_pool, 1);
----
To measure GPU times for the draw calls(s) we first tell the GPU to write a timestamp at the `vk::PipelineStageFlagBits::eTopOfPipe` pipeline stage.
This is not a real pipeline stage (as in e.g.
the vertex or fragment stages) but a special constant that tells the GPU to write the timestamp when all previous commands have been processed by the GPU's command processor.
This ensures that we get a timestamp right before starting on the draw calls we want to measure, which will be the base for calculating our delta time.
The second timestamp is written at the `vk::PipelineStageFlagBits::eBottomOfPipe` pipeline stage.
Once again this is not a real pipeline stage, but it again tells the GPU to write the timestamp after all work has been finished.
== Getting the results
Reading back the results can be done in two ways:
* Copy the results into a `vk::Buffer` inside the command buffer using `vk::CommandBuffer::copyQueryPoolResults`
* Get the results after the command buffer has finished executing using `vk::Device::getQueryPoolResults`
For our sample we'll use option two (see `get_time_stamp_results`):
[,cpp]
----
queue.submit(submit_info);
...
// The number of timestamps changes if the bloom pass is disabled
uint32_t count = bloom ? time_stamps.size() : time_stamps.size() - 2;
vk::Result result = device->get_handle().getQueryPoolResults(time_stamps_query_pool,
0,
count,
time_stamps.size() * sizeof(uint64_t),
time_stamps.data(),
sizeof(uint64_t),
vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait);
----
Most arguments are straightforward, e.g.
where the data will be copied to (the `time_stamps` vector).
The important part here are the `vk::QueryResultFlags ` flags used here.
`vk::QueryResultFlagBits::e64` will tell the api that we want to get the results as 64 bit values.
Without this flag, we would only get 32 bit values.
And since timestamp queries can operate in nanoseconds, only using 32 bits could result into an overflow.
E.g.
if your device has a `timestampPeriod` of 1, so that one increment in the result maps to exactly one nanosecond, with 32 bit precision you'd run into such an overflow after only about 0.43 seconds.
The `vk::QueryResultFlagBits::eWait` bit then tells the api to wait for all results to be available.
So when using this flag the values written to our `time_stamps` vector is guaranteed to be available after calling `vk::Device::getQueryPoolResults`.
This is fine for our use-case where we want to immediately access the results, but may introduce unnecessary stalls in other scenarios.
Alternatively you can use the `vk::QueryResultFlagBits::eWithAvailability` flag, which will let you poll the availability of the results and defer writing new timestamps until the results are available.
This should be the preferred way of fetching the results in a real-world application.
Using this flag an additional availability value is inserted after each query value.
If that value becomes non-zero, the result is available.
You then check availability before writing the timestamp again.
Here is a basic example of how this could look like for a single timestamp value:
[,cpp]
----
// time_stamp_with_availibility[current_frame * 2] contains the queried timestamp
// time_stamp_with_availibility[current_frame * 2 + 1] contains availability of the timestamp
std::array<uint64_t, max_frames_in_flight * 2> time_stamp_with_availibility{};
void drawFrame()
{
command_buffer.begin(command_buffer_begin_info);
// Only write new timestamp if previous result is available
if (time_stamp_with_availibility[current_frame * 2 + 1] != 0)
{
command_buffer.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe, time_stamps_query_pool, 0);
}
// Issue draw commands
command_buffer.end();
// Get deferred time stamp query for the current frame
vk::Result result = device.getQueryPoolResults(time_stamps_query_pool,
0,
1,
2 * sizeof(uint64_t),
&time_stamp_with_availibility[current_frame * max_frames_in_flight],
2 * sizeof(uint64_t),
vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWithAvailability);
assert(result == vk::Result::eSuccess);
// Display time stamp for the current frame if available
if (time_stamp_with_availibility[current_frame * 2 + 1] != 0) {
std::cout << "Timestamp = " << time_stamp_with_availibility[current_frame * 2] << "\n";
}
}
----
== Interpreting the results
After we have read back the results to the host, we are ready to interpret them.
E.g.
for displaying them in a user interface.
The results we got back do not actually contain a time value, but rather a number of "ticks".
So to get the actual time value we need to translate these values first.
This is done using `timestampPeriod` limit of the physical device.
It contains the number of nanoseconds it takes for a timestamp query value to be increased by 1 ("tick").
In our sample, we want to display the delta between two timestamps in milliseconds, so in addition to the above rule we also multiply the value accordingly.
[,cpp]
----
vk::PhysicalDeviceLimits const &device_limits = device->get_gpu().get_properties().limits;
float delta_in_ms = float(time_stamps[1] - time_stamps[0]) * device_limits.timestampPeriod / 1000000.0f;
----
== vk::CommandBuffer::writeTimestamp2
The https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_synchronization2.html[VK_KHR_synchronization2] extension introduced `vk::CommandBuffer::writeTimestamp2`.
This is pretty much the same as the `vk::CommandBuffer::writeTimestamp` function used in this sample, but adds support for some additional pipeline stages using `vk::PipelineStageFlags2`.
== Verdict
Even though timestamp queries are limited due to how a GPU works, they can still be useful for profiling and finding performance GPU bottlenecks.
@@ -1,821 +0,0 @@
/* Copyright (c) 2023-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.
*/
/*
* Timestamp queries (based on the HDR sample), using vulkan.hpp
*/
#include "hpp_timestamp_queries.h"
#include "core/hpp_queue.h"
HPPTimestampQueries::HPPTimestampQueries()
{
title = "Timestamp queries";
}
HPPTimestampQueries::~HPPTimestampQueries()
{
if (has_device() && get_device().get_handle())
{
vk::Device device = get_device().get_handle();
time_stamps.destroy(device);
bloom.destroy(device);
composition.destroy(device);
filter_pass.destroy(device);
models.destroy(device, descriptor_pool);
offscreen.destroy(device);
textures.destroy(device);
}
}
bool HPPTimestampQueries::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
// Check if the selected device supports timestamps. A value of zero means no support.
vk::PhysicalDeviceLimits const &device_limits = get_device().get_gpu().get_properties().limits;
if (device_limits.timestampPeriod == 0)
{
throw std::runtime_error{"The selected device does not support timestamp queries!"};
}
// Check if all queues support timestamp queries, if not we need to check on a per-queue basis
if (!device_limits.timestampComputeAndGraphics)
{
// Check if the graphics queue used in this sample supports time stamps
vk::QueueFamilyProperties const &graphics_queue_family_properties = get_device().get_queue_by_flags(vk::QueueFlagBits::eGraphics, 0).get_properties();
if (graphics_queue_family_properties.timestampValidBits == 0)
{
throw std::runtime_error{"The selected graphics queue family does not support timestamp queries!"};
}
}
prepare_camera();
load_assets();
prepare_uniform_buffers();
prepare_offscreen_buffer();
descriptor_pool = create_descriptor_pool();
prepare_bloom();
prepare_composition();
prepare_models();
prepare_time_stamps();
build_command_buffers();
prepared = true;
}
return prepared;
}
bool HPPTimestampQueries::resize(const uint32_t width, const uint32_t height)
{
HPPApiVulkanSample::resize(width, height);
update_uniform_buffers();
return true;
}
void HPPTimestampQueries::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = true;
}
}
void HPPTimestampQueries::build_command_buffers()
{
vk::CommandBufferBeginInfo command_buffer_begin_info;
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
vk::CommandBuffer command_buffer = draw_cmd_buffers[i];
command_buffer.begin(command_buffer_begin_info);
// Reset the timestamp query pool, so we can start fetching new values into it
command_buffer.resetQueryPool(time_stamps.query_pool, 0, static_cast<uint32_t>(time_stamps.values.size()));
{
/*
First pass: Render scene to offscreen framebuffer
*/
command_buffer.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe, time_stamps.query_pool, 0);
std::array<vk::ClearValue, 3> clear_values = {{vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 0.0f}})),
vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 0.0f}})),
vk::ClearDepthStencilValue{0.0f, 0}}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = offscreen.render_pass,
.framebuffer = offscreen.framebuffer,
.renderArea = {{0, 0}, offscreen.extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(offscreen.extent.width), static_cast<float>(offscreen.extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, offscreen.extent};
command_buffer.setScissor(0, scissor);
// Skybox
if (display_skybox)
{
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, models.skybox.pipeline);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, models.pipeline_layout, 0, models.skybox.descriptor_set, {});
draw_model(models.skybox.meshes[0], command_buffer);
}
// 3D object
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, models.objects.pipeline);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, models.pipeline_layout, 0, models.objects.descriptor_set, {});
draw_model(models.objects.meshes[models.object_index], command_buffer);
command_buffer.endRenderPass();
command_buffer.writeTimestamp(vk::PipelineStageFlagBits::eBottomOfPipe, time_stamps.query_pool, 1);
}
/*
Second render pass: First bloom pass
*/
if (bloom.enabled)
{
vk::ClearValue clear_value(vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 0.0f}})));
// Bloom filter
command_buffer.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe, time_stamps.query_pool, 2);
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = filter_pass.render_pass,
.framebuffer = filter_pass.framebuffer,
.renderArea = {{0, 0}, filter_pass.extent},
.clearValueCount = 1,
.pClearValues = &clear_value};
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(filter_pass.extent.width), static_cast<float>(filter_pass.extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, filter_pass.extent};
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, bloom.pipeline_layout, 0, bloom.descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, bloom.pipelines[1]);
command_buffer.draw(3, 1, 0, 0);
command_buffer.endRenderPass();
command_buffer.writeTimestamp(vk::PipelineStageFlagBits::eBottomOfPipe, time_stamps.query_pool, 3);
}
/*
Note: Explicit synchronization is not required between the render pass, as this is done implicitly via sub pass dependencies
*/
/*
Third render pass: Scene rendering with applied second bloom pass (when enabled)
*/
{
std::array<vk::ClearValue, 2> clear_values = {{vk::ClearColorValue(std::array<float, 4>({{0.0f, 0.0f, 0.0f, 0.0f}})),
vk::ClearDepthStencilValue{0.0f, 0}}};
// Final composition
command_buffer.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe, time_stamps.query_pool, bloom.enabled ? 4 : 2);
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = render_pass,
.framebuffer = framebuffers[i],
.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, extent};
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, composition.pipeline_layout, 0, composition.descriptor_set, {});
// Scene
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, composition.pipeline);
command_buffer.draw(3, 1, 0, 0);
// Bloom
if (bloom.enabled)
{
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, bloom.pipelines[0]);
command_buffer.draw(3, 1, 0, 0);
}
draw_ui(command_buffer);
command_buffer.endRenderPass();
command_buffer.writeTimestamp(vk::PipelineStageFlagBits::eBottomOfPipe, time_stamps.query_pool, bloom.enabled ? 5 : 3);
}
command_buffer.end();
}
}
void HPPTimestampQueries::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Settings"))
{
if (drawer.combo_box("Object type", &models.object_index, object_names))
{
update_uniform_buffers();
rebuild_command_buffers();
}
if (drawer.input_float("Exposure", &ubo_params.exposure, 0.025f, "%.3f"))
{
update_params();
}
if (drawer.checkbox("Bloom", &bloom.enabled))
{
rebuild_command_buffers();
}
if (drawer.checkbox("Skybox", &display_skybox))
{
rebuild_command_buffers();
}
}
if (drawer.header("timing"))
{
// Timestamps don't have a time unit themselves, but are read as timesteps
// The timestampPeriod property of the device tells how many nanoseconds such a timestep translates to on the selected device
float timestampFrequency = get_device().get_gpu().get_properties().limits.timestampPeriod;
drawer.text("Pass 1: Offscreen scene rendering: %.3f ms", static_cast<float>(time_stamps.values[1] - time_stamps.values[0]) * timestampFrequency / 1000000.0f);
drawer.text("Pass 2: %s %.3f ms", (bloom.enabled ? "First bloom pass" : "Scene display"), static_cast<float>(time_stamps.values[3] - time_stamps.values[2]) * timestampFrequency / 1000000.0f);
if (bloom.enabled)
{
drawer.text("Pass 3: Second bloom pass %.3f ms", static_cast<float>(time_stamps.values[5] - time_stamps.values[4]) * timestampFrequency / 1000000.0f);
drawer.set_dirty(true);
}
}
}
void HPPTimestampQueries::render(float delta_time)
{
if (prepared)
{
draw();
if (camera.updated)
{
update_uniform_buffers();
}
}
}
vk::DeviceMemory HPPTimestampQueries::allocate_memory(vk::Image image)
{
vk::MemoryRequirements memory_requirements = get_device().get_handle().getImageMemoryRequirements(image);
vk::MemoryAllocateInfo memory_allocate_info{.allocationSize = memory_requirements.size,
.memoryTypeIndex = get_device().get_gpu().get_memory_type(memory_requirements.memoryTypeBits,
vk::MemoryPropertyFlagBits::eDeviceLocal)};
return get_device().get_handle().allocateMemory(memory_allocate_info);
}
HPPTimestampQueries::FramebufferAttachment HPPTimestampQueries::create_attachment(vk::Format format, vk::ImageUsageFlagBits usage)
{
vk::Image image = create_image(format, usage);
vk::DeviceMemory memory = allocate_memory(image);
get_device().get_handle().bindImageMemory(image, memory, 0);
vk::ImageView view =
vkb::common::create_image_view(get_device().get_handle(), image, vk::ImageViewType::e2D, format, vkb::common::get_image_aspect_flags(usage, format));
return {format, image, memory, view};
}
vk::DescriptorPool HPPTimestampQueries::create_descriptor_pool()
{
std::array<vk::DescriptorPoolSize, 2> pool_sizes = {{{vk::DescriptorType::eUniformBuffer, 4}, {vk::DescriptorType::eCombinedImageSampler, 6}}};
return get_device().get_handle().createDescriptorPool(
{.maxSets = 4, .poolSizeCount = static_cast<uint32_t>(pool_sizes.size()), .pPoolSizes = pool_sizes.data()});
}
vk::Pipeline HPPTimestampQueries::create_bloom_pipeline(uint32_t direction)
{
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages{load_shader("hdr", "bloom.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("hdr", "bloom.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Set constant parameters via specialization constants
vk::SpecializationMapEntry specialization_map_entry{0, 0, sizeof(uint32_t)};
vk::SpecializationInfo specialization_info{1, &specialization_map_entry, sizeof(uint32_t), &direction};
shader_stages[1].pSpecializationInfo = &specialization_info;
vk::PipelineColorBlendAttachmentState blend_attachment_state{.blendEnable = true,
.srcColorBlendFactor = vk::BlendFactor::eOne,
.dstColorBlendFactor = vk::BlendFactor::eOne,
.colorBlendOp = vk::BlendOp::eAdd,
.srcAlphaBlendFactor = vk::BlendFactor::eSrcAlpha,
.dstAlphaBlendFactor = vk::BlendFactor::eDstAlpha,
.alphaBlendOp = vk::BlendOp::eAdd,
.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
// Empty vertex input state, full screen triangles are generated by the vertex shader
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
{},
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eFront,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
bloom.pipeline_layout,
direction == 1 ? render_pass : filter_pass.render_pass);
}
vk::Pipeline HPPTimestampQueries::create_composition_pipeline()
{
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages{load_shader("hdr", "composition.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("hdr", "composition.frag.spv", vk::ShaderStageFlagBits::eFragment)};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
// Empty vertex input state, full screen triangles are generated by the vertex shader
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
{},
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eFront,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
composition.pipeline_layout,
render_pass);
}
vk::RenderPass HPPTimestampQueries::create_filter_render_pass()
{
// Set up separate renderpass with references to the color and depth attachments
vk::AttachmentDescription attachment_description{.format = filter_pass.color.format,
.samples = vk::SampleCountFlagBits::e1,
.loadOp = vk::AttachmentLoadOp::eClear,
.storeOp = vk::AttachmentStoreOp::eStore,
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
.initialLayout = vk::ImageLayout::eUndefined,
.finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal};
vk::AttachmentReference color_reference{.attachment = 0, .layout = vk::ImageLayout::eColorAttachmentOptimal};
vk::SubpassDescription subpass{.pipelineBindPoint = vk::PipelineBindPoint::eGraphics, .colorAttachmentCount = 1, .pColorAttachments = &color_reference};
return create_render_pass({attachment_description}, subpass);
}
vk::Image HPPTimestampQueries::create_image(vk::Format format, vk::ImageUsageFlagBits usage)
{
vk::ImageCreateInfo image_create_info{.imageType = vk::ImageType::e2D,
.format = format,
.extent = {offscreen.extent.width, offscreen.extent.height, 1},
.mipLevels = 1,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.tiling = vk::ImageTiling::eOptimal,
.usage = usage | vk::ImageUsageFlagBits::eSampled};
return get_device().get_handle().createImage(image_create_info);
}
vk::Pipeline HPPTimestampQueries::create_models_pipeline(uint32_t shaderType, vk::CullModeFlagBits cullMode, bool depthTestAndWrite)
{
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages{load_shader("hdr", "gbuffer.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("hdr", "gbuffer.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Set constant parameters via specialization constants
vk::SpecializationMapEntry specialization_map_entry{0, 0, sizeof(uint32_t)};
// Set constant parameters via specialization constants
vk::SpecializationInfo specialization_info{1, &specialization_map_entry, sizeof(uint32_t), &shaderType};
shader_stages[0].pSpecializationInfo = &specialization_info;
shader_stages[1].pSpecializationInfo = &specialization_info;
// Vertex bindings an attributes for model rendering
// Binding description
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
// Attribute descriptions
std::vector<vk::VertexInputAttributeDescription> vertex_input_attributes = {{0, 0, vk::Format::eR32G32B32Sfloat, 0},
{1, 0, vk::Format::eR32G32B32Sfloat, 3 * sizeof(float)}};
vk::PipelineVertexInputStateCreateInfo vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &vertex_input_binding,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size()),
.pVertexAttributeDescriptions = vertex_input_attributes.data()};
std::vector<vk::PipelineColorBlendAttachmentState> blend_attachment_states(2);
blend_attachment_states[0].colorWriteMask =
vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA;
blend_attachment_states[1].colorWriteMask = blend_attachment_states[0].colorWriteMask;
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eGreater;
depth_stencil_state.depthWriteEnable = depthTestAndWrite;
depth_stencil_state.depthTestEnable = depthTestAndWrite;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
cullMode,
vk::FrontFace::eCounterClockwise,
blend_attachment_states,
depth_stencil_state,
models.pipeline_layout,
offscreen.render_pass);
}
vk::RenderPass HPPTimestampQueries::create_offscreen_render_pass()
{
// Set up separate renderpass with references to the color and depth attachments
std::vector<vk::AttachmentDescription> attachment_descriptions(3);
// Init attachment properties
for (uint32_t i = 0; i < 3; ++i)
{
attachment_descriptions[i].samples = vk::SampleCountFlagBits::e1;
attachment_descriptions[i].loadOp = vk::AttachmentLoadOp::eClear;
attachment_descriptions[i].storeOp = vk::AttachmentStoreOp::eStore;
attachment_descriptions[i].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
attachment_descriptions[i].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
attachment_descriptions[i].initialLayout = vk::ImageLayout::eUndefined;
attachment_descriptions[i].finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
}
attachment_descriptions[2].finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal;
// Formats
attachment_descriptions[0].format = offscreen.color[0].format;
attachment_descriptions[1].format = offscreen.color[1].format;
attachment_descriptions[2].format = offscreen.depth.format;
std::array<vk::AttachmentReference, 2> color_references{{{0, vk::ImageLayout::eColorAttachmentOptimal},
{1, vk::ImageLayout::eColorAttachmentOptimal}}};
vk::AttachmentReference depth_reference{2, vk::ImageLayout::eDepthStencilAttachmentOptimal};
vk::SubpassDescription subpass{.pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
.colorAttachmentCount = static_cast<uint32_t>(color_references.size()),
.pColorAttachments = color_references.data(),
.pDepthStencilAttachment = &depth_reference};
return create_render_pass(attachment_descriptions, subpass);
}
vk::RenderPass HPPTimestampQueries::create_render_pass(std::vector<vk::AttachmentDescription> const &attachment_descriptions, vk::SubpassDescription const &subpass_description)
{
// Use subpass dependencies for attachment layout transitions
std::array<vk::SubpassDependency, 2> subpass_dependencies;
subpass_dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
subpass_dependencies[0].dstSubpass = 0;
// End of previous commands
subpass_dependencies[0].srcStageMask = vk::PipelineStageFlagBits::eBottomOfPipe;
subpass_dependencies[0].srcAccessMask = vk::AccessFlagBits::eNoneKHR;
// Read/write from/to depth
subpass_dependencies[0].dstStageMask = vk::PipelineStageFlagBits::eEarlyFragmentTests;
subpass_dependencies[0].dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite;
// Write to attachment
subpass_dependencies[0].dstStageMask |= vk::PipelineStageFlagBits::eColorAttachmentOutput;
subpass_dependencies[0].dstAccessMask |= vk::AccessFlagBits::eColorAttachmentWrite;
subpass_dependencies[1].srcSubpass = 0;
subpass_dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
// End of write to attachment
subpass_dependencies[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
subpass_dependencies[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
// Attachment later read using sampler in 'bloom[0]' pipeline
subpass_dependencies[1].dstStageMask = vk::PipelineStageFlagBits::eFragmentShader;
subpass_dependencies[1].dstAccessMask = vk::AccessFlagBits::eShaderRead;
vk::RenderPassCreateInfo render_pass_create_info{.attachmentCount = static_cast<uint32_t>(attachment_descriptions.size()),
.pAttachments = attachment_descriptions.data(),
.subpassCount = 1,
.pSubpasses = &subpass_description,
.dependencyCount = static_cast<uint32_t>(subpass_dependencies.size()),
.pDependencies = subpass_dependencies.data()};
return get_device().get_handle().createRenderPass(render_pass_create_info);
}
void HPPTimestampQueries::draw()
{
HPPApiVulkanSample::prepare_frame();
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
// Read back the time stamp query results after the frame is finished
get_time_stamp_results();
}
void HPPTimestampQueries::get_time_stamp_results()
{
// The number of timestamps changes if the bloom pass is disabled
uint32_t count = static_cast<uint32_t>(bloom.enabled ? time_stamps.values.size() : time_stamps.values.size() - 2);
// Fetch the time stamp results written in the command buffer submissions
// A note on the flags used:
// vk::QueryResultFlagBits::e64: Results will have 64 bits. As time stamp values are on nano-seconds, this flag should always be used to avoid 32 bit overflows
// vk::QueryResultFlagBits::eWait: Since we want to immediately display the results, we use this flag to have the CPU wait until the results are available
vk::Result result = get_device().get_handle().getQueryPoolResults(time_stamps.query_pool,
0,
count,
time_stamps.values.size() * sizeof(uint64_t),
time_stamps.values.data(),
sizeof(uint64_t),
vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait);
assert(result == vk::Result::eSuccess);
}
void HPPTimestampQueries::load_assets()
{
// Models
models.skybox.meshes.emplace_back(load_model("scenes/cube.gltf"));
std::vector<std::string> filenames = {"geosphere.gltf", "teapot.gltf", "torusknot.gltf"};
object_names = {"Sphere", "Teapot", "Torusknot"};
for (auto file : filenames)
{
models.objects.meshes.emplace_back(load_model("scenes/" + file));
}
// Transforms
auto geosphere_matrix = glm::mat4(1.0f);
models.transforms.push_back(geosphere_matrix);
auto teapot_matrix = glm::mat4(1.0f);
teapot_matrix = glm::scale(teapot_matrix, glm::vec3(10.0f, 10.0f, 10.0f));
teapot_matrix = glm::rotate(teapot_matrix, glm::radians(180.0f), glm::vec3(1.0f, 0.0f, 0.0f));
models.transforms.push_back(teapot_matrix);
auto torus_matrix = glm::mat4(1.0f);
models.transforms.push_back(torus_matrix);
// Load HDR cube map
textures.envmap = load_texture_cubemap("textures/uffizi_rgba16f_cube.ktx", vkb::scene_graph::components::HPPImage::Color);
}
void HPPTimestampQueries::prepare_bloom()
{
std::array<vk::DescriptorSetLayoutBinding, 2> bindings = {{{0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}};
vk::Device device = get_device().get_handle();
bloom.descriptor_set_layout = device.createDescriptorSetLayout({.bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data()});
bloom.pipeline_layout = device.createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &bloom.descriptor_set_layout});
bloom.pipelines[0] = create_bloom_pipeline(1);
bloom.pipelines[1] = create_bloom_pipeline(0);
bloom.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, bloom.descriptor_set_layout);
update_bloom_descriptor_set();
}
void HPPTimestampQueries::prepare_camera()
{
camera.type = vkb::CameraType::LookAt;
camera.set_position(glm::vec3(0.0f, 0.0f, -4.0f));
camera.set_rotation(glm::vec3(0.0f, 180.0f, 0.0f));
// Note: Using reversed depth-buffer for increased precision, so Znear and Zfar are flipped
camera.set_perspective(60.0f, static_cast<float>(extent.width) / static_cast<float>(extent.height), 256.0f, 0.1f);
}
void HPPTimestampQueries::prepare_composition()
{
std::array<vk::DescriptorSetLayoutBinding, 2> bindings = {{{0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}}};
vk::Device device = get_device().get_handle();
composition.descriptor_set_layout =
device.createDescriptorSetLayout({.bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data()});
composition.pipeline_layout = device.createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &composition.descriptor_set_layout});
composition.pipeline = create_composition_pipeline();
composition.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, composition.descriptor_set_layout);
update_composition_descriptor_set();
}
void HPPTimestampQueries::prepare_models()
{
std::array<vk::DescriptorSetLayoutBinding, 3> bindings = {{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{2, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eFragment}}};
vk::Device device = get_device().get_handle();
models.descriptor_set_layout = device.createDescriptorSetLayout({.bindingCount = static_cast<uint32_t>(bindings.size()), .pBindings = bindings.data()});
models.pipeline_layout = device.createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &models.descriptor_set_layout});
models.objects.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, models.descriptor_set_layout);
update_model_descriptor_set(models.objects.descriptor_set);
models.objects.pipeline = create_models_pipeline(1, vk::CullModeFlagBits::eFront, true);
models.skybox.descriptor_set = vkb::common::allocate_descriptor_set(device, descriptor_pool, models.descriptor_set_layout);
update_model_descriptor_set(models.skybox.descriptor_set);
models.skybox.pipeline = create_models_pipeline(0, vk::CullModeFlagBits::eBack, false);
}
// Prepare a new framebuffer and attachments for offscreen rendering (G-Buffer)
void HPPTimestampQueries::prepare_offscreen_buffer()
{
{
offscreen.extent = extent;
// Color attachments
// We are using two 128-Bit RGBA floating point color buffers for this sample
// In a performance or bandwidth-limited scenario you should consider using a format with lower precision
offscreen.color[0] = create_attachment(vk::Format::eR32G32B32A32Sfloat, vk::ImageUsageFlagBits::eColorAttachment);
offscreen.color[1] = create_attachment(vk::Format::eR32G32B32A32Sfloat, vk::ImageUsageFlagBits::eColorAttachment);
// Depth attachment
offscreen.depth = create_attachment(depth_format, vk::ImageUsageFlagBits::eDepthStencilAttachment);
offscreen.render_pass = create_offscreen_render_pass();
offscreen.framebuffer = vkb::common::create_framebuffer(
get_device().get_handle(), offscreen.render_pass, {offscreen.color[0].view, offscreen.color[1].view, offscreen.depth.view}, offscreen.extent);
// Create sampler to sample from the color attachments
offscreen.sampler = vkb::common::create_sampler(get_device().get_gpu().get_handle(), get_device().get_handle(),
offscreen.color[0].format, vk::Filter::eNearest, vk::SamplerAddressMode::eClampToEdge, 1.0f, 1.0f);
}
// Bloom separable filter pass
{
filter_pass.extent = extent;
// Color attachments - needs to be a blendable format, so choose from a priority ordered list
const std::vector<vk::Format> float_format_priority_list = {
vk::Format::eR32G32B32A32Sfloat,
vk::Format::eR16G16B16A16Sfloat // Guaranteed blend support for this
};
vk::Format color_format = vkb::common::choose_blendable_format(get_device().get_gpu().get_handle(), float_format_priority_list);
// One floating point color buffer
filter_pass.color = create_attachment(color_format, vk::ImageUsageFlagBits::eColorAttachment);
filter_pass.render_pass = create_filter_render_pass();
filter_pass.framebuffer = vkb::common::create_framebuffer(get_device().get_handle(), filter_pass.render_pass, {filter_pass.color.view}, filter_pass.extent);
filter_pass.sampler = vkb::common::create_sampler(get_device().get_gpu().get_handle(), get_device().get_handle(),
filter_pass.color.format, vk::Filter::eNearest, vk::SamplerAddressMode::eClampToEdge, 1.0f, 1.0f);
}
}
void HPPTimestampQueries::prepare_time_stamps()
{
// Create the query pool object used to get the GPU time tamps
time_stamps.query_pool =
vkb::common::create_query_pool(get_device().get_handle(), vk::QueryType::eTimestamp, static_cast<uint32_t>(time_stamps.values.size()));
}
// Prepare and initialize uniform buffer containing shader uniforms
void HPPTimestampQueries::prepare_uniform_buffers()
{
// Matrices vertex shader uniform buffer
uniform_buffers.matrices = std::make_unique<vkb::core::BufferCpp>(get_device(),
sizeof(ubo_matrices),
vk::BufferUsageFlagBits::eUniformBuffer,
VMA_MEMORY_USAGE_CPU_TO_GPU);
// Params
uniform_buffers.params = std::make_unique<vkb::core::BufferCpp>(get_device(),
sizeof(ubo_params),
vk::BufferUsageFlagBits::eUniformBuffer,
VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
update_params();
}
void HPPTimestampQueries::update_composition_descriptor_set()
{
std::array<vk::DescriptorImageInfo, 2> color_descriptors = {{{offscreen.sampler, offscreen.color[0].view, vk::ImageLayout::eShaderReadOnlyOptimal},
{offscreen.sampler, filter_pass.color.view, vk::ImageLayout::eShaderReadOnlyOptimal}}};
std::array<vk::WriteDescriptorSet, 2> sampler_write_descriptor_sets = {{{.dstSet = composition.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &color_descriptors[0]},
{.dstSet = composition.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &color_descriptors[1]}}};
get_device().get_handle().updateDescriptorSets(sampler_write_descriptor_sets, {});
}
void HPPTimestampQueries::update_bloom_descriptor_set()
{
std::array<vk::DescriptorImageInfo, 2> color_descriptors = {{{offscreen.sampler, offscreen.color[0].view, vk::ImageLayout::eShaderReadOnlyOptimal},
{offscreen.sampler, offscreen.color[1].view, vk::ImageLayout::eShaderReadOnlyOptimal}}};
std::array<vk::WriteDescriptorSet, 2> sampler_write_descriptor_sets = {{{.dstSet = bloom.descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &color_descriptors[0]},
{.dstSet = bloom.descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &color_descriptors[1]}}};
get_device().get_handle().updateDescriptorSets(sampler_write_descriptor_sets, {});
}
void HPPTimestampQueries::update_model_descriptor_set(vk::DescriptorSet descriptor_set)
{
vk::DescriptorBufferInfo matrix_buffer_descriptor{uniform_buffers.matrices->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo environment_image_descriptor{textures.envmap.sampler,
textures.envmap.image->get_vk_image_view().get_handle(),
descriptor_type_to_image_layout(vk::DescriptorType::eCombinedImageSampler,
textures.envmap.image->get_vk_image_view().get_format())};
vk::DescriptorBufferInfo params_buffer_descriptor{uniform_buffers.params->get_handle(), 0, vk::WholeSize};
std::array<vk::WriteDescriptorSet, 3> write_descriptor_sets = {{{.dstSet = descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &matrix_buffer_descriptor},
{.dstSet = descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &environment_image_descriptor},
{.dstSet = descriptor_set,
.dstBinding = 2,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &params_buffer_descriptor}}};
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, {});
}
void HPPTimestampQueries::update_params()
{
uniform_buffers.params->convert_and_update(ubo_params);
}
void HPPTimestampQueries::update_uniform_buffers()
{
ubo_matrices.projection = camera.matrices.perspective;
ubo_matrices.modelview = camera.matrices.view * models.transforms[models.object_index];
ubo_matrices.skybox_modelview = camera.matrices.view;
ubo_matrices.inverse_modelview = glm::inverse(camera.matrices.view);
uniform_buffers.matrices->convert_and_update(ubo_matrices);
}
std::unique_ptr<vkb::Application> create_hpp_timestamp_queries()
{
return std::make_unique<HPPTimestampQueries>();
}
@@ -1,246 +0,0 @@
/* Copyright (c) 2023-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.
*/
/*
* Timestamp queries (based on the HDR sample), using vulkan.hpp
*/
#pragma once
#include <hpp_api_vulkan_sample.h>
class HPPTimestampQueries : public HPPApiVulkanSample
{
public:
HPPTimestampQueries();
~HPPTimestampQueries();
private:
struct Bloom
{
bool enabled = true;
vk::DescriptorSetLayout descriptor_set_layout = {};
vk::DescriptorSet descriptor_set;
vk::PipelineLayout pipeline_layout;
vk::Pipeline pipelines[2];
void destroy(vk::Device device)
{
device.destroyPipeline(pipelines[0]);
device.destroyPipeline(pipelines[1]);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
// no need to free the descriptor_set, as it's implicitly free'd with the descriptor_pool
}
};
struct Composition
{
vk::DescriptorSetLayout descriptor_set_layout = {};
vk::DescriptorSet descriptor_set = {};
vk::PipelineLayout pipeline_layout = {};
vk::Pipeline pipeline = {};
void destroy(vk::Device device)
{
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
// no need to free the descriptor_set, as it's implicitly free'd with the descriptor_pool
}
};
// Framebuffer for offscreen rendering
struct FramebufferAttachment
{
vk::Format format = {};
vk::Image image = {};
vk::DeviceMemory mem = {};
vk::ImageView view = {};
void destroy(vk::Device device)
{
device.destroyImageView(view);
device.destroyImage(image);
device.freeMemory(mem);
}
};
struct FilterPass
{
vk::Extent2D extent = {};
vk::Framebuffer framebuffer = {};
FramebufferAttachment color = {};
vk::RenderPass render_pass = {};
vk::Sampler sampler = {};
void destroy(vk::Device device)
{
device.destroySampler(sampler);
device.destroyFramebuffer(framebuffer);
device.destroyRenderPass(render_pass);
color.destroy(device);
}
};
struct Geometry
{
vk::DescriptorSet descriptor_set = {};
vk::Pipeline pipeline = {};
std::vector<std::unique_ptr<vkb::scene_graph::components::HPPSubMesh>> meshes = {};
void destroy(vk::Device device, vk::DescriptorPool descriptor_pool)
{
// no need to free the descriptor_set, as it's implicitly free'd with the descriptor_pool
device.destroyPipeline(pipeline);
}
};
struct Models
{
vk::DescriptorSetLayout descriptor_set_layout = {};
vk::PipelineLayout pipeline_layout = {};
Geometry objects = {};
Geometry skybox = {};
std::vector<glm::mat4> transforms = {};
int32_t object_index = 0;
void destroy(vk::Device device, vk::DescriptorPool descriptor_pool)
{
objects.destroy(device, descriptor_pool);
skybox.destroy(device, descriptor_pool);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
}
};
struct Offscreen
{
vk::Extent2D extent = {};
vk::Framebuffer framebuffer = {};
FramebufferAttachment color[2] = {};
FramebufferAttachment depth = {};
vk::RenderPass render_pass = {};
vk::Sampler sampler = {};
void destroy(vk::Device device)
{
device.destroySampler(sampler);
device.destroyFramebuffer(framebuffer);
device.destroyRenderPass(render_pass);
color[0].destroy(device);
color[1].destroy(device);
depth.destroy(device);
}
};
struct Textures
{
HPPTexture envmap;
void destroy(vk::Device device)
{
device.destroySampler(envmap.sampler);
}
};
struct TimeStamps
{
std::array<uint64_t, 6> values; // GPU time stamps will be stored in an array
vk::QueryPool query_pool; // A query pool is required to use GPU time stamps
void destroy(vk::Device device)
{
device.destroyQueryPool(query_pool);
}
};
struct UBOMatrices
{
glm::mat4 projection;
glm::mat4 modelview;
glm::mat4 skybox_modelview;
glm::mat4 inverse_modelview;
float modelscale = 0.05f;
};
struct UBOParams
{
float exposure = 1.0f;
};
struct UniformBuffers
{
std::unique_ptr<vkb::core::BufferCpp> matrices;
std::unique_ptr<vkb::core::BufferCpp> params;
};
private:
// from vkb::Application
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual bool resize(const uint32_t width, const uint32_t height) override;
// from vkb::VulkanSample
virtual void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
virtual void build_command_buffers() override;
virtual void on_update_ui_overlay(vkb::Drawer &drawer) override;
virtual void render(float delta_time) override;
vk::DeviceMemory allocate_memory(vk::Image image);
FramebufferAttachment create_attachment(vk::Format format, vk::ImageUsageFlagBits usage);
vk::DescriptorPool create_descriptor_pool();
vk::Pipeline create_bloom_pipeline(uint32_t direction);
vk::Pipeline create_composition_pipeline();
vk::RenderPass create_filter_render_pass();
vk::Image create_image(vk::Format format, vk::ImageUsageFlagBits usage);
vk::Pipeline create_models_pipeline(uint32_t shaderType, vk::CullModeFlagBits cullMode, bool depthTestAndWrite);
vk::RenderPass create_offscreen_render_pass();
vk::RenderPass create_render_pass(std::vector<vk::AttachmentDescription> const &attachment_descriptions, vk::SubpassDescription const &subpass_description);
void draw();
void get_time_stamp_results();
void load_assets();
void prepare_bloom();
void prepare_camera();
void prepare_composition();
void prepare_models();
void prepare_offscreen_buffer();
void prepare_time_stamps();
void prepare_uniform_buffers();
void update_composition_descriptor_set();
void update_bloom_descriptor_set();
void update_model_descriptor_set(vk::DescriptorSet descriptor_set);
void update_params();
void update_uniform_buffers();
private:
Bloom bloom;
Composition composition;
bool display_skybox = true;
FilterPass filter_pass;
Models models;
std::vector<std::string> object_names;
Offscreen offscreen;
Textures textures;
TimeStamps time_stamps;
UBOMatrices ubo_matrices;
UBOParams ubo_params;
UniformBuffers uniform_buffers;
};
std::unique_ptr<vkb::Application> create_hpp_timestamp_queries();
-41
View File
@@ -1,41 +0,0 @@
# Copyright (c) 2019-2024, Sascha Willems
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 the "License";
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
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 "Sascha Willems"
NAME "Instancing"
DESCRIPTION "Instanced mesh rendering, uses a separate vertex buffer for instanced data"
SHADER_FILES_GLSL
"instancing/glsl/instancing.vert"
"instancing/glsl/instancing.frag"
"instancing/glsl/planet.vert"
"instancing/glsl/planet.frag"
"instancing/glsl/starfield.vert"
"instancing/glsl/starfield.frag"
SHADER_FILES_HLSL
"instancing/hlsl/instancing.vert.hlsl"
"instancing/hlsl/instancing.frag.hlsl"
"instancing/hlsl/planet.vert.hlsl"
"instancing/hlsl/planet.frag.hlsl"
"instancing/hlsl/starfield.vert.hlsl"
"instancing/hlsl/starfield.frag.hlsl")
-27
View File
@@ -1,27 +0,0 @@
////
- Copyright (c) 2019-2023, 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.
-
////
= Instancing
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/instancing[Khronos Vulkan samples github repository].
endif::[]
Uses the instancing feature for rendering many instances of the same mesh from a single vertex buffer with variable parameters and textures.
-522
View File
@@ -1,522 +0,0 @@
/* Copyright (c) 2019-2025, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Instanced mesh rendering, uses a separate vertex buffer for instanced data
*/
#include "instancing.h"
#include "benchmark_mode/benchmark_mode.h"
Instancing::Instancing()
{
title = "Instanced mesh rendering";
}
Instancing::~Instancing()
{
if (has_device())
{
vkDestroyPipeline(get_device().get_handle(), pipelines.instanced_rocks, nullptr);
vkDestroyPipeline(get_device().get_handle(), pipelines.planet, nullptr);
vkDestroyPipeline(get_device().get_handle(), pipelines.starfield, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layout, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), descriptor_set_layout, nullptr);
vkDestroySampler(get_device().get_handle(), textures.rocks.sampler, nullptr);
vkDestroySampler(get_device().get_handle(), textures.planet.sampler, nullptr);
}
}
void Instancing::request_gpu_features(vkb::PhysicalDevice &gpu)
{
auto &requested_features = gpu.get_mutable_requested_features();
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
requested_features.samplerAnisotropy = VK_TRUE;
}
// Enable texture compression
if (gpu.get_features().textureCompressionBC)
{
requested_features.textureCompressionBC = VK_TRUE;
}
else if (gpu.get_features().textureCompressionASTC_LDR)
{
requested_features.textureCompressionASTC_LDR = VK_TRUE;
}
else if (gpu.get_features().textureCompressionETC2)
{
requested_features.textureCompressionETC2 = VK_TRUE;
}
};
void Instancing::build_command_buffers()
{
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VkClearValue clear_values[2];
clear_values[0].color = {{0.0f, 0.0f, 0.033f, 0.0f}};
clear_values[1].depthStencil = {0.0f, 0};
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.renderPass = render_pass;
render_pass_begin_info.renderArea.extent.width = width;
render_pass_begin_info.renderArea.extent.height = height;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.pClearValues = clear_values;
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
// Set target frame buffer
render_pass_begin_info.framebuffer = framebuffers[i];
VK_CHECK(vkBeginCommandBuffer(draw_cmd_buffers[i], &command_buffer_begin_info));
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(width, height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
VkDeviceSize offsets[1] = {0};
// Star field
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_sets.planet, 0, NULL);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.starfield);
vkCmdDraw(draw_cmd_buffers[i], 4, 1, 0, 0);
// Planet
auto &planet_vertex_buffer = models.planet->vertex_buffers.at("vertex_buffer");
auto &planet_index_buffer = models.planet->index_buffer;
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_sets.planet, 0, NULL);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.planet);
vkCmdBindVertexBuffers(draw_cmd_buffers[i], 0, 1, planet_vertex_buffer.get(), offsets);
vkCmdBindIndexBuffer(draw_cmd_buffers[i], planet_index_buffer->get_handle(), 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(draw_cmd_buffers[i], models.planet->vertex_indices, 1, 0, 0, 0);
// Instanced rocks
auto &rock_vertex_buffer = models.rock->vertex_buffers.at("vertex_buffer");
auto &rock_index_buffer = models.rock->index_buffer;
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_sets.instanced_rocks, 0, NULL);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.instanced_rocks);
// Binding point 0 : Mesh vertex buffer
vkCmdBindVertexBuffers(draw_cmd_buffers[i], 0, 1, rock_vertex_buffer.get(), offsets);
// Binding point 1 : Instance data buffer
vkCmdBindVertexBuffers(draw_cmd_buffers[i], 1, 1, &instance_buffer.buffer->get_handle(), offsets);
vkCmdBindIndexBuffer(draw_cmd_buffers[i], rock_index_buffer->get_handle(), 0, VK_INDEX_TYPE_UINT32);
// Render instances
vkCmdDrawIndexed(draw_cmd_buffers[i], models.rock->vertex_indices, INSTANCE_COUNT, 0, 0, 0);
draw_ui(draw_cmd_buffers[i]);
vkCmdEndRenderPass(draw_cmd_buffers[i]);
VK_CHECK(vkEndCommandBuffer(draw_cmd_buffers[i]));
}
}
void Instancing::load_assets()
{
models.rock = load_model("scenes/rock.gltf");
models.planet = load_model("scenes/planet.gltf");
// models.rock.loadFromFile(getAssetPath() + "scenes/rock.gltf", device.get(), queue);
// models.planet.loadFromFile(getAssetPath() + "scenes/planet.gltf", device.get(), queue);
textures.rocks = load_texture_array("textures/texturearray_rocks_color_rgba.ktx", vkb::sg::Image::Color);
textures.planet = load_texture("textures/lavaplanet_color_rgba.ktx", vkb::sg::Image::Color);
// textures.rocks.loadFromFile(getAssetPath() + "textures/texturearray_rocks_color_rgba.ktx", device.get(), queue);
// textures.planet.loadFromFile(getAssetPath() + "textures/lavaplanet_color_rgba.ktx", device.get(), queue);
}
void Instancing::setup_descriptor_pool()
{
// Example uses one ubo
std::vector<VkDescriptorPoolSize> pool_sizes =
{
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2),
};
VkDescriptorPoolCreateInfo descriptor_pool_create_info =
vkb::initializers::descriptor_pool_create_info(
vkb::to_u32(pool_sizes.size()),
pool_sizes.data(),
2);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &descriptor_pool_create_info, nullptr, &descriptor_pool));
}
void Instancing::setup_descriptor_set_layout()
{
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings =
{
// Binding 0 : Vertex shader uniform buffer
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
VK_SHADER_STAGE_VERTEX_BIT,
0),
// Binding 1 : Fragment shader combined sampler
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
VK_SHADER_STAGE_FRAGMENT_BIT,
1),
};
VkDescriptorSetLayoutCreateInfo descriptor_layout_create_info =
vkb::initializers::descriptor_set_layout_create_info(
set_layout_bindings.data(),
vkb::to_u32(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout_create_info, nullptr, &descriptor_set_layout));
VkPipelineLayoutCreateInfo pipeline_layout_create_info =
vkb::initializers::pipeline_layout_create_info(
&descriptor_set_layout,
1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &pipeline_layout));
}
void Instancing::setup_descriptor_set()
{
VkDescriptorSetAllocateInfo descriptor_set_alloc_info;
std::vector<VkWriteDescriptorSet> write_descriptor_sets;
descriptor_set_alloc_info = vkb::initializers::descriptor_set_allocate_info(descriptor_pool, &descriptor_set_layout, 1);
// Instanced rocks
VkDescriptorBufferInfo buffer_descriptor = create_descriptor(*uniform_buffers.scene);
VkDescriptorImageInfo image_descriptor = create_descriptor(textures.rocks);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &descriptor_set_alloc_info, &descriptor_sets.instanced_rocks));
write_descriptor_sets = {
vkb::initializers::write_descriptor_set(descriptor_sets.instanced_rocks, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &buffer_descriptor), // Binding 0 : Vertex shader uniform buffer
vkb::initializers::write_descriptor_set(descriptor_sets.instanced_rocks, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &image_descriptor) // Binding 1 : Color map
};
vkUpdateDescriptorSets(get_device().get_handle(), vkb::to_u32(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
// Planet
buffer_descriptor = create_descriptor(*uniform_buffers.scene);
image_descriptor = create_descriptor(textures.planet);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &descriptor_set_alloc_info, &descriptor_sets.planet));
write_descriptor_sets = {
vkb::initializers::write_descriptor_set(descriptor_sets.planet, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &buffer_descriptor), // Binding 0 : Vertex shader uniform buffer
vkb::initializers::write_descriptor_set(descriptor_sets.planet, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &image_descriptor) // Binding 1 : Color map
};
vkUpdateDescriptorSets(get_device().get_handle(), vkb::to_u32(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
}
void Instancing::prepare_pipelines()
{
VkPipelineInputAssemblyStateCreateInfo input_assembly_state =
vkb::initializers::pipeline_input_assembly_state_create_info(
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
0,
VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterization_state =
vkb::initializers::pipeline_rasterization_state_create_info(
VK_POLYGON_MODE_FILL,
VK_CULL_MODE_BACK_BIT,
VK_FRONT_FACE_CLOCKWISE,
0);
VkPipelineColorBlendAttachmentState blend_attachment_state =
vkb::initializers::pipeline_color_blend_attachment_state(
0xf,
VK_FALSE);
VkPipelineColorBlendStateCreateInfo color_blend_state =
vkb::initializers::pipeline_color_blend_state_create_info(
1,
&blend_attachment_state);
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
VkPipelineDepthStencilStateCreateInfo depth_stencil_state =
vkb::initializers::pipeline_depth_stencil_state_create_info(
VK_TRUE,
VK_TRUE,
VK_COMPARE_OP_GREATER);
VkPipelineViewportStateCreateInfo viewport_state =
vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0);
VkPipelineMultisampleStateCreateInfo multisample_state =
vkb::initializers::pipeline_multisample_state_create_info(
VK_SAMPLE_COUNT_1_BIT,
0);
std::vector<VkDynamicState> dynamic_state_enables = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic_state =
vkb::initializers::pipeline_dynamic_state_create_info(
dynamic_state_enables.data(),
vkb::to_u32(dynamic_state_enables.size()),
0);
// Load shaders
std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages;
VkGraphicsPipelineCreateInfo pipeline_create_info =
vkb::initializers::pipeline_create_info(
pipeline_layout,
render_pass,
0);
pipeline_create_info.pInputAssemblyState = &input_assembly_state;
pipeline_create_info.pRasterizationState = &rasterization_state;
pipeline_create_info.pColorBlendState = &color_blend_state;
pipeline_create_info.pMultisampleState = &multisample_state;
pipeline_create_info.pViewportState = &viewport_state;
pipeline_create_info.pDepthStencilState = &depth_stencil_state;
pipeline_create_info.pDynamicState = &dynamic_state;
pipeline_create_info.stageCount = vkb::to_u32(shader_stages.size());
pipeline_create_info.pStages = shader_stages.data();
// This example uses two different input states, one for the instanced part and one for non-instanced rendering
VkPipelineVertexInputStateCreateInfo input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
std::vector<VkVertexInputBindingDescription> binding_descriptions;
std::vector<VkVertexInputAttributeDescription> attribute_descriptions;
// Vertex input bindings
// The instancing pipeline uses a vertex input state with two bindings
binding_descriptions = {
// Binding point 0: Mesh vertex layout description at per-vertex rate
vkb::initializers::vertex_input_binding_description(0, sizeof(Vertex), VK_VERTEX_INPUT_RATE_VERTEX),
// Binding point 1: Instanced data at per-instance rate
vkb::initializers::vertex_input_binding_description(1, sizeof(InstanceData), VK_VERTEX_INPUT_RATE_INSTANCE)};
// Vertex attribute bindings
// Note that the shader declaration for per-vertex and per-instance attributes is the same, the different input rates are only stored in the bindings:
// instanced.vert:
// layout (location = 0) in vec3 inPos; Per-Vertex
// ...
// layout (location = 4) in vec3 instancePos; Per-Instance
attribute_descriptions = {
// Per-vertex attributes
// These are advanced for each vertex fetched by the vertex shader
vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0), // Location 0: Position
vkb::initializers::vertex_input_attribute_description(0, 1, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3), // Location 1: Normal
vkb::initializers::vertex_input_attribute_description(0, 2, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 6), // Location 2: Texture coordinates
// Per-Instance attributes
// These are fetched for each instance rendered
vkb::initializers::vertex_input_attribute_description(1, 3, VK_FORMAT_R32G32B32_SFLOAT, 0), // Location 3: Position
vkb::initializers::vertex_input_attribute_description(1, 4, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3), // Location 4: Rotation
vkb::initializers::vertex_input_attribute_description(1, 5, VK_FORMAT_R32_SFLOAT, sizeof(float) * 6), // Location 5: Scale
vkb::initializers::vertex_input_attribute_description(1, 6, VK_FORMAT_R32_SINT, sizeof(float) * 7), // Location 6: Texture array layer index
};
input_state.pVertexBindingDescriptions = binding_descriptions.data();
input_state.pVertexAttributeDescriptions = attribute_descriptions.data();
pipeline_create_info.pVertexInputState = &input_state;
// Instancing pipeline
shader_stages[0] = load_shader("instancing", "instancing.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("instancing", "instancing.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
// Use all input bindings and attribute descriptions
input_state.vertexBindingDescriptionCount = static_cast<uint32_t>(binding_descriptions.size());
input_state.vertexAttributeDescriptionCount = static_cast<uint32_t>(attribute_descriptions.size());
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.instanced_rocks));
// Planet rendering pipeline
shader_stages[0] = load_shader("instancing", "planet.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("instancing", "planet.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
// Only use the non-instanced input bindings and attribute descriptions
input_state.vertexBindingDescriptionCount = 1;
input_state.vertexAttributeDescriptionCount = 3;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.planet));
// Star field pipeline
rasterization_state.cullMode = VK_CULL_MODE_NONE;
depth_stencil_state.depthWriteEnable = VK_FALSE;
depth_stencil_state.depthTestEnable = VK_FALSE;
shader_stages[0] = load_shader("instancing", "starfield.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("instancing", "starfield.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
// Vertices are generated in the vertex shader
input_state.vertexBindingDescriptionCount = 0;
input_state.vertexAttributeDescriptionCount = 0;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.starfield));
}
void Instancing::prepare_instance_data()
{
std::vector<InstanceData> instance_data;
instance_data.resize(INSTANCE_COUNT);
std::default_random_engine rnd_generator(lock_simulation_speed ? 0 : static_cast<unsigned>(time(nullptr)));
std::uniform_real_distribution<float> uniform_dist(0.0, 1.0);
std::uniform_int_distribution<uint32_t> rnd_texture_index(0, textures.rocks.image->get_vk_image().get_array_layer_count());
// Distribute rocks randomly on two different rings
for (auto i = 0; i < INSTANCE_COUNT / 2; i++)
{
glm::vec2 ring0{7.0f, 11.0f};
glm::vec2 ring1{14.0f, 18.0f};
float rho, theta;
// Inner ring
rho = sqrt((pow(ring0[1], 2.0f) - pow(ring0[0], 2.0f)) * uniform_dist(rnd_generator) + pow(ring0[0], 2.0f));
theta = 2.0f * glm::pi<float>() * uniform_dist(rnd_generator);
instance_data[i].pos = glm::vec3(rho * cos(theta), uniform_dist(rnd_generator) * 0.5f - 0.25f, rho * sin(theta));
instance_data[i].rot = glm::vec3(glm::pi<float>() * uniform_dist(rnd_generator), glm::pi<float>() * uniform_dist(rnd_generator), glm::pi<float>() * uniform_dist(rnd_generator));
instance_data[i].scale = 1.5f + uniform_dist(rnd_generator) - uniform_dist(rnd_generator);
instance_data[i].texIndex = rnd_texture_index(rnd_generator);
instance_data[i].scale *= 0.75f;
// Outer ring
rho = sqrt((pow(ring1[1], 2.0f) - pow(ring1[0], 2.0f)) * uniform_dist(rnd_generator) + pow(ring1[0], 2.0f));
theta = 2.0f * glm::pi<float>() * uniform_dist(rnd_generator);
instance_data[static_cast<size_t>(i + INSTANCE_COUNT / 2)].pos = glm::vec3(rho * cos(theta), uniform_dist(rnd_generator) * 0.5f - 0.25f, rho * sin(theta));
instance_data[static_cast<size_t>(i + INSTANCE_COUNT / 2)].rot = glm::vec3(glm::pi<float>() * uniform_dist(rnd_generator), glm::pi<float>() * uniform_dist(rnd_generator), glm::pi<float>() * uniform_dist(rnd_generator));
instance_data[static_cast<size_t>(i + INSTANCE_COUNT / 2)].scale = 1.5f + uniform_dist(rnd_generator) - uniform_dist(rnd_generator);
instance_data[static_cast<size_t>(i + INSTANCE_COUNT / 2)].texIndex = rnd_texture_index(rnd_generator);
instance_data[static_cast<size_t>(i + INSTANCE_COUNT / 2)].scale *= 0.75f;
}
instance_buffer.size = instance_data.size() * sizeof(InstanceData);
// Staging
// Instanced data is static, copy to device local memory
// On devices with separate memory types for host visible and device local memory this will result in better performance
// On devices with unified memory types (DEVICE_LOCAL_BIT and HOST_VISIBLE_BIT supported at once) this isn't necessary and you could skip the staging
vkb::core::BufferC staging_buffer = vkb::core::BufferC::create_staging_buffer(get_device(), instance_data);
instance_buffer.buffer = std::make_unique<vkb::core::BufferC>(get_device(), instance_buffer.size, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_GPU_ONLY);
// Copy to staging buffer
VkCommandBuffer copy_command = get_device().create_command_buffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
VkBufferCopy copy_region = {};
copy_region.size = instance_buffer.size;
vkCmdCopyBuffer(
copy_command,
staging_buffer.get_handle(),
instance_buffer.buffer->get_handle(),
1,
&copy_region);
get_device().flush_command_buffer(copy_command, queue, true);
instance_buffer.descriptor.range = instance_buffer.size;
instance_buffer.descriptor.buffer = instance_buffer.buffer->get_handle();
instance_buffer.descriptor.offset = 0;
}
void Instancing::prepare_uniform_buffers()
{
uniform_buffers.scene = std::make_unique<vkb::core::BufferC>(get_device(),
sizeof(ubo_vs),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffer(0.0f);
}
void Instancing::update_uniform_buffer(float delta_time)
{
ubo_vs.projection = camera.matrices.perspective;
ubo_vs.view = camera.matrices.view;
if (!paused)
{
ubo_vs.loc_speed += delta_time * 0.35f;
ubo_vs.glob_speed += delta_time * 0.01f;
}
uniform_buffers.scene->convert_and_update(ubo_vs);
}
void Instancing::draw()
{
ApiVulkanSample::prepare_frame();
// Command buffer to be submitted to the queue
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &draw_cmd_buffers[current_buffer];
// Submit to queue
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE));
ApiVulkanSample::submit_frame();
}
bool Instancing::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
// Note: Using reversed depth-buffer for increased precision, so Znear and Zfar are flipped
camera.type = vkb::CameraType::LookAt;
camera.set_perspective(60.0f, static_cast<float>(width) / static_cast<float>(height), 256.0f, 0.1f);
camera.set_rotation(glm::vec3(-17.2f, -4.7f, 0.0f));
camera.set_translation(glm::vec3(5.5f, -1.85f, -18.5f));
load_assets();
prepare_instance_data();
prepare_uniform_buffers();
setup_descriptor_set_layout();
prepare_pipelines();
setup_descriptor_pool();
setup_descriptor_set();
build_command_buffers();
prepared = true;
return true;
}
void Instancing::render(float delta_time)
{
if (!prepared)
{
return;
}
draw();
if (!paused || camera.updated)
{
update_uniform_buffer(delta_time);
}
}
void Instancing::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Statistics"))
{
drawer.text("Instances: %d", INSTANCE_COUNT);
}
}
bool Instancing::resize(const uint32_t width, const uint32_t height)
{
ApiVulkanSample::resize(width, height);
rebuild_command_buffers();
return true;
}
std::unique_ptr<vkb::Application> create_instancing()
{
return std::make_unique<Instancing>();
}
-111
View File
@@ -1,111 +0,0 @@
/* Copyright (c) 2019-2024, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Instanced mesh rendering, uses a separate vertex buffer for instanced data
*/
#pragma once
#include "api_vulkan_sample.h"
#if defined(__ANDROID__)
# define INSTANCE_COUNT 4096
#else
# define INSTANCE_COUNT 8192
#endif
class Instancing : public ApiVulkanSample
{
public:
struct Textures
{
Texture rocks;
Texture planet;
} textures;
struct Models
{
std::unique_ptr<vkb::sg::SubMesh> rock;
std::unique_ptr<vkb::sg::SubMesh> planet;
} models;
// Per-instance data block
struct InstanceData
{
glm::vec3 pos;
glm::vec3 rot;
float scale;
uint32_t texIndex;
};
// Contains the instanced data
struct InstanceBuffer
{
std::unique_ptr<vkb::core::BufferC> buffer;
size_t size = 0;
VkDescriptorBufferInfo descriptor;
} instance_buffer;
struct UBOVS
{
glm::mat4 projection;
glm::mat4 view;
glm::vec4 light_pos = glm::vec4(0.0f, -5.0f, 0.0f, 1.0f);
float loc_speed = 0.0f;
float glob_speed = 0.0f;
} ubo_vs;
struct UniformBuffers
{
std::unique_ptr<vkb::core::BufferC> scene;
} uniform_buffers;
VkPipelineLayout pipeline_layout;
struct Pipelines
{
VkPipeline instanced_rocks;
VkPipeline planet;
VkPipeline starfield;
} pipelines;
VkDescriptorSetLayout descriptor_set_layout;
struct DescriptorSets
{
VkDescriptorSet instanced_rocks;
VkDescriptorSet planet;
} descriptor_sets;
Instancing();
~Instancing();
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
void build_command_buffers() override;
void load_assets();
void setup_descriptor_pool();
void setup_descriptor_set_layout();
void setup_descriptor_set();
void prepare_pipelines();
void prepare_instance_data();
void prepare_uniform_buffers();
void update_uniform_buffer(float delta_time);
void draw();
bool prepare(const vkb::ApplicationOptions &options) override;
virtual void render(float delta_time) override;
virtual void on_update_ui_overlay(vkb::Drawer &drawer) override;
virtual bool resize(const uint32_t width, const uint32_t height) override;
};
std::unique_ptr<vkb::Application> create_instancing();
@@ -1,35 +0,0 @@
# Copyright (c) 2024, Google
#
# 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 "Google"
NAME "Order-independent transparency (depth peeling)"
DESCRIPTION "Order-independent transparency using depth peeling"
SHADER_FILES_GLSL
"oit_depth_peeling/background.frag"
"oit_depth_peeling/combine.frag"
"oit_depth_peeling/fullscreen.vert"
"oit_depth_peeling/gather.frag"
"oit_depth_peeling/gather.vert"
"oit_depth_peeling/gather_first.frag")
-78
View File
@@ -1,78 +0,0 @@
////
- Copyright (c) 2024, Google
-
- 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.
-
////
= Order-independent transparency with depth peeling
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/oit_depth_peeling[Khronos Vulkan samples github repository].
endif::[]
:pp: {plus}{plus}
image::./images/sample.png[Sample]
== Overview
This sample implements an order-independent transparency (OIT) algorithm using depth peeling.
It renders a single torus whose opacity can be controlled via the UI.
It produces pixel-perfect results.
It is based on the https://developer.download.nvidia.com/assets/gamedev/docs/OrderIndependentTransparency.pdf[original paper] from Cass Everitt.
== Algorithm
The OIT algorithm consists of several _gather_ passes followed by one _combine_ pass.
Each _gather_ pass renders one layer of transparent geometry.
The first pass renders the first layer, the second pass the second layer, etc.
The N^th^ layer consists of all the N^th^ fragments of each pixel when the fragments are ordered from front to back.
The _combine_ pass is a screen-space operation.
It merges the layer images from back to front to produce the final result.
The algorithm can produce pixel-perfect results, even with intersecting geometry.
When there are more geometry layers than gather passes, the backmost layers get skipped, but the visual results stay stable (i.e. no flickering pixels).
== Options
[cols="2,4,4"]
|===
| Option | Description | Comments
| Camera auto-rotation
| Enable the automatic rotation of the camera
|
| Background grayscale
| Specify the grayscale value by which the background color is multiplied (0.0 to 1.0)
|
| Object alpha
| Specify the opacity of the transparent object (0.0 to 1.0)
|
| Front layer index
| The first layer to be rendered (0 to 7).
|
| Back layer index
| The last layer to be rendered (0 to 7).
| This cannot be less that the front layer index.
|===
Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

@@ -1,578 +0,0 @@
/* Copyright (c) 2024-2025, Google
*
* 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 "oit_depth_peeling.h"
#include <algorithm>
OITDepthPeeling::OITDepthPeeling()
{
}
OITDepthPeeling::~OITDepthPeeling()
{
if (!has_device())
{
return;
}
vkDestroyPipeline(get_device().get_handle(), background_pipeline, nullptr);
vkDestroyPipeline(get_device().get_handle(), combine_pipeline, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), combine_pipeline_layout, nullptr);
vkDestroyPipeline(get_device().get_handle(), gather_pipeline, nullptr);
vkDestroyPipeline(get_device().get_handle(), gather_first_pipeline, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), gather_pipeline_layout, nullptr);
vkDestroyDescriptorPool(get_device().get_handle(), descriptor_pool, VK_NULL_HANDLE);
destroy_sized_objects();
scene_constants.reset();
vkDestroyDescriptorSetLayout(get_device().get_handle(), gather_descriptor_set_layout, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), combine_descriptor_set_layout, nullptr);
vkDestroySampler(get_device().get_handle(), point_sampler, VK_NULL_HANDLE);
vkDestroySampler(get_device().get_handle(), background_texture.sampler, nullptr);
object.reset();
}
bool OITDepthPeeling::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
camera.type = vkb::CameraType::LookAt;
camera.set_position({0.0f, 0.0f, -4.0f});
camera.set_rotation({0.0f, 0.0f, 0.0f});
camera.set_perspective(60.0f, static_cast<float>(width) / static_cast<float>(height), 16.0f, 0.1f);
load_assets();
create_samplers();
create_constant_buffers();
create_descriptors();
create_sized_objects(width, height);
create_pipelines();
update_scene_constants();
update_descriptors();
build_command_buffers();
prepared = true;
return true;
}
bool OITDepthPeeling::resize(const uint32_t width, const uint32_t height)
{
destroy_sized_objects();
create_sized_objects(width, height);
update_descriptors();
ApiVulkanSample::resize(width, height);
return true;
}
void OITDepthPeeling::render(float delta_time)
{
if (!prepared)
{
return;
}
ApiVulkanSample::prepare_frame();
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &draw_cmd_buffers[current_buffer];
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE));
ApiVulkanSample::submit_frame();
if (camera_auto_rotation)
{
camera.rotate({delta_time * 5.0f, delta_time * 5.0f, 0.0f});
}
update_scene_constants();
}
void OITDepthPeeling::request_gpu_features(vkb::PhysicalDevice &gpu)
{
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = VK_TRUE;
}
else
{
throw std::runtime_error("This sample requires support for anisotropic sampling");
}
}
void OITDepthPeeling::on_update_ui_overlay(vkb::Drawer &drawer)
{
drawer.checkbox("Camera auto-rotation", &camera_auto_rotation);
drawer.slider_float("Background grayscale", &background_grayscale, kBackgroundGrayscaleMin, kBackgroundGrayscaleMax);
drawer.slider_float("Object opacity", &object_alpha, kObjectAlphaMin, kObjectAlphaMax);
drawer.slider_int("Front layer index", &front_layer_index, 0, back_layer_index);
drawer.slider_int("Back layer index", &back_layer_index, front_layer_index, kLayerMaxCount - 1);
}
void OITDepthPeeling::build_command_buffers()
{
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VkClearValue clear_values[2];
clear_values[0].color = {{0.0f, 0.0f, 0.0f, 0.0f}};
clear_values[1].depthStencil = {0.0f, 0};
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.renderArea.offset.x = 0;
render_pass_begin_info.renderArea.offset.y = 0;
render_pass_begin_info.renderArea.extent.width = width;
render_pass_begin_info.renderArea.extent.height = height;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.pClearValues = clear_values;
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
VK_CHECK(vkBeginCommandBuffer(draw_cmd_buffers[i], &command_buffer_begin_info));
{
// Gather passes
// Each pass renders a single transparent layer into a layer texture.
for (uint32_t l = 0; l <= back_layer_index; ++l)
{
// Two depth textures are used.
// Their roles alternates for each pass.
// The first depth texture is used for fixed-function depth test.
// The second one is the result of the depth test from the previous gather pass.
// It is bound as texture and read in the shader to discard fragments from the
// previous layers.
VkImageSubresourceRange depth_subresource_range = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
vkb::image_layout_transition(
draw_cmd_buffers[i], depth_image[l % kDepthCount]->get_handle(),
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
l <= 1 ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
depth_subresource_range);
if (l > 0)
{
vkb::image_layout_transition(
draw_cmd_buffers[i], depth_image[(l + 1) % kDepthCount]->get_handle(),
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL,
depth_subresource_range);
}
// Set one of the layer textures as color attachment, as the gather pass will render to it.
VkImageSubresourceRange layer_subresource_range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkb::image_layout_transition(
draw_cmd_buffers[i], layer_image[l]->get_handle(),
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
layer_subresource_range);
render_pass_begin_info.framebuffer = gather_framebuffer[l];
render_pass_begin_info.renderPass = gather_render_pass;
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
{
// Render the geometry into the layer texture.
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(width, height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, gather_pipeline_layout, 0, 1, &gather_descriptor_set[l % kDepthCount], 0, NULL);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, l == 0 ? gather_first_pipeline : gather_pipeline);
draw_model(object, draw_cmd_buffers[i]);
}
vkCmdEndRenderPass(draw_cmd_buffers[i]);
// Get the layer texture ready to be read by the combine pass.
vkb::image_layout_transition(
draw_cmd_buffers[i], layer_image[l]->get_handle(),
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
layer_subresource_range);
}
// Combine pass
// This pass blends all the layers into the final transparent color.
// The final color is then alpha blended into the background.
render_pass_begin_info.framebuffer = framebuffers[i];
render_pass_begin_info.renderPass = render_pass;
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
{
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(width, height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, combine_pipeline_layout, 0, 1, &combine_descriptor_set, 0, NULL);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, background_pipeline);
vkCmdDraw(draw_cmd_buffers[i], 3, 1, 0, 0);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, combine_pipeline);
vkCmdDraw(draw_cmd_buffers[i], 3, 1, 0, 0);
draw_ui(draw_cmd_buffers[i]);
}
vkCmdEndRenderPass(draw_cmd_buffers[i]);
}
VK_CHECK(vkEndCommandBuffer(draw_cmd_buffers[i]));
}
}
////////////////////////////////////////////////////////////////////////////////
void OITDepthPeeling::create_sized_objects(const uint32_t width, const uint32_t height)
{
create_images(width, height);
create_gather_pass_objects(width, height);
}
void OITDepthPeeling::destroy_sized_objects()
{
for (uint32_t i = 0; i < kDepthCount; ++i)
{
depth_image_view[i].reset();
depth_image[i].reset();
}
for (uint32_t i = 0; i < kLayerMaxCount; ++i)
{
vkDestroyFramebuffer(get_device().get_handle(), gather_framebuffer[i], nullptr);
layer_image_view[i].reset();
layer_image[i].reset();
}
vkDestroyRenderPass(get_device().get_handle(), gather_render_pass, nullptr);
}
void OITDepthPeeling::create_gather_pass_objects(const uint32_t width, const uint32_t height)
{
const VkAttachmentReference color_attachment_reference = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
const VkAttachmentReference depth_attachment_reference = {1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color_attachment_reference;
subpass.pDepthStencilAttachment = &depth_attachment_reference;
VkAttachmentDescription attachment_descriptions[2] = {};
attachment_descriptions[0].format = VK_FORMAT_R8G8B8A8_UNORM;
attachment_descriptions[0].samples = VK_SAMPLE_COUNT_1_BIT;
attachment_descriptions[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachment_descriptions[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment_descriptions[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment_descriptions[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment_descriptions[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachment_descriptions[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
attachment_descriptions[1].format = VK_FORMAT_D32_SFLOAT;
attachment_descriptions[1].samples = VK_SAMPLE_COUNT_1_BIT;
attachment_descriptions[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachment_descriptions[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment_descriptions[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment_descriptions[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment_descriptions[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachment_descriptions[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkRenderPassCreateInfo render_pass_create_info = {};
render_pass_create_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
render_pass_create_info.subpassCount = 1;
render_pass_create_info.pSubpasses = &subpass;
render_pass_create_info.attachmentCount = 2;
render_pass_create_info.pAttachments = attachment_descriptions;
VK_CHECK(vkCreateRenderPass(get_device().get_handle(), &render_pass_create_info, nullptr, &gather_render_pass));
VkFramebufferCreateInfo framebuffer_create_info = vkb::initializers::framebuffer_create_info();
framebuffer_create_info.width = width;
framebuffer_create_info.height = height;
framebuffer_create_info.layers = 1;
framebuffer_create_info.attachmentCount = 2;
for (uint32_t i = 0; i < kLayerMaxCount; ++i)
{
framebuffer_create_info.renderPass = gather_render_pass;
const VkImageView attachments[] = {
layer_image_view[i]->get_handle(),
depth_image_view[i % kDepthCount]->get_handle(),
};
framebuffer_create_info.pAttachments = attachments;
VK_CHECK(vkCreateFramebuffer(get_device().get_handle(), &framebuffer_create_info, nullptr, gather_framebuffer + i));
}
}
void OITDepthPeeling::create_images(const uint32_t width, const uint32_t height)
{
const VkExtent3D image_extent = {width, height, 1};
for (uint32_t i = 0; i < kLayerMaxCount; ++i)
{
layer_image[i] = std::make_unique<vkb::core::Image>(get_device(), image_extent, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, VMA_MEMORY_USAGE_GPU_ONLY, VK_SAMPLE_COUNT_1_BIT);
layer_image_view[i] = std::make_unique<vkb::core::ImageView>(*layer_image[i], VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_R8G8B8A8_UNORM);
}
for (uint32_t i = 0; i < kDepthCount; ++i)
{
depth_image[i] = std::make_unique<vkb::core::Image>(get_device(), image_extent, VK_FORMAT_D32_SFLOAT, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VMA_MEMORY_USAGE_GPU_ONLY, VK_SAMPLE_COUNT_1_BIT);
depth_image_view[i] = std::make_unique<vkb::core::ImageView>(*depth_image[i], VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_D32_SFLOAT);
}
}
////////////////////////////////////////////////////////////////////////////////
void OITDepthPeeling::load_assets()
{
object = load_model("scenes/torusknot.gltf");
background_texture = load_texture("textures/vulkan_logo_full.ktx", vkb::sg::Image::Color);
}
void OITDepthPeeling::create_samplers()
{
VkSamplerCreateInfo sampler_info = vkb::initializers::sampler_create_info();
sampler_info.magFilter = VK_FILTER_NEAREST;
sampler_info.minFilter = VK_FILTER_NEAREST;
sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
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.mipLodBias = 0.0f;
sampler_info.maxAnisotropy = 1.0f;
sampler_info.minLod = 0.0f;
sampler_info.maxLod = 1.0f;
sampler_info.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
VK_CHECK(vkCreateSampler(get_device().get_handle(), &sampler_info, nullptr, &point_sampler));
}
void OITDepthPeeling::create_constant_buffers()
{
scene_constants = std::make_unique<vkb::core::BufferC>(get_device(), sizeof(SceneConstants), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
}
void OITDepthPeeling::create_descriptors()
{
{
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings = {
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1),
};
VkDescriptorSetLayoutCreateInfo descriptor_layout_create_info = vkb::initializers::descriptor_set_layout_create_info(set_layout_bindings.data(), static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout_create_info, nullptr, &gather_descriptor_set_layout));
}
{
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings = {
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 2, kLayerMaxCount),
};
VkDescriptorSetLayoutCreateInfo descriptor_layout_create_info = vkb::initializers::descriptor_set_layout_create_info(set_layout_bindings.data(), static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout_create_info, nullptr, &combine_descriptor_set_layout));
}
{
const uint32_t num_gather_pass_combined_image_sampler = kDepthCount;
const uint32_t num_gather_pass_uniform_buffer = kDepthCount;
const uint32_t num_combine_pass_combined_image_sampler = kLayerMaxCount + 1;
const uint32_t num_combine_pass_uniform_buffer = 1;
const uint32_t num_uniform_buffer_descriptors = num_gather_pass_uniform_buffer + num_combine_pass_uniform_buffer;
const uint32_t num_combined_image_sampler_descriptors = num_gather_pass_combined_image_sampler + num_combine_pass_combined_image_sampler;
std::vector<VkDescriptorPoolSize> pool_sizes = {
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, num_uniform_buffer_descriptors),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, num_combined_image_sampler_descriptors),
};
const uint32_t num_gather_descriptor_sets = 2;
const uint32_t num_combine_descriptor_sets = 1;
const uint32_t num_descriptor_sets = num_gather_descriptor_sets + num_combine_descriptor_sets;
VkDescriptorPoolCreateInfo descriptor_pool_create_info = vkb::initializers::descriptor_pool_create_info(static_cast<uint32_t>(pool_sizes.size()), pool_sizes.data(), num_descriptor_sets);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &descriptor_pool_create_info, nullptr, &descriptor_pool));
}
{
const VkDescriptorSetLayout layouts[kDepthCount] = {gather_descriptor_set_layout, gather_descriptor_set_layout};
VkDescriptorSetAllocateInfo alloc_info = vkb::initializers::descriptor_set_allocate_info(descriptor_pool, layouts, kDepthCount);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, gather_descriptor_set));
}
{
VkDescriptorSetAllocateInfo alloc_info = vkb::initializers::descriptor_set_allocate_info(descriptor_pool, &combine_descriptor_set_layout, 1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &combine_descriptor_set));
}
}
void OITDepthPeeling::update_descriptors()
{
VkDescriptorBufferInfo scene_constants_descriptor = create_descriptor(*scene_constants);
for (uint32_t i = 0; i < kDepthCount; ++i)
{
VkDescriptorImageInfo depth_texture_descriptor = {};
depth_texture_descriptor.sampler = point_sampler;
depth_texture_descriptor.imageView = depth_image_view[(i + 1) % kDepthCount]->get_handle();
depth_texture_descriptor.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
std::vector<VkWriteDescriptorSet> write_descriptor_sets = {
vkb::initializers::write_descriptor_set(gather_descriptor_set[i], VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &scene_constants_descriptor),
vkb::initializers::write_descriptor_set(gather_descriptor_set[i], VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &depth_texture_descriptor),
};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
}
{
std::vector<VkWriteDescriptorSet> write_descriptor_sets(3);
write_descriptor_sets[0] = vkb::initializers::write_descriptor_set(combine_descriptor_set, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &scene_constants_descriptor);
VkDescriptorImageInfo background_texture_descriptor = create_descriptor(background_texture);
write_descriptor_sets[1] = vkb::initializers::write_descriptor_set(combine_descriptor_set, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &background_texture_descriptor);
VkDescriptorImageInfo layer_texture_descriptor[kLayerMaxCount];
for (uint32_t i = 0; i < kLayerMaxCount; ++i)
{
layer_texture_descriptor[i].sampler = point_sampler;
layer_texture_descriptor[i].imageView = layer_image_view[i]->get_handle();
layer_texture_descriptor[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}
write_descriptor_sets[2] = vkb::initializers::write_descriptor_set(combine_descriptor_set, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2, layer_texture_descriptor, kLayerMaxCount);
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
}
}
void OITDepthPeeling::create_pipelines()
{
{
VkPipelineLayoutCreateInfo pipeline_layout_create_info = vkb::initializers::pipeline_layout_create_info(&gather_descriptor_set_layout, 1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &gather_pipeline_layout));
}
{
VkPipelineLayoutCreateInfo pipeline_layout_create_info = vkb::initializers::pipeline_layout_create_info(&combine_descriptor_set_layout, 1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &combine_pipeline_layout));
}
{
VkPipelineVertexInputStateCreateInfo vertex_input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
VkPipelineInputAssemblyStateCreateInfo input_assembly_state = vkb::initializers::pipeline_input_assembly_state_create_info(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterization_state = vkb::initializers::pipeline_rasterization_state_create_info(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, 0);
VkPipelineColorBlendAttachmentState blend_attachment_state = vkb::initializers::pipeline_color_blend_attachment_state(0xF, VK_FALSE);
VkPipelineColorBlendStateCreateInfo color_blend_state = vkb::initializers::pipeline_color_blend_state_create_info(1, &blend_attachment_state);
VkPipelineMultisampleStateCreateInfo multisample_state = vkb::initializers::pipeline_multisample_state_create_info(VK_SAMPLE_COUNT_1_BIT, 0);
VkPipelineViewportStateCreateInfo viewport_state = vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0);
VkPipelineDepthStencilStateCreateInfo depth_stencil_state = vkb::initializers::pipeline_depth_stencil_state_create_info(VK_TRUE, VK_TRUE, VK_COMPARE_OP_GREATER);
std::vector<VkDynamicState> dynamic_state_enables = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamicState = vkb::initializers::pipeline_dynamic_state_create_info(dynamic_state_enables.data(), static_cast<uint32_t>(dynamic_state_enables.size()), 0);
std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages{};
VkGraphicsPipelineCreateInfo pipeline_create_info = vkb::initializers::pipeline_create_info(gather_pipeline_layout, gather_render_pass, 0);
pipeline_create_info.pVertexInputState = &vertex_input_state;
pipeline_create_info.pInputAssemblyState = &input_assembly_state;
pipeline_create_info.pRasterizationState = &rasterization_state;
pipeline_create_info.pColorBlendState = &color_blend_state;
pipeline_create_info.pMultisampleState = &multisample_state;
pipeline_create_info.pViewportState = &viewport_state;
pipeline_create_info.pDepthStencilState = &depth_stencil_state;
pipeline_create_info.pDynamicState = &dynamicState;
pipeline_create_info.stageCount = static_cast<uint32_t>(shader_stages.size());
pipeline_create_info.pStages = shader_stages.data();
{
const std::vector<VkVertexInputBindingDescription> vertex_input_bindings = {
vkb::initializers::vertex_input_binding_description(0, sizeof(Vertex), VK_VERTEX_INPUT_RATE_VERTEX),
};
const std::vector<VkVertexInputAttributeDescription> vertex_input_attributes = {
vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, pos)),
vkb::initializers::vertex_input_attribute_description(0, 1, VK_FORMAT_R32G32_SFLOAT, offsetof(Vertex, uv)),
};
vertex_input_state.vertexBindingDescriptionCount = static_cast<uint32_t>(vertex_input_bindings.size());
vertex_input_state.pVertexBindingDescriptions = vertex_input_bindings.data();
vertex_input_state.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size());
vertex_input_state.pVertexAttributeDescriptions = vertex_input_attributes.data();
shader_stages[0] = load_shader("oit_depth_peeling/gather.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("oit_depth_peeling/gather_first.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &gather_first_pipeline));
shader_stages[1] = load_shader("oit_depth_peeling/gather.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &gather_pipeline));
}
pipeline_create_info.layout = combine_pipeline_layout;
vertex_input_state.vertexBindingDescriptionCount = 0;
vertex_input_state.pVertexBindingDescriptions = nullptr;
vertex_input_state.vertexAttributeDescriptionCount = 0;
vertex_input_state.pVertexAttributeDescriptions = nullptr;
depth_stencil_state = vkb::initializers::pipeline_depth_stencil_state_create_info(VK_FALSE, VK_FALSE, VK_COMPARE_OP_GREATER);
pipeline_create_info.renderPass = render_pass;
{
shader_stages[0] = load_shader("oit_depth_peeling/fullscreen.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("oit_depth_peeling/background.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &background_pipeline));
}
{
blend_attachment_state.blendEnable = VK_TRUE;
blend_attachment_state.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
blend_attachment_state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment_state.colorBlendOp = VK_BLEND_OP_ADD;
blend_attachment_state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
blend_attachment_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
blend_attachment_state.alphaBlendOp = VK_BLEND_OP_ADD;
shader_stages[0] = load_shader("oit_depth_peeling/fullscreen.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("oit_depth_peeling/combine.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &combine_pipeline));
}
}
}
////////////////////////////////////////////////////////////////////////////////
void OITDepthPeeling::update_scene_constants()
{
SceneConstants constants = {};
constants.model_view_projection = camera.matrices.perspective * camera.matrices.view * glm::scale(glm::mat4(1.0f), glm::vec3(0.08));
constants.background_grayscale = background_grayscale;
constants.object_alpha = object_alpha;
constants.front_layer_index = front_layer_index;
constants.back_layer_index = back_layer_index;
scene_constants->convert_and_update(constants);
}
////////////////////////////////////////////////////////////////////////////////
std::unique_ptr<vkb::VulkanSampleC> create_oit_depth_peeling()
{
return std::make_unique<OITDepthPeeling>();
}
@@ -1,110 +0,0 @@
/* Copyright (c) 2024, Google
*
* 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 "api_vulkan_sample.h"
#include "rendering/render_pipeline.h"
#include "scene_graph/components/camera.h"
class OITDepthPeeling : public ApiVulkanSample
{
public:
OITDepthPeeling();
~OITDepthPeeling();
bool prepare(const vkb::ApplicationOptions &options) override;
bool resize(const uint32_t width, const uint32_t height) override;
void render(float delta_time) override;
void build_command_buffers() override;
void request_gpu_features(vkb::PhysicalDevice &gpu) override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
private:
void create_sized_objects(const uint32_t width, const uint32_t height);
void destroy_sized_objects();
void create_gather_pass_objects(const uint32_t width, const uint32_t height);
void create_images(const uint32_t width, const uint32_t height);
void load_assets();
void create_samplers();
void create_constant_buffers();
void create_descriptors();
void create_pipelines();
void update_descriptors();
void update_scene_constants();
private:
static constexpr uint32_t kLayerMaxCount = 8;
static constexpr uint32_t kDepthCount = 2;
static constexpr float kBackgroundGrayscaleMin = 0.0f;
static constexpr float kBackgroundGrayscaleMax = 1.0f;
static constexpr float kObjectAlphaMin = 0.0f;
static constexpr float kObjectAlphaMax = 1.0f;
struct SceneConstants
{
glm::mat4 model_view_projection;
glm::f32 background_grayscale;
glm::f32 object_alpha;
glm::int32 front_layer_index;
glm::int32 back_layer_index;
};
private:
std::unique_ptr<vkb::sg::SubMesh> object;
Texture background_texture;
std::unique_ptr<vkb::core::BufferC> scene_constants;
VkSampler point_sampler = VK_NULL_HANDLE;
std::unique_ptr<vkb::core::Image> layer_image[kLayerMaxCount];
std::unique_ptr<vkb::core::ImageView> layer_image_view[kLayerMaxCount];
std::unique_ptr<vkb::core::Image> depth_image[kDepthCount];
std::unique_ptr<vkb::core::ImageView> depth_image_view[kDepthCount];
VkRenderPass gather_render_pass = VK_NULL_HANDLE;
VkFramebuffer gather_framebuffer[kLayerMaxCount] = {};
VkDescriptorSetLayout gather_descriptor_set_layout = VK_NULL_HANDLE;
VkDescriptorSet gather_descriptor_set[kDepthCount] = {};
VkDescriptorSetLayout combine_descriptor_set_layout = VK_NULL_HANDLE;
VkDescriptorSet combine_descriptor_set = VK_NULL_HANDLE;
VkDescriptorPool descriptor_pool = VK_NULL_HANDLE;
VkPipelineLayout gather_pipeline_layout = VK_NULL_HANDLE;
VkPipeline gather_first_pipeline = VK_NULL_HANDLE;
VkPipeline gather_pipeline = VK_NULL_HANDLE;
VkPipelineLayout combine_pipeline_layout = VK_NULL_HANDLE;
VkPipeline combine_pipeline = VK_NULL_HANDLE;
VkPipeline background_pipeline = VK_NULL_HANDLE;
int32_t camera_auto_rotation = false;
float background_grayscale = 0.3f;
float object_alpha = 0.5f;
int32_t front_layer_index = 0;
int32_t back_layer_index = kLayerMaxCount - 1;
};
std::unique_ptr<vkb::VulkanSampleC> create_oit_depth_peeling();
@@ -1,35 +0,0 @@
# Copyright (c) 2023-2024, Google
#
# 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 "Google"
NAME "Order-independent transparency (ordered linked lists)"
DESCRIPTION "Order-independent transparency using per-pixel ordered linked lists"
SHADER_FILES_GLSL
"oit_linked_lists/background.frag"
"oit_linked_lists/combine.frag"
"oit_linked_lists/combine.vert"
"oit_linked_lists/fullscreen.vert"
"oit_linked_lists/gather.frag"
"oit_linked_lists/gather.vert")
-89
View File
@@ -1,89 +0,0 @@
////
- Copyright (c) 2023, Google
-
- 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.
-
////
= Order-independent transparency with per-pixel ordered linked lists
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/oit_linked_lists[Khronos Vulkan samples github repository].
endif::[]
:pp: {plus}{plus}
image::./images/sample.png[Sample]
== Overview
This sample implements an order-independent transparency (OIT) algorithm using per-pixel ordered linked lists.
It renders 64 spheres with random color and opacity (from 0.2 to 1.0).
It produces pixel-perfect results.
== Algorithm
The OIT algorithm consists of two passes: the _gather_ pass and the _combine_ pass.
During the gather pass, the transparent geometry is rendered into per-pixel order linked lists.
Each fragment color and depth is pushed into the linked list associated with its destination pixel.
The linked lists head are stored into a storage image that is the size of the screen.
The fragment data (color and depth) is stored into a storage buffer shared by all linked lists.
The _combine_ pass is a screen-space operation.
For each pixel, it sorts the fragments stored in the linked list of that pixel.
It then alpha blends (in the shader code) them to produce the final transparent color and coverage.
Finally, it alpha blends (via the fixed blend function) the transparent color into the backbuffer.
The algorithm can produce pixel-perfect results, even with intersecting geometry.
However, there is a catch.
To keep performance high, the maximum number of sorted fragments per-pixel is limited to 16.
For more than 16 fragments, the algorithm does its best effort to blend the extra fragments, but the results might be inaccurate.
This is well enough for the sample, due to the way the objects are placed in the scene.
In general, there is a trade-off between performance and correctness.
To keep occupancy high, the maximum number of sorted fragments (`SORTED_FRAGMENT_MAX_COUNT` in `combine.frag`) should be kept low.
To get correct results in every situation, that same number should be as high as possible.
The artifacts resulting from a low number of sorted fragments per pixel can be observed by using the `Sorted fragments per pixel` option.
== Options
[cols="2,4,4"]
|===
| Option | Description | Comments
| Sort fragments
| Enable fragment sorting in the combine pass
| This option, when disabled, is meant to demonstrate the visual issues that occur with non-sorted transparent geometry.
| Camera auto-rotation
| Enable the automatic rotation of the camera
|
| Sorted fragments per pixel
| Specify the maximum number of fragments sorted per pixel
| This option, when set to a low number (e.g. 4), highlights the main weakness of the algorithm.
| Background grayscale
| Specify the grayscale value by which the background color is multiplied (0.0 to 1.0)
|
|===
== Tests
This sample was tested on Windows.
The validation layers were enabled and all reported issues were fixed.
The sample was also tested on Linux during development.
Both systems featured an AMD GPU.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

@@ -1,524 +0,0 @@
/* Copyright (c) 2023-2025, Google
*
* 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 "oit_linked_lists.h"
#include <algorithm>
OITLinkedLists::OITLinkedLists()
{
}
OITLinkedLists::~OITLinkedLists()
{
if (!has_device())
{
return;
}
vkDestroyPipeline(get_device().get_handle(), combine_pipeline, nullptr);
vkDestroyPipeline(get_device().get_handle(), background_pipeline, nullptr);
vkDestroyPipeline(get_device().get_handle(), gather_pipeline, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layout, nullptr);
vkDestroyDescriptorPool(get_device().get_handle(), descriptor_pool, VK_NULL_HANDLE);
vkDestroyDescriptorSetLayout(get_device().get_handle(), descriptor_set_layout, nullptr);
destroy_sized_objects();
instance_data.reset();
scene_constants.reset();
vkDestroySampler(get_device().get_handle(), background_texture.sampler, nullptr);
object.reset();
}
bool OITLinkedLists::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
camera.type = vkb::CameraType::LookAt;
camera.set_position({0.0f, 0.0f, -4.0f});
camera.set_rotation({0.0f, 0.0f, 0.0f});
camera.set_perspective(60.0f, static_cast<float>(width) / static_cast<float>(height), 256.0f, 0.1f);
load_assets();
create_constant_buffers();
create_descriptors();
create_sized_objects(width, height);
create_pipelines();
update_scene_constants();
fill_instance_data();
update_descriptors();
build_command_buffers();
prepared = true;
return true;
}
bool OITLinkedLists::resize(const uint32_t width, const uint32_t height)
{
if ((width != this->width) || (height != this->height))
{
destroy_sized_objects();
create_sized_objects(width, height);
update_descriptors();
}
ApiVulkanSample::resize(width, height);
return true;
}
void OITLinkedLists::render(float delta_time)
{
if (!prepared)
{
return;
}
ApiVulkanSample::prepare_frame();
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &draw_cmd_buffers[current_buffer];
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE));
ApiVulkanSample::submit_frame();
if (camera_auto_rotation)
{
camera.rotate({delta_time * 5.0f, delta_time * 5.0f, 0.0f});
}
update_scene_constants();
}
void OITLinkedLists::request_gpu_features(vkb::PhysicalDevice &gpu)
{
if (gpu.get_features().fragmentStoresAndAtomics)
{
gpu.get_mutable_requested_features().fragmentStoresAndAtomics = VK_TRUE;
}
else
{
throw std::runtime_error("This sample requires support for buffers and images stores and atomic operations in the fragment shader stage");
}
}
void OITLinkedLists::on_update_ui_overlay(vkb::Drawer &drawer)
{
drawer.checkbox("Sort fragments", &sort_fragments);
drawer.checkbox("Camera auto-rotation", &camera_auto_rotation);
drawer.slider_int("Sorted fragments per pixel", &sorted_fragment_count, kSortedFragmentMinCount, kSortedFragmentMaxCount);
drawer.slider_float("Background grayscale", &background_grayscale, kBackgroundGrayscaleMin, kBackgroundGrayscaleMax);
}
void OITLinkedLists::build_command_buffers()
{
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.renderArea.offset.x = 0;
render_pass_begin_info.renderArea.offset.y = 0;
render_pass_begin_info.renderArea.extent.width = width;
render_pass_begin_info.renderArea.extent.height = height;
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
VK_CHECK(vkBeginCommandBuffer(draw_cmd_buffers[i], &command_buffer_begin_info));
{
// Gather pass
{
render_pass_begin_info.framebuffer = gather_framebuffer;
render_pass_begin_info.renderPass = gather_render_pass;
render_pass_begin_info.clearValueCount = 0;
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
{
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(width, height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set, 0, NULL);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, gather_pipeline);
draw_model(object, draw_cmd_buffers[i], kInstanceCount);
}
vkCmdEndRenderPass(draw_cmd_buffers[i]);
}
VkImageSubresourceRange subresource_range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkb::image_layout_transition(
draw_cmd_buffers[i], linked_list_head_image->get_handle(),
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL,
subresource_range);
// Combine pass
{
VkClearValue clear_values[2];
clear_values[0].color = {{0.0f, 0.0f, 0.0f, 0.0f}};
clear_values[1].depthStencil = {0.0f, 0};
render_pass_begin_info.framebuffer = framebuffers[i];
render_pass_begin_info.renderPass = render_pass;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.pClearValues = clear_values;
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
{
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(width, height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set, 0, NULL);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, background_pipeline);
vkCmdDraw(draw_cmd_buffers[i], 3, 1, 0, 0);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, combine_pipeline);
vkCmdDraw(draw_cmd_buffers[i], 3, 1, 0, 0);
draw_ui(draw_cmd_buffers[i]);
}
vkCmdEndRenderPass(draw_cmd_buffers[i]);
}
}
VK_CHECK(vkEndCommandBuffer(draw_cmd_buffers[i]));
}
}
////////////////////////////////////////////////////////////////////////////////
void OITLinkedLists::create_sized_objects(const uint32_t width, const uint32_t height)
{
create_gather_pass_objects(width, height);
create_fragment_resources(width, height);
clear_sized_resources();
}
void OITLinkedLists::destroy_sized_objects()
{
vkDestroyFramebuffer(get_device().get_handle(), gather_framebuffer, nullptr);
vkDestroyRenderPass(get_device().get_handle(), gather_render_pass, nullptr);
fragment_counter.reset();
fragment_buffer.reset();
fragment_max_count = 0;
linked_list_head_image_view.reset();
linked_list_head_image.reset();
}
void OITLinkedLists::create_gather_pass_objects(const uint32_t width, const uint32_t height)
{
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
VkRenderPassCreateInfo render_pass_create_info = {};
render_pass_create_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
render_pass_create_info.subpassCount = 1;
render_pass_create_info.pSubpasses = &subpass;
VK_CHECK(vkCreateRenderPass(get_device().get_handle(), &render_pass_create_info, nullptr, &gather_render_pass));
VkFramebufferCreateInfo framebuffer_create_info = vkb::initializers::framebuffer_create_info();
framebuffer_create_info.renderPass = gather_render_pass;
framebuffer_create_info.width = width;
framebuffer_create_info.height = height;
framebuffer_create_info.layers = 1;
VK_CHECK(vkCreateFramebuffer(get_device().get_handle(), &framebuffer_create_info, nullptr, &gather_framebuffer));
}
void OITLinkedLists::create_fragment_resources(const uint32_t width, const uint32_t height)
{
{
const VkExtent3D image_extent = {width, height, 1};
linked_list_head_image = std::make_unique<vkb::core::Image>(get_device(), image_extent, VK_FORMAT_R32_UINT, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_GPU_ONLY, VK_SAMPLE_COUNT_1_BIT);
linked_list_head_image_view = std::make_unique<vkb::core::ImageView>(*linked_list_head_image, VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_R32_UINT);
}
{
fragment_max_count = width * height * kFragmentsPerPixelAverage;
const uint32_t fragment_buffer_size = sizeof(glm::uvec3) * fragment_max_count;
fragment_buffer = std::make_unique<vkb::core::BufferC>(get_device(), fragment_buffer_size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VMA_MEMORY_USAGE_GPU_ONLY);
}
{
fragment_counter = std::make_unique<vkb::core::BufferC>(get_device(), sizeof(glm::uint), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_GPU_ONLY);
}
}
void OITLinkedLists::clear_sized_resources()
{
VkCommandBuffer command_buffer;
VkCommandBufferAllocateInfo command_buffer_allocate_info = vkb::initializers::command_buffer_allocate_info(cmd_pool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1);
VK_CHECK(vkAllocateCommandBuffers(get_device().get_handle(), &command_buffer_allocate_info, &command_buffer));
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VK_CHECK(vkBeginCommandBuffer(command_buffer, &command_buffer_begin_info));
{
vkCmdFillBuffer(command_buffer, fragment_counter->get_handle(), 0, sizeof(glm::uint), 0);
VkImageSubresourceRange subresource_range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkb::image_layout_transition(
command_buffer, linked_list_head_image->get_handle(),
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL,
subresource_range);
VkClearColorValue linked_lists_clear_value;
linked_lists_clear_value.uint32[0] = kLinkedListEndSentinel;
linked_lists_clear_value.uint32[1] = kLinkedListEndSentinel;
linked_lists_clear_value.uint32[2] = kLinkedListEndSentinel;
linked_lists_clear_value.uint32[3] = kLinkedListEndSentinel;
vkCmdClearColorImage(command_buffer, linked_list_head_image->get_handle(), VK_IMAGE_LAYOUT_GENERAL, &linked_lists_clear_value, 1, &subresource_range);
}
VK_CHECK(vkEndCommandBuffer(command_buffer));
{
VkSubmitInfo submit_info = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &command_buffer;
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE));
VK_CHECK(vkQueueWaitIdle(queue));
}
vkFreeCommandBuffers(get_device().get_handle(), cmd_pool, 1, &command_buffer);
}
////////////////////////////////////////////////////////////////////////////////
void OITLinkedLists::load_assets()
{
object = load_model("scenes/geosphere.gltf");
background_texture = load_texture("textures/vulkan_logo_full.ktx", vkb::sg::Image::Color);
}
void OITLinkedLists::create_constant_buffers()
{
scene_constants = std::make_unique<vkb::core::BufferC>(get_device(), sizeof(SceneConstants), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
instance_data = std::make_unique<vkb::core::BufferC>(get_device(), sizeof(Instance) * kInstanceCount, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
}
void OITLinkedLists::create_descriptors()
{
{
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings = {
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 1),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_SHADER_STAGE_FRAGMENT_BIT, 2),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT, 3),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT, 4),
vkb::initializers::descriptor_set_layout_binding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 5),
};
VkDescriptorSetLayoutCreateInfo descriptor_layout_create_info = vkb::initializers::descriptor_set_layout_create_info(set_layout_bindings.data(), static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout_create_info, nullptr, &descriptor_set_layout));
}
{
std::vector<VkDescriptorPoolSize> pool_sizes = {
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1),
};
const uint32_t num_descriptor_sets = 1;
VkDescriptorPoolCreateInfo descriptor_pool_create_info = vkb::initializers::descriptor_pool_create_info(static_cast<uint32_t>(pool_sizes.size()), pool_sizes.data(), num_descriptor_sets);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &descriptor_pool_create_info, nullptr, &descriptor_pool));
}
VkDescriptorSetAllocateInfo alloc_info = vkb::initializers::descriptor_set_allocate_info(descriptor_pool, &descriptor_set_layout, 1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &descriptor_set));
}
void OITLinkedLists::update_descriptors()
{
VkDescriptorBufferInfo scene_constants_descriptor = create_descriptor(*scene_constants);
VkDescriptorBufferInfo instance_data_descriptor = create_descriptor(*instance_data);
VkDescriptorImageInfo linked_list_head_image_view_descriptor = vkb::initializers::descriptor_image_info(VK_NULL_HANDLE, linked_list_head_image_view->get_handle(), VK_IMAGE_LAYOUT_GENERAL);
VkDescriptorBufferInfo fragment_buffer_descriptor = create_descriptor(*fragment_buffer);
VkDescriptorBufferInfo fragment_counter_descriptor = create_descriptor(*fragment_counter);
VkDescriptorImageInfo background_texture_descriptor = create_descriptor(background_texture);
std::vector<VkWriteDescriptorSet> write_descriptor_sets = {
vkb::initializers::write_descriptor_set(descriptor_set, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &scene_constants_descriptor),
vkb::initializers::write_descriptor_set(descriptor_set, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, &instance_data_descriptor),
vkb::initializers::write_descriptor_set(descriptor_set, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 2, &linked_list_head_image_view_descriptor),
vkb::initializers::write_descriptor_set(descriptor_set, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3, &fragment_buffer_descriptor),
vkb::initializers::write_descriptor_set(descriptor_set, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4, &fragment_counter_descriptor),
vkb::initializers::write_descriptor_set(descriptor_set, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 5, &background_texture_descriptor),
};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
}
void OITLinkedLists::create_pipelines()
{
{
VkPipelineLayoutCreateInfo pipeline_layout_create_info = vkb::initializers::pipeline_layout_create_info(&descriptor_set_layout, 1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &pipeline_layout));
}
{
VkPipelineVertexInputStateCreateInfo vertex_input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
VkPipelineInputAssemblyStateCreateInfo input_assembly_state = vkb::initializers::pipeline_input_assembly_state_create_info(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterization_state = vkb::initializers::pipeline_rasterization_state_create_info(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, 0);
VkPipelineColorBlendAttachmentState blend_attachment_state = vkb::initializers::pipeline_color_blend_attachment_state(0xF, VK_FALSE);
VkPipelineColorBlendStateCreateInfo color_blend_state = vkb::initializers::pipeline_color_blend_state_create_info(1, &blend_attachment_state);
VkPipelineMultisampleStateCreateInfo multisample_state = vkb::initializers::pipeline_multisample_state_create_info(VK_SAMPLE_COUNT_1_BIT, 0);
VkPipelineViewportStateCreateInfo viewport_state = vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0);
VkPipelineDepthStencilStateCreateInfo depth_stencil_state = vkb::initializers::pipeline_depth_stencil_state_create_info(VK_FALSE, VK_FALSE, VK_COMPARE_OP_GREATER);
std::vector<VkDynamicState> dynamic_state_enables = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamicState = vkb::initializers::pipeline_dynamic_state_create_info(dynamic_state_enables.data(), static_cast<uint32_t>(dynamic_state_enables.size()), 0);
std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages{};
VkGraphicsPipelineCreateInfo pipeline_create_info = vkb::initializers::pipeline_create_info(pipeline_layout, render_pass, 0);
pipeline_create_info.pVertexInputState = &vertex_input_state;
pipeline_create_info.pInputAssemblyState = &input_assembly_state;
pipeline_create_info.pRasterizationState = &rasterization_state;
pipeline_create_info.pColorBlendState = &color_blend_state;
pipeline_create_info.pMultisampleState = &multisample_state;
pipeline_create_info.pViewportState = &viewport_state;
pipeline_create_info.pDepthStencilState = &depth_stencil_state;
pipeline_create_info.pDynamicState = &dynamicState;
pipeline_create_info.stageCount = static_cast<uint32_t>(shader_stages.size());
pipeline_create_info.pStages = shader_stages.data();
{
const std::vector<VkVertexInputBindingDescription> vertex_input_bindings = {
vkb::initializers::vertex_input_binding_description(0, sizeof(Vertex), VK_VERTEX_INPUT_RATE_VERTEX),
};
const std::vector<VkVertexInputAttributeDescription> vertex_input_attributes = {
vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, pos)),
};
vertex_input_state.vertexBindingDescriptionCount = static_cast<uint32_t>(vertex_input_bindings.size());
vertex_input_state.pVertexBindingDescriptions = vertex_input_bindings.data();
vertex_input_state.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size());
vertex_input_state.pVertexAttributeDescriptions = vertex_input_attributes.data();
shader_stages[0] = load_shader("oit_linked_lists/gather.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("oit_linked_lists/gather.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
pipeline_create_info.renderPass = gather_render_pass;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &gather_pipeline));
}
{
vertex_input_state.vertexBindingDescriptionCount = 0;
vertex_input_state.pVertexBindingDescriptions = nullptr;
vertex_input_state.vertexAttributeDescriptionCount = 0;
vertex_input_state.pVertexAttributeDescriptions = nullptr;
shader_stages[0] = load_shader("oit_linked_lists/fullscreen.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("oit_linked_lists/background.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
pipeline_create_info.renderPass = render_pass;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &background_pipeline));
}
{
vertex_input_state.vertexBindingDescriptionCount = 0;
vertex_input_state.pVertexBindingDescriptions = nullptr;
vertex_input_state.vertexAttributeDescriptionCount = 0;
vertex_input_state.pVertexAttributeDescriptions = nullptr;
blend_attachment_state.blendEnable = VK_TRUE;
blend_attachment_state.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
blend_attachment_state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment_state.colorBlendOp = VK_BLEND_OP_ADD;
blend_attachment_state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
blend_attachment_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment_state.alphaBlendOp = VK_BLEND_OP_ADD;
shader_stages[0] = load_shader("oit_linked_lists/combine.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("oit_linked_lists/combine.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
pipeline_create_info.renderPass = render_pass;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &combine_pipeline));
}
}
}
////////////////////////////////////////////////////////////////////////////////
void OITLinkedLists::update_scene_constants()
{
SceneConstants constants = {};
constants.projection = camera.matrices.perspective;
constants.view = camera.matrices.view;
constants.background_grayscale = background_grayscale;
constants.sort_fragments = sort_fragments ? 1U : 0U;
constants.fragment_max_count = fragment_max_count;
constants.sorted_fragment_count = sorted_fragment_count;
scene_constants->convert_and_update(constants);
}
void OITLinkedLists::fill_instance_data()
{
Instance instances[kInstanceCount] = {};
auto get_random_float = []() {
return static_cast<float>(rand()) / (RAND_MAX);
};
for (uint32_t l = 0, instance_index = 0; l < kInstanceLayerCount; ++l)
{
for (uint32_t c = 0; c < kInstanceColumnCount; ++c)
{
for (uint32_t r = 0; r < kInstanceRowCount; ++r, ++instance_index)
{
const float x = static_cast<float>(r) - ((kInstanceRowCount - 1) * 0.5f);
const float y = static_cast<float>(c) - ((kInstanceColumnCount - 1) * 0.5f);
const float z = static_cast<float>(l) - ((kInstanceLayerCount - 1) * 0.5f);
const float scale = 0.02f;
instances[instance_index].model =
glm::scale(
glm::translate(glm::mat4(1.0f), glm::vec3(x, y, z)),
glm::vec3(scale));
instances[instance_index].color.r = get_random_float();
instances[instance_index].color.g = get_random_float();
instances[instance_index].color.b = get_random_float();
instances[instance_index].color.a = get_random_float() * 0.8f + 0.2f;
}
}
}
instance_data->convert_and_update(instances);
}
////////////////////////////////////////////////////////////////////////////////
std::unique_ptr<vkb::VulkanSampleC> create_oit_linked_lists()
{
return std::make_unique<OITLinkedLists>();
}
@@ -1,116 +0,0 @@
/* Copyright (c) 2023-2024, Google
*
* 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 "api_vulkan_sample.h"
#include "rendering/render_pipeline.h"
#include "scene_graph/components/camera.h"
class OITLinkedLists : public ApiVulkanSample
{
public:
OITLinkedLists();
~OITLinkedLists();
bool prepare(const vkb::ApplicationOptions &options) override;
bool resize(const uint32_t width, const uint32_t height) override;
void render(float delta_time) override;
void build_command_buffers() override;
void request_gpu_features(vkb::PhysicalDevice &gpu) override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
private:
void create_sized_objects(const uint32_t width, const uint32_t height);
void destroy_sized_objects();
void create_gather_pass_objects(const uint32_t width, const uint32_t height);
void create_fragment_resources(const uint32_t width, const uint32_t height);
void clear_sized_resources();
void load_assets();
void create_constant_buffers();
void create_descriptors();
void create_pipelines();
void update_descriptors();
void update_scene_constants();
void fill_instance_data();
private:
static constexpr uint32_t kInstanceRowCount = 4;
static constexpr uint32_t kInstanceColumnCount = 4;
static constexpr uint32_t kInstanceLayerCount = 4;
static constexpr uint32_t kInstanceCount = kInstanceRowCount * kInstanceColumnCount * kInstanceLayerCount;
static constexpr uint32_t kFragmentsPerPixelAverage = 8;
static constexpr int32_t kSortedFragmentMinCount = 1;
static constexpr int32_t kSortedFragmentMaxCount = 16;
static constexpr float kBackgroundGrayscaleMin = 0.0f;
static constexpr float kBackgroundGrayscaleMax = 1.0f;
static constexpr uint32_t kLinkedListEndSentinel = 0xFFFFFFFFU;
struct SceneConstants
{
glm::mat4 projection;
glm::mat4 view;
glm::f32 background_grayscale;
glm::uint sort_fragments;
glm::uint fragment_max_count;
glm::uint sorted_fragment_count;
};
struct Instance
{
glm::mat4 model;
glm::vec4 color;
};
private:
std::unique_ptr<vkb::sg::SubMesh> object;
Texture background_texture;
std::unique_ptr<vkb::core::BufferC> scene_constants;
std::unique_ptr<vkb::core::BufferC> instance_data;
std::unique_ptr<vkb::core::Image> linked_list_head_image;
std::unique_ptr<vkb::core::ImageView> linked_list_head_image_view;
std::unique_ptr<vkb::core::BufferC> fragment_buffer;
std::unique_ptr<vkb::core::BufferC> fragment_counter;
glm::uint fragment_max_count = 0U;
VkRenderPass gather_render_pass = VK_NULL_HANDLE;
VkFramebuffer gather_framebuffer = VK_NULL_HANDLE;
VkDescriptorSetLayout descriptor_set_layout = VK_NULL_HANDLE;
VkDescriptorPool descriptor_pool = VK_NULL_HANDLE;
VkDescriptorSet descriptor_set = VK_NULL_HANDLE;
VkPipelineLayout pipeline_layout = VK_NULL_HANDLE;
VkPipeline gather_pipeline = VK_NULL_HANDLE;
VkPipeline background_pipeline = VK_NULL_HANDLE;
VkPipeline combine_pipeline = VK_NULL_HANDLE;
int32_t sort_fragments = true;
int32_t camera_auto_rotation = false;
int32_t sorted_fragment_count = kSortedFragmentMaxCount;
float background_grayscale = 0.3f;
};
std::unique_ptr<vkb::VulkanSampleC> create_oit_linked_lists();
@@ -1,33 +0,0 @@
# Copyright (c) 2021-2024, Sascha Willems
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 the "License";
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
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 "Sascha Willems"
NAME "Separating image and sampler"
DESCRIPTION "Displays a texture with a separated image and sampler"
SHADER_FILES_GLSL
"separate_image_sampler/glsl/separate_image_sampler.vert"
"separate_image_sampler/glsl/separate_image_sampler.frag"
SHADER_FILES_HLSL
"separate_image_sampler/hlsl/separate_image_sampler.vert.hlsl"
"separate_image_sampler/hlsl/separate_image_sampler.frag.hlsl")
@@ -1,159 +0,0 @@
////
- Copyright (c) 2021-2023, Sascha Willems
-
- SPDX-License-Identifier: Apache-2.0
-
- Licensed under the Apache License, Version 2.0 the "License";
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
////
= Separating samplers and images
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/separate_image_sampler[Khronos Vulkan samples github repository].
endif::[]
This tutorial, along with the accompanying example code, shows how to separate samplers and images in a Vulkan application.
Opposite to combined image and samplers, this allows the application to freely mix an arbitrary set of samplers and images in the shader.
In the sample code, a single image and multiple samplers with different options will be created.
The sampler to be used for sampling the image can then be selected at runtime.
As image and sampler objects are separated, this only requires selecting a different descriptor at runtime.
== In the application
From the application's point of view, images and samplers are always created separately.
Access to the image is done via the image's `VkImageView`.
Samplers are created using a `VkSampler` object, specifying how an image will be sampled.
The difference between separating and combining them starts at the descriptor level, which defines how the shader accesses the samplers and images.
A separate setup uses a descriptor of type `VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE` for the sampled image, and a `VK_DESCRIPTOR_TYPE_SAMPLER` for the sampler, separating the image and sampler object:
[,cpp]
----
// Image info only references the image
VkDescriptorImageInfo image_info{};
image_info.imageView = texture.image->get_vk_image_view().get_handle();
image_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// Sampled image descriptor
VkWriteDescriptorSet image_write_descriptor_set{};
image_write_descriptor_set.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
image_write_descriptor_set.dstSet = base_descriptor_set;
image_write_descriptor_set.dstBinding = 1;
image_write_descriptor_set.descriptorCount = 1;
image_write_descriptor_set.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
image_write_descriptor_set.pImageInfo = &image_info;
// One set for the sampled image
std::vector<VkWriteDescriptorSet> write_descriptor_sets = {
...
// Binding 1 : Fragment shader sampled image
image_write_descriptor_set};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, nullptr);
----
For this sample, we then create two samplers with different filtering options:
[,cpp]
----
// Sets for each of the sampler
descriptor_set_alloc_info.pSetLayouts = &sampler_descriptor_set_layout;
for (size_t i = 0; i < sampler_descriptor_sets.size(); i++)
{
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &descriptor_set_alloc_info, &sampler_descriptor_sets[i]));
// Descriptor info only references the sampler
VkDescriptorImageInfo sampler_info{};
sampler_info.sampler = samplers[i];
VkWriteDescriptorSet sampler_write_descriptor_set{};
sampler_write_descriptor_set.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
sampler_write_descriptor_set.dstSet = sampler_descriptor_sets[i];
sampler_write_descriptor_set.dstBinding = 0;
sampler_write_descriptor_set.descriptorCount = 1;
sampler_write_descriptor_set.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER;
sampler_write_descriptor_set.pImageInfo = &sampler_info;
vkUpdateDescriptorSets(get_device().get_handle(), 1, &sampler_write_descriptor_set, 0, nullptr);
}
----
At draw-time, the descriptor containing the sampled image is bound to set 0 and the descriptor for the currently selected sampler is bound to set 1:
[,cpp]
----
// Base descriptor with the image to be sampled in set 0
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &base_descriptor_set, 0, nullptr);
// Descriptor for the selected sampler in set 1
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 1, 1, &sampler_descriptor_sets[selected_sampler], 0, nullptr);
...
vkCmdDrawIndexed(draw_cmd_buffers[i], index_count, 1, 0, 0, 0);
----
== In the shader
With the above setup, the shader interface for the fragment shader also separates the sampler and image as two distinct uniforms:
[,glsl]
----
layout (set = 0, binding = 1) uniform texture2D _texture;
layout (set = 1, binding = 0) uniform sampler _sampler;
----
To sample from the image referenced by `_texture`, with the currently set sampler in '_sampler', we create a sampled image in the fragment shader at runtime using the `sampler2D` function.
[,glsl]
----
void main()
{
vec4 color = texture(sampler2D(_texture, _sampler), inUV);
}
----
== Comparison with combined image samplers
For reference, a combined image and sampler setup would differ for both the application and the shader.
The app would use a single descriptor of type `VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER`, and set both image and sampler related values in the descriptor:
[,cpp]
----
// Descriptor info references image and sampler
VkDescriptorImageInfo image_info;
image_info.imageView = texture.view;
image_info.sampler = texture.sampler;
image_info.imageLayout = texture.image_layout;
VkWriteDescriptorSet image_write_descriptor_set{};
image_write_descriptor_set.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
image_write_descriptor_set.dstSet = descriptor_set;
image_write_descriptor_set.dstBinding = 0;
image_write_descriptor_set.descriptorCount = 1;
image_write_descriptor_set.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
image_write_descriptor_set.pImageInfo = &image_info;
----
The shader interface only uses one uniform for accessing the combined image and sampler and also doesn't construct a `sampler2D` at runtime:
[,glsl]
----
layout (binding = 1) uniform sampler2D _combined_image;
void main()
{
vec4 color = texture(_combined_image, inUV);
}
----
Compared to the separated setup, changing a sampler in this setup would either require creating multiple descriptors with each image/sampler combination or rebuilding the descriptor.
@@ -1,495 +0,0 @@
/* Copyright (c) 2021-2025, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Separate samplers and image to draw a single image with different sampling options
*/
#include "separate_image_sampler.h"
SeparateImageSampler::SeparateImageSampler()
{
zoom = -0.5f;
rotation = {45.0f, 0.0f, 0.0f};
title = "Separate sampler and image";
}
SeparateImageSampler::~SeparateImageSampler()
{
if (has_device())
{
// Clean up used Vulkan resources
// Note : Inherited destructor cleans up resources stored in base class
vkDestroyPipeline(get_device().get_handle(), pipeline, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layout, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), base_descriptor_set_layout, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), sampler_descriptor_set_layout, nullptr);
for (VkSampler sampler : samplers)
{
vkDestroySampler(get_device().get_handle(), sampler, nullptr);
}
// Delete the implicitly created sampler for the texture loaded via the framework
vkDestroySampler(get_device().get_handle(), texture.sampler, nullptr);
}
}
// Enable physical device features required for this example
void SeparateImageSampler::request_gpu_features(vkb::PhysicalDevice &gpu)
{
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = VK_TRUE;
}
}
void SeparateImageSampler::build_command_buffers()
{
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VkClearValue clear_values[2];
clear_values[0].color = default_clear_color;
clear_values[1].depthStencil = {0.0f, 0};
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.renderPass = render_pass;
render_pass_begin_info.renderArea.offset.x = 0;
render_pass_begin_info.renderArea.offset.y = 0;
render_pass_begin_info.renderArea.extent.width = width;
render_pass_begin_info.renderArea.extent.height = height;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.pClearValues = clear_values;
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
// Set target frame buffer
render_pass_begin_info.framebuffer = framebuffers[i];
VK_CHECK(vkBeginCommandBuffer(draw_cmd_buffers[i], &command_buffer_begin_info));
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(static_cast<int32_t>(width), static_cast<int32_t>(height), 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
// Bind the uniform buffer and sampled image to set 0
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &base_descriptor_set, 0, nullptr);
// Bind the selected sampler to set 1
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 1, 1, &sampler_descriptor_sets[selected_sampler], 0, nullptr);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
VkDeviceSize offsets[1] = {0};
vkCmdBindVertexBuffers(draw_cmd_buffers[i], 0, 1, vertex_buffer->get(), offsets);
vkCmdBindIndexBuffer(draw_cmd_buffers[i], index_buffer->get_handle(), 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(draw_cmd_buffers[i], index_count, 1, 0, 0, 0);
draw_ui(draw_cmd_buffers[i]);
vkCmdEndRenderPass(draw_cmd_buffers[i]);
VK_CHECK(vkEndCommandBuffer(draw_cmd_buffers[i]));
}
}
void SeparateImageSampler::setup_samplers()
{
// Create two samplers with different options
VkSamplerCreateInfo samplerCI = vkb::initializers::sampler_create_info();
samplerCI.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
samplerCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerCI.mipLodBias = 0.0f;
samplerCI.compareOp = VK_COMPARE_OP_NEVER;
samplerCI.minLod = 0.0f;
samplerCI.maxLod = static_cast<float>(texture.image->get_mipmaps().size());
if (get_device().get_gpu().get_features().samplerAnisotropy)
{
// Use max. level of anisotropy for this example
samplerCI.maxAnisotropy = get_device().get_gpu().get_properties().limits.maxSamplerAnisotropy;
samplerCI.anisotropyEnable = VK_TRUE;
}
else
{
// The device does not support anisotropic filtering
samplerCI.maxAnisotropy = 1.0;
samplerCI.anisotropyEnable = VK_FALSE;
}
samplerCI.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
// First sampler with linear filtering
samplerCI.magFilter = VK_FILTER_LINEAR;
samplerCI.minFilter = VK_FILTER_LINEAR;
VK_CHECK(vkCreateSampler(get_device().get_handle(), &samplerCI, nullptr, &samplers[0]));
// Second sampler with nearest filtering
samplerCI.magFilter = VK_FILTER_NEAREST;
samplerCI.minFilter = VK_FILTER_NEAREST;
VK_CHECK(vkCreateSampler(get_device().get_handle(), &samplerCI, nullptr, &samplers[1]));
}
void SeparateImageSampler::load_assets()
{
texture = load_texture("textures/metalplate01_rgba.ktx", vkb::sg::Image::Color);
}
void SeparateImageSampler::draw()
{
ApiVulkanSample::prepare_frame();
// Command buffer to be submitted to the queue
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &draw_cmd_buffers[current_buffer];
// Submit to queue
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE));
ApiVulkanSample::submit_frame();
}
void SeparateImageSampler::generate_quad()
{
// Setup vertices for a single uv-mapped quad made from two triangles
std::vector<VertexStructure> vertices =
{
{{1.0f, 1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, 1.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
{{-1.0f, -1.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}},
{{1.0f, -1.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}}};
// Setup indices
std::vector<uint32_t> indices = {0, 1, 2, 2, 3, 0};
index_count = static_cast<uint32_t>(indices.size());
auto vertex_buffer_size = vkb::to_u32(vertices.size() * sizeof(VertexStructure));
auto index_buffer_size = vkb::to_u32(indices.size() * sizeof(uint32_t));
// Create buffers
// For the sake of simplicity we won't stage the vertex data to the gpu memory
// Vertex buffer
vertex_buffer = std::make_unique<vkb::core::BufferC>(get_device(),
vertex_buffer_size,
VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
vertex_buffer->update(vertices.data(), vertex_buffer_size);
index_buffer = std::make_unique<vkb::core::BufferC>(get_device(),
index_buffer_size,
VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
index_buffer->update(indices.data(), index_buffer_size);
}
void SeparateImageSampler::setup_descriptor_pool()
{
std::vector<VkDescriptorPoolSize> pool_sizes = {
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_SAMPLER, 2)};
VkDescriptorPoolCreateInfo descriptor_pool_create_info =
vkb::initializers::descriptor_pool_create_info(
static_cast<uint32_t>(pool_sizes.size()),
pool_sizes.data(),
3);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &descriptor_pool_create_info, nullptr, &descriptor_pool));
}
void SeparateImageSampler::setup_descriptor_set_layout()
{
// We separate the descriptor sets for the uniform buffer + image and samplers, so we don't need to duplicate the descriptors for the former
VkDescriptorSetLayoutCreateInfo descriptor_layout_create_info{};
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings{};
// Set layout for the uniform buffer and the image
set_layout_bindings = {
// Binding 0 : Vertex shader uniform buffer
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
VK_SHADER_STAGE_VERTEX_BIT,
0),
// Binding 1 : Fragment shader sampled image
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
VK_SHADER_STAGE_FRAGMENT_BIT,
1)};
descriptor_layout_create_info =
vkb::initializers::descriptor_set_layout_create_info(
set_layout_bindings.data(),
static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout_create_info, nullptr, &base_descriptor_set_layout));
// Set layout for the samplers
set_layout_bindings = {
// Binding 0: Fragment shader sampler
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_SAMPLER,
VK_SHADER_STAGE_FRAGMENT_BIT,
0)};
descriptor_layout_create_info =
vkb::initializers::descriptor_set_layout_create_info(
set_layout_bindings.data(),
static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout_create_info, nullptr, &sampler_descriptor_set_layout));
// Pipeline layout
// Set layout for the base descriptors in set 0 and set layout for the sampler descriptors in set 1
std::vector<VkDescriptorSetLayout> set_layouts = {base_descriptor_set_layout, sampler_descriptor_set_layout};
VkPipelineLayoutCreateInfo pipeline_layout_create_info =
vkb::initializers::pipeline_layout_create_info(
set_layouts.data(),
static_cast<uint32_t>(set_layouts.size()));
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &pipeline_layout));
}
void SeparateImageSampler::setup_descriptor_set()
{
// We separate the descriptor sets for the uniform buffer + image and samplers, so we don't need to duplicate the descriptors for the former
VkDescriptorSetAllocateInfo descriptor_set_alloc_info{};
// Descriptors set for the uniform buffer and the image
descriptor_set_alloc_info =
vkb::initializers::descriptor_set_allocate_info(
descriptor_pool,
&base_descriptor_set_layout,
1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &descriptor_set_alloc_info, &base_descriptor_set));
VkDescriptorBufferInfo buffer_descriptor = create_descriptor(*uniform_buffer_vs);
// Image info only references the image
VkDescriptorImageInfo image_info{};
image_info.imageView = texture.image->get_vk_image_view().get_handle();
image_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// Sampled image descriptor
VkWriteDescriptorSet image_write_descriptor_set{};
image_write_descriptor_set.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
image_write_descriptor_set.dstSet = base_descriptor_set;
image_write_descriptor_set.dstBinding = 1;
image_write_descriptor_set.descriptorCount = 1;
image_write_descriptor_set.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
image_write_descriptor_set.pImageInfo = &image_info;
std::vector<VkWriteDescriptorSet> write_descriptor_sets = {
// Binding 0 : Vertex shader uniform buffer
vkb::initializers::write_descriptor_set(
base_descriptor_set,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
0,
&buffer_descriptor),
// Binding 1 : Fragment shader sampled image
image_write_descriptor_set};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, nullptr);
// Sets for each of the sampler
descriptor_set_alloc_info.pSetLayouts = &sampler_descriptor_set_layout;
for (size_t i = 0; i < sampler_descriptor_sets.size(); i++)
{
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &descriptor_set_alloc_info, &sampler_descriptor_sets[i]));
// Descriptor info only references the sampler
VkDescriptorImageInfo sampler_info{};
sampler_info.sampler = samplers[i];
VkWriteDescriptorSet sampler_write_descriptor_set{};
sampler_write_descriptor_set.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
sampler_write_descriptor_set.dstSet = sampler_descriptor_sets[i];
sampler_write_descriptor_set.dstBinding = 0;
sampler_write_descriptor_set.descriptorCount = 1;
sampler_write_descriptor_set.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER;
sampler_write_descriptor_set.pImageInfo = &sampler_info;
vkUpdateDescriptorSets(get_device().get_handle(), 1, &sampler_write_descriptor_set, 0, nullptr);
}
}
void SeparateImageSampler::prepare_pipelines()
{
VkPipelineInputAssemblyStateCreateInfo input_assembly_state =
vkb::initializers::pipeline_input_assembly_state_create_info(
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
0,
VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterization_state =
vkb::initializers::pipeline_rasterization_state_create_info(
VK_POLYGON_MODE_FILL,
VK_CULL_MODE_NONE,
VK_FRONT_FACE_COUNTER_CLOCKWISE,
0);
VkPipelineColorBlendAttachmentState blend_attachment_state =
vkb::initializers::pipeline_color_blend_attachment_state(
0xf,
VK_FALSE);
VkPipelineColorBlendStateCreateInfo color_blend_state =
vkb::initializers::pipeline_color_blend_state_create_info(
1,
&blend_attachment_state);
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
VkPipelineDepthStencilStateCreateInfo depth_stencil_state =
vkb::initializers::pipeline_depth_stencil_state_create_info(
VK_TRUE,
VK_TRUE,
VK_COMPARE_OP_GREATER);
VkPipelineViewportStateCreateInfo viewport_state =
vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0);
VkPipelineMultisampleStateCreateInfo multisample_state =
vkb::initializers::pipeline_multisample_state_create_info(
VK_SAMPLE_COUNT_1_BIT,
0);
std::vector<VkDynamicState> dynamic_state_enables = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic_state =
vkb::initializers::pipeline_dynamic_state_create_info(
dynamic_state_enables.data(),
static_cast<uint32_t>(dynamic_state_enables.size()),
0);
// Load shaders
std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages{};
shader_stages[0] = load_shader("separate_image_sampler", "separate_image_sampler.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("separate_image_sampler", "separate_image_sampler.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
// Vertex bindings and attributes
const std::vector<VkVertexInputBindingDescription> vertex_input_bindings = {
vkb::initializers::vertex_input_binding_description(0, sizeof(VertexStructure), VK_VERTEX_INPUT_RATE_VERTEX),
};
const std::vector<VkVertexInputAttributeDescription> vertex_input_attributes = {
vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(VertexStructure, pos)),
vkb::initializers::vertex_input_attribute_description(0, 1, VK_FORMAT_R32G32_SFLOAT, offsetof(VertexStructure, uv)),
vkb::initializers::vertex_input_attribute_description(0, 2, VK_FORMAT_R32G32B32_SFLOAT, offsetof(VertexStructure, normal)),
};
VkPipelineVertexInputStateCreateInfo vertex_input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
vertex_input_state.vertexBindingDescriptionCount = static_cast<uint32_t>(vertex_input_bindings.size());
vertex_input_state.pVertexBindingDescriptions = vertex_input_bindings.data();
vertex_input_state.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size());
vertex_input_state.pVertexAttributeDescriptions = vertex_input_attributes.data();
VkGraphicsPipelineCreateInfo pipeline_create_info =
vkb::initializers::pipeline_create_info(
pipeline_layout,
render_pass,
0);
pipeline_create_info.pVertexInputState = &vertex_input_state;
pipeline_create_info.pInputAssemblyState = &input_assembly_state;
pipeline_create_info.pRasterizationState = &rasterization_state;
pipeline_create_info.pColorBlendState = &color_blend_state;
pipeline_create_info.pMultisampleState = &multisample_state;
pipeline_create_info.pViewportState = &viewport_state;
pipeline_create_info.pDepthStencilState = &depth_stencil_state;
pipeline_create_info.pDynamicState = &dynamic_state;
pipeline_create_info.stageCount = static_cast<uint32_t>(shader_stages.size());
pipeline_create_info.pStages = shader_stages.data();
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipeline));
}
// Prepare and initialize uniform buffer containing shader uniforms
void SeparateImageSampler::prepare_uniform_buffers()
{
// Vertex shader uniform buffer block
uniform_buffer_vs = std::make_unique<vkb::core::BufferC>(get_device(),
sizeof(ubo_vs),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
}
void SeparateImageSampler::update_uniform_buffers()
{
// Vertex shader
ubo_vs.projection = glm::perspective(glm::radians(60.0f), static_cast<float>(width) / static_cast<float>(height), 0.001f, 256.0f);
glm::mat4 view_matrix = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, zoom));
ubo_vs.model = view_matrix * glm::translate(glm::mat4(1.0f), camera_pos);
ubo_vs.model = glm::rotate(ubo_vs.model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
ubo_vs.model = glm::rotate(ubo_vs.model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
ubo_vs.model = glm::rotate(ubo_vs.model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
ubo_vs.view_pos = glm::vec4(0.0f, 0.0f, -zoom, 0.0f);
uniform_buffer_vs->convert_and_update(ubo_vs);
}
bool SeparateImageSampler::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
load_assets();
generate_quad();
prepare_uniform_buffers();
setup_samplers();
setup_descriptor_set_layout();
prepare_pipelines();
setup_descriptor_pool();
setup_descriptor_set();
build_command_buffers();
prepared = true;
return true;
}
void SeparateImageSampler::render(float delta_time)
{
if (!prepared)
{
return;
}
draw();
}
void SeparateImageSampler::view_changed()
{
update_uniform_buffers();
}
void SeparateImageSampler::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Settings"))
{
const std::vector<std::string> sampler_names = {"Linear filtering",
"Nearest filtering"};
if (drawer.combo_box("Sampler", &selected_sampler, sampler_names))
{
update_uniform_buffers();
}
}
}
std::unique_ptr<vkb::Application> create_separate_image_sampler()
{
return std::make_unique<SeparateImageSampler>();
}
@@ -1,85 +0,0 @@
/* Copyright (c) 2021-2024, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Separate samplers and image to draw a single image with different sampling options
*/
#pragma once
#include <ktx.h>
#include "api_vulkan_sample.h"
class SeparateImageSampler : public ApiVulkanSample
{
public:
// Vertex layout for this example
struct VertexStructure
{
float pos[3];
float uv[2];
float normal[3];
};
Texture texture;
std::array<VkSampler, 2> samplers{};
int32_t selected_sampler = 0;
std::array<VkDescriptorSet, 2> sampler_descriptor_sets{};
std::unique_ptr<vkb::core::BufferC> vertex_buffer;
std::unique_ptr<vkb::core::BufferC> index_buffer;
uint32_t index_count;
std::unique_ptr<vkb::core::BufferC> uniform_buffer_vs;
struct
{
glm::mat4 projection;
glm::mat4 model;
glm::vec4 view_pos;
} ubo_vs;
VkPipeline pipeline = VK_NULL_HANDLE;
VkPipelineLayout pipeline_layout = VK_NULL_HANDLE;
VkDescriptorSet base_descriptor_set = VK_NULL_HANDLE;
VkDescriptorSetLayout base_descriptor_set_layout;
VkDescriptorSetLayout sampler_descriptor_set_layout;
SeparateImageSampler();
~SeparateImageSampler() override;
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
void build_command_buffers() override;
void setup_samplers();
void load_assets();
void draw();
void generate_quad();
void setup_descriptor_pool();
void setup_descriptor_set_layout();
void setup_descriptor_set();
void prepare_pipelines();
void prepare_uniform_buffers();
void update_uniform_buffers();
bool prepare(const vkb::ApplicationOptions &options) override;
virtual void render(float delta_time) override;
virtual void view_changed() override;
virtual void on_update_ui_overlay(vkb::Drawer &drawer) override;
};
std::unique_ptr<vkb::Application> create_separate_image_sampler();
@@ -1,27 +0,0 @@
# Copyright (c) 2023, Google
#
# 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 "Google"
NAME "Swapchain Recreation"
DESCRIPTION "Best practices when dealing with Vulkan swapchain recreation.")
@@ -1,92 +0,0 @@
////
- Copyright (c) 2023-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.
-
////
= Swapchain Recreation
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/swapchain_recreation[Khronos Vulkan samples github repository].
endif::[]
A sample that implements best practices in handling present resources and swapchain recreation, for example due to window resizing or present mode changes.
Before VK_EXT_swapchain_maintenance1, there is no straightforward way to tell when a semaphore associated with a present operation can be recycled, or when a retired swapchain can be destroyed.
Both these operations depend on knowing when the presentation engine has acquired a reference to these resources as part of the present job, for which there is no indicator.
In this sample, a workaround is implemented where a fence signaled by vkAcquireNextImageKHR is used to determine when the _previous_ present job involving the same image index has been completed.
This is often much later than the point where the present resources can be freed.
Take the following shorthand notation:
* PE: Presentation Engine
* ANI: vkAcquireNextImageKHR
* QS: vkQueueSubmit
* QP: vkQueuePresentKHR
* W: Wait
* S: Signal
* R: Render
* P: Present
* SN: Semaphore N
* IN: Swapchain image N
* FN: Fence N
Assuming both ANI calls below return the same index:
CPU: ANI ... QS ... QP ANI ... QS ... QP
S:S1 W:S1 W:S2 S:S3 W:S3 W:S4
S:F1 S:S2 S:F2 S:S4
GPU: <------ R ------> <------ R ------>
PE: <-- P --> <-- P -->
The following holds:
F2 is signaled
=> The PE has handed the image to the application
=> The PE is no longer presenting the image (the first P operation is finished)
=> The PE is done waiting on S2
At this point, we can destroy or recycle S2.
To implement this, a history of present operations is maintained, which includes the wait semaphore used with that presentation.
Associated with each present operation, is a fence that is used to determine when that semaphore can be destroyed.
Since the fence is not actually known at present time (QP), the present operation is kept in history without an associated fence.
Once ANI returns the same index, the fence given to ANI is associated with the previous QP of that index.
After each present call, the present history is inspected.
Any present operation whose fence is signaled is cleaned up.
== Swapchain recreation
When recreating the swapchain, all images are eventually freed and new ones are created, possibly with a different count and present mode.
For the old swapchain, we can no longer rely on a future ANI to know when a previous presentation's semaphore can be destroyed, as there won't be any more acquisitions from the old swapchain.
Similarly, we cannot know when the old swapchain itself can be destroyed.
This issue is resolved by deferring the destruction of the old swapchain and its remaining present semaphores to the time when the semaphore corresponding to the first present of the new swapchain can be destroyed.
Because once the first present semaphore of the new swapchain can be destroyed, the first present operation of the new swapchain is done, which means the old swapchain is no longer being presented.
Note that the swapchain may be recreated without a second acquire.
This means that the swapchain could be recreated while there are pending old swapchains to be destroyed.
The destruction of both old swapchains must now be deferred to when the first QP of the new swapchain has been processed.
If an application resizes the window constantly and at a high rate, we would keep accumulating old swapchains and not free them until it stops.
== VK_EXT_swapchain_maintenance1
With the VK_EXT_swapchain_maintenance1, all the above is unnecessary.
Each QP operation can have an associated fence, which can be used to know when the semaphore associated with it can be recycled.
The old swapchains can be destroyed at the same time as before.
File diff suppressed because it is too large Load Diff
@@ -1,216 +0,0 @@
/* Copyright (c) 2023-2025, Google
*
* 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/vk_common.h"
#include "platform/application.h"
#include "vulkan_sample.h"
/**
* @brief A sample that implements best practices in handling present resources and swapchain
* recreation, for example due to window resizing or present mode changes.
*/
class SwapchainRecreation : public vkb::VulkanSampleC
{
struct SwapchainObjects
{
std::vector<VkImage> images;
std::vector<VkImageView> views;
std::vector<VkFramebuffer> framebuffers;
};
/**
* @brief Per-frame data. This is not per swapchain image!
* A queue of this data structure is used to remember the history of submissions. To avoid
* the CPU getting too far ahead of the GPU, the sample paces itself by waiting for the
* submission before last to finish before recording commands for the new frame. This means
* that frame N+1 doesn't start recording until frame N-1 finishes executing on the GPU (and
* likely frame N starts). In a real application, this minimizes latency from input to
* screen.
*/
struct PerFrame
{
VkFence submit_fence = VK_NULL_HANDLE;
VkCommandPool command_pool = VK_NULL_HANDLE;
VkCommandBuffer command_buffer = VK_NULL_HANDLE;
VkSemaphore acquire_semaphore = VK_NULL_HANDLE;
VkSemaphore present_semaphore = VK_NULL_HANDLE;
// Garbage to clean up once the submit_fence is signaled, if any.
std::vector<SwapchainObjects> swapchain_garbage;
};
struct SwapchainCleanupData
{
/// The old swapchain to be destroyed.
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
/**
* @brief Any present semaphores that were pending recycle at the time the swapchain
* was recreated will be scheduled for recycling at the same time as the swapchain's
* destruction.
*/
std::vector<VkSemaphore> semaphores;
};
struct PresentOperationInfo
{
/**
* @brief Fence that tells when the present semaphore can be destroyed. Without
* VK_EXT_swapchain_maintenance1, the fence used with the vkAcquireNextImageKHR that
* returns the same image index in the future is used to know when the semaphore can
* be recycled.
*/
VkFence cleanup_fence = VK_NULL_HANDLE;
VkSemaphore present_semaphore = VK_NULL_HANDLE;
/**
* @brief Old swapchains are scheduled to be destroyed at the same time as the last
* wait semaphore used to present an image to the old swapchains can be recycled.
*/
std::vector<SwapchainCleanupData> old_swapchains;
/**
* @brief Used to associate an acquire fence with the previous present operation of
* the image. Only relevant when VK_EXT_swapchain_maintenance1 is not supported;
* otherwise a fence is always associated with the present operation.
*/
uint32_t image_index = std::numeric_limits<uint32_t>::max();
};
public:
SwapchainRecreation();
virtual ~SwapchainRecreation() override;
void create_render_context() override;
void prepare_render_context() override;
void update(float delta_time) override;
bool resize(uint32_t width, uint32_t height) override;
void input_event(const vkb::InputEvent &input_event) override;
private:
/// Submission and present queue.
const vkb::Queue *queue = nullptr;
/// Allow enabling VK_EXT_surface_maintenance1 and VK_EXT_swapchain_maintenance1.
///
/// Can be set to false by setting environment variable `USE_MAINTENANCE1=no`
bool allow_maintenance1 = true;
/// Whether the VK_EXT_surface_maintenance1 and VK_EXT_swapchain_maintenance1 extensions are
/// enabled.
bool has_maintenance1 = false;
/// Surface data.
VkSurfaceFormatKHR surface_format = {};
std::vector<VkPresentModeKHR> present_modes = {};
std::vector<VkPresentModeKHR> compatible_modes = {};
VkExtent2D swapchain_extents = {};
/// The swapchain.
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
/// Swapchain data.
VkPresentModeKHR current_present_mode = VK_PRESENT_MODE_FIFO_KHR;
VkPresentModeKHR desired_present_mode = VK_PRESENT_MODE_FIFO_KHR;
SwapchainObjects swapchain_objects;
/// The render pass used for rendering.
VkRenderPass render_pass = VK_NULL_HANDLE;
/// The submission history. This is a fixed-size queue, implemented as a circular buffer.
std::array<PerFrame, 2> submit_history = {};
size_t submit_history_index = 0;
/// The present operation history. This is used to clean up present semaphores and old swapchains.
std::deque<PresentOperationInfo> present_history;
/**
* @brief The previous swapchain which needs to be scheduled for destruction when
* appropriate. This will be done when the first image of the current swapchain is
* presented. If there were older swapchains pending destruction when the swapchain is
* recreated, they will accumulate and be destroyed with the previous swapchain.
*
* Note that if the user resizes the window such that the swapchain is recreated every
* frame, this array can go grow indefinitely.
*/
std::vector<SwapchainCleanupData> old_swapchains;
/// Resource pools.
std::vector<VkSemaphore> semaphore_pool;
std::vector<VkFence> fence_pool;
/// Time.
uint32_t frame_number = 0;
// FPS log.
float fps_timer = 0;
uint32_t fps_last_logged_frame_number = 0;
// Other statistics
uint32_t swapchain_creation_count = 0;
// User toggles.
bool recreate_swapchain_on_present_mode_change = false;
// from vkb::VulkanSample
void request_gpu_features(vkb::PhysicalDevice &gpu) override;
std::unique_ptr<vkb::core::DeviceC> create_device(vkb::PhysicalDevice &gpu) override;
void get_queue();
void query_surface_format();
void query_present_modes();
void query_compatible_present_modes(VkPresentModeKHR present_mode);
void adjust_desired_present_mode();
void create_render_pass();
bool are_present_modes_compatible();
void init_swapchain();
void init_swapchain_image(uint32_t index);
void cleanup_swapchain_objects(SwapchainObjects &garbage);
bool recreate_swapchain();
void setup_frame();
void render(uint32_t index);
VkResult acquire_next_image(uint32_t *index);
VkResult present_image(uint32_t index);
void add_present_to_history(uint32_t index, VkFence present_fence);
void cleanup_present_history();
void cleanup_present_info(PresentOperationInfo &present_info);
void cleanup_old_swapchain(SwapchainCleanupData &old_swapchain);
void associate_fence_with_present_history(uint32_t index, VkFence acquire_fence);
void schedule_old_swapchain_for_destruction(VkSwapchainKHR old_swapchain);
VkSemaphore get_semaphore();
void recycle_semaphore(VkSemaphore semaphore);
VkFence get_fence();
void recycle_fence(VkFence fence);
VkPhysicalDevice get_gpu_handle();
VkDevice get_device_handle();
};
std::unique_ptr<vkb::Application> create_swapchain_recreation();
@@ -1,41 +0,0 @@
# Copyright (c) 2019-2024, Sascha Willems
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 the "License";
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
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 "Sascha Willems"
NAME "Terrain tessellation"
DESCRIPTION "Using tessellation shaders for dynamic terrain level-of-detail"
SHADER_FILES_GLSL
"terrain_tessellation/glsl/terrain.vert"
"terrain_tessellation/glsl/terrain.frag"
"terrain_tessellation/glsl/terrain.tesc"
"terrain_tessellation/glsl/terrain.tese"
"terrain_tessellation/glsl/skysphere.vert"
"terrain_tessellation/glsl/skysphere.frag"
SHADER_FILES_HLSL
"terrain_tessellation/hlsl/terrain.vert.hlsl"
"terrain_tessellation/hlsl/terrain.frag.hlsl"
"terrain_tessellation/hlsl/terrain.tesc.hlsl"
"terrain_tessellation/hlsl/terrain.tese.hlsl"
"terrain_tessellation/hlsl/skysphere.vert.hlsl"
"terrain_tessellation/hlsl/skysphere.frag.hlsl")
@@ -1,27 +0,0 @@
////
- Copyright (c) 2019-2023, 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.
-
////
= Terrain Tessellation
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/terrain_tessellation[Khronos Vulkan samples github repository].
endif::[]
Uses a tessellation shader for rendering a terrain with dynamic level-of-detail and frustum culling.
@@ -1,771 +0,0 @@
/* Copyright (c) 2019-2025, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Dynamic terrain tessellation
*/
#include "terrain_tessellation.h"
#include "heightmap.h"
TerrainTessellation::TerrainTessellation()
{
title = "Dynamic terrain tessellation";
}
TerrainTessellation::~TerrainTessellation()
{
if (has_device())
{
// Clean up used Vulkan resources
// Note : Inherited destructor cleans up resources stored in base class
vkDestroyPipeline(get_device().get_handle(), pipelines.terrain, nullptr);
if (pipelines.wireframe != VK_NULL_HANDLE)
{
vkDestroyPipeline(get_device().get_handle(), pipelines.wireframe, nullptr);
}
vkDestroyPipeline(get_device().get_handle(), pipelines.skysphere, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layouts.skysphere, nullptr);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layouts.terrain, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), descriptor_set_layouts.terrain, nullptr);
vkDestroyDescriptorSetLayout(get_device().get_handle(), descriptor_set_layouts.skysphere, nullptr);
uniform_buffers.skysphere_vertex.reset();
uniform_buffers.terrain_tessellation.reset();
textures.heightmap.image.reset();
vkDestroySampler(get_device().get_handle(), textures.heightmap.sampler, nullptr);
textures.skysphere.image.reset();
vkDestroySampler(get_device().get_handle(), textures.skysphere.sampler, nullptr);
textures.terrain_array.image.reset();
vkDestroySampler(get_device().get_handle(), textures.terrain_array.sampler, nullptr);
if (query_pool != VK_NULL_HANDLE)
{
vkDestroyQueryPool(get_device().get_handle(), query_pool, nullptr);
}
}
}
void TerrainTessellation::request_gpu_features(vkb::PhysicalDevice &gpu)
{
auto &requested_features = gpu.get_mutable_requested_features();
// Tessellation shader support is required for this example
if (gpu.get_features().tessellationShader)
{
requested_features.tessellationShader = VK_TRUE;
}
else
{
throw vkb::VulkanException(VK_ERROR_FEATURE_NOT_PRESENT, "Selected GPU does not support tessellation shaders!");
}
// Fill mode non solid is required for wireframe display
if (gpu.get_features().fillModeNonSolid)
{
requested_features.fillModeNonSolid = VK_TRUE;
}
// Pipeline statistics
if (gpu.get_features().pipelineStatisticsQuery)
{
requested_features.pipelineStatisticsQuery = VK_TRUE;
}
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
requested_features.samplerAnisotropy = VK_TRUE;
}
}
// Setup pool and buffer for storing pipeline statistics results
void TerrainTessellation::setup_query_result_buffer()
{
// Create query pool
if (get_device().get_gpu().get_features().pipelineStatisticsQuery)
{
VkQueryPoolCreateInfo query_pool_info = {};
query_pool_info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
query_pool_info.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS;
query_pool_info.pipelineStatistics =
VK_QUERY_PIPELINE_STATISTIC_VERTEX_SHADER_INVOCATIONS_BIT |
VK_QUERY_PIPELINE_STATISTIC_TESSELLATION_EVALUATION_SHADER_INVOCATIONS_BIT;
query_pool_info.queryCount = 2;
VK_CHECK(vkCreateQueryPool(get_device().get_handle(), &query_pool_info, NULL, &query_pool));
}
}
// Retrieves the results of the pipeline statistics query submitted to the command buffer
void TerrainTessellation::get_query_results()
{
// We use vkGetQueryResults to copy the results into a host visible buffer
vkGetQueryPoolResults(
get_device().get_handle(),
query_pool,
0,
1,
sizeof(pipeline_stats),
pipeline_stats,
sizeof(uint64_t),
VK_QUERY_RESULT_64_BIT);
}
void TerrainTessellation::load_assets()
{
skysphere = load_model("scenes/geosphere.gltf");
textures.skysphere = load_texture("textures/skysphere_rgba.ktx", vkb::sg::Image::Color);
// Terrain textures are stored in a texture array with layers corresponding to terrain height
textures.terrain_array = load_texture_array("textures/terrain_texturearray_rgba.ktx", vkb::sg::Image::Color);
// Height data is stored in a one-channel texture
textures.heightmap = load_texture("textures/terrain_heightmap_r16.ktx", vkb::sg::Image::Other);
VkSamplerCreateInfo sampler_create_info = vkb::initializers::sampler_create_info();
// Calculate valid filter and mipmap modes
VkFilter filter = VK_FILTER_LINEAR;
VkSamplerMipmapMode mipmap_mode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
vkb::make_filters_valid(get_device().get_gpu().get_handle(), textures.heightmap.image->get_format(), &filter, &mipmap_mode);
// Setup a mirroring sampler for the height map
vkDestroySampler(get_device().get_handle(), textures.heightmap.sampler, nullptr);
sampler_create_info.magFilter = filter;
sampler_create_info.minFilter = filter;
sampler_create_info.mipmapMode = mipmap_mode;
sampler_create_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
sampler_create_info.addressModeV = sampler_create_info.addressModeU;
sampler_create_info.addressModeW = sampler_create_info.addressModeU;
sampler_create_info.compareOp = VK_COMPARE_OP_NEVER;
sampler_create_info.minLod = 0.0f;
sampler_create_info.maxLod = static_cast<float>(textures.heightmap.image->get_mipmaps().size());
sampler_create_info.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
VK_CHECK(vkCreateSampler(get_device().get_handle(), &sampler_create_info, nullptr, &textures.heightmap.sampler));
filter = VK_FILTER_LINEAR;
mipmap_mode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
vkb::make_filters_valid(get_device().get_gpu().get_handle(), textures.terrain_array.image->get_format(), &filter, &mipmap_mode);
// Setup a repeating sampler for the terrain texture layers
vkDestroySampler(get_device().get_handle(), textures.terrain_array.sampler, nullptr);
sampler_create_info = vkb::initializers::sampler_create_info();
sampler_create_info.magFilter = filter;
sampler_create_info.minFilter = filter;
sampler_create_info.mipmapMode = mipmap_mode;
sampler_create_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
sampler_create_info.addressModeV = sampler_create_info.addressModeU;
sampler_create_info.addressModeW = sampler_create_info.addressModeU;
sampler_create_info.compareOp = VK_COMPARE_OP_NEVER;
sampler_create_info.minLod = 0.0f;
sampler_create_info.maxLod = static_cast<float>(textures.terrain_array.image->get_mipmaps().size());
sampler_create_info.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
if (get_device().get_gpu().get_features().samplerAnisotropy)
{
sampler_create_info.maxAnisotropy = 4.0f;
sampler_create_info.anisotropyEnable = VK_TRUE;
}
VK_CHECK(vkCreateSampler(get_device().get_handle(), &sampler_create_info, nullptr, &textures.terrain_array.sampler));
}
void TerrainTessellation::build_command_buffers()
{
VkCommandBufferBeginInfo command_buffer_begin_info = vkb::initializers::command_buffer_begin_info();
VkClearValue clear_values[2];
clear_values[0].color = default_clear_color;
clear_values[1].depthStencil = {0.0f, 0};
VkRenderPassBeginInfo render_pass_begin_info = vkb::initializers::render_pass_begin_info();
render_pass_begin_info.renderPass = render_pass;
render_pass_begin_info.renderArea.offset.x = 0;
render_pass_begin_info.renderArea.offset.y = 0;
render_pass_begin_info.renderArea.extent.width = width;
render_pass_begin_info.renderArea.extent.height = height;
render_pass_begin_info.clearValueCount = 2;
render_pass_begin_info.pClearValues = clear_values;
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
render_pass_begin_info.framebuffer = framebuffers[i];
VK_CHECK(vkBeginCommandBuffer(draw_cmd_buffers[i], &command_buffer_begin_info));
if (get_device().get_gpu().get_features().pipelineStatisticsQuery)
{
vkCmdResetQueryPool(draw_cmd_buffers[i], query_pool, 0, 2);
}
vkCmdBeginRenderPass(draw_cmd_buffers[i], &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport = vkb::initializers::viewport(static_cast<float>(width), static_cast<float>(height), 0.0f, 1.0f);
vkCmdSetViewport(draw_cmd_buffers[i], 0, 1, &viewport);
VkRect2D scissor = vkb::initializers::rect2D(width, height, 0, 0);
vkCmdSetScissor(draw_cmd_buffers[i], 0, 1, &scissor);
vkCmdSetLineWidth(draw_cmd_buffers[i], 1.0f);
VkDeviceSize offsets[1] = {0};
// Skysphere
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.skysphere);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layouts.skysphere, 0, 1, &descriptor_sets.skysphere, 0, NULL);
draw_model(skysphere, draw_cmd_buffers[i]);
// Terrain
if (get_device().get_gpu().get_features().pipelineStatisticsQuery)
{
// Begin pipeline statistics query
vkCmdBeginQuery(draw_cmd_buffers[i], query_pool, 0, 0);
}
// Render
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, wireframe ? pipelines.wireframe : pipelines.terrain);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layouts.terrain, 0, 1, &descriptor_sets.terrain, 0, NULL);
vkCmdBindVertexBuffers(draw_cmd_buffers[i], 0, 1, terrain.vertices->get(), offsets);
vkCmdBindIndexBuffer(draw_cmd_buffers[i], terrain.indices->get_handle(), 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(draw_cmd_buffers[i], terrain.index_count, 1, 0, 0, 0);
if (get_device().get_gpu().get_features().pipelineStatisticsQuery)
{
// End pipeline statistics query
vkCmdEndQuery(draw_cmd_buffers[i], query_pool, 0);
}
draw_ui(draw_cmd_buffers[i]);
vkCmdEndRenderPass(draw_cmd_buffers[i]);
VK_CHECK(vkEndCommandBuffer(draw_cmd_buffers[i]));
}
}
// Generate a terrain quad patch for feeding to the tessellation control shader
void TerrainTessellation::generate_terrain()
{
const uint32_t patch_size = 64;
const float uv_scale = 1.0f;
const uint32_t vertex_count = patch_size * patch_size;
std::vector<Vertex> vertices(vertex_count);
const float wx = 2.0f;
const float wy = 2.0f;
for (auto x = 0; x < patch_size; x++)
{
for (auto y = 0; y < patch_size; y++)
{
uint32_t index = (x + y * patch_size);
vertices[index].pos[0] = x * wx + wx / 2.0f - static_cast<float>(patch_size) * wx / 2.0f;
vertices[index].pos[1] = 0.0f;
vertices[index].pos[2] = y * wy + wy / 2.0f - static_cast<float>(patch_size) * wy / 2.0f;
vertices[index].uv = glm::vec2(static_cast<float>(x) / patch_size, static_cast<float>(y) / patch_size) * uv_scale;
}
}
// Calculate normals from height map using a sobel filter
vkb::HeightMap heightmap("textures/terrain_heightmap_r16.ktx", patch_size);
for (auto x = 0; x < patch_size; x++)
{
for (auto y = 0; y < patch_size; y++)
{
// Get height samples centered around current position
float heights[3][3];
for (auto hx = -1; hx <= 1; hx++)
{
for (auto hy = -1; hy <= 1; hy++)
{
heights[hx + 1][hy + 1] = heightmap.get_height(x + hx, y + hy);
}
}
// Calculate the normal
glm::vec3 normal;
// Gx sobel filter
normal.x = heights[0][0] - heights[2][0] + 2.0f * heights[0][1] - 2.0f * heights[2][1] + heights[0][2] - heights[2][2];
// Gy sobel filter
normal.z = heights[0][0] + 2.0f * heights[1][0] + heights[2][0] - heights[0][2] - 2.0f * heights[1][2] - heights[2][2];
// Calculate missing up component of the normal using the filtered x and y axis
// The first value controls the bump strength
normal.y = 0.25f * sqrt(1.0f - normal.x * normal.x - normal.z * normal.z);
vertices[x + y * patch_size].normal = glm::normalize(normal * glm::vec3(2.0f, 1.0f, 2.0f));
}
}
// Indices
const uint32_t w = (patch_size - 1);
const uint32_t index_count = w * w * 4;
std::vector<uint32_t> indices(index_count);
for (auto x = 0; x < w; x++)
{
for (auto y = 0; y < w; y++)
{
uint32_t index = (x + y * w) * 4;
indices[index] = (x + y * patch_size);
indices[index + 1] = indices[index] + patch_size;
indices[index + 2] = indices[index + 1] + 1;
indices[index + 3] = indices[index] + 1;
}
}
terrain.index_count = index_count;
uint32_t vertex_buffer_size = vertex_count * sizeof(Vertex);
uint32_t index_buffer_size = index_count * sizeof(uint32_t);
// Create staging buffers
vkb::core::BufferC vertex_staging = vkb::core::BufferC::create_staging_buffer(get_device(), vertices);
vkb::core::BufferC index_staging = vkb::core::BufferC::create_staging_buffer(get_device(), indices);
terrain.vertices = std::make_unique<vkb::core::BufferC>(get_device(),
vertex_buffer_size,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VMA_MEMORY_USAGE_GPU_ONLY);
terrain.indices = std::make_unique<vkb::core::BufferC>(get_device(),
index_buffer_size,
VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VMA_MEMORY_USAGE_GPU_ONLY);
// Copy from staging buffers
VkCommandBuffer copy_command = get_device().create_command_buffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
VkBufferCopy copy_region = {};
copy_region.size = vertex_buffer_size;
vkCmdCopyBuffer(
copy_command,
vertex_staging.get_handle(),
terrain.vertices->get_handle(),
1,
&copy_region);
copy_region.size = index_buffer_size;
vkCmdCopyBuffer(
copy_command,
index_staging.get_handle(),
terrain.indices->get_handle(),
1,
&copy_region);
get_device().flush_command_buffer(copy_command, queue, true);
}
void TerrainTessellation::setup_descriptor_pool()
{
std::vector<VkDescriptorPoolSize> pool_sizes =
{
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 3),
vkb::initializers::descriptor_pool_size(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 3)};
VkDescriptorPoolCreateInfo descriptor_pool_create_info =
vkb::initializers::descriptor_pool_create_info(
static_cast<uint32_t>(pool_sizes.size()),
pool_sizes.data(),
2);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &descriptor_pool_create_info, nullptr, &descriptor_pool));
}
void TerrainTessellation::setup_descriptor_set_layouts()
{
VkDescriptorSetLayoutCreateInfo descriptor_layout;
VkPipelineLayoutCreateInfo pipeline_layout_create_info;
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings;
// Terrain
set_layout_bindings =
{
// Binding 0 : Shared Tessellation shader ubo
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT | VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT,
0),
// Binding 1 : Height map
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT | VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
1),
// Binding 3 : Terrain texture array layers
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
VK_SHADER_STAGE_FRAGMENT_BIT,
2),
};
descriptor_layout = vkb::initializers::descriptor_set_layout_create_info(set_layout_bindings.data(), static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout, nullptr, &descriptor_set_layouts.terrain));
pipeline_layout_create_info = vkb::initializers::pipeline_layout_create_info(&descriptor_set_layouts.terrain, 1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &pipeline_layouts.terrain));
// Skysphere
set_layout_bindings =
{
// Binding 0 : Vertex shader ubo
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
VK_SHADER_STAGE_VERTEX_BIT,
0),
// Binding 1 : Color map
vkb::initializers::descriptor_set_layout_binding(
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
VK_SHADER_STAGE_FRAGMENT_BIT,
1),
};
descriptor_layout = vkb::initializers::descriptor_set_layout_create_info(set_layout_bindings.data(), static_cast<uint32_t>(set_layout_bindings.size()));
VK_CHECK(vkCreateDescriptorSetLayout(get_device().get_handle(), &descriptor_layout, nullptr, &descriptor_set_layouts.skysphere));
pipeline_layout_create_info = vkb::initializers::pipeline_layout_create_info(&descriptor_set_layouts.skysphere, 1);
VK_CHECK(vkCreatePipelineLayout(get_device().get_handle(), &pipeline_layout_create_info, nullptr, &pipeline_layouts.skysphere));
}
void TerrainTessellation::setup_descriptor_sets()
{
VkDescriptorSetAllocateInfo alloc_info;
std::vector<VkWriteDescriptorSet> write_descriptor_sets;
// Terrain
alloc_info = vkb::initializers::descriptor_set_allocate_info(descriptor_pool, &descriptor_set_layouts.terrain, 1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &descriptor_sets.terrain));
VkDescriptorBufferInfo terrain_buffer_descriptor = create_descriptor(*uniform_buffers.terrain_tessellation);
VkDescriptorImageInfo heightmap_image_descriptor = create_descriptor(textures.heightmap);
VkDescriptorImageInfo terrainmap_image_descriptor = create_descriptor(textures.terrain_array);
write_descriptor_sets =
{
// Binding 0 : Shared tessellation shader ubo
vkb::initializers::write_descriptor_set(
descriptor_sets.terrain,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
0,
&terrain_buffer_descriptor),
// Binding 1 : Displacement map
vkb::initializers::write_descriptor_set(
descriptor_sets.terrain,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
1,
&heightmap_image_descriptor),
// Binding 2 : Color map (alpha channel)
vkb::initializers::write_descriptor_set(
descriptor_sets.terrain,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
2,
&terrainmap_image_descriptor),
};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
// Skysphere
alloc_info = vkb::initializers::descriptor_set_allocate_info(descriptor_pool, &descriptor_set_layouts.skysphere, 1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &alloc_info, &descriptor_sets.skysphere));
VkDescriptorBufferInfo skysphere_buffer_descriptor = create_descriptor(*uniform_buffers.skysphere_vertex);
VkDescriptorImageInfo skysphere_image_descriptor = create_descriptor(textures.skysphere);
write_descriptor_sets =
{
// Binding 0 : Vertex shader ubo
vkb::initializers::write_descriptor_set(
descriptor_sets.skysphere,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
0,
&skysphere_buffer_descriptor),
// Binding 1 : Fragment shader color map
vkb::initializers::write_descriptor_set(
descriptor_sets.skysphere,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
1,
&skysphere_image_descriptor),
};
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
}
void TerrainTessellation::prepare_pipelines()
{
VkPipelineInputAssemblyStateCreateInfo input_assembly_state_create_info =
vkb::initializers::pipeline_input_assembly_state_create_info(
VK_PRIMITIVE_TOPOLOGY_PATCH_LIST,
0,
VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterization_state =
vkb::initializers::pipeline_rasterization_state_create_info(
VK_POLYGON_MODE_FILL,
VK_CULL_MODE_BACK_BIT,
VK_FRONT_FACE_COUNTER_CLOCKWISE,
0);
VkPipelineColorBlendAttachmentState blend_attachment_state =
vkb::initializers::pipeline_color_blend_attachment_state(
0xf,
VK_FALSE);
VkPipelineColorBlendStateCreateInfo color_blend_state =
vkb::initializers::pipeline_color_blend_state_create_info(
1,
&blend_attachment_state);
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
VkPipelineDepthStencilStateCreateInfo depth_stencil_state =
vkb::initializers::pipeline_depth_stencil_state_create_info(
VK_TRUE,
VK_TRUE,
VK_COMPARE_OP_GREATER);
VkPipelineViewportStateCreateInfo viewport_state =
vkb::initializers::pipeline_viewport_state_create_info(1, 1, 0);
VkPipelineMultisampleStateCreateInfo multisample_state =
vkb::initializers::pipeline_multisample_state_create_info(
VK_SAMPLE_COUNT_1_BIT,
0);
std::vector<VkDynamicState> dynamic_state_enables = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR,
VK_DYNAMIC_STATE_LINE_WIDTH};
VkPipelineDynamicStateCreateInfo dynamic_state =
vkb::initializers::pipeline_dynamic_state_create_info(
dynamic_state_enables.data(),
static_cast<uint32_t>(dynamic_state_enables.size()),
0);
// We render the terrain as a grid of quad patches
VkPipelineTessellationStateCreateInfo tessellation_state =
vkb::initializers::pipeline_tessellation_state_create_info(4);
// Vertex bindings an attributes
// Binding description
std::vector<VkVertexInputBindingDescription> vertex_input_bindings = {
vkb::initializers::vertex_input_binding_description(0, sizeof(TerrainTessellation::Vertex), VK_VERTEX_INPUT_RATE_VERTEX),
};
// Attribute descriptions
std::vector<VkVertexInputAttributeDescription> vertex_input_attributes = {
vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0), // Position
vkb::initializers::vertex_input_attribute_description(0, 1, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3), // Normal
vkb::initializers::vertex_input_attribute_description(0, 2, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 6), // UV
};
VkPipelineVertexInputStateCreateInfo vertex_input_state = vkb::initializers::pipeline_vertex_input_state_create_info();
vertex_input_state.vertexBindingDescriptionCount = static_cast<uint32_t>(vertex_input_bindings.size());
vertex_input_state.pVertexBindingDescriptions = vertex_input_bindings.data();
vertex_input_state.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size());
vertex_input_state.pVertexAttributeDescriptions = vertex_input_attributes.data();
std::array<VkPipelineShaderStageCreateInfo, 4> shader_stages;
// Terrain tessellation pipeline
shader_stages[0] = load_shader("terrain_tessellation", "terrain.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("terrain_tessellation", "terrain.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
shader_stages[2] = load_shader("terrain_tessellation", "terrain.tesc.spv", VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT);
shader_stages[3] = load_shader("terrain_tessellation", "terrain.tese.spv", VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT);
VkGraphicsPipelineCreateInfo pipeline_create_info =
vkb::initializers::pipeline_create_info(pipeline_layouts.terrain, render_pass, 0);
pipeline_create_info.pVertexInputState = &vertex_input_state;
pipeline_create_info.pInputAssemblyState = &input_assembly_state_create_info;
pipeline_create_info.pRasterizationState = &rasterization_state;
pipeline_create_info.pColorBlendState = &color_blend_state;
pipeline_create_info.pMultisampleState = &multisample_state;
pipeline_create_info.pViewportState = &viewport_state;
pipeline_create_info.pDepthStencilState = &depth_stencil_state;
pipeline_create_info.pDynamicState = &dynamic_state;
pipeline_create_info.pTessellationState = &tessellation_state;
pipeline_create_info.stageCount = static_cast<uint32_t>(shader_stages.size());
pipeline_create_info.pStages = shader_stages.data();
pipeline_create_info.renderPass = render_pass;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.terrain));
// Terrain wireframe pipeline
if (get_device().get_gpu().get_features().fillModeNonSolid)
{
rasterization_state.polygonMode = VK_POLYGON_MODE_LINE;
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.wireframe));
};
// Skysphere pipeline
// Stride from glTF model vertex layout
vertex_input_bindings[0].stride = sizeof(::Vertex);
rasterization_state.polygonMode = VK_POLYGON_MODE_FILL;
// Revert to triangle list topology
input_assembly_state_create_info.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
// Reset tessellation state
pipeline_create_info.pTessellationState = nullptr;
// Don't write to depth buffer
depth_stencil_state.depthWriteEnable = VK_FALSE;
pipeline_create_info.stageCount = 2;
pipeline_create_info.layout = pipeline_layouts.skysphere;
shader_stages[0] = load_shader("terrain_tessellation", "skysphere.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shader_stages[1] = load_shader("terrain_tessellation", "skysphere.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
VK_CHECK(vkCreateGraphicsPipelines(get_device().get_handle(), pipeline_cache, 1, &pipeline_create_info, nullptr, &pipelines.skysphere));
}
// Prepare and initialize uniform buffer containing shader uniforms
void TerrainTessellation::prepare_uniform_buffers()
{
// Shared tessellation shader stages uniform buffer
uniform_buffers.terrain_tessellation = std::make_unique<vkb::core::BufferC>(get_device(),
sizeof(ubo_tess),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
// Skysphere vertex shader uniform buffer
uniform_buffers.skysphere_vertex = std::make_unique<vkb::core::BufferC>(get_device(),
sizeof(ubo_vs),
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
}
void TerrainTessellation::update_uniform_buffers()
{
// Tessellation
ubo_tess.projection = camera.matrices.perspective;
ubo_tess.modelview = camera.matrices.view * glm::mat4(1.0f);
ubo_tess.light_pos.y = -0.5f - ubo_tess.displacement_factor; // todo: Not used yet
ubo_tess.viewport_dim = glm::vec2(static_cast<float>(width), static_cast<float>(height));
frustum.update(ubo_tess.projection * ubo_tess.modelview);
memcpy(ubo_tess.frustum_planes, frustum.get_planes().data(), sizeof(glm::vec4) * 6);
float saved_factor = ubo_tess.tessellation_factor;
if (!tessellation)
{
// Setting this to zero sets all tessellation factors to 1.0 in the shader
ubo_tess.tessellation_factor = 0.0f;
}
uniform_buffers.terrain_tessellation->convert_and_update(ubo_tess);
if (!tessellation)
{
ubo_tess.tessellation_factor = saved_factor;
}
// Skysphere vertex shader
ubo_vs.mvp = camera.matrices.perspective * glm::mat4(glm::mat3(camera.matrices.view));
uniform_buffers.skysphere_vertex->convert_and_update(ubo_vs.mvp);
}
void TerrainTessellation::draw()
{
ApiVulkanSample::prepare_frame();
// Command buffer to be submitted to the queue
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &draw_cmd_buffers[current_buffer];
// Submit to queue
VK_CHECK(vkQueueSubmit(queue, 1, &submit_info, VK_NULL_HANDLE));
if (get_device().get_gpu().get_features().pipelineStatisticsQuery)
{
// Read query results for displaying in next frame
get_query_results();
}
ApiVulkanSample::submit_frame();
}
bool TerrainTessellation::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
// Note: Using reversed depth-buffer for increased precision, so Znear and Zfar are flipped
camera.type = vkb::CameraType::FirstPerson;
camera.set_perspective(60.0f, static_cast<float>(width) / static_cast<float>(height), 512.0f, 0.1f);
camera.set_rotation(glm::vec3(-12.0f, 159.0f, 0.0f));
camera.set_translation(glm::vec3(18.0f, 22.5f, 57.5f));
camera.translation_speed = 7.5f;
load_assets();
generate_terrain();
if (get_device().get_gpu().get_features().pipelineStatisticsQuery)
{
setup_query_result_buffer();
}
prepare_uniform_buffers();
setup_descriptor_set_layouts();
prepare_pipelines();
setup_descriptor_pool();
setup_descriptor_sets();
build_command_buffers();
prepared = true;
return true;
}
void TerrainTessellation::render(float delta_time)
{
if (!prepared)
{
return;
}
draw();
}
void TerrainTessellation::view_changed()
{
update_uniform_buffers();
}
void TerrainTessellation::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Settings"))
{
if (drawer.checkbox("Tessellation", &tessellation))
{
update_uniform_buffers();
}
if (drawer.input_float("Factor", &ubo_tess.tessellation_factor, 0.05f, "%.2f"))
{
update_uniform_buffers();
}
if (get_device().get_gpu().get_features().fillModeNonSolid)
{
if (drawer.checkbox("Wireframe", &wireframe))
{
rebuild_command_buffers();
}
}
}
if (get_device().get_gpu().get_features().pipelineStatisticsQuery)
{
if (drawer.header("Pipeline statistics"))
{
drawer.text("VS invocations: %d", pipeline_stats[0]);
drawer.text("TE invocations: %d", pipeline_stats[1]);
}
}
}
std::unique_ptr<vkb::Application> create_terrain_tessellation()
{
return std::make_unique<TerrainTessellation>();
}
@@ -1,136 +0,0 @@
/* Copyright (c) 2019-2024, Sascha Willems
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Dynamic terrain tessellation
*/
#pragma once
#include "api_vulkan_sample.h"
#include "core/buffer.h"
#include "geometry/frustum.h"
class TerrainTessellation : public ApiVulkanSample
{
public:
bool wireframe = false;
bool tessellation = true;
struct
{
Texture heightmap;
Texture skysphere;
Texture terrain_array;
} textures;
std::unique_ptr<vkb::sg::SubMesh> skysphere;
struct Vertex
{
glm::vec3 pos;
glm::vec3 normal;
glm::vec2 uv;
};
struct Terrain
{
std::unique_ptr<vkb::core::BufferC> vertices;
std::unique_ptr<vkb::core::BufferC> indices;
uint32_t index_count;
} terrain;
struct
{
std::unique_ptr<vkb::core::BufferC> terrain_tessellation;
std::unique_ptr<vkb::core::BufferC> skysphere_vertex;
} uniform_buffers;
// Shared values for tessellation control and evaluation stages
struct
{
glm::mat4 projection;
glm::mat4 modelview;
glm::vec4 light_pos = glm::vec4(-48.0f, -40.0f, 46.0f, 0.0f);
glm::vec4 frustum_planes[6];
float displacement_factor = 32.0f;
float tessellation_factor = 0.75f;
glm::vec2 viewport_dim;
// Desired size of tessellated quad patch edge
float tessellated_edge_size = 20.0f;
} ubo_tess;
// Skysphere vertex shader stage
struct
{
glm::mat4 mvp;
} ubo_vs;
struct Pipelines
{
VkPipeline terrain;
VkPipeline wireframe = VK_NULL_HANDLE;
VkPipeline skysphere;
} pipelines;
struct
{
VkDescriptorSetLayout terrain;
VkDescriptorSetLayout skysphere;
} descriptor_set_layouts;
struct
{
VkPipelineLayout terrain;
VkPipelineLayout skysphere;
} pipeline_layouts;
struct
{
VkDescriptorSet terrain;
VkDescriptorSet skysphere;
} descriptor_sets;
// Pipeline statistics
VkQueryPool query_pool = VK_NULL_HANDLE;
uint64_t pipeline_stats[2] = {0};
// View frustum passed to tessellation control shader for culling
vkb::Frustum frustum;
TerrainTessellation();
~TerrainTessellation();
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
void setup_query_result_buffer();
void get_query_results();
void load_assets();
void build_command_buffers() override;
void generate_terrain();
void setup_descriptor_pool();
void setup_descriptor_set_layouts();
void setup_descriptor_sets();
void prepare_pipelines();
void prepare_uniform_buffers();
void update_uniform_buffers();
void draw();
bool prepare(const vkb::ApplicationOptions &options) override;
virtual void render(float delta_time) override;
virtual void view_changed() override;
virtual void on_update_ui_overlay(vkb::Drawer &drawer) override;
};
std::unique_ptr<vkb::Application> create_terrain_tessellation();
@@ -1,33 +0,0 @@
# Copyright (c) 2019-2024, Sascha Willems
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 the "License";
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
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 "Sascha Willems"
NAME "Texture mipmap generation"
DESCRIPTION "Generate mipmaps for a texture"
SHADER_FILES_GLSL
"texture_mipmap_generation/glsl/texture.vert"
"texture_mipmap_generation/glsl/texture.frag"
SHADER_FILES_HLSL
"texture_mipmap_generation/hlsl/texture.vert.hlsl"
"texture_mipmap_generation/hlsl/texture.frag.hlsl")
@@ -1,266 +0,0 @@
////
- Copyright (c) 2019-2023, Sascha Willems
-
- SPDX-License-Identifier: Apache-2.0
-
- Licensed under the Apache License, Version 2.0 the "License";
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
////
= Run-time mip-map generation
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/texture_mipmap_generation[Khronos Vulkan samples github repository].
endif::[]
== Overview
Generates a complete texture mip-chain at runtime from a base image using image blits and proper image barriers.
This examples demonstrates how to generate a complete texture mip-chain at runtime instead of loading offline generated mip-maps from a texture file.
While usually not applied for textures stored on the disk (that usually have the mips generated offline and stored in the file) this technique is often used for dynamic textures like cubemaps for reflections or other render-to-texture effects.
Having mip-maps for runtime generated textures offers lots of benefits, both in terms of image stability and performance.
Without mip mapping the image will become noisy, especially with high frequency textures (and texture components like specular) and using mip mapping will result in higher performance due to caching.
Though this example only generates one mip-chain for a single texture at the beginning this technique can also be used during normal frame rendering to generate mip-chains for dynamic textures.
Some GPUs also offer `asynchronous transfer queues` that may be used for doing such operations in the background.
To detect this, check for queue families with only the `VK_QUEUE_TRANSFER_BIT` set.
== Comparison
Without mip mapping:
image::./images/mip_mapping_off.jpg[Off,512px]
Using mip mapping with a bilinear filter:
image:./images/mip_mapping_bilinear.jpg[Bilinear,512px]
Using mip mapping with an anisotropic filter:
image:./images/mip_mapping_anisotropic.jpg[Anisotropic,512px]
== Requirements
To downsample from one mip level to the next, we will be using https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkCmdBlitImage.html[`vkCmdBlitImage`].
This requires the format used to support the `BLIT_SRC_BIT` and the `BLIT_DST_BIT` flags.
If these are not supported, the image format can't be used to blit and you'd either have to choose a different format or use a custom shader to generate mip levels.
The example uses the `VK_FORMAT_R8G8B8A8_SRGB` that should support these flags on most implementations.
*_Note:_* Use https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkGetPhysicalDeviceFormatProperties.html[`vkGetPhysicalDeviceFormatProperties`] to check if the format supports the blit flags first.
== Points of interest
=== Image setup
Even though we'll only upload the first mip level initially, we create the image with number of desired mip levels.
The following formula is used to calculate the number of mip levels based on the max.
image extent:
[,cpp]
----
texture.mip_levels = static_cast<uint32_t>(floor(log2(std::max(texture.width, texture.height))) + 1);
----
This is then passed to the image creat info:
[,cpp]
----
VkImageCreateInfo image_create_info = vkb::initializers::image_create_info();
image_create_info.imageType = VK_IMAGE_TYPE_2D;
image_create_info.format = format;
image_create_info.mipLevels = texture.mip_levels;
----
Setting the number of desired mip levels is necessary as this is used for allocating the correct amount of memory required the image (`vkAllocateMemory`).
=== Upload base mip level
Before generating the mip-chain we need to copy the image data loaded from disk into the newly generated image.
This image will be the base for our mip-chain:
[,cpp]
----
VkBufferImageCopy buffer_copy_region = {};
buffer_copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
buffer_copy_region.imageSubresource.mipLevel = 0;
buffer_copy_region.imageSubresource.baseArrayLayer = 0;
buffer_copy_region.imageSubresource.layerCount = 1;
buffer_copy_region.imageExtent.width = texture.width;
buffer_copy_region.imageExtent.height = texture.height;
buffer_copy_region.imageExtent.depth = 1;
vkCmdCopyBufferToImage(copy_command, staging_buffer, texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &buffer_copy_region);
----
=== Prepare base mip level
As we are going to blit *_from_* the base mip-level just uploaded we also need to insert an image memory barrier that transitions the image layout to `TRANSFER_SRC` for the base mip level:
[,cpp]
----
vkb::insert_image_memory_barrier(
copy_command,
texture.image,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1});
----
=== Generating the mip-chain
There are two different ways of generating the mip-chain.
The first one is to blit down the whole mip-chain from level n-1 to n, the other way would be to always use the base image and blit down from that to all levels.
This example uses the first one.
*_Note:_* Blitting (same for copying) images is done inside of a command buffer that has to be submitted and as such has to be synchronized before using the new image with e.g.
a `vkFence`.
We simply loop over all remaining mip levels (level 0 was loaded from disk) and prepare a `VkImageBlit` structure for each blit from mip level i-1 to level i.
First the source for our blit.
This is the previous mip level.
The dimensions of the blit source are specified by srcOffset:
// {% raw %}
[,cpp]
----
for (int32_t i = 1; i < texture.mipLevels; i++)
{
VkImageBlit image_blit{};
// Source
image_blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
image_blit.srcSubresource.layerCount = 1;
image_blit.srcSubresource.mipLevel = i - 1;
image_blit.srcOffsets[1].x = int32_t(texture.width >> (i - 1));
image_blit.srcOffsets[1].y = int32_t(texture.height >> (i - 1));
image_blit.srcOffsets[1].z = 1;
}
----
// {% endraw %}
Setup for the destination mip level (1), with the dimensions for the blit destination specified in dstOffsets[1]:
[,cpp]
----
// Destination
image_blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
image_blit.dstSubresource.layerCount = 1;
image_blit.dstSubresource.mipLevel = i;
image_blit.dstOffsets[1].x = int32_t(texture.width >> i);
image_blit.dstOffsets[1].y = int32_t(texture.height >> i);
image_blit.dstOffsets[1].z = 1;
----
Before we can blit to this mip level, we need to transition it's image layout to `TRANSFER_DST`:
[,cpp]
----
// Prepare current mip level as image blit destination
vkb::insert_image_memory_barrier(
blit_command,
texture.image,
0,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
{VK_IMAGE_ASPECT_COLOR_BIT, i, 1, 0, 1});
----
Note that we set the `baseMipLevel` of the subresource range to `i`, so the image memory barrier will only affect the one mip level we want to copy to.
Now that the mip level we want to copy from and the one we'll copy to are in the proper layout (transfer source and destination) we can issue the https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkCmdBlitImage.html[`vkCmdBlitImage`] to copy from mip level (i-1) to mip level (i):
[,cpp]
----
vkCmdBlitImage(
blit_command,
texture.image,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
texture.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1,
&image_blit,
VK_FILTER_LINEAR);
----
`vkCmdBlitImage` does the down sampling from mip level (i-1) to mip level (i) using a linear filter, if you need better or more advanced filtering for this you need to resort to using custom shaders for generating the mip chain instead of blitting.
After the blit is done we can use this mip level as a base for the next level, so we transition the layout from `TRANSFER_DST_OPTIMAL` to `TRANSFER_SRC_OPTIMAL` so we can use this level as transfer source for the next level:
[,cpp]
----
vkb::insert_image_memory_barrier(
blit_command,
texture.image,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
{VK_IMAGE_ASPECT_COLOR_BIT, i, 1, 0, 1});
}
----
=== Final image layout transitions
Once the loop is done we need to transition all mip levels of the image to their actual usage layout, which is `SHADER_READ` for this example.
Note that after the loop above all levels will be in the `TRANSER_SRC` layout allowing us to transfer the whole image with a single barrier:
[,cpp]
----
vkb::insert_image_memory_barrier(
blit_command,
texture.image,
VK_ACCESS_TRANSFER_READ_BIT,
VK_ACCESS_SHADER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
{VK_IMAGE_ASPECT_COLOR_BIT, 0, texture.mip_levels, 0, 1});
----
Submitting that command buffer will result in an image with a complete mip-chain and all mip levels being transitioned to the proper image layout for shader reads.
=== Image View creation
The Image View also requires information about how many Mip Levels are used.
This is specified in the `VkImageViewCreateInfo.subresourceRange.levelCount` field.
[,cpp]
----
VkImageViewCreateInfo view = vkb::initializers::image_view_create_info();
view.image = texture.image;
view.viewType = VK_IMAGE_VIEW_TYPE_2D;
view.format = format;
view.components = {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A};
view.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view.subresourceRange.baseMipLevel = 0;
view.subresourceRange.baseArrayLayer = 0;
view.subresourceRange.layerCount = 1;
view.subresourceRange.levelCount = texture.mip_levels;
VK_CHECK(vkCreateImageView(device->get_handle(), &view, nullptr, &texture.view));
----

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