Files
Vulkan-Samples/framework/buffer_pool.h
T
2025-09-04 10:54:47 +08:00

378 lines
13 KiB
C++

/* 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 <vkb::BindingType bindingType>
class BufferAllocation
{
public:
using DeviceSizeType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::DeviceSize, VkDeviceSize>::type;
public:
BufferAllocation() = default;
BufferAllocation(const BufferAllocation &) = default;
BufferAllocation(BufferAllocation &&) = default;
BufferAllocation &operator=(const BufferAllocation &) = default;
BufferAllocation &operator=(BufferAllocation &&) = default;
BufferAllocation(vkb::core::Buffer<bindingType> &buffer, DeviceSizeType size, DeviceSizeType offset);
bool empty() const;
vkb::core::Buffer<bindingType> &get_buffer();
DeviceSizeType get_offset() const;
DeviceSizeType get_size() const;
void update(const std::vector<uint8_t> &data, uint32_t offset = 0);
template <typename T>
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<vkb::BindingType::C>;
using BufferAllocationCpp = BufferAllocation<vkb::BindingType::Cpp>;
template <>
inline BufferAllocation<vkb::BindingType::Cpp>::BufferAllocation(vkb::core::BufferCpp &buffer, vk::DeviceSize size, vk::DeviceSize offset) :
buffer(&buffer),
offset(offset),
size(size)
{}
template <>
inline BufferAllocation<vkb::BindingType::C>::BufferAllocation(vkb::core::BufferC &buffer, VkDeviceSize size, VkDeviceSize offset) :
buffer(reinterpret_cast<vkb::core::BufferCpp *>(&buffer)),
offset(static_cast<vk::DeviceSize>(offset)),
size(static_cast<vk::DeviceSize>(size))
{}
template <vkb::BindingType bindingType>
bool BufferAllocation<bindingType>::empty() const
{
return size == 0 || buffer == nullptr;
}
template <vkb::BindingType bindingType>
typename vkb::core::Buffer<bindingType> &BufferAllocation<bindingType>::get_buffer()
{
assert(buffer && "Invalid buffer pointer");
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return *buffer;
}
else
{
return reinterpret_cast<vkb::core::BufferC &>(*buffer);
}
}
template <vkb::BindingType bindingType>
typename BufferAllocation<bindingType>::DeviceSizeType BufferAllocation<bindingType>::get_offset() const
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return offset;
}
else
{
return static_cast<VkDeviceSize>(offset);
}
}
template <vkb::BindingType bindingType>
typename BufferAllocation<bindingType>::DeviceSizeType BufferAllocation<bindingType>::get_size() const
{
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return size;
}
else
{
return static_cast<VkDeviceSize>(size);
}
}
template <vkb::BindingType bindingType>
void BufferAllocation<bindingType>::update(const std::vector<uint8_t> &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 <vkb::BindingType bindingType>
template <typename T>
void BufferAllocation<bindingType>::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 <vkb::BindingType bindingType>
class BufferBlock
{
public:
using BufferUsageFlagsType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::BufferUsageFlags, VkBufferUsageFlags>::type;
using DeviceSizeType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::DeviceSize, VkDeviceSize>::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<bindingType> &device, DeviceSizeType size, BufferUsageFlagsType usage, VmaMemoryUsage memory_usage);
/**
* @return An usable view on a portion of the underlying buffer
*/
BufferAllocation<bindingType> 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<vkb::BindingType::C>;
using BufferBlockCpp = BufferBlock<vkb::BindingType::Cpp>;
template <vkb::BindingType bindingType>
BufferBlock<bindingType>::BufferBlock(vkb::core::Device<bindingType> &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<vk::BufferUsageFlags>(usage), static_cast<vk::PhysicalDeviceLimits>(device.get_gpu().get_properties().limits));
}
}
template <vkb::BindingType bindingType>
BufferAllocation<bindingType> BufferBlock<bindingType>::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<vkb::core::BufferC &>(buffer), static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(aligned)};
}
}
// No more space available from the underlying buffer, return empty allocation
return BufferAllocation<bindingType>{};
}
template <vkb::BindingType bindingType>
bool BufferBlock<bindingType>::can_allocate(DeviceSizeType size) const
{
assert(size > 0 && "Allocation size must be greater than zero");
return (aligned_offset() + size <= buffer.get_size());
}
template <vkb::BindingType bindingType>
typename BufferBlock<bindingType>::DeviceSizeType BufferBlock<bindingType>::get_size() const
{
return buffer.get_size();
}
template <vkb::BindingType bindingType>
void BufferBlock<bindingType>::reset()
{
offset = 0;
}
template <vkb::BindingType bindingType>
vk::DeviceSize BufferBlock<bindingType>::aligned_offset() const
{
return (offset + alignment - 1) & ~(alignment - 1);
}
template <vkb::BindingType bindingType>
vk::DeviceSize BufferBlock<bindingType>::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 <vkb::BindingType bindingType>
class BufferPool
{
public:
using BufferUsageFlagsType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::BufferUsageFlags, VkBufferUsageFlags>::type;
using DeviceSizeType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::DeviceSize, VkDeviceSize>::type;
public:
BufferPool(
vkb::core::Device<bindingType> &device, DeviceSizeType block_size, BufferUsageFlagsType usage, VmaMemoryUsage memory_usage = VMA_MEMORY_USAGE_CPU_TO_GPU);
BufferBlock<bindingType> &request_buffer_block(DeviceSizeType minimum_size, bool minimal = false);
void reset();
private:
vkb::core::DeviceCpp &device;
std::vector<std::unique_ptr<BufferBlockCpp>> 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<vkb::BindingType::C>;
using BufferPoolCpp = BufferPool<vkb::BindingType::Cpp>;
template <vkb::BindingType bindingType>
BufferPool<bindingType>::BufferPool(vkb::core::Device<bindingType> &device,
DeviceSizeType block_size,
BufferUsageFlagsType usage,
VmaMemoryUsage memory_usage) :
device{reinterpret_cast<vkb::core::DeviceCpp &>(device)}, block_size{block_size}, usage{usage}, memory_usage{memory_usage}
{
}
template <vkb::BindingType bindingType>
BufferBlock<bindingType> &BufferPool<bindingType>::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<BufferBlockCpp>(device, new_block_size, usage, memory_usage));
}
if constexpr (bindingType == vkb::BindingType::Cpp)
{
return *it->get();
}
else
{
return reinterpret_cast<BufferBlockC &>(*it->get());
}
}
template <vkb::BindingType bindingType>
void BufferPool<bindingType>::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