Vulkan利用SpirV-Cross提取Shader反射信息来自动构建MeshDrawCommand

Image

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工程中。

Image

这个库使用起来非常的简单,我们只需准备好编译后的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需要设置的内容大大减少了。

Image

© - 2024 · 月光下的旅行。 禁止转载