// // Copyright 2022 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_manager.mm: // Implements the class methods for BufferManager. // #include "libANGLE/renderer/metal/mtl_buffer_manager.h" #include "libANGLE/renderer/metal/ContextMtl.h" #include "libANGLE/renderer/metal/DisplayMtl.h" namespace rx { namespace mtl { namespace { int storageModeToCacheIndex(MTLStorageMode storageMode) { static_assert(MTLStorageModeShared == 0); #if TARGET_OS_OSX || TARGET_OS_MACCATALYST static_assert(MTLStorageModeManaged == 1); #endif return static_cast(storageMode); } } // namespace BufferManager::BufferManager() {} void BufferManager::freeUnusedBuffers(ContextMtl *contextMtl) { // Scan for the first buffer still in use. BufferList::iterator firstInUseIter = std::find_if(mInUseBuffers.begin(), mInUseBuffers.end(), [&contextMtl](auto ref) { return ref->isBeingUsedByGPU(contextMtl); }); // Move unused buffers to the free lists for (BufferList::iterator it = mInUseBuffers.begin(); it != firstInUseIter; ++it) { addBufferRefToFreeLists(*it); } mInUseBuffers.erase(mInUseBuffers.begin(), firstInUseIter); } void BufferManager::addBufferRefToFreeLists(mtl::BufferRef &bufferRef) { int cacheIndex = storageModeToCacheIndex(bufferRef->storageMode()); ASSERT(cacheIndex < kNumCachedStorageModes); mFreeBuffers[cacheIndex].insert(BufferMap::value_type(bufferRef->size(), bufferRef)); } void BufferManager::returnBuffer(ContextMtl *contextMtl, BufferRef &bufferRef) { int cacheIndex = storageModeToCacheIndex(bufferRef->storageMode()); if (cacheIndex >= kNumCachedStorageModes) { return; // Storage mode that we do not have a cache for. } bufferRef->setNumContextSwitchesAtLastUse(mContextSwitches); bufferRef->setNumCommandBufferCommitsAtLastUse(mCommandBufferCommits); if (bufferRef->isBeingUsedByGPU(contextMtl)) { mInUseBuffers.push_back(bufferRef); } else { addBufferRefToFreeLists(bufferRef); } } void BufferManager::incrementNumContextSwitches() { ++mContextSwitches; // Ignore wraparound for the moment if (mContextSwitches - mContextSwitchesAtLastGC >= kContextSwitchesBetweenGC) { collectGarbage(GCReason::ContextSwitches); } } void BufferManager::incrementNumCommandBufferCommits() { ++mCommandBufferCommits; // Ignore wraparound for the moment if (mCommandBufferCommits - mCommandBufferCommitsAtLastGC >= kCommandBufferCommitsBetweenGC) { collectGarbage(GCReason::CommandBufferCommits); } } void BufferManager::collectGarbage(BufferManager::GCReason reason) { ANGLE_UNUSED_VARIABLE(reason); #ifdef ANGLE_MTL_TRACK_BUFFER_MEM { fprintf(stderr, "** Before BufferManager GC (reason=%s): totalMem: %zu, ", (reason == GCReason::ContextSwitches ? "context switches" : (reason == GCReason::CommandBufferCommits ? "command buffer commits" : "total memory")), mTotalMem); size_t numBuffers = 0; for (auto iter = mAllocatedSizes.begin(); iter != mAllocatedSizes.end(); ++iter) { fprintf(stderr, "%zu: %zu, ", iter->first, iter->second); numBuffers += iter->second; } fprintf(stderr, " total: %zu\n", numBuffers); } #endif for (int i = 0; i < kNumCachedStorageModes; ++i) { BufferMap &map = mFreeBuffers[i]; auto iter = map.begin(); while (iter != map.end()) { // Clean out cached buffers for the following reasons: // 1) GC was triggered because of total memory use. // 2) The buffer has gone unused for multiple context switches. // 3) The buffer hasn't been used for many command buffer commits. if (reason == GCReason::TotalMem || (mContextSwitches - iter->second->getNumContextSwitchesAtLastUse() >= kContextSwitchesBetweenGC) || (mCommandBufferCommits - iter->second->getNumCommandBufferCommitsAtLastUse() >= kCommandBufferCommitsBetweenGC)) { iter = map.erase(iter); } else { ++iter; } } } mContextSwitchesAtLastGC = mContextSwitches; mCommandBufferCommitsAtLastGC = mCommandBufferCommits; { mTotalMem = 0; #ifdef ANGLE_MTL_TRACK_BUFFER_MEM mAllocatedSizes.clear(); #endif for (auto iter = mInUseBuffers.begin(); iter != mInUseBuffers.end(); ++iter) { size_t sz = (*iter)->size(); mTotalMem += sz; #ifdef ANGLE_MTL_TRACK_BUFFER_MEM ++mAllocatedSizes[sz]; #endif } for (int i = 0; i < kNumCachedStorageModes; ++i) { BufferMap &map = mFreeBuffers[i]; for (auto iter = map.begin(); iter != map.end(); ++iter) { size_t sz = iter->first; #ifdef ANGLE_MTL_TRACK_BUFFER_MEM ++mAllocatedSizes[sz]; #endif mTotalMem += sz; } } #ifdef ANGLE_MTL_TRACK_BUFFER_MEM fprintf(stderr, "** After BufferManager GC: totalMem: %zu, ", mTotalMem); size_t numBuffers = 0; for (auto iter = mAllocatedSizes.begin(); iter != mAllocatedSizes.end(); ++iter) { fprintf(stderr, "%zu: %zu, ", iter->first, iter->second); numBuffers += iter->second; } fprintf(stderr, " total: %zu\n", numBuffers); #endif mTotalMemAtLastGC = mTotalMem; } } angle::Result BufferManager::getBuffer(ContextMtl *contextMtl, MTLStorageMode storageMode, size_t size, BufferRef &bufferRef) { freeUnusedBuffers(contextMtl); const int cacheIndex = storageModeToCacheIndex(storageMode); if (cacheIndex < kNumCachedStorageModes) { // Buffer has a storage mode that have a cache for. BufferMap &freeBuffers = mFreeBuffers[cacheIndex]; auto iter = freeBuffers.find(size); if (iter != freeBuffers.end()) { bufferRef = iter->second; freeBuffers.erase(iter); bufferRef->setNumContextSwitchesAtLastUse(mContextSwitches); bufferRef->setNumCommandBufferCommitsAtLastUse(mCommandBufferCommits); return angle::Result::Continue; } } // Create a new one mtl::BufferRef newBufferRef; ANGLE_TRY(mtl::Buffer::MakeBufferWithStorageMode(contextMtl, storageMode, size, nullptr, &newBufferRef)); mTotalMem += size; #ifdef ANGLE_MTL_TRACK_BUFFER_MEM { mAllocatedSizes[size]++; fprintf(stderr, "totalMem: %zu, ", mTotalMem); size_t numBuffers = 0; for (auto iter = mAllocatedSizes.begin(); iter != mAllocatedSizes.end(); ++iter) { fprintf(stderr, "%zu: %zu, ", iter->first, iter->second); numBuffers += iter->second; } fprintf(stderr, " total: %zu\n", numBuffers); } #endif bool lotOfMemAllocedRecently = (mTotalMem > mTotalMemAtLastGC + kMemAllocedBetweenGC); if (mTotalMem > kMinMemBasedGC && (mTotalMem > 2 * mTotalMemAtLastGC || lotOfMemAllocedRecently)) { if (mInUseBuffers.size() > 0 && lotOfMemAllocedRecently) { // A considerable amount of memory has been allocated since the last GC. Wait for all // pending GPU work to finish in order to unreference in-flight buffers more quickly. contextMtl->flushCommandBuffer(mtl::WaitUntilFinished); } else { // Flush the current command buffer so that the in-flight buffers are scheduled and will // eventually become unreferenced. They won't be collected during this GC cycle but // hopefully will in the next one. contextMtl->flushCommandBuffer(mtl::NoWait); } freeUnusedBuffers(contextMtl); collectGarbage(GCReason::TotalMem); } bufferRef = newBufferRef; return angle::Result::Continue; } angle::Result BufferManager::queueBlitCopyDataToBuffer(ContextMtl *contextMtl, const void *srcPtr, size_t sizeToCopy, size_t offset, mtl::BufferRef &dstMetalBuffer) { const uint8_t *src = reinterpret_cast(srcPtr); for (size_t srcOffset = 0; srcOffset < sizeToCopy; srcOffset += kMaxStagingBufferSize) { size_t subSizeToCopy = std::min(kMaxStagingBufferSize, sizeToCopy - srcOffset); mtl::BufferRef bufferRef; // TODO(anglebug.com/40644888): Here we pass DynamicDraw to get managed buffer for the // operation. This should be checked to see if this makes sense. auto storageMode = Buffer::getStorageModeForUsage(contextMtl, Buffer::Usage::DynamicDraw); ANGLE_TRY(getBuffer(contextMtl, storageMode, subSizeToCopy, bufferRef)); // copy data to buffer uint8_t *ptr = bufferRef->mapWithOpt(contextMtl, false, true); std::copy(src + srcOffset, src + srcOffset + subSizeToCopy, ptr); bufferRef->unmapAndFlushSubset(contextMtl, 0, subSizeToCopy); // queue blit mtl::BlitCommandEncoder *blitEncoder = contextMtl->getBlitCommandEncoder(); blitEncoder->copyBuffer(bufferRef, 0, dstMetalBuffer, offset + srcOffset, subSizeToCopy); returnBuffer(contextMtl, bufferRef); } return angle::Result::Continue; } } // namespace mtl } // namespace rx