When buffers/images are allocated that use larger limits than supported by the GPU blender would crash. This PR adds some safety mechanism to allow Blender to recover from allocation errors. This has been tested on NVIDIA drivers. Pull Request: https://projects.blender.org/blender/blender/pulls/139876
234 lines
7.0 KiB
C++
234 lines
7.0 KiB
C++
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup gpu
|
|
*/
|
|
|
|
#include "vk_buffer.hh"
|
|
#include "vk_backend.hh"
|
|
#include "vk_context.hh"
|
|
#include <vulkan/vulkan_core.h>
|
|
|
|
namespace blender::gpu {
|
|
|
|
VKBuffer::~VKBuffer()
|
|
{
|
|
if (is_allocated()) {
|
|
free();
|
|
}
|
|
}
|
|
|
|
bool VKBuffer::create(size_t size_in_bytes,
|
|
VkBufferUsageFlags buffer_usage,
|
|
VkMemoryPropertyFlags required_flags,
|
|
VkMemoryPropertyFlags preferred_flags,
|
|
VmaAllocationCreateFlags allocation_flags,
|
|
bool export_memory)
|
|
{
|
|
BLI_assert(!is_allocated());
|
|
BLI_assert(vk_buffer_ == VK_NULL_HANDLE);
|
|
BLI_assert(mapped_memory_ == nullptr);
|
|
if (allocation_failed_) {
|
|
return false;
|
|
}
|
|
|
|
size_in_bytes_ = size_in_bytes;
|
|
/*
|
|
* Vulkan doesn't allow empty buffers but some areas (DrawManager Instance data, PyGPU) create
|
|
* them.
|
|
*/
|
|
alloc_size_in_bytes_ = ceil_to_multiple_ul(max_ulul(size_in_bytes_, 16), 16);
|
|
VKDevice &device = VKBackend::get().device;
|
|
|
|
VmaAllocator allocator = device.mem_allocator_get();
|
|
VkBufferCreateInfo create_info = {};
|
|
create_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
|
create_info.flags = 0;
|
|
create_info.size = alloc_size_in_bytes_;
|
|
create_info.usage = buffer_usage;
|
|
/* We use the same command queue for the compute and graphics pipeline, so it is safe to use
|
|
* exclusive resource handling. */
|
|
create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
create_info.queueFamilyIndexCount = 1;
|
|
const uint32_t queue_family_indices[1] = {device.queue_family_get()};
|
|
create_info.pQueueFamilyIndices = queue_family_indices;
|
|
|
|
VkExternalMemoryBufferCreateInfo external_memory_create_info = {
|
|
VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, 0};
|
|
|
|
VmaAllocationCreateInfo vma_create_info = {};
|
|
vma_create_info.flags = allocation_flags;
|
|
vma_create_info.priority = 1.0f;
|
|
vma_create_info.requiredFlags = required_flags;
|
|
vma_create_info.preferredFlags = preferred_flags;
|
|
vma_create_info.usage = VMA_MEMORY_USAGE_AUTO;
|
|
|
|
if (export_memory) {
|
|
create_info.pNext = &external_memory_create_info;
|
|
#ifdef _WIN32
|
|
external_memory_create_info.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT;
|
|
#else
|
|
external_memory_create_info.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
|
|
#endif
|
|
/* Dedicated allocation for zero offset. */
|
|
vma_create_info.flags |= VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT;
|
|
vma_create_info.pool = device.vma_pools.external_memory;
|
|
}
|
|
|
|
VkResult result = vmaCreateBuffer(
|
|
allocator, &create_info, &vma_create_info, &vk_buffer_, &allocation_, nullptr);
|
|
if (result != VK_SUCCESS) {
|
|
allocation_failed_ = true;
|
|
size_in_bytes_ = 0;
|
|
alloc_size_in_bytes_ = 0;
|
|
return false;
|
|
}
|
|
|
|
device.resources.add_buffer(vk_buffer_);
|
|
|
|
vmaGetAllocationMemoryProperties(allocator, allocation_, &vk_memory_property_flags_);
|
|
|
|
if (vk_memory_property_flags_ & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) {
|
|
return map();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void VKBuffer::update_immediately(const void *data) const
|
|
{
|
|
update_sub_immediately(0, size_in_bytes_, data);
|
|
}
|
|
|
|
void VKBuffer::update_sub_immediately(size_t start_offset,
|
|
size_t data_size,
|
|
const void *data) const
|
|
{
|
|
BLI_assert_msg(is_mapped(), "Cannot update a non-mapped buffer.");
|
|
memcpy(static_cast<uint8_t *>(mapped_memory_) + start_offset, data, data_size);
|
|
}
|
|
|
|
void VKBuffer::update_render_graph(VKContext &context, void *data) const
|
|
{
|
|
BLI_assert(size_in_bytes_ <= 65536 && size_in_bytes_ % 4 == 0);
|
|
render_graph::VKUpdateBufferNode::CreateInfo update_buffer = {};
|
|
update_buffer.dst_buffer = vk_buffer_;
|
|
update_buffer.data_size = size_in_bytes_;
|
|
update_buffer.data = data;
|
|
context.render_graph().add_node(update_buffer);
|
|
}
|
|
|
|
void VKBuffer::flush() const
|
|
{
|
|
const VKDevice &device = VKBackend::get().device;
|
|
VmaAllocator allocator = device.mem_allocator_get();
|
|
vmaFlushAllocation(allocator, allocation_, 0, max_ulul(size_in_bytes(), 1));
|
|
}
|
|
|
|
void VKBuffer::clear(VKContext &context, uint32_t clear_value)
|
|
{
|
|
render_graph::VKFillBufferNode::CreateInfo fill_buffer = {};
|
|
fill_buffer.vk_buffer = vk_buffer_;
|
|
fill_buffer.data = clear_value;
|
|
fill_buffer.size = alloc_size_in_bytes_;
|
|
context.render_graph().add_node(fill_buffer);
|
|
}
|
|
|
|
void VKBuffer::async_flush_to_host(VKContext &context)
|
|
{
|
|
BLI_assert(async_timeline_ == 0);
|
|
context.rendering_end();
|
|
async_timeline_ = context.flush_render_graph(RenderGraphFlushFlags::SUBMIT |
|
|
RenderGraphFlushFlags::RENEW_RENDER_GRAPH);
|
|
}
|
|
|
|
void VKBuffer::read_async(VKContext &context, void *data)
|
|
{
|
|
BLI_assert_msg(is_mapped(), "Cannot read a non-mapped buffer.");
|
|
if (async_timeline_ == 0) {
|
|
async_flush_to_host(context);
|
|
}
|
|
VKDevice &device = VKBackend::get().device;
|
|
device.wait_for_timeline(async_timeline_);
|
|
async_timeline_ = 0;
|
|
memcpy(data, mapped_memory_, size_in_bytes_);
|
|
}
|
|
|
|
void VKBuffer::read(VKContext &context, void *data) const
|
|
{
|
|
|
|
BLI_assert_msg(is_mapped(), "Cannot read a non-mapped buffer.");
|
|
BLI_assert(async_timeline_ == 0);
|
|
context.rendering_end();
|
|
context.flush_render_graph(RenderGraphFlushFlags::SUBMIT |
|
|
RenderGraphFlushFlags::WAIT_FOR_COMPLETION |
|
|
RenderGraphFlushFlags::RENEW_RENDER_GRAPH);
|
|
memcpy(data, mapped_memory_, size_in_bytes_);
|
|
}
|
|
|
|
bool VKBuffer::map()
|
|
{
|
|
BLI_assert(!is_mapped());
|
|
const VKDevice &device = VKBackend::get().device;
|
|
VmaAllocator allocator = device.mem_allocator_get();
|
|
VkResult result = vmaMapMemory(allocator, allocation_, &mapped_memory_);
|
|
return result == VK_SUCCESS;
|
|
}
|
|
|
|
void VKBuffer::unmap()
|
|
{
|
|
BLI_assert(is_mapped());
|
|
const VKDevice &device = VKBackend::get().device;
|
|
VmaAllocator allocator = device.mem_allocator_get();
|
|
vmaUnmapMemory(allocator, allocation_);
|
|
mapped_memory_ = nullptr;
|
|
}
|
|
|
|
VkDeviceMemory VKBuffer::export_memory_get(size_t &memory_size)
|
|
{
|
|
const VKDevice &device = VKBackend::get().device;
|
|
VmaAllocator allocator = device.mem_allocator_get();
|
|
|
|
VmaAllocationInfo info = {};
|
|
vmaGetAllocationInfo(allocator, allocation_, &info);
|
|
|
|
/* VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT should ensure this. */
|
|
if (info.offset != 0) {
|
|
BLI_assert(!"Failed to get zero offset export memory for Vulkan buffer");
|
|
return nullptr;
|
|
}
|
|
|
|
memory_size = info.size;
|
|
return info.deviceMemory;
|
|
}
|
|
|
|
bool VKBuffer::free()
|
|
{
|
|
if (is_mapped()) {
|
|
unmap();
|
|
}
|
|
|
|
VKDiscardPool::discard_pool_get().discard_buffer(vk_buffer_, allocation_);
|
|
|
|
allocation_ = VK_NULL_HANDLE;
|
|
vk_buffer_ = VK_NULL_HANDLE;
|
|
|
|
return true;
|
|
}
|
|
|
|
void VKBuffer::free_immediately(VKDevice &device)
|
|
{
|
|
BLI_assert(vk_buffer_ != VK_NULL_HANDLE);
|
|
BLI_assert(allocation_ != VK_NULL_HANDLE);
|
|
if (is_mapped()) {
|
|
unmap();
|
|
}
|
|
device.resources.remove_buffer(vk_buffer_);
|
|
vmaDestroyBuffer(device.mem_allocator_get(), vk_buffer_, allocation_);
|
|
allocation_ = VK_NULL_HANDLE;
|
|
vk_buffer_ = VK_NULL_HANDLE;
|
|
}
|
|
|
|
} // namespace blender::gpu
|