使用 Metal 3 实现无绑定渲染
Go bindless with Metal 3
2022年6月6日
一句话判断
Metal 3 的 bindless 模型让 Argument Encoder 成为可选——直接写 C 结构体就能管理 GPU 资源,这是图形程序员今年最值得关注的 API 简化。
这场 Session 讲了什么
Metal 3 对 bindless 渲染模型做了四项关键简化:
无需 Argument Encoder:过去写 Argument Buffer 需要创建 Argument Encoder 对象来编码资源引用。Metal 3 允许你直接像写 C 结构体一样写入 Argument Buffer——用 GPU 地址和 Resource ID 直接填充结构体成员。
无界数组简化:Metal 3 把无界数组的编码从 Argument Encoder 操作简化为普通的数组遍历和填充。数组大小完全由 CPU 端控制,shader 不需要声明固定大小。
从 Heap 分配加速结构:现在可以从 Metal Heap 分配 ray tracing 的加速结构,改善内存管理和性能。
Shader Validation Layer 增强:新增资源驻留检查,当资源不在 GPU 内存中时会发出警告。
值得深挖的点
直接写 Argument Buffer 的范式转变是这次更新最核心的变化。传统流程是:创建 Encoder -> 设置目标 Buffer -> 调用 Encoder 方法写入资源。新流程是:分配 Buffer -> 获取指针 -> 转型为结构体 -> 直接赋值。代码量减半,而且直观程度完全不同。
CPU 和 GPU 结构体的一致性保证让这一切成为可能。Metal 保证 C 侧的结构体布局和 Metal Shading Language 侧的结构体布局完全一致。你可以用 __METAL_VERSION__ 宏在共享头文件中统一声明,shader 编译时用 Metal 类型,CPU 编译时用 C 类型。
无界数组的灵活性真正体现了 bindless 的设计哲学。CPU 端只需要 malloc(sizeof(MeshStruct) * meshCount),然后遍历填充。Shader 端声明一个指针参数就可以自由索引,不需要提前告诉编译器数组有多大。这让动态场景管理(加载/卸载模型)变得非常自然。
代码片段
Metal 3 直接写 Argument Buffer:
// CPU 侧结构体声明
typedef struct {
uint64_t normalsGPUAddress; // GPU 地址,64位
uint64_t positionsGPUAddress;
MTLResourceID albedoTexture; // 纹理资源 ID
uint32_t meshIndex;
} MeshArgumentBuffer;
// 分配并填充
size_t bufferSize = sizeof(MeshArgumentBuffer) * meshCount;
id<MTLBuffer> argBuffer = [device newBufferWithLength:bufferSize
options:MTLResourceStorageModeShared];
MeshArgumentBuffer *meshes = (MeshArgumentBuffer *)argBuffer.contents;
for (int i = 0; i < meshCount; i++) {
meshes[i].normalsGPUAddress = normalBuffer.gpuAddress;
meshes[i].positionsGPUAddress = positionBuffer.gpuAddress;
meshes[i].albedoTexture = texture.gpuResourceID;
meshes[i].meshIndex = i;
}
GPU 侧 Shader 声明:
// Metal Shading Language 侧
struct MeshArgumentBuffer {
device float *normals;
device float *positions;
texture2d<float> albedoTexture;
uint meshIndex;
};
// 无界数组——大小不需要在编译时确定
kernel void renderShader(
device MeshArgumentBuffer *meshes [[buffer(0)]],
uint meshIndex [[thread_position_in_threadgroup]]
) {
// 自由索引,无固定大小约束
device auto &mesh = meshes[meshIndex];
// 访问法线、位置、纹理...
}
共享头文件声明(CPU/GPU 统一):
// 共享头文件
#ifdef __METAL_VERSION__
// Metal shader 编译时
#define GPU_ADDRESS device
#define TEXTURE_TYPE texture2d<float>
#else
// C 编译时
#define GPU_ADDRESS uint64_t
#define TEXTURE_TYPE MTLResourceID
#endif
typedef struct {
GPU_ADDRESS normals;
GPU_ADDRESS positions;
TEXTURE_TYPE albedoTexture;
} SharedMeshStruct;
最佳实践
- 用共享头文件避免结构体不一致:
__METAL_VERSION__宏让你在一个文件中维护 CPU 和 GPU 两侧的声明 - Argument Buffer Tier 2 是前提:2016 年以后的 Mac 或 A13 以后的 iOS 设备支持
- 无界数组大小在 CPU 端控制:
sizeof(Struct) * count计算大小,shader 端不需要知道 - Heap 分配加速结构优化内存:从 Heap 分配可以改善内存局部性和碎片问题
- 用 Shader Validation Layer 检查资源驻留:开发阶段打开验证,发布前关闭
还有什么值得关注
- Bindless 模型与 Metal Heaps 配合可以显著降低 CPU 端压力
- Argument Encoder 仍然可用,Metal 3 只是提供了更简洁的替代方案
- Session 建议搭配去年的 bindless 渲染 Session 一起看
- 混合渲染(光栅化 + 光线追踪)是 bindless 模型的典型应用场景