将 Metal 应用移植到 Apple Silicon Mac
Bring your Metal app to Apple silicon Macs
2020年6月24日
一句话判断
Apple Silicon 的 GPU 和 Intel GPU 架构完全不同——如果你的 Metal 应用只测试过 Intel Mac,移植到 Apple Silicon 时一定会踩坑。这场 Session 告诉你坑在哪里。
这场 Session 讲了什么
这场 Session 是 Apple 宣布转向自研芯片后最重要的开发者指南之一。演讲者详细讲解了如何将现有的 Metal 应用从 Intel Mac(使用 Intel Iris 集显或 AMD 独显)移植到 Apple Silicon Mac(使用 Apple 自研 GPU)。
首先介绍了 Apple Silicon 与 Intel Mac 在 GPU 架构上的根本差异。Intel/AMD GPU 使用 Immediate Mode Rendering(IMR),Apple GPU 使用 Tile-Based Deferred Rendering(TBDR)。这意味着很多在 Intel/AMD 上”碰巧能用”的渲染技巧,在 Apple GPU 上可能表现完全不同。
Session 的核心部分列举了最常见的移植问题:存储模式(Storage Mode)的不兼容——Intel Mac 上的 .managed 模式在 Apple GPU 上需要改为 .shared 或 .private;隐式的内存同步假设——TBDR 架构对内存可见性的时序与 IMR 不同;以及 Shader 编译的差异——Apple GPU 使用不同的指令集。
演讲者还讨论了 Universal Binary 的构建策略——如何在同一个 App Bundle 中同时包含 Intel 和 Apple Silicon 的 GPU 代码,以及如何使用 Rosetta 2 的 Metal 翻译层来运行尚未移植的应用。
值得深挖的点
- Storage Mode 的迁移矩阵:Intel Mac 上
.managed模式(CPU 和 GPU 各有一份拷贝,需要手动同步)在 Apple Silicon 上不存在——因为 CPU 和 GPU 共享统一内存。.managed需要改为.shared(CPU 和 GPU 直接访问同一块内存)。如果你在代码中硬编码了.managed,在 Apple Silicon 上会回退到低效的模拟。 - Rosetta 2 的 Metal 翻译限制:Rosetta 2 可以翻译 CPU 指令,但 Metal shader 无法实时翻译——它们需要在目标 GPU 上编译。Apple 的方案是在 App 首次启动时将所有 Metal shader 重新编译为 Apple GPU 指令。这可能导致首次启动非常慢。
代码片段
// 正确处理跨平台的 Storage Mode
func optimalStorageMode(for device: MTLDevice) -> MTLStorageMode {
if device.hasUnifiedMemory {
// Apple Silicon:统一内存架构
return .shared
} else {
// Intel Mac:分离内存架构
return .managed
}
}
// 创建跨平台兼容的 Buffer
func createBuffer(
device: MTLDevice,
size: Int,
options: MTLResourceOptions = []
) -> MTLBuffer {
let storageMode = optimalStorageMode(for: device)
return device.makeBuffer(
length: size,
options: MTLResourceOptions(rawValue: UInt(storageMode.rawValue))
)!
}
// 跨平台的纹理创建
func createTexture(device: MTLDevice) -> MTLTexture {
let descriptor = MTLTextureDescriptor()
descriptor.width = 1024
descriptor.height = 1024
descriptor.pixelFormat = .rgba8Unorm
if device.hasUnifiedMemory {
// Apple Silicon:使用 shared 模式
descriptor.storageMode = .shared
} else {
// Intel Mac:GPU 纹理使用 private 模式
// 需要上传时用 managed buffer 中转
descriptor.storageMode = .private
}
return device.makeTexture(descriptor: descriptor)!
}
// 检测设备特性做差异化处理
func configureForDevice(_ device: MTLDevice) {
// 检测是否为 Apple GPU
if device.supportsFamily(.apple1) {
// Apple GPU 特定优化
// 使用 memoryless render target
// 使用 tile shader
configureAppleGPUOptimizations(device)
} else if device.supportsFamily(.mac1) {
// Intel/AMD GPU 传统路径
configureTraditionalGPUPath(device)
}
// 检测统一内存
if device.hasUnifiedMemory {
// 统一内存优化:CPU 和 GPU 可以共享指针
setupSharedMemoryBuffers()
}
}
最佳实践
- 在 Xcode 中同时配置 Intel 和 Apple Silicon 两个 destination 进行测试
- 将所有
.managedstorage mode 替换为根据设备动态选择的逻辑 - 利用
device.hasUnifiedMemory和 GPU Family 检测做差异化代码路径 - 构建时包含 Metal Binary Archive,减少 Apple Silicon 上的首次启动 shader 编译开销
- 在 CI 中添加 Apple Silicon 测试节点,确保移植问题能被及早发现
还有什么值得关注
- Apple Silicon 的统一内存架构不仅影响 GPU 代码,也影响 CPU 侧的内存管理策略
- 搭配 “Optimize Metal Performance for Apple silicon Macs” 一起看,可以在移植完成后进一步优化性能
- “Harness Apple GPUs with Metal” 讲解了 Apple GPU 架构的技术细节,是理解移植问题的理论基础