使用用SpirV-Cross提取反射信息自动构建DrawCommand
Vulkan准备一次MeshDrawCommand非常麻烦,要写很多代码,这时候一种可自适应性的、自动填充pipeline和vao的Mesh Draw框架就很有必要了。
Mesh Draw Command的生成有两种方向,一种是C++端硬编码设计好渲染管线,然后再依据C++的输入内容编写Shader;第二种是先写好Shader,然后再在C++端根据Shader信息生成对应的Descriptor、Pipeline等信息。
UE4是第一种做法,所以写起自定义Shader很痛苦,Unity、bgfx则是后者,并且它们都自己设计了一套Shader语法,增加了Shader语言的表达能力,非常的Nice。
目前我是用GLSL编译到SPV的方式加载Shader,并没有自己实现了一套语法解析器,但可以利用SpirV-Cross库提取Spv中的反射信息,在C++端再自动动态生成Descriptors,减轻Mesh Draw Command的生成压力。
SpirV-Cross虽然随Vulkan SDK附带了Lib,但基本都不会用SDK里的,因为经常会因为链接时Lib版本信息对不上导致链接失败。
正确的做法是在Github上下载最新的SpirV-Cross库,编译对应的Debug Lib和Release Lib,然后再放入自己的Vulkan工程中。
这个库使用起来非常的简单,我们只需准备好编译后的shader code (uin8_t或者char),就可以构建一个SpirV-Cross的compiler,然后很简单就可以得到shader resources了:
#pragma warning(push)
#pragma warning(disable:4099)
#include "spirv_cross/spirv_cross.hpp"
#pragma warning(pop)
spirv_cross::Compiler compiler((uint32_t*)shader_module->shader_code.data(),shader_module->shader_code.size()/sizeof(uint32_t));
spirv_cross::ShaderResources resources = compiler.get_shader_resources();
ShaderResources结构存储了shader引用的所有资源,具体结构如下:
struct ShaderResources
{
SmallVector<Resource> uniform_buffers;
SmallVector<Resource> storage_buffers;
SmallVector<Resource> stage_inputs;
SmallVector<Resource> stage_outputs;
SmallVector<Resource> subpass_inputs;
SmallVector<Resource> storage_images;
SmallVector<Resource> sampled_images;
SmallVector<Resource> atomic_counters;
SmallVector<Resource> acceleration_structures;
// There can only be one push constant block,
// but keep the vector in case this restriction is lifted in the future.
SmallVector<Resource> push_constant_buffers;
// For Vulkan GLSL and HLSL source,
// these correspond to separate texture2D and samplers respectively.
SmallVector<Resource> separate_images;
SmallVector<Resource> separate_samplers;
};
我们要做的,就是遍历ShaderResources结构里的每种GPU Resource,非常简单就可得到对应的VkDescriptorSetLayoutBinding,并用自定义容器将它们存起来:
for(int32_t i = 0; i<resources.uniform_buffers.size(); ++i)
{
spirv_cross::Resource& res = resources.uniform_buffers[i];
spirv_cross::SPIRType type = compiler.get_type(res.type_id);
spirv_cross::SPIRType base_type = compiler.get_type(res.base_type_id);
const std::string& varName = compiler.get_name(res.id);
const std::string& typeName = compiler.get_name(res.base_type_id);
uint32_t uniformBufferStructSize = (uint32_t)compiler.get_declared_struct_size(type);
int32_t set = compiler.get_decoration(res.id,spv::DecorationDescriptorSet);
int32_t binding = compiler.get_decoration(res.id,spv::DecorationBinding);
VkDescriptorSetLayoutBinding setLayoutBinding = {};
setLayoutBinding.binding = binding;
setLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
setLayoutBinding.descriptorCount = 1;
setLayoutBinding.stageFlags = stageFlags;
setLayoutBinding.pImmutableSamplers = nullptr;
set_layouts_info.add_descriptor_set_layout_binding(varName,set,setLayoutBinding);
auto it = buffer_params.find(varName);
if(it==buffer_params.end())
{
buffer_info bufferInfo = {};
bufferInfo.set = set;
bufferInfo.binding = binding;
bufferInfo.buffer_size = uniformBufferStructSize;
bufferInfo.stage_flags = stageFlags;
bufferInfo.descriptor_type = setLayoutBinding.descriptorType;
buffer_params.insert(std::make_pair(varName,bufferInfo));
}
else
{
it->second.stage_flags |= setLayoutBinding.stageFlags;
}
}
for(int32_t i = 0; i < resources.subpass_inputs.size(); ++i)
{
spirv_cross::Resource& res = resources.subpass_inputs[i];
spirv_cross::SPIRType type = compiler.get_type(res.type_id);
spirv_cross::SPIRType base_type = compiler.get_type(res.base_type_id);
const std::string& varName = compiler.get_name(res.id);
int32_t set = compiler.get_decoration(res.id,spv::DecorationDescriptorSet);
int32_t binding = compiler.get_decoration(res.id,spv::DecorationBinding);
VkDescriptorSetLayoutBinding setLayoutBinding = {};
setLayoutBinding.binding = binding;
setLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
setLayoutBinding.descriptorCount = 1;
setLayoutBinding.stageFlags = stageFlags;
setLayoutBinding.pImmutableSamplers = nullptr;
set_layouts_info.add_descriptor_set_layout_binding(varName,set,setLayoutBinding);
auto it = image_params.find(varName);
if( it == image_params.end())
{
image_info imageInfo = {};
imageInfo.set = set;
imageInfo.binding = binding;
imageInfo.stage_flags = stageFlags;
imageInfo.descriptor_type = setLayoutBinding.descriptorType;
image_params.insert(std::make_pair(varName,imageInfo));
}
else
{
it->second.stage_flags |= stageFlags;
}
}
//... 其它同理
然后是descriptor的申请,在shader的VkDescriptorSetLayoutBinding都准备好后,创建descriptor异常方便:
std::shared_ptr<vk_descriptor_set> vk_shader_mix::create_descriptor_set()
{
if(set_layouts_info.set_layouts.size() == 0 )
{
return nullptr;
}
auto sets = std::make_shared<vk_descriptor_set>(device);
sets->set_layouts_info = set_layouts_info;
sets->descriptor_sets.resize(set_layouts_info.set_layouts.size());
for(int32_t i = (int32_t)descriptor_set_pools.size() - 1; i>=0; --i)
{
if(descriptor_set_pools[i]->allocate_descriptor_set(sets->descriptor_sets.data()))
{
return sets;
}
}
auto set_pool = std::make_shared<vk_descriptor_set_pool>(device,64,set_layouts_info,shader_descriptor_set_layouts);
descriptor_set_pools.push_back(set_pool);
set_pool->allocate_descriptor_set(sets->descriptor_sets.data());
return sets;
}
此时,与shader绑定的material只需设置好每一个descriptor_set即可。
material是与mesh绑定的,而mesh是与vertex input attribute绑定的。
首先是mesh的内存管理,专门使用一个全局的mesh_manager类来管理所有的网格加载:
class meshes_manager
{
public:
meshes_manager() { };
~meshes_manager() { };
void initialize(vk_device* indevice,VkCommandPool inpool);
// 释放加载到内存中的网格数据
void release_cpu_mesh_data();
void release()
{
sponza_mesh.reset();
}
public:
std::shared_ptr<mesh> sponza_mesh;
private:
vk_device* device;
VkCommandPool pool;
};
extern meshes_manager g_meshes_manager;
// cpp
void meshes_manager::initialize(vk_device* indevice,VkCommandPool inpool)
{
pool = inpool;
device = indevice;
// sponza 网格加载到内存中
sponza_mesh = std::make_shared<mesh>(device,pool);
sponza_mesh->load_obj_mesh("data/model/sponza/sponza.obj","");
}
// 释放加载到cpu中的网格数据
void meshes_manager::release_cpu_mesh_data()
{
sponza_mesh->raw_data.release_cpu_data();
}
这里为了方便理解我硬编码了一个sponza_mesh变量,真正使用需要用vector配合scene actor的mesh component做到随意加载收集。
mesh的使用分离renderpass与material的设计,尽可能保证两者解耦:
// 按材质划分Mesh
class mesh
{
public:
mesh(vk_device* indevice,
VkCommandPool pool):
device(indevice),
pool(pool)
{
}
~mesh(){ }
std::array<std::shared_ptr<vk_vertex_buffer>,renderpass_type::max_index> vertex_bufs = { };
std::array<bool,renderpass_type::max_index> has_registered = { };
std::vector<sub_mesh> sub_meshes;
void draw(std::shared_ptr<vk_command_buffer> cmd_buf,int32_t pass_type);
// 在此处存储的所有顶点
vertex_raw_data raw_data = {};
// 注册render pass 对应的 mesh
void register_renderpass(
std::shared_ptr<vk_renderpass> pass,
std::shared_ptr<vk_shader_mix> shader,
bool reload_vertex_buf = true
);
private:
vk_device* device;
VkCommandPool pool;
void load_obj_mesh(
std::string mesh_path,
std::string mat_path
);
friend class meshes_manager;
};
这里的submesh是按照材质划分的,代表一次drawcall,并且raw_data代表了内存中的顶点数据,我不会立即释放它,因为一个网格可能需要被多个renderpass使用,而不同的renderpass的shader vertex input binding可能不一样,因此,应该保留该内存直到meshes_manager明确调用release_cpu_mesh_data再释放。
每个mesh都有一个register_renderpass函数,只有真正调用该注册函数,has_registered会变化为true,这时才会在绘制时收集它,否则过滤掉:
namespace renderpass_type
{
constexpr auto texture_pass = 0;
constexpr auto gbuffer_pass = 1;
//...
constexpr auto max_index = 2;
}
// 对每种 renderpass 都应该注册对应的 shader material
void sub_mesh::register_renderpass(
int32_t passtype,
vk_device* indevice,
VkRenderPass in_renderpass,
VkCommandPool in_pool
){
ASSERT(passtype < renderpass_type::max_index,"render pass type越界。");
// 所有的模型矩阵的暂时用默认模型矩阵
model = glm::rotate(glm::mat4(1.0f),glm::radians(0.0f),glm::vec3(-1.0f,0.0f,0.0f));
// 对于每种renderpass,需要特殊设置它们的descriptor创建
if(passtype == renderpass_type::texture_pass)
{
mat_map[passtype] = material_texture::create(
indevice,
in_renderpass,
in_pool,
texture_ids[texture_id_type::diffuse],
model
);
}
else if(passtype == renderpass_type::gbuffer_pass)
{
mat_map[passtype] = material_gbuffer::create(
indevice,
in_renderpass,
in_pool,
texture_ids,
model
);
}
has_registered[passtype] = true;
}
void mesh::register_renderpass(std::shared_ptr<vk_renderpass> pass,std::shared_ptr<vk_shader_mix> shader,bool reload_vertex_buf)
{
for(auto& submesh:sub_meshes)
{
submesh.register_renderpass(pass->type,device,pass->render_pass,pool);
}
if(reload_vertex_buf)
{
// 上传render pass对应的顶点buffer
vertex_bufs[pass->type] = vk_vertex_buffer::create(
device,
pool,
// 根据input attribute layout生成对应的顶点内存布局。
raw_data.pack_type_stream(shader->per_vertex_attributes),
shader->per_vertex_attributes
);
}
has_registered[pass->type] = true;
}
对于mesh draw函数,需要显式指定当前绘制位于哪个renderpass,做好binding,然后调用drawcall即可:
void sub_mesh::draw(std::shared_ptr<vk_command_buffer> cmd_buf,int32_t passtype)
{
ASSERT(passtype<renderpass_type::max_index,"render pass type越界。");
mat_map[passtype]->pipeline->bind(*cmd_buf);
vkCmdBindDescriptorSets(
*cmd_buf,
VK_PIPELINE_BIND_POINT_GRAPHICS,
mat_map[passtype]->pipeline->layout,
0,
1,
mat_map[passtype]->descriptor_set->descriptor_sets.data(),
0,
nullptr
);
index_buf->bind_and_draw(*cmd_buf);
}
void mesh::draw(std::shared_ptr<vk_command_buffer> cmd_buf,int32_t pass_type)
{
ASSERT(pass_type < renderpass_type::max_index,"render pass type越界。");
// 绑定所有的顶点缓冲
vertex_bufs[pass_type]->bind(*cmd_buf);
for (auto& submesh : sub_meshes)
{
submesh.draw(cmd_buf,pass_type);
}
}
此时,Meshdraw Command设计初具雏形,调用方法如下:
void pbr_deferred::initialize_special()
{
vk_renderpass_mix_data mixdata(&device,&swapchain);
pass_texture = texture_pass::create(mixdata);
pass_gbuffer = gbuffer_pass::create(mixdata,graphics_command_pool);
g_meshes_manager.sponza_mesh->register_renderpass(pass_texture,g_shader_manager.texture_map_shader);
g_meshes_manager.sponza_mesh->register_renderpass(pass_gbuffer,g_shader_manager.gbuffer_shader);
record_renderCommand();
}
void pbr_deferred::record_renderCommand()
{
pass_gbuffer->cmd_buf->begin(VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT);
{
pass_gbuffer->begin(*pass_gbuffer->cmd_buf);
{
g_meshes_manager.sponza_mesh->draw(pass_gbuffer->cmd_buf,renderpass_type::gbuffer_pass);
}
pass_gbuffer->end(*pass_gbuffer->cmd_buf);
}
pass_gbuffer->cmd_buf->end();
for (size_t i = 0; i < graphics_command_buffers.size(); i++)
{
pass_gbuffer->begin(*pass_gbuffer->cmd_buf,i);
{
g_meshes_manager.sponza_mesh->draw(pass_gbuffer->cmd_buf,renderpass_type::texture_pass);
}
pass_gbuffer->end(*pass_gbuffer->cmd_buf,i);
}
}
这样,vulkan绘制Mesh需要设置的内容大大减少了。