/* Copyright (c) 2021-2025, NVIDIA CORPORATION. All rights reserved. * Copyright (c) 2024-2025, Bradley Austin Davis. 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/error.h" #include "core/physical_device.h" #include "core/vulkan_resource.h" namespace vkb { namespace allocated { /** * @brief Retrieves a reference to the VMA allocator singleton. It will hold an opaque handle to the VMA * allocator between calls to `init` and `shutdown`. Otherwise it contains a null pointer. * @return A reference to the VMA allocator singleton handle. */ VmaAllocator &get_memory_allocator(); /** * @brief The non-templatized VMA initializer function, referenced by the template version to smooth * over the differences between the `vkb::Device` and `vkb::core::HPPDevice` classes. * Idempotent, but should be paired with `shutdown`. * @param create_info The VMA allocator create info. */ void init(const VmaAllocatorCreateInfo &create_info); /** * @brief Initializes the VMA allocator with the specified device, expressed * as the `vkb` wrapper class, which might be `vkb::Device` or `vkb::core::HPPDevice`. * @tparam DeviceType The type of the device. * @param device The Vulkan device. */ template void init(const DeviceType &device) { VmaVulkanFunctions vma_vulkan_func{}; vma_vulkan_func.vkGetInstanceProcAddr = vkGetInstanceProcAddr; vma_vulkan_func.vkGetDeviceProcAddr = vkGetDeviceProcAddr; VmaAllocatorCreateInfo allocator_info{}; allocator_info.pVulkanFunctions = &vma_vulkan_func; allocator_info.physicalDevice = static_cast(device.get_gpu().get_handle()); allocator_info.device = static_cast(device.get_handle()); allocator_info.instance = static_cast(device.get_gpu().get_instance().get_handle()); bool can_get_memory_requirements = device.get_gpu().is_extension_supported(VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME); bool has_dedicated_allocation = device.get_gpu().is_extension_supported(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME); if (can_get_memory_requirements && has_dedicated_allocation && device.is_extension_enabled(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME)) { allocator_info.flags |= VMA_ALLOCATOR_CREATE_KHR_DEDICATED_ALLOCATION_BIT; } if (device.get_gpu().is_extension_supported(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME) && device.is_extension_enabled(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME)) { allocator_info.flags |= VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT; } if (device.get_gpu().is_extension_supported(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME) && device.is_extension_enabled(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) { allocator_info.flags |= VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT; } if (device.get_gpu().is_extension_supported(VK_EXT_MEMORY_PRIORITY_EXTENSION_NAME) && device.is_extension_enabled(VK_EXT_MEMORY_PRIORITY_EXTENSION_NAME)) { allocator_info.flags |= VMA_ALLOCATOR_CREATE_EXT_MEMORY_PRIORITY_BIT; } if (device.get_gpu().is_extension_supported(VK_KHR_BIND_MEMORY_2_EXTENSION_NAME) && device.is_extension_enabled(VK_KHR_BIND_MEMORY_2_EXTENSION_NAME)) { allocator_info.flags |= VMA_ALLOCATOR_CREATE_KHR_BIND_MEMORY2_BIT; } if (device.get_gpu().is_extension_supported(VK_AMD_DEVICE_COHERENT_MEMORY_EXTENSION_NAME) && device.is_extension_enabled(VK_AMD_DEVICE_COHERENT_MEMORY_EXTENSION_NAME)) { allocator_info.flags |= VMA_ALLOCATOR_CREATE_AMD_DEVICE_COHERENT_MEMORY_BIT; } init(allocator_info); } /** * @brief Shuts down the VMA allocator and releases all resources. Should be preceeded with a call to `init`. */ void shutdown(); /** * @brief The `Allocated` class serves as a base class for wrappers around Vulkan that require memory allocation * (`VkImage` and `VkBuffer`). This class mostly ensures proper behavior for a RAII pattern, preventing double-release by * preventing copy assignment and copy construction in favor of move semantics, as well as preventing default construction * in favor of explicit construction with a pre-existing handle or a populated create info struct. * * This project uses the [VMA](https://gpuopen.com/vulkan-memory-allocator/) to handle the low * level details of memory allocation and management, as it hides away many of the messyy details of * memory allocation when a user is first learning Vulkan, but still allows for fine grained control * when a user becomes more experienced and the situation calls for it. * * @note Constants used in this documentation in the form of `HOST_COHERENT` are shorthand for * `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` used for the sake of brevity. * * @tparam bindingType A flag indicating whether this is being used with the C or C++ API */ template class Allocated : public vkb::core::VulkanResource { public: using ParentType = vkb::core::VulkanResource; using BufferType = typename std::conditional::type; using BufferCreateInfoType = typename std::conditional::type; using DeviceMemoryType = typename std::conditional::type; using DeviceSizeType = typename std::conditional::type; using ImageCreateInfoType = typename std::conditional::type; using ImageType = typename std::conditional::type; public: Allocated() = delete; Allocated(const Allocated &) = delete; Allocated(Allocated &&other) noexcept; Allocated &operator=(Allocated const &other) = delete; Allocated &operator=(Allocated &&other) = default; protected: /** * @brief The VMA-specific constructor for new objects. This should only be visible to derived classes. * @param allocation_create_info All of the non-resource-specific information needed by the VMA to allocate the memory. * @param args Additional constructor arguments needed for the derived class. Typically a `VkImageCreateInfo` or `VkBufferCreateInfo` struct. */ template Allocated(const VmaAllocationCreateInfo &allocation_create_info, Args &&...args); /** * @brief This constructor is used when the handle is already created, and the user wants to wrap it in an `Allocated` object. * @note This constructor is used when the API provides us a pre-existing handle to something we didn't actually allocate, for instance * when we allocate a swapchain and access the images in it. In these cases the `allocation` member variable will remain null for the * lifetime of the wrapper object (which is NOT necessarily the lifetime of the handle) and the wrapper will make no attempt to apply * RAII semantics. */ Allocated(HandleType handle, vkb::core::Device *device_ = nullptr); public: const HandleType *get() const; /** * @brief Flushes memory if it is NOT `HOST_COHERENT` (which also implies `HOST_VISIBLE`). * This is a no-op for `HOST_COHERENT` memory. * * @param offset The offset into the memory to flush. Defaults to 0. * @param size The size of the memory to flush. Defaults to the entire block of memory. */ void flush(DeviceSizeType offset = 0, DeviceSizeType size = VK_WHOLE_SIZE); /** * @brief Retrieves a pointer to the host visible memory as an unsigned byte array. * @return The pointer to the host visible memory. * @note This performs no checking that the memory is actually mapped, so it's possible to get a nullptr */ const uint8_t *get_data() const; /** * @brief Retrieves the raw Vulkan memory object. * @return The Vulkan memory object. */ DeviceMemoryType get_memory() const; /** * @brief Maps Vulkan memory if it isn't already mapped to a host visible address. Does nothing if the * allocation is already mapped (including persistently mapped allocations). * @return Pointer to host visible memory. */ uint8_t *map(); /** * @brief Returns true if the memory is mapped (i.e. the object contains a pointer for the mapping). * This is true for both objects where `map` has been called as well as objects created with persistent * mapping, where no call to `map` is necessary. * @return mapping status. */ bool mapped() const; /** * @brief Unmaps Vulkan memory from the host visible address. Does nothing if the memory is not mapped or * if the allocation is persistently mapped. */ void unmap(); /** * @brief Copies the specified unsigned byte data into the mapped memory region. * @note For non-persistently mapped memory, this function will call the `map` and `unmap` methods and SHOULD NOT * be used if the user intends to make multiple updates to the memory region. In that case, the user should call * `map` once, make all the updates against the pointer returned by `get_data`, and then call `unmap`. This may * be a poor design choice as it creates a side effect of using the method (that mapped memory will * unexpectedly be unmapped), but it is the current design of the method and changing it would be burdensome. * Refactoring could be eased by creating a new method with a more explicit name, and then removing this method * entirely. * * @param data The data to copy from. * @param size The amount of bytes to copy. * @param offset The offset to start the copying into the mapped data. Defaults to 0. */ size_t update(const uint8_t *data, size_t size, size_t offset = 0); /** * @brief Converts any non-byte data into bytes and then updates the buffer. This allows the user to pass * arbitrary structure pointers to the update method, which will then be copied into the buffer as bytes. * @param data The data to copy from. * @param size The amount of bytes to copy. * @param offset The offset to start the copying into the mapped data. Defaults to 0. */ size_t update(void const *data, size_t size, size_t offset = 0); /** * @brief Copies a vector of items into the buffer. This is a convenience method that allows the user to * pass a vector of items to the update method, which will then be copied into the buffer as bytes. * * This function DOES NOT automatically manage adhering to the alignment requirements of the items being copied, * for instance the `minUniformBufferOffsetAlignment` property of the [device](https://vulkan.gpuinfo.org/displaydevicelimit.php?name=minUniformBufferOffsetAlignment&platform=all). * If the data needs to be aligned on something other than `sizeof(T)`, the user must manage that themselves. * @param data The data vector to upload * @param offset The offset to start the copying into the mapped data * @deprecated Use the `updateTyped` method that uses the `vk::ArrayProxy` class instead. */ template size_t update(std::vector const &data, size_t offset = 0) { return update(data.data(), data.size() * sizeof(T), offset); } /** * @brief Another convenience method, similar to the vector update method, but for std::array. The same caveats apply. * @param data The data vector to upload * @param offset The offset to start the copying into the mapped data * @see update(std::vector const &data, size_t offset = 0) * @deprecated Use the `updateTyped` method that uses the `vk::ArrayProxy` class instead. */ template size_t update(std::array const &data, size_t offset = 0) { return update(data.data(), data.size() * sizeof(T), offset); } /** * @brief Copies an object as byte data into the buffer. This is a convenience method that allows the user to * pass an object to the update method, which will then be copied into the buffer as bytes. The name difference * is to avoid amibuity with the `update` method signatures (including the non-templated version) * @param object The object to convert into byte data * @param offset The offset to start the copying into the mapped data * @deprecated Use the `updateTyped` method that uses the `vk::ArrayProxy` class instead. */ template size_t convert_and_update(const T &object, size_t offset = 0) { return update(reinterpret_cast(&object), sizeof(T), offset); } /** * @brief Copies an object as byte data into the buffer. This is a convenience method that allows the user to * pass an object to the update method, which will then be copied into the buffer as bytes. The use of the `vk::ArrayProxy` * type here to wrap the passed data means you can use any type related to T that can be used as a constructor to `vk::ArrayProxy`. * This includes `T`, `std::vector`, `std::array`, and `vk::ArrayProxy`. * * @remark This was previously not feasible as it would have been undesirable to create a strong coupling with the * C++ Vulkan bindings where the `vk::ArrayProxy` type is defined. However, structural changes have ensured that this * coupling is always present, so the `vk::ArrayProxy` may as well be used to our advantage here. * * @note This function DOES NOT automatically manage adhering to the alignment requirements of the items being copied, * for instance the `minUniformBufferOffsetAlignment` property of the [device](https://vulkan.gpuinfo.org/displaydevicelimit.php?name=minUniformBufferOffsetAlignment&platform=all). * If the data needs to be aligned on something other than `sizeof(T)`, the user must manage that themselves. * * @todo create `updateTypedAligned` which has an additional argument specifying the required GPU alignment of the elements of the array. */ template size_t updateTyped(const vk::ArrayProxy &object, size_t offset = 0) { return update(reinterpret_cast(object.data()), object.size() * sizeof(T), offset); } protected: /** * @brief Internal method to actually create the buffer, allocate the memory and bind them. * Should only be called from the `Buffer` derived class. * * Present in this common base class in order to allow the internal state members to remain `private` * instead of `protected`, and because it (mostly) isolates interaction with the VMA to a single class */ [[nodiscard]] BufferType create_buffer(BufferCreateInfoType const &create_info); /** * @brief Internal method to actually create the image, allocate the memory and bind them. * Should only be called from the `Image` derived class. * * Present in this common base class in order to allow the internal state members to remain `private` * instead of `protected`, and because it (mostly) isolates interaction with the VMA to a single class */ [[nodiscard]] ImageType create_image(ImageCreateInfoType const &create_info); /** * @brief The post_create method is called after the creation of a buffer or image to store the allocation info internally. Derived classes * could in theory override this to ensure any post-allocation operations are performed, but the base class should always be called to ensure * the allocation info is stored. * Should only be called in the corresponding `create_xxx` methods. */ virtual void post_create(VmaAllocationInfo const &allocation_info); /** * @brief Internal method to actually destroy the buffer and release the allocated memory. Should * only be called from the `Buffer` derived class. * Present in this common base class in order to allow the internal state members to remain `private` * instead of `protected`, and because it (mostly) isolates interaction with the VMA to a single class */ void destroy_buffer(BufferType buffer); /** * @brief Internal method to actually destroy the image and release the allocated memory. Should * only be called from the `Image` derived class. * Present in this common base class in order to allow the internal state members to remain `private` * instead of `protected`, and because it (mostly) isolates interaction with the VMA to a single class */ void destroy_image(ImageType image); /** * @brief Clears the internal state. Can be overridden by derived classes to perform additional cleanup of members. * Should only be called in the corresping `destroy_xxx` methods. */ void clear(); private: vk::Buffer create_buffer_impl(vk::BufferCreateInfo const &create_info); vk::Image create_image_impl(vk::ImageCreateInfo const &create_info); VmaAllocationCreateInfo allocation_create_info = {}; VmaAllocation allocation = VK_NULL_HANDLE; /** * @brief A pointer to the allocation memory, if the memory is HOST_VISIBLE and is currently (or persistently) mapped. * Contains null otherwise. */ uint8_t *mapped_data = nullptr; /** * @brief This flag is set to true if the memory is coherent and doesn't need to be flushed after writes. * * @note This is initialized at allocation time to avoid subsequent need to call a function to fetch the * allocation information from the VMA, since this property won't change for the lifetime of the allocation. */ bool coherent = false; /** * @brief This flag is set to true if the memory is persistently mapped (i.e. not just HOST_VISIBLE, but available * as a pointer to the application for the lifetime of the allocation). * * @note This is initialized at allocation time to avoid subsequent need to call a function to fetch the * allocation information from the VMA, since this property won't change for the lifetime of the allocation. */ bool persistent = false; }; template inline Allocated::Allocated(Allocated &&other) noexcept : ParentType{static_cast(other)}, allocation_create_info(std::exchange(other.allocation_create_info, {})), allocation(std::exchange(other.allocation, {})), mapped_data(std::exchange(other.mapped_data, {})), coherent(std::exchange(other.coherent, {})), persistent(std::exchange(other.persistent, {})) { } template template inline Allocated::Allocated(const VmaAllocationCreateInfo &allocation_create_info, Args &&...args) : ParentType{std::forward(args)...}, allocation_create_info(allocation_create_info) {} template inline Allocated::Allocated(HandleType handle, vkb::core::Device *device_) : ParentType(handle, device_) {} template inline const HandleType *Allocated::get() const { return &ParentType::get_handle(); } template inline void Allocated::clear() { mapped_data = nullptr; persistent = false; allocation_create_info = {}; } template inline typename Allocated::BufferType Allocated::create_buffer(BufferCreateInfoType const &create_info) { if constexpr (bindingType == vkb::BindingType::Cpp) { return create_buffer_impl(create_info); } else { return static_cast(create_buffer_impl(reinterpret_cast(create_info))); } } template inline vk::Buffer Allocated::create_buffer_impl(vk::BufferCreateInfo const &create_info) { vk::Buffer buffer = VK_NULL_HANDLE; VmaAllocationInfo allocation_info{}; auto result = vmaCreateBuffer( get_memory_allocator(), reinterpret_cast(&create_info), &allocation_create_info, reinterpret_cast(&buffer), &allocation, &allocation_info); if (result != VK_SUCCESS) { throw VulkanException{result, "Cannot create Buffer"}; } post_create(allocation_info); return buffer; } template inline typename Allocated::ImageType Allocated::create_image(ImageCreateInfoType const &create_info) { if constexpr (bindingType == vkb::BindingType::Cpp) { return create_image_impl(create_info); } else { return static_cast(create_image_impl(reinterpret_cast(create_info))); } } template inline vk::Image Allocated::create_image_impl(vk::ImageCreateInfo const &create_info) { assert(0 < create_info.mipLevels && "Images should have at least one level"); assert(0 < create_info.arrayLayers && "Images should have at least one layer"); assert(create_info.usage && "Images should have at least one usage type"); vk::Image image = VK_NULL_HANDLE; VmaAllocationInfo allocation_info{}; #if 0 // If the image is an attachment, prefer dedicated memory constexpr vk::ImageUsageFlags attachment_only_flags = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eTransientAttachment; if (create_info.usage & attachment_only_flags) { allocation_create_info.flags |= VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT; } if (create_info.usage & vk::ImageUsageFlagBits::eTransientAttachment) { allocation_create_info.preferredFlags |= VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT; } #endif VkResult result = vmaCreateImage(get_memory_allocator(), reinterpret_cast(&create_info), &allocation_create_info, reinterpret_cast(&image), &allocation, &allocation_info); if (result != VK_SUCCESS) { throw VulkanException{result, "Cannot create Image"}; } post_create(allocation_info); return image; } template inline void Allocated::destroy_buffer(BufferType handle) { if (handle != VK_NULL_HANDLE && allocation != VK_NULL_HANDLE) { unmap(); if constexpr (bindingType == vkb::BindingType::Cpp) { vmaDestroyBuffer(get_memory_allocator(), static_cast(handle), allocation); } else { vmaDestroyBuffer(get_memory_allocator(), handle, allocation); } clear(); } } template inline void Allocated::destroy_image(ImageType image) { if (image != VK_NULL_HANDLE && allocation != VK_NULL_HANDLE) { unmap(); if constexpr (bindingType == vkb::BindingType::Cpp) { vmaDestroyImage(get_memory_allocator(), static_cast(image), allocation); } else { vmaDestroyImage(get_memory_allocator(), image, allocation); } clear(); } } template inline void Allocated::flush(DeviceSizeType offset, DeviceSizeType size) { if (!coherent) { if constexpr (bindingType == vkb::BindingType::Cpp) { vmaFlushAllocation(get_memory_allocator(), allocation, static_cast(offset), static_cast(size)); } else { vmaFlushAllocation(get_memory_allocator(), allocation, offset, size); } } } template inline const uint8_t *Allocated::get_data() const { return mapped_data; } template inline typename Allocated::DeviceMemoryType Allocated::get_memory() const { VmaAllocationInfo alloc_info; vmaGetAllocationInfo(get_memory_allocator(), allocation, &alloc_info); if constexpr (bindingType == vkb::BindingType::Cpp) { return static_cast(alloc_info.deviceMemory); } else { return alloc_info.deviceMemory; } } template inline uint8_t *Allocated::map() { if (!persistent && !mapped()) { VK_CHECK(vmaMapMemory(get_memory_allocator(), allocation, reinterpret_cast(&mapped_data))); assert(mapped_data); } return mapped_data; } template inline bool Allocated::mapped() const { return mapped_data != nullptr; } template inline void Allocated::post_create(VmaAllocationInfo const &allocation_info) { VkMemoryPropertyFlags memory_properties; vmaGetAllocationMemoryProperties(get_memory_allocator(), allocation, &memory_properties); coherent = (memory_properties & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; mapped_data = static_cast(allocation_info.pMappedData); persistent = mapped(); } template inline void Allocated::unmap() { if (!persistent && mapped()) { vmaUnmapMemory(get_memory_allocator(), allocation); mapped_data = nullptr; } } template inline size_t Allocated::update(const uint8_t *data, size_t size, size_t offset) { if (persistent) { std::copy(data, data + size, mapped_data + offset); flush(); } else { map(); std::copy(data, data + size, mapped_data + offset); flush(); unmap(); } return size; } template inline size_t Allocated::update(void const *data, size_t size, size_t offset) { return update(reinterpret_cast(data), size, offset); } template using AllocatedC = Allocated; template using AllocatedCpp = Allocated; } // namespace allocated } // namespace vkb