881 lines
36 KiB
C++
881 lines
36 KiB
C++
/* 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, ©_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>();
|
|
}
|