/* Copyright (c) 2019-2025, Arm Limited and Contributors * Copyright (c) 2024-2025, NVIDIA CORPORATION. All rights reserved. * * 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 "common/helpers.h" #include "core/buffer.h" #include "core/device.h" namespace vkb { /** * @brief An allocation of vulkan memory; different buffer allocations, * with different offset and size, may come from the same Vulkan buffer */ template class BufferAllocation { public: using DeviceSizeType = typename std::conditional::type; public: BufferAllocation() = default; BufferAllocation(const BufferAllocation &) = default; BufferAllocation(BufferAllocation &&) = default; BufferAllocation &operator=(const BufferAllocation &) = default; BufferAllocation &operator=(BufferAllocation &&) = default; BufferAllocation(vkb::core::Buffer &buffer, DeviceSizeType size, DeviceSizeType offset); bool empty() const; vkb::core::Buffer &get_buffer(); DeviceSizeType get_offset() const; DeviceSizeType get_size() const; void update(const std::vector &data, uint32_t offset = 0); template void update(const T &value, uint32_t offset = 0); private: vkb::core::BufferCpp *buffer = nullptr; vk::DeviceSize offset = 0; vk::DeviceSize size = 0; }; using BufferAllocationC = BufferAllocation; using BufferAllocationCpp = BufferAllocation; template <> inline BufferAllocation::BufferAllocation(vkb::core::BufferCpp &buffer, vk::DeviceSize size, vk::DeviceSize offset) : buffer(&buffer), offset(offset), size(size) {} template <> inline BufferAllocation::BufferAllocation(vkb::core::BufferC &buffer, VkDeviceSize size, VkDeviceSize offset) : buffer(reinterpret_cast(&buffer)), offset(static_cast(offset)), size(static_cast(size)) {} template bool BufferAllocation::empty() const { return size == 0 || buffer == nullptr; } template typename vkb::core::Buffer &BufferAllocation::get_buffer() { assert(buffer && "Invalid buffer pointer"); if constexpr (bindingType == vkb::BindingType::Cpp) { return *buffer; } else { return reinterpret_cast(*buffer); } } template typename BufferAllocation::DeviceSizeType BufferAllocation::get_offset() const { if constexpr (bindingType == vkb::BindingType::Cpp) { return offset; } else { return static_cast(offset); } } template typename BufferAllocation::DeviceSizeType BufferAllocation::get_size() const { if constexpr (bindingType == vkb::BindingType::Cpp) { return size; } else { return static_cast(size); } } template void BufferAllocation::update(const std::vector &data, uint32_t offset) { assert(buffer && "Invalid buffer pointer"); if (offset + data.size() <= size) { buffer->update(data, to_u32(this->offset) + offset); } else { LOGE("Ignore buffer allocation update"); } } template template void BufferAllocation::update(const T &value, uint32_t offset) { update(to_bytes(value), offset); } /** * @brief Helper class which handles multiple allocation from the same underlying Vulkan buffer. */ template class BufferBlock { public: using BufferUsageFlagsType = typename std::conditional::type; using DeviceSizeType = typename std::conditional::type; public: BufferBlock() = delete; BufferBlock(BufferBlock const &rhs) = delete; BufferBlock(BufferBlock &&rhs) = default; BufferBlock &operator=(BufferBlock const &rhs) = delete; BufferBlock &operator=(BufferBlock &&rhs) = default; BufferBlock(vkb::core::Device &device, DeviceSizeType size, BufferUsageFlagsType usage, VmaMemoryUsage memory_usage); /** * @return An usable view on a portion of the underlying buffer */ BufferAllocation allocate(DeviceSizeType size); /** * @brief check if this BufferBlock can allocate a given amount of memory * @param size the number of bytes to check * @return \c true if \a size bytes can be allocated from this \c BufferBlock, otherwise \c false. */ bool can_allocate(DeviceSizeType size) const; DeviceSizeType get_size() const; void reset(); private: /** * @ brief Determine the current aligned offset. * @return The current aligned offset. */ vk::DeviceSize aligned_offset() const; vk::DeviceSize determine_alignment(vk::BufferUsageFlags usage, vk::PhysicalDeviceLimits const &limits) const; private: vkb::core::BufferCpp buffer; vk::DeviceSize alignment = 0; // Memory alignment, it may change according to the usage vk::DeviceSize offset = 0; // Current offset, it increases on every allocation }; using BufferBlockC = BufferBlock; using BufferBlockCpp = BufferBlock; template BufferBlock::BufferBlock(vkb::core::Device &device, DeviceSizeType size, BufferUsageFlagsType usage, VmaMemoryUsage memory_usage) : buffer{device, size, usage, memory_usage} { if constexpr (bindingType == BindingType::Cpp) { alignment = determine_alignment(usage, device.get_gpu().get_properties().limits); } else { alignment = determine_alignment(static_cast(usage), static_cast(device.get_gpu().get_properties().limits)); } } template BufferAllocation BufferBlock::allocate(DeviceSizeType size) { if (can_allocate(size)) { // Move the current offset and return an allocation auto aligned = aligned_offset(); offset = aligned + size; if constexpr (bindingType == vkb::BindingType::Cpp) { return BufferAllocationCpp{buffer, size, aligned}; } else { return BufferAllocationC{reinterpret_cast(buffer), static_cast(size), static_cast(aligned)}; } } // No more space available from the underlying buffer, return empty allocation return BufferAllocation{}; } template bool BufferBlock::can_allocate(DeviceSizeType size) const { assert(size > 0 && "Allocation size must be greater than zero"); return (aligned_offset() + size <= buffer.get_size()); } template typename BufferBlock::DeviceSizeType BufferBlock::get_size() const { return buffer.get_size(); } template void BufferBlock::reset() { offset = 0; } template vk::DeviceSize BufferBlock::aligned_offset() const { return (offset + alignment - 1) & ~(alignment - 1); } template vk::DeviceSize BufferBlock::determine_alignment(vk::BufferUsageFlags usage, vk::PhysicalDeviceLimits const &limits) const { if (usage == vk::BufferUsageFlagBits::eUniformBuffer) { return limits.minUniformBufferOffsetAlignment; } else if (usage == vk::BufferUsageFlagBits::eStorageBuffer) { return limits.minStorageBufferOffsetAlignment; } else if (usage == vk::BufferUsageFlagBits::eUniformTexelBuffer) { return limits.minTexelBufferOffsetAlignment; } else if (usage == vk::BufferUsageFlagBits::eIndexBuffer || usage == vk::BufferUsageFlagBits::eVertexBuffer || usage == vk::BufferUsageFlagBits::eIndirectBuffer) { // Used to calculate the offset, required when allocating memory (its value should be power of 2) return 16; } else { throw std::runtime_error("Usage not recognised"); } } /** * @brief A pool of buffer blocks for a specific usage. * It may contain inactive blocks that can be recycled. * * BufferPool is a linear allocator for buffer chunks, it gives you a view of the size you want. * A BufferBlock is the corresponding VkBuffer and you can get smaller offsets inside it. * Since a shader cannot specify dynamic UBOs, it has to be done from the code * (set_resource_dynamic). * * When a new frame starts, buffer blocks are returned: the offset is reset and contents are * overwritten. The minimum allocation size is 256 kb, if you ask for more you get a dedicated * buffer allocation. * * We re-use descriptor sets: we only need one for the corresponding buffer infos (and we only * have one VkBuffer per BufferBlock), then it is bound and we use dynamic offsets. */ template class BufferPool { public: using BufferUsageFlagsType = typename std::conditional::type; using DeviceSizeType = typename std::conditional::type; public: BufferPool( vkb::core::Device &device, DeviceSizeType block_size, BufferUsageFlagsType usage, VmaMemoryUsage memory_usage = VMA_MEMORY_USAGE_CPU_TO_GPU); BufferBlock &request_buffer_block(DeviceSizeType minimum_size, bool minimal = false); void reset(); private: vkb::core::DeviceCpp &device; std::vector> buffer_blocks; /// List of blocks requested (need to be pointers in order to keep their address constant on vector resizing) vk::DeviceSize block_size = 0; /// Minimum size of the blocks vk::BufferUsageFlags usage; VmaMemoryUsage memory_usage{}; }; using BufferPoolC = BufferPool; using BufferPoolCpp = BufferPool; template BufferPool::BufferPool(vkb::core::Device &device, DeviceSizeType block_size, BufferUsageFlagsType usage, VmaMemoryUsage memory_usage) : device{reinterpret_cast(device)}, block_size{block_size}, usage{usage}, memory_usage{memory_usage} { } template BufferBlock &BufferPool::request_buffer_block(DeviceSizeType minimum_size, bool minimal) { // Find a block in the range of the blocks which can fit the minimum size auto it = minimal ? std::ranges::find_if(buffer_blocks, [&minimum_size](auto const &buffer_block) { return (buffer_block->get_size() == minimum_size) && buffer_block->can_allocate(minimum_size); }) : std::ranges::find_if(buffer_blocks, [&minimum_size](auto const &buffer_block) { return buffer_block->can_allocate(minimum_size); }); if (it == buffer_blocks.end()) { LOGD("Building #{} buffer block ({})", buffer_blocks.size(), vk::to_string(usage)); vk::DeviceSize new_block_size = minimal ? minimum_size : std::max(block_size, minimum_size); // Create a new block and get the iterator on it it = buffer_blocks.emplace(buffer_blocks.end(), std::make_unique(device, new_block_size, usage, memory_usage)); } if constexpr (bindingType == vkb::BindingType::Cpp) { return *it->get(); } else { return reinterpret_cast(*it->get()); } } template void BufferPool::reset() { // Attention: Resetting the BufferPool is not supposed to clear the BufferBlocks, but just reset them! // The actual VkBuffers are used to hash the DescriptorSet in RenderFrame::request_descriptor_set. // Don't know (for now) how that works with resetted buffers! for (auto &buffer_block : buffer_blocks) { buffer_block->reset(); } } } // namespace vkb