跳到主要内容

管线状态缓存

在着色器程序的章节中曾经介绍过,由于编译着色器代码比较耗时且没必要每一帧都进行编译,因此基于着色器代码的字符串进行缓存。实际上,不仅是 wgpu::ShaderModule 需要缓存,还有一系列 WebGPU 对象需要进行缓存,以换取运行时的性能:

/**
* @brief Struct to hold the internal state of the Resource Cache
*
*/
struct ResourceCacheState {
std::unordered_map<std::size_t, wgpu::BindGroupLayout> bindGroupLayouts;
std::unordered_map<std::size_t, wgpu::PipelineLayout> pipelineLayouts;
std::unordered_map<std::size_t, wgpu::RenderPipeline> renderPipelines;
std::unordered_map<std::size_t, wgpu::BindGroup> bindGroups;

std::unordered_map<std::size_t, std::unique_ptr<ShaderProgram>> shaders;
};
note

其中缓存 wgpu::RenderPipeline 是最具必要性的,因为这个对象表示底层的图形 API 根据 wgpu::RenderPipelineDescriptor 中关联的配置(几乎涉及所有渲染管线的配置项), 生成了最优的 GPU 配置状态,在这一对象录制到渲染管线时,实际却只耗费一段内存拷贝的时间。

哈希

ShaderProgram 利用着色器代码的字符串进行缓存不同,其余四种类型都有对应的 Descriptor 结构体,需要使用对应的结构体进行构造:

wgpu::BindGroupLayout &requestBindGroupLayout(wgpu::BindGroupLayoutDescriptor &descriptor);

wgpu::PipelineLayout &requestPipelineLayout(wgpu::PipelineLayoutDescriptor &descriptor);

wgpu::RenderPipeline &requestRenderPipeline(wgpu::RenderPipelineDescriptor &descriptor);

wgpu::BindGroup &requestBindGroup(wgpu::BindGroupDescriptor &descriptor);

因此,缓存这些类型也需要基于这些结构体进行计算。好在这些结构体都是用来描述该对象的状态,因此很容易对基础变量计算哈希值,然后将这些哈希值合并起来:

/**
* @brief Helper function to combine a given hash
* with a generated hash for the input param.
*/
template<class T>
inline void hash_combine(size_t &seed, const T &v) {
std::hash<T> hasher;
size_t hash = hasher(v);
hash += 0x9e3779b9 + (seed << 6) + (seed >> 2);
seed ^= hash;
}

引擎通过特化 std::hash 模板函数为所有相关类型声明了计算哈希值的仿函数,例如:

template<>
struct hash<wgpu::RenderPipelineDescriptor> {
std::size_t operator()(const wgpu::RenderPipelineDescriptor &descriptor) const {
std::size_t result = 0;

hash_combine(result, descriptor.layout.Get()); // internal address
hash_combine(result, descriptor.primitive);
hash_combine(result, descriptor.multisample);
if (descriptor.depthStencil) {
hash_combine(result, *descriptor.depthStencil);
}
hash_combine(result, descriptor.vertex);
if (descriptor.fragment) {
hash_combine(result, *descriptor.fragment);
}

return result;
}
};

template<>
struct hash<wgpu::PrimitiveState> {
std::size_t operator()(const wgpu::PrimitiveState &state) const {
std::size_t result = 0;

hash_combine(result, state.topology);
hash_combine(result, state.frontFace);
hash_combine(result, state.cullMode);
hash_combine(result, state.stripIndexFormat);

return result;
}
};
tip

结构体中有些是数值和枚举,有些确是指针,并且这些对象一般都是由 wgpu::Device 创建的。 为了将这些信息纳入到哈希计算中,于是直接使用对象的内存地址,例如:

template<>
struct hash<wgpu::VertexState> {
std::size_t operator()(const wgpu::VertexState &state) const {
std::size_t result = 0;

hash_combine(result, state.module.Get()); // internal address
hash_combine(result, state.entryPoint);
hash_combine(result, state.bufferCount);
hash_combine(result, state.buffers); // internal address

return result;
}
};

从需要缓存的对象所使用的描述结构体出发,将相关的类型全部定义哈希函数,就可以最终使用std::hash 模板函数计算对应的哈希值:

wgpu::RenderPipeline &ResourceCache::requestRenderPipeline(wgpu::RenderPipelineDescriptor &descriptor) {
std::hash<wgpu::RenderPipelineDescriptor> hasher;
size_t hash = hasher(descriptor);

auto iter = _state.renderPipelines.find(hash);
if (iter == _state.renderPipelines.end()) {
_state.renderPipelines[hash] = _device.CreateRenderPipeline(&descriptor);
return _state.renderPipelines[hash];
} else {
return iter->second;
}
}

Arche.js 中的实践

在浏览器中,Arche.js 并没有实现上述的缓存机制,因为我发现对于像 wgpu::RenderPipeline 这样比较耗时的对象,浏览器中的构建时间几乎可以忽略。 虽然目前我还没有找到相关信息证明 WebGPU 在浏览器侧内部就是实现了类似的缓存机制,但由于这种缓存机制在现代图形API中是普遍的一种操作,因此我相信内部或许已经实现了类似的对象池。