Fix #108792: Ensure Metal buffers correctly freed on exit #108940

Merged
Jeroen Bakker merged 5 commits from Jason-Fielder/blender:Fix_108792 into blender-v3.6-release 2023-06-19 20:28:58 +02:00
2 changed files with 131 additions and 17 deletions

View File

@ -107,6 +107,11 @@ class MTLUniformBuf;
/* MTLBuffer allocation wrapper. */
class MTLBuffer {
public:
/* NOTE: ListBase API is not used due to cutsom destructor operation required to release

use ListBase, ListBaseWrapperTemplate and the functions in BLI_listbase.h. Some casts might be needed, but it reduces code-duplication.

use `ListBase`, `ListBaseWrapperTemplate` and the functions in `BLI_listbase.h`. Some casts might be needed, but it reduces code-duplication.

Thanks, have modified the implementation. Admittedly not 100% sure if there are any catches with ListBase.
I have attempted to utilise both BLI_freelinkN for deletion and BLI_freelistN for complete freeing of the list, but a little unsure how the builtin MEM_freeN functions work alongside C++ new/delete, and with constructors.

i.e. is it valid for gpu::MTLBuffer to be created using new gpu::MTLBuffer(params...), and freed using MEM_freeN(buffer)?

It appears to be releasing the resources correctly, as it looked like it was sufficient to use MEM_freeN based functions, if you have MEM_CXX_CLASS_ALLOC_FUNCS specified, however, it would be very helpful if someone could confirm whether the new implementation in the latest commit is correct.

Cheers!

Thanks, have modified the implementation. Admittedly not 100% sure if there are any catches with ListBase. I have attempted to utilise both `BLI_freelinkN` for deletion and `BLI_freelistN` for complete freeing of the list, but a little unsure how the builtin `MEM_freeN` functions work alongside C++ new/delete, and with constructors. i.e. is it valid for gpu::MTLBuffer to be created using `new gpu::MTLBuffer(params...)`, and freed using `MEM_freeN(buffer)`? It appears to be releasing the resources correctly, as it looked like it was sufficient to use `MEM_freeN` based functions, if you have `MEM_CXX_CLASS_ALLOC_FUNCS` specified, however, it would be very helpful if someone could confirm whether the new implementation in the latest commit is correct. Cheers!
* Metal objective C buffer resource. */
gpu::MTLBuffer *next, *prev;
private:
/* Metal resource. */
id<MTLBuffer> metal_buffer_;
@ -177,6 +182,8 @@ class MTLBuffer {
/* Safety check to ensure buffers are not used after free. */
void debug_ensure_used();
MEM_CXX_CLASS_ALLOC_FUNCS("MTLBuffer");
};
/* View into part of an MTLBuffer. */
@ -333,6 +340,8 @@ class MTLSafeFreeList {
}
}
}
MEM_CXX_CLASS_ALLOC_FUNCS("MTLSafeFreeList");
};
/* MTLBuffer pools. */
@ -362,7 +371,7 @@ class MTLBufferPool {
#endif
/* Metal resources. */
bool ensure_initialised_ = false;
bool initialized_ = false;
id<MTLDevice> device_ = nil;
/* The buffer selection aims to pick a buffer which meets the minimum size requirements.
@ -389,7 +398,10 @@ class MTLBufferPool {
std::mutex buffer_pool_lock_;
blender::Map<MTLBufferResourceOptions, MTLBufferPoolOrderedList *> buffer_pools_;
blender::Vector<gpu::MTLBuffer *> allocations_;
/* Linked list to track all existing allocations. Prioritizing fast insert/deletion. */
gpu::MTLBuffer *allocations_list_base_;
uint allocations_list_size_;
/* Maintain a queue of all MTLSafeFreeList's that have been released
* by the GPU and are ready to have their buffers re-inserted into the
@ -432,6 +444,11 @@ class MTLBufferPool {
void ensure_buffer_pool(MTLResourceOptions options);
void insert_buffer_into_pool(MTLResourceOptions options, gpu::MTLBuffer *buffer);
void free();
/* Allocations list. */
void allocations_list_insert(gpu::MTLBuffer *buffer);
void allocations_list_delete(gpu::MTLBuffer *buffer);
void allocations_list_delete_all();
};
/* Scratch buffers are circular-buffers used for temporary data within the current frame.
@ -492,6 +509,8 @@ class MTLScratchBufferManager {
* This call will perform a partial flush of the buffer starting from
* the last offset the data was flushed from, to the current offset. */
void flush_active_scratch_buffer();
MEM_CXX_CLASS_ALLOC_FUNCS("MTLBufferPool");
};
/** \} */

View File

@ -25,9 +25,9 @@ namespace blender::gpu {
void MTLBufferPool::init(id<MTLDevice> mtl_device)
{
if (!ensure_initialised_) {
if (!initialized_) {
BLI_assert(mtl_device);
ensure_initialised_ = true;
initialized_ = true;
device_ = mtl_device;
#if MTL_DEBUG_MEMORY_STATISTICS == 1
@ -39,6 +39,10 @@ void MTLBufferPool::init(id<MTLDevice> mtl_device)
/* Track pool allocation size. */
allocations_in_pool_ = 0;
/* Live allocations list. */
allocations_list_base_ = nullptr;
allocations_list_size_ = 0;
/* Free pools -- Create initial safe free pool */
BLI_assert(current_free_list_ == nullptr);
this->begin_new_safe_list();
@ -53,17 +57,29 @@ MTLBufferPool::~MTLBufferPool()
void MTLBufferPool::free()
{
buffer_pool_lock_.lock();
for (auto buffer : allocations_) {
BLI_assert(buffer);
delete buffer;
}
allocations_.clear();
/* Delete all existing allocations. */
allocations_list_delete_all();
/* Release safe free lists. */
for (int safe_pool_free_index = 0; safe_pool_free_index < completed_safelist_queue_.size();
safe_pool_free_index++)
{
delete completed_safelist_queue_[safe_pool_free_index];
}
completed_safelist_queue_.clear();
if (current_free_list_ != nullptr) {
delete current_free_list_;
current_free_list_ = nullptr;
}
/* Clear and release memory pools. */
for (std::multiset<blender::gpu::MTLBufferHandle, blender::gpu::CompareMTLBuffer> *buffer_pool :
buffer_pools_.values())
{
delete buffer_pool;
}
buffer_pools_.clear();
buffer_pool_lock_.unlock();
}
@ -155,10 +171,7 @@ gpu::MTLBuffer *MTLBufferPool::allocate_aligned(uint64_t size,
new_buffer = new gpu::MTLBuffer(device_, size, options, alignment);
/* Track allocation in context. */
allocations_.append(new_buffer);
#if MTL_DEBUG_MEMORY_STATISTICS == 1
total_allocation_bytes_ += aligned_alloc_size;
#endif
allocations_list_insert(new_buffer);
}
else {
/* Re-use suitable buffer. */
@ -289,6 +302,7 @@ void MTLBufferPool::update_memory_pools()
* animation.
* If memory is continually used, then we do not want to free this memory as it will be
* re-allocated during a short time period. */
const time_t time_now = std::time(nullptr);
for (auto buffer_pool_list : buffer_pools_.items()) {
MTLBufferPoolOrderedList *pool_allocations = buffer_pool_list.value;
@ -323,12 +337,13 @@ void MTLBufferPool::update_memory_pools()
if (time_passed > deletion_time_threshold_s) {
/* Delete allocation. */
delete handle.buffer;
/* Remove buffer from global allocations list and release resource. */
allocations_list_delete(handle.buffer);
/* Remove buffer from pool and update pool statistics. */
pool_iterator = pool_allocations->erase(pool_iterator);
allocations_in_pool_ -= handle.buffer_size;
#if MTL_DEBUG_MEMORY_STATISTICS == 1
total_allocation_bytes_ -= handle.buffer_size;
buffers_in_pool_--;
#endif
continue;
@ -343,7 +358,7 @@ void MTLBufferPool::update_memory_pools()
uint framealloc = (uint)per_frame_allocation_count_;
printf(" Allocations in frame: %u\n", framealloc);
printf(" Total Buffers allocated: %u\n", (uint)allocations_.size());
printf(" Total Buffers allocated: %u\n", allocations_list_size_);
printf(" Total Memory allocated: %u MB\n", (uint)total_allocation_bytes_ / (1024 * 1024));
uint allocs = (uint)(allocations_in_pool_) / 1024 / 2024;
@ -453,6 +468,80 @@ void MTLBufferPool::insert_buffer_into_pool(MTLResourceOptions options, gpu::MTL
#endif
}
void MTLBufferPool::allocations_list_insert(gpu::MTLBuffer *buffer)
{
/* NOTE: Function should only be called while buffer_pool_lock_ is acquired. */
BLI_assert(initialized_);
BLI_assert(buffer != nullptr);
/* Insert buffer at base of allocations list. */
gpu::MTLBuffer *current_head = allocations_list_base_;
buffer->next = current_head;
buffer->prev = nullptr;
if (current_head != nullptr) {
current_head->prev = buffer;
}
allocations_list_base_ = buffer;
allocations_list_size_++;
#if MTL_DEBUG_MEMORY_STATISTICS == 1
total_allocation_bytes_ += buffer->get_size();
#endif
}
void MTLBufferPool::allocations_list_delete(gpu::MTLBuffer *buffer)
{
/* NOTE: Function should only be called while buffer_pool_lock_ is acquired. */
/* Remove a buffer link in the allocations chain. */
BLI_assert(initialized_);
BLI_assert(buffer != nullptr);
BLI_assert(allocations_list_size_ >= 1);
gpu::MTLBuffer *next = buffer->next;
gpu::MTLBuffer *prev = buffer->prev;
if (prev != nullptr) {
BLI_assert(prev->next == buffer);
prev->next = next;
}
if (next != nullptr) {
BLI_assert(next->prev == buffer);
next->prev = prev;
}
if (allocations_list_base_ == buffer) {
allocations_list_base_ = next;
BLI_assert(prev == nullptr);
}
allocations_list_size_--;

allocations_list_size_ is unsigned, so the check is always true. You can do something like

BLI_assert(allocations_list_size_ >= 1);
allocations_list_size_--;
`allocations_list_size_` is unsigned, so the check is always true. You can do something like ``` BLI_assert(allocations_list_size_ >= 1); allocations_list_size_--; ```
#if MTL_DEBUG_MEMORY_STATISTICS == 1
total_allocation_bytes_ -= buffer->get_size();
#endif
/* Delete buffer. */
delete buffer;
}
void MTLBufferPool::allocations_list_delete_all()
{
gpu::MTLBuffer *current = allocations_list_base_;
while (current != nullptr) {
gpu::MTLBuffer *next = current->next;
delete current;
current = next;
}
allocations_list_size_ = 0;
allocations_list_base_ = nullptr;
#if MTL_DEBUG_MEMORY_STATISTICS == 1
total_allocation_bytes_ = 0;
#endif
}
MTLSafeFreeList::MTLSafeFreeList()
{
reference_count_ = 1;
@ -565,6 +654,9 @@ MTLBuffer::MTLBuffer(id<MTLDevice> mtl_device,
else {
data_ = nullptr;
}
/* Linked resources. */
next = prev = nullptr;
}
MTLBuffer::MTLBuffer(id<MTLBuffer> external_buffer)
@ -584,6 +676,9 @@ MTLBuffer::MTLBuffer(id<MTLBuffer> external_buffer)
this->set_usage_size(size_);
data_ = [metal_buffer_ contents];
in_use_ = true;
/* Linked resources. */
next = prev = nullptr;
}
gpu::MTLBuffer::~MTLBuffer()