Files
face_sdk/vulkan/framework/rendering/render_frame.h
T

563 lines
24 KiB
C++

/* 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/hpp_resource_caching.h"
#include "core/command_pool.h"
#include "core/hpp_queue.h"
#include "core/queue.h"
#include "hpp_semaphore_pool.h"
namespace vkb
{
namespace rendering
{
enum BufferAllocationStrategy
{
OneAllocationPerBuffer,
MultipleAllocationsPerBuffer
};
enum DescriptorManagementStrategy
{
StoreInCache,
CreateDirectly
};
/**
* @brief RenderFrame is a container for per-frame data, including BufferPool objects,
* synchronization primitives (semaphores, fences) and the swapchain RenderTarget.
*
* When creating a RenderTarget, we need to provide images that will be used as attachments
* within a RenderPass. The RenderFrame is responsible for creating a RenderTarget using
* RenderTarget::CreateFunc. A custom RenderTarget::CreateFunc can be provided if a different
* render target is required.
*
* A RenderFrame cannot be destroyed individually since frames are managed by the RenderContext,
* the whole context must be destroyed. This is because each RenderFrame holds Vulkan objects
* such as the swapchain image.
*/
template <vkb::BindingType bindingType>
class RenderFrame
{
public:
using BufferUsageFlagsType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::BufferUsageFlags, VkBufferUsageFlags>::type;
using CommandBufferLevelType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::CommandBufferLevel, VkCommandBufferLevel>::type;
using DescriptorBufferInfoType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::DescriptorBufferInfo, VkDescriptorBufferInfo>::type;
using DescriptorImageInfoType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::DescriptorImageInfo, VkDescriptorImageInfo>::type;
using DescriptorSetType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::DescriptorSet, VkDescriptorSet>::type;
using DeviceSizeType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::DeviceSize, VkDeviceSize>::type;
using FenceType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::Fence, VkFence>::type;
using SemaphoreType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::Semaphore, VkSemaphore>::type;
using DescriptorSetLayoutType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vkb::core::HPPDescriptorSetLayout, vkb::DescriptorSetLayout>::type;
using FencePoolType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vkb::HPPFencePool, vkb::FencePool>::type;
using QueueType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vkb::core::HPPQueue, vkb::Queue>::type;
using RenderTargetType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vkb::rendering::HPPRenderTarget, vkb::RenderTarget>::type;
using SemaphorePoolType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vkb::HPPSemaphorePool, vkb::SemaphorePool>::type;
public:
RenderFrame(vkb::core::Device<bindingType> &device, std::unique_ptr<RenderTargetType> &&render_target, size_t thread_count = 1);
RenderFrame(RenderFrame<bindingType> const &) = delete;
RenderFrame(RenderFrame<bindingType> &&) = default;
RenderFrame &operator=(RenderFrame<bindingType> const &) = delete;
RenderFrame &operator=(RenderFrame<bindingType> &&) = default;
/**
* @param usage Usage of the buffer
* @param size Amount of memory required
* @param thread_index Index of the buffer pool to be used by the current thread
* @return The requested allocation, it may be empty
*/
vkb::BufferAllocation<bindingType> allocate_buffer(BufferUsageFlagsType usage, DeviceSizeType size, size_t thread_index = 0);
void clear_descriptors();
/**
* @brief Get the command pool of the active frame
* A frame should be active at the moment of requesting it
* @param queue The queue command buffers will be submitted on
* @param reset_mode Indicate how the command buffer will be used, may trigger a pool re-creation to set necessary flags
* @param thread_index Selects the thread's command pool used to manage the buffer
* @return The command pool related to the current active frame
*/
vkb::core::CommandPool<bindingType> &get_command_pool(
QueueType const &queue, vkb::CommandBufferResetMode reset_mode = vkb::CommandBufferResetMode::ResetPool, size_t thread_index = 0);
vkb::core::Device<bindingType> &get_device();
FencePoolType &get_fence_pool();
FencePoolType const &get_fence_pool() const;
RenderTargetType &get_render_target();
RenderTargetType const &get_render_target() const;
SemaphorePoolType &get_semaphore_pool();
SemaphorePoolType const &get_semaphore_pool() const;
DescriptorSetType request_descriptor_set(DescriptorSetLayoutType const &descriptor_set_layout,
BindingMap<DescriptorBufferInfoType> const &buffer_infos,
BindingMap<DescriptorImageInfoType> const &image_infos,
bool update_after_bind,
size_t thread_index = 0);
void reset();
/**
* @brief Sets a new buffer allocation strategy
* @param new_strategy The new buffer allocation strategy
*/
void set_buffer_allocation_strategy(BufferAllocationStrategy new_strategy);
/**
* @brief Sets a new descriptor set management strategy
* @param new_strategy The new descriptor set management strategy
*/
void set_descriptor_management_strategy(DescriptorManagementStrategy new_strategy);
/**
* @brief Updates all the descriptor sets in the current frame at a specific thread index
*/
void update_descriptor_sets(size_t thread_index = 0);
/**
* @brief Called when the swapchain changes
* @param render_target A new render target with updated images
*/
void update_render_target(std::unique_ptr<RenderTargetType> &&render_target);
private:
vkb::BufferAllocationCpp allocate_buffer_impl(vk::BufferUsageFlags usage, vk::DeviceSize size, size_t thread_index);
vkb::core::CommandPoolCpp &get_command_pool_impl(vkb::core::HPPQueue const &queue, vkb::CommandBufferResetMode reset_mode, size_t thread_index);
/**
* @brief Retrieve the frame's command pool(s)
* @param queue The queue command buffers will be submitted on
* @param reset_mode Indicate how the command buffers will be reset after execution,
* may trigger a pool re-creation to set necessary flags
* @return The frame's command pool(s)
*/
std::vector<vkb::core::CommandPoolCpp> &get_command_pools(const vkb::core::HPPQueue &queue, vkb::CommandBufferResetMode reset_mode);
vk::DescriptorSet request_descriptor_set_impl(vkb::core::HPPDescriptorSetLayout const &descriptor_set_layout,
BindingMap<vk::DescriptorBufferInfo> const &buffer_infos,
BindingMap<vk::DescriptorImageInfo> const &image_infos,
bool update_after_bind,
size_t thread_index = 0);
private:
vkb::core::DeviceCpp &device;
std::map<vk::BufferUsageFlags, std::vector<std::pair<vkb::BufferPoolCpp, vkb::BufferBlockCpp *>>> buffer_pools;
std::map<uint32_t, std::vector<vkb::core::CommandPoolCpp>> command_pools; // Commands pools per queue family index
std::vector<std::unordered_map<std::size_t, vkb::core::HPPDescriptorPool>> descriptor_pools; // Descriptor pools per thread
std::vector<std::unordered_map<std::size_t, vkb::core::HPPDescriptorSet>> descriptor_sets; // Descriptor sets per thread
vkb::HPPFencePool fence_pool;
vkb::HPPSemaphorePool semaphore_pool;
std::unique_ptr<vkb::rendering::HPPRenderTarget> swapchain_render_target;
size_t thread_count;
BufferAllocationStrategy buffer_allocation_strategy = BufferAllocationStrategy::MultipleAllocationsPerBuffer;
DescriptorManagementStrategy descriptor_management_strategy = DescriptorManagementStrategy::StoreInCache;
};
using RenderFrameC = RenderFrame<vkb::BindingType::C>;
using RenderFrameCpp = RenderFrame<vkb::BindingType::Cpp>;
template <vkb::BindingType bindingType>
inline RenderFrame<bindingType>::RenderFrame(vkb::core::Device<bindingType> &device_,
std::unique_ptr<RenderTargetType> &&render_target,
size_t thread_count) :
device(reinterpret_cast<vkb::core::DeviceCpp &>(device_)), fence_pool{device}, semaphore_pool{device}, thread_count{thread_count}, descriptor_pools(thread_count), descriptor_sets(thread_count)
{
static constexpr uint32_t BUFFER_POOL_BLOCK_SIZE = 256; // Block size of a buffer pool in kilobytes
// A map of the supported usages to a multiplier for the BUFFER_POOL_BLOCK_SIZE
static const std::unordered_map<vk::BufferUsageFlags, uint32_t> supported_usage_map = {
{vk::BufferUsageFlagBits::eUniformBuffer, 1},
{vk::BufferUsageFlagBits::eStorageBuffer, 2}, // x2 the size of BUFFER_POOL_BLOCK_SIZE since SSBOs are normally much larger than other types of buffers
{vk::BufferUsageFlagBits::eVertexBuffer, 1},
{vk::BufferUsageFlagBits::eIndexBuffer, 1}};
update_render_target(std::move(render_target));
for (auto &usage_it : supported_usage_map)
{
auto [buffer_pools_it, inserted] = buffer_pools.emplace(usage_it.first, std::vector<std::pair<vkb::BufferPoolCpp, vkb::BufferBlockCpp *>>{});
if (!inserted)
{
throw std::runtime_error("Failed to insert buffer pool");
}
for (size_t i = 0; i < thread_count; ++i)
{
buffer_pools_it->second.push_back(
std::make_pair(vkb::BufferPoolCpp{device, BUFFER_POOL_BLOCK_SIZE * 1024 * usage_it.second, usage_it.first}, nullptr));
}
}
}
template <vkb::BindingType bindingType>
inline BufferAllocation<bindingType> RenderFrame<bindingType>::allocate_buffer(BufferUsageFlagsType usage, DeviceSizeType size, size_t thread_index)
{
assert(thread_index < thread_count && "Thread index is out of bounds");
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return allocate_buffer_impl(usage, size, thread_index);
}
else
{
vkb::BufferAllocationCpp buffer_allocation =
allocate_buffer_impl(static_cast<vk::BufferUsageFlags>(usage), static_cast<vk::DeviceSize>(size), thread_index);
return *reinterpret_cast<vkb::BufferAllocationC *>(&buffer_allocation);
}
}
template <vkb::BindingType bindingType>
inline vkb::BufferAllocationCpp RenderFrame<bindingType>::allocate_buffer_impl(vk::BufferUsageFlags usage, vk::DeviceSize size, size_t thread_index)
{
// Find a pool for this usage
auto buffer_pool_it = buffer_pools.find(usage);
if (buffer_pool_it == buffer_pools.end())
{
LOGE("No buffer pool for buffer usage {} ", vk::to_string(usage));
return vkb::BufferAllocationCpp{};
}
assert(thread_index < buffer_pool_it->second.size());
auto &buffer_pool = buffer_pool_it->second[thread_index].first;
auto &buffer_block = buffer_pool_it->second[thread_index].second;
bool want_minimal_block = (buffer_allocation_strategy == BufferAllocationStrategy::OneAllocationPerBuffer);
if (want_minimal_block || !buffer_block || !buffer_block->can_allocate(size))
{
// If we are creating a buffer for each allocation of there is no block associated with the pool or the current block is too small
// for this allocation, request a new buffer block
buffer_block = &buffer_pool.request_buffer_block(size, want_minimal_block);
}
return buffer_block->allocate(to_u32(size));
}
template <vkb::BindingType bindingType>
inline void RenderFrame<bindingType>::clear_descriptors()
{
for (auto &desc_sets_per_thread : descriptor_sets)
{
desc_sets_per_thread.clear();
}
for (auto &desc_pools_per_thread : descriptor_pools)
{
for (auto &desc_pool : desc_pools_per_thread)
{
desc_pool.second.reset();
}
}
}
template <vkb::BindingType bindingType>
inline std::vector<vkb::core::CommandPoolCpp> &RenderFrame<bindingType>::get_command_pools(const vkb::core::HPPQueue &queue,
vkb::CommandBufferResetMode reset_mode)
{
auto command_pool_it = command_pools.find(queue.get_family_index());
if (command_pool_it != command_pools.end())
{
assert(!command_pool_it->second.empty());
if (command_pool_it->second[0].get_reset_mode() != reset_mode)
{
device.get_handle().waitIdle();
// Delete pools
command_pools.erase(command_pool_it);
}
else
{
return command_pool_it->second;
}
}
bool inserted = false;
std::tie(command_pool_it, inserted) = command_pools.emplace(queue.get_family_index(), std::vector<vkb::core::CommandPoolCpp>{});
if (!inserted)
{
throw std::runtime_error("Failed to insert command pool");
}
for (size_t i = 0; i < thread_count; i++)
{
command_pool_it->second.emplace_back(device, queue.get_family_index(), reinterpret_cast<vkb::rendering::RenderFrameCpp *>(this), i, reset_mode);
}
return command_pool_it->second;
}
template <vkb::BindingType bindingType>
inline vkb::core::CommandPool<bindingType> &
RenderFrame<bindingType>::get_command_pool(QueueType const &queue, vkb::CommandBufferResetMode reset_mode, size_t thread_index)
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return get_command_pool_impl(queue, reset_mode, thread_index);
}
else
{
return reinterpret_cast<vkb::core::CommandPoolC &>(
get_command_pool_impl(reinterpret_cast<vkb::core::HPPQueue const &>(queue), reset_mode, thread_index));
}
}
template <vkb::BindingType bindingType>
inline vkb::core::CommandPoolCpp &
RenderFrame<bindingType>::get_command_pool_impl(vkb::core::HPPQueue const &queue, vkb::CommandBufferResetMode reset_mode, size_t thread_index)
{
assert(thread_index < thread_count && "Thread index is out of bounds");
auto &command_pools = get_command_pools(queue, reset_mode);
auto command_pool_it = std::ranges::find_if(
command_pools, [&thread_index](vkb::core::CommandPoolCpp &cmd_pool) { return cmd_pool.get_thread_index() == thread_index; });
assert(command_pool_it != command_pools.end());
return *command_pool_it;
}
template <vkb::BindingType bindingType>
inline typename vkb::core::Device<bindingType> &RenderFrame<bindingType>::get_device()
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return device;
}
else
{
return reinterpret_cast<vkb::core::DeviceC &>(device);
}
}
template <vkb::BindingType bindingType>
inline typename RenderFrame<bindingType>::FencePoolType &RenderFrame<bindingType>::get_fence_pool()
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return fence_pool;
}
else
{
return reinterpret_cast<vkb::FencePool &>(fence_pool);
}
}
template <vkb::BindingType bindingType>
inline typename RenderFrame<bindingType>::FencePoolType const &RenderFrame<bindingType>::get_fence_pool() const
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return fence_pool;
}
else
{
return reinterpret_cast<vkb::FencePool const &>(fence_pool);
}
}
template <vkb::BindingType bindingType>
inline typename RenderFrame<bindingType>::RenderTargetType &RenderFrame<bindingType>::get_render_target()
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return *swapchain_render_target;
}
else
{
return reinterpret_cast<vkb::RenderTarget &>(*swapchain_render_target);
}
}
template <vkb::BindingType bindingType>
inline typename RenderFrame<bindingType>::RenderTargetType const &RenderFrame<bindingType>::get_render_target() const
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return *swapchain_render_target;
}
else
{
return reinterpret_cast<vkb::RenderTarget const &>(*swapchain_render_target);
}
}
template <vkb::BindingType bindingType>
inline typename RenderFrame<bindingType>::SemaphorePoolType &RenderFrame<bindingType>::get_semaphore_pool()
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return semaphore_pool;
}
else
{
return reinterpret_cast<vkb::SemaphorePool &>(semaphore_pool);
}
}
template <vkb::BindingType bindingType>
inline typename RenderFrame<bindingType>::SemaphorePoolType const &RenderFrame<bindingType>::get_semaphore_pool() const
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return semaphore_pool;
}
else
{
return reinterpret_cast<vkb::SemaphorePool const &>(semaphore_pool);
}
}
template <vkb::BindingType bindingType>
inline typename RenderFrame<bindingType>::DescriptorSetType RenderFrame<bindingType>::request_descriptor_set(DescriptorSetLayoutType const &descriptor_set_layout,
BindingMap<DescriptorBufferInfoType> const &buffer_infos,
BindingMap<DescriptorImageInfoType> const &image_infos,
bool update_after_bind,
size_t thread_index)
{
assert(thread_index < thread_count && "Thread index is out of bounds");
assert(thread_index < descriptor_pools.size());
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return request_descriptor_set_impl(descriptor_set_layout, buffer_infos, image_infos, update_after_bind, thread_index);
}
else
{
return static_cast<VkDescriptorSet>(request_descriptor_set_impl(reinterpret_cast<vkb::core::HPPDescriptorSetLayout const &>(descriptor_set_layout),
reinterpret_cast<BindingMap<vk::DescriptorBufferInfo> const &>(buffer_infos),
reinterpret_cast<BindingMap<vk::DescriptorImageInfo> const &>(image_infos),
update_after_bind,
thread_index));
}
}
template <vkb::BindingType bindingType>
inline vk::DescriptorSet RenderFrame<bindingType>::request_descriptor_set_impl(vkb::core::HPPDescriptorSetLayout const &descriptor_set_layout,
BindingMap<vk::DescriptorBufferInfo> const &buffer_infos,
BindingMap<vk::DescriptorImageInfo> const &image_infos,
bool update_after_bind,
size_t thread_index)
{
auto &descriptor_pool = vkb::common::request_resource(device, nullptr, descriptor_pools[thread_index], descriptor_set_layout);
if (descriptor_management_strategy == DescriptorManagementStrategy::StoreInCache)
{
// The bindings we want to update before binding, if empty we update all bindings
std::set<uint32_t> bindings_to_update;
// If update after bind is enabled, we store the binding index of each binding that need to be updated before being bound
if (update_after_bind)
{
auto aggregate_binding_to_update = [&bindings_to_update, &descriptor_set_layout](const auto &infos_map) {
for (const auto &[binding_index, ignored] : infos_map)
{
if (!(descriptor_set_layout.get_layout_binding_flag(binding_index) & vk::DescriptorBindingFlagBits::eUpdateAfterBind))
{
bindings_to_update.insert(binding_index);
}
}
};
aggregate_binding_to_update(buffer_infos);
aggregate_binding_to_update(image_infos);
}
// Request a descriptor set from the render frame, and write the buffer infos and image infos of all the specified bindings
assert(thread_index < descriptor_sets.size());
auto &descriptor_set =
vkb::common::request_resource(device, nullptr, descriptor_sets[thread_index], descriptor_set_layout, descriptor_pool, buffer_infos, image_infos);
descriptor_set.update({bindings_to_update.begin(), bindings_to_update.end()});
return descriptor_set.get_handle();
}
else
{
// Request a descriptor pool, allocate a descriptor set, write buffer and image data to it
vkb::core::HPPDescriptorSet descriptor_set{device, descriptor_set_layout, descriptor_pool, buffer_infos, image_infos};
descriptor_set.apply_writes();
return descriptor_set.get_handle();
}
}
template <vkb::BindingType bindingType>
inline void RenderFrame<bindingType>::reset()
{
VK_CHECK(fence_pool.wait());
fence_pool.reset();
for (auto &command_pools_per_queue : command_pools)
{
for (auto &command_pool : command_pools_per_queue.second)
{
command_pool.reset_pool();
}
}
for (auto &buffer_pools_per_usage : buffer_pools)
{
for (auto &buffer_pool : buffer_pools_per_usage.second)
{
buffer_pool.first.reset();
buffer_pool.second = nullptr;
}
}
semaphore_pool.reset();
if (descriptor_management_strategy == DescriptorManagementStrategy::CreateDirectly)
{
clear_descriptors();
}
}
template <vkb::BindingType bindingType>
inline void RenderFrame<bindingType>::set_buffer_allocation_strategy(BufferAllocationStrategy new_strategy)
{
buffer_allocation_strategy = new_strategy;
}
template <vkb::BindingType bindingType>
inline void RenderFrame<bindingType>::set_descriptor_management_strategy(DescriptorManagementStrategy new_strategy)
{
descriptor_management_strategy = new_strategy;
}
template <vkb::BindingType bindingType>
inline void RenderFrame<bindingType>::update_descriptor_sets(size_t thread_index)
{
assert(thread_index < descriptor_sets.size());
auto &thread_descriptor_sets = descriptor_sets[thread_index];
for (auto &descriptor_set_it : thread_descriptor_sets)
{
descriptor_set_it.second.update();
}
}
template <vkb::BindingType bindingType>
inline void RenderFrame<bindingType>::update_render_target(std::unique_ptr<RenderTargetType> &&render_target)
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
swapchain_render_target = std::move(render_target);
}
else
{
swapchain_render_target.reset(reinterpret_cast<vkb::rendering::HPPRenderTarget *>(render_target.release()));
}
}
} // namespace rendering
} // namespace vkb