Highlight
Apple 在 Metal 里加入 Binary Archives、Dynamic Libraries 和新的离线工具链,让开发者可以收集、序列化并随 App 分发 GPU 二进制,减少首次创建 Pipeline State Object 时的后端编译成本。
核心内容
大型 Metal 游戏第一次启动时,常常会先创建大量 Pipeline State Object(PSO,管线状态对象)。这样做能减少游戏过程中的 hitch,却会把成本集中到 loading screen。即使 shader 已经离线编译成 AIR(Apple Intermediate Representation,中间表示),设备仍要在创建每个 PSO 时生成当前 GPU 可执行的机器码。
系统级 Metal Shader Cache 能加速之后的运行,可第一次安装、设备重启后的冷启动、或者缓存还没被填充时,开发者仍要等待后端编译。共享 utility shader 代码也有类似问题:多个 compute pipeline 可能重复编译同一批函数,并在内存里保存重复机器码。
这场 session 把问题拆成三个工作流。Metal Binary Archives 让应用显式收集、保存和分发 pipeline 编译结果。Metal Dynamic Libraries 让 compute utility 函数以动态库形式复用。Metal Developer Tools 则把 AIR、static library、dynamic library、symbol inspection 和 binary slice 管理放进命令行,也支持 Windows 构建环境。
详细内容
Binary Archives:显式管理 pipeline 缓存
(03:03)Binary Archives 给应用直接控制 pipeline 缓存的能力。开发者可以把编译好的 pipeline 函数收进 archive,按关卡、场景或常用 pipeline 分组,再从设备收割并分发到同 GPU、同系统版本的兼容设备。设备不兼容时,Metal 会回退到运行时编译。
创建空 archive 从 MTLBinaryArchiveDescriptor 开始。
let descriptor = MTLBinaryArchiveDescriptor()
descriptor.url = nil
let binaryArchive = try device.makeBinaryArchive(descriptor:descriptor)
关键点:
MTLBinaryArchiveDescriptor决定创建新 archive,还是从磁盘加载已有 archive。descriptor.url = nil表示新建一个空 archive。device.makeBinaryArchive从当前MTLDevice创建 archive,后续收集到的二进制会和 GPU 兼容性相关。
(06:47)archive 不会自动收集所有 pipeline。应用要把关心的 descriptor 加进去,Metal 才会执行后端编译,把机器码放入 archive。
// Render pipelines
try binaryArchive.addRenderPipelineFunctions(with: renderPipelineDescriptor)
// Compute pipelines
try binaryArchive.addComputePipelineFunctions(with: computePipelineDescriptor)
// Tile render pipelines
try binaryArchive.addTileRenderPipelineFunctions(with: tileRenderPipelineDescriptor)
关键点:
addRenderPipelineFunctions收集 render pipeline 的函数。addComputePipelineFunctions收集 compute pipeline 的函数。addTileRenderPipelineFunctions收集 tile render pipeline 的函数。- 加入 descriptor 时会触发后端编译,目标是把机器码写入 archive。
(06:56)使用 archive 时,把它挂到 pipeline descriptor 的 binaryArchives 数组上。Metal 会按数组顺序搜索。命中后,PSO 创建可以跳过后端编译,并且不写入系统 shader cache。
// Reusing compiled functions to build a pipeline state object from a file
let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
// ...
renderPipelineDescriptor.binaryArchives = [ binaryArchive ]
let renderPipeline = try device.makeRenderPipelineState(descriptor:
renderPipelineDescriptor)
关键点:
binaryArchives声明 pipeline 创建时要搜索哪些 archive。- 数组顺序就是搜索顺序。
makeRenderPipelineState仍是创建 render PSO 的调用点,只是函数二进制可能直接来自 archive。
(07:15)收集完成后,应用把 archive 序列化到可写位置。session 也提醒,archive 文件会 memory-map 到进程里;释放不再使用的 archive,可以释放虚拟地址空间。
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let archiveURL = documentsURL.appendingPathComponent("binaryArchive.metallib")
try binaryArchive.serialize(to: NSURL.fileURL(withPath: archiveURL))
关键点:
serialize把收集到的 archive 写入.metallib文件。- 这个文件可以在下一次启动复用,也可以作为 GPU 二进制资产分发。
- 分组保存常用 pipeline 和分关卡 pipeline,有助于及时释放不用的 cache。
(07:26)下次启动时,descriptor 的 url 指向已有文件,Metal 会加载 archive 并立即用于后续 pipeline 创建。
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let serializeURL = documentsURL.appendingPathComponent("binaryArchive.metallib")
let descriptor = MTLBinaryArchiveDescriptor()
descriptor.url = NSURL.fileURL(withPath: serializeURL)
let binaryArchive = try device.makeBinaryArchive(descriptor: descriptor)
关键点:
descriptor.url指向已有 archive 时,调用会加载磁盘上的二进制。- 加载后的 archive 可以放入 pipeline descriptor 的
binaryArchives。 - 如果想调试 archive 是否完整,可以使用 transcript 提到的
failOnBinaryArchiveMiss,让未命中时直接返回 nil。
(10:35)Apple 用 Epic Games 的 Fortnite workload 做了量化。这个 workload 有超过 11,000 个 PSO。预置一个收集了 1,700 个 PSO 函数变体的 archive 后,pipeline build 时间从 1 分 26 秒降到 3 秒,约 28 倍加速。
Dynamic Libraries:复用 compute utility 机器码
(13:52)MTLDynamicLibrary 面向共享 compute utility 代码。旧模型里,多个 pipeline 依赖同一批 utility 函数时,后端编译和机器码存储都可能重复发生。动态库把这些函数做成可链接、可加载、可序列化的机器码集合。
运行时创建动态库时,先用 MTLCompileOptions 指定动态库类型和 install name,再从 MTLLibrary 生成 MTLDynamicLibrary。
let options = MTLCompileOptions();
options.libraryType = .dynamic;
options.installName = "@executable_path/myDynamicLibrary.metallib"
let utilityLib = try device.makeLibrary(source: dylibSrc, options: options)
let utilityDylib = try device.makeDynamicLibrary(library: utilityLib)
关键点:
libraryType = .dynamic表示这份 library 会作为动态库使用。installName是 pipeline 创建时 linker 查找动态库的名字。makeDynamicLibrary把 Metal 代码后端编译成机器码;session 说创建动态库时只需要做这一次编译。
(17:59)依赖动态库的 kernel 在编译时通过 MTLCompileOptions.libraries 声明依赖。编译阶段会检查至少存在一个匹配的函数签名,真正绑定实现发生在 pipeline 创建时。
let options = MTLCompileOptions()
options.libraries = [ utilityDylib ]
let library = try device.makeLibrary(source: kernelStr, options: options)
关键点:
options.libraries告诉 Metal 这个 kernel 会链接哪些动态库。- 编译时会检查缺失符号,缺少匹配签名会失败。
- pipeline 创建时才选择具体实现,所以动态库可以支持替换式 compute 行为。
(19:37)session 还介绍了 insertLibraries。它类似 DYLD_INSERT_LIBRARIES:创建 compute pipeline 时,linker 会先搜索插入的库,再搜索原本链接的库。这个能力可以让 middleware 暴露 hook,也可以让用户提供自定义 kernel 行为,而不用重建入口函数所在的 MTLLibrary。
(20:36)动态库可以序列化并随 App 分发。序列化结果包含预编译二进制和通用 AIR。目标设备不能使用预编译 slice 时,Metal 会从 AIR 重新编译;这个编译结果不会进入 Metal Shader Cache,所以 session 建议再次序列化并在下次加载。
Metal Developer Tools:把 shader 构建拆成可自动化步骤
(23:55)Ravi 展示了新的 Metal Developer Tools。工具链借鉴 CPU 构建模型:metal 把源文件编译成 AIR,metal-libtool 创建 static library,linker 可以生成 executable 或 dynamic MetalLib。静态链接会把 utility 代码复制进每个 executable MetalLib,部署简单,但可能增大 bundle。
(26:05)动态库的命令行入口是 linker 的 -dynamiclib 和 -install_name。install_name 会写进 load command,运行时 loader 用它定位动态库。Metal 支持 @loader_path 和 @executable_path,前者解析到包含 load command 的 MetalLib 路径,后者解析到包含入口函数的 executable MetalLib 路径。
(29:53)动态链接还要求开发者管理符号可见性。默认情况下,Metal 会导出 library 里的所有符号。两个动态库无意中导出同名函数时,loader 可能在运行时绑定到非预期实现。session 建议用 metal-nm 检查导出符号,并用 static、匿名 namespace、visibility attributes、namespace 和 exported-symbols-list 控制接口边界。
(31:02)metal-lipo 用来查看和合并 harvested MetalLib 的 slice。session 的例子里,从 A13 设备收割出的 MetalLib 包含 A13 后端编译 slice 和通用 AIR slice。A13 设备可以跳过后端编译,其他设备仍能从 AIR 回退。合并 A12、A13 等 slice 能提升更多设备的启动体验,但会增加 App bundle 大小。
(34:40)最后,Metal Developer Tools on Windows 让开发者可以在 macOS、Windows 或混合环境中构建面向 Apple 平台的 MetalLib。对已有 Windows asset farm 的游戏和图形团队,这意味着 shader 构建可以接入现有基础设施。
核心启发
-
做什么:为大型游戏建立首启 pipeline 资产包。为什么值得做:Binary Archives 可以把关键 PSO 的后端编译结果随 App 分发,session 中 Fortnite workload 的 pipeline build 时间从 1 分 26 秒降到 3 秒。怎么开始:在构建设备上跑覆盖主菜单、角色选择和核心关卡的场景,调用
addRenderPipelineFunctions、addComputePipelineFunctions收集 archive,序列化后打入 bundle。 -
做什么:把重复使用的 compute 后处理函数拆成 Metal 动态库。为什么值得做:
MTLDynamicLibrary可以让多个 compute pipeline 共享同一份 utility 机器码,减少重复后端编译和重复存储。怎么开始:为 utility shader 设置稳定的函数签名,创建.dynamiclibrary 和installName,再在 kernel 编译时通过MTLCompileOptions.libraries声明依赖。 -
做什么:给图像处理 App 做可替换的滤镜 hook。为什么值得做:
insertLibraries可以让 pipeline 创建阶段优先搜索插入的动态库,session 的 sample 用它改变全屏后处理的像素颜色函数。怎么开始:定义默认动态库和替换动态库都导出的函数接口,在创建 compute pipeline 时按用户选择插入对应 library。 -
做什么:把 shader 构建纳入 CI 审计。为什么值得做:
metal-libtool、metal-nm、metal-lipo分别覆盖库构建、符号检查和 slice 管理,适合沉淀成可重复的发布流程。怎么开始:CI 先生成 AIR,再构建 static 或 dynamic MetalLib,随后检查导出符号和发布包里的 GPU slice 清单。
关联 Session
- Harness Apple GPUs with Metal — 理解 Apple GPU 的 TBDR、Tile Memory 和带宽模型,能帮助判断哪些 pipeline 值得提前编译。
- Optimize Metal apps and games with GPU counters — 用 GPU counters 找到真正拖慢帧或 loading 的 Metal workload。
- Gain insights into your Metal app with Xcode 12 — 继续学习 Xcode 12 里的 Metal capture、debugger 和性能洞察工具。
- Debug GPU-side errors in Metal — 在引入更复杂的 shader 构建流程后,用 Xcode 定位 GPU 端错误。
- Get to know Metal function pointers — 函数指针和动态库同属 Metal 2020 的 shader 可扩展性更新。
评论
GitHub Issues · utterances