init
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2019-2021, 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(
|
||||
ID ${FOLDER_NAME}
|
||||
CATEGORY ${CATEGORY_NAME}
|
||||
AUTHOR "Arm"
|
||||
NAME "Command Buffer Usage"
|
||||
DESCRIPTION "Primary and secondary command buffers, allocation and recycling."
|
||||
SHADER_FILES_GLSL
|
||||
"base.vert"
|
||||
"base.frag")
|
||||
@@ -0,0 +1,210 @@
|
||||
////
|
||||
- Copyright (c) 2019-2024, 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.
|
||||
-
|
||||
////
|
||||
= Command buffer usage and multi-threaded recording
|
||||
|
||||
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/command_buffer_usage[Khronos Vulkan samples github repository].
|
||||
endif::[]
|
||||
|
||||
:pp: {plus}{plus}
|
||||
|
||||
== Overview
|
||||
|
||||
This sample demonstrates how to use and manage secondary command buffers, and how to record them concurrently.
|
||||
|
||||
Implementing multi-threaded recording of draw calls can help reduce CPU frame time.
|
||||
|
||||
In tile-based renderers, the best approach to split the draw calls is to record them in secondary command buffers.
|
||||
This way they can all be submitted to the same render pass, and can take advantage of tile local memory.
|
||||
|
||||
== Secondary command buffers
|
||||
|
||||
Secondary command buffers can inherit the render pass state from a primary command buffer using a https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferInheritanceInfo.html[VkCommandBufferInheritanceInfo] structure which is passed to https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkBeginCommandBuffer.html[vkBeginCommandBuffer] as part of https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferBeginInfo.html[VkCommandBufferBeginInfo], along with the flag `VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT`.
|
||||
Secondary command buffers may then be recorded concurrently.
|
||||
|
||||
The primary command buffer must have used the flag `VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS` in https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdBeginRenderPass.html[vkCmdBeginRenderPass].
|
||||
|
||||
Finally, the primary command buffer records https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdExecuteCommands.html[vkCmdExecuteCommands] (before https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdEndRenderPass.html[vkCmdEndRenderPass]) with an array of recorded secondary command buffers to execute.
|
||||
The sample divides the draw calls for opaque objects based on the slider value.
|
||||
It then submits a separate buffer for transparent objects if any, and finally one for the GUI elements if visible.
|
||||
|
||||
== Multi-threaded recording
|
||||
|
||||
To record command buffers concurrently, the framework needs to manage resource pools per frame and per thread.
|
||||
According to the Vulkan Spec:
|
||||
|
||||
* https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandPool.html[_A command pool must not be used concurrently in multiple threads._]
|
||||
* https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkDescriptorPool.html[_The application must not allocate and/or free descriptor sets from the same pool in multiple threads simultaneously._]
|
||||
|
||||
In the framework, each frame in the queue (e.g.
|
||||
three frames in case of triple buffering) manages a collection of pools so that each thread can own:
|
||||
|
||||
* A command pool
|
||||
* A descriptor pool cache
|
||||
* A descriptor set cache
|
||||
* A buffer pool
|
||||
|
||||
This sample then uses a thread pool to push work to multiple threads.
|
||||
When splitting the draw calls, it is advisable to keep the loads balanced.
|
||||
The sample allows to change the number of buffers, but if the number of calls is not divisible, the remaining will be evenly spread through other buffers.
|
||||
The average number of draws per buffer is shown on the screen.
|
||||
|
||||
Note that since state is not reused across command buffers, a reasonable number of draw calls should be submitted per command buffer, to avoid having the GPU going idle while processing commands.
|
||||
Therefore having many secondary command buffers with few draw calls can negatively affect performance.
|
||||
In any case there is no advantage in exceeding the CPU parallelism level i.e.
|
||||
using more command buffers than threads.
|
||||
Similarly having more threads than buffers may have a performance impact.
|
||||
To keep all threads busy, the sample resizes the thread pool for low number of buffers.
|
||||
The sample slider can help illustrate these trade-offs and their impact on performance, as shown by the performance graphs.
|
||||
|
||||
NOTE: Since the time of writing this tutorial, the CPU counter provider, HWCPipe, has been updated and it no longer provides CPU cycles. These may still be measured using external tools, as shown later.
|
||||
|
||||
In this case, a scene with a high number of draw calls (~1800, this number may be found in the link:../../../docs/misc.adoc#debug-window[debug window]) shows a 15% improvement in performance when dividing the workload among 8 buffers across 8 threads:
|
||||
|
||||
image::./images/single_threading.png[Single-threaded]
|
||||
|
||||
image::./images/multi_threading.png[Multi-threaded]
|
||||
|
||||
To test the sample, make sure to build it in release mode and without validation layers.
|
||||
Both these factors can significantly affect the results.
|
||||
|
||||
== Recycling strategies
|
||||
|
||||
Vulkan provides different ways to manage and allocate command buffers.
|
||||
This sample compares them and demonstrates the best approach.
|
||||
|
||||
* <<allocate-and-free,Allocate and Free>>
|
||||
* <<resetting-individual-command-buffers,Resetting individual command buffers>>
|
||||
* <<resetting-the-command-pool,Resetting the command pool>>
|
||||
|
||||
=== Allocate and free
|
||||
|
||||
Command buffers are allocated from a command pool with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkAllocateCommandBuffers.html[vkAllocateCommandBuffers].
|
||||
They can then be recorded and submitted to a queue for the Vulkan device to execute them.
|
||||
|
||||
A possible approach to managing the command buffers for each frame in our application would be to free them once they are executed, using https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkFreeCommandBuffers.html[vkFreeCommandBuffers].
|
||||
|
||||
The command pool will not automatically recycle memory from deleted command buffers if the command pool was created without the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandPoolCreateFlagBits.html[RESET_COMMAND_BUFFER_BIT] flag.
|
||||
This flag however will force separate internal allocators to be used for each command buffer in the pool, which can increase CPU overhead compared to a single pool reset.
|
||||
|
||||
This is the worst-performing method of managing command buffers as it involves a significant CPU overhead for allocating and freeing memory frequently.
|
||||
The sample shows how to use the framework to follow this approach and profile its performance.
|
||||
|
||||
image::./images/allocate_and_free.jpg[Allocate and Free]
|
||||
|
||||
Rather than freeing and re-allocating the memory used by a command buffer, it is more efficient to recycle it for recording new commands.
|
||||
There are two ways of resetting a command buffer: individually, with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandBuffer.html[vkResetCommandBuffer], or indirectly by resetting the command pool with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandPool.html[vkResetCommandPool].
|
||||
|
||||
=== Resetting individual command buffers
|
||||
|
||||
In order to reset command buffers individually with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandBuffer.html[vkResetCommandBuffer], the pool must have been created with the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandPoolCreateFlagBits.html[RESET_COMMAND_BUFFER_BIT] flag set.
|
||||
The buffer will then return to a recordable state and the command pool can reuse the memory it allocated for it.
|
||||
|
||||
However frequent calls to https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandBuffer.html[vkResetCommandBuffer] are more expensive than a command pool reset.
|
||||
|
||||
image::./images/reset_buffers.jpg[Reset Buffers]
|
||||
|
||||
=== Resetting the command pool
|
||||
|
||||
Resetting the pool with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandPool.html[vkResetCommandPool] automatically resets all the command buffers allocated by it.
|
||||
Doing this periodically will allow the pool to reuse the memory allocated for command buffers with lower CPU overhead.
|
||||
|
||||
To reset the pool the flag https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandPoolCreateFlagBits.html[RESET_COMMAND_BUFFER_BIT] is _not_ required, and it is actually better to avoid it since it prevents it from using a single large allocator for all buffers in the pool thus increasing memory overhead.
|
||||
|
||||
image::./images/reset_pool.jpg[Reset Pool]
|
||||
|
||||
=== Relative performance
|
||||
|
||||
The sample offers the option of recording all drawing operations on a single command buffer, or to divide the opaque object draw calls among a given number of secondary command buffers.
|
||||
The second method allows multi-threaded command buffer construction.
|
||||
However the number of secondary command buffers should be kept low since their invocations are expensive.
|
||||
This sample lets the user adjust the number of command buffers.
|
||||
Using a high number of secondary command buffers causes the application to become CPU bound and makes the differences between the described memory allocation approaches more pronounced.
|
||||
|
||||
All command buffers in this sample are initialized with the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferUsageFlagBits.html[ONE_TIME_SUBMIT_BIT] flag set.
|
||||
This indicates to the driver that the buffer will not be re-submitted after execution, and allows it to optimize accordingly.
|
||||
Performance may be reduced if the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferUsageFlagBits.html[SIMULTANEOUS_USE_BIT] flag is set instead.
|
||||
|
||||
This sample provides options to try the three different approaches to command buffer management described above and monitor their efficiency.
|
||||
This is relatively obvious directly on the device by monitoring frame time.
|
||||
|
||||
Since the application is CPU bound, the https://developer.android.com/studio/profile/android-profiler[Android Profiler] is a helpful tool to analyze the differences in performance.
|
||||
As expected, most of the time goes into the https://github.com/ARM-software/vulkan_best_practice_for_mobile_developers/blob/master/framework/core/command_pool.cpp[command pool framework functions]: `request_command_buffer` and `reset`.
|
||||
These handle the different modes of operation exposed by this sample.
|
||||
|
||||
image:./images/android_profiler_allocate_and_free.png[Android Profiler: Allocate and Free] _Android Profiler capture: use the Flame Chart to visualize which functions take the most time in the capture.
|
||||
Filter for command buffer functions, and use the tooltips to find out how much time out of the overall capture was used by each function._
|
||||
|
||||
Capturing the C{pp} calls this way allows us to determine how much each function contributed to the overall running time.
|
||||
The results are captured in the table below.
|
||||
|
||||
|===
|
||||
| Mode | Request + Reset command buffers (ms) | Total capture (ms) | Contribution
|
||||
|
||||
| Reset pool
|
||||
| 53.3
|
||||
| 11 877
|
||||
| 0.45 %
|
||||
|
||||
| Reset buffers
|
||||
| 140.29
|
||||
| 12 087
|
||||
| 1.16 %
|
||||
|
||||
| Allocate and free
|
||||
| 3 319.25
|
||||
| 11 513
|
||||
| 28.8 %
|
||||
|===
|
||||
|
||||
In this application the differences between individual reset and pool reset are more subtle, but allocating and freeing buffers are clearly the bottleneck in the worst performing case.
|
||||
|
||||
== Further reading
|
||||
|
||||
* xref:samples/performance/multithreading_render_passes/README.adoc[Multi-threaded recording with multiple render passes]
|
||||
* https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap6.html#commandbuffer-allocation[Command Buffer Allocation and Management]
|
||||
* https://www.khronos.org/registry/vulkan/specs/1.0-wsi_extensions/html/chap5.html#commandbuffers-lifecycle[Command Buffer Lifecycle]
|
||||
* _"Writing an efficient Vulkan renderer"_ by Arseny Kapoulkine (from "GPU Zen 2: Advanced Rendering Techniques")
|
||||
|
||||
== Best-practice summary
|
||||
|
||||
*Do*
|
||||
|
||||
* Use secondary command buffers to allow multi-threaded render pass construction.
|
||||
* Minimize the number of secondary command buffer invocations used per frame.
|
||||
* Set https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferUsageFlagBits.html[ONE_TIME_SUBMIT_BIT] if you are not going to reuse the command buffer.
|
||||
* Periodically call https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandPool.html[vkResetCommandPool()] to release the memory if you are not reusing command buffers.
|
||||
|
||||
*Don't*
|
||||
|
||||
* Set https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandPoolCreateFlagBits.html[RESET_COMMAND_BUFFER_BIT] if you only need to free the whole pool.
|
||||
If the bit is not set, some implementations might use a single large allocator for the pool, reducing memory management overhead.
|
||||
* Call https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandBuffer.html[vkResetCommandBuffer()] on a high frequency call path.
|
||||
|
||||
*Impact*
|
||||
|
||||
* Increased CPU load will be incurred with secondary command buffers.
|
||||
* Increased CPU load will be incurred if command pool creation and command buffer begin flags are not used appropriately.
|
||||
* Increased CPU overhead if command buffer resets are too frequent.
|
||||
* Increased memory usage until a manual command pool reset is triggered.
|
||||
|
||||
*Debugging*
|
||||
|
||||
* Evaluate every use of any command buffer flag other than https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkCommandBufferUsageFlagBits.html[ONE_TIME_SUBMIT_BIT], and review whether it's a necessary use of the flag combination.
|
||||
* Evaluate every use of https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandBuffer.html[vkResetCommandBuffer()] and see if it could be replaced with https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkResetCommandPool.html[vkResetCommandPool()] instead.
|
||||
@@ -0,0 +1,453 @@
|
||||
/* Copyright (c) 2019-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 "command_buffer_usage.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
#include "core/device.h"
|
||||
#include "core/pipeline_layout.h"
|
||||
#include "core/shader_module.h"
|
||||
#include "filesystem/legacy.h"
|
||||
#include "gltf_loader.h"
|
||||
#include "gui.h"
|
||||
|
||||
#include "stats/stats.h"
|
||||
|
||||
CommandBufferUsage::CommandBufferUsage()
|
||||
{
|
||||
auto &config = get_configuration();
|
||||
|
||||
config.insert<vkb::IntSetting>(0, gui_secondary_cmd_buf_count, 0);
|
||||
config.insert<vkb::BoolSetting>(0, gui_multi_threading, false);
|
||||
config.insert<vkb::IntSetting>(0, gui_command_buffer_reset_mode, 0);
|
||||
|
||||
config.insert<vkb::IntSetting>(1, gui_secondary_cmd_buf_count, 8);
|
||||
config.insert<vkb::BoolSetting>(1, gui_multi_threading, true);
|
||||
config.insert<vkb::IntSetting>(1, gui_command_buffer_reset_mode, 0);
|
||||
|
||||
config.insert<vkb::IntSetting>(2, gui_secondary_cmd_buf_count, 16);
|
||||
config.insert<vkb::BoolSetting>(2, gui_multi_threading, true);
|
||||
config.insert<vkb::IntSetting>(2, gui_command_buffer_reset_mode, 1);
|
||||
|
||||
config.insert<vkb::IntSetting>(3, gui_secondary_cmd_buf_count, 32);
|
||||
config.insert<vkb::BoolSetting>(3, gui_multi_threading, true);
|
||||
config.insert<vkb::IntSetting>(3, gui_command_buffer_reset_mode, 2);
|
||||
}
|
||||
|
||||
bool CommandBufferUsage::prepare(const vkb::ApplicationOptions &options)
|
||||
{
|
||||
if (!VulkanSample::prepare(options))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
load_scene("scenes/bonza/Bonza4X.gltf");
|
||||
|
||||
auto &camera_node = vkb::add_free_camera(get_scene(), "main_camera", get_render_context().get_surface_extent());
|
||||
camera = dynamic_cast<vkb::sg::PerspectiveCamera *>(&camera_node.get_component<vkb::sg::Camera>());
|
||||
|
||||
vkb::ShaderSource vert_shader("base.vert.spv");
|
||||
vkb::ShaderSource frag_shader("base.frag.spv");
|
||||
auto scene_subpass =
|
||||
std::make_unique<ForwardSubpassSecondary>(get_render_context(), std::move(vert_shader), std::move(frag_shader), get_scene(), *camera);
|
||||
|
||||
auto render_pipeline = std::make_unique<vkb::RenderPipeline>();
|
||||
render_pipeline->add_subpass(std::move(scene_subpass));
|
||||
|
||||
set_render_pipeline(std::move(render_pipeline));
|
||||
|
||||
get_stats().request_stats({vkb::StatIndex::frame_times, vkb::StatIndex::cpu_cycles});
|
||||
|
||||
create_gui(*window, &get_stats());
|
||||
|
||||
// Adjust the maximum number of secondary command buffers
|
||||
// In this sample, only the recording of opaque meshes will be multi-threaded
|
||||
auto is_opaque = [](vkb::sg::SubMesh *sub_mesh) {
|
||||
return sub_mesh->get_material()->alpha_mode != vkb::sg::AlphaMode::Blend;
|
||||
};
|
||||
auto count_opaque_submeshes = [is_opaque](uint32_t accumulated, const vkb::sg::Mesh *mesh) -> uint32_t {
|
||||
return accumulated + vkb::to_u32(mesh->get_nodes().size() * std::count_if(mesh->get_submeshes().begin(), mesh->get_submeshes().end(), is_opaque));
|
||||
};
|
||||
const auto &mesh_components = get_scene().get_components<vkb::sg::Mesh>();
|
||||
const uint32_t opaque_mesh_count = std::accumulate(mesh_components.begin(), mesh_components.end(), 0, count_opaque_submeshes);
|
||||
|
||||
max_secondary_command_buffer_count = std::min(opaque_mesh_count, max_secondary_command_buffer_count);
|
||||
|
||||
// Show number of opaque meshes in debug window
|
||||
get_debug_info().insert<vkb::field::Static, uint32_t>("opaque_mesh_count", opaque_mesh_count);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CommandBufferUsage::prepare_render_context()
|
||||
{
|
||||
max_thread_count = std::max(std::thread::hardware_concurrency(), MIN_THREAD_COUNT);
|
||||
get_render_context().prepare(max_thread_count);
|
||||
}
|
||||
|
||||
void CommandBufferUsage::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);
|
||||
|
||||
auto &subpass_state = static_cast<ForwardSubpassSecondary *>(get_render_pipeline().get_active_subpass().get())->get_state();
|
||||
|
||||
// Process GUI input
|
||||
subpass_state.secondary_cmd_buf_count = vkb::to_u32(gui_secondary_cmd_buf_count);
|
||||
|
||||
use_secondary_command_buffers = subpass_state.secondary_cmd_buf_count > 0;
|
||||
|
||||
// If there are not enough command buffers to keep all threads busy, use fewer threads
|
||||
subpass_state.thread_count = std::min(subpass_state.secondary_cmd_buf_count, max_thread_count);
|
||||
|
||||
subpass_state.command_buffer_reset_mode = static_cast<vkb::CommandBufferResetMode>(gui_command_buffer_reset_mode);
|
||||
|
||||
subpass_state.multi_threading = gui_multi_threading;
|
||||
|
||||
auto &render_context = get_render_context();
|
||||
|
||||
update_scene(delta_time);
|
||||
|
||||
update_gui(delta_time);
|
||||
|
||||
auto primary_command_buffer = render_context.begin(subpass_state.command_buffer_reset_mode);
|
||||
|
||||
update_stats(delta_time);
|
||||
|
||||
primary_command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
|
||||
get_stats().begin_sampling(*primary_command_buffer);
|
||||
|
||||
draw(*primary_command_buffer, render_context.get_active_frame().get_render_target());
|
||||
|
||||
get_stats().end_sampling(*primary_command_buffer);
|
||||
primary_command_buffer->end();
|
||||
|
||||
render_context.submit(primary_command_buffer);
|
||||
}
|
||||
|
||||
void CommandBufferUsage::draw_gui()
|
||||
{
|
||||
const bool landscape = camera->get_aspect_ratio() > 1.0f;
|
||||
uint32_t lines = landscape ? 3 : 5;
|
||||
|
||||
const auto &subpass = static_cast<ForwardSubpassSecondary *>(get_render_pipeline().get_active_subpass().get());
|
||||
|
||||
get_gui().show_options_window(
|
||||
/* body = */
|
||||
[&]() {
|
||||
// Secondary command buffer count
|
||||
ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.55f);
|
||||
ImGui::SliderInt("##secCmdBufs", &gui_secondary_cmd_buf_count, 0, max_secondary_command_buffer_count, "Secondary CmdBuffs: %d");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Draws/buf: %.1f", subpass->get_avg_draws_per_buffer());
|
||||
|
||||
// Multi-threading (no effect if 0 secondary command buffers)
|
||||
ImGui::Checkbox("Multi-threading", &gui_multi_threading);
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("(%d threads)", subpass->get_state().thread_count);
|
||||
|
||||
// Buffer management options
|
||||
ImGui::RadioButton(
|
||||
"Allocate and free", &gui_command_buffer_reset_mode, static_cast<int>(vkb::CommandBufferResetMode::AlwaysAllocate));
|
||||
if (landscape)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::RadioButton(
|
||||
"Reset buffer", &gui_command_buffer_reset_mode, static_cast<int>(vkb::CommandBufferResetMode::ResetIndividually));
|
||||
if (landscape)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::RadioButton(
|
||||
"Reset pool", &gui_command_buffer_reset_mode, static_cast<int>(vkb::CommandBufferResetMode::ResetPool));
|
||||
},
|
||||
/* lines = */ lines);
|
||||
}
|
||||
|
||||
void CommandBufferUsage::render(vkb::core::CommandBufferC &primary_command_buffer)
|
||||
{
|
||||
if (has_render_pipeline())
|
||||
{
|
||||
if (use_secondary_command_buffers)
|
||||
{
|
||||
// The user will set the number of secondary command buffers used for opaque meshes
|
||||
// There will be additional buffers for transparent meshes and for the GUI
|
||||
get_render_pipeline().draw(
|
||||
primary_command_buffer, get_render_context().get_active_frame().get_render_target(), VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS);
|
||||
}
|
||||
else
|
||||
{
|
||||
get_render_pipeline().draw(primary_command_buffer, get_render_context().get_active_frame().get_render_target(), VK_SUBPASS_CONTENTS_INLINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CommandBufferUsage::draw_renderpass(vkb::core::CommandBufferC &primary_command_buffer, vkb::RenderTarget &render_target)
|
||||
{
|
||||
const auto &subpass = static_cast<ForwardSubpassSecondary *>(get_render_pipeline().get_active_subpass().get());
|
||||
auto &extent = render_target.get_extent();
|
||||
|
||||
VkViewport viewport{};
|
||||
viewport.width = static_cast<float>(extent.width);
|
||||
viewport.height = static_cast<float>(extent.height);
|
||||
viewport.minDepth = 0.0f;
|
||||
viewport.maxDepth = 1.0f;
|
||||
primary_command_buffer.set_viewport(0, {viewport});
|
||||
subpass->set_viewport(viewport);
|
||||
|
||||
VkRect2D scissor{};
|
||||
scissor.extent = extent;
|
||||
primary_command_buffer.set_scissor(0, {scissor});
|
||||
subpass->set_scissor(scissor);
|
||||
|
||||
render(primary_command_buffer);
|
||||
|
||||
// Draw gui
|
||||
if (has_gui())
|
||||
{
|
||||
if (use_secondary_command_buffers)
|
||||
{
|
||||
const auto &queue = get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
|
||||
|
||||
auto secondary_command_buffer = get_render_context()
|
||||
.get_active_frame()
|
||||
.get_command_pool(queue, subpass->get_state().command_buffer_reset_mode)
|
||||
.request_command_buffer(VK_COMMAND_BUFFER_LEVEL_SECONDARY);
|
||||
|
||||
secondary_command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT | VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT, &primary_command_buffer);
|
||||
|
||||
secondary_command_buffer->set_viewport(0, {viewport});
|
||||
|
||||
secondary_command_buffer->set_scissor(0, {scissor});
|
||||
|
||||
get_gui().draw(*secondary_command_buffer);
|
||||
|
||||
secondary_command_buffer->end();
|
||||
|
||||
primary_command_buffer.execute_commands(*secondary_command_buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
get_gui().draw(primary_command_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
primary_command_buffer.end_render_pass();
|
||||
}
|
||||
|
||||
CommandBufferUsage::ForwardSubpassSecondary::ForwardSubpassSecondary(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}
|
||||
{
|
||||
}
|
||||
|
||||
void CommandBufferUsage::ForwardSubpassSecondary::record_draw(vkb::core::CommandBufferC &command_buffer,
|
||||
const std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> &nodes,
|
||||
uint32_t mesh_start,
|
||||
uint32_t mesh_end,
|
||||
size_t thread_index)
|
||||
{
|
||||
command_buffer.set_color_blend_state(color_blend_state);
|
||||
|
||||
command_buffer.set_depth_stencil_state(get_depth_stencil_state());
|
||||
|
||||
command_buffer.bind_lighting(get_lighting_state(), 0, 4);
|
||||
|
||||
assert(mesh_end <= nodes.size());
|
||||
for (uint32_t i = mesh_start; i < mesh_end; i++)
|
||||
{
|
||||
update_uniform(command_buffer, *nodes[i].first, thread_index);
|
||||
|
||||
draw_submesh(command_buffer, *nodes[i].second);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<vkb::core::CommandBufferC>
|
||||
CommandBufferUsage::ForwardSubpassSecondary::record_draw_secondary(vkb::core::CommandBufferC &primary_command_buffer,
|
||||
const std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> &nodes,
|
||||
uint32_t mesh_start,
|
||||
uint32_t mesh_end,
|
||||
size_t thread_index)
|
||||
{
|
||||
const auto &queue = get_render_context().get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
|
||||
|
||||
auto secondary_command_buffer = get_render_context()
|
||||
.get_active_frame()
|
||||
.get_command_pool(queue, state.command_buffer_reset_mode, thread_index)
|
||||
.request_command_buffer(VK_COMMAND_BUFFER_LEVEL_SECONDARY);
|
||||
|
||||
secondary_command_buffer->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT | VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT, &primary_command_buffer);
|
||||
|
||||
secondary_command_buffer->set_viewport(0, {viewport});
|
||||
|
||||
secondary_command_buffer->set_scissor(0, {scissor});
|
||||
|
||||
record_draw(*secondary_command_buffer, nodes, mesh_start, mesh_end, thread_index);
|
||||
|
||||
secondary_command_buffer->end();
|
||||
|
||||
return secondary_command_buffer;
|
||||
}
|
||||
|
||||
void CommandBufferUsage::ForwardSubpassSecondary::draw(vkb::core::CommandBufferC &primary_command_buffer)
|
||||
{
|
||||
std::multimap<float, std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> opaque_nodes;
|
||||
|
||||
std::multimap<float, std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> transparent_nodes;
|
||||
|
||||
get_sorted_nodes(opaque_nodes, transparent_nodes);
|
||||
|
||||
// Sort opaque objects in front-to-back order
|
||||
// Note: sorting objects does not help on PowerVR, so it can be avoided to save CPU cycles
|
||||
std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> sorted_opaque_nodes;
|
||||
for (auto node_it = opaque_nodes.begin(); node_it != opaque_nodes.end(); node_it++)
|
||||
{
|
||||
sorted_opaque_nodes.push_back(node_it->second);
|
||||
}
|
||||
const auto opaque_submeshes = vkb::to_u32(sorted_opaque_nodes.size());
|
||||
|
||||
// Sort transparent objects in back-to-front order
|
||||
std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> sorted_transparent_nodes;
|
||||
for (auto node_it = transparent_nodes.rbegin(); node_it != transparent_nodes.rend(); node_it++)
|
||||
{
|
||||
sorted_transparent_nodes.push_back(node_it->second);
|
||||
}
|
||||
const auto transparent_submeshes = vkb::to_u32(sorted_transparent_nodes.size());
|
||||
|
||||
allocate_lights<vkb::ForwardLights>(scene.get_components<vkb::sg::Light>(), MAX_FORWARD_LIGHT_COUNT);
|
||||
|
||||
color_blend_attachment.blend_enable = VK_FALSE;
|
||||
color_blend_state.attachments.resize(get_output_attachments().size());
|
||||
color_blend_state.attachments[0] = color_blend_attachment;
|
||||
|
||||
// Draw opaque objects. Depending on the subpass state, use one or multiple
|
||||
// command buffers, and one or multiple threads
|
||||
const bool use_secondary_command_buffers = state.secondary_cmd_buf_count > 0;
|
||||
std::vector<std::shared_ptr<vkb::core::CommandBufferC>> secondary_command_buffers;
|
||||
avg_draws_per_buffer = (state.secondary_cmd_buf_count > 0) ? static_cast<float>(opaque_submeshes) / state.secondary_cmd_buf_count : 0;
|
||||
|
||||
if (state.thread_count != thread_pool.size())
|
||||
{
|
||||
thread_pool.resize(state.thread_count);
|
||||
}
|
||||
|
||||
if (use_secondary_command_buffers)
|
||||
{
|
||||
std::vector<std::future<std::shared_ptr<vkb::core::CommandBufferC>>> secondary_cmd_buf_futures;
|
||||
|
||||
// Save the number of draws left over, these will be distributed among the first buffers
|
||||
uint32_t draws_per_buffer = vkb::to_u32(std::floor(avg_draws_per_buffer));
|
||||
uint32_t remainder_draws = opaque_submeshes % state.secondary_cmd_buf_count;
|
||||
uint32_t mesh_start = 0;
|
||||
|
||||
for (uint32_t cb_count = 0; cb_count < state.secondary_cmd_buf_count; cb_count++)
|
||||
{
|
||||
// Latter command buffers may contain fewer draws
|
||||
uint32_t mesh_end = std::min(opaque_submeshes, mesh_start + draws_per_buffer);
|
||||
if (remainder_draws > 0)
|
||||
{
|
||||
mesh_end++;
|
||||
remainder_draws--;
|
||||
}
|
||||
|
||||
if (state.multi_threading)
|
||||
{
|
||||
auto fut = thread_pool.push(std::bind(&CommandBufferUsage::ForwardSubpassSecondary::record_draw_secondary,
|
||||
this,
|
||||
std::ref(primary_command_buffer),
|
||||
std::cref(sorted_opaque_nodes),
|
||||
mesh_start,
|
||||
mesh_end,
|
||||
std::placeholders::_1));
|
||||
|
||||
secondary_cmd_buf_futures.push_back(std::move(fut));
|
||||
}
|
||||
else
|
||||
{
|
||||
secondary_command_buffers.push_back(record_draw_secondary(primary_command_buffer, sorted_opaque_nodes, mesh_start, mesh_end));
|
||||
}
|
||||
|
||||
mesh_start = mesh_end;
|
||||
}
|
||||
|
||||
if (state.multi_threading)
|
||||
{
|
||||
for (auto &fut : secondary_cmd_buf_futures)
|
||||
{
|
||||
secondary_command_buffers.push_back(fut.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
record_draw(primary_command_buffer, sorted_opaque_nodes, 0, opaque_submeshes);
|
||||
}
|
||||
|
||||
// Enable alpha blending
|
||||
color_blend_attachment.blend_enable = VK_TRUE;
|
||||
color_blend_attachment.src_color_blend_factor = VK_BLEND_FACTOR_SRC_ALPHA;
|
||||
color_blend_attachment.dst_color_blend_factor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
color_blend_attachment.src_alpha_blend_factor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
|
||||
color_blend_state.attachments[0] = color_blend_attachment;
|
||||
|
||||
// Draw transparent objects
|
||||
if (transparent_submeshes > 0)
|
||||
{
|
||||
if (use_secondary_command_buffers)
|
||||
{
|
||||
secondary_command_buffers.push_back(record_draw_secondary(primary_command_buffer, sorted_transparent_nodes, 0, transparent_submeshes));
|
||||
}
|
||||
else
|
||||
{
|
||||
record_draw(primary_command_buffer, sorted_transparent_nodes, 0, transparent_submeshes);
|
||||
}
|
||||
}
|
||||
|
||||
if (use_secondary_command_buffers)
|
||||
{
|
||||
primary_command_buffer.execute_commands(secondary_command_buffers);
|
||||
}
|
||||
}
|
||||
|
||||
void CommandBufferUsage::ForwardSubpassSecondary::set_viewport(VkViewport &viewport)
|
||||
{
|
||||
this->viewport = viewport;
|
||||
}
|
||||
|
||||
void CommandBufferUsage::ForwardSubpassSecondary::set_scissor(VkRect2D &scissor)
|
||||
{
|
||||
this->scissor = scissor;
|
||||
}
|
||||
|
||||
float CommandBufferUsage::ForwardSubpassSecondary::get_avg_draws_per_buffer() const
|
||||
{
|
||||
return avg_draws_per_buffer;
|
||||
}
|
||||
|
||||
CommandBufferUsage::ForwardSubpassSecondaryState &CommandBufferUsage::ForwardSubpassSecondary::get_state()
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
std::unique_ptr<vkb::VulkanSampleC> create_command_buffer_usage()
|
||||
{
|
||||
return std::make_unique<CommandBufferUsage>();
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/* Copyright (c) 2019-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 "buffer_pool.h"
|
||||
#include "common/utils.h"
|
||||
#include "rendering/render_pipeline.h"
|
||||
#include "rendering/subpasses/forward_subpass.h"
|
||||
#include "scene_graph/components/material.h"
|
||||
#include "scene_graph/components/mesh.h"
|
||||
#include "scene_graph/components/perspective_camera.h"
|
||||
#include "vulkan_sample.h"
|
||||
|
||||
class ThreadPool
|
||||
{
|
||||
public:
|
||||
explicit ThreadPool() :
|
||||
stop_flag(false)
|
||||
{}
|
||||
|
||||
~ThreadPool()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
template <class F, class... Args>
|
||||
auto push(F &&f, Args &&...args) -> std::future<std::invoke_result_t<F, Args..., size_t>>
|
||||
{
|
||||
using return_type = std::invoke_result_t<F, Args..., size_t>;
|
||||
auto task_ptr = std::make_shared<std::packaged_task<return_type(size_t)>>(std::bind(std::forward<F>(f), std::forward<Args>(args)..., std::placeholders::_1));
|
||||
std::future<return_type> res = task_ptr->get_future();
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
tasks.emplace([task_ptr](size_t thread_index) { (*task_ptr)(thread_index); });
|
||||
}
|
||||
condition.notify_one();
|
||||
return res;
|
||||
}
|
||||
|
||||
void resize(size_t thread_count)
|
||||
{
|
||||
if (thread_count != workers.size())
|
||||
{
|
||||
shutdown();
|
||||
|
||||
for (size_t i = 0; i < thread_count; ++i)
|
||||
{
|
||||
workers.emplace_back([this, i] {
|
||||
size_t thread_index = i;
|
||||
while (true)
|
||||
{
|
||||
std::function<void(size_t)> task;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
condition.wait(lock, [this] { return stop_flag || !tasks.empty(); });
|
||||
if (stop_flag && tasks.empty())
|
||||
return;
|
||||
task = std::move(tasks.front());
|
||||
tasks.pop();
|
||||
}
|
||||
task(thread_index);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void shutdown()
|
||||
{
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
stop_flag = true;
|
||||
}
|
||||
condition.notify_all();
|
||||
for (auto &worker : workers)
|
||||
worker.join();
|
||||
workers.clear();
|
||||
stop_flag = false;
|
||||
}
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return workers.size();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::thread> workers;
|
||||
std::queue<std::function<void(size_t)>> tasks;
|
||||
std::mutex queue_mutex;
|
||||
std::condition_variable condition;
|
||||
std::atomic<bool> stop_flag;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Sample showing the use of secondary command buffers for
|
||||
* multi-threaded recording, as well as the different
|
||||
* strategies for recycling command buffers every frame
|
||||
*/
|
||||
class CommandBufferUsage : public vkb::VulkanSampleC
|
||||
{
|
||||
public:
|
||||
CommandBufferUsage();
|
||||
|
||||
virtual ~CommandBufferUsage() = default;
|
||||
|
||||
virtual bool prepare(const vkb::ApplicationOptions &options) override;
|
||||
|
||||
virtual void update(float delta_time) override;
|
||||
|
||||
/**
|
||||
* @brief Helper structure used to set subpass state
|
||||
*/
|
||||
struct ForwardSubpassSecondaryState
|
||||
{
|
||||
uint32_t secondary_cmd_buf_count = 0;
|
||||
|
||||
vkb::CommandBufferResetMode command_buffer_reset_mode = vkb::CommandBufferResetMode::ResetPool;
|
||||
|
||||
bool multi_threading = false;
|
||||
|
||||
uint32_t thread_count = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Overrides the draw method to allow for dividing draw calls
|
||||
* into multiple secondary command buffers, optionally
|
||||
* in different threads
|
||||
*/
|
||||
class ForwardSubpassSecondary : public vkb::ForwardSubpass
|
||||
{
|
||||
public:
|
||||
ForwardSubpassSecondary(vkb::RenderContext &render_context,
|
||||
vkb::ShaderSource &&vertex_source, vkb::ShaderSource &&fragment_source,
|
||||
vkb::sg::Scene &scene, vkb::sg::Camera &camera);
|
||||
|
||||
void draw(vkb::core::CommandBufferC &primary_command_buffer) override;
|
||||
|
||||
void set_viewport(VkViewport &viewport);
|
||||
|
||||
void set_scissor(VkRect2D &scissor);
|
||||
|
||||
float get_avg_draws_per_buffer() const;
|
||||
|
||||
ForwardSubpassSecondaryState &get_state();
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Records the necessary commands to draw the specified range of scene meshes
|
||||
* @param command_buffer The primary command buffer to record
|
||||
* @param nodes The meshes to draw
|
||||
* @param mesh_start Index to the first mesh to draw
|
||||
* @param mesh_end Index to the mesh where recording will stop (not included)
|
||||
* @param thread_index Identifies the resources allocated for this thread
|
||||
*/
|
||||
void record_draw(vkb::core::CommandBufferC &command_buffer,
|
||||
const std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> &nodes,
|
||||
uint32_t mesh_start,
|
||||
uint32_t mesh_end,
|
||||
size_t thread_index = 0);
|
||||
|
||||
/**
|
||||
* @brief Records the necessary commands to draw the specified range of scene meshes
|
||||
* The primary command buffer provided is used to initialize, record, end and return a
|
||||
* pointer to a new secondary command buffer.
|
||||
* @param primary_command_buffer The primary command buffer used to inherit a secondary
|
||||
* @param nodes The meshes to draw
|
||||
* @param mesh_start Index to the first mesh to draw
|
||||
* @param mesh_end Index to the mesh where recording will stop (not included)
|
||||
* @param thread_index Identifies the resources allocated for this thread
|
||||
* @return a pointer to the recorded secondary command buffer
|
||||
*/
|
||||
std::shared_ptr<vkb::core::CommandBufferC> record_draw_secondary(vkb::core::CommandBufferC &primary_command_buffer,
|
||||
const std::vector<std::pair<vkb::sg::Node *, vkb::sg::SubMesh *>> &nodes,
|
||||
uint32_t mesh_start,
|
||||
uint32_t mesh_end,
|
||||
size_t thread_index = 0);
|
||||
|
||||
VkViewport viewport{};
|
||||
|
||||
VkRect2D scissor{};
|
||||
|
||||
vkb::ColorBlendAttachmentState color_blend_attachment{};
|
||||
|
||||
vkb::ColorBlendState color_blend_state{};
|
||||
|
||||
ForwardSubpassSecondaryState state{};
|
||||
|
||||
float avg_draws_per_buffer{0};
|
||||
|
||||
ThreadPool thread_pool;
|
||||
};
|
||||
|
||||
private:
|
||||
virtual void prepare_render_context() override;
|
||||
|
||||
vkb::sg::PerspectiveCamera *camera{nullptr};
|
||||
|
||||
void render(vkb::core::CommandBufferC &command_buffer) override;
|
||||
|
||||
void draw_renderpass(vkb::core::CommandBufferC &primary_command_buffer, vkb::RenderTarget &render_target) override;
|
||||
|
||||
void draw_gui() override;
|
||||
|
||||
int gui_secondary_cmd_buf_count{0};
|
||||
|
||||
uint32_t max_secondary_command_buffer_count{100};
|
||||
|
||||
bool use_secondary_command_buffers{false};
|
||||
|
||||
int gui_command_buffer_reset_mode{0};
|
||||
|
||||
bool gui_multi_threading{false};
|
||||
|
||||
const uint32_t MIN_THREAD_COUNT{4};
|
||||
|
||||
uint32_t max_thread_count{0};
|
||||
};
|
||||
|
||||
std::unique_ptr<vkb::VulkanSampleC> create_command_buffer_usage();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 365 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 364 KiB |
Reference in New Issue
Block a user