Optimize Metal Performance for Apple silicon Macs
Graphics & Games 高级 30m

为 Apple Silicon Mac 优化 Metal 性能

Optimize Metal Performance for Apple silicon Macs

2020年6月25日

在 Apple 官方观看视频

一句话判断

移植完只是第一步——这场 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 渲染
  • 统一内存设备上使用 .shared storage 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