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 需要三步:
- 下载 metal-cpp 并添加到 Xcode 项目的 Header Search Paths
- 设置 C++ 语言标准为 C++17 或更高
- 添加 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 的内存管理规则:
- 用
alloc、new、copy、mutableCopy、create开头的方法创建的对象,你拥有它 - 用
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
- Discover Metal 3 — Metal 3 的新特性概览
- Scale compute workloads across Apple GPUs — 跨 Apple GPU 扩展计算工作负载
- Transform your geometry with Metal mesh shaders — Metal Mesh Shaders 几何处理管线
评论
GitHub Issues · utterances