This commit is contained in:
xsl
2025-09-04 10:54:47 +08:00
commit 6bc8f61b18
1808 changed files with 208268 additions and 0 deletions
@@ -0,0 +1,32 @@
# Copyright (c) 2021, Holochip Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 the "License";
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample_with_tags(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Holochip Corporation"
NAME "Multi-Draw Indirect"
DESCRIPTION "Improve performance by offloading common tasks from CPU to GPU."
SHADER_FILES_GLSL
"multi_draw_indirect/cull.comp"
"multi_draw_indirect/cull_address.comp"
"multi_draw_indirect/multi_draw_indirect.vert"
"multi_draw_indirect/multi_draw_indirect.frag")
@@ -0,0 +1,94 @@
////
- Copyright (c) 2021-2025, Holochip Corporation
-
- 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.
-
////
= GPU Rendering and Multi-Draw Indirect
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/performance/multi_draw_indirect[Khronos Vulkan samples github repository].
endif::[]
This sample demonstrates how to reduce CPU usage by offloading draw call generation and frustum culling to the GPU.
== Draw Call Generation
A common method of rendering large scenes is to iterate through each model and bind its resources such as vertex buffers, index buffers, and descriptors prior to each draw call.
However, each bound resource has an overhead both in command buffer generation (e.g.
calls to `vkCmdBindVertexBuffer`) and rendering (e.g.
binding the resource).
An alternative method is introduced by using GPU rendering and the use of the indirect call functions introduced in Vulkan 1.2.
Whereas the draw parameters to the function `vkCmdDrawIndexed` are provided by the CPU, calls to the function `vkCmdDrawIndexIndirect` query commands from a GPU buffer.
This has two significant advantages:
. Draw calls can be generated from the GPU (such as in a "compute shader"), and
. An array of draw calls can be called at once, reducing command buffer overhead
The information for indirect commands is provided by the struct `VkDrawIndexedIndirectCommand`, which contains information about the vertices and indices to draw.
Since the struct allows for offsets of both vertex and index buffers (through firstIndex and vertexOffset), multiple models from a scene can be placed into a single index buffer and vertex buffer and bound just once, wherein each portion of the index buffer is zero-indexed.
This also means that a large index buffer of type `uint16_t` can be used even when the total number of vertices in the scene exceeds the upper limit of `+2^16+` for a single draw call.
In this sample, the scene is composed of a 16x16 grid of sub-meshes.
One vertex buffer and one index buffer contain all the geometry information in the scene, and the draw calls are placed into a device-local buffer containing an array of `VkDrawIndexedIndirectCommand` commands.
Each command contains the vertex offset, index offset, and index count.
To control whether a sub-mesh is drawn, the instance count is set to either 0 or 1.
Alternatively, the draw command could be completely removed from the array.
If the device supports multi-draw indirect (`VkPhysicalDeviceFeatures2::multiDrawIndirect`), then the entire array of draw commands can be executed through a single call to `VkDrawIndexedIndirectCommand`.
Otherwise, each draw call must be executed through a separate call to `VkDrawIndexIndirectCommand`:
[,cpp]
----
// m_enable_mci: supports multiDrawIndirect
if (m_enable_mci && m_supports_mci)
{
vkCmdDrawIndexedIndirect(draw_cmd_buffers[i], indirect_call_buffer->get_handle(), 0, cpu_commands.size(), sizeof(cpu_commands[0]));
}
else
{
for (size_t j = 0; j < cpu_commands.size(); ++j)
{
vkCmdDrawIndexedIndirect(draw_cmd_buffers[i], indirect_call_buffer->get_handle(), j * sizeof(cpu_commands[0]), 1, sizeof(cpu_commands[0]));
}
}
----
== CPU vs. GPU Call Generation
The sample provides three methods of generating draw calls: CPU-only, GPU, and GPU using buffer device address.
In all three methods, the model vertex/index information is fixed, and only the number of instances is changed (to disable / enable drawing) by determining whether the bounding sphere of the model fits within the view (i.e.
frustum culling).
In the CPU method, frustum culling is performed through the structure `VisibilityTester` using the model/view matrix.
An on-CPU array is modified each frame, and then pushed to the GPU through a staging buffer.
In the GPU method, a "compute shader" is called.
Each invocation of the "compute shader" corresponds to a `VkDrawIndexedIndirectCommand` struct, and the bounding sphere is queried from an SSBO (`ModelInformationBuffer`).
To determine whether that model is drawn, the instance count is toggled between 0 and 1.
The GPU is entirely responsible for generating the draw calls apart from the initial set up of the draw command buffer, which is performed by the GPU.
The GPU method using buffer device address is similar to the standard GPU method, but with an additional feature: the starting address of the `VkDrawIndexedIndirectCommand` array is provided using `buffer_reference`.
The advantage of this method is that each invocation of the culling compute shader can point to a different indirect command array without needing to change descriptor sets if the camera information and buffer address is provided through push constants.
This allows culling of the next frame to occur prior to completion of rendering of the current frame with minimal overhead.
== Texture / Resource Access
One of the biggest advantages of GPU rendering and draw call generation is the elimination of binding calls.
Rather than re-binding descriptor sets for textures or other resources with each model, an array can be used.
In this sample, the textures of all sub-meshes are placed into an indexed array, and the `ModelInformationBuffer` is used to determine the correct index of the texture.
This allows rendering of the entire scene without requiring different textures to be bound before each render call.
@@ -0,0 +1,963 @@
/* Copyright (c) 2021-2025, Holochip Corporation
*
* 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 "multi_draw_indirect.h"
#include "gltf_loader.h"
#include "ktx.h"
#include "scene_graph/components/camera.h"
#include "scene_graph/components/image.h"
#include "scene_graph/components/material.h"
#include "scene_graph/components/mesh.h"
#include "scene_graph/components/pbr_material.h"
namespace
{
template <typename T>
struct CopyBuffer
{
std::vector<T> operator()(std::unordered_map<std::string, vkb::core::BufferC> &buffers, const char *bufferName)
{
auto iter = buffers.find(bufferName);
if (iter == buffers.cend())
{
return {};
}
auto &buffer = iter->second;
std::vector<T> out;
const size_t sz = buffer.get_size();
out.resize(sz / sizeof(T));
const bool alreadyMapped = buffer.get_data() != nullptr;
if (!alreadyMapped)
{
buffer.map();
}
memcpy(&out[0], buffer.get_data(), sz);
if (!alreadyMapped)
{
buffer.unmap();
}
return out;
}
};
} // namespace
MultiDrawIndirect::MultiDrawIndirect()
{
set_api_version(VK_API_VERSION_1_2);
add_device_extension(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, true /* optional */);
}
MultiDrawIndirect::~MultiDrawIndirect()
{
if (has_device())
{
vertex_buffer.reset();
index_buffer.reset();
model_information_buffer.reset();
scene_uniform_buffer.reset();
vkDestroyPipeline(get_device().get_handle(), pipeline, VK_NULL_HANDLE);
vkDestroyPipelineLayout(get_device().get_handle(), pipeline_layout, VK_NULL_HANDLE);
vkDestroyDescriptorSetLayout(get_device().get_handle(), descriptor_set_layout, VK_NULL_HANDLE);
vkDestroySampler(get_device().get_handle(), sampler_linear, VK_NULL_HANDLE);
vkDestroySampler(get_device().get_handle(), sampler_nearest, VK_NULL_HANDLE);
vkDestroyPipeline(get_device().get_handle(), gpu_cull_pipeline, VK_NULL_HANDLE);
vkDestroyPipelineLayout(get_device().get_handle(), gpu_cull_pipeline_layout, VK_NULL_HANDLE);
vkDestroyDescriptorSetLayout(get_device().get_handle(), gpu_cull_descriptor_set_layout, VK_NULL_HANDLE);
vkDestroyPipeline(get_device().get_handle(), device_address_pipeline, VK_NULL_HANDLE);
vkDestroyPipelineLayout(get_device().get_handle(), device_address_pipeline_layout, VK_NULL_HANDLE);
vkDestroyDescriptorSetLayout(get_device().get_handle(), device_address_descriptor_set_layout, VK_NULL_HANDLE);
device_address_buffer.reset();
cpu_staging_buffer.reset();
indirect_call_buffer.reset();
}
}
void MultiDrawIndirect::request_gpu_features(vkb::PhysicalDevice &gpu)
{
if (gpu.get_features().multiDrawIndirect)
{
gpu.get_mutable_requested_features().multiDrawIndirect = VK_TRUE;
m_supports_mdi = true;
}
if (gpu.get_features().drawIndirectFirstInstance)
{
gpu.get_mutable_requested_features().drawIndirectFirstInstance = VK_TRUE;
m_supports_first_instance = true;
}
// Query whether the device supports buffer device addresses
m_supports_buffer_device =
REQUEST_OPTIONAL_FEATURE(gpu, VkPhysicalDeviceVulkan12Features, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, bufferDeviceAddress);
// This sample references 128 objects. We need to check whether this is supported by the device
VkPhysicalDeviceProperties physical_device_properties;
vkGetPhysicalDeviceProperties(gpu.get_handle(), &physical_device_properties);
if (physical_device_properties.limits.maxPerStageDescriptorSamplers < 128)
{
throw std::runtime_error(fmt::format(FMT_STRING("This sample requires at least 128 descriptor samplers, but device only supports {:d}"), physical_device_properties.limits.maxPerStageDescriptorSamplers));
}
}
void MultiDrawIndirect::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 = {1.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 (size_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));
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);
vkCmdBindPipeline(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
vkCmdBindDescriptorSets(draw_cmd_buffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set, 0, nullptr);
VkDeviceSize offsets[1] = {0};
vkCmdBindIndexBuffer(draw_cmd_buffers[i], index_buffer->get_handle(), 0, VK_INDEX_TYPE_UINT16);
vkCmdBindVertexBuffers(draw_cmd_buffers[i], 0, 1, vertex_buffer->get(), offsets);
vkCmdBindVertexBuffers(draw_cmd_buffers[i], 1, 1, model_information_buffer->get(), offsets);
if (m_enable_mdi && m_supports_mdi)
{
vkCmdDrawIndexedIndirect(draw_cmd_buffers[i], indirect_call_buffer->get_handle(), 0, static_cast<uint32_t>(cpu_commands.size()), sizeof(cpu_commands[0]));
}
else
{
for (size_t j = 0; j < cpu_commands.size(); ++j)
{
vkCmdDrawIndexedIndirect(draw_cmd_buffers[i], indirect_call_buffer->get_handle(), j * sizeof(cpu_commands[0]), 1, sizeof(cpu_commands[0]));
}
}
draw_ui(draw_cmd_buffers[i]);
vkCmdEndRenderPass(draw_cmd_buffers[i]);
VK_CHECK(vkEndCommandBuffer(draw_cmd_buffers[i]));
}
}
void MultiDrawIndirect::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("GPU Rendering"))
{
static const std::array<const char *, 2> supported = {"Not supported", "Supported"};
drawer.text("Multi-Draw Indirect: %s", supported[this->m_supports_mdi]);
drawer.text("drawIndirectFirstInstance: %s", supported[this->m_supports_first_instance]);
drawer.text("Device buffer address: %s", supported[this->m_supports_buffer_device]);
drawer.text("");
uint32_t instance_count = 0;
if (render_mode == RenderMode::GPU || render_mode == RenderMode::GPU_DEVICE_ADDRESS)
{
// copy over the GPU-culled data to the CPU command so that we can count the number of instances
assert(!!indirect_call_buffer && !!cpu_staging_buffer && indirect_call_buffer->get_size() == cpu_staging_buffer->get_size());
assert(cpu_commands.size() * sizeof(cpu_commands[0]) == cpu_staging_buffer->get_size());
auto cmd = get_device().get_command_pool().request_command_buffer();
cmd->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
cmd->copy_buffer(*indirect_call_buffer, *cpu_staging_buffer, cpu_staging_buffer->get_size());
cmd->end();
auto &queue = get_device().get_queue_by_flags(VK_QUEUE_COMPUTE_BIT, 0);
queue.submit(*cmd, get_device().get_fence_pool().request_fence());
get_device().get_fence_pool().wait();
memcpy(cpu_commands.data(), cpu_staging_buffer->get_data(), cpu_staging_buffer->get_size());
}
for (auto &&cmd : cpu_commands)
{
instance_count += cmd.instanceCount;
}
drawer.text("Instances: %d / %d", instance_count, 256);
m_requires_rebuild |= drawer.checkbox("Enable multi-draw", &m_enable_mdi);
drawer.checkbox("Freeze culling", &m_freeze_cull);
int32_t render_selection = render_mode;
if (drawer.combo_box("Cull mode", &render_selection, {"CPU", "GPU", "GPU Device Address"}))
{
m_requires_rebuild = true;
render_mode = static_cast<RenderMode>(render_selection);
}
}
}
void MultiDrawIndirect::create_samplers()
{
VkSamplerCreateInfo sampler_info = vkb::initializers::sampler_create_info();
sampler_info.magFilter = VK_FILTER_LINEAR;
sampler_info.minFilter = VK_FILTER_LINEAR;
sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
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, &sampler_linear));
// Some formats don't support linear filtering, so create a nearest filtered sampler as a fallback
sampler_info.magFilter = VK_FILTER_NEAREST;
sampler_info.minFilter = VK_FILTER_NEAREST;
sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
VK_CHECK(vkCreateSampler(get_device().get_handle(), &sampler_info, nullptr, &sampler_nearest));
}
bool MultiDrawIndirect::prepare(const vkb::ApplicationOptions &options)
{
if (!ApiVulkanSample::prepare(options))
{
return false;
}
camera.type = vkb::CameraType::FirstPerson;
camera.set_perspective(60.0f, static_cast<float>(width) / static_cast<float>(height), 0.001f, 512.0f);
camera.set_rotation(glm::vec3(-23.5, -45, 0));
camera.set_translation(glm::vec3(0, 0.5, -0.2));
if (!compute_queue)
{
compute_queue = &get_device().get_queue_by_flags(VK_QUEUE_COMPUTE_BIT, 0);
}
queue_families.clear();
for (auto &&queue_bit : {VK_QUEUE_GRAPHICS_BIT, VK_QUEUE_COMPUTE_BIT})
{
const auto index = get_device().get_queue_by_flags(queue_bit, 0).get_family_index();
if (std::find(queue_families.cbegin(), queue_families.cend(), index) == queue_families.cend())
{
queue_families.emplace_back(index);
}
}
create_samplers();
load_scene();
initialize_resources();
update_scene_uniform();
create_pipeline();
create_compute_pipeline();
initialize_descriptors();
build_command_buffers();
cpu_cull(); // initialize buffer
run_cull();
prepared = true;
return true;
}
void MultiDrawIndirect::load_scene()
{
const std::string scene_path = "scenes/vokselia/";
ApiVulkanSample::load_scene(scene_path + "vokselia.gltf");
assert(has_scene());
for (auto &&mesh : get_scene().get_components<vkb::sg::Mesh>())
{
const size_t texture_index = textures.size();
const auto &short_name = mesh->get_name();
auto image_name = scene_path + short_name + ".ktx";
auto image = vkb::sg::Image::load(image_name, image_name, vkb::sg::Image::Color);
image->create_vk_image(get_device());
Texture texture;
texture.n_mip_maps = static_cast<uint32_t>(image->get_mipmaps().size());
assert(texture.n_mip_maps == 1);
texture.image = std::make_unique<vkb::core::Image>(get_device(),
image->get_extent(),
image->get_format(),
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
VMA_MEMORY_USAGE_GPU_ONLY,
VK_SAMPLE_COUNT_1_BIT,
1,
1,
VK_IMAGE_TILING_OPTIMAL,
0);
auto data_buffer = vkb::core::BufferC::create_staging_buffer(get_device(), image->get_data());
auto texture_cmd = get_device().get_command_pool().request_command_buffer();
texture_cmd->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, VK_NULL_HANDLE);
VkImageSubresourceRange subresource_range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
subresource_range.baseMipLevel = 0;
subresource_range.levelCount = texture.n_mip_maps;
vkb::image_layout_transition(
texture_cmd->get_handle(), texture.image->get_handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, subresource_range);
auto offsets = image->get_offsets();
VkBufferImageCopy region = {};
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;
region.imageExtent = image->get_extent();
region.bufferOffset = offsets[0][0];
texture_cmd->copy_buffer_to_image(data_buffer, *texture.image, {region});
texture_cmd->end();
auto &queue = get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
queue.submit(*texture_cmd, get_device().get_fence_pool().request_fence());
get_device().get_fence_pool().wait();
get_device().get_fence_pool().reset();
texture.image_view = std::make_unique<vkb::core::ImageView>(*texture.image, VK_IMAGE_VIEW_TYPE_2D);
// Get the properties for the image format. We'll need to check whether a linear sampler is valid.
const VkFormatProperties fmtProps = get_device().get_gpu().get_format_properties(image->get_format());
VkDescriptorImageInfo image_descriptor;
image_descriptor.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
image_descriptor.imageView = texture.image_view->get_handle();
image_descriptor.sampler = (fmtProps.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) ?
sampler_linear :
sampler_nearest;
image_descriptors.push_back(image_descriptor);
textures.emplace_back(std::move(texture));
for (auto &&sub_mesh : mesh->get_submeshes())
{
SceneModel model;
model.texture_index = texture_index;
auto pts = CopyBuffer<glm::vec3>{}(sub_mesh->vertex_buffers, "position");
auto uvs = CopyBuffer<glm::vec2>{}(sub_mesh->vertex_buffers, "texcoord_0");
assert(uvs.size() == pts.size());
model.vertices.resize(pts.size());
for (size_t i = 0; i < pts.size(); ++i)
{
model.vertices[i].pt = {pts[i].x, -pts[i].y, pts[i].z};
model.vertices[i].uv = uvs[i];
}
assert(sub_mesh->index_type == VK_INDEX_TYPE_UINT16);
auto buffer = sub_mesh->index_buffer.get();
if (buffer)
{
const size_t sz = buffer->get_size();
const size_t nTriangles = sz / sizeof(uint16_t) / 3;
model.triangles.resize(nTriangles);
auto ptr = buffer->get_data();
assert(!!ptr);
std::vector<uint16_t> temp_buffer(nTriangles * 3);
memcpy(temp_buffer.data(), ptr, nTriangles * 3 * sizeof(temp_buffer[0]));
model.triangles.resize(nTriangles);
for (size_t i = 0; i < nTriangles; ++i)
{
model.triangles[i] = {
static_cast<uint16_t>(temp_buffer[3 * i]),
static_cast<uint16_t>(temp_buffer[3 * i + 1]),
static_cast<uint16_t>(temp_buffer[3 * i + 2])};
}
}
model.bounding_sphere = BoundingSphere(pts);
models.emplace_back(std::move(model));
}
}
std::vector<std::pair<VkImage, VkImageSubresourceRange>> imagesAndRanges;
imagesAndRanges.reserve(textures.size());
for (auto const &texture : textures)
{
imagesAndRanges.emplace_back(
std::make_pair(texture.image->get_handle(), VkImageSubresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, 0, texture.n_mip_maps, 0, 1}));
}
auto cmd = get_device().get_command_pool().request_command_buffer();
cmd->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, VK_NULL_HANDLE);
vkb::image_layout_transition(cmd->get_handle(), imagesAndRanges, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
cmd->end();
auto &queue = get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
queue.submit(*cmd, get_device().get_fence_pool().request_fence());
get_device().get_fence_pool().wait();
}
void MultiDrawIndirect::initialize_resources()
{
size_t vertex_buffer_size = 0, index_buffer_size = 0;
const size_t model_buffer_size = models.size() * sizeof(SceneModel);
for (auto &&model : models)
{
model.vertex_buffer_offset = vertex_buffer_size;
model.index_buffer_offset = index_buffer_size;
vertex_buffer_size += model.vertices.size() * sizeof(Vertex);
index_buffer_size += model.triangles.size() * sizeof(model.triangles[0]);
}
assert(vertex_buffer_size && index_buffer_size && model_buffer_size);
auto staging_vertex_buffer = vkb::core::BufferC::create_staging_buffer(get_device(), vertex_buffer_size, nullptr);
auto staging_index_buffer = vkb::core::BufferC::create_staging_buffer(get_device(), index_buffer_size, nullptr);
auto staging_model_buffer = vkb::core::BufferC::create_staging_buffer(get_device(), model_buffer_size, nullptr);
// We will store the GPU commands in the indirect call buffer
constexpr auto default_indirect_flags = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
auto indirect_flags = default_indirect_flags;
if (m_supports_buffer_device)
{
indirect_flags |= VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;
}
indirect_call_buffer = std::make_unique<vkb::core::BufferC>(get_device(), models.size() * sizeof(VkDrawIndexedIndirectCommand), indirect_flags, VMA_MEMORY_USAGE_GPU_ONLY, VMA_ALLOCATION_CREATE_MAPPED_BIT, queue_families);
// Create a buffer containing the addresses of the indirect calls.
// In this sample, the order of the addresses will match that of the other buffers, but in general they could be in any order
const size_t address_buffer_size = sizeof(VkDeviceAddress);
auto staging_address_buffer = std::make_unique<vkb::core::BufferC>(get_device(), address_buffer_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
if (m_supports_buffer_device)
{
auto *destPtr = (uint64_t *) staging_address_buffer->get_data();
VkBufferDeviceAddressInfoKHR address_info{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO_KHR};
address_info.buffer = indirect_call_buffer->get_handle();
VkDeviceAddress srcPtr = vkGetBufferDeviceAddressKHR(get_device().get_handle(), &address_info);
*destPtr = srcPtr;
}
for (size_t i = 0; i < models.size(); ++i)
{
auto &model = models[i];
staging_vertex_buffer.update(model.vertices.data(), model.vertices.size() * sizeof(Vertex), model.vertex_buffer_offset);
staging_index_buffer.update(model.triangles.data(), model.triangles.size() * sizeof(model.triangles[0]), model.index_buffer_offset);
GpuModelInformation model_information;
model_information.bounding_sphere_center = model.bounding_sphere.center;
model_information.bounding_sphere_radius = model.bounding_sphere.radius;
model_information.texture_index = static_cast<uint32_t>(model.texture_index);
model_information.firstIndex = static_cast<uint32_t>(model.index_buffer_offset / (sizeof(model.triangles[0][0])));
model_information.indexCount = static_cast<uint32_t>(model.triangles.size());
staging_model_buffer.update(&model_information, sizeof(GpuModelInformation), i * sizeof(GpuModelInformation));
}
staging_vertex_buffer.flush();
staging_index_buffer.flush();
staging_model_buffer.flush();
auto cmd = get_device().get_command_pool().request_command_buffer();
cmd->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, VK_NULL_HANDLE);
auto copy = [this, &cmd](vkb::core::BufferC &staging_buffer, VkBufferUsageFlags buffer_usage_flags) {
auto output_buffer = std::make_unique<vkb::core::BufferC>(get_device(), staging_buffer.get_size(), buffer_usage_flags | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_GPU_ONLY, VMA_ALLOCATION_CREATE_MAPPED_BIT, queue_families);
cmd->copy_buffer(staging_buffer, *output_buffer, staging_buffer.get_size());
vkb::BufferMemoryBarrier barrier;
barrier.src_stage_mask = VK_PIPELINE_STAGE_TRANSFER_BIT;
barrier.dst_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
barrier.src_access_mask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dst_access_mask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
cmd->buffer_memory_barrier(*output_buffer, 0, VK_WHOLE_SIZE, barrier);
return output_buffer;
};
vertex_buffer = copy(staging_vertex_buffer, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
index_buffer = copy(staging_index_buffer, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
model_information_buffer = copy(staging_model_buffer, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
if (m_supports_buffer_device)
{
// In this sample, we use a staging buffer for the device address buffer (i.e. for device exclusive memory).
// However, since the size of each element (sizeof(uint64_t)) is smaller than the objects it's pointing to, it could instead use host-visible memory
// for fast referencing of the underlying data
device_address_buffer = copy(*staging_address_buffer, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
}
cmd->end();
auto &queue = get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
queue.submit(*cmd, get_device().get_fence_pool().request_fence());
get_device().get_fence_pool().wait();
}
void MultiDrawIndirect::create_pipeline()
{
std::vector<VkDescriptorPoolSize> pool_sizes = {
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 6},
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 * static_cast<uint32_t>(textures.size())},
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 6}};
VkDescriptorPoolCreateInfo descriptor_pool_create_info = vkb::initializers::descriptor_pool_create_info(pool_sizes, 3);
VK_CHECK(vkCreateDescriptorPool(get_device().get_handle(), &descriptor_pool_create_info, nullptr, &descriptor_pool));
// The model information will be used to index textures in the fragment shader,
// as well as perform frustum culling in the compute shader
VkDescriptorSetLayoutBinding model_information_binding{};
model_information_binding.binding = 0;
model_information_binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
model_information_binding.descriptorCount = 1;
model_information_binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
// This array of textures will be accessed via the instance ID
VkDescriptorSetLayoutBinding image_array_binding{};
image_array_binding.binding = 1;
image_array_binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
image_array_binding.descriptorCount = static_cast<uint32_t>(textures.size());
image_array_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
VkDescriptorSetLayoutBinding scene_uniform_binding{};
scene_uniform_binding.binding = 2;
scene_uniform_binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
scene_uniform_binding.descriptorCount = 1;
scene_uniform_binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_COMPUTE_BIT;
VkDescriptorSetLayoutBinding command_buffer_binding{};
command_buffer_binding.binding = 3;
command_buffer_binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
command_buffer_binding.descriptorCount = 1;
command_buffer_binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
// Create descriptors
auto create_descriptors = [this](const std::vector<VkDescriptorSetLayoutBinding> &set_layout_bindings, VkDescriptorSetLayout &_descriptor_set_layout, VkPipelineLayout &_pipeline_layout) {
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));
};
// Render pipeline
std::vector<VkDescriptorSetLayoutBinding> set_layout_bindings = {model_information_binding, image_array_binding, scene_uniform_binding, command_buffer_binding};
create_descriptors(set_layout_bindings, descriptor_set_layout, pipeline_layout);
// Compute pipeline
// Note we don't include the texture array
std::vector<VkDescriptorSetLayoutBinding> gpu_compute_set_layout_bindings = {model_information_binding, scene_uniform_binding, command_buffer_binding};
create_descriptors(gpu_compute_set_layout_bindings, gpu_cull_descriptor_set_layout, gpu_cull_pipeline_layout);
// Device address pipeline
// Note that we don't bind the command buffer directly; instead, we use the references from the device addresses
// This will be used in the device address shader (cull_address.comp)
if (m_supports_buffer_device)
{
VkDescriptorSetLayoutBinding device_address_binding{};
device_address_binding.binding = 4;
device_address_binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
device_address_binding.descriptorCount = 1;
device_address_binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
std::vector<VkDescriptorSetLayoutBinding> device_address_layout_bindings = {model_information_binding, scene_uniform_binding, device_address_binding};
create_descriptors(device_address_layout_bindings, device_address_descriptor_set_layout, device_address_pipeline_layout);
}
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);
VkPipelineDepthStencilStateCreateInfo depth_stencil_state = vkb::initializers::pipeline_depth_stencil_state_create_info(VK_TRUE, VK_TRUE, VK_COMPARE_OP_LESS);
depth_stencil_state.depthBoundsTestEnable = VK_FALSE;
depth_stencil_state.minDepthBounds = 0.f;
depth_stencil_state.maxDepthBounds = 1.f;
VkPipelineViewportStateCreateInfo viewport_state = vkb::initializers::pipeline_viewport_state_create_info(1, 1, 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);
VkPipelineMultisampleStateCreateInfo multisample_state = vkb::initializers::pipeline_multisample_state_create_info(VK_SAMPLE_COUNT_1_BIT, 0);
// 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),
vkb::initializers::vertex_input_binding_description(1, sizeof(GpuModelInformation), VK_VERTEX_INPUT_RATE_INSTANCE),
};
const std::vector<VkVertexInputAttributeDescription> vertex_input_attributes = {
vkb::initializers::vertex_input_attribute_description(0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, pt)),
vkb::initializers::vertex_input_attribute_description(0, 1, VK_FORMAT_R32G32_SFLOAT, offsetof(Vertex, uv)),
vkb::initializers::vertex_input_attribute_description(1, 2, VK_FORMAT_R32G32B32_SFLOAT, offsetof(GpuModelInformation, bounding_sphere_center)),
vkb::initializers::vertex_input_attribute_description(1, 3, VK_FORMAT_R32_SFLOAT, offsetof(GpuModelInformation, bounding_sphere_radius)),
vkb::initializers::vertex_input_attribute_description(1, 4, VK_FORMAT_R32_UINT, offsetof(GpuModelInformation, texture_index)),
};
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;
const std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages = {
load_shader("multi_draw_indirect/multi_draw_indirect.vert.spv", VK_SHADER_STAGE_VERTEX_BIT),
load_shader("multi_draw_indirect/multi_draw_indirect.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT)};
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));
}
void MultiDrawIndirect::create_compute_pipeline()
{
auto create = [this](VkPipelineLayout &layout, VkPipeline &_pipeline, const char *filename) {
VkComputePipelineCreateInfo compute_create_info = vkb::initializers::compute_pipeline_create_info(layout, 0);
compute_create_info.stage = load_shader(filename, VK_SHADER_STAGE_COMPUTE_BIT);
VK_CHECK(vkCreateComputePipelines(get_device().get_handle(), pipeline_cache, 1, &compute_create_info, nullptr, &_pipeline));
};
create(gpu_cull_pipeline_layout, gpu_cull_pipeline, "multi_draw_indirect/cull.comp.spv");
if (m_supports_buffer_device)
{
create(device_address_pipeline_layout, device_address_pipeline, "multi_draw_indirect/cull_address.comp.spv");
}
}
void MultiDrawIndirect::initialize_descriptors()
{
enum class Target
{
RenderPipeline,
ComputePipeline,
AddressPipeline
};
auto bind = [this](VkDescriptorSet &_descriptor_set, VkDescriptorSetLayout &_descriptor_set_layout, Target target) {
VkDescriptorSetAllocateInfo descriptor_set_allocate_info = vkb::initializers::descriptor_set_allocate_info(descriptor_pool, &_descriptor_set_layout, 1);
VK_CHECK(vkAllocateDescriptorSets(get_device().get_handle(), &descriptor_set_allocate_info, &_descriptor_set));
VkDescriptorBufferInfo model_buffer_descriptor = create_descriptor(*model_information_buffer);
VkWriteDescriptorSet model_write = vkb::initializers::write_descriptor_set(_descriptor_set, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0, &model_buffer_descriptor, 1);
VkWriteDescriptorSet texture_array_write = vkb::initializers::write_descriptor_set(_descriptor_set, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, image_descriptors.data(), static_cast<uint32_t>(image_descriptors.size()));
VkDescriptorBufferInfo scene_descriptor = create_descriptor(*scene_uniform_buffer);
VkWriteDescriptorSet scene_write = vkb::initializers::write_descriptor_set(_descriptor_set, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2, &scene_descriptor, 1);
VkDescriptorBufferInfo draw_command_descriptor = create_descriptor(*indirect_call_buffer);
VkWriteDescriptorSet draw_command_write = vkb::initializers::write_descriptor_set(_descriptor_set, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3, &draw_command_descriptor, 1);
VkDescriptorBufferInfo device_address_descriptor;
VkWriteDescriptorSet device_address_write;
if (m_supports_buffer_device && device_address_buffer)
{
device_address_descriptor = create_descriptor(*device_address_buffer);
device_address_write = vkb::initializers::write_descriptor_set(_descriptor_set, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4, &device_address_descriptor, 1);
}
std::vector<VkWriteDescriptorSet> write_descriptor_sets;
switch (target)
{
case Target::RenderPipeline:
write_descriptor_sets = {model_write, texture_array_write, scene_write, draw_command_write};
break;
case Target::ComputePipeline:
write_descriptor_sets = {model_write, scene_write, draw_command_write};
break;
case Target::AddressPipeline:
write_descriptor_sets = {model_write, scene_write, device_address_write};
break;
}
vkUpdateDescriptorSets(get_device().get_handle(), static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, VK_NULL_HANDLE);
};
// render pipeline
bind(descriptor_set, descriptor_set_layout, Target::RenderPipeline);
// compute pipeline
bind(gpu_cull_descriptor_set, gpu_cull_descriptor_set_layout, Target::ComputePipeline);
// Device address pipeline
if (m_supports_buffer_device)
{
bind(device_address_descriptor_set, device_address_descriptor_set_layout, Target::AddressPipeline);
}
}
void MultiDrawIndirect::update_scene_uniform()
{
if (!scene_uniform_buffer)
{
scene_uniform_buffer = std::make_unique<vkb::core::BufferC>(
get_device(), sizeof(SceneUniform), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU, VMA_ALLOCATION_CREATE_MAPPED_BIT, queue_families);
}
scene_uniform.proj = camera.matrices.perspective;
scene_uniform.view = camera.matrices.view;
scene_uniform.proj_view = scene_uniform.proj * scene_uniform.view;
scene_uniform.model_count = static_cast<uint32_t>(models.size());
scene_uniform_buffer->update(&scene_uniform, sizeof(scene_uniform), 0);
scene_uniform_buffer->flush();
}
void MultiDrawIndirect::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 MultiDrawIndirect::render(float delta_time)
{
if (!prepared)
{
return;
}
if (render_mode == GPU_DEVICE_ADDRESS && !m_supports_buffer_device)
{
render_mode = GPU;
}
if (m_requires_rebuild)
{
rebuild_command_buffers();
m_requires_rebuild = false;
}
draw();
update_scene_uniform();
if (!m_freeze_cull)
{
run_cull();
}
get_device().get_fence_pool().wait();
get_device().get_fence_pool().reset();
}
void MultiDrawIndirect::finish()
{
}
void MultiDrawIndirect::run_cull()
{
switch (render_mode)
{
case RenderMode::CPU:
cpu_cull();
break;
case RenderMode::GPU:
case RenderMode::GPU_DEVICE_ADDRESS:
run_gpu_cull();
break;
}
}
void MultiDrawIndirect::run_gpu_cull()
{
assert(!!gpu_cull_pipeline);
auto cmd = get_device().create_command_buffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY);
VkCommandBufferBeginInfo begin = vkb::initializers::command_buffer_begin_info();
vkBeginCommandBuffer(cmd, &begin);
auto bind = [&cmd](VkPipeline &_pipeline, VkPipelineLayout &_pipeline_layout, VkDescriptorSet &_descriptor_set) {
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, _pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, _pipeline_layout, 0, 1, &_descriptor_set, 0, nullptr);
};
if (render_mode == RenderMode::GPU)
{
bind(gpu_cull_pipeline, gpu_cull_pipeline_layout, gpu_cull_descriptor_set);
}
else
{
bind(device_address_pipeline, device_address_pipeline_layout, device_address_descriptor_set);
}
const uint32_t dispatch_x = !models.empty() ? 1 + static_cast<uint32_t>((models.size() - 1) / 64) : 1;
vkCmdDispatch(cmd, dispatch_x, 1, 1);
vkEndCommandBuffer(cmd);
VkSubmitInfo submit = vkb::initializers::submit_info();
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
vkQueueSubmit(compute_queue->get_handle(), 1, &submit, get_device().get_fence_pool().request_fence());
get_device().get_fence_pool().wait();
get_device().get_fence_pool().reset();
// we're done so dealloc it from the pool.
vkFreeCommandBuffers(get_device().get_handle(), get_device().get_command_pool().get_handle(), 1, &cmd);
}
namespace
{
/**
* @brief Test for visibility using bounding sphere.
* See https://www.gamedevs.org/uploads/fast-extraction-viewing-frustum-planes-from-world-view-projection-matrix.pdf
*/
struct VisibilityTester
{
explicit VisibilityTester(glm::mat4x4 view_matrix) :
planes(get_view_planes(view_matrix))
{}
std::array<glm::vec4, 6> planes;
static std::array<glm::vec4, 6> get_view_planes(const glm::mat4x4 &mat)
{
using namespace glm;
std::array<vec4, 6> out{};
for (auto i = 0; i < 3; ++i)
{
for (size_t j = 0; j < 2; ++j)
{
const float sign = j ? 1.f : -1.f;
for (auto k = 0; k < 4; ++k)
{
out[2 * i + j][k] = mat[k][3] + sign * mat[k][i];
}
}
}
// normalize plane; see Appendix A.2
for (auto &&plane : out)
{
plane /= static_cast<float>(length(vec3(plane.xyz)));
}
return out;
}
bool is_visible(glm::vec3 origin, float radius) const
{
using namespace glm;
std::array<int, 4> V{0, 1, 4, 5};
return std::ranges::all_of(V, [this, origin, radius](size_t i) {
const auto &plane = planes[i];
return dot(origin, vec3(plane.xyz)) + plane.w + radius >= 0;
});
}
};
} // namespace
void MultiDrawIndirect::cpu_cull()
{
cpu_commands.resize(models.size());
VisibilityTester tester(scene_uniform.proj * scene_uniform.view);
for (size_t i = 0; i < models.size(); ++i)
{
// we control visibility by changing the instance count
auto &model = models[i];
VkDrawIndexedIndirectCommand cmd{};
cmd.firstIndex = static_cast<uint32_t>(model.index_buffer_offset / (sizeof(model.triangles[0][0])));
cmd.indexCount = static_cast<uint32_t>(model.triangles.size()) * 3;
cmd.vertexOffset = static_cast<int32_t>(model.vertex_buffer_offset / sizeof(Vertex));
cmd.firstInstance = static_cast<uint32_t>(i);
cmd.instanceCount = tester.is_visible(model.bounding_sphere.center, model.bounding_sphere.radius);
cpu_commands[i] = cmd;
}
const auto call_buffer_size = cpu_commands.size() * sizeof(cpu_commands[0]);
assert(!!indirect_call_buffer && indirect_call_buffer->get_size() == call_buffer_size);
if (!cpu_staging_buffer || cpu_staging_buffer->get_size() != call_buffer_size)
{
cpu_staging_buffer = std::make_unique<vkb::core::BufferC>(get_device(), models.size() * sizeof(VkDrawIndexedIndirectCommand), VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
}
cpu_staging_buffer->update(cpu_commands.data(), call_buffer_size, 0);
cpu_staging_buffer->flush();
auto transfer_cmd = get_device().get_command_pool().request_command_buffer();
transfer_cmd->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, VK_NULL_HANDLE);
transfer_cmd->copy_buffer(*cpu_staging_buffer, *indirect_call_buffer, call_buffer_size);
transfer_cmd->end();
auto &queue = get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
queue.submit(*transfer_cmd, get_device().get_fence_pool().request_fence());
get_device().get_fence_pool().wait();
}
std::unique_ptr<vkb::VulkanSampleC> create_multi_draw_indirect()
{
return std::make_unique<MultiDrawIndirect>();
}
MultiDrawIndirect::BoundingSphere::BoundingSphere(const std::vector<glm::vec3> &pts)
{
if (pts.empty())
{
return;
}
// This is a simple method of calculating a bounding sphere.
// For finding an optimal bounding sphere, see Welzl's algorithm
this->center = {0, 0, 0};
for (auto &&pt : pts)
{
this->center += pt;
}
this->center /= static_cast<float>(pts.size());
this->radius = glm::distance2(pts[0], this->center);
for (size_t i = 1; i < pts.size(); ++i)
{
this->radius = std::max(this->radius, glm::distance2(pts[i], this->center));
}
this->radius = std::nextafter(sqrtf(this->radius), std::numeric_limits<float>::max());
}
@@ -0,0 +1,153 @@
/* Copyright (c) 2021-2025, Holochip Corporation
*
* 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"
/**
* @brief Offloading processes from CPU to GPU
*/
class MultiDrawIndirect : public ApiVulkanSample
{
public:
MultiDrawIndirect();
~MultiDrawIndirect() override;
bool prepare(const vkb::ApplicationOptions &options) override;
void render(float delta_time) override;
void finish() override;
private:
enum RenderMode
{
CPU,
GPU,
GPU_DEVICE_ADDRESS
} render_mode = GPU;
struct Vertex
{
glm::vec3 pt;
glm::vec2 uv;
};
struct BoundingSphere
{
BoundingSphere() = default;
explicit BoundingSphere(const std::vector<glm::vec3> &pts);
glm::vec3 center = {0, 0, 0};
float radius = 0;
};
struct GpuModelInformation
{
glm::vec3 bounding_sphere_center;
float bounding_sphere_radius;
uint32_t texture_index = 0;
uint32_t firstIndex = 0;
uint32_t indexCount = 0;
uint32_t _pad = 0;
};
struct SceneUniform
{
glm::mat4 view;
glm::mat4 proj;
glm::mat4 proj_view;
uint32_t model_count;
} scene_uniform;
struct SceneModel
{
std::vector<Vertex> vertices;
std::vector<std::array<uint16_t, 3>> triangles;
size_t vertex_buffer_offset = 0;
size_t index_buffer_offset = 0;
size_t texture_index = 0;
BoundingSphere bounding_sphere;
};
struct Texture
{
std::unique_ptr<vkb::core::Image> image;
std::unique_ptr<vkb::core::ImageView> image_view;
uint32_t n_mip_maps;
};
std::vector<SceneModel> models;
std::unique_ptr<vkb::core::BufferC> vertex_buffer;
std::unique_ptr<vkb::core::BufferC> index_buffer;
std::unique_ptr<vkb::core::BufferC> model_information_buffer;
std::unique_ptr<vkb::core::BufferC> scene_uniform_buffer;
std::vector<Texture> textures;
std::vector<VkDescriptorImageInfo> image_descriptors;
bool m_freeze_cull = false;
bool m_enable_mdi = true;
bool m_requires_rebuild = false;
VkPipeline pipeline{VK_NULL_HANDLE};
VkPipelineLayout pipeline_layout{VK_NULL_HANDLE};
VkDescriptorSetLayout descriptor_set_layout{VK_NULL_HANDLE};
VkDescriptorSet descriptor_set{VK_NULL_HANDLE};
VkSampler sampler_linear{VK_NULL_HANDLE};
VkSampler sampler_nearest{VK_NULL_HANDLE};
// GPU Draw Calls
void run_cull();
void run_gpu_cull();
VkPipeline gpu_cull_pipeline{VK_NULL_HANDLE};
VkPipelineLayout gpu_cull_pipeline_layout{VK_NULL_HANDLE};
VkDescriptorSetLayout gpu_cull_descriptor_set_layout{VK_NULL_HANDLE};
VkDescriptorSet gpu_cull_descriptor_set{VK_NULL_HANDLE};
// Device Address
VkPipeline device_address_pipeline{VK_NULL_HANDLE};
VkPipelineLayout device_address_pipeline_layout{VK_NULL_HANDLE};
VkDescriptorSetLayout device_address_descriptor_set_layout{VK_NULL_HANDLE};
VkDescriptorSet device_address_descriptor_set{VK_NULL_HANDLE};
std::unique_ptr<vkb::core::BufferC> device_address_buffer{nullptr};
std::vector<vkb::core::CommandBufferC> compute_command_buffers{};
const vkb::Queue *compute_queue{nullptr};
std::vector<uint32_t> queue_families;
// CPU Draw Calls
void cpu_cull();
std::vector<VkDrawIndexedIndirectCommand> cpu_commands;
std::unique_ptr<vkb::core::BufferC> cpu_staging_buffer;
std::unique_ptr<vkb::core::BufferC> indirect_call_buffer;
void request_gpu_features(vkb::PhysicalDevice &gpu) override;
void build_command_buffers() override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
void create_samplers();
void load_scene();
void initialize_resources();
void create_pipeline();
void create_compute_pipeline();
void initialize_descriptors();
void update_scene_uniform();
void draw();
bool m_supports_mdi = false;
bool m_supports_first_instance = false;
bool m_supports_buffer_device = false;
};
std::unique_ptr<vkb::VulkanSampleC> create_multi_draw_indirect();