Graphics & Games 高级 30m
为 Apple Silicon Mac 优化 Metal 性能
Optimize Metal Performance for Apple silicon Macs
2020年6月25日
一句话判断
移植完只是第一步——这场 Session 告诉你如何利用 Apple GPU 的 TBDR 架构特性,让你的 Metal 应用在 Apple Silicon 上跑得比在 Intel Mac 上更快。
这场 Session 讲了什么
这场 Session 是 “Bring your Metal app to Apple silicon Macs” 的后续,聚焦于移植完成后的性能优化。演讲者假设你已经完成了基本的移植工作,现在要挖掘 Apple GPU 架构的性能潜力。
首先回顾了 Apple GPU 的 TBDR(Tile-Based Deferred Rendering)架构。与 Intel/AMD 的 IMR 架构不同,TBDR 将渲染工作分成多个 tile,每个 tile 在片上缓存中完成所有渲染操作后再写回主内存。这意味着中间渲染结果(如 G-Buffer)不需要占用主内存带宽。理解这个架构是所有优化的基础。
Session 的核心部分讲解了四类关键优化:Memoryless Render Target(消除中间结果写回)、Tile Shader(在 tile 级别执行自定义计算)、统一内存的利用(CPU 和 GPU 直接共享数据),以及 Shader Core 的高效利用(SIMD 对齐和寄存器管理)。
演讲者通过多个真实案例对比了优化前后的性能数据——在某些场景下,仅通过启用 memoryless render target 就能减少 40% 的带宽消耗。Tile Shader 的使用甚至可以将多 pass 渲染合并为单 pass。
值得深挖的点
- 延迟渲染的 Tile Shader 实现:传统延迟渲染需要三个 pass——G-Buffer 填充、光照计算、最终合成。利用 Tile Shader,这三个 pass 可以合并为一个:在 tile 内完成 G-Buffer 填充后,立即在同一个 tile 内做光照计算并输出最终颜色。G-Buffer 数据永远不离开片上缓存。
- 统一内存的零拷贝纹理:在 Intel Mac 上,CPU 生成的纹理数据需要通过 managed buffer 复制到 GPU 可见的内存中。Apple Silicon 的统一内存意味着 CPU 和 GPU 访问同一块物理内存——你可以在 CPU 端写入像素数据,GPU 端直接读取,零拷贝。
代码片段
// 延迟渲染的 Tile Shader 优化
func setupDeferredTileRendering() -> MTLRenderPipelineDescriptor {
let descriptor = MTLRenderPipelineDescriptor()
// G-Buffer 附件全部设为 memoryless
// 法线、深度、albedo 不写回主内存
descriptor.colorAttachments[0].isBlendingEnabled = false
// ... 其他 G-Buffer 附件配置 ...
// 使用 tile shader 在 tile 内完成光照
descriptor.tileFunction = tileLightingFunction
// 最终输出只需要一个颜色附件
descriptor.colorAttachments[3].pixelFormat = .bgra8Unorm
return descriptor
}
// 利用统一内存实现零拷贝纹理更新
func updateTextureZeroCopy(device: MTLDevice) {
guard device.hasUnifiedMemory else {
// 非 Apple Silicon,回退到传统方式
updateTextureViaBlit()
return
}
// 创建 shared 模式的纹理,CPU 和 GPU 共享
let descriptor = MTLTextureDescriptor()
descriptor.width = 1024
descriptor.height = 1024
descriptor.pixelFormat = .rgba8Unorm
descriptor.storageMode = .shared // 统一内存的关键
descriptor.usage = [.shaderRead, .shaderWrite]
let texture = device.makeTexture(descriptor: descriptor)!
// CPU 端直接写入像素数据
texture.replace(
region: MTLRegion(origin: .init(x: 0, y: 0),
size: .init(width: 1024, height: 1024)),
mipmapLevel: 0,
withBytes: pixelData,
bytesPerRow: 1024 * 4
)
// GPU 可以立即使用,无需复制
}
// 使用 MTLHeap 和 memoryless 资源
func createOptimizedFrameResources(device: MTLDevice) {
let heapDescriptor = MTLHeapDescriptor()
heapDescriptor.storageMode = .private
heapDescriptor.size = calculateRequiredHeapSize()
let heap = device.makeHeap(descriptor: heapDescriptor)!
// Memoryless 纹理(只在 tile 内存在)
let gbufferDescriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: .rgba16Float,
width: viewportWidth,
height: viewportHeight,
mipLevel: 0
)
gbufferDescriptor.storageMode = .memoryless
gbufferDescriptor.usage = .renderTarget
let normalTexture = heap.makeTexture(descriptor: gbufferDescriptor)
// 这些纹理在 render pass 结束后自动丢弃
}
最佳实践
- 所有中间渲染目标标记为 memoryless,只在最终输出时写回主内存
- 将多 pass 渲染合并为使用 Tile Shader 的单 pass 渲染
- 统一内存设备上使用
.sharedstorage mode 避免不必要的内存拷贝 - 线程组大小选择 32 或 64 的倍数,匹配 Apple GPU 的 SIMD 宽度
- 使用 GPU Counters 持续监控带宽利用率,目标控制在 50% 以下
还有什么值得关注
- Apple Silicon Mac 的 GPU 性能随型号差异很大——M1、M1 Pro、M1 Max、M1 Ultra 的 GPU 核心数差异可达 4 倍
- 搭配 “Harness Apple GPUs with Metal” 和 “Bring your Metal app to Apple silicon Macs” 一起看,构建完整的 Apple GPU 知识体系
- “Optimize Metal apps and games with GPU counters” 讲解了如何用工具验证优化效果
WWDC 2020