Graphics & Games 高级 22m
认识 Metal 函数指针
Get to know Metal function pointers
2020年6月24日
一句话判断
Metal 函数指针让你的 Compute Shader 可以在运行时动态选择调用哪个函数——这对构建可扩展的渲染管线(如材质系统、光线追踪的 Hit Shader)意义重大。
这场 Session 讲了什么
Metal 函数指针是 2020 年引入的重要特性,它允许 GPU 代码在运行时通过函数表(Function Table)动态调用不同的函数,而不是在编译时硬编码调用关系。
Session 从传统的渲染管线讲起:过去每种材质需要编译一个单独的 Shader 变体,随着材质种类增多,编译时间和二进制体积都会膨胀。函数指针提供了一种替代方案——你可以在一个通用的 Shader 中,通过函数指针动态调用不同的材质函数。
核心 API 包括:MTFunctionHandle(表示一个可以在 GPU 端调用的函数)、MTFunctionTable(函数表,用于在 GPU 端通过索引查找和调用函数)、以及在 Metal Shading Language 中使用 call 语法进行动态调用。
值得深挖的点
从 Shader Variant 爆炸到函数指针。 传统做法中,每种材质+光照组合都需要一个编译后的 Shader Variant。100 种材质 x 3 种光照 = 300 个 Variant。函数指针让你只编译一个通用 Shader + 100 个材质函数 + 3 个光照函数,编译时间和运行时内存都大幅减少。
函数指针与光线追踪的天然搭配。 光线追踪中,不同几何体命中后需要不同的着色逻辑。通过 Intersection Function Table,你可以为每个几何体绑定不同的 Hit Shader,这正是函数指针的用武之地。
代码片段
import Metal
// 创建函数表
let functionTable = device.makeFunctionTable(
descriptor: MTLFunctionTableDescriptor()
)!
// 从 Metal Library 获取函数句柄
let library = device.makeDefaultLibrary(bundle: .main)!
let functionDescriptor = MTLFunctionDescriptor()
functionDescriptor.name = "materialGold"
let functionHandle = library.makeFunction(
descriptor: functionDescriptor,
completion: { handle, error in
if let handle = handle {
// 将函数句柄添加到函数表中
functionTable.addFunction(handle)
}
}
)
// Metal Shading Language 中的函数指针使用
// 声明函数表
constant function_table<void(float3, float3)> materialTable [[function_table(0)]];
// 在 Compute Shader 中通过函数指针动态调用
kernel void renderKernel(
uint2 tid [[thread_position_in_grid]]
) {
float3 normal = getNormal(tid);
float3 position = getPosition(tid);
// 根据材质索引动态选择调用哪个函数
uint materialIndex = getMaterialIndex(tid);
materialTable[materialIndex](normal, position);
}
// 在渲染时绑定函数表
let computeEncoder = commandBuffer.makeComputeCommandEncoder()!
computeEncoder.setComputePipelineState(pipeline)
computeEncoder.setFunctionTable(functionTable, bufferIndex: 0)
computeEncoder.dispatchThreads(threadCount,
threadsPerThreadgroup: groupSize)
computeEncoder.endEncoding()
最佳实践
- 用函数指针替代庞大的 Shader Variant 集合。 当你的 Variant 数量超过几十个时,函数指针方案在编译时间和内存占用上都有明显优势。
- 合理划分函数粒度。 函数指针调用的函数应该足够独立(如材质函数只负责 BRDF 计算),避免函数间有过多的数据依赖。
- 函数表在渲染帧之间复用。 创建和填充函数表有开销,只要材质列表不变就可以跨帧复用。
- 注意函数调用的性能开销。 GPU 端的动态调用比静态调用慢,但相比编译和内存开销的节省通常是值得的。
还有什么值得关注
- 函数指针可以和 Ray Tracing 的 Intersection Function Table 配合使用
- 动态库(Dynamic Library)和函数指针可以组合,实现运行时加载新的 Shader
- 函数指针支持返回值和参数,调用方式与普通函数类似
- 这个特性需要 A14 / M1 及以上的 GPU 支持
WWDC 2020