Bring your game to Mac, Part 2: Compile your shaders
Graphics & Games 进阶 20m

把游戏移植到 Mac(二):Shader 编译与 Metal Shader Converter

Bring your game to Mac, Part 2: Compile your shaders

2023年6月5日

在 Apple 官方观看视频

一句话判断

Metal Shader Converter 让你把 DXIL shader 一键转成 Metal IR,支持几何/曲面细分/计算/光追/mesh shader 全管线。

这场 Session 讲了什么

Varun Subramanian 介绍了 Metal 编译工具链的新成员——Metal Shader Converter。这是将高端游戏移植到 Mac 的三部曲第二部分。

Metal Shader Converter 的定位。它接受 DXIL(DirectX Intermediate Language)作为输入,输出 Metal IR。配合开源 DXC 编译器,你可以建立一条从 HLSL 到 Metal 的端到端 shader 管线。因为是在二进制层面做转换,速度非常快,能显著减少 shader 资产的构建时间。

支持的 shader 阶段。传统图形管线(包括曲面细分和几何 shader)、计算 shader、光线追踪相关阶段、amplification 和 mesh shader——几乎覆盖了现代游戏引擎用到的所有 shader 类型。

两种使用方式。命令行工具适合单个 shader 的快速转换,也可以写 shell 脚本批量处理;动态库版本提供纯 C 接口,可以在 macOS 和 Windows 上集成到自定义的构建管线中。

资源绑定模型。转换后的 shader 资源被映射到 Metal 的 Argument Buffer。提供两种布局模式:自动布局(资源依次排列,最简单)和显式布局(匹配 root signature,适合分离纹理/采样器表或 bindless 场景)。

值得深挖的点

几何和曲面细分 shader 到 Mesh Shader 的映射。Metal 是现代 API,提供了 viewport ID 和 amplification 等特性,理论上不需要老式的几何和曲面细分 shader。但很多游戏依赖这些管线实现草地、粒子等效果,手工转换成本很高。Shader Converter 自动把这类管线映射到 Metal 的 Mesh Shader,包括将固定功能的 tessellator 也做了相应处理。

Argument Buffer 的内存管理。顶级 Argument Buffer 是 CPU 和 GPU 共享的资源,写入时需要协调访问以避免竞态。不需要串行化 CPU 和 GPU 的工作,推荐使用 bump allocator:从一个大的 Metal buffer 中为每帧子分配不同资源,然后为每帧在途的数据做 shadow copy。

构建时 vs 运行时编译。在设备上生成 Metal IR 会增加编译开销,延迟 GPU 工作。理想的做法是在构建阶段就生成 Metal 库,然后随游戏分发。Shader Converter 让这一步变得简单。

代码片段

# 命令行转换 shader 的完整流程
# 第一步:用 DXC 编译 HLSL 到 DXIL
dxc -T ps_6_0 -E mainPS shader.hlsl -Fo shader.dxil

# 第二步:用 Metal Shader Converter 转换到 Metal 库
metal-shaderconverter shader.dxil -o shader.metallib
# 默认生成 Metal 库和 JSON 反射数据

# 也可以批量转换
for f in shaders/*.dxil; do
    metal-shaderconverter "$f" -o "output/$(basename "$f" .dxil).metallib"
done
// 运行时加载预编译的 Metal 库
id<MTLLibrary> library = [device newLibraryWithFile:@"shader.metallib" error:nil];
id<MTLFunction> vertexFunction = [library newFunctionWithName:@"vertexShader"];
id<MTLFunction> fragmentFunction = [library newFunctionWithName:@"fragmentShader"];

// 创建管线状态
MTLRenderPipelineDescriptor *desc = [[MTLRenderPipelineDescriptor alloc] init];
desc.vertexFunction = vertexFunction;
desc.fragmentFunction = fragmentFunction;
id<MTLRenderPipelineState> pipeline = [device newRenderPipelineStateWithDescriptor:desc error:nil];

最佳实践

  • 优先在构建阶段预编译 Metal 库,避免运行时编译开销。
  • 使用 bump allocator 管理 Argument Buffer 内存,避免 CPU/GPU 竞态。
  • 自动布局适合快速迁移,显式布局适合需要精细控制资源绑定的场景。
  • 动态库版本可以在 Windows 上运行,方便集成到现有的 Windows 构建管线。
  • 检查生成的 JSON 反射数据来理解资源映射关系。

还有什么值得关注

  • 三部曲的第一部分讲了如何评估和规划游戏移植
  • Metal 的 bindless 编程模式在去年有专门 Session 介绍
  • bump allocator 的示例代码可以从 Apple 开发者网站下载
  • 今年 Metal 新增了 visible function 链接到 mesh shader object 和 mesh 阶段的能力
WWDC 2023