Metal编程中MTLBuffer复用池memcpy崩溃问题排查
问题描述
在Mac应用中,业务需求为每帧绘制时接收数百甚至数千张网络传输的小位图,需将其bitblit至一张大MTLTexture中。
为提升渲染性能,实现了MTLBuffer复用池:收到新位图时,进入临界区查找足够大小的可复用Buffer,找到则memcpy数据用于渲染;未找到则创建新MTLBuffer加入池。
获取可复用MTLBuffer的代码
std::lock_guard<std::mutex> guard(_bitmapLock); for(auto& [bitmapBuffer, bitmapInfo] : _bitmapBuffers) { if (!bitmapInfo.toBeBlit && // 该布尔值表示Buffer是否可复用 bitmapBuffer.length >= length) { bitmapInfo = TWBitmapCacheInfo{ destPoint, width, height, bytesPerRow, true }; // 最后一个true表示Buffer正被使用,不可复用 _bitmapBufferQueue.push(bitmapBuffer); return bitmapBuffer; } } return nil; // 返回nil时,将创建新MTLBuffer并加入_bitmapBuffers
Buffer复用/创建逻辑
if(!bitmapBuffer) { // 无可用复用Buffer if(!(bitmapBuffer = [_device newBufferWithBytes:pixelBytes length:length options:MTLResourceStorageModeShared])) { return nil; } AddBitmapBuffer(bitmapBuffer, destPoint, width, height, bytesPerRow, accumulatedReachLimit); } else { // 复用MTLBuffer auto pBitmapBufferContent = [bitmapBuffer contents]; if(pBitmapBufferContent != nullptr) { memcpy(pBitmapBufferContent, pixelBytes, length); } }
完成后重置Buffer状态的逻辑
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> cb) { std::lock_guard<std::mutex> guard(_bitmapLock); for(auto& [buffer, bitmapCacheInfo] : _accumulatedBitmaps) { _bitmapBuffers[buffer].toBeBlit = false; // MTLBuffer现在可复用 } }];
性能提升显著,FPS从15提升至60,但应用存在极低概率在复用MTLBuffer执行memcpy时崩溃,崩溃日志如下:
Exception Type: EXC_BAD_ACCESS (SIGSEGV) Exception Codes: KERN_INVALID_ADDRESS at 0x00007f7d76aab000 Exception Codes: 0x0000000000000001, 0x00007f7d76aab000 Termination Reason: Namespace SIGNAL, Code 11 Segmentation fault: 11 Terminating Process: exc handler [32593] VM Region Info: 0x7f7d76aab000 is not in any region. Bytes after previous region: 1 Bytes before following region: 5591040 REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL MALLOC_LARGE_REUSABLE 7f7d73000000-7f7d76aab000 [ 58.7M] rw-/rwx SM=PRV ---> GAP OF 0x555000 BYTES MALLOC_SMALL 7f7d77000000-7f7d77800000 [ 8192K] rw-/rwx SM=PRV ...... Thread 19 Crashed:: Dispatch queue: ****** 0 libsystem_platform.dylib 0x7ff80bc6e909 _platform_memmove$VARIANT$Haswell + 41 1 My application 0x108c9f2ec myApp::MetalBltRenderer::Draw(_Point const&, _Point const&, unsigned int, unsigned int, myApp::decodeBuffer*, _Color*) + 348
已用互斥锁保障多线程安全,确保Buffer不会同时被CPU和GPU使用,疑问:哪里出错了?是否可能completedHandler触发时GPU尚未完全使用完Buffer?
问题分析与解决方案
核心问题:MTLBuffer内存被系统回收
崩溃日志显示访问的地址处于内存间隙,说明对应的MTLBuffer内存已经被系统释放,而复用池还持有该Buffer的无效引用,导致memcpy时访问无效内存。
内存释放的原因
- ARC自动释放MTLBuffer:如果
_bitmapBuffers容器存储的是MTLBuffer的弱引用,当没有其他强引用持有该Buffer时,ARC会自动回收其内存,此时复用池中的Buffer就变成了野指针。 - 容器关联逻辑漏洞:
_accumulatedBitmaps与_bitmapBuffers的映射关系如果出现不一致,比如某个Buffer被从_bitmapBuffers中移除但仍在_accumulatedBitmaps中,会导致状态管理失效;但当前崩溃场景更指向Buffer本身被释放。
关键修复步骤
- 确保复用池存储强引用:检查
_bitmapBuffers的容器类型,比如在Objective-C++中,使用std::unordered_map<id<MTLBuffer>, TWBitmapCacheInfo>,id默认是强引用,能避免Buffer被意外回收。 - 验证AddBitmapBuffer逻辑:确认该函数是将新创建的Buffer以强引用方式加入
_bitmapBuffers,没有误存弱引用。
关于completedHandler的疑问解答
苹果官方文档明确说明:commandBuffer的completedHandler会在GPU完成所有命令执行、资源不再被GPU占用后才触发,因此不存在GPU还在使用Buffer的情况,你的崩溃和这个无关。
额外优化建议
- 复用Buffer时,除检查
toBeBlit和长度外,可额外验证[bitmapBuffer contents]是否非空,虽不是根本解决办法,但能降低崩溃概率。 - 定期清理复用池中长时间未使用的大Buffer,避免内存占用过高,清理时需确保这些Buffer未被标记为正在使用。
内容的提问来源于stack exchange,提问作者SufficientManner1345
相关产品推荐
相关产品推荐

