/* 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 class RenderFrame { public: using BufferUsageFlagsType = typename std::conditional::type; using CommandBufferLevelType = typename std::conditional::type; using DescriptorBufferInfoType = typename std::conditional::type; using DescriptorImageInfoType = typename std::conditional::type; using DescriptorSetType = typename std::conditional::type; using DeviceSizeType = typename std::conditional::type; using FenceType = typename std::conditional::type; using SemaphoreType = typename std::conditional::type; using DescriptorSetLayoutType = typename std::conditional::type; using FencePoolType = typename std::conditional::type; using QueueType = typename std::conditional::type; using RenderTargetType = typename std::conditional::type; using SemaphorePoolType = typename std::conditional::type; public: RenderFrame(vkb::core::Device &device, std::unique_ptr &&render_target, size_t thread_count = 1); RenderFrame(RenderFrame const &) = delete; RenderFrame(RenderFrame &&) = default; RenderFrame &operator=(RenderFrame const &) = delete; RenderFrame &operator=(RenderFrame &&) = 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 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 &get_command_pool( QueueType const &queue, vkb::CommandBufferResetMode reset_mode = vkb::CommandBufferResetMode::ResetPool, size_t thread_index = 0); vkb::core::Device &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 const &buffer_infos, BindingMap 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 &&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 &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 const &buffer_infos, BindingMap const &image_infos, bool update_after_bind, size_t thread_index = 0); private: vkb::core::DeviceCpp &device; std::map>> buffer_pools; std::map> command_pools; // Commands pools per queue family index std::vector> descriptor_pools; // Descriptor pools per thread std::vector> descriptor_sets; // Descriptor sets per thread vkb::HPPFencePool fence_pool; vkb::HPPSemaphorePool semaphore_pool; std::unique_ptr swapchain_render_target; size_t thread_count; BufferAllocationStrategy buffer_allocation_strategy = BufferAllocationStrategy::MultipleAllocationsPerBuffer; DescriptorManagementStrategy descriptor_management_strategy = DescriptorManagementStrategy::StoreInCache; }; using RenderFrameC = RenderFrame; using RenderFrameCpp = RenderFrame; template inline RenderFrame::RenderFrame(vkb::core::Device &device_, std::unique_ptr &&render_target, size_t thread_count) : device(reinterpret_cast(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 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>{}); 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 inline BufferAllocation RenderFrame::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(usage), static_cast(size), thread_index); return *reinterpret_cast(&buffer_allocation); } } template inline vkb::BufferAllocationCpp RenderFrame::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 inline void RenderFrame::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 inline std::vector &RenderFrame::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{}); 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(this), i, reset_mode); } return command_pool_it->second; } template inline vkb::core::CommandPool & RenderFrame::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( get_command_pool_impl(reinterpret_cast(queue), reset_mode, thread_index)); } } template inline vkb::core::CommandPoolCpp & RenderFrame::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 inline typename vkb::core::Device &RenderFrame::get_device() { if constexpr (bindingType == vkb::BindingType::Cpp) { return device; } else { return reinterpret_cast(device); } } template inline typename RenderFrame::FencePoolType &RenderFrame::get_fence_pool() { if constexpr (bindingType == vkb::BindingType::Cpp) { return fence_pool; } else { return reinterpret_cast(fence_pool); } } template inline typename RenderFrame::FencePoolType const &RenderFrame::get_fence_pool() const { if constexpr (bindingType == vkb::BindingType::Cpp) { return fence_pool; } else { return reinterpret_cast(fence_pool); } } template inline typename RenderFrame::RenderTargetType &RenderFrame::get_render_target() { if constexpr (bindingType == vkb::BindingType::Cpp) { return *swapchain_render_target; } else { return reinterpret_cast(*swapchain_render_target); } } template inline typename RenderFrame::RenderTargetType const &RenderFrame::get_render_target() const { if constexpr (bindingType == vkb::BindingType::Cpp) { return *swapchain_render_target; } else { return reinterpret_cast(*swapchain_render_target); } } template inline typename RenderFrame::SemaphorePoolType &RenderFrame::get_semaphore_pool() { if constexpr (bindingType == vkb::BindingType::Cpp) { return semaphore_pool; } else { return reinterpret_cast(semaphore_pool); } } template inline typename RenderFrame::SemaphorePoolType const &RenderFrame::get_semaphore_pool() const { if constexpr (bindingType == vkb::BindingType::Cpp) { return semaphore_pool; } else { return reinterpret_cast(semaphore_pool); } } template inline typename RenderFrame::DescriptorSetType RenderFrame::request_descriptor_set(DescriptorSetLayoutType const &descriptor_set_layout, BindingMap const &buffer_infos, BindingMap 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(request_descriptor_set_impl(reinterpret_cast(descriptor_set_layout), reinterpret_cast const &>(buffer_infos), reinterpret_cast const &>(image_infos), update_after_bind, thread_index)); } } template inline vk::DescriptorSet RenderFrame::request_descriptor_set_impl(vkb::core::HPPDescriptorSetLayout const &descriptor_set_layout, BindingMap const &buffer_infos, BindingMap 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 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 inline void RenderFrame::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 inline void RenderFrame::set_buffer_allocation_strategy(BufferAllocationStrategy new_strategy) { buffer_allocation_strategy = new_strategy; } template inline void RenderFrame::set_descriptor_management_strategy(DescriptorManagementStrategy new_strategy) { descriptor_management_strategy = new_strategy; } template inline void RenderFrame::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 inline void RenderFrame::update_render_target(std::unique_ptr &&render_target) { if constexpr (bindingType == vkb::BindingType::Cpp) { swapchain_render_target = std::move(render_target); } else { swapchain_render_target.reset(reinterpret_cast(render_target.release())); } } } // namespace rendering } // namespace vkb