| // |
| // Copyright 2019 The ANGLE Project Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| // mtl_buffer_pool.mm: |
| // Implements the class methods for BufferPool. |
| // |
| |
| #include "libANGLE/renderer/metal/mtl_buffer_pool.h" |
| |
| #include <atomic> |
| #include "libANGLE/renderer/metal/ContextMtl.h" |
| #include "libANGLE/renderer/metal/DisplayMtl.h" |
| #include "libANGLE/trace.h" |
| |
| namespace rx |
| { |
| |
| namespace mtl |
| { |
| |
| namespace |
| { |
| void UpdateBufferPoolMetrics(int64_t totalMemoryDelta, |
| int64_t freeMemoryDelta, |
| int64_t totalBuffersDelta) |
| { |
| #if defined(ANGLE_USE_PERFETTO) |
| static std::atomic<int64_t> s_BufferPoolTotalMemory(0); |
| static std::atomic<int64_t> s_BufferPoolFreeMemory(0); |
| static std::atomic<int64_t> s_BufferPoolTotalBuffers(0); |
| |
| int64_t newTotalMemory = |
| s_BufferPoolTotalMemory.fetch_add(totalMemoryDelta, std::memory_order_relaxed) + |
| totalMemoryDelta; |
| ANGLE_TRACE_COUNTER("gpu.angle", "ANGLEMetalBufferPoolTotalMemoryKb", newTotalMemory / 1024); |
| |
| int64_t newFreeMemory = |
| s_BufferPoolFreeMemory.fetch_add(freeMemoryDelta, std::memory_order_relaxed) + |
| freeMemoryDelta; |
| ANGLE_TRACE_COUNTER("gpu.angle", "ANGLEMetalBufferPoolFreeMemoryKb", newFreeMemory / 1024); |
| |
| int64_t newTotalBuffers = |
| s_BufferPoolTotalBuffers.fetch_add(totalBuffersDelta, std::memory_order_relaxed) + |
| totalBuffersDelta; |
| ANGLE_TRACE_COUNTER("gpu.angle", "ANGLEMetalBufferPoolTotalBuffers", newTotalBuffers); |
| #endif |
| } |
| } // namespace |
| |
| // BufferPool implementation. |
| BufferPool::BufferPool() : BufferPool(false) {} |
| |
| BufferPool::BufferPool(bool alwaysAllocNewBuffer) |
| : mBuffer(nullptr), mAlwaysAllocateNewBuffer(alwaysAllocNewBuffer) |
| {} |
| |
| angle::Result BufferPool::reset(ContextMtl *contextMtl, |
| size_t initialSize, |
| size_t alignment, |
| size_t maxBuffers) |
| { |
| ANGLE_TRY(finalizePendingBuffer(contextMtl)); |
| releaseInFlightBuffers(contextMtl); |
| |
| mSize = 0; |
| if (mBufferFreeList.size() && mInitialSize <= mBufferFreeList.front()->size()) |
| { |
| // Instead of deleteing old buffers, we should reset them to avoid excessive |
| // memory re-allocations |
| if (maxBuffers && mBufferFreeList.size() > maxBuffers) |
| { |
| for (size_t i = maxBuffers; i < mBufferFreeList.size(); ++i) |
| { |
| if (mBufferFreeList[i]) |
| { |
| int64_t size = static_cast<int64_t>(mBufferFreeList[i]->size()); |
| UpdateBufferPoolMetrics(-size, -size, -1); |
| } |
| } |
| mBufferFreeList.resize(maxBuffers); |
| mBuffersAllocated = maxBuffers; |
| } |
| |
| mSize = mBufferFreeList.front()->size(); |
| for (size_t i = 0; i < mBufferFreeList.size(); ++i) |
| { |
| BufferRef &buffer = mBufferFreeList[i]; |
| if (!buffer->isBeingUsedByGPU(contextMtl)) |
| { |
| // If buffer is not used by GPU, re-use it immediately. |
| continue; |
| } |
| if (IsError(buffer->reset(contextMtl, storageMode(contextMtl), mSize))) |
| { |
| destroyBufferList(contextMtl, &mBufferFreeList, true); |
| mSize = 0; |
| break; |
| } |
| } |
| } |
| else |
| { |
| destroyBufferList(contextMtl, &mBufferFreeList, true); |
| } |
| |
| mInitialSize = initialSize; |
| |
| mMaxBuffers = maxBuffers; |
| |
| updateAlignment(contextMtl, alignment); |
| |
| return angle::Result::Continue; |
| } |
| |
| void BufferPool::initialize(Context *context, |
| size_t initialSize, |
| size_t alignment, |
| size_t maxBuffers) |
| { |
| if (mBuffersAllocated) |
| { |
| // Invalid call, must call destroy() first. |
| UNREACHABLE(); |
| } |
| |
| mInitialSize = initialSize; |
| |
| mMaxBuffers = maxBuffers; |
| |
| updateAlignment(context, alignment); |
| } |
| |
| BufferPool::~BufferPool() |
| { |
| if (mBuffer) |
| { |
| UpdateBufferPoolMetrics(-static_cast<int64_t>(mBuffer->size()), 0, -1); |
| } |
| for (auto &buffer : mInFlightBuffers) |
| { |
| if (buffer) |
| { |
| UpdateBufferPoolMetrics(-static_cast<int64_t>(buffer->size()), 0, -1); |
| } |
| } |
| for (auto &buffer : mBufferFreeList) |
| { |
| if (buffer) |
| { |
| int64_t size = static_cast<int64_t>(buffer->size()); |
| UpdateBufferPoolMetrics(-size, -size, -1); |
| } |
| } |
| } |
| |
| MTLStorageMode BufferPool::storageMode(ContextMtl *contextMtl) const |
| { |
| #if TARGET_OS_OSX || TARGET_OS_MACCATALYST |
| if (mSize > kSharedMemBufferMaxBufSizeHint) |
| { |
| return MTLStorageModeManaged; |
| } |
| #endif |
| return Buffer::getStorageModeForSharedBuffer(contextMtl); |
| } |
| |
| angle::Result BufferPool::allocateNewBuffer(ContextMtl *contextMtl) |
| { |
| if (mMaxBuffers > 0 && mBuffersAllocated >= mMaxBuffers) |
| { |
| // We reach the max number of buffers allowed. |
| // Try to deallocate old and smaller size inflight buffers. |
| releaseInFlightBuffers(contextMtl); |
| } |
| |
| if (mMaxBuffers > 0 && mBuffersAllocated >= mMaxBuffers) |
| { |
| // If we reach this point, it means there was no buffer deallocated inside |
| // releaseInFlightBuffers() thus, the number of buffers allocated still exceeds number |
| // allowed. |
| ASSERT(!mBufferFreeList.empty()); |
| |
| // Reuse the buffer in free list: |
| if (mBufferFreeList.front()->isBeingUsedByGPU(contextMtl)) |
| { |
| contextMtl->flushCommandBuffer(mtl::NoWait); |
| // Force the GPU to finish its rendering and make the old buffer available. |
| contextMtl->cmdQueue().ensureResourceReadyForCPU(mBufferFreeList.front()); |
| } |
| |
| mBuffer = mBufferFreeList.front(); |
| mBufferFreeList.erase(mBufferFreeList.begin()); |
| |
| UpdateBufferPoolMetrics(0, -static_cast<int64_t>(mBuffer->size()), 0); |
| |
| return angle::Result::Continue; |
| } |
| |
| ANGLE_TRY( |
| Buffer::MakeBufferWithStorageMode(contextMtl, storageMode(contextMtl), mSize, &mBuffer)); |
| |
| ASSERT(mBuffer); |
| |
| UpdateBufferPoolMetrics(mBuffer->size(), 0, 1); |
| |
| mBuffersAllocated++; |
| |
| return angle::Result::Continue; |
| } |
| |
| angle::Result BufferPool::allocateAndMap(ContextMtl *contextMtl, |
| size_t sizeInBytes, |
| angle::Span<uint8_t> *mappedOut, |
| BufferSlice *outBuffer) |
| { |
| ANGLE_TRY(allocate(contextMtl, sizeInBytes, outBuffer)); |
| // We don't need to synchronize with GPU access, since allocation should return a |
| // non-overlapped region each time. |
| *mappedOut = mBuffer->mapNoSync(contextMtl, outBuffer->offset(), sizeInBytes); |
| return angle::Result::Continue; |
| } |
| |
| angle::Result BufferPool::allocate(ContextMtl *contextMtl, |
| size_t sizeInBytes, |
| BufferSlice *outBuffer) |
| { |
| size_t sizeToAllocate = roundUp(sizeInBytes, mAlignment); |
| |
| angle::base::CheckedNumeric<size_t> checkedNextWriteOffset = mNextAllocationOffset; |
| checkedNextWriteOffset += sizeToAllocate; |
| |
| if (!mBuffer || !checkedNextWriteOffset.IsValid() || |
| checkedNextWriteOffset.ValueOrDie() >= mSize || |
| // If the current buffer has been modified by GPU, do not reuse it: |
| mBuffer->isCPUReadMemNeedSync() || mAlwaysAllocateNewBuffer) |
| { |
| if (mBuffer) |
| { |
| ANGLE_TRY(finalizePendingBuffer(contextMtl)); |
| } |
| |
| if (sizeToAllocate > mSize) |
| { |
| mSize = std::max(mInitialSize, sizeToAllocate); |
| |
| // Clear the free list since the free buffers are now too small. |
| destroyBufferList(contextMtl, &mBufferFreeList, true); |
| } |
| |
| // The front of the free list should be the oldest. Thus if it is in use the rest of the |
| // free list should be in use as well. |
| if (mBufferFreeList.empty() || mBufferFreeList.front()->isBeingUsedByGPU(contextMtl)) |
| { |
| ANGLE_TRY(allocateNewBuffer(contextMtl)); |
| } |
| else |
| { |
| mBuffer = mBufferFreeList.front(); |
| mBufferFreeList.erase(mBufferFreeList.begin()); |
| UpdateBufferPoolMetrics(0, -static_cast<int64_t>(mBuffer->size()), 0); |
| } |
| |
| ASSERT(mBuffer->size() == mSize); |
| |
| mNextAllocationOffset = 0; |
| mLastFlushOffset = 0; |
| } |
| |
| ASSERT(mBuffer != nullptr); |
| |
| if (outBuffer != nullptr) |
| { |
| *outBuffer = BufferSlice(mBuffer).subslice(mNextAllocationOffset, sizeInBytes); |
| } |
| mNextAllocationOffset += sizeToAllocate; |
| return angle::Result::Continue; |
| } |
| |
| angle::Result BufferPool::commit(ContextMtl *contextMtl, bool flushEntireBuffer) |
| { |
| if (mBuffer && mNextAllocationOffset > mLastFlushOffset) |
| { |
| if (flushEntireBuffer) |
| { |
| mBuffer->flush(contextMtl, 0, mLastFlushOffset); |
| } |
| else |
| { |
| mBuffer->flush(contextMtl, mLastFlushOffset, mNextAllocationOffset - mLastFlushOffset); |
| } |
| mLastFlushOffset = mNextAllocationOffset; |
| } |
| return angle::Result::Continue; |
| } |
| |
| angle::Result BufferPool::finalizePendingBuffer(ContextMtl *contextMtl) |
| { |
| if (mBuffer) |
| { |
| ANGLE_TRY(commit(contextMtl)); |
| // commit() already flushes so no need to flush here. |
| mBuffer->unmapNoFlush(contextMtl); |
| |
| mInFlightBuffers.push_back(mBuffer); |
| mBuffer = nullptr; |
| } |
| |
| mNextAllocationOffset = 0; |
| mLastFlushOffset = 0; |
| |
| return angle::Result::Continue; |
| } |
| |
| void BufferPool::releaseInFlightBuffers(ContextMtl *contextMtl) |
| { |
| for (auto &toRelease : mInFlightBuffers) |
| { |
| // If the dynamic buffer was resized we cannot reuse the retained buffer. |
| if (toRelease->size() < mSize |
| #if TARGET_OS_OSX || TARGET_OS_MACCATALYST |
| // Also release buffer if it was allocated in different policy |
| || toRelease->storageMode() != storageMode(contextMtl) |
| #endif |
| ) |
| { |
| UpdateBufferPoolMetrics(-static_cast<int64_t>(toRelease->size()), 0, -1); |
| toRelease = nullptr; |
| mBuffersAllocated--; |
| } |
| |
| // Need to maintain the requirement of the free list that buffers in use |
| // by the GPU are stored in FIFO order and that after the first in-use |
| // buffer, the rest of the free list is in-use as well. To achieve this |
| // in-use buffers are appended to the end of the free list and free buffers |
| // are prepended to the beginning of the free list to maintain the following: |
| // |
| // +------+------+-------+-------+-------+ |
| // | Free | Free | Inuse | ... | Inuse | |
| // +------+------+-------+-------+-------+ |
| // ^ ^ ^-------- Youngest, in-use buffer |
| // | +------------------------ Oldest, in-use buffer |
| // +-------------------------------------- First, free buffer |
| else if (toRelease->isBeingUsedByGPU(contextMtl)) |
| { |
| mBufferFreeList.push_back(toRelease); |
| UpdateBufferPoolMetrics(0, toRelease->size(), 0); |
| } |
| else |
| { |
| mBufferFreeList.push_front(toRelease); |
| UpdateBufferPoolMetrics(0, toRelease->size(), 0); |
| } |
| } |
| |
| mInFlightBuffers.clear(); |
| } |
| |
| void BufferPool::destroyBufferList(ContextMtl *contextMtl, |
| std::deque<BufferRef> *buffers, |
| bool isFreeList) |
| { |
| ASSERT(mBuffersAllocated >= buffers->size()); |
| mBuffersAllocated -= buffers->size(); |
| for (auto &buffer : *buffers) |
| { |
| if (buffer) |
| { |
| int64_t size = static_cast<int64_t>(buffer->size()); |
| UpdateBufferPoolMetrics(-size, isFreeList ? -size : 0, -1); |
| } |
| } |
| buffers->clear(); |
| } |
| |
| void BufferPool::destroy(ContextMtl *contextMtl) |
| { |
| destroyBufferList(contextMtl, &mInFlightBuffers, false); |
| destroyBufferList(contextMtl, &mBufferFreeList, true); |
| |
| reset(); |
| |
| if (mBuffer) |
| { |
| UpdateBufferPoolMetrics(-static_cast<int64_t>(mBuffer->size()), 0, -1); |
| mBuffer->unmap(contextMtl); |
| |
| mBuffer = nullptr; |
| } |
| } |
| |
| void BufferPool::updateAlignment(Context *context, size_t alignment) |
| { |
| ASSERT(alignment > 0); |
| |
| // NOTE(hqle): May check additional platform limits. |
| |
| // If alignment has changed, make sure the next allocation is done at an aligned offset. |
| if (alignment != mAlignment) |
| { |
| mNextAllocationOffset = roundUp(mNextAllocationOffset, alignment); |
| mAlignment = alignment; |
| } |
| } |
| |
| void BufferPool::reset() |
| { |
| mSize = 0; |
| mNextAllocationOffset = 0; |
| mLastFlushOffset = 0; |
| mMaxBuffers = 0; |
| mAlwaysAllocateNewBuffer = false; |
| mBuffersAllocated = 0; |
| } |
| } // namespace mtl |
| } // namespace rx |