You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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  
---&gt;  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时访问无效内存。

内存释放的原因

  1. ARC自动释放MTLBuffer:如果_bitmapBuffers容器存储的是MTLBuffer的弱引用,当没有其他强引用持有该Buffer时,ARC会自动回收其内存,此时复用池中的Buffer就变成了野指针。
  2. 容器关联逻辑漏洞:_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 19:45:18