Get to know Metal function pointers
Graphics & Games 高级 22m

认识 Metal 函数指针

Get to know Metal function pointers

2020年6月24日

在 Apple 官方观看视频

一句话判断

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