WWDC Quick Look 💓 By SwiftGGTeam
Program Metal in C++ with metal-cpp

Program Metal in C++ with metal-cpp

观看原视频

Highlight

metal-cpp 是一个头文件-only 的 C++ 封装库,通过 C 语言直接调用 Objective-C 运行时,为 C++ 代码提供 100% 的 Metal API 覆盖,且与 Objective-C Metal 的函数调用几乎一一对应,开发者工具(GPU Frame Capture、Xcode debugger)完全兼容。

核心内容

你的游戏引擎或图形应用用 C++ 编写,但 Metal 原生基于 Objective-C。过去你需要在两种语言之间来回切换,或者把整个渲染层用 Objective-C 重写。metal-cpp 解决了这个痛点。

01:09)metal-cpp 是一个轻量级 C++ 封装库。轻量体现在:它是头文件-only(header-only)的实现,所有函数调用都是内联的。它通过 C 语言直接调用 Objective-C 运行时,这跟 Objective-C 编译器执行方法调用的机制完全相同,所以额外开销极小。

02:47)metal-cpp 提供 100% 的 Metal API 覆盖。它同时封装了 Foundation 和 QuartzCore 框架的部分接口。代码开源,Apache 2 许可证,可以直接修改并集成到你的项目中。

因为是一一映射,所有开发者工具都能无缝工作。GPU Frame Capture、Xcode debugger、性能分析工具都不需要特殊配置。

详细内容

快速开始

05:30)使用 metal-cpp 需要三步:

  1. 下载 metal-cpp 并添加到 Xcode 项目的 Header Search Paths
  2. 设置 C++ 语言标准为 C++17 或更高
  3. 添加 Foundation、QuartzCore、Metal 三个框架

然后在一个 .cpp 文件中定义三个宏并引入头文件:

#define NS_PRIVATE_IMPLEMENTATION
#define CA_PRIVATE_IMPLEMENTATION
#define MTL_PRIVATE_IMPLEMENTATION

#include <Foundation/Foundation.hpp>
#include <Metal/Metal.hpp>
#include <QuartzCore/QuartzCore.hpp>

关键点:

  • 这三个宏会在编译时生成对应框架的实现代码
  • 每个宏只能定义一次,重复定义会导致链接错误
  • 可以把三个头文件合并成一个统一的头文件来引入

画一个三角形

03:10)用 metal-cpp 画三角形的代码:

MTL::CommandBuffer* pCmd = _pCommandQueue->commandBuffer();
MTL::RenderCommandEncoder* pEnc = pCmd->renderCommandEncoder(pRpd);
pEnc->setRenderPipelineState(_pPSO);
pEnc->drawPrimitives(MTL::PrimitiveTypeTriangle,
                     NS::UInteger(0),
                     NS::UInteger(3));
pEnc->endEncoding();
pCmd->presentDrawable(pView->currentDrawable());
pCmd->commit();

对比 Objective-C 版本:

id<MTLCommandBuffer> cmd = [_commandQueue commandBuffer];
id<MTLRenderCommandEncoder> enc = [cmd renderCommandEncoderWithDescriptor:pRpd];
[enc setRenderPipelineState:_pSO];
[enc drawPrimitives:MTLPrimitiveTypeTriangle
        vertexStart:0
        vertexCount:3];
[enc endEncoding];
[cmd presentDrawable:view.currentDrawable];
[cmd commit];

两者的函数调用几乎完全一致,只有命名风格上的差异。如果你已经熟悉 Objective-C Metal,切换到 metal-cpp 的学习成本极低。

对象生命周期管理

06:56)metal-cpp 使用手动引用计数(Manual Retain-Release,MRR),遵循 Cocoa 的内存管理规则:

  • allocnewcopymutableCopycreate 开头的方法创建的对象,你拥有它
  • retain 获取所有权
  • release 释放所有权
  • 不能释放你不拥有的对象

10:08)Metal 在运行时会创建一些自动释放(autoreleased)的临时对象。这些对象会被放入 AutoreleasePool,由池负责释放。你需要为每个线程创建 AutoreleasePool:

NS::AutoreleasePool* pPool = NS::AutoreleasePool::alloc()->init();

MTL::CommandBuffer* pCmd = _pCommandQueue->commandBuffer();
MTL::RenderPassDescriptor* pRpd = pView->currentRenderPassDescriptor();
MTL::RenderCommandEncoder* pEnc = pCmd->renderCommandEncoder(pRpd);
pEnc->endEncoding();
pCmd->presentDrawable(pView->currentDrawable());
pCmd->commit();

pPool->release();

关键点:

  • commandBuffer()currentRenderPassDescriptor()currentDrawable() 都返回 autoreleased 对象
  • AutoreleasePool 由 alloc()->init() 创建,你需要手动 release()
  • 每个线程都需要自己的 AutoreleasePool

NS::SharedPtr 智能指针

13:40)metal-cpp 提供了 NS::SharedPtr 来简化生命周期管理。它跟 std::shared_ptr 不同:不依赖 C++ 标准库,引用计数直接存储在对象内部。

// TransferPtr:转移所有权,不增加引用计数
{
    auto ptr = NS::TransferPtr(pMRR);
    // ptr 离开作用域时自动 release
}

// RetainPtr:保留对象,增加引用计数
{
    auto ptr = NS::RetainPtr(pMRR);
    // ptr 离开作用域时自动 release,原始对象仍存活
}

16:48)用 NS::TransferPtr 管理渲染管线状态对象:

// 创建 descriptor 和 pipeline state
MTL::RenderPipelineDescriptor* pDesc = MTL::RenderPipelineDescriptor::alloc()->init();
// ... 配置 descriptor ...
MTL::RenderPipelineState* pPSO = _pDevice->newRenderPipelineState(pDesc, &pError);

// 用 TransferPtr 接管所有权,无需手动 release
auto pPSOPtr = NS::TransferPtr(pPSO);
auto pDescPtr = NS::TransferPtr(pDesc);

// 使用 pPSOPtr->... 访问方法
// 离开作用域时自动释放

调试工具:NSZombie

17:55)use-after-free 是手动内存管理的常见 bug。NSZombie 可以帮你捕获这类问题。在 Xcode scheme 中启用 Zombie Objects,或者设置环境变量 NSZombieEnabled=YES。当向已释放对象发送消息时,会触发断点并显示调用栈。

与 Objective-C 代码互操作

19:50)大多数游戏还需要调用其他框架(Game Controller、Audio 等),这些框架仍然是 Objective-C API。推荐用 Adapter 模式隔离两种语言。

从 Objective-C 调用 C++

@interface AAPLRendererAdapter () {
    AAPLRenderer* _pRenderer;
}
@end

@implementation AAPLRendererAdapter
- (void)drawInMTKView:(MTKView *)pMtkView
{
    _pRenderer->draw((__bridge MTK::View*)pMtkView);
}
@end

从 C++ 调用 Objective-C

CA::MetalDrawable* AAPLViewAdapter::currentDrawable() const
{
    return (__bridge CA::MetalDrawable*)[(__bridge MTKView *)m_pMTKView currentDrawable];
}

MTL::Texture* AAPLViewAdapter::depthStencilTexture() const
{
    return (__bridge MTL::Texture*)[(__bridge MTKView *)m_pMTKView depthStencilTexture];
}

三种桥接类型:

  • __bridge:只做类型转换,不转移所有权
  • __bridge_retained:从 Objective-C(ARC)转到 metal-cpp(MRR),夺取所有权
  • __bridge_transfer:从 metal-cpp(MRR)转到 Objective-C(ARC),转移所有权

24:06)用 __bridge_retained 从 MetalKit 加载纹理:

MTL::Texture* newTextureFromCatalog(MTL::Device* pDevice, const char* name,
                                    MTL::StorageMode storageMode, MTL::TextureUsage usage)
{
    NSDictionary<MTKTextureLoaderOption, id>* options = @{
        MTKTextureLoaderOptionTextureStorageMode : @( (MTLStorageMode)storageMode ),
        MTKTextureLoaderOptionTextureUsage : @( (MTLTextureUsage)usage )
    };

    MTKTextureLoader* textureLoader = [[MTKTextureLoader alloc]
        initWithDevice:(__bridge id<MTLDevice>)pDevice];

    NSError* __autoreleasing err = nil;
    id<MTLTexture> texture = [textureLoader
        newTextureWithName:[NSString stringWithUTF8String:name]
        scaleFactor:1
        bundle:nil
        options:options
        error:&err];

    // 从 ARC 夺取所有权,转为 MRR 管理
    return (__bridge_retained MTL::Texture*)texture;
}

关键点:

  • pDevice__bridge 转换,因为不转移所有权
  • 返回的 texture__bridge_retained 转换,因为要从 ARC 中夺取所有权,之后由调用者负责 release

核心启发

把现有 C++ 游戏引擎移植到 Apple Silicon

如果你的游戏引擎用 C++ 编写,现在可以直接集成 Metal 渲染后端,无需把渲染层重写为 Objective-C。metal-cpp 的 API 与 Objective-C Metal 一一对应,现有 Metal 教程和文档完全适用。从下载 metal-cpp 到画出第一个三角形,只需要配置三个宏和头文件路径。

在 C++ 项目中使用 Metal Performance Shaders

做图像处理或机器学习推理的 C++ 项目,可以用 metal-cpp 调用 MPS 和 MPSGraph。结合 NS::SharedPtr 管理对象生命周期,避免手动 retain/release 的繁琐和出错风险。

构建跨平台渲染抽象层

如果你的引擎需要同时支持 Metal(Apple)和 Vulkan/DirectX(其他平台),可以用 metal-cpp 把 Metal 后端封装成与平台无关的 C++ 接口。Adapter 模式让 Objective-C 框架(如 MetalKit)的调用被隔离在少量适配文件中,主引擎代码保持纯 C++。

关联 Session

评论

GitHub Issues · utterances