Discover compilation workflows in Metal
Graphics & Games 高级 20m

探索 Metal 编译工作流

Discover compilation workflows in Metal

2021年6月9日

在 Apple 官方观看视频

一句话判断

Metal 3 的新编译管线让着色器(Shader)编译从运行时提前到构建时——应用启动不再卡在 shader 编译上,帧率更稳定。

这场 Session 讲了什么

Session 介绍了 Metal 在 iOS 15 / macOS Monterey 中的编译工作流改进。Metal 着色器之前有两种编译方式:源码编译(运行时编译 .metal 文件,灵活但慢)和库编译(构建时编译成 .metallib,快但不够灵活)。Metal 3 引入了新的混合编译方案,结合了两者的优势。

关键新特性包括:Metal 工具链(Metal Toolchain)支持在构建时将 .metal 源码编译为默认的 Metal 库(Default Metal Library),同时支持运行时的动态链接和函数特化(Function Specialization)。这意味着你可以在构建时完成大部分编译工作,只把真正需要动态配置的部分留到运行时。

还介绍了新的 Pipeline State 对象创建方式——异步创建、二进制归档(Binary Archive)让编译结果可以缓存到磁盘,避免重复编译。

值得深挖的点

二进制归档的缓存策略

Pipeline State 对象的创建涉及 GPU 驱动层的编译,是 Metal 中最耗时的操作之一。新的 Binary Archive API 让你可以把编译好的 PSO 序列化到磁盘。下次启动 App 时直接从磁盘加载,跳过编译步骤。这对于有大量着色器变体的游戏 App 来说意义巨大——启动时间可以从几秒缩短到几百毫秒。但要注意,Binary Archive 是 GPU 架构相关的,A14 和 M1 的归档不能互换。

函数特化的编译时优化

Metal 支持函数特化——你可以对着色器函数的常量参数做特化,让编译器针对特定值做优化(比如把循环次数为 8 的循环完全展开)。之前特化只能在运行时做。Metal 3 允许你在构建时预定义一组常用的特化配置,提前编译好。运行时只需要选择对应的预编译版本,不需要等待即时编译。

代码片段

创建和使用 Binary Archive

// 创建二进制归档来缓存编译后的 PSO
let archiveDescriptor = MTLPipelineStateBinaryArchiveDescriptor()
archiveDescriptor.url = binaryArchiveURL

// 加载已有的归档(如果存在)
if let archive = try? device.makeBinaryArchive(descriptor: archiveDescriptor) {
    // 在创建 PSO 时使用缓存的归档
    let pipelineDescriptor = MTLRenderPipelineDescriptor()
    pipelineDescriptor.binaryArchives = [archive]
    
    // 如果归档中有对应的编译结果,直接加载
    let pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
}

// 保存新的编译结果到归档
try archive.serialize()

异步创建 Pipeline State

// 异步创建 PSO,不阻塞主线程
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction

device.makeRenderPipelineState(descriptor: pipelineDescriptor) { pipelineState, error in
    if let pipelineState = pipelineState {
        // 编译完成,保存引用
        self.pipelineState = pipelineState
    } else {
        print("PSO 编译失败: \(error!)")
    }
}

函数特化

// 定义可特化的常量
let constants = MTLFunctionConstantValues()
var lightCount: Int = 4
constants.setConstantValue(&lightCount, type: .uint, index: 0)

// 使用特化常量创建函数
let specializedFunction = try library.makeFunction(name: "fragmentShader", 
                                                     constantValues: constants)

// 特化后的函数在编译时已针对 lightCount=4 优化
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.fragmentFunction = specializedFunction

最佳实践

新项目: 从一开始就使用 Binary Archive 缓存编译结果。在 App 启动时异步预创建常用的 Pipeline State,用归档保存。首次启动可能稍慢,后续启动直接从归档加载。

已有项目: 识别 App 中创建 Pipeline State 的热点(用 Instruments 的 Metal System Trace)。把耗时的 PSO 创建改为异步。对于有大量着色器变体的场景,用函数特化减少运行时的编译负担。

还有什么值得关注

  • Metal 3 支持 mesh shader 和 ray tracing 的硬件加速(M1 及更新 GPU)。
  • Xcode 13 的 Metal Shader Profiler 可以直接对着色器代码逐行分析性能。
  • 新的 Metal validation layer 在 Debug 模式下提供更详细的错误信息。
WWDC 2021