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,914 @@
/* Copyright (c) 2021-2025, Arm Limited and Contributors
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "async_compute.h"
#include "api_vulkan_sample.h"
#include "common/vk_common.h"
#include "filesystem/legacy.h"
#include "gltf_loader.h"
#include "gui.h"
#include "scene_graph/components/orthographic_camera.h"
#include "stats/stats.h"
AsyncComputeSample::AsyncComputeSample()
{
auto &config = get_configuration();
config.insert<vkb::BoolSetting>(0, async_enabled, false);
config.insert<vkb::BoolSetting>(1, async_enabled, true);
config.insert<vkb::BoolSetting>(0, rotate_shadows, false);
config.insert<vkb::BoolSetting>(1, rotate_shadows, true);
config.insert<vkb::BoolSetting>(0, double_buffer_hdr_frames, false);
config.insert<vkb::BoolSetting>(1, double_buffer_hdr_frames, true);
}
void AsyncComputeSample::request_gpu_features(vkb::PhysicalDevice &gpu)
{
#ifdef VKB_ENABLE_PORTABILITY
// Since sampler_info.compareEnable = VK_TRUE, must enable the mutableComparisonSamplers feature of VK_KHR_portability_subset
REQUEST_REQUIRED_FEATURE(
gpu, VkPhysicalDevicePortabilitySubsetFeaturesKHR, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PORTABILITY_SUBSET_FEATURES_KHR, mutableComparisonSamplers);
#endif
}
void AsyncComputeSample::draw_gui()
{
get_gui().show_options_window(
/* body = */ [this]() {
ImGui::Checkbox("Enable async queues", &async_enabled);
ImGui::Checkbox("Double buffer HDR", &double_buffer_hdr_frames);
ImGui::Checkbox("Rotate shadows", &rotate_shadows);
},
/* lines = */ 3);
}
static VkExtent3D downsample_extent(const VkExtent3D &extent, uint32_t level)
{
return {
std::max(1u, extent.width >> level),
std::max(1u, extent.height >> level),
std::max(1u, extent.depth >> level)};
}
void AsyncComputeSample::prepare_render_targets()
{
// To make this sample demanding enough to saturate the tested mobile devices, use 4K.
// Could base this off the swapchain extent, but comparing cross-device performance
// could get awkward.
VkExtent3D size = {3840, 2160, 1};
// Support double-buffered HDR.
vkb::core::Image color_targets[2]{
{get_device(), size, VK_FORMAT_R16G16B16A16_SFLOAT,
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY},
{get_device(), size, VK_FORMAT_R16G16B16A16_SFLOAT,
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY},
};
color_targets[0].set_debug_name("color_targets[0]");
color_targets[1].set_debug_name("color_targets[1]");
// Should only really need one depth target, but vkb::RenderTarget needs to own the resource.
vkb::core::Image depth_targets[2]{
{get_device(), size, VK_FORMAT_D32_SFLOAT,
VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY},
{get_device(), size, VK_FORMAT_D32_SFLOAT,
VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY},
};
depth_targets[0].set_debug_name("depth_targets[0]");
depth_targets[1].set_debug_name("depth_targets[1]");
// 8K shadow-map overkill to stress devices.
// Min-spec is 4K however, so clamp to that if required.
VkExtent3D shadow_resolution{8 * 1024, 8 * 1024, 1};
VkImageFormatProperties depth_properties{};
vkGetPhysicalDeviceImageFormatProperties(get_device().get_gpu().get_handle(), VK_FORMAT_D16_UNORM, VK_IMAGE_TYPE_2D,
VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
0, &depth_properties);
shadow_resolution.width = std::min(depth_properties.maxExtent.width, shadow_resolution.width);
shadow_resolution.height = std::min(depth_properties.maxExtent.height, shadow_resolution.height);
shadow_resolution.width = std::min(get_device().get_gpu().get_properties().limits.maxFramebufferWidth, shadow_resolution.width);
shadow_resolution.height = std::min(get_device().get_gpu().get_properties().limits.maxFramebufferHeight, shadow_resolution.height);
vkb::core::Image shadow_target{get_device(), shadow_resolution, VK_FORMAT_D16_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VMA_MEMORY_USAGE_GPU_ONLY};
shadow_target.set_debug_name("shadow_target");
// Create a simple mip-chain used for bloom blur.
// Could technically mip-map the HDR target,
// but there's no real reason to do it like that.
for (uint32_t level = 1; level < 7; level++)
{
blur_chain.push_back(std::make_unique<vkb::core::Image>(
get_device(), downsample_extent(size, level),
VK_FORMAT_R16G16B16A16_SFLOAT,
VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VMA_MEMORY_USAGE_GPU_ONLY));
blur_chain_views.push_back(std::make_unique<vkb::core::ImageView>(
*blur_chain.back(), VK_IMAGE_VIEW_TYPE_2D));
}
// Calculate valid filter
VkFilter filter = VK_FILTER_LINEAR;
vkb::make_filters_valid(get_device().get_gpu().get_handle(), depth_targets[0].get_format(), &filter);
auto sampler_info = vkb::initializers::sampler_create_info();
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.minFilter = filter;
sampler_info.magFilter = filter;
sampler_info.maxLod = VK_LOD_CLAMP_NONE;
linear_sampler = std::make_unique<vkb::core::Sampler>(get_device(), sampler_info);
// Inverse Z, so use GEQ test.
sampler_info.compareOp = VK_COMPARE_OP_GREATER_OR_EQUAL;
sampler_info.compareEnable = VK_TRUE;
comparison_sampler = std::make_unique<vkb::core::Sampler>(get_device(), sampler_info);
for (unsigned i = 0; i < 2; i++)
{
std::vector<vkb::core::Image> color_attachments;
color_attachments.push_back(std::move(color_targets[i]));
color_attachments.push_back(std::move(depth_targets[i]));
forward_render_targets[i] = std::make_unique<vkb::RenderTarget>(std::move(color_attachments));
}
std::vector<vkb::core::Image> shadow_attachments;
shadow_attachments.push_back(std::move(shadow_target));
shadow_render_target = std::make_unique<vkb::RenderTarget>(std::move(shadow_attachments));
}
void AsyncComputeSample::setup_queues()
{
present_graphics_queue = &get_device().get_queue_by_present(0);
last_async_enabled = async_enabled;
// Need to be careful about sync if we're going to suddenly switch to async compute.
get_device().wait_idle();
// The way we set things up here somewhat heavily favors devices where we have 2 or more graphics queues.
// The pipeline we ideally want is:
// - Low priority graphics queue renders the HDR frames
// - Async compute queue does post
// - High priority queue does (HDR + Bloom) tonemap + UI in one graphics pass and presents.
//
// We want to present in the high priority graphics queue since on at least Arm devices,
// we can get pre-emption behavior
// where we can start rendering the next frame in parallel with async compute post,
// but the next frame will not block presentation. This keeps latency low, and
// is important to achieve full utilization of the fragment queue.
// Getting the async queue idle as fast as possible unblocks vertex shading work for the next frame.
// On desktop, in particular on architectures with just one graphics queue, this setup isn't very appealing
// since we cannot have a low and high priority graphics queue.
// We would ideally change the entire pipeline to be geared towards presenting in the async compute queue where
// tonemap + UI happens in compute instead.
// This complicates things since we would have to render UI in a fragment pass, which compute just composites.
// The hardcore alternative is to render the UI entirely in compute, but all of these consideration
// are outside the scope of this sample.
if (async_enabled)
{
const auto &queue_family_properties = get_device().get_gpu().get_queue_family_properties();
uint32_t graphics_family_index = vkb::get_queue_family_index(queue_family_properties, VK_QUEUE_GRAPHICS_BIT);
uint32_t compute_family_index = vkb::get_queue_family_index(queue_family_properties, VK_QUEUE_COMPUTE_BIT);
if (queue_family_properties[graphics_family_index].queueCount >= 2)
{
LOGI("Device has 2 or more graphics queues.");
early_graphics_queue = &get_device().get_queue(graphics_family_index, 1);
}
else
{
LOGI("Device has just 1 graphics queue.");
early_graphics_queue = present_graphics_queue;
}
if (graphics_family_index == compute_family_index)
{
LOGI("Device does not have a dedicated compute queue family.");
post_compute_queue = early_graphics_queue;
}
else
{
LOGI("Device has async compute queue.");
post_compute_queue = &get_device().get_queue(compute_family_index, 0);
}
}
else
{
// Force everything through the same queue.
early_graphics_queue = present_graphics_queue;
post_compute_queue = present_graphics_queue;
}
}
bool AsyncComputeSample::prepare(const vkb::ApplicationOptions &options)
{
// Set setup_queues() for details.
set_high_priority_graphics_queue_enable(true);
if (!VulkanSample::prepare(options))
{
return false;
}
load_scene("scenes/bonza/Bonza.gltf");
auto &camera_node = vkb::add_free_camera(get_scene(), "main_camera", get_render_context().get_surface_extent());
camera = &camera_node.get_component<vkb::sg::Camera>();
// Attach a shadow camera to the directional light.
auto lights = get_scene().get_components<vkb::sg::Light>();
for (auto &light : lights)
{
if (light->get_light_type() == vkb::sg::LightType::Directional)
{
vkb::sg::LightProperties props{};
props.color = glm::vec3(50.0f, 40.0f, 30.0f);
light->set_properties(props);
auto *node = light->get_node();
// Hardcoded to fit to the scene.
auto ortho_camera = std::make_unique<vkb::sg::OrthographicCamera>("shadow_camera",
-2000.0f, 3000.0f,
-2500.0f, 1500.0f,
-2000.0f, 2000.0f);
ortho_camera->set_node(*node);
get_scene().add_component(std::move(ortho_camera), *node);
shadow_camera = &node->get_component<vkb::sg::Camera>();
break;
}
}
prepare_render_targets();
vkb::ShaderSource vert_shader("async_compute/forward.vert.spv");
vkb::ShaderSource frag_shader("async_compute/forward.frag.spv");
auto scene_subpass =
std::make_unique<ShadowMapForwardSubpass>(get_render_context(), std::move(vert_shader), std::move(frag_shader), get_scene(), *camera, *shadow_camera);
vkb::ShaderSource shadow_vert_shader("async_compute/shadow.vert.spv");
vkb::ShaderSource shadow_frag_shader("async_compute/shadow.frag.spv");
auto shadow_scene_subpass =
std::make_unique<DepthMapSubpass>(get_render_context(), std::move(shadow_vert_shader), std::move(shadow_frag_shader), get_scene(), *shadow_camera);
shadow_render_pipeline.add_subpass(std::move(shadow_scene_subpass));
shadow_render_pipeline.set_load_store({{VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE}});
vkb::ShaderSource composite_vert_shader("async_compute/composite.vert.spv");
vkb::ShaderSource composite_frag_shader("async_compute/composite.frag.spv");
auto composite_scene_subpass =
std::make_unique<CompositeSubpass>(get_render_context(), std::move(composite_vert_shader), std::move(composite_frag_shader));
forward_render_pipeline.add_subpass(std::move(scene_subpass));
forward_render_pipeline.set_load_store({{VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE},
{VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_DONT_CARE}});
auto blit_render_pipeline = std::make_unique<vkb::RenderPipeline>();
blit_render_pipeline->add_subpass(std::move(composite_scene_subpass));
blit_render_pipeline->set_load_store({{VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE},
{VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_DONT_CARE}});
set_render_pipeline(std::move(blit_render_pipeline));
vkb::CounterSamplingConfig config;
config.mode = vkb::CounterSamplingMode::Continuous;
get_stats().request_stats({
vkb::StatIndex::frame_times,
vkb::StatIndex::gpu_cycles,
vkb::StatIndex::gpu_vertex_cycles,
vkb::StatIndex::gpu_fragment_cycles,
},
config);
create_gui(*window, &get_stats());
// Store the start time to calculate rotation
start_time = std::chrono::system_clock::now();
auto &threshold_module = get_device().get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource("async_compute/threshold.comp.spv"));
auto &blur_up_module = get_device().get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource("async_compute/blur_up.comp.spv"));
auto &blur_down_module = get_device().get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource("async_compute/blur_down.comp.spv"));
threshold_pipeline = &get_device().get_resource_cache().request_pipeline_layout({&threshold_module});
blur_up_pipeline = &get_device().get_resource_cache().request_pipeline_layout({&blur_up_module});
blur_down_pipeline = &get_device().get_resource_cache().request_pipeline_layout({&blur_down_module});
setup_queues();
return true;
}
void AsyncComputeSample::render_shadow_pass()
{
auto &queue = *early_graphics_queue;
auto command_buffer = get_render_context().get_active_frame().get_command_pool(queue).request_command_buffer();
command_buffer->set_debug_name("shadow_pass");
command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
auto &views = shadow_render_target->get_views();
assert(!views.empty());
{
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
memory_barrier.src_access_mask = 0;
memory_barrier.dst_access_mask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
command_buffer->image_memory_barrier(views[0], memory_barrier);
}
set_viewport_and_scissor(*command_buffer, shadow_render_target->get_extent());
shadow_render_pipeline.draw(*command_buffer, *shadow_render_target, VK_SUBPASS_CONTENTS_INLINE);
command_buffer->end_render_pass();
{
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
memory_barrier.src_access_mask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
memory_barrier.dst_access_mask = VK_ACCESS_SHADER_READ_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
command_buffer->image_memory_barrier(views[0], memory_barrier);
}
command_buffer->end();
get_render_context().submit(queue, {command_buffer});
}
vkb::RenderTarget &AsyncComputeSample::get_current_forward_render_target()
{
return *forward_render_targets[forward_render_target_index];
}
VkSemaphore AsyncComputeSample::render_forward_offscreen_pass(VkSemaphore hdr_wait_semaphore)
{
auto &queue = *early_graphics_queue;
auto command_buffer = get_render_context().get_active_frame().get_command_pool(queue).request_command_buffer();
command_buffer->set_debug_name("forward_offscreen_pass");
command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
auto &views = get_current_forward_render_target().get_views();
assert(1 < views.size());
{
// If maintenance9 is not enabled, resources with VK_SHARING_MODE_EXCLUSIVE must only be accessed by queues in the queue family that has ownership of the resource.
// Upon creation resources with VK_SHARING_MODE_EXCLUSIVE are not owned by any queue, ownership is implicitly acquired upon first use.
// The application must perform a queue family ownership transfer if it wishes to make the memory contents of the resource accessible to a different queue family.
// A queue family can take ownership of a resource without an ownership transfer, in the same way as for a resource that was just created, but the content will be undefined.
// We do not need to acquire color_targets[0] from present_graphics to early_graphics
// A queue transfer barrier is not necessary for the resource first access.
// Moreover, in our sample we do not care about the content at this point so we can skip the queue transfer barrier.
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
memory_barrier.src_access_mask = 0;
memory_barrier.dst_access_mask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
command_buffer->image_memory_barrier(views[0], memory_barrier);
}
{
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
memory_barrier.src_access_mask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
memory_barrier.dst_access_mask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
command_buffer->image_memory_barrier(views[1], memory_barrier);
}
set_viewport_and_scissor(*command_buffer, get_current_forward_render_target().get_extent());
forward_render_pipeline.draw(*command_buffer, get_current_forward_render_target(), VK_SUBPASS_CONTENTS_INLINE);
command_buffer->end_render_pass();
const bool queue_family_transfer = early_graphics_queue->get_family_index() != post_compute_queue->get_family_index();
{
// When doing async compute this barrier is used to do a queue family ownership transfer
// release_barrier_0: Releasing color_targets[0] from early_graphics to post_compute
// This release barrier is replicated by the corresponding acquire_barrier_0 in the post_compute queue
// The application must ensure the release operation happens before the acquire operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier{
.src_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.dst_stage_mask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, // Ignored for the release barrier.
// Release barriers ignore dst_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.src_access_mask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
.dst_access_mask = 0, // dst_access_mask is ignored for release barriers, without affecting its validity
.old_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, // We want a layout transition, so the old_layout and new_layout values need to be replicated in the acquire barrier
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = queue_family_transfer ?
early_graphics_queue->get_family_index() :
VK_QUEUE_FAMILY_IGNORED, // Release barriers are executed from a queue of the source queue family
.dst_queue_family = queue_family_transfer ? post_compute_queue->get_family_index() : VK_QUEUE_FAMILY_IGNORED,
};
command_buffer->image_memory_barrier(views[0], memory_barrier);
}
command_buffer->end();
// Conditionally waits on hdr_wait_semaphore.
// This resolves the write-after-read hazard where previous frame tonemap read from HDR buffer.
// We are not using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
// so VK_PIPELINE_STAGE_ALL_COMMANDS_BIT is the only valid stage to wait for queue transfer operations.
const VkPipelineStageFlags wait_stage = queue_family_transfer ? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
auto signal_semaphore = get_render_context().submit(queue, {command_buffer}, hdr_wait_semaphore, wait_stage);
if (hdr_wait_semaphore)
{
get_render_context().release_owned_semaphore(hdr_wait_semaphore);
}
return signal_semaphore;
}
VkSemaphore AsyncComputeSample::render_swapchain(VkSemaphore post_semaphore)
{
auto &queue = *present_graphics_queue;
auto command_buffer = get_render_context().get_active_frame().get_command_pool(queue).request_command_buffer();
command_buffer->set_debug_name("swapchain");
command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
if (post_compute_queue->get_family_index() != present_graphics_queue->get_family_index())
{
// acquire_barrier_1: Acquiring color_targets[0] from post_compute to present_graphics
// This acquire barrier is replicated by the corresponding release_barrier_1 in the post_compute queue
// The application must ensure the acquire operation happens after the release operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier{
.src_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, // Ignored for the acquire barrier.
// Acquire barriers ignore src_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.dst_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
.src_access_mask = 0, // src_access_mask is ignored for acquire barriers, without affecting its validity
.dst_access_mask = VK_ACCESS_SHADER_READ_BIT,
.old_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, // Purely ownership transfer. We do not need a layout transition.
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = post_compute_queue->get_family_index(),
.dst_queue_family = present_graphics_queue->get_family_index(), // Acquire barriers are executed from a queue of the destination queue family
};
command_buffer->image_memory_barrier(get_current_forward_render_target().get_views()[0], memory_barrier);
// acquire_barrier_2: Acquiring blur_chain_views[1] from post_compute to present_graphics
// This acquire barrier is replicated by the corresponding release_barrier_2 in the post_compute queue
// The application must ensure the acquire operation happens after the release operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier_2{
.src_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, // Ignored for the acquire barrier.
// Acquire barriers ignore src_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.dst_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
.src_access_mask = 0, // src_access_mask is ignored for acquire barriers, without affecting its validity
.dst_access_mask = VK_ACCESS_SHADER_READ_BIT,
.old_layout = VK_IMAGE_LAYOUT_GENERAL, // We want a layout transition, so the old_layout and new_layout values need to be replicated in the acquire barrier
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = post_compute_queue->get_family_index(),
.dst_queue_family = present_graphics_queue->get_family_index(), // Acquire barriers are executed from a queue of the destination queue family
};
command_buffer->image_memory_barrier(*blur_chain_views[1], memory_barrier_2);
}
draw(*command_buffer, get_render_context().get_active_frame().get_render_target());
// If maintenance9 is not enabled, resources with VK_SHARING_MODE_EXCLUSIVE must only be accessed by queues in the queue family that has ownership of the resource.
// Upon creation resources with VK_SHARING_MODE_EXCLUSIVE are not owned by any queue, ownership is implicitly acquired upon first use.
// The application must perform a queue family ownership transfer if it wishes to make the memory contents of the resource accessible to a different queue family.
// A queue family can take ownership of a resource without an ownership transfer, in the same way as for a resource that was just created, but the content will be undefined.
// We do not need to release blur_chain_views[1] and color_targets[0] from present_graphics
// A queue transfer barrier is not necessary for the resource first access.
// Moreover, in our sample we do not care about the content after presenting so we can skip the queue transfer barrier.
command_buffer->end();
// We're going to wait on this semaphore in different frame,
// so we need to hold ownership of the semaphore until we complete the wait.
hdr_wait_semaphores[forward_render_target_index] = get_render_context().request_semaphore_with_ownership();
// We've read the post buffer outputs, so we need to consider write-after-read
// next frame. This is only meaningful if we're doing double buffered HDR since it's
// theoretically possible to complete HDR rendering for frame N + 1 while we're doing presentation.
// In that case, the async compute post pipeline can start writing blur results *before* we're done reading.
compute_post_semaphore = get_render_context().request_semaphore_with_ownership();
const VkSemaphore signal_semaphores[] = {
get_render_context().request_semaphore(),
hdr_wait_semaphores[forward_render_target_index],
compute_post_semaphore,
};
const VkSemaphore wait_semaphores[] = {
post_semaphore,
get_render_context().consume_acquired_semaphore(),
};
const VkPipelineStageFlags wait_stages[] = {
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
};
auto info = vkb::initializers::submit_info();
info.pSignalSemaphores = signal_semaphores;
info.signalSemaphoreCount = 3;
info.pWaitSemaphores = wait_semaphores;
info.waitSemaphoreCount = 2;
info.pWaitDstStageMask = wait_stages;
info.commandBufferCount = 1;
info.pCommandBuffers = &command_buffer->get_handle();
queue.submit({info}, get_render_context().get_active_frame().get_fence_pool().request_fence());
get_render_context().release_owned_semaphore(wait_semaphores[1]);
return signal_semaphores[0];
}
VkSemaphore AsyncComputeSample::render_compute_post(VkSemaphore wait_graphics_semaphore, VkSemaphore wait_present_semaphore)
{
auto &queue = *post_compute_queue;
auto command_buffer = get_render_context().get_active_frame().get_command_pool(queue).request_command_buffer();
command_buffer->set_debug_name("compute_post");
command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
if (early_graphics_queue->get_family_index() != post_compute_queue->get_family_index())
{
// acquire_barrier_0: Acquiring color_targets[0] from early_graphics to post_compute
// This acquire barrier is replicated by the corresponding release_barrier_0 in the early_graphics queue
// The application must ensure the acquire operation happens after the release operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier{
.src_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, // Ignored for the acquire barrier.
// Acquire barriers ignore src_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.dst_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
.src_access_mask = 0, // src_access_mask is ignored for acquire barriers, without affecting its validity
.dst_access_mask = VK_ACCESS_SHADER_READ_BIT,
.old_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, // We want a layout transition, so the old_layout and new_layout values need to be replicated in the release barrier
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = early_graphics_queue->get_family_index(),
.dst_queue_family = post_compute_queue->get_family_index(), // Acquire barriers are executed from a queue of the destination queue family
};
command_buffer->image_memory_barrier(get_current_forward_render_target().get_views()[0], memory_barrier);
}
const auto discard_blur_view = [&](const vkb::core::ImageView &view) {
// If maintenance9 is not enabled, resources with VK_SHARING_MODE_EXCLUSIVE must only be accessed by queues in the queue family that has ownership of the resource.
// Upon creation resources with VK_SHARING_MODE_EXCLUSIVE are not owned by any queue, ownership is implicitly acquired upon first use.
// The application must perform a queue family ownership transfer if it wishes to make the memory contents of the resource accessible to a different queue family.
// A queue family can take ownership of a resource without an ownership transfer, in the same way as for a resource that was just created, but the content will be undefined.
// We do not need to acquire blur_chain_views[1] from present_graphics to post_compute
// A queue transfer barrier is not necessary for the resource first access.
// Moreover, in our sample we do not care about the content at this point so we can skip the queue transfer barrier.
vkb::ImageMemoryBarrier memory_barrier{};
memory_barrier.old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
memory_barrier.new_layout = VK_IMAGE_LAYOUT_GENERAL;
memory_barrier.src_access_mask = 0;
memory_barrier.dst_access_mask = VK_ACCESS_SHADER_WRITE_BIT;
memory_barrier.src_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
memory_barrier.dst_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
command_buffer->image_memory_barrier(view, memory_barrier);
};
const auto read_only_blur_view = [&](const vkb::core::ImageView &view, bool is_final) {
const bool queue_family_transfer = is_final && post_compute_queue->get_family_index() != present_graphics_queue->get_family_index();
// release_barrier_2: Releasing blur_chain_views[1] from post_compute to present_graphics
// This release barrier is replicated by the corresponding acquire_barrier_2 in the present_graphics queue
// The application must ensure the release operation happens before the acquire operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier{
.src_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
.dst_stage_mask = is_final ? VkPipelineStageFlags(VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT) : VkPipelineStageFlags(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT), // Ignored for the release barrier.
// Release barriers ignore dst_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.src_access_mask = VK_ACCESS_SHADER_WRITE_BIT,
.dst_access_mask = is_final ? VkAccessFlags(0) : VkAccessFlags(VK_ACCESS_SHADER_READ_BIT), // dst_access_mask is ignored for release barriers, without affecting its validity
.old_layout = VK_IMAGE_LAYOUT_GENERAL, // We want a layout transition, so the old_layout and new_layout values need to be replicated in the acquire barrier
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = queue_family_transfer ? post_compute_queue->get_family_index() : VK_QUEUE_FAMILY_IGNORED, // Release barriers are executed from a queue of the source queue family
.dst_queue_family = queue_family_transfer ? present_graphics_queue->get_family_index() : VK_QUEUE_FAMILY_IGNORED, // Release barriers are executed from a queue of the source queue family
};
command_buffer->image_memory_barrier(view, memory_barrier);
};
struct Push
{
uint32_t width, height;
float inv_width, inv_height;
float inv_input_width, inv_input_height;
};
const auto dispatch_pass = [&](const vkb::core::ImageView &dst, const vkb::core::ImageView &src, bool is_final = false) {
discard_blur_view(dst);
auto dst_extent = downsample_extent(dst.get_image().get_extent(), dst.get_subresource_range().baseMipLevel);
auto src_extent = downsample_extent(src.get_image().get_extent(), src.get_subresource_range().baseMipLevel);
Push push{};
push.width = dst_extent.width;
push.height = dst_extent.height;
push.inv_width = 1.0f / static_cast<float>(push.width);
push.inv_height = 1.0f / static_cast<float>(push.height);
push.inv_input_width = 1.0f / static_cast<float>(src_extent.width);
push.inv_input_height = 1.0f / static_cast<float>(src_extent.height);
command_buffer->push_constants(push);
command_buffer->bind_image(src, *linear_sampler, 0, 0, 0);
command_buffer->bind_image(dst, 0, 1, 0);
command_buffer->dispatch((push.width + 7) / 8, (push.height + 7) / 8, 1);
read_only_blur_view(dst, is_final);
};
// A very basic and dumb HDR Bloom pipeline. Don't consider this a particularly good or efficient implementation.
// It's here to represent a plausible compute post workload.
// - Threshold pass
// - Blur down
// - Blur up
command_buffer->bind_pipeline_layout(*threshold_pipeline);
dispatch_pass(*blur_chain_views[0], get_current_forward_render_target().get_views()[0]);
command_buffer->bind_pipeline_layout(*blur_down_pipeline);
for (uint32_t index = 1; index < blur_chain_views.size(); index++)
{
dispatch_pass(*blur_chain_views[index], *blur_chain_views[index - 1]);
}
command_buffer->bind_pipeline_layout(*blur_up_pipeline);
for (uint32_t index = static_cast<uint32_t>(blur_chain_views.size() - 2); index >= 1; index--)
{
dispatch_pass(*blur_chain_views[index], *blur_chain_views[index + 1], index == 1);
}
if (post_compute_queue->get_family_index() != present_graphics_queue->get_family_index())
{
// release_barrier_1: Releasing color_targets[0] from post_compute to present_graphics
// This release barrier is replicated by the corresponding acquire_barrier_1 in the present_graphics queue
// The application must ensure the release operation happens before the acquire operation. This sample uses semaphores for that.
// The transfer ownership barriers are submitted twice (release and acquire) but they are only executed once.
vkb::ImageMemoryBarrier memory_barrier{
.src_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
.dst_stage_mask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, // Ignored for the release barrier.
// Release barriers ignore dst_access_mask unless using VK_DEPENDENCY_QUEUE_FAMILY_OWNERSHIP_TRANSFER_USE_ALL_STAGES_BIT_KHR
.src_access_mask = VK_ACCESS_SHADER_READ_BIT,
.dst_access_mask = 0, // dst_access_mask is ignored for release barriers, without affecting its validity
.old_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, // Purely ownership transfer. We do not need a layout transition.
.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
.src_queue_family = post_compute_queue->get_family_index(), // Release barriers are executed from a queue of the source queue family
.dst_queue_family = present_graphics_queue->get_family_index(),
};
command_buffer->image_memory_barrier(get_current_forward_render_target().get_views()[0], memory_barrier);
}
command_buffer->end();
VkPipelineStageFlags wait_stages[] = {VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT};
VkSemaphore wait_semaphores[] = {wait_graphics_semaphore, wait_present_semaphore};
VkSemaphore signal_semaphore = get_render_context().request_semaphore();
auto info = vkb::initializers::submit_info();
info.pSignalSemaphores = &signal_semaphore;
info.signalSemaphoreCount = 1;
info.pWaitSemaphores = wait_semaphores;
info.waitSemaphoreCount = wait_present_semaphore != VK_NULL_HANDLE ? 2 : 1;
info.pWaitDstStageMask = wait_stages;
info.commandBufferCount = 1;
info.pCommandBuffers = &command_buffer->get_handle();
if (wait_present_semaphore != VK_NULL_HANDLE)
{
get_render_context().release_owned_semaphore(wait_present_semaphore);
}
queue.submit({info}, VK_NULL_HANDLE);
return signal_semaphore;
}
void AsyncComputeSample::update(float delta_time)
{
// don't call the parent's update, because it's done differently here... but call the grandparent's update for fps logging
vkb::Application::update(delta_time);
if (last_async_enabled != async_enabled)
{
setup_queues();
}
// We can potentially get more overlap if we double buffer the HDR render target.
// In this scenario, the next frame can run ahead a little further before it needs to block.
if (double_buffer_hdr_frames)
{
forward_render_target_index = 1 - forward_render_target_index;
}
else
{
forward_render_target_index = 0;
}
auto *forward_subpass = static_cast<ShadowMapForwardSubpass *>(forward_render_pipeline.get_subpasses()[0].get());
auto *composite_subpass = static_cast<CompositeSubpass *>(get_render_pipeline().get_subpasses()[0].get());
forward_subpass->set_shadow_map(&shadow_render_target->get_views()[0], comparison_sampler.get());
composite_subpass->set_texture(&get_current_forward_render_target().get_views()[0], blur_chain_views[1].get(), linear_sampler.get()); // blur_chain[1] and color_targets[0] will be used by the present queue
float rotation_factor = std::chrono::duration<float>(std::chrono::system_clock::now() - start_time).count();
glm::quat orientation;
// Lots of random jank to get a desired orientation quaternion for the directional light.
if (rotate_shadows)
{
// Move shadows and directional light slightly.
orientation = glm::normalize(
glm::angleAxis(glm::pi<float>(), glm::vec3(0.0f, -1.0f, 0.0f)) *
glm::angleAxis(-0.2f * glm::half_pi<float>(), glm::vec3(1.0f, 0.0f, 0.0f)) *
glm::angleAxis(glm::two_pi<float>() * glm::fract(rotation_factor * 0.05f), glm::vec3(0.0f, 0.0f, -1.0f)) *
glm::angleAxis(-0.05f * glm::half_pi<float>(), glm::vec3(1.0f, 0.0f, 0.0f)));
}
else
{
orientation = glm::normalize(
glm::angleAxis(glm::pi<float>(), glm::vec3(0.0f, -1.0f, 0.0f)) *
glm::angleAxis(-0.2f * glm::half_pi<float>(), glm::vec3(1.0f, 0.0f, 0.0f)));
}
auto &shadow_camera_transform = shadow_camera->get_node()->get_component<vkb::sg::Transform>();
shadow_camera_transform.set_rotation(orientation);
// Explicit begin_frame and end_frame since we're doing async compute, many submissions and custom semaphores ...
get_render_context().begin_frame();
update_scene(delta_time);
update_gui(delta_time);
// Collect the performance data for the sample graphs
update_stats(delta_time);
// Setup render pipeline:
// - Shadow pass
// - HDR
// - Async compute post
// - Composite
render_shadow_pass();
VkSemaphore graphics_semaphore = render_forward_offscreen_pass(hdr_wait_semaphores[forward_render_target_index]);
hdr_wait_semaphores[forward_render_target_index] = VK_NULL_HANDLE;
VkSemaphore post_semaphore = render_compute_post(graphics_semaphore, compute_post_semaphore);
compute_post_semaphore = VK_NULL_HANDLE;
VkSemaphore present_semaphore = render_swapchain(post_semaphore);
get_render_context().end_frame(present_semaphore);
}
void AsyncComputeSample::finish()
{
if (has_device())
{
for (auto &sem : hdr_wait_semaphores)
{
// We're outside a frame context, so free the semaphore manually.
get_device().wait_idle();
vkDestroySemaphore(get_device().get_handle(), sem, nullptr);
}
if (compute_post_semaphore)
{
// We're outside a frame context, so free the semaphore manually.
get_device().wait_idle();
vkDestroySemaphore(get_device().get_handle(), compute_post_semaphore, nullptr);
}
}
}
std::unique_ptr<vkb::VulkanSampleC> create_async_compute()
{
return std::make_unique<AsyncComputeSample>();
}
AsyncComputeSample::DepthMapSubpass::DepthMapSubpass(vkb::RenderContext &render_context,
vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader,
vkb::sg::Scene &scene, vkb::sg::Camera &camera) :
vkb::ForwardSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera)
{
// PCF, so need depth bias to avoid (most) shadow acne.
base_rasterization_state.depth_bias_enable = VK_TRUE;
}
void AsyncComputeSample::DepthMapSubpass::draw(vkb::core::CommandBufferC &command_buffer)
{
// Negative bias since we're using inverted Z.
command_buffer.set_depth_bias(-1.0f, 0.0f, -2.0f);
vkb::ForwardSubpass::draw(command_buffer);
}
AsyncComputeSample::ShadowMapForwardSubpass::ShadowMapForwardSubpass(vkb::RenderContext &render_context,
vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader,
vkb::sg::Scene &scene, vkb::sg::Camera &camera, vkb::sg::Camera &shadow_camera_) :
vkb::ForwardSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera),
shadow_camera(shadow_camera_)
{
}
void AsyncComputeSample::ShadowMapForwardSubpass::set_shadow_map(const vkb::core::ImageView *view, const vkb::core::Sampler *sampler)
{
shadow_view = view;
shadow_sampler = sampler;
}
void AsyncComputeSample::ShadowMapForwardSubpass::draw(vkb::core::CommandBufferC &command_buffer)
{
auto shadow_matrix = vkb::rendering::vulkan_style_projection(shadow_camera.get_projection()) * shadow_camera.get_view();
shadow_matrix = glm::translate(glm::vec3(0.5f, 0.5f, 0.0f)) * glm::scale(glm::vec3(0.5f, 0.5f, 1.0f)) * shadow_matrix;
auto &render_frame = get_render_context().get_active_frame();
auto allocation = render_frame.allocate_buffer(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, sizeof(glm::mat4), thread_index);
allocation.update(shadow_matrix);
// Custom part, bind shadow map to the fragment shader.
command_buffer.bind_buffer(allocation.get_buffer(), allocation.get_offset(), allocation.get_size(), 0, 5, 0);
command_buffer.bind_image(*shadow_view, *shadow_sampler, 0, 6, 0);
vkb::ForwardSubpass::draw(command_buffer);
}
AsyncComputeSample::CompositeSubpass::CompositeSubpass(vkb::RenderContext &render_context, vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader) :
vkb::rendering::SubpassC(render_context, std::move(vertex_shader), std::move(fragment_shader))
{
}
void AsyncComputeSample::CompositeSubpass::set_texture(const vkb::core::ImageView *hdr_view_, const vkb::core::ImageView *bloom_view_,
const vkb::core::Sampler *sampler_)
{
hdr_view = hdr_view_;
bloom_view = bloom_view_;
sampler = sampler_;
}
void AsyncComputeSample::CompositeSubpass::prepare()
{
auto &device = get_render_context().get_device();
auto &vertex = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_VERTEX_BIT, get_vertex_shader());
auto &fragment = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_FRAGMENT_BIT, get_fragment_shader());
layout = &device.get_resource_cache().request_pipeline_layout({&vertex, &fragment});
}
void AsyncComputeSample::CompositeSubpass::draw(vkb::core::CommandBufferC &command_buffer)
{
command_buffer.bind_image(*hdr_view, *sampler, 0, 0, 0);
command_buffer.bind_image(*bloom_view, *sampler, 0, 1, 0);
command_buffer.bind_pipeline_layout(*layout);
// A depth-stencil attachment exists in the default render pass, make sure we ignore it.
vkb::DepthStencilState ds_state = {};
ds_state.depth_test_enable = VK_FALSE;
ds_state.stencil_test_enable = VK_FALSE;
ds_state.depth_write_enable = VK_FALSE;
ds_state.depth_compare_op = VK_COMPARE_OP_ALWAYS;
command_buffer.set_depth_stencil_state(ds_state);
command_buffer.draw(3, 1, 0, 0);
}