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