Build GPU binaries with Metal
Graphics & Games 高级 25m

使用 Metal 构建 GPU 二进制文件

Build GPU binaries with Metal

2020年6月24日

在 Apple 官方观看视频

一句话判断

如果你的 Metal 应用在启动时因为 shader 编译而卡顿,Binary Archive 就是解药——它让你在构建时预编译 shader,运行时零编译开销。

这场 Session 讲了什么

这场 Session 介绍了 Metal Binary Archive——一种在构建时预编译 Metal Shader 的机制。传统的 Metal 工作流中,Shader 以源码或中间表示(AIR)的形式打包在 App 中,运行时由 Metal 驱动编译为 GPU 可执行的机器码。这个过程发生在你第一次创建 Pipeline State 时,可能导致数百毫秒甚至数秒的卡顿。

Binary Archive 改变了这个模型。你可以在开发阶段或构建阶段将已编译的 GPU 二进制码保存到文件中,然后在 App 运行时直接加载这些预编译的二进制,跳过编译步骤。这对游戏和实时图形应用特别有价值——你可以在玩家看到第一帧之前就准备好所有 GPU 代码。

演讲者详细讲解了 Binary Archive 的生命周期:创建、添加 Pipeline State、序列化到文件、在 App 中加载。Session 还讨论了 Binary Archive 的版本兼容性——当 GPU 架构或驱动更新时,旧的 Binary 可能失效,Metal 会自动回退到运行时编译。

值得深挖的点

  • Binary Archive 的命中策略:Metal 不会无条件使用 Binary Archive 中的预编译代码——它会验证二进制的兼容性(GPU 架构、驱动版本等)。如果验证失败,就回退到运行时编译。你需要监控实际命中率,确保大多数 Pipeline State 都能命中预编译缓存。
  • 构建时 vs 运行时的权衡:最理想的情况是在 App 构建阶段就生成完整的 Binary Archive。但这需要你的构建环境与目标设备的 GPU 架构匹配。Apple 的方案是使用 Xcode 构建时的设备(或模拟器)来生成 Binary Archive,然后在首次启动时补充缺失的条目。

代码片段

// 创建 Binary Archive 并添加 Pipeline State
let device = MTLCreateSystemDefaultDevice()!

// 创建 Binary Archive 描述符
let binaryArchiveDescriptor = MTLBinaryArchiveDescriptor()
binaryArchiveDescriptor.url = binaryArchiveURL

let binaryArchive = try device.makeBinaryArchive(
    descriptor: binaryArchiveDescriptor
)

// 创建带有 Binary Archive 的 Pipeline State
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.binaryArchives = [binaryArchive]

let pipelineState = try device.makeRenderPipelineState(
    descriptor: pipelineDescriptor
)

// 将 Pipeline State 添加到 Binary Archive
try binaryArchive.addRenderPipeline(
    withDescriptor: pipelineDescriptor
)

// 序列化到文件
try binaryArchive.serialize()
// 在 App 启动时加载预编译的 Binary Archive
func loadBinaryArchives() -> [MTLBinaryArchive] {
    var archives: [MTLBinaryArchive] = []

    // 加载主 Archive
    let mainArchiveURL = Bundle.main.url(
        forResource: "default-metallib",
        withExtension: "bin"
    )!

    let descriptor = MTLBinaryArchiveDescriptor()
    descriptor.url = mainArchiveURL

    if let archive = try? device.makeBinaryArchive(descriptor: descriptor) {
        archives.append(archive)
    }

    return archives
}

// 创建 Pipeline State 时使用预加载的 Archive
func createPipelineState() -> MTLRenderPipelineState {
    let descriptor = MTLRenderPipelineDescriptor()
    // ... 配置 descriptor ...
    descriptor.binaryArchives = loadBinaryArchives()

    return try! device.makeRenderPipelineState(
        descriptor: descriptor
    )
}
// 首次启动时补全缺失的 Pipeline State
func warmupShaders() {
    let descriptor = MTLBinaryArchiveDescriptor()
    descriptor.url = writableArchiveURL

    let archive = try! device.makeBinaryArchive(
        descriptor: descriptor
    )

    // 创建所有需要的 Pipeline State
    for shaderConfig in allShaderConfigurations {
        let pipelineDescriptor = shaderConfig.makeDescriptor()
        pipelineDescriptor.binaryArchives = [archive]

        // 创建时如果 Archive 中有命中就不需要编译
        _ = try? device.makeRenderPipelineState(
            descriptor: pipelineDescriptor
        )

        // 将新编译的添加到 Archive
        try? archive.addRenderPipeline(
            withDescriptor: pipelineDescriptor
        )
    }

    // 持久化
    try? archive.serialize()
}

最佳实践

  • 在 Xcode Build Phase 中自动生成 Binary Archive,确保每次构建都有最新的预编译缓存
  • 首次启动时在 loading screen 后台执行 Pipeline State 预热,避免游戏过程中的卡顿
  • 监控 Binary Archive 的命中率,低于 90% 时需要重新生成 Archive
  • 为不同的 GPU 架构生成不同的 Binary Archive(Apple GPU vs Intel GPU)
  • 将 Archive 文件包含在 App Bundle 中,而非运行时生成

还有什么值得关注

  • Binary Archive 与 Apple Silicon 的关系尤其密切——新的 GPU 架构意味着运行时编译成本更高
  • 搭配 “Harness Apple GPUs with Metal” 一起看,可以全面理解 Apple GPU 上的 Metal 优化策略
  • “Debug GPU-side errors in Metal” 讲解了如何处理预编译二进制中的 shader 错误
WWDC 2020